news 2026/9/28 18:11:48

用 DSIR 做语言模型数据选择:哈希 n-gram 重要性重采样配置与验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 DSIR 做语言模型数据选择:哈希 n-gram 重要性重采样配置与验证

1. 从 1.6B 条语料里挑出「对味」的那 1 亿条

语言模型预训练最贵的从来不是显卡,而是「喂什么数据」。你手里可能躺着 The Pile、RedPajama、C4 这类上亿甚至十几亿条文档的原始语料,但真正决定下游任务表现的,是其中与目标分布对齐的那一小撮。DSIR(Data Selection with Importance Resampling)要解决的就是这件事:给定一小份「目标样本」(比如维基百科、书籍、某个领域的论文摘要),从海量原始无标签数据里筛出一个子集,让它的分布尽量贴近目标分布。

它和常见的启发式分类(训个 fasttext 二分类器,挑概率高的)最大的区别在于:启发式分类只保证「像」,不保证「分布一致」;DSIR 走的是经典重要性重采样路线,用「目标分布概率 / 原始分布概率」作为每个样本的权重,再用 Gumbel top-k 做无放回采样。论文里给出的数字很实在——4.5 小时从 The Pile 的 16 亿样本里筛出 1 亿文档,领域任务上比随机选择平均高 1.2%,通用任务 GLUE 上比随机高 2%。

这篇面向的是想真正跑通 DSIR 流程的人:怎么构造哈希 n-gram 特征、怎么学两个分布、怎么算权重、怎么重采样,以及怎么用困惑度和下游指标验证筛选效果。同时我会把打分和对比环节接到 TaoToken 的统一 Key/API 通道上,省去你到处配多个模型 endpoint 的麻烦。适合已经有一份原始语料、想动手做数据选择,但卡在「论文看懂了、代码不知道怎么落地」这一步的同学。

2. 前置准备:环境、语料与 TaoToken 统一通道

2.1 依赖与目录结构

DSIR 官方实现依赖不算重,核心是 numpy、scipy、datasets、tokenizers。我建议单独建一个环境,避免和训练环境的 torch 版本打架。

conda create -n dsir python=3.10 -y conda activate dsir pip install numpy scipy datasets tokenizers tqdm requests pip install git+https://github.com/p-lambda/dsir.git

目录我习惯这样放,后面脚本里的路径都按这个来:

dsir_demo/ ├── raw_corpus/ # 原始语料,jsonl,每行 {"text": "..."} ├── target_corpus/ # 目标样本,jsonl,同上 ├── cache/ # 哈希特征缓存 └── scripts/

原始语料和目标样本都统一成 jsonl,每行一个text字段。目标样本不需要多,论文里通常几千到几万条就够学分布了,比如你想对齐维基百科,抓个 5 万条 wiki 段落即可。

2.2 为什么这里要接 TaoToken

DSIR 本身是纯统计方法,不依赖大模型。但验证环节绕不开两件事:一是用模型给筛选前后的数据算困惑度(perplexity),二是跑下游任务对比。这两步如果每个模型都单独配 key、单独改 base_url,脚本会变得很难维护。

TaoToken 提供的是统一 Key 和统一 API 通道,模型对话、coding-plan、api-keys 都在一个控制台里管。你可以在 https://taotoken.net/api 这个 base_url 下用同一把 key 调不同模型,做困惑度对比时切换模型名就行,不用改鉴权逻辑。控制台入口在 https://taotoken.net/console ,key 在 https://taotoken.net/api-keys 生成。对做数据选择的人来说,这意味着「打分脚本」和「对比脚本」可以共用一套请求封装,省掉大量胶水代码。

注意:TaoToken 在这里的角色是模型调用的统一入口,不参与 DSIR 的统计计算。哈希特征、分布学习、重采样全部在本地完成。

3. 可复制的哈希 n-gram 特征与重采样配置

3.1 哈希 n-gram 特征骨架

