
1. 为什么要在 NLP 里做数据增广先把话说在前面很多人一听到“数据增广”第一反应是计算机视觉那套——翻转、裁剪、调亮度一张图变十张图。这个思路放到 NLP 里天然就容易踩坑。原因很简单图像是连续的像素你翻转一下猫还是那只猫语义没变但文本是离散的 token一个词替换错了整句话的意思可能就反了。我见过不少刚接触这个方向的人上来就抱着“增广 复制粘贴 改几个词”的心态最后训练出来的模型在验证集上漂漂亮亮一到真实场景就露馅。真正有效的 NLP 数据增广不是靠量堆是靠保持语义不变的前提下制造输入扰动。这篇文章要聊的是基于 Hugging Face Transformers 库做 NLP 数据增广的 4 种常用方法。为什么单拎 Transformers 出来说因为现在做 NLP 基本绕不开这个生态不管是加载预训练模型、做 token 级别的替换还是用生成模型做 paraphraseTransformers 都提供了现成的接口你不用自己从头写 tokenizer、不用手动实现 attention mask省掉一大半脏活累活。适用的人也很明确你手上有一批标注好的文本数据想扩大训练集或者你的类别严重不平衡想给少数类补点样本又或者你就是单纯想让模型更鲁棒别一换说法就识别不出来。这篇文章会把这 4 种方法的原理、代码、坑全部捋一遍照着抄就行。2. 动手之前先想清楚什么场景才真正需要增广先泼一盆冷水不是所有项目都需要数据增广。如果你的原始数据已经有大几万条模型是分类任务且类别分布均衡那增广带来的收益可能非常有限反而增加训练时间和过拟合风险。增广真正有价值的场景我总结下来大概是下面这三类第一类是标注成本高的任务。比如命名实体识别、关系抽取、医疗文本分类这些任务找人来标一条数据的成本可能高达几块钱甚至几十块。你手里可能只有两三千条标注数据想训练一个稍大点的预训练模型比如 BERT-base那数据量是明显不够的。这种时候增广的价值就非常直接让几百条数据发挥几千条的效果。第二类是类别不平衡明显的场景。二分类任务里正样本只占 5%负样本占 95%常见做法是过采样少数类但过采样只是重复原样本模型容易记住那几条样本的“长相”。增广的价值在于换一种说法保留同样的标签让模型学到的是类别的本质特征而不是个别样本的表面模式。第三类是鲁棒性敏感的场景。例如在线客服系统里的意图识别用户说话千奇百怪“我东西没到”和“我的包裹怎么还没送来”和“物流是不是卡住了”在意图层面是一回事在字面上差别巨大。如果训练数据里只有一种说法模型部署后就特别脆。增广能让你在训练时就模拟出这种表达多样性。想清楚自己属于哪一类再往下看方法的选型。要是你就是想“多点数据总觉得安心”那我建议先停一下拿一版增广前和增广后的模型跑一遍对比测试用数据说话别凭感觉决定。3. 四种常用增广方法的原理与实操3.1 同义词替换最基础也最容易失控的方法同义词替换的思路一句话就能说清把句子里的某些词换成它的同义词或近义词得到一个语义一致的新句子。比如“这部电影真的非常精彩”把“精彩”换成“出色”或者“好看”训练语料就多了一种表达方式。这个方法源自 EDAEasy Data Augmentation那篇论文原文用的是 WordNet 同义词库但实操里遇到两个问题一是 WordNet 对中文支持极差二是它不考虑上下文同一个词在不同语境下意思可能完全不同“苹果好吃”和“苹果系统流畅”里的“苹果”根本不是一个东西。一个更可靠的做法是借助预训练语言模型来选替换词。Transformers 生态里的pipeline(fill-mask)就是干这个的。你给模型一个带[MASK]的句子让它预测这个位置最可能的几个词然后从候选项里挑一个和原词含义相近的填回去。这里贴一段我实际用过的代码from transformers import pipeline unmasker pipeline( fill-mask, modelbert-base-chinese, top_k20 ) def synonym_replace(sentence, mask_index0): tokens sentence.split( ) target_word tokens[mask_index] masked_sentence .join( tokens[:mask_index] [[MASK]] tokens[mask_index 1:] ) candidates unmasker(masked_sentence) # 过滤掉和原词完全一样的剩下的按概率取 replaced [item[token_str] for item in candidates if item[token_str] ! target_word] if not replaced: return None tokens[mask_index] replaced[0] return .join(tokens) sentence 这家 餐厅 的 菜品 非常 美味 print(synonym_replace(sentence, mask_index5))注意一个细节中文文本必须先分词BERT 的 tokenizer 会把“美味”拆成“美”和“味”这种子词如果你直接对原始字符串做替换token 对不上出来的结果会很奇怪。我一开始就是偷懒直接传整句结果fill-mask返回的都是单字根本没法用。这个方法的主要问题在“度”的控制上。如果一句话里 1/3 的词都被替换了句子可能变得很别扭如果只换一个词增广强度又不够。业内常见做法是根据句子长度按比例随机选词替换短句换 1 个中等句子换 1-2 个长句最多换 3 个不要贪多。3.2 随机插入与随机删除成本最低、效果看天随机插入和随机删除是 EDA 里的另外两个基础操作。随机插入是在句子里随机位置插入一个“合理的词”随机删除是按一定概率删掉句子里的词。这两个方法听起来简单到不值一提但实际用起来效果很有意思随机删除在多数分类任务上的表现往往比随机插入更稳。为什么因为删除词会迫使模型更关注剩余的关键词类似于一种隐式的 dropout 正则。插入操作如果插入的词语义关联太弱反而会引入噪声。当然插入时如果你用同义词库或语言模型挑选“和上下文相关性较高的词”效果会更好。下面是一段配合 Transformers 生态实现的随机删除代码import random def random_delete(sentence, delete_prob0.1): words sentence.split( ) # 保证至少保留一个词别删空了 if len(words) 1: return sentence new_words [ word for word in words if random.random() delete_prob ] if not new_words: # 万一全删完了随机保一个 new_words [random.choice(words)] return .join(new_words) sentence 这家 餐厅 的 菜品 非常 美味 for _ in range(3): print(random_delete(sentence, delete_prob0.2))实操经验里有一个重要提醒删除概率不要超过 0.3。我做过一组对比实验当删除概率从 0.1 升到 0.3 时文本分类的 F1 值先升后降0.3 以上基本就是纯粹破坏语义。而且要注意像“不”、“没”、“非”这类否定词尽量别删。删了“我不喜欢”变成“我喜欢”标签反转这就不是增广是在制造脏数据。这也是为什么我强调要加一个“保护词表”的思路protected_words {不, 没, 没有, 非, 无, 很, 非常} def safe_random_delete(sentence, delete_prob0.1): words sentence.split( ) if len(words) 1: return sentence new_words [] for word in words: if word in protected_words: new_words.append(word) continue if random.random() delete_prob: new_words.append(word) if not new_words: new_words [random.choice(words)] return .join(new_words)3.3 回译效果稳定的“万金油”回译是 NLP 增广里公认质量较高的方法。思路很简单把原句翻译成另一种语言再翻译回原语言。因为两种语言的表达习惯不同回来的句子天然会换一种说法但大概率保留原意。比如“这家餐厅的菜品非常美味”先翻译成英语变成 The food at this restaurant is very delicious再翻译回中文可能变成“这家餐厅的食物很好吃”。瞧词换了句式也换了但意思还在。这在 Transformers 生态里怎么实现两个核心步骤用pipeline(translation, modelHelsinki-NLP/opus-mt-zh-en)做中译英用pipeline(translation, modelHelsinki-NLP/opus-mt-en-zh)做英译中代码参考from transformers import pipeline zh_to_en pipeline( translation, modelHelsinki-NLP/opus-mt-zh-en ) en_to_zh pipeline( translation, modelHelsinki-NLP/opus-mt-en-zh ) def back_translate(sentence): en_text zh_to_en(sentence)[0][translation_text] zh_text en_to_zh(en_text)[0][translation_text] return zh_text sentence 这家餐厅的菜品非常美味 for i in range(3): print(back_translate(sentence))注意几个坑第一回译结果有一定随机性。使用同样的模型和输入多次执行结果可能不完全一样beam search 等解码策略会影响输出。这不是 bug增广要的就是这种多样性你可以利用这一点生成多条增广样本。第二回译需要过滤掉和原句完全相同的输出。尤其是一些短句“你好”翻成英文再翻回来大概率还是“你好”这种就去掉避免重复样本浪费训练开销。第三不要选太长的句子直接回译。句子超过 30 个词时翻译模型的错误率明显上升回来可能面目全非。长句建议先按标点或子句切分分段回译再拼接质量会稳很多。相比同义词替换回译有一个很大的优势它不只替换词还会改变语序和句式结构增广出来的样本多样性明显更高。缺点是慢依赖外部翻译模型的推理时间。我的建议是增广量大的场景提前把回译结果批处理离线生成不要放在训练循环里实时做不然训练速度会让你怀疑人生。3.4 基于生成模型的文本改写最灵活但最“吃”资源的方法如果你已经接受了 Transformers 生态大概率也听说过 T5 或 GPT 这类生成模型。把它们用在增广上核心思路是把原始句子输入给生成模型用 prompt 让模型生成一个“语义相同但表达不同”的句子。这个方法的灵活性是前面几种没法比的。同义词替换只能改词回译只能依赖翻译而生成式改写可以改句式、改语序、改风格、改详略甚至可以把短句扩写成带更多上下文的长句。对于对话系统、生成式任务的数据扩充这个方法几乎是唯一能够提供高质量多样性的方案。以中文场景为例一个简单的 prompt 写法改写下面的句子保持意思不变但换一种表达方式 原始句子这家餐厅的菜品非常美味。 改写结果配合 Transformers 的生成 pipelinefrom transformers import pipeline generator pipeline( text2text-generation, modelmengzi-ftp/mengzi-t5-base ) def generate_rewrite(sentence): prompt ( 改写下面的句子保持意思不变但换一种表达方式\n f原始句子{sentence}\n 改写结果 ) result generator( prompt, max_length64, num_return_sequences3, do_sampleTrue, top_p0.9 ) return [item[generated_text] for item in result] sentence 这家餐厅的菜品非常美味 print(generate_rewrite(sentence))有一个参数值得专门说num_return_sequences。这是控制一次生成多少条改写结果的关键。如果只取 1 条效果往往和原句差别不大取 3-5 条时多样性会明显提升但也会出现质量参差不齐的情况。所以我建议生成之后加一道过滤规则和原句字符级相似度过高的不要完全驴唇不对马嘴的不要。实际操作中我更喜欢把生成式改写纳入一个更完整的流程先用规则判断句子长度太短的句子不适合改写信息量太少生成容易跑偏用生成模型产出 5 条候选用一个小模型或者相似度算法比如句向量 cosine similarity筛出和原句最相近但不相同的 2-3 条人工抽检一小批确认筛选阈值合理这个流程成本较高但效果是真的好。如果你的任务重要且项目周期允许值得投入。4. 四种方法放在一起怎么选聊到这儿很多人会问到底用哪个我的建议是别只用一种也别全用。先看一张对比表方法多样性语义保持度速度实现成本适用场景同义词替换低中快低分类任务、快速扩充随机插入/删除低中低极快极低鲁棒性提升、简单诊断回译中高中慢中大部分通用 NLP 任务生成式改写高高需审核慢高对话、生成、小样本场景实际项目里有三种组合法我试下来最顺手方案 A快速起步型。相同领域任务、时间紧迫直接用同义词替换 随机删除批量生成成本最低。方案 B均衡稳健型。大多数实际项目的最优解。用回译作为主增广再叠加少量同义词替换增加局部扰动质量和成本都比较平衡。方案 C高质量攻坚型。数据量极少但任务重要的场景。以生成式改写为主配合回译生成候选池再结合人工筛选或模型打分会话保留高质量样本。选择的时候还要看你任务的下游模型。如果下游是 BERT 这类 encoder-only 模型生成式改写带来的收益最直接如果是 BART 这类 seq2seq 模型回译的效果通常更稳。没有绝对的最优每个项目跑一轮对比实验让指标说话。5. 实操中的常见坑与排查建议这里集中整理我在实操中踩过、以及帮别人排查过的典型问题每一条都对应了具体的解决方法。坑一增广后的数据全部和原句重复这个问题最容易出现在回译和生成式改写里。原因通常是解码参数里没有引入随机性比如do_sampleFalse、beam_search固定输出每次都相同。解决办法是设置do_sampleTrue并配合top_p或temperature随机采样。坑二中文分词不一致导致 token 替换失败我在同义词替换那节提过中文用 BERT 相关模型时tokenizer 会做 subword 切分直接按空格切词和模型内部的 token 映射对不上。建议定义一个“词边界匹配”的逻辑先分好词再逐个词去模型里查tokenizer.convert_tokens_to_ids能查到且不是[UNK]的才作为候选替换位。坑三增广样本破坏标签这个最隐蔽。比如否定词被删、专有名词被替换成不相关词都会让样本的实际语义偏离原始标签。我的经验是增广后的数据要做一个快速的一致性检查比如用一个已经训练好的模型对增广样本做预测如果预测标签和原标签不一致这批样本要么丢弃要么人工复核。坑四增广比例过大导致过拟合反而变严重增广量不是越大越好。我从实践中得到的一个参考范围增广样本量控制在原始数据的 1 到 3 倍之间效果最理想。超过 5 倍模型开始出现明显的重复样本记忆现象。坑五生成式改写产生与原文相似度极低的“自由发挥”样本常见于 prompt 写得过于开放的时候。给模型的指令越具体它越不容易跑偏。如果还是出现离谱样本就加一层相似度过滤用句向量计算和原句的 cosine 相似度低于 0.7 的样本直接丢弃。坑六回译引入不符合目标领域表达的洋腔洋调比如剧本对话、口语化文本回译后可能变得书面化。处理方式是在回译后再用一个领域内的小模型做风格迁移或人工抽检。短期看成本增加长期看数据质量高了模型迭代省下的时间远大于这部分投入。6. 增广质量怎么评估很多人增广完直接合并数据训练不检查质量这是不对的。我建议至少做三件事。第一手动抽检。从增广后的数据里随机抽 50-100 条人工看一遍语义是否保持、表达是否自然。这个成本很小但能发现很多规则无法量化的“别扭感”问题。第二加一个小规模测试集。从原始数据中留出原始标注测试集不动分别用“原数据训练”和“原数据增广训练”跑一遍测试集指标对比准确率/F1 的提升幅度。如果增广后指标反而下降说明增广方案有问题别急着微调模型先回头查数据。第三做消融实验。如果你用了两种以上增广方法可以分别尝试“只有 A 增广”“只有 B 增广”“AB 增广”三种配置直观看到每种方法的贡献度。这能帮你后续省下很多不必要的增广成本。7. 最后想说的几句实话数据增广这件事看着简单做起来全是细节。同一套方法有人用来提升 2 个点有人把模型搞崩了。差别往往不在代码而在筛选规则、在质量把控、在适可而止。我个人在做了几轮增广实验后最深的体会是增广不是技术问题是“度”的把握问题。方法只是工具真正值钱的是你判断一条增广样本“该不该留”的能力。这种能力没有捷径就是多跑实验、多抽检、多对比。如果你也是第一次在自己的项目里引入 NLP 数据增广我建议从小处着手先用回译或同义词替换在小数据集上跑一个完整的对比实验看看增益到底有多大再决定要不要上生成式改写这种重方法。毕竟增广本质上是为你服务不是让你为增广服务。