根据两页手写笔记整理。原笔记内容照原样保留,绿色底色的“补充”是后来加上的修正和说明。
| 模型 | |
| 基座模型 | Qwen3-1.7B-Base:28 个 Transformer 块,残差维度 2048,词表 151,936,bf16 推理;从 ModelScope 镜像下载 |
| SAE(原版) | Qwen-Scope SAE-Res-Qwen3-1.7B-Base-W32K-L0_50 的第 14 层:TopK(k = 50),32,768 个 latent,接在第 14 块输出(resid_post)上;编码时不减 b_dec,解码器每列是单位向量 |
| SAE(自己微调) | ft-L14-1M 和 ft-L14-20M:从 Qwen-Scope 初始化,在 FineWeb 33% / Civil Comments 45% / Reddit tldr-17 22% 的混合语料上继续训练 1M / 20M token;Adam,lr 3e-5,bf16 autocast,每步 4,096 个 token,AuxK 系数 1/32;20M 用时 89 分钟 |
| 数据 | |
| 分类数据 | IHC(HF SALT-NLP/ImplicitHate),6,266 条隐性仇恨推文,6 个子类;每条推文单独作为一个序列输入模型,最长 128 个 token,丢掉第 0 个 token |
| 留出集 | FineWeb、Civil Comments、Reddit 的留出部分和 IHC 各取 500 条,最长 256 个 token,用来算 FVU 和 ΔCE |
| 分类 | |
| 特征 | 第 14 层 SAE latent,对 token 做 max-pool → 32,768 维,再用 MaxAbsScaler 缩放到 0~1 |
| 探针 | scikit-learn LogisticRegression(多分类,lbfgs,max_iter 3000),L2 正则,C 在 0.002–0.2 之间扫 6 个值,最好的是 0.05;class_weight="balanced" |
| 对照组 | 同一层的原始残差:推文所有 token 的 xₜ 取平均(不含第 0 个)→ 2048 维,StandardScaler,C 在 0.0002–0.02 之间扫,最好的是 0.001;另有 max-pool 版和 TF-IDF 基线 |
| 评估 | 分层 5 折交叉验证 × 3 次重复,共 15 次,看 macro-F1;不同 SAE 之间用同一组折逐折配对比较,t 值用 Nadeau–Bengio 校正 |
| 环境 | |
| 硬件 | 本机 MacBook,M5 Pro,24GB 统一内存,15 个 CPU 核心;模型和 SAE 跑在 GPU(MPS)上,探针跑在 CPU 上 |
| 软件 | Python 3.12,PyTorch 2.14.1,transformers 5.21.0,scikit-learn 1.9.1 |
| 代码 | ~/research/implicit-hate-sae/scripts/:sae_features.py(编码 + 探针)、compare_probes.py(对比)、eval_sae.py(FVU / ΔCE)、train_sae.py(训练 SAE) |
SALT-NLP/ImplicitHate),6,266 条隐性仇恨推文,有 6 个 implicit hate 种类。| 子类 | 中文 | 条数 |
|---|---|---|
| white_grievance | 白人受害叙事 | 1,538 |
| incitement | 煽动 | 1,269 |
| stereotypical | 刻板印象 | 1,133 |
| inferiority | 贬低为低等 | 863 |
| irony | 讽刺 | 797 |
| threatening | 威胁 | 666 |
顺序是先挑出最大的 50 个,再对这 50 个做 ReLU;其中如果有负数会被置 0,所以严格说是“最多 50 个”。先做 ReLU 再挑最大的 50 个,结果完全一样。
pre = W_enc·x + b_enc 算出的是 32,768 个分数,还不是 x̂。完整过程还要加一步:
这样才有“输入 x → 输出 x̂”。另外,分类只用到 a,完全不用解码器;解码器只用于训练 SAE 和计算 FVU / ΔCE。
x̄ 是在每个评估集上分别算的,比如 IHC 一个、FineWeb 一个,不是所有数据共用一个。第 0 个 token(attention sink)和范数异常的 token 不参与计算。
| 留出集 | Qwen-Scope | 微调 1M | 微调 20M |
|---|---|---|---|
| FineWeb(通用文本) | 0.252 | 0.247 | 0.238 |
| Civil Comments | 0.271 | 0.256 | 0.233 |
| 0.280 | 0.268 | 0.250 | |
| IHC 推文(训练中未见) | 0.289 | 0.271 | 0.243 |
FVU 单看并不是越低越好。不限制每个 token 能用多少个 latent(比如 k ≥ 2048),SAE 可以学成恒等映射,FVU ≈ 0,但每个 latent 就是原始方向的翻版,没有可解释性。所以只有在稀疏度相同的前提下比较 FVU 才有意义。这里所有 SAE 都是 k = 50、32,768 个 latent,而且都在没参与训练的留出集上测。
原始残差基线就是跳过 SAE,直接把 17 个 xt 取平均,得到 2048 维向量,再训练同样的逻辑回归,结果是 0.633。SAE 版高出的 1.3 个点,是 SAE 带来的;15 折里 15 折都更好。
| 方法 | macro-F1 |
|---|---|
| 随机乱猜 | ≈ 0.17 |
| TF-IDF(只看词频) | 0.546 |
| 原始残差 mean-pool(对照组) | 0.633 |
| Qwen-Scope SAE,max-pool | 0.6465 |
| 微调 1M SAE | 0.6464 |
| 微调 20M SAE | 0.6461 |
5 折 × 3 次重复的平均,每种特征各自选最好的 C。微调后的 SAE 重建更好,但分类成绩和原版一样。
在第 14 块的输出处把每个 x 换成 x̂,再让后面第 15–27 块接着算到输出,然后在和 CE_clean 同样的位置上求平均。第 0 个 token 和那几个范数异常的 token 不替换,保留原值。ΔCE = 0.108 相当于困惑度上升约 11%(e0.108 ≈ 1.11)。
SAE 本身不分类,也从没见过标签。判断推文属于哪一类的,是另外在 SAE 输出上训练的逻辑回归(探针)。
第一项希望所有权重都小,第二项希望把训练集分对,C 决定第二项的分量。C 越小,权重被压得越小。W 有 6 × 32,768 = 196,608 个数,训练集只有 5,012 条;有 2,426 个 latent 只在 1 条训练推文里激活过,给它一个大权重就能把那条推文的标签“背”下来。
| 子类 | 训练条数 | 权重 s | 不加权:召回 | 加权:召回 |
|---|---|---|---|---|
| white_grievance | 1,230 | 0.679 | 0.669 | 0.633 |
| incitement | 1,015 | 0.823 | 0.654 | 0.654 |
| stereotypical | 906 | 0.922 | 0.581 | 0.586 |
| inferiority | 690 | 1.211 | 0.532 | 0.543 |
| irony | 638 | 1.309 | 0.654 | 0.660 |
| threatening | 533 | 1.567 | 0.722 | 0.752 |
| macro-F1 | 0.6396 | 0.6353 |
第 1 折,C = 0.05。分错一条 threatening 的代价是分错一条 white_grievance 的 2.3 倍,效果是把一部分推文从大类挪到小类。但这份数据只是轻度不平衡,macro-F1 几乎没变;以后做“仇恨 vs 非仇恨”时两类可能差好几倍,它会更重要。
W 是所有推文共用的投票表,z 是每条推文自己的。一条推文打分时只用到它自己那 479 列,其余列都乘以 0。5,012 条训练推文合起来点亮过 24,159 个不同的 latent,每个都需要一个权重;剩下 8,609 个在训练集里一次都没激活过,梯度永远是 0,所以权重一直是 0。
所有实验都在这台 Mac 上跑:M5 Pro,24GB 统一内存,15 个 CPU 核心,GPU 通过 PyTorch 的 MPS 调用。Qwen3-1.7B 跑到第 14 层约 8–10k tok/s;32K SAE 的训练步 bf16 约 15k tok/s,fp32 约 6k tok/s。
| 步骤 | 硬件 | 用时 |
|---|---|---|
| 训练 SAE(从 Qwen-Scope 微调 20M token) | GPU | 89 分钟 |
| 留出集评估(3 个 SAE × 4 组 × 500 条) | GPU | 约 5 分钟 |
| 编码 6,266 条推文,并跑 4 个探针 | GPU + CPU | 约 45 秒 / 每个 SAE |
| 对比实验:450 次探针训练 | CPU,5 个进程 | 约 3 分钟 |
| 训练一次探针(5,012 条 × 32,768 维,稀疏) | CPU | 0.5–2.4 秒 |
探针快,是因为训练矩阵是稀疏的(每条推文平均 611 个非零,总共约 300 万个),逻辑回归又是凸优化,几十次迭代就收敛。瓶颈始终在 GPU 上:生成模型激活和训练 SAE。从头训练 1 亿 token 的 SAE 在本机约 6 小时。