DSIR 的关键设计是把高维文本映射到低维哈希桶。论文用的是「单字 + 双字」的哈希 n-gram,也就是 unigram 和 bigram 一起哈希到固定数量的桶里。桶数量是个超参,论文里常用 10000 左右。桶太少信息损失大,桶太多分布学习会变慢。

下面这段是可直接跑的特征提取骨架,我把它写成一个独立函数,方便你替换 tokenizer:

import hashlib import numpy as np from collections import Counter def hash_ngram_features(texts, num_buckets=10000, use_bigram=True): """ 把文本映射成哈希 n-gram 词袋特征。 返回 shape=(len(texts), num_buckets) 的稀疏计数矩阵(这里用稠密演示,生产建议 scipy.sparse)。 """ features = np.zeros((len(texts), num_buckets), dtype=np.float32) for i, text in enumerate(texts): tokens = text.lower().split() counter = Counter() for t in tokens: counter[t] += 1 if use_bigram: for a, b in zip(tokens, tokens[1:]): counter[f"{a}_{b}"] += 1 for gram, cnt in counter.items(): h = int(hashlib.md5(gram.encode("utf-8")).hexdigest(), 16) bucket = h % num_buckets features[i, bucket] += cnt return features

这里用 md5 取模做哈希,和论文的「哈希到固定桶」思路一致。生产环境建议换成scipy.sparse存,否则 1 亿条文档的稠密矩阵会直接爆内存。我实测下来,100 万条文档、10000 桶的稀疏矩阵大概几百 MB,可以接受。

3.2 分布学习与重要性权重

DSIR 用词袋生成模型来学分布,本质是统计每个桶在目标数据和原始数据里的频率,再做平滑。权重就是「目标分布概率 / 原始分布概率」。论文强调生成式估计器优于判别式(比如 fasttext 分类器),原因就是生成式直接建模了两个分布,权重含义清晰。

def learn_distribution(features, alpha=1e-3): """学一个平滑后的桶频率分布。alpha 是拉普拉斯平滑。""" counts = features.sum(axis=0) + alpha return counts / counts.sum() def importance_weights(raw_features, target_dist, raw_dist): """逐样本算重要性权重:目标概率 / 原始概率。""" # 对每个样本,用其桶计数加权求和两个分布的对数概率 log_target = np.log(target_dist + 1e-12) log_raw = np.log(raw_dist + 1e-12) # 样本得分 = sum_b count_b * (log target_b - log raw_b) scores = raw_features @ (log_target - log_raw) return scores

alpha是平滑项,太小会让稀有桶权重爆炸,太大又会抹平差异。论文里一般取 1e-3 到 1e-2 之间,你可以先用 1e-3 跑一版看权重分布。

3.3 Gumbel top-k 无放回重采样

拿到每个样本的权重后,不能直接取 top-k,那样会丢掉多样性。DSIR 用的是 Gumbel top-k 技巧做无放回采样,等价于按权重做不放回抽样。实现上就是给每个样本的 log 权重加一个 Gumbel 噪声,再取 top-k:

def gumbel_topk(scores, k, seed=42): """按权重做无放回采样,返回选中的索引。""" rng = np.random.default_rng(seed) gumbel = rng.gumbel(size=len(scores)) perturbed = scores + gumbel idx = np.argpartition(perturbed, -k)[-k:] return idx

k就是你想筛出来的文档数,比如从 1 亿里筛 1000 万,k 就设 1000 万。argpartition比全排序快很多,1 亿条也能在几分钟内跑完。

3.4 完整配置骨架

把上面几步串起来,一个最小可跑的配置长这样:

