Files
confident-ai--deepeval/deepeval/models/_summac_model.py
T
2026-07-13 13:32:05 +08:00

593 lines
19 KiB
Python

# mypy: check_untyped_defs = False
###############################################
# Source: https://github.com/tingofurro/summac
###############################################
import nltk
import os
import json
import torch
from deepeval import utils as utils_misc
model_map = {
"snli-base": {
"model_card": "boychaboy/SNLI_roberta-base",
"entailment_idx": 0,
"contradiction_idx": 2,
},
"snli-large": {
"model_card": "boychaboy/SNLI_roberta-large",
"entailment_idx": 0,
"contradiction_idx": 2,
},
"mnli-base": {
"model_card": "microsoft/deberta-base-mnli",
"entailment_idx": 2,
"contradiction_idx": 0,
},
"mnli": {
"model_card": "roberta-large-mnli",
"entailment_idx": 2,
"contradiction_idx": 0,
},
"anli": {
"model_card": "ynie/roberta-large-snli_mnli_fever_anli_R1_R2_R3-nli",
"entailment_idx": 0,
"contradiction_idx": 2,
},
"vitc-base": {
"model_card": "tals/albert-base-vitaminc-mnli",
"entailment_idx": 0,
"contradiction_idx": 1,
},
"vitc": {
"model_card": "tals/albert-xlarge-vitaminc-mnli",
"entailment_idx": 0,
"contradiction_idx": 1,
},
"vitc-only": {
"model_card": "tals/albert-xlarge-vitaminc",
"entailment_idx": 0,
"contradiction_idx": 1,
},
}
def card_to_name(card):
card2name = {v["model_card"]: k for k, v in model_map.items()}
if card in card2name:
return card2name[card]
return card
def name_to_card(name):
if name in model_map:
return model_map[name]["model_card"]
return name
def get_neutral_idx(ent_idx, con_idx):
return list(set([0, 1, 2]) - set([ent_idx, con_idx]))[0]
class _SummaCImager:
def __init__(
self,
model_name="mnli",
granularity="paragraph",
use_cache=True,
max_doc_sents=100,
device="cuda",
**kwargs
):
self.grans = granularity.split("-")
assert (
all(
gran in ["paragraph", "sentence", "document", "2sents", "mixed"]
for gran in self.grans
)
and len(self.grans) <= 2
), "Unrecognized `granularity` %s" % (granularity)
assert (
model_name in model_map.keys()
), "Unrecognized model name: `%s`" % (model_name)
self.model_name = model_name
if model_name != "decomp":
self.model_card = name_to_card(model_name)
self.entailment_idx = model_map[model_name]["entailment_idx"]
self.contradiction_idx = model_map[model_name]["contradiction_idx"]
self.neutral_idx = get_neutral_idx(
self.entailment_idx, self.contradiction_idx
)
self.granularity = granularity
self.use_cache = use_cache
self.cache_folder = "/export/share/plaban/summac_cache/"
self.max_doc_sents = max_doc_sents
self.max_input_length = 500
self.device = device
self.cache = {}
self.model = None # Lazy loader
def load_nli(self):
if self.model_name == "decomp":
try:
from allennlp.predictors.predictor import Predictor
except ModuleNotFoundError:
print(
"allennlp library is not installed. "
"Please install the library by following the instruction from their documentation:"
"https://docs.allennlp.org/main/"
)
self.model = Predictor.from_path(
"https://storage.googleapis.com/allennlp-public-models/decomposable-attention-elmo-2020.04.09.tar.gz",
cuda_device=0,
)
else:
try:
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
)
except ModuleNotFoundError:
print(
"transformers library is not installed. Run 'pip install transformers'"
)
self.tokenizer = AutoTokenizer.from_pretrained(self.model_card)
self.model = AutoModelForSequenceClassification.from_pretrained(
self.model_card
).eval()
self.model.to(self.device)
def split_sentences(self, text):
sentences = nltk.tokenize.sent_tokenize(text)
sentences = [sent for sent in sentences if len(sent) > 10]
return sentences
def split_2sents(self, text):
sentences = nltk.tokenize.sent_tokenize(text)
sentences = [sent for sent in sentences if len(sent) > 10]
two_sents = [
" ".join(sentences[i : (i + 2)]) for i in range(len(sentences))
]
return two_sents
def split_paragraphs(self, text):
if text.count("\n\n") > 0:
paragraphs = [p.strip() for p in text.split("\n\n")]
else:
paragraphs = [p.strip() for p in text.split("\n")]
return [p for p in paragraphs if len(p) > 10]
def split_text(self, text, granularity="sentence"):
if granularity == "document":
return [text]
elif granularity == "paragraph":
return self.split_paragraphs(text)
elif granularity == "sentence":
return self.split_sentences(text)
elif granularity == "2sents":
return self.split_2sents(text)
elif granularity == "mixed":
return self.split_sentences(text) + self.split_paragraphs(text)
def build_image(self, original, generated):
import numpy as np
cache_key = (original, generated)
if self.use_cache and cache_key in self.cache:
cached_image = self.cache[cache_key]
cached_image = cached_image[:, : self.max_doc_sents, :]
return cached_image
if len(self.grans) == 1:
gran_doc, gran_sum = self.grans[0], self.grans[0]
else:
gran_doc, gran_sum = self.grans[0], self.grans[1]
original_chunks = self.split_text(original, granularity=gran_doc)[
: self.max_doc_sents
]
generated_chunks = self.split_text(generated, granularity=gran_sum)
N_ori = len(original_chunks)
N_gen = len(generated_chunks)
if N_ori == 0 or N_gen == 0:
return np.zeros((3, 1, 1))
# assert (N_ori > 0 and N_gen > 0), "One of the inputs has no chunks"
image = np.zeros((3, N_ori, N_gen))
if self.model is None:
self.load_nli()
dataset = [
{
"premise": original_chunks[i],
"hypothesis": generated_chunks[j],
"doc_i": i,
"gen_i": j,
}
for i in range(N_ori)
for j in range(N_gen)
]
for batch in utils_misc.batcher(dataset, batch_size=20):
if self.model_name == "decomp":
batch_evids, batch_conts, batch_neuts = [], [], []
batch_json = [
{"premise": d["premise"], "hypothesis": d["hypothesis"]}
for d in batch
]
model_outs = self.model.predict_batch_json(batch_json)
for out in model_outs:
probs = out["label_probs"]
batch_evids.append(probs[0])
batch_conts.append(probs[1])
batch_neuts.append(probs[2])
else:
batch_prems = [b["premise"] for b in batch]
batch_hypos = [b["hypothesis"] for b in batch]
batch_tokens = self.tokenizer.batch_encode_plus(
list(zip(batch_prems, batch_hypos)),
padding=True,
truncation=True,
max_length=self.max_input_length,
return_tensors="pt",
truncation_strategy="only_first",
)
batch_tokens = {
k: v.to(self.device) for k, v in batch_tokens.items()
}
with torch.no_grad():
model_outputs = self.model(**batch_tokens)
batch_probs = torch.nn.functional.softmax(
model_outputs["logits"], dim=-1
)
batch_evids = batch_probs[:, self.entailment_idx].tolist()
batch_conts = batch_probs[:, self.contradiction_idx].tolist()
batch_neuts = batch_probs[:, self.neutral_idx].tolist()
for b, evid, cont, neut in zip(
batch, batch_evids, batch_conts, batch_neuts
):
image[0, b["doc_i"], b["gen_i"]] = evid
image[1, b["doc_i"], b["gen_i"]] = cont
image[2, b["doc_i"], b["gen_i"]] = neut
if self.use_cache:
self.cache[cache_key] = image
return image
def get_cache_file(self):
return os.path.join(
self.cache_folder,
"cache_%s_%s.json" % (self.model_name, self.granularity),
)
def save_cache(self):
cache_cp = {"[///]".join(k): v.tolist() for k, v in self.cache.items()}
with open(self.get_cache_file(), "w") as f:
json.dump(cache_cp, f)
def load_cache(self):
import numpy as np
cache_file = self.get_cache_file()
if os.path.isfile(cache_file):
with open(cache_file, "r") as f:
cache_cp = json.load(f)
self.cache = {
tuple(k.split("[///]")): np.array(v)
for k, v in cache_cp.items()
}
class _SummaCConv(torch.nn.Module):
def __init__(
self,
models=["mnli", "anli", "vitc"],
bins="even50",
granularity="sentence",
nli_labels="e",
device="cuda",
start_file=None,
imager_load_cache=True,
agg="mean",
norm_histo=False,
**kwargs
):
import numpy as np
# `bins` should be `even%d` or `percentiles`
assert nli_labels in [
"e",
"c",
"n",
"ec",
"en",
"cn",
"ecn",
], "Unrecognized nli_labels argument %s" % (nli_labels)
super(SummaCConv, self).__init__()
self.device = device
self.models = models
self.imagers = []
for model_name in models:
self.imagers.append(
SummaCImager(
model_name=model_name, granularity=granularity, **kwargs
)
)
if imager_load_cache:
for imager in self.imagers:
imager.load_cache()
assert len(self.imagers) > 0, "Imager names were empty or unrecognized"
if "even" in bins:
n_bins = int(bins.replace("even", ""))
self.bins = list(np.arange(0, 1, 1 / n_bins)) + [1.0]
elif bins == "percentile":
self.bins = [
0.0,
0.01,
0.02,
0.03,
0.04,
0.07,
0.13,
0.37,
0.90,
0.91,
0.92,
0.93,
0.94,
0.95,
0.955,
0.96,
0.965,
0.97,
0.975,
0.98,
0.985,
0.99,
0.995,
1.0,
]
self.nli_labels = nli_labels
self.n_bins = len(self.bins) - 1
self.norm_histo = norm_histo
self.n_rows = 10
self.n_labels = 2
self.n_depth = len(self.imagers) * len(self.nli_labels)
self.full_size = self.n_depth * self.n_bins
if self.norm_histo:
self.full_size += (
2 # Will explicitly give the count of originals and generateds
)
self.agg = agg
self.mlp = torch.nn.Linear(self.full_size, 1).to(device)
self.layer_final = torch.nn.Linear(3, self.n_labels).to(device)
if start_file is not None:
print(self.load_state_dict(torch.load(start_file)))
def build_image(self, original, generated):
import numpy as np
images = [
imager.build_image(original, generated) for imager in self.imagers
]
image = np.concatenate(images, axis=0)
return image
def compute_histogram(self, original=None, generated=None, image=None):
import numpy as np
# Takes the two texts, and generates a (n_rows, 2*n_bins)
if image is None:
image = self.build_image(original, generated)
N_depth, N_ori, N_gen = image.shape
full_histogram = []
for i_gen in range(N_gen):
histos = []
for i_depth in range(N_depth):
if (
(i_depth % 3 == 0 and "e" in self.nli_labels)
or (i_depth % 3 == 1 and "c" in self.nli_labels)
or (i_depth % 3 == 2 and "n" in self.nli_labels)
):
histo, X = np.histogram(
image[i_depth, :, i_gen],
range=(0, 1),
bins=self.bins,
density=self.norm_histo,
)
histos.append(histo)
if self.norm_histo:
histos = [[N_ori, N_gen]] + histos
histogram_row = np.concatenate(histos)
full_histogram.append(histogram_row)
n_rows_missing = self.n_rows - len(full_histogram)
full_histogram += [[0.0] * self.full_size] * n_rows_missing
full_histogram = full_histogram[: self.n_rows]
full_histogram = np.array(full_histogram)
return image, full_histogram
def forward(self, originals, generateds, images=None):
if images is not None:
# In case they've been pre-computed.
histograms = []
for image in images:
_, histogram = self.compute_histogram(image=image)
histograms.append(histogram)
else:
images, histograms = [], []
for original, generated in zip(originals, generateds):
image, histogram = self.compute_histogram(
original=original, generated=generated
)
images.append(image)
histograms.append(histogram)
N = len(histograms)
histograms = torch.FloatTensor(histograms).to(self.device)
non_zeros = (torch.sum(histograms, dim=-1) != 0.0).long()
seq_lengths = non_zeros.sum(dim=-1).tolist()
mlp_outs = self.mlp(histograms).reshape(N, self.n_rows)
features = []
for mlp_out, seq_length in zip(mlp_outs, seq_lengths):
if seq_length > 0:
Rs = mlp_out[:seq_length]
if self.agg == "mean":
features.append(
torch.cat(
[
torch.mean(Rs).unsqueeze(0),
torch.mean(Rs).unsqueeze(0),
torch.mean(Rs).unsqueeze(0),
]
).unsqueeze(0)
)
elif self.agg == "min":
features.append(
torch.cat(
[
torch.min(Rs).unsqueeze(0),
torch.min(Rs).unsqueeze(0),
torch.min(Rs).unsqueeze(0),
]
).unsqueeze(0)
)
elif self.agg == "max":
features.append(
torch.cat(
[
torch.max(Rs).unsqueeze(0),
torch.max(Rs).unsqueeze(0),
torch.max(Rs).unsqueeze(0),
]
).unsqueeze(0)
)
elif self.agg == "all":
features.append(
torch.cat(
[
torch.min(Rs).unsqueeze(0),
torch.mean(Rs).unsqueeze(0),
torch.max(Rs).unsqueeze(0),
]
).unsqueeze(0)
)
else:
features.append(
torch.FloatTensor([0.0, 0.0, 0.0]).unsqueeze(0)
) # .cuda()
features = torch.cat(features)
logits = self.layer_final(features)
histograms_out = [histogram.cpu().numpy() for histogram in histograms]
return logits, histograms_out, images
def save_imager_cache(self):
for imager in self.imagers:
imager.save_cache()
def score(self, originals, generateds, **kwargs):
with torch.no_grad():
logits, histograms, images = self.forward(originals, generateds)
probs = torch.nn.functional.softmax(logits, dim=-1)
batch_scores = probs[:, 1].tolist()
return {
"scores": batch_scores
} # , "histograms": histograms, "images": images
class _SummaCZS:
def __init__(
self,
model_name="mnli",
granularity="paragraph",
op1="max",
op2="mean",
use_ent=True,
use_con=True,
imager_load_cache=True,
device="cuda",
**kwargs
):
assert op2 in ["min", "mean", "max"], "Unrecognized `op2`"
assert op1 in ["max", "mean", "min"], "Unrecognized `op1`"
self.imager = _SummaCImager(
model_name=model_name,
granularity=granularity,
device=device,
**kwargs
)
if imager_load_cache:
self.imager.load_cache()
self.op2 = op2
self.op1 = op1
self.use_ent = use_ent
self.use_con = use_con
def save_imager_cache(self):
self.imager.save_cache()
def score_one(self, original, generated):
import numpy as np
image = self.imager.build_image(original, generated)
ent_scores = np.max(image[0], axis=0)
co_scores = np.max(image[1], axis=0)
if self.op1 == "mean":
ent_scores = np.mean(image[0], axis=0)
co_scores = np.mean(image[1], axis=0)
elif self.op1 == "min":
ent_scores = np.min(image[0], axis=0)
co_scores = np.min(image[1], axis=0)
if self.use_ent and self.use_con:
scores = ent_scores - co_scores
elif self.use_ent:
scores = ent_scores
elif self.use_con:
scores = 1.0 - co_scores
final_score = np.mean(scores)
if self.op2 == "min":
final_score = np.min(scores)
elif self.op2 == "max":
final_score = np.max(scores)
return {"score": final_score, "image": image}
def score(self, sources, generateds, **kwargs):
output = {"scores": [], "images": []}
for source, gen in zip(sources, generateds):
score = self.score_one(source, gen)
output["scores"].append(score["score"])
output["images"].append(score["image"])
return output