import matplotlib.pyplot as plt import numpy as np import datasets import transformers import re import torch import torch.nn.functional as F import tqdm import random from sklearn.metrics import roc_curve, precision_recall_curve, auc import argparse import datetime import os import json import functools import custom_datasets from multiprocessing.pool import ThreadPool import time # 15 colorblind-friendly colors COLORS = ["#0072B2", "#009E73", "#D55E00", "#CC79A7", "#F0E442", "#56B4E9", "#E69F00", "#000000", "#0072B2", "#009E73", "#D55E00", "#CC79A7", "#F0E442", "#56B4E9", "#E69F00"] # define regex to match all tokens, where * is an integer pattern = re.compile(r"") def load_base_model(): print('MOVING BASE MODEL TO GPU...', end='', flush=True) start = time.time() try: mask_model.cpu() except NameError: pass if args.openai_model is None: base_model.to(DEVICE) print(f'DONE ({time.time() - start:.2f}s)') def load_mask_model(): print('MOVING MASK MODEL TO GPU...', end='', flush=True) start = time.time() if args.openai_model is None: base_model.cpu() if not args.random_fills: mask_model.to(DEVICE) print(f'DONE ({time.time() - start:.2f}s)') def tokenize_and_mask(text, span_length, pct, ceil_pct=False): tokens = text.split(' ') mask_string = '<<>>' n_spans = pct * len(tokens) / (span_length + args.buffer_size * 2) if ceil_pct: n_spans = np.ceil(n_spans) n_spans = int(n_spans) n_masks = 0 while n_masks < n_spans: start = np.random.randint(0, len(tokens) - span_length) end = start + span_length search_start = max(0, start - args.buffer_size) search_end = min(len(tokens), end + args.buffer_size) if mask_string not in tokens[search_start:search_end]: tokens[start:end] = [mask_string] n_masks += 1 # replace each occurrence of mask_string with , where NUM increments num_filled = 0 for idx, token in enumerate(tokens): if token == mask_string: tokens[idx] = f'' num_filled += 1 assert num_filled == n_masks, f"num_filled {num_filled} != n_masks {n_masks}" text = ' '.join(tokens) return text def count_masks(texts): return [len([x for x in text.split() if x.startswith("")[0] tokens = mask_tokenizer(texts, return_tensors="pt", padding=True).to(DEVICE) outputs = mask_model.generate(**tokens, max_length=150, do_sample=True, top_p=args.mask_top_p, num_return_sequences=1, eos_token_id=stop_id) return mask_tokenizer.batch_decode(outputs, skip_special_tokens=False) def extract_fills(texts): # remove from beginning of each text texts = [x.replace("", "").replace("", "").strip() for x in texts] # return the text in between each matched mask token extracted_fills = [pattern.split(x)[1:-1] for x in texts] # remove whitespace around each fill extracted_fills = [[y.strip() for y in x] for x in extracted_fills] return extracted_fills def apply_extracted_fills(masked_texts, extracted_fills): # split masked text into tokens, only splitting on spaces (not newlines) tokens = [x.split(' ') for x in masked_texts] n_expected = count_masks(masked_texts) # replace each mask token with the corresponding fill for idx, (text, fills, n) in enumerate(zip(tokens, extracted_fills, n_expected)): if len(fills) < n: tokens[idx] = [] else: for fill_idx in range(n): text[text.index(f"")] = fills[fill_idx] # join tokens back into text texts = [" ".join(x) for x in tokens] return texts def perturb_texts_(texts, span_length, pct, ceil_pct=False): if not args.random_fills: masked_texts = [tokenize_and_mask(x, span_length, pct, ceil_pct) for x in texts] raw_fills = replace_masks(masked_texts) extracted_fills = extract_fills(raw_fills) perturbed_texts = apply_extracted_fills(masked_texts, extracted_fills) # Handle the fact that sometimes the model doesn't generate the right number of fills and we have to try again attempts = 1 while '' in perturbed_texts: idxs = [idx for idx, x in enumerate(perturbed_texts) if x == ''] print(f'WARNING: {len(idxs)} texts have no fills. Trying again [attempt {attempts}].') masked_texts = [tokenize_and_mask(x, span_length, pct, ceil_pct) for idx, x in enumerate(texts) if idx in idxs] raw_fills = replace_masks(masked_texts) extracted_fills = extract_fills(raw_fills) new_perturbed_texts = apply_extracted_fills(masked_texts, extracted_fills) for idx, x in zip(idxs, new_perturbed_texts): perturbed_texts[idx] = x attempts += 1 else: if args.random_fills_tokens: # tokenize base_tokenizer tokens = base_tokenizer(texts, return_tensors="pt", padding=True).to(DEVICE) valid_tokens = tokens.input_ids != base_tokenizer.pad_token_id replace_pct = args.pct_words_masked * (args.span_length / (args.span_length + 2 * args.buffer_size)) # replace replace_pct of input_ids with random tokens random_mask = torch.rand(tokens.input_ids.shape, device=DEVICE) < replace_pct random_mask &= valid_tokens random_tokens = torch.randint(0, base_tokenizer.vocab_size, (random_mask.sum(),), device=DEVICE) # while any of the random tokens are special tokens, replace them with random non-special tokens while any(base_tokenizer.decode(x) in base_tokenizer.all_special_tokens for x in random_tokens): random_tokens = torch.randint(0, base_tokenizer.vocab_size, (random_mask.sum(),), device=DEVICE) tokens.input_ids[random_mask] = random_tokens perturbed_texts = base_tokenizer.batch_decode(tokens.input_ids, skip_special_tokens=True) else: masked_texts = [tokenize_and_mask(x, span_length, pct, ceil_pct) for x in texts] perturbed_texts = masked_texts # replace each with args.span_length random words from FILL_DICTIONARY for idx, text in enumerate(perturbed_texts): filled_text = text for fill_idx in range(count_masks([text])[0]): fill = random.sample(FILL_DICTIONARY, span_length) filled_text = filled_text.replace(f"", " ".join(fill)) assert count_masks([filled_text])[0] == 0, "Failed to replace all masks" perturbed_texts[idx] = filled_text return perturbed_texts def perturb_texts(texts, span_length, pct, ceil_pct=False): chunk_size = args.chunk_size if '11b' in mask_filling_model_name: chunk_size //= 2 outputs = [] for i in tqdm.tqdm(range(0, len(texts), chunk_size), desc="Applying perturbations"): outputs.extend(perturb_texts_(texts[i:i + chunk_size], span_length, pct, ceil_pct=ceil_pct)) return outputs def drop_last_word(text): return ' '.join(text.split(' ')[:-1]) def _openai_sample(p): if args.dataset != 'pubmed': # keep Answer: prefix for pubmed p = drop_last_word(p) # sample from the openai model kwargs = { "engine": args.openai_model, "max_tokens": 200 } if args.do_top_p: kwargs['top_p'] = args.top_p r = openai.Completion.create(prompt=f"{p}", **kwargs) return p + r['choices'][0].text # sample from base_model using ****only**** the first 30 tokens in each example as context def sample_from_model(texts, min_words=55, prompt_tokens=30): # encode each text as a list of token ids if args.dataset == 'pubmed': texts = [t[:t.index(custom_datasets.SEPARATOR)] for t in texts] all_encoded = base_tokenizer(texts, return_tensors="pt", padding=True).to(DEVICE) else: all_encoded = base_tokenizer(texts, return_tensors="pt", padding=True).to(DEVICE) all_encoded = {key: value[:, :prompt_tokens] for key, value in all_encoded.items()} if args.openai_model: # decode the prefixes back into text prefixes = base_tokenizer.batch_decode(all_encoded['input_ids'], skip_special_tokens=True) pool = ThreadPool(args.batch_size) decoded = pool.map(_openai_sample, prefixes) else: decoded = ['' for _ in range(len(texts))] # sample from the model until we get a sample with at least min_words words for each example # this is an inefficient way to do this (since we regenerate for all inputs if just one is too short), but it works tries = 0 while (m := min(len(x.split()) for x in decoded)) < min_words: if tries != 0: print() print(f"min words: {m}, needed {min_words}, regenerating (try {tries})") sampling_kwargs = {} if args.do_top_p: sampling_kwargs['top_p'] = args.top_p elif args.do_top_k: sampling_kwargs['top_k'] = args.top_k min_length = 50 if args.dataset in ['pubmed'] else 150 outputs = base_model.generate(**all_encoded, min_length=min_length, max_length=200, do_sample=True, **sampling_kwargs, pad_token_id=base_tokenizer.eos_token_id, eos_token_id=base_tokenizer.eos_token_id) decoded = base_tokenizer.batch_decode(outputs, skip_special_tokens=True) tries += 1 if args.openai_model: global API_TOKEN_COUNTER # count total number of tokens with GPT2_TOKENIZER total_tokens = sum(len(GPT2_TOKENIZER.encode(x)) for x in decoded) API_TOKEN_COUNTER += total_tokens return decoded def get_likelihood(logits, labels): assert logits.shape[0] == 1 assert labels.shape[0] == 1 logits = logits.view(-1, logits.shape[-1])[:-1] labels = labels.view(-1)[1:] log_probs = torch.nn.functional.log_softmax(logits, dim=-1) log_likelihood = log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1) return log_likelihood.mean() # Get the log likelihood of each text under the base_model def get_ll(text): if args.openai_model: kwargs = { "engine": args.openai_model, "temperature": 0, "max_tokens": 0, "echo": True, "logprobs": 0} r = openai.Completion.create(prompt=f"<|endoftext|>{text}", **kwargs) result = r['choices'][0] tokens, logprobs = result["logprobs"]["tokens"][1:], result["logprobs"]["token_logprobs"][1:] assert len(tokens) == len(logprobs), f"Expected {len(tokens)} logprobs, got {len(logprobs)}" return np.mean(logprobs) else: with torch.no_grad(): tokenized = base_tokenizer(text, return_tensors="pt").to(DEVICE) labels = tokenized.input_ids return -base_model(**tokenized, labels=labels).loss.item() def get_lls(texts): if not args.openai_model: return [get_ll(text) for text in texts] else: global API_TOKEN_COUNTER # use GPT2_TOKENIZER to get total number of tokens total_tokens = sum(len(GPT2_TOKENIZER.encode(text)) for text in texts) API_TOKEN_COUNTER += total_tokens * 2 # multiply by two because OpenAI double-counts echo_prompt tokens pool = ThreadPool(args.batch_size) return pool.map(get_ll, texts) # get the average rank of each observed token sorted by model likelihood def get_rank(text, log=False): assert args.openai_model is None, "get_rank not implemented for OpenAI models" with torch.no_grad(): tokenized = base_tokenizer(text, return_tensors="pt").to(DEVICE) logits = base_model(**tokenized).logits[:,:-1] labels = tokenized.input_ids[:,1:] # get rank of each label token in the model's likelihood ordering matches = (logits.argsort(-1, descending=True) == labels.unsqueeze(-1)).nonzero() assert matches.shape[1] == 3, f"Expected 3 dimensions in matches tensor, got {matches.shape}" ranks, timesteps = matches[:,-1], matches[:,-2] # make sure we got exactly one match for each timestep in the sequence assert (timesteps == torch.arange(len(timesteps)).to(timesteps.device)).all(), "Expected one match per timestep" ranks = ranks.float() + 1 # convert to 1-indexed rank if log: ranks = torch.log(ranks) return ranks.float().mean().item() # get average entropy of each token in the text def get_entropy(text): assert args.openai_model is None, "get_entropy not implemented for OpenAI models" with torch.no_grad(): tokenized = base_tokenizer(text, return_tensors="pt").to(DEVICE) logits = base_model(**tokenized).logits[:,:-1] neg_entropy = F.softmax(logits, dim=-1) * F.log_softmax(logits, dim=-1) return -neg_entropy.sum(-1).mean().item() def get_roc_metrics(real_preds, sample_preds): fpr, tpr, _ = roc_curve([0] * len(real_preds) + [1] * len(sample_preds), real_preds + sample_preds) roc_auc = auc(fpr, tpr) return fpr.tolist(), tpr.tolist(), float(roc_auc) def get_precision_recall_metrics(real_preds, sample_preds): precision, recall, _ = precision_recall_curve([0] * len(real_preds) + [1] * len(sample_preds), real_preds + sample_preds) pr_auc = auc(recall, precision) return precision.tolist(), recall.tolist(), float(pr_auc) # save the ROC curve for each experiment, given a list of output dictionaries, one for each experiment, using colorblind-friendly colors def save_roc_curves(experiments): # first, clear plt plt.clf() for experiment, color in zip(experiments, COLORS): metrics = experiment["metrics"] plt.plot(metrics["fpr"], metrics["tpr"], label=f"{experiment['name']}, roc_auc={metrics['roc_auc']:.3f}", color=color) # print roc_auc for this experiment print(f"{experiment['name']} roc_auc: {metrics['roc_auc']:.3f}") plt.plot([0, 1], [0, 1], color='black', lw=2, linestyle='--') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title(f'ROC Curves ({base_model_name} - {args.mask_filling_model_name})') plt.legend(loc="lower right", fontsize=6) plt.savefig(f"{SAVE_FOLDER}/roc_curves.png") # save the histogram of log likelihoods in two side-by-side plots, one for real and real perturbed, and one for sampled and sampled perturbed def save_ll_histograms(experiments): # first, clear plt plt.clf() for experiment in experiments: try: results = experiment["raw_results"] # plot histogram of sampled/perturbed sampled on left, original/perturbed original on right plt.figure(figsize=(20, 6)) plt.subplot(1, 2, 1) plt.hist([r["sampled_ll"] for r in results], alpha=0.5, bins='auto', label='sampled') plt.hist([r["perturbed_sampled_ll"] for r in results], alpha=0.5, bins='auto', label='perturbed sampled') plt.xlabel("log likelihood") plt.ylabel('count') plt.legend(loc='upper right') plt.subplot(1, 2, 2) plt.hist([r["original_ll"] for r in results], alpha=0.5, bins='auto', label='original') plt.hist([r["perturbed_original_ll"] for r in results], alpha=0.5, bins='auto', label='perturbed original') plt.xlabel("log likelihood") plt.ylabel('count') plt.legend(loc='upper right') plt.savefig(f"{SAVE_FOLDER}/ll_histograms_{experiment['name']}.png") except: pass # save the histograms of log likelihood ratios in two side-by-side plots, one for real and real perturbed, and one for sampled and sampled perturbed def save_llr_histograms(experiments): # first, clear plt plt.clf() for experiment in experiments: try: results = experiment["raw_results"] # plot histogram of sampled/perturbed sampled on left, original/perturbed original on right plt.figure(figsize=(20, 6)) plt.subplot(1, 2, 1) # compute the log likelihood ratio for each result for r in results: r["sampled_llr"] = r["sampled_ll"] - r["perturbed_sampled_ll"] r["original_llr"] = r["original_ll"] - r["perturbed_original_ll"] plt.hist([r["sampled_llr"] for r in results], alpha=0.5, bins='auto', label='sampled') plt.hist([r["original_llr"] for r in results], alpha=0.5, bins='auto', label='original') plt.xlabel("log likelihood ratio") plt.ylabel('count') plt.legend(loc='upper right') plt.savefig(f"{SAVE_FOLDER}/llr_histograms_{experiment['name']}.png") except: pass def get_perturbation_results(span_length=10, n_perturbations=1, n_samples=500): load_mask_model() torch.manual_seed(0) np.random.seed(0) results = [] original_text = data["original"] sampled_text = data["sampled"] perturb_fn = functools.partial(perturb_texts, span_length=span_length, pct=args.pct_words_masked) p_sampled_text = perturb_fn([x for x in sampled_text for _ in range(n_perturbations)]) p_original_text = perturb_fn([x for x in original_text for _ in range(n_perturbations)]) for _ in range(n_perturbation_rounds - 1): try: p_sampled_text, p_original_text = perturb_fn(p_sampled_text), perturb_fn(p_original_text) except AssertionError: break assert len(p_sampled_text) == len(sampled_text) * n_perturbations, f"Expected {len(sampled_text) * n_perturbations} perturbed samples, got {len(p_sampled_text)}" assert len(p_original_text) == len(original_text) * n_perturbations, f"Expected {len(original_text) * n_perturbations} perturbed samples, got {len(p_original_text)}" for idx in range(len(original_text)): results.append({ "original": original_text[idx], "sampled": sampled_text[idx], "perturbed_sampled": p_sampled_text[idx * n_perturbations: (idx + 1) * n_perturbations], "perturbed_original": p_original_text[idx * n_perturbations: (idx + 1) * n_perturbations] }) load_base_model() for res in tqdm.tqdm(results, desc="Computing log likelihoods"): p_sampled_ll = get_lls(res["perturbed_sampled"]) p_original_ll = get_lls(res["perturbed_original"]) res["original_ll"] = get_ll(res["original"]) res["sampled_ll"] = get_ll(res["sampled"]) res["all_perturbed_sampled_ll"] = p_sampled_ll res["all_perturbed_original_ll"] = p_original_ll res["perturbed_sampled_ll"] = np.mean(p_sampled_ll) res["perturbed_original_ll"] = np.mean(p_original_ll) res["perturbed_sampled_ll_std"] = np.std(p_sampled_ll) if len(p_sampled_ll) > 1 else 1 res["perturbed_original_ll_std"] = np.std(p_original_ll) if len(p_original_ll) > 1 else 1 return results def run_perturbation_experiment(results, criterion, span_length=10, n_perturbations=1, n_samples=500): # compute diffs with perturbed predictions = {'real': [], 'samples': []} for res in results: if criterion == 'd': predictions['real'].append(res['original_ll'] - res['perturbed_original_ll']) predictions['samples'].append(res['sampled_ll'] - res['perturbed_sampled_ll']) elif criterion == 'z': if res['perturbed_original_ll_std'] == 0: res['perturbed_original_ll_std'] = 1 print("WARNING: std of perturbed original is 0, setting to 1") print(f"Number of unique perturbed original texts: {len(set(res['perturbed_original']))}") print(f"Original text: {res['original']}") if res['perturbed_sampled_ll_std'] == 0: res['perturbed_sampled_ll_std'] = 1 print("WARNING: std of perturbed sampled is 0, setting to 1") print(f"Number of unique perturbed sampled texts: {len(set(res['perturbed_sampled']))}") print(f"Sampled text: {res['sampled']}") predictions['real'].append((res['original_ll'] - res['perturbed_original_ll']) / res['perturbed_original_ll_std']) predictions['samples'].append((res['sampled_ll'] - res['perturbed_sampled_ll']) / res['perturbed_sampled_ll_std']) fpr, tpr, roc_auc = get_roc_metrics(predictions['real'], predictions['samples']) p, r, pr_auc = get_precision_recall_metrics(predictions['real'], predictions['samples']) name = f'perturbation_{n_perturbations}_{criterion}' print(f"{name} ROC AUC: {roc_auc}, PR AUC: {pr_auc}") return { 'name': name, 'predictions': predictions, 'info': { 'pct_words_masked': args.pct_words_masked, 'span_length': span_length, 'n_perturbations': n_perturbations, 'n_samples': n_samples, }, 'raw_results': results, 'metrics': { 'roc_auc': roc_auc, 'fpr': fpr, 'tpr': tpr, }, 'pr_metrics': { 'pr_auc': pr_auc, 'precision': p, 'recall': r, }, 'loss': 1 - pr_auc, } def run_baseline_threshold_experiment(criterion_fn, name, n_samples=500): torch.manual_seed(0) np.random.seed(0) results = [] for batch in tqdm.tqdm(range(n_samples // batch_size), desc=f"Computing {name} criterion"): original_text = data["original"][batch * batch_size:(batch + 1) * batch_size] sampled_text = data["sampled"][batch * batch_size:(batch + 1) * batch_size] for idx in range(len(original_text)): results.append({ "original": original_text[idx], "original_crit": criterion_fn(original_text[idx]), "sampled": sampled_text[idx], "sampled_crit": criterion_fn(sampled_text[idx]), }) # compute prediction scores for real/sampled passages predictions = { 'real': [x["original_crit"] for x in results], 'samples': [x["sampled_crit"] for x in results], } fpr, tpr, roc_auc = get_roc_metrics(predictions['real'], predictions['samples']) p, r, pr_auc = get_precision_recall_metrics(predictions['real'], predictions['samples']) print(f"{name}_threshold ROC AUC: {roc_auc}, PR AUC: {pr_auc}") return { 'name': f'{name}_threshold', 'predictions': predictions, 'info': { 'n_samples': n_samples, }, 'raw_results': results, 'metrics': { 'roc_auc': roc_auc, 'fpr': fpr, 'tpr': tpr, }, 'pr_metrics': { 'pr_auc': pr_auc, 'precision': p, 'recall': r, }, 'loss': 1 - pr_auc, } # strip newlines from each example; replace one or more newlines with a single space def strip_newlines(text): return ' '.join(text.split()) # trim to shorter length def trim_to_shorter_length(texta, textb): # truncate to shorter of o and s shorter_length = min(len(texta.split(' ')), len(textb.split(' '))) texta = ' '.join(texta.split(' ')[:shorter_length]) textb = ' '.join(textb.split(' ')[:shorter_length]) return texta, textb def truncate_to_substring(text, substring, idx_occurrence): # truncate everything after the idx_occurrence occurrence of substring assert idx_occurrence > 0, 'idx_occurrence must be > 0' idx = -1 for _ in range(idx_occurrence): idx = text.find(substring, idx + 1) if idx == -1: return text return text[:idx] def generate_samples(raw_data, batch_size): torch.manual_seed(42) np.random.seed(42) data = { "original": [], "sampled": [], } for batch in range(len(raw_data) // batch_size): print('Generating samples for batch', batch, 'of', len(raw_data) // batch_size) original_text = raw_data[batch * batch_size:(batch + 1) * batch_size] sampled_text = sample_from_model(original_text, min_words=30 if args.dataset in ['pubmed'] else 55) for o, s in zip(original_text, sampled_text): if args.dataset == 'pubmed': s = truncate_to_substring(s, 'Question:', 2) o = o.replace(custom_datasets.SEPARATOR, ' ') o, s = trim_to_shorter_length(o, s) # add to the data data["original"].append(o) data["sampled"].append(s) if args.pre_perturb_pct > 0: print(f'APPLYING {args.pre_perturb_pct}, {args.pre_perturb_span_length} PRE-PERTURBATIONS') load_mask_model() data["sampled"] = perturb_texts(data["sampled"], args.pre_perturb_span_length, args.pre_perturb_pct, ceil_pct=True) load_base_model() return data def generate_data(dataset, key): # load data if dataset in custom_datasets.DATASETS: data = custom_datasets.load(dataset, cache_dir) else: data = datasets.load_dataset(dataset, split='train', cache_dir=cache_dir)[key] # get unique examples, strip whitespace, and remove newlines # then take just the long examples, shuffle, take the first 5,000 to tokenize to save time # then take just the examples that are <= 512 tokens (for the mask model) # then generate n_samples samples # remove duplicates from the data data = list(dict.fromkeys(data)) # deterministic, as opposed to set() # strip whitespace around each example data = [x.strip() for x in data] # remove newlines from each example data = [strip_newlines(x) for x in data] # try to keep only examples with > 250 words if dataset in ['writing', 'squad', 'xsum']: long_data = [x for x in data if len(x.split()) > 250] if len(long_data) > 0: data = long_data random.seed(0) random.shuffle(data) data = data[:5_000] # keep only examples with <= 512 tokens according to mask_tokenizer # this step has the extra effect of removing examples with low-quality/garbage content tokenized_data = preproc_tokenizer(data) data = [x for x, y in zip(data, tokenized_data["input_ids"]) if len(y) <= 512] # print stats about remainining data print(f"Total number of samples: {len(data)}") print(f"Average number of words: {np.mean([len(x.split()) for x in data])}") return generate_samples(data[:n_samples], batch_size=batch_size) def load_base_model_and_tokenizer(name): if args.openai_model is None: print(f'Loading BASE model {args.base_model_name}...') base_model_kwargs = {} if 'gpt-j' in name or 'neox' in name: base_model_kwargs.update(dict(torch_dtype=torch.float16)) if 'gpt-j' in name: base_model_kwargs.update(dict(revision='float16')) base_model = transformers.AutoModelForCausalLM.from_pretrained(name, **base_model_kwargs, cache_dir=cache_dir) else: base_model = None optional_tok_kwargs = {} if "facebook/opt-" in name: print("Using non-fast tokenizer for OPT") optional_tok_kwargs['fast'] = False if args.dataset in ['pubmed']: optional_tok_kwargs['padding_side'] = 'left' base_tokenizer = transformers.AutoTokenizer.from_pretrained(name, **optional_tok_kwargs, cache_dir=cache_dir) base_tokenizer.pad_token_id = base_tokenizer.eos_token_id return base_model, base_tokenizer def eval_supervised(data, model): print(f'Beginning supervised evaluation with {model}...') detector = transformers.AutoModelForSequenceClassification.from_pretrained(model, cache_dir=cache_dir).to(DEVICE) tokenizer = transformers.AutoTokenizer.from_pretrained(model, cache_dir=cache_dir) real, fake = data['original'], data['sampled'] with torch.no_grad(): # get predictions for real real_preds = [] for batch in tqdm.tqdm(range(len(real) // batch_size), desc="Evaluating real"): batch_real = real[batch * batch_size:(batch + 1) * batch_size] batch_real = tokenizer(batch_real, padding=True, truncation=True, max_length=512, return_tensors="pt").to(DEVICE) real_preds.extend(detector(**batch_real).logits.softmax(-1)[:,0].tolist()) # get predictions for fake fake_preds = [] for batch in tqdm.tqdm(range(len(fake) // batch_size), desc="Evaluating fake"): batch_fake = fake[batch * batch_size:(batch + 1) * batch_size] batch_fake = tokenizer(batch_fake, padding=True, truncation=True, max_length=512, return_tensors="pt").to(DEVICE) fake_preds.extend(detector(**batch_fake).logits.softmax(-1)[:,0].tolist()) predictions = { 'real': real_preds, 'samples': fake_preds, } fpr, tpr, roc_auc = get_roc_metrics(real_preds, fake_preds) p, r, pr_auc = get_precision_recall_metrics(real_preds, fake_preds) print(f"{model} ROC AUC: {roc_auc}, PR AUC: {pr_auc}") # free GPU memory del detector torch.cuda.empty_cache() return { 'name': model, 'predictions': predictions, 'info': { 'n_samples': n_samples, }, 'metrics': { 'roc_auc': roc_auc, 'fpr': fpr, 'tpr': tpr, }, 'pr_metrics': { 'pr_auc': pr_auc, 'precision': p, 'recall': r, }, 'loss': 1 - pr_auc, } if __name__ == '__main__': DEVICE = "cuda" parser = argparse.ArgumentParser() parser.add_argument('--dataset', type=str, default="xsum") parser.add_argument('--dataset_key', type=str, default="document") parser.add_argument('--pct_words_masked', type=float, default=0.3) # pct masked is actually pct_words_masked * (span_length / (span_length + 2 * buffer_size)) parser.add_argument('--span_length', type=int, default=2) parser.add_argument('--n_samples', type=int, default=200) parser.add_argument('--n_perturbation_list', type=str, default="1,10") parser.add_argument('--n_perturbation_rounds', type=int, default=1) parser.add_argument('--base_model_name', type=str, default="gpt2-medium") parser.add_argument('--scoring_model_name', type=str, default="") parser.add_argument('--mask_filling_model_name', type=str, default="t5-large") parser.add_argument('--batch_size', type=int, default=50) parser.add_argument('--chunk_size', type=int, default=20) parser.add_argument('--n_similarity_samples', type=int, default=20) parser.add_argument('--int8', action='store_true') parser.add_argument('--half', action='store_true') parser.add_argument('--base_half', action='store_true') parser.add_argument('--do_top_k', action='store_true') parser.add_argument('--top_k', type=int, default=40) parser.add_argument('--do_top_p', action='store_true') parser.add_argument('--top_p', type=float, default=0.96) parser.add_argument('--output_name', type=str, default="") parser.add_argument('--openai_model', type=str, default=None) parser.add_argument('--openai_key', type=str) parser.add_argument('--baselines_only', action='store_true') parser.add_argument('--skip_baselines', action='store_true') parser.add_argument('--buffer_size', type=int, default=1) parser.add_argument('--mask_top_p', type=float, default=1.0) parser.add_argument('--pre_perturb_pct', type=float, default=0.0) parser.add_argument('--pre_perturb_span_length', type=int, default=5) parser.add_argument('--random_fills', action='store_true') parser.add_argument('--random_fills_tokens', action='store_true') parser.add_argument('--cache_dir', type=str, default="~/.cache") args = parser.parse_args() API_TOKEN_COUNTER = 0 if args.openai_model is not None: import openai assert args.openai_key is not None, "Must provide OpenAI API key as --openai_key" openai.api_key = args.openai_key START_DATE = datetime.datetime.now().strftime('%Y-%m-%d') START_TIME = datetime.datetime.now().strftime('%H-%M-%S-%f') # define SAVE_FOLDER as the timestamp - base model name - mask filling model name # create it if it doesn't exist precision_string = "int8" if args.int8 else ("fp16" if args.half else "fp32") sampling_string = "top_k" if args.do_top_k else ("top_p" if args.do_top_p else "temp") output_subfolder = f"{args.output_name}/" if args.output_name else "" if args.openai_model is None: base_model_name = args.base_model_name.replace('/', '_') else: base_model_name = "openai-" + args.openai_model.replace('/', '_') scoring_model_string = (f"-{args.scoring_model_name}" if args.scoring_model_name else "").replace('/', '_') SAVE_FOLDER = f"tmp_results/{output_subfolder}{base_model_name}{scoring_model_string}-{args.mask_filling_model_name}-{sampling_string}/{START_DATE}-{START_TIME}-{precision_string}-{args.pct_words_masked}-{args.n_perturbation_rounds}-{args.dataset}-{args.n_samples}" if not os.path.exists(SAVE_FOLDER): os.makedirs(SAVE_FOLDER) print(f"Saving results to absolute path: {os.path.abspath(SAVE_FOLDER)}") # write args to file with open(os.path.join(SAVE_FOLDER, "args.json"), "w") as f: json.dump(args.__dict__, f, indent=4) mask_filling_model_name = args.mask_filling_model_name n_samples = args.n_samples batch_size = args.batch_size n_perturbation_list = [int(x) for x in args.n_perturbation_list.split(",")] n_perturbation_rounds = args.n_perturbation_rounds n_similarity_samples = args.n_similarity_samples cache_dir = args.cache_dir os.environ["XDG_CACHE_HOME"] = cache_dir if not os.path.exists(cache_dir): os.makedirs(cache_dir) print(f"Using cache dir {cache_dir}") GPT2_TOKENIZER = transformers.GPT2Tokenizer.from_pretrained('gpt2', cache_dir=cache_dir) # generic generative model base_model, base_tokenizer = load_base_model_and_tokenizer(args.base_model_name) # mask filling t5 model if not args.baselines_only and not args.random_fills: int8_kwargs = {} half_kwargs = {} if args.int8: int8_kwargs = dict(load_in_8bit=True, device_map='auto', torch_dtype=torch.bfloat16) elif args.half: half_kwargs = dict(torch_dtype=torch.bfloat16) print(f'Loading mask filling model {mask_filling_model_name}...') mask_model = transformers.AutoModelForSeq2SeqLM.from_pretrained(mask_filling_model_name, **int8_kwargs, **half_kwargs, cache_dir=cache_dir) try: n_positions = mask_model.config.n_positions except AttributeError: n_positions = 512 else: n_positions = 512 preproc_tokenizer = transformers.AutoTokenizer.from_pretrained('t5-small', model_max_length=512, cache_dir=cache_dir) mask_tokenizer = transformers.AutoTokenizer.from_pretrained(mask_filling_model_name, model_max_length=n_positions, cache_dir=cache_dir) if args.dataset in ['english', 'german']: preproc_tokenizer = mask_tokenizer load_base_model() print(f'Loading dataset {args.dataset}...') data = generate_data(args.dataset, args.dataset_key) if args.random_fills: FILL_DICTIONARY = set() for texts in data.values(): for text in texts: FILL_DICTIONARY.update(text.split()) FILL_DICTIONARY = sorted(list(FILL_DICTIONARY)) if args.scoring_model_name: print(f'Loading SCORING model {args.scoring_model_name}...') del base_model del base_tokenizer torch.cuda.empty_cache() base_model, base_tokenizer = load_base_model_and_tokenizer(args.scoring_model_name) load_base_model() # Load again because we've deleted/replaced the old model # write the data to a json file in the save folder with open(os.path.join(SAVE_FOLDER, "raw_data.json"), "w") as f: print(f"Writing raw data to {os.path.join(SAVE_FOLDER, 'raw_data.json')}") json.dump(data, f) if not args.skip_baselines: baseline_outputs = [run_baseline_threshold_experiment(get_ll, "likelihood", n_samples=n_samples)] if args.openai_model is None: rank_criterion = lambda text: -get_rank(text, log=False) baseline_outputs.append(run_baseline_threshold_experiment(rank_criterion, "rank", n_samples=n_samples)) logrank_criterion = lambda text: -get_rank(text, log=True) baseline_outputs.append(run_baseline_threshold_experiment(logrank_criterion, "log_rank", n_samples=n_samples)) entropy_criterion = lambda text: get_entropy(text) baseline_outputs.append(run_baseline_threshold_experiment(entropy_criterion, "entropy", n_samples=n_samples)) baseline_outputs.append(eval_supervised(data, model='roberta-base-openai-detector')) baseline_outputs.append(eval_supervised(data, model='roberta-large-openai-detector')) outputs = [] if not args.baselines_only: # run perturbation experiments for n_perturbations in n_perturbation_list: perturbation_results = get_perturbation_results(args.span_length, n_perturbations, n_samples) for perturbation_mode in ['d', 'z']: output = run_perturbation_experiment( perturbation_results, perturbation_mode, span_length=args.span_length, n_perturbations=n_perturbations, n_samples=n_samples) outputs.append(output) with open(os.path.join(SAVE_FOLDER, f"perturbation_{n_perturbations}_{perturbation_mode}_results.json"), "w") as f: json.dump(output, f) if not args.skip_baselines: # write likelihood threshold results to a file with open(os.path.join(SAVE_FOLDER, f"likelihood_threshold_results.json"), "w") as f: json.dump(baseline_outputs[0], f) if args.openai_model is None: # write rank threshold results to a file with open(os.path.join(SAVE_FOLDER, f"rank_threshold_results.json"), "w") as f: json.dump(baseline_outputs[1], f) # write log rank threshold results to a file with open(os.path.join(SAVE_FOLDER, f"logrank_threshold_results.json"), "w") as f: json.dump(baseline_outputs[2], f) # write entropy threshold results to a file with open(os.path.join(SAVE_FOLDER, f"entropy_threshold_results.json"), "w") as f: json.dump(baseline_outputs[3], f) # write supervised results to a file with open(os.path.join(SAVE_FOLDER, f"roberta-base-openai-detector_results.json"), "w") as f: json.dump(baseline_outputs[-2], f) # write supervised results to a file with open(os.path.join(SAVE_FOLDER, f"roberta-large-openai-detector_results.json"), "w") as f: json.dump(baseline_outputs[-1], f) outputs += baseline_outputs save_roc_curves(outputs) save_ll_histograms(outputs) save_llr_histograms(outputs) # move results folder from tmp_results/ to results/, making sure necessary directories exist new_folder = SAVE_FOLDER.replace("tmp_results", "results") if not os.path.exists(os.path.dirname(new_folder)): os.makedirs(os.path.dirname(new_folder)) os.rename(SAVE_FOLDER, new_folder) print(f"Used an *estimated* {API_TOKEN_COUNTER} API tokens (may be inaccurate)")