laya-ad / code /scripts /build_dual_dataset.py
x6nux's picture
laya-ad v2: one dual-head model (binary + 7-class kind), six precision dirs
dd18e84 verified
Raw History Blame Contribute Delete
4.17 kB
#!/usr/bin/env python3
"""构建双头联合训练数据:每行同时带二分类标签和 7 类细类标签。
- label_bin: 0=ad, 1=normal
- label_kind: 0..6 细类;非广告或广告但无细类标签 = -100
- 训练池 = processed_v2/train;3% 分层切 dev(按 normal / 细类 / ad_unknown 分组)
- 测试 = processed_v2/val;source=archive 的子集即 827 干净 holdout
"""
import argparse
import collections
import json
from pathlib import Path
import numpy as np
from sklearn.model_selection import train_test_split
KINDS = ["crypto", "gambling", "porn", "porn_bait", "promo", "scam", "spam_flood"]
MAP = {"contact": "porn"}
DROP = {"spam", "impersonate", "unspecified", "normal", "none", None, ""}
def load(path):
return [json.loads(l) for l in open(path, encoding="utf-8") if l.strip()]
def clean(lab):
if lab in MAP:
lab = MAP[lab]
return lab if lab in KINDS else None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--gold", default="data/raw/dataset/converted/gold.kind.jsonl")
ap.add_argument("--archive", default="data/raw/archive/converted/archive.strong.binary.jsonl")
ap.add_argument("--split_train", default="data/processed_v2/train.jsonl")
ap.add_argument("--split_val", default="data/processed_v2/val.jsonl")
ap.add_argument("--out", default="data/processed_dual")
ap.add_argument("--dev_ratio", type=float, default=0.03)
ap.add_argument("--seed", type=int, default=42)
args = ap.parse_args()
gold, arch = {}, {}
gold_all, arch_all = set(), set()
for r in load(args.gold):
gold_all.add(r["text"].strip())
lab = clean(r["label"])
if lab:
gold.setdefault(r["text"].strip(), lab)
for r in load(args.archive):
arch_all.add(r["text"].strip())
if r.get("label") != "ad":
continue
lab = clean(r.get("ad_kind"))
if lab:
arch.setdefault(r["text"].strip(), lab)
def kind_of(text):
t = text.strip()
return gold.get(t) or arch.get(t)
def pack(r, source=None):
text = r["text"]
is_ad = r["label"] == "ad"
k = kind_of(text) if is_ad else None
row = {"text": text,
"label_bin": 0 if is_ad else 1,
"label_kind": KINDS.index(k) if k else -100}
if source is not None:
row["source"] = source
return row
train_rows = load(args.split_train)
val_rows = load(args.split_val)
train = [pack(r) for r in train_rows]
val = []
for r in val_rows:
t = r["text"].strip()
src = "archive" if t in arch_all else ("gold" if t in gold_all else "none")
val.append(pack(r, src))
# dev 分层键:normal / 细类 / ad_unknown
keys = []
for r in train:
if r["label_bin"] == 1:
keys.append("normal")
elif r["label_kind"] >= 0:
keys.append(KINDS[r["label_kind"]])
else:
keys.append("ad_unknown")
idx = np.arange(len(train))
tr_idx, dev_idx = train_test_split(idx, test_size=args.dev_ratio,
stratify=np.array(keys), random_state=args.seed)
fit = [train[i] for i in tr_idx]
dev = [train[i] for i in dev_idx]
holdout = [r for r in val if r.get("source") == "archive"]
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
for name, rows in [("train", fit), ("dev", dev), ("val", val), ("holdout827", holdout)]:
with open(out / f"{name}.jsonl", "w", encoding="utf-8") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
b = collections.Counter(r["label_bin"] for r in rows)
k = collections.Counter(KINDS[r["label_kind"]] for r in rows if r["label_kind"] >= 0)
print(f"{name:<12} n={len(rows):5d} bin={dict(b)} kind={dict(k)}")
(out / "label_map.json").write_text(json.dumps({
"binary": {"ad": 0, "normal": 1},
"kind": {k: i for i, k in enumerate(KINDS)},
}, ensure_ascii=False, indent=2), encoding="utf-8")
print("输出:", out.resolve())
if __name__ == "__main__":
main()