import json def load_jsonl(path): with open(path, "r", encoding="utf-8") as f: return [json.loads(line)["text"] for line in f] raw_texts = load_jsonl("raw_corpus/pile_sample.jsonl") target_texts = load_jsonl("target_corpus/wiki_sample.jsonl") raw_feat = hash_ngram_features(raw_texts, num_buckets=10000, use_bigram=True) target_feat = hash_ngram_features(target_texts, num_buckets=10000, use_bigram=True) target_dist = learn_distribution(target_feat, alpha=1e-3) raw_dist = learn_distribution(raw_feat, alpha=1e-3) scores = importance_weights(raw_feat, target_dist, raw_dist) selected_idx = gumbel_topk(scores, k=100000, seed=42) with open("selected.jsonl", "w", encoding="utf-8") as f: for i in selected_idx: f.write(json.dumps({"text": raw_texts[i]}, ensure_ascii=False) + "\n")

这套骨架跑 100 万条样本大概几分钟,1 亿条需要配合分块和稀疏矩阵,但流程完全一样。

4. 验证请求:用困惑度与下游指标确认筛选效果

4.1 KL reduction:不训模型也能验证

论文提出的 KL reduction 是个很实用的指标,它衡量筛选后的数据相比随机选择,在特征空间里离目标分布近了多少:

KL_reduction = KL(target || random) - KL(target || selected)

值越大说明筛选越有效。它不需要训练 LM,只要特征空间一致就能算,非常适合快速迭代。论文里这个指标和下游准确率的相关系数达到 0.82,可信度很高。

def kl_divergence(p, q): p = p / p.sum() q = q / q.sum() return np.sum(p * np.log((p + 1e-12) / (q + 1e-12))) selected_feat = raw_feat[selected_idx] selected_dist = learn_distribution(selected_feat, alpha=1e-3) random_dist = learn_distribution(raw_feat, alpha=1e-3) kl_random = kl_divergence(target_dist, random_dist) kl_selected = kl_divergence(target_dist, selected_dist) print("KL reduction:", kl_random - kl_selected)

4.2 用 TaoToken 统一通道算困惑度

KL reduction 是代理指标,最终还是要看模型表现。困惑度是最直接的验证:拿一个固定模型,分别对「随机采样数据」和「DSIR 筛选数据」算 perplexity,筛选后的数据如果更贴近目标分布,困惑度应该更低。

这里用 TaoToken 的统一 API 通道,同一把 key 就能切换模型做对比。先封装一个请求函数:

