pierrefdz's picture
inintal commit
8e6cbe9
"""
Main script for watermark detection.
Test with:
python -m wm_interactive.core.main --model_name smollm2-135m --prompt_path data/prompts.json --method maryland --delta 4.0 --ngram 1
"""
import os
import json
import time
import tqdm
import torch
import numpy as np
import pandas as pd
import argparse
from transformers import AutoModelForCausalLM, AutoTokenizer
from wm_interactive.core.generator import WmGenerator, OpenaiGenerator, MarylandGenerator
from wm_interactive.core.detector import WmDetector, OpenaiDetector, OpenaiDetectorZ, MarylandDetector, MarylandDetectorZ
# model names mapping
model_names = {
# 'llama-3.2-1b': 'meta-llama/Llama-3.2-1B-Instruct',
'smollm2-135m': 'HuggingFaceTB/SmolLM2-135M-Instruct',
'smollm2-360m': 'HuggingFaceTB/SmolLM2-360M-Instruct',
}
CACHE_DIR = "wm_interactive/static/hf_cache"
def load_prompts(json_path: str, prompt_type: str = "smollm", nsamples: int = None) -> list[dict]:
"""Load prompts from a JSON file.
Args:
json_path: Path to the JSON file
prompt_type: Type of prompt dataset (alpaca, smollm)
nsamples: Number of samples to load (if None, load all)
Returns:
List of prompts
"""
if not os.path.exists(json_path):
raise FileNotFoundError(f"File {json_path} not found")
with open(json_path, 'r') as f:
data = json.load(f)
if prompt_type == "alpaca":
prompts = [{"instruction": item["instruction"]} for item in data]
elif prompt_type == "smollm":
prompts = []
for item in data:
prompt = "<|im_start|>system\nYou are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>\n"
prompt += f"<|im_start|>user\n{item['instruction']}<|im_end|>\n<|im_start|>assistant\n"
prompts.append({"instruction": prompt})
else:
raise ValueError(f"Prompt type {prompt_type} not supported")
if nsamples is not None:
prompts = prompts[:nsamples]
return prompts
def load_results(json_path: str, result_key: str = "result", nsamples: int = None) -> list[str]:
"""Load results from a JSONL file.
Args:
json_path: Path to the JSONL file
result_key: Key to extract from each JSON line
nsamples: Number of samples to load (if None, load all)
Returns:
List of results
"""
if not os.path.exists(json_path):
raise FileNotFoundError(f"File {json_path} not found")
results = []
with open(json_path, 'r') as f:
for line in f:
if line.strip(): # Skip empty lines
data = json.loads(line)
results.append(data[result_key])
if nsamples is not None and len(results) >= nsamples:
break
return results
def get_args_parser():
parser = argparse.ArgumentParser('Args', add_help=False)
# model parameters
parser.add_argument('--model_name', type=str, required=True,
help='Name of the model to use. Choose from: llama-3.2-1b, smollm2-135m')
# prompts parameters
parser.add_argument('--prompt_path', type=str, default=None,
help='Path to the prompt dataset. Required if --prompt is not provided')
parser.add_argument('--prompt_type', type=str, default="smollm",
help='Type of prompt dataset. Only used if --prompt_path is provided')
parser.add_argument('--prompt', type=str, nargs='+', default=None,
help='List of prompts to use. If not provided, prompts will be loaded from --prompt_path')
# generation parameters
parser.add_argument('--temperature', type=float, default=0.8,
help='Temperature for sampling (higher = more random)')
parser.add_argument('--top_p', type=float, default=0.95,
help='Top p for nucleus sampling (lower = more focused)')
parser.add_argument('--max_gen_len', type=int, default=256,
help='Maximum length of generated text')
# watermark parameters
parser.add_argument('--method', type=str, default='none',
help='Watermarking method. Choose from: none (no watermarking), openai (Aaronson et al.), maryland (Kirchenbauer et al.)')
parser.add_argument('--method_detect', type=str, default='same',
help='Statistical test to detect watermark. Choose from: same (same as method), openai, openaiz, maryland, marylandz')
parser.add_argument('--seed', type=int, default=0,
help='Random seed for reproducibility')
parser.add_argument('--ngram', type=int, default=1,
help='n-gram size for rng key generation')
parser.add_argument('--gamma', type=float, default=0.5,
help='For maryland method: proportion of greenlist tokens')
parser.add_argument('--delta', type=float, default=2.0,
help='For maryland method: bias to add to greenlist tokens')
parser.add_argument('--scoring_method', type=str, default='v2',
help='Method for scoring. Choose from: none (score every token), v1 (score when context unique), v2 (score when context+token unique)')
# experiment parameters
parser.add_argument('--nsamples', type=int, default=None,
help='Number of samples to generate from the prompt dataset')
parser.add_argument('--do_eval', type=bool, default=True,
help='Whether to evaluate the generated text')
parser.add_argument('--output_dir', type=str, default='output',
help='Directory to save results')
return parser
def main(args):
print('job dir: {}'.format(os.path.dirname(os.path.realpath(__file__))))
print("{}".format(args).replace(', ', ',\n'))
torch.manual_seed(args.seed)
np.random.seed(args.seed)
# build model
model_name = args.model_name.lower()
if model_name not in model_names:
raise ValueError(f"Model {model_name} not supported. Choose from: {list(model_names.keys())}")
model_name = model_names[model_name]
# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=CACHE_DIR)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = AutoModelForCausalLM.from_pretrained(
model_name,
cache_dir=CACHE_DIR
).to(device)
# build watermark generator
if args.method == "none":
generator = WmGenerator(model, tokenizer)
elif args.method == "openai":
generator = OpenaiGenerator(model, tokenizer, args.ngram, args.seed)
elif args.method == "maryland":
generator = MarylandGenerator(model, tokenizer, args.ngram, args.seed, gamma=args.gamma, delta=args.delta)
else:
raise NotImplementedError("method {} not implemented".format(args.method))
# load prompts
if args.prompt is not None:
prompts = args.prompt
prompts = [{"instruction": prompt} for prompt in prompts]
elif args.prompt_path is not None:
prompts = load_prompts(json_path=args.prompt_path, prompt_type=args.prompt_type, nsamples=args.nsamples)
else:
raise ValueError("Either --prompt or --prompt_path must be provided")
# (re)start experiment
os.makedirs(args.output_dir, exist_ok=True)
start_point = 0 # if resuming, start from the last line of the file
if os.path.exists(os.path.join(args.output_dir, f"results.jsonl")):
with open(os.path.join(args.output_dir, f"results.jsonl"), "r") as f:
for _ in f:
start_point += 1
print(f"Starting from {start_point}")
# generate
all_times = []
with open(os.path.join(args.output_dir, f"results.jsonl"), "a") as f:
for ii in range(start_point, len(prompts)):
# generate text
time0 = time.time()
prompt = prompts[ii]["instruction"]
result = generator.generate(
prompt,
max_gen_len=args.max_gen_len,
temperature=args.temperature,
top_p=args.top_p
)
time1 = time.time()
# time chunk
speed = 1 / (time1 - time0)
eta = (len(prompts) - ii) / speed
eta = time.strftime("%Hh%Mm%Ss", time.gmtime(eta))
all_times.append(time1 - time0)
print(f"Generated {ii:5d} - Speed {speed:.2f} prompts/s - ETA {eta}")
# log
f.write(json.dumps({
"prompt": prompt,
"result": result[len(prompt):],
"speed": speed,
"eta": eta}) + "\n")
f.flush()
print(f"Average time per prompt: {np.sum(all_times) / (len(prompts) - start_point) :.2f}")
if args.method_detect == 'same':
args.method_detect = args.method
if (not args.do_eval) or (args.method_detect not in ["openai", "maryland", "marylandz", "openaiz"]):
return
# build watermark detector
if args.method_detect == "openai":
detector = OpenaiDetector(tokenizer, args.ngram, args.seed)
elif args.method_detect == "openaiz":
detector = OpenaiDetectorZ(tokenizer, args.ngram, args.seed)
elif args.method_detect == "maryland":
detector = MarylandDetector(tokenizer, args.ngram, args.seed, gamma=args.gamma, delta=args.delta)
elif args.method_detect == "marylandz":
detector = MarylandDetectorZ(tokenizer, args.ngram, args.seed, gamma=args.gamma, delta=args.delta)
# evaluate
results = load_results(json_path=os.path.join(args.output_dir, f"results.jsonl"), result_key="result", nsamples=args.nsamples)
log_stats = []
with open(os.path.join(args.output_dir, 'scores.jsonl'), 'w') as f:
for text in tqdm.tqdm(results):
# get token details and pvalues
token_details = detector.get_details(text, scoring_method=args.scoring_method)
pvalues, aux_info = detector.get_pvalues_by_tok(token_details)
# log stats
log_stat = {
'num_token': aux_info['ntoks_scored'],
'score': aux_info['final_score'],
'pvalue': aux_info['final_pvalue'],
'log10_pvalue': np.log10(aux_info['final_pvalue']),
}
log_stats.append(log_stat)
f.write('\n' + json.dumps({k: float(v) for k, v in log_stat.items()}))
df = pd.DataFrame(log_stats)
print(f">>> Scores: \n{df.describe(percentiles=[])}")
print(f"Saved scores to {os.path.join(args.output_dir, 'scores.csv')}")
if __name__ == "__main__":
args = get_args_parser().parse_args()
main(args)