文本基础增强:五招搞定小样本困境
做 NLP 的朋友应该都有体会——标注数据永远不够用。我入行那会儿,接过一个情感分析项目,甲方只给了 500 条标注数据。500 条啊,连 BERT 都喂不饱。后来怎么解决的?就是靠今天要讲的这五招文本增强技巧。
说白了,文本增强就是「无中生有」。在不改变语义的前提下,把一条数据变成多条。你想想看,这比重新标注划算太多了。
核心思路:通过微小的文本扰动,生成语义不变的新样本。既扩充了数据量,又提升了模型的鲁棒性。
1. 同义词替换:最基础也最稳妥
这个方法最简单。找到句子中的非停用词,用同义词替换掉。比如「这家餐厅的菜品很好」→「这家餐厅的菜肴很棒」。
我在项目中遇到过一个问题:直接用 WordNet 替换,结果把「苹果」替换成了「公司名」那个意思。嗯,这里要注意——一定要考虑词义消歧。
我的经验:替换比例控制在 20%-30% 比较安全。替换太多,句子会变得不自然。我一般每句话只替换 1-2 个词。
import nltk
from nltk.corpus import wordnet
import random
def synonym_replacement(text, replace_ratio=0.2):
words = text.split()
# 只替换非停用词
stop_words = set(nltk.corpus.stopwords.words('chinese'))
candidate_indices = [i for i, w in enumerate(words) if w not in stop_words]
n_replace = max(1, int(len(candidate_indices) * replace_ratio))
selected = random.sample(candidate_indices, n_replace)
for i in selected:
synonyms = wordnet.synsets(words[i], lang='cmn')
if synonyms:
# 取第一个同义词
new_word = synonyms[0].lemmas()[0].name()
words[i] = new_word
return ' '.join(words)
# 示例
text = "这家餐厅的菜品味道很好"
print(synonym_replacement(text))
# 可能输出:这家餐厅的菜肴味道很好
2. 随机插入:给模型加点「噪音」
随机插入就是在句子中随机位置插入一个词。这个词可以是同义词,也可以是随机词。目的是让模型学会忽略无关信息。
说白了,就是训练模型「抓重点」的能力。你想想看,真实场景中用户输入经常带一些废话,模型得能扛得住。
我曾经踩过的坑:插入太多无关词,导致语义完全变了。比如「今天天气真好」插成「今天天气真好苹果」——这就离谱了。建议插入的词和上下文相关,或者干脆用停用词填充。
def random_insertion(text, insert_ratio=0.1):
words = text.split()
n_insert = max(1, int(len(words) * insert_ratio))
for _ in range(n_insert):
# 随机选一个位置插入
pos = random.randint(0, len(words))
# 从原文中随机选一个词插入
insert_word = random.choice(words)
words.insert(pos, insert_word)
return ' '.join(words)
# 示例
text = "这部电影非常精彩"
print(random_insertion(text))
# 可能输出:这部电影非常非常精彩
3. 随机交换:打乱局部顺序
随机交换就是随机挑两个词,交换它们的位置。注意,不是全局打乱,而是局部微调。
我习惯只交换相邻的词,或者间隔不超过 2 个位置的词。为什么?因为交换太远,句子就读不通了。比如「我爱北京天安门」交换成「天安门爱北京我」——这谁看得懂?
| 交换策略 | 示例(原句:我喜欢在周末去公园跑步) | 效果评价 |
|---|---|---|
| 相邻交换 | 我喜欢在周末去跑步公园 | ✅ 可接受,语义基本保留 |
| 间隔 2 词交换 | 我喜欢公园周末去在跑步 | ⚠️ 有点别扭,但还能理解 |
| 全局随机交换 | 跑步周末去公园在我喜欢 | ❌ 语义完全混乱 |
def random_swap(text, swap_ratio=0.1):
words = text.split()
n_swap = max(1, int(len(words) * swap_ratio))
for _ in range(n_swap):
i, j = random.sample(range(len(words)), 2)
# 只交换距离不超过 3 的词
if abs(i - j) <= 3:
words[i], words[j] = words[j], words[i]
return ' '.join(words)
# 示例
text = "我每天早上都会喝一杯咖啡"
print(random_swap(text))
# 可能输出:我每天早上都会喝咖啡一杯
4. 随机删除:教模型「脑补」
这个方法很暴力——随机删掉句子中的一些词。目的是让模型学会根据上下文推断缺失的信息。
我在做文本分类时发现,随机删除对提升模型泛化能力特别有效。尤其是当你的数据中有很多「废话」时,模型学会了抓关键词。
参数建议:删除比例控制在 10%-15%。我试过 30% 的删除率,结果模型直接懵了——输入「我 吃饭」它都不知道该输出什么。
def random_deletion(text, delete_ratio=0.1):
words = text.split()
# 至少保留一个词
if len(words) == 1:
return text
remaining = [w for w in words if random.random() > delete_ratio]
# 防止删光
if not remaining:
return random.choice(words)
return ' '.join(remaining)
# 示例
text = "今天天气非常好适合出去散步"
print(random_deletion(text))
# 可能输出:今天天气适合出去散步
5. 回译:最「高级」的增强方法
回译是我个人最喜欢的方法。流程很简单:中文 → 英文 → 中文。因为翻译过程会引入自然的语序变化和同义表达,效果比前几种方法都好。
举个例子:「我很喜欢这部电影」→ 英译「I really like this movie」→ 回译「我真的很喜欢这部电影」。你看,多了个「真的」,语气更强了,但语义完全没变。
我曾经用回译给一个医疗问答项目做数据增强,效果立竿见影——F1 分数直接提升了 3 个点。不过要注意,回译依赖翻译 API,有成本问题。
# 使用 transformers 库做回译
from transformers import pipeline
# 加载翻译 pipeline
zh_to_en = pipeline("translation", model="Helsinki-NLP/opus-mt-zh-en")
en_to_zh = pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh")
def back_translation(text):
# 中文 → 英文
en_text = zh_to_en(text)[0]['translation_text']
# 英文 → 中文
zh_text = en_to_zh(en_text)[0]['translation_text']
return zh_text
# 示例
text = "这家酒店的性价比很高"
print(back_translation(text))
# 可能输出:这家酒店性价比很高
实战建议:这五种方法不要单独用。我一般会组合使用:先做同义词替换,再做随机删除,最后用回译。这样一条数据能变出 5-10 条,数据量瞬间就上来了。
好了,文本增强的基础方法就这些。记住一个原则:增强后的数据一定要人工抽检。我见过太多人一股脑跑完代码,结果生成的数据里全是乱码——那还不如不用。