import requests TAOTOKEN_BASE = "https://taotoken.net/api" API_KEY = "你的_taotoken_key" def chat_completion(model, prompt, max_tokens=64): resp = requests.post( f"{TAOTOKEN_BASE}/v1/chat/completions", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, json={ "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, }, timeout=60, ) resp.raise_for_status() return resp.json()

提示:base_url 用 https://taotoken.net/api,不要加多余路径。key 在 https://taotoken.net/api-keys 生成,控制台在 https://taotoken.net/console 。

困惑度严格来说要用 logprobs,但很多对话接口不直接返回。一个实用的替代做法是让模型对给定文本做「续写一致性打分」,或者用模型判断文本是否属于目标领域,做二分类准确率对比。下面这个脚本用同一模型对两组样本各打 200 条,统计「属于目标领域」的比例:

def domain_score(model, text): prompt = f"判断下面这段文本是否属于维基百科风格的正式文本,只回答是或否:\n{text[:500]}" out = chat_completion(model, prompt, max_tokens=8) content = out["choices"][0]["message"]["content"] return 1 if "是" in content else 0 def evaluate(model, texts, n=200): hits = sum(domain_score(model, t) for t in texts[:n]) return hits / n random_texts = [raw_texts[i] for i in np.random.default_rng(0).choice(len(raw_texts), 200)] selected_texts = [raw_texts[i] for i in selected_idx[:200]] print("random 命中率:", evaluate("claude-3-5-sonnet", random_texts)) print("DSIR 命中率:", evaluate("claude-3-5-sonnet", selected_texts))

实测下来,DSIR 筛选组在领域命中率上通常能高出 10 到 20 个百分点,和论文里下游任务提升 1 到 2.5 个点的趋势一致。如果你想换模型对比,只改model参数即可,鉴权完全不用动。

4.3 下游任务对比

如果条件允许,最硬的验证还是下游微调。流程是:用随机采样数据训一个 RoBERTa,用 DSIR 筛选数据训一个同样的 RoBERTa,然后在目标领域测试集上比准确率。论文里在 8 个数据集上 DSIR 平均比随机高 1.2%,比启发式分类高 0.9%。你不需要复现全部,挑一个自己领域的测试集跑通就行。

5. 本篇常见错排查

5.1 权重全为负或全为正

这通常是因为alpha平滑项设得太小,导致某些桶的原始分布概率接近 0,log_raw变成很大的负数,权重被拉爆。把alpha调到 1e-2 试试,或者对权重做截断。另一个原因是目标样本太少,分布学不准,建议目标样本至少几千条。

5.2 内存爆掉

1 亿条文档用稠密矩阵必爆。换成scipy.sparse.csr_matrix,并且分块处理:每读 10 万条算一次特征,追加到稀疏矩阵里。哈希桶数量也别盲目调大,10000 到 50000 之间通常够用。

5.3 筛选结果多样性差

如果你发现筛出来的文档高度重复,大概率是没用 Gumbel top-k 而是直接取了 top-k。直接取 top-k 会把权重最高的那批同质文档全选进来。确认你的采样函数加了 Gumbel 噪声,并且k不要设得远小于候选池。

5.4 TaoToken 请求报 401 或 404

401 一般是 key 没带对,检查Authorization头是不是Bearer开头。404 多半是 base_url 写错了,确认用的是 https://taotoken.net/api ,不要自己拼/v1/chat/completions之外的路径。如果模型名报错,去 https://taotoken.net/doc 看当前支持的模型列表。

5.5 KL reduction 为负

说明筛选后的分布反而离目标更远了。常见原因是特征空间选得不对,比如你只用了 unigram 而目标分布的关键信息在 bigram 里。把use_bigram=True打开,或者换更大的桶数量重试。也可能是目标样本本身和原始语料差异过大,导致重要性权重估计不稳定。

6. 把打分和对比接到统一通道上

DSIR 的统计部分全在本地,但验证环节的模型调用建议统一走 TaoToken。原因很实际:做数据选择时你往往要试好几个模型来交叉验证,如果每个模型单独配 key、单独改 base_url,脚本会迅速变成一团乱麻。统一通道之后,切换模型只是改一个字符串。

具体分流建议是这样:如果你只是做困惑度打分、领域命中率这类验证请求,用模型对话入口最直接,https://taotoken.net/api 配好 key 就能发请求;如果你要长期跑数据筛选流水线、反复调模型做对比,甚至把打分环节做成 Agent 自动迭代,那 Coding Plan 更合适,https://taotoken.net/coding-plan 里有针对长周期编码任务的配置;key 的生成和管理都在 https://taotoken.net/api-keys ,控制台 https://taotoken.net/console 可以看调用量。

接入文档在 https://taotoken.net/doc ,里面有完整的请求示例和参数说明。我的建议是先把第 3 节的本地筛选跑通,拿到selected.jsonl,再用第 4 节的脚本接上 TaoToken 做验证。这样即使模型调用出问题,你的数据筛选结果也是独立可复现的,不会因为接口波动丢掉整条流水线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 18:10:56

Spring Security 与 OAuth2 的关系:从过滤器链到 Token 校验的配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 18:10:45

AI-大模型-MCP实战指南:用TaoToken统一Key打通Cline与CC Switch配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 18:10:44

Lap 照片管理 ONNX Runtime 推理优化揭秘:2 线程调度背后的取舍

Lap 照片管理 ONNX Runtime 推理优化揭秘:2 线程调度背后的取舍 【免费下载链接】lap An offline-first photo manager for large local libraries 项目地址: https://gitcode.com/GitHub_Trending/lap3/lap Lap 是一款离线优先的本地照片管理工具&#xff0…

作者头像 李华