據(jù)增廣實戰(zhàn):基于Transformers的四種方法詳解)
1. 為什么要在 NLP 里做數(shù)據(jù)增廣先把話說在前面很多人一聽到“數(shù)據(jù)增廣”第一反應(yīng)是計算機視覺那套——翻轉(zhuǎn)、裁剪、調(diào)亮度一張圖變十張圖。這個思路放到 NLP 里天然就容易踩坑。原因很簡單圖像是連續(xù)的像素你翻轉(zhuǎn)一下貓還是那只貓語義沒變但文本是離散的 token一個詞替換錯了整句話的意思可能就反了。我見過不少剛接觸這個方向的人上來就抱著“增廣 復(fù)制粘貼 改幾個詞”的心態(tài)最后訓(xùn)練出來的模型在驗證集上漂漂亮亮一到真實場景就露餡。真正有效的 NLP 數(shù)據(jù)增廣不是靠量堆是靠保持語義不變的前提下制造輸入擾動。這篇文章要聊的是基于 Hugging Face Transformers 庫做 NLP 數(shù)據(jù)增廣的 4 種常用方法。為什么單拎 Transformers 出來說因為現(xiàn)在做 NLP 基本繞不開這個生態(tài)不管是加載預(yù)訓(xùn)練模型、做 token 級別的替換還是用生成模型做 paraphraseTransformers 都提供了現(xiàn)成的接口你不用自己從頭寫 tokenizer、不用手動實現(xiàn) attention mask省掉一大半臟活累活。適用的人也很明確你手上有一批標注好的文本數(shù)據(jù)想擴大訓(xùn)練集或者你的類別嚴重不平衡想給少數(shù)類補點樣本又或者你就是單純想讓模型更魯棒別一換說法就識別不出來。這篇文章會把這 4 種方法的原理、代碼、坑全部捋一遍照著抄就行。2. 動手之前先想清楚什么場景才真正需要增廣先潑一盆冷水不是所有項目都需要數(shù)據(jù)增廣。如果你的原始數(shù)據(jù)已經(jīng)有大幾萬條模型是分類任務(wù)且類別分布均衡那增廣帶來的收益可能非常有限反而增加訓(xùn)練時間和過擬合風(fēng)險。增廣真正有價值的場景我總結(jié)下來大概是下面這三類第一類是標注成本高的任務(wù)。比如命名實體識別、關(guān)系抽取、醫(yī)療文本分類這些任務(wù)找人來標一條數(shù)據(jù)的成本可能高達幾塊錢甚至幾十塊。你手里可能只有兩三千條標注數(shù)據(jù)想訓(xùn)練一個稍大點的預(yù)訓(xùn)練模型比如 BERT-base那數(shù)據(jù)量是明顯不夠的。這種時候增廣的價值就非常直接讓幾百條數(shù)據(jù)發(fā)揮幾千條的效果。第二類是類別不平衡明顯的場景。二分類任務(wù)里正樣本只占 5%負樣本占 95%常見做法是過采樣少數(shù)類但過采樣只是重復(fù)原樣本模型容易記住那幾條樣本的“長相”。增廣的價值在于換一種說法保留同樣的標簽讓模型學(xué)到的是類別的本質(zhì)特征而不是個別樣本的表面模式。第三類是魯棒性敏感的場景。例如在線客服系統(tǒng)里的意圖識別用戶說話千奇百怪“我東西沒到”和“我的包裹怎么還沒送來”和“物流是不是卡住了”在意圖層面是一回事在字面上差別巨大。如果訓(xùn)練數(shù)據(jù)里只有一種說法模型部署后就特別脆。增廣能讓你在訓(xùn)練時就模擬出這種表達多樣性。想清楚自己屬于哪一類再往下看方法的選型。要是你就是想“多點數(shù)據(jù)總覺得安心”那我建議先停一下拿一版增廣前和增廣后的模型跑一遍對比測試用數(shù)據(jù)說話別憑感覺決定。3. 四種常用增廣方法的原理與實操3.1 同義詞替換最基礎(chǔ)也最容易失控的方法同義詞替換的思路一句話就能說清把句子里的某些詞換成它的同義詞或近義詞得到一個語義一致的新句子。比如“這部電影真的非常精彩”把“精彩”換成“出色”或者“好看”訓(xùn)練語料就多了一種表達方式。這個方法源自 EDAEasy Data Augmentation那篇論文原文用的是 WordNet 同義詞庫但實操里遇到兩個問題一是 WordNet 對中文支持極差二是它不考慮上下文同一個詞在不同語境下意思可能完全不同“蘋果好吃”和“蘋果系統(tǒng)流暢”里的“蘋果”根本不是一個東西。一個更可靠的做法是借助預(yù)訓(xùn)練語言模型來選替換詞。Transformers 生態(tài)里的pipeline(fill-mask)就是干這個的。你給模型一個帶[MASK]的句子讓它預(yù)測這個位置最可能的幾個詞然后從候選項里挑一個和原詞含義相近的填回去。這里貼一段我實際用過的代碼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))注意一個細節(jié)中文文本必須先分詞BERT 的 tokenizer 會把“美味”拆成“美”和“味”這種子詞如果你直接對原始字符串做替換token 對不上出來的結(jié)果會很奇怪。我一開始就是偷懶直接傳整句結(jié)果fill-mask返回的都是單字根本沒法用。這個方法的主要問題在“度”的控制上。如果一句話里 1/3 的詞都被替換了句子可能變得很別扭如果只換一個詞增廣強度又不夠。業(yè)內(nèi)常見做法是根據(jù)句子長度按比例隨機選詞替換短句換 1 個中等句子換 1-2 個長句最多換 3 個不要貪多。3.2 隨機插入與隨機刪除成本最低、效果看天隨機插入和隨機刪除是 EDA 里的另外兩個基礎(chǔ)操作。隨機插入是在句子里隨機位置插入一個“合理的詞”隨機刪除是按一定概率刪掉句子里的詞。這兩個方法聽起來簡單到不值一提但實際用起來效果很有意思隨機刪除在多數(shù)分類任務(wù)上的表現(xiàn)往往比隨機插入更穩(wěn)。為什么因為刪除詞會迫使模型更關(guān)注剩余的關(guān)鍵詞類似于一種隱式的 dropout 正則。插入操作如果插入的詞語義關(guān)聯(lián)太弱反而會引入噪聲。當然插入時如果你用同義詞庫或語言模型挑選“和上下文相關(guān)性較高的詞”效果會更好。下面是一段配合 Transformers 生態(tài)實現(xiàn)的隨機刪除代碼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))實操經(jīng)驗里有一個重要提醒刪除概率不要超過 0.3。我做過一組對比實驗當刪除概率從 0.1 升到 0.3 時文本分類的 F1 值先升后降0.3 以上基本就是純粹破壞語義。而且要注意像“不”、“沒”、“非”這類否定詞盡量別刪。刪了“我不喜歡”變成“我喜歡”標簽反轉(zhuǎn)這就不是增廣是在制造臟數(shù)據(jù)。這也是為什么我強調(diào)要加一個“保護詞表”的思路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 回譯效果穩(wěn)定的“萬金油”回譯是 NLP 增廣里公認質(zhì)量較高的方法。思路很簡單把原句翻譯成另一種語言再翻譯回原語言。因為兩種語言的表達習(xí)慣不同回來的句子天然會換一種說法但大概率保留原意。比如“這家餐廳的菜品非常美味”先翻譯成英語變成 The food at this restaurant is very delicious再翻譯回中文可能變成“這家餐廳的食物很好吃”。瞧詞換了句式也換了但意思還在。這在 Transformers 生態(tài)里怎么實現(xiàn)兩個核心步驟用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))注意幾個坑第一回譯結(jié)果有一定隨機性。使用同樣的模型和輸入多次執(zhí)行結(jié)果可能不完全一樣beam search 等解碼策略會影響輸出。這不是 bug增廣要的就是這種多樣性你可以利用這一點生成多條增廣樣本。第二回譯需要過濾掉和原句完全相同的輸出。尤其是一些短句“你好”翻成英文再翻回來大概率還是“你好”這種就去掉避免重復(fù)樣本浪費訓(xùn)練開銷。第三不要選太長的句子直接回譯。句子超過 30 個詞時翻譯模型的錯誤率明顯上升回來可能面目全非。長句建議先按標點或子句切分分段回譯再拼接質(zhì)量會穩(wěn)很多。相比同義詞替換回譯有一個很大的優(yōu)勢它不只替換詞還會改變語序和句式結(jié)構(gòu)增廣出來的樣本多樣性明顯更高。缺點是慢依賴外部翻譯模型的推理時間。我的建議是增廣量大的場景提前把回譯結(jié)果批處理離線生成不要放在訓(xùn)練循環(huán)里實時做不然訓(xùn)練速度會讓你懷疑人生。3.4 基于生成模型的文本改寫最靈活但最“吃”資源的方法如果你已經(jīng)接受了 Transformers 生態(tài)大概率也聽說過 T5 或 GPT 這類生成模型。把它們用在增廣上核心思路是把原始句子輸入給生成模型用 prompt 讓模型生成一個“語義相同但表達不同”的句子。這個方法的靈活性是前面幾種沒法比的。同義詞替換只能改詞回譯只能依賴翻譯而生成式改寫可以改句式、改語序、改風(fēng)格、改詳略甚至可以把短句擴寫成帶更多上下文的長句。對于對話系統(tǒng)、生成式任務(wù)的數(shù)據(jù)擴充這個方法幾乎是唯一能夠提供高質(zhì)量多樣性的方案。以中文場景為例一個簡單的 prompt 寫法改寫下面的句子保持意思不變但換一種表達方式 原始句子這家餐廳的菜品非常美味。 改寫結(jié)果配合 Transformers 的生成 pipelinefrom transformers import pipeline generator pipeline( text2text-generation, modelmengzi-ftp/mengzi-t5-base ) def generate_rewrite(sentence): prompt ( 改寫下面的句子保持意思不變但換一種表達方式\n f原始句子{sentence}\n 改寫結(jié)果 ) 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))有一個參數(shù)值得專門說num_return_sequences。這是控制一次生成多少條改寫結(jié)果的關(guān)鍵。如果只取 1 條效果往往和原句差別不大取 3-5 條時多樣性會明顯提升但也會出現(xiàn)質(zhì)量參差不齊的情況。所以我建議生成之后加一道過濾規(guī)則和原句字符級相似度過高的不要完全驢唇不對馬嘴的不要。實際操作中我更喜歡把生成式改寫納入一個更完整的流程先用規(guī)則判斷句子長度太短的句子不適合改寫信息量太少生成容易跑偏用生成模型產(chǎn)出 5 條候選用一個小模型或者相似度算法比如句向量 cosine similarity篩出和原句最相近但不相同的 2-3 條人工抽檢一小批確認篩選閾值合理這個流程成本較高但效果是真的好。如果你的任務(wù)重要且項目周期允許值得投入。4. 四種方法放在一起怎么選聊到這兒很多人會問到底用哪個我的建議是別只用一種也別全用。先看一張對比表方法多樣性語義保持度速度實現(xiàn)成本適用場景同義詞替換低中快低分類任務(wù)、快速擴充隨機插入/刪除低中低極快極低魯棒性提升、簡單診斷回譯中高中慢中大部分通用 NLP 任務(wù)生成式改寫高高需審核慢高對話、生成、小樣本場景實際項目里有三種組合法我試下來最順手方案 A快速起步型。相同領(lǐng)域任務(wù)、時間緊迫直接用同義詞替換 隨機刪除批量生成成本最低。方案 B均衡穩(wěn)健型。大多數(shù)實際項目的最優(yōu)解。用回譯作為主增廣再疊加少量同義詞替換增加局部擾動質(zhì)量和成本都比較平衡。方案 C高質(zhì)量攻堅型。數(shù)據(jù)量極少但任務(wù)重要的場景。以生成式改寫為主配合回譯生成候選池再結(jié)合人工篩選或模型打分會話保留高質(zhì)量樣本。選擇的時候還要看你任務(wù)的下游模型。如果下游是 BERT 這類 encoder-only 模型生成式改寫帶來的收益最直接如果是 BART 這類 seq2seq 模型回譯的效果通常更穩(wěn)。沒有絕對的最優(yōu)每個項目跑一輪對比實驗讓指標說話。5. 實操中的常見坑與排查建議這里集中整理我在實操中踩過、以及幫別人排查過的典型問題每一條都對應(yīng)了具體的解決方法??右辉鰪V后的數(shù)據(jù)全部和原句重復(fù)這個問題最容易出現(xiàn)在回譯和生成式改寫里。原因通常是解碼參數(shù)里沒有引入隨機性比如do_sampleFalse、beam_search固定輸出每次都相同。解決辦法是設(shè)置do_sampleTrue并配合top_p或temperature隨機采樣??佣形姆衷~不一致導(dǎo)致 token 替換失敗我在同義詞替換那節(jié)提過中文用 BERT 相關(guān)模型時tokenizer 會做 subword 切分直接按空格切詞和模型內(nèi)部的 token 映射對不上。建議定義一個“詞邊界匹配”的邏輯先分好詞再逐個詞去模型里查tokenizer.convert_tokens_to_ids能查到且不是[UNK]的才作為候選替換位??尤鰪V樣本破壞標簽這個最隱蔽。比如否定詞被刪、專有名詞被替換成不相關(guān)詞都會讓樣本的實際語義偏離原始標簽。我的經(jīng)驗是增廣后的數(shù)據(jù)要做一個快速的一致性檢查比如用一個已經(jīng)訓(xùn)練好的模型對增廣樣本做預(yù)測如果預(yù)測標簽和原標簽不一致這批樣本要么丟棄要么人工復(fù)核。坑四增廣比例過大導(dǎo)致過擬合反而變嚴重增廣量不是越大越好。我從實踐中得到的一個參考范圍增廣樣本量控制在原始數(shù)據(jù)的 1 到 3 倍之間效果最理想。超過 5 倍模型開始出現(xiàn)明顯的重復(fù)樣本記憶現(xiàn)象??游迳墒礁膶懏a(chǎn)生與原文相似度極低的“自由發(fā)揮”樣本常見于 prompt 寫得過于開放的時候。給模型的指令越具體它越不容易跑偏。如果還是出現(xiàn)離譜樣本就加一層相似度過濾用句向量計算和原句的 cosine 相似度低于 0.7 的樣本直接丟棄??恿刈g引入不符合目標領(lǐng)域表達的洋腔洋調(diào)比如劇本對話、口語化文本回譯后可能變得書面化。處理方式是在回譯后再用一個領(lǐng)域內(nèi)的小模型做風(fēng)格遷移或人工抽檢。短期看成本增加長期看數(shù)據(jù)質(zhì)量高了模型迭代省下的時間遠大于這部分投入。6. 增廣質(zhì)量怎么評估很多人增廣完直接合并數(shù)據(jù)訓(xùn)練不檢查質(zhì)量這是不對的。我建議至少做三件事。第一手動抽檢。從增廣后的數(shù)據(jù)里隨機抽 50-100 條人工看一遍語義是否保持、表達是否自然。這個成本很小但能發(fā)現(xiàn)很多規(guī)則無法量化的“別扭感”問題。第二加一個小規(guī)模測試集。從原始數(shù)據(jù)中留出原始標注測試集不動分別用“原數(shù)據(jù)訓(xùn)練”和“原數(shù)據(jù)增廣訓(xùn)練”跑一遍測試集指標對比準確率/F1 的提升幅度。如果增廣后指標反而下降說明增廣方案有問題別急著微調(diào)模型先回頭查數(shù)據(jù)。第三做消融實驗。如果你用了兩種以上增廣方法可以分別嘗試“只有 A 增廣”“只有 B 增廣”“AB 增廣”三種配置直觀看到每種方法的貢獻度。這能幫你后續(xù)省下很多不必要的增廣成本。7. 最后想說的幾句實話數(shù)據(jù)增廣這件事看著簡單做起來全是細節(jié)。同一套方法有人用來提升 2 個點有人把模型搞崩了。差別往往不在代碼而在篩選規(guī)則、在質(zhì)量把控、在適可而止。我個人在做了幾輪增廣實驗后最深的體會是增廣不是技術(shù)問題是“度”的把握問題。方法只是工具真正值錢的是你判斷一條增廣樣本“該不該留”的能力。這種能力沒有捷徑就是多跑實驗、多抽檢、多對比。如果你也是第一次在自己的項目里引入 NLP 數(shù)據(jù)增廣我建議從小處著手先用回譯或同義詞替換在小數(shù)據(jù)集上跑一個完整的對比實驗看看增益到底有多大再決定要不要上生成式改寫這種重方法。畢竟增廣本質(zhì)上是為你服務(wù)不是讓你為增廣服務(wù)。