laya-ad / code /scripts /predict_dual_head.py
x6nux's picture
laya-ad v2.1: six precision dirs
f8d2531 verified
Raw History Blame Contribute Delete
4.08 kB
#!/usr/bin/env python3
"""四头模型推理:一次前向输出 is_ad / kind / scope / severity(0–3 期望分数)。"""
import argparse
import json
from pathlib import Path
import torch
from transformers import AutoTokenizer
from dual_head_model import load_dual_head_any
KINDS = ["crypto", "gambling", "porn", "porn_bait", "promo", "scam", "spam_flood"]
DEMO = [
"加微信 abc123 免费领取涨粉秘籍,名额有限!",
"今晚合唱团排练改到七点半,别走错了",
"这款面霜绝了,用了一个月肉眼可见变好,链接放评论区",
]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="outputs/ad-4head-teacher-heads/best")
ap.add_argument("--text", default=None)
ap.add_argument("--file", default=None, help="jsonl({text}) 或 txt,每行一条")
ap.add_argument("--threshold", type=float, default=0.5)
ap.add_argument("--kind_threshold", type=float, default=0.0,
help=">0 时 kind 置信度低于该值输出 ad_uncertain")
ap.add_argument("--batch_size", type=int, default=64)
ap.add_argument("--max_len", type=int, default=384)
args = ap.parse_args()
if args.text:
texts = [args.text]
elif args.file:
if args.file.endswith(".jsonl"):
texts = [json.loads(l)["text"] for l in open(args.file, encoding="utf-8") if l.strip()]
else:
texts = [l.rstrip("\n") for l in open(args.file, encoding="utf-8") if l.strip()]
else:
texts = DEMO
cfg_path = Path(args.model) / "dual_config.json"
dc = json.loads(cfg_path.read_text(encoding="utf-8")) if cfg_path.is_file() else {}
kind_labels = dc.get("kind_labels") or KINDS
scope_labels = dc.get("scope_labels") or []
severity_labels = dc.get("severity_labels") or []
device = "cuda" if torch.cuda.is_available() else "cpu"
model = load_dual_head_any(args.model, device=device, dtype=torch.bfloat16)
tok = AutoTokenizer.from_pretrained(args.model)
with torch.inference_mode():
for i in range(0, len(texts), args.batch_size):
chunk = texts[i:i + args.batch_size]
enc = tok(chunk, truncation=True, max_length=args.max_len,
padding=True, return_tensors="pt").to(device)
with torch.autocast(device_type=device, dtype=torch.bfloat16, enabled=device == "cuda"):
blog, klog, slog, vlog, _ = model.forward_all(enc["input_ids"], enc["attention_mask"])
p_ad = torch.softmax(blog.float(), -1)[:, 0].cpu()
pk = torch.softmax(klog.float(), -1).cpu()
ps = torch.softmax(slog.float(), -1).cpu() if slog is not None else None
pv = torch.softmax(vlog.float(), -1).cpu() if vlog is not None else None
for j, (t, pa) in enumerate(zip(chunk, p_ad)):
is_ad = bool(pa >= args.threshold)
k = int(pk[j].argmax())
kind = kind_labels[k] if is_ad else "normal"
if is_ad and args.kind_threshold > 0 and float(pk[j][k]) < args.kind_threshold:
kind = "ad_uncertain"
out = {"is_ad": is_ad, "p_ad": round(float(pa), 6),
"kind": kind, "kind_conf": round(float(pk[j][k]), 6),
"kind_probs": {n: round(float(pk[j][x]), 6) for x, n in enumerate(kind_labels)}}
if is_ad and ps is not None:
s = int(ps[j].argmax())
out["scope"] = scope_labels[s] if scope_labels else str(s)
out["scope_probs"] = {n: round(float(ps[j][x]), 6) for x, n in enumerate(scope_labels)}
if is_ad and pv is not None:
probs = [float(x) for x in pv[j]]
out["severity_score"] = round(sum(x * p for x, p in enumerate(probs)), 4)
out["severity_probs"] = {n: round(probs[x], 6) for x, n in enumerate(severity_labels)}
print(json.dumps({"text": t, **out}, ensure_ascii=False))
if __name__ == "__main__":
main()