據(jù)增強實踐)
簡介面向中文NLP數(shù)據(jù)增強場景的實踐項目利用faiss索引與Chinese SimBERT向量化實現(xiàn)最近鄰樣本擴充主要解決標(biāo)注數(shù)據(jù)不足時如何高效補充相似文本的問題。資源共6個文件壓縮包55.06MB包含3個csv數(shù)據(jù)集文件、1個yaml配置文件、1個faiss索引文件和1個Python數(shù)據(jù)增強腳本覆蓋從配置、索引構(gòu)建到數(shù)據(jù)輸出的完整鏈路。已有197人學(xué)習(xí)。項目基于Python 3.7、bert4keras 0.11.3與faiss-gpu 1.7.2思路清晰先由無標(biāo)簽語料構(gòu)建faiss索引再為帶標(biāo)簽樣本檢索最近鄰最終生成含相似文本、相似度與標(biāo)簽的擴充數(shù)據(jù)。腳本與配置可直接參考或二次開發(fā)有助于快速落地基于向量檢索的中文數(shù)據(jù)增強流程適合NLP項目實踐與算法調(diào)參場景。1. 數(shù)據(jù)增強為什么不先想到生成模型而先動 faiss 做最近鄰做過中文 NLP 分類任務(wù)的工程師應(yīng)該都有同感標(biāo)注數(shù)據(jù)永遠(yuǎn)不夠尤其是帶 label 的業(yè)務(wù)語料攢幾個月也就幾千條。常見的增強思路是回譯、EDA 同義詞替換但對中文而言回譯依賴翻譯接口批量跑又慢又貴EDA 的詞表替換在領(lǐng)域語料上經(jīng)常把糖尿病換成糖果病這種離譜結(jié)果。有一種更省心的做法是先用 faiss 建一個語義查找表把已有標(biāo)注數(shù)據(jù)用 Chinese SimBERT 向量化同一 batch 里做最近鄰查詢再從鄰居句子里產(chǎn)生新樣本。這條路的好處是全程本地運行不依賴外部接口而且最近鄰本身就能反映數(shù)據(jù)分布密度適合把稀疏類別的樣本往稠密區(qū)域補。這個方向適合誰你手里已經(jīng)有一批干凈的中文標(biāo)注數(shù)據(jù)想在不改模型結(jié)構(gòu)的前提下擴充訓(xùn)練集或者你剛接手一個冷啟動項目只有幾百條高質(zhì)量正例需要快速讓分類器不至于一訓(xùn)練就過擬合。本文會用可復(fù)現(xiàn)的做法講清楚為什么選 SimBERT 做向量化、faiss 的索引和查詢參數(shù)怎么定、最近鄰能生成哪幾種增強樣本以及最容易讓你翻車的五個細(xì)節(jié)。2. 向量化與索引選型chinese simbert 配 faiss動手前先想明白三件事2.1 為什么用 chinese simbert而不是 BGE、M3E 或通用 BERT提到中文句向量現(xiàn)在社區(qū)里更常聊的是 BGE 和 M3E它們在語義相似度榜單上表現(xiàn)不錯。但做數(shù)據(jù)增強有一個容易被忽略的點我們需要的是局部語義一致而不僅僅是全局相似度排名。SimBERT 是蘇劍林基于 BERT 做的一個句向量模型它的訓(xùn)練目標(biāo)里包含了句對相似和句對生成兩個任務(wù)。什么意思呢它的向量空間在同類句子上擠得更緊而且相鄰句子往往在字面上也有可替換的關(guān)系。比如這款手機電池耐用和這款機器續(xù)航時間長在 SimBERT 的向量空間里距離比較近而在通用 BERT 的向量空間里可能被拉到話題相關(guān)但不字面相近的區(qū)域。做增強時我們恰恰希望鄰居是字面可替換的而不是話題沾邊但結(jié)構(gòu)完全不同的。另外SimBERT 的語義表示對中文的字粒度更敏感。因為是全詞掩碼WWM訓(xùn)練出來的它對哪 / 里 / 的 / 服 / 務(wù)這種分詞邊界模糊的中文短語有更好的穩(wěn)定性。實際使用中我用通用 BERT 做向量化喂給 faiss 查出來的鄰居經(jīng)常出現(xiàn)用戶反饋和客服反饋這類同義不同結(jié)構(gòu)的樣本拼接出來的句子生硬換成 SimBERT 之后鄰居句子的結(jié)構(gòu)相似度明顯提高。使用上的注意點SimBERT 輸出的是 768 維句向量如果你在 transformers 里加載記得取出 CLS 位置的輸出或者對非 padding 位置做均值池化不要直接用 last_hidden_state 的高維張量丟給 faiss那樣索引維度會變成序列長度乘 768既慢又沒有意義。2.2 faiss 索引類型怎么選IndexFlatIP 與 IndexIVFFlat 的取舍faiss 官方文檔把索引分得很細(xì)但做中文文本最近鄰常見做法其實就是在兩種里挑精確索引和倒排索引。精確索引用 IndexFlatIP。它做的事情簡單粗暴把所有向量全部平鋪在內(nèi)存里每次查詢都做一次全量內(nèi)積計算。優(yōu)點是召回率 100%不需要調(diào)參缺點是數(shù)據(jù)量上了 10 萬條之后單條查詢耗時可能會到十幾毫秒批量增強時要循環(huán)幾萬次整體時間不可接受。倒排索引用 IndexIVFFlat。它先對全量向量做聚類nlist 參數(shù)控制簇的個數(shù)查詢時只在最近的幾個簇里搜索nprobe 參數(shù)控制查詢的簇數(shù)。實測中5 萬條樣本、nlist100、nprobe10 的情況下召回率能保持在 95% 以上單條查詢耗時降到 1 毫秒以內(nèi)。做增強場景如果你的數(shù)據(jù)集在 1 萬條以下直接用 IndexFlatIP 最省心超過這個量級帶上 IndexIVFFlat。還有一個折中方案是把向量先降維到 256 維再建索引但中文語義表示降維后丟失的信息不容易找回所以我不推薦為了速度去動向量維度。索引類型適合數(shù)據(jù)量參數(shù)召回率典型耗時5 萬條CPUIndexFlatIP 1 萬條無100%3-5 ms/條IndexIVFFlat1 萬 - 100 萬條nlist, nprobe95%nprobe 足夠時 1 ms/條IndexHNSWFlat向量維度高、查詢頻繁M, efSearch高快但建索引較慢2.3 建索引之前必須先做的兩個預(yù)處理向量正則化與 ID 映射faiss 做內(nèi)積相似度對向量模長非常敏感。如果你沒有把句向量做 L2 歸一化那么長句子的向量模長普遍比短句子大查詢結(jié)果會被句子長度這個無關(guān)因素主導(dǎo)。這是做最近鄰增強時最容易踩的坑。建議在向量化之后統(tǒng)一做一次歸一化把內(nèi)積相似度變成余弦相似度。另外faiss 的索引內(nèi)部只認(rèn)它自己分配的序號0 到 N-1。如果你有一條原始數(shù)據(jù)的 ID 是 10086寫入 faiss 時它在索引里的位置可能是 38。查詢返回的 38 這個值你需要通過一個映射表還原成原始 ID。最穩(wěn)妥的做法是使用 IDMap 類型的索引比如 IndexIDMap2 包住 IndexFlatIP直接把原始數(shù)據(jù) ID 傳進(jìn)去。3. 從原始中文文本到最近鄰列表完整的數(shù)據(jù)管線腳本3.1 文本清洗與 chinese simbert 向量化的最小實現(xiàn)先交代一下我的做法拿到原始數(shù)據(jù)后我不會立刻向量化而是先做一遍輕量清洗。清洗規(guī)則按你的業(yè)務(wù)數(shù)據(jù)來常見做法包括統(tǒng)一全半角、去掉 HTML 標(biāo)簽、規(guī)范重復(fù)標(biāo)點連續(xù)三個以上的逗號 / 句號壓縮成一個、以及根據(jù)業(yè)務(wù)詞表過濾無效字符。清洗完的文本統(tǒng)一寫入一個列表然后開始向量化。下面是最小可運行的代碼import numpy as np from transformers import AutoTokenizer, AutoModel import torch # 載入中文 SimBERT 模型與分詞器 model_name 你的 SimBERT 權(quán)重路徑或 Hugging Face 模型名 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) model.eval() texts [這款手機電池耐用, 這個機器續(xù)航時間長, 屏幕碎了售后不管] encoded tokenizer( texts, max_length64, paddingTrue, truncationTrue, return_tensorspt ) with torch.no_grad(): outputs model(**encoded) # 取 CLS 位置的向量作為句向量表示 cls_vectors outputs.last_hidden_state[:, 0, :].numpy() # L2 歸一化把內(nèi)積變成余弦相似度 norms np.linalg.norm(cls_vectors, axis1, keepdimsTrue) cls_vectors cls_vectors / norms print(cls_vectors.shape)代碼邏輯說明我先用批量化方式一次處理多個文本避免單條循環(huán)過慢。max_length64控制截斷閾值如果你的句子普遍較長可以調(diào)整到 128但 SimBERT 的位置編碼上限是 512超過部分會被截斷后面避坑章節(jié)會細(xì)說。取 CLS 向量是因為 SimBERT 在訓(xùn)練時用 CLS 輸出做相似度損失它比均值池化更貼合這個模型本身的語義空間。參數(shù)說明paddingTrue會讓一個 batch 內(nèi)所有句子補齊到最長但請留意padding 出來的[PAD]token 也會參與注意力計算我建議你改造為 Attention Mask 讓模型忽略 padding 位上面的最小實現(xiàn)沒有體現(xiàn)正式代碼里要加上attention_maskencoded[attention_mask]否則短句的向量會被無效位稀釋。3.2 用 faiss 建索引并完成最近鄰查詢直接返回中文候選向量化完成后下一步就是讓 faiss 上場。完整流程先把向量寫入索引再對每一條數(shù)據(jù)查詢它的最近鄰。import faiss # 把所有向量寫入 IndexFlatIP并綁定原始 ID dim cls_vectors.shape[1] index faiss.IndexFlatIP(dim) index faiss.IndexIDMap2(index) # 原始 ID 列表長度必須與向量數(shù)量一致 original_ids np.array([0, 1, 2], dtypenp.int64) index.add_with_ids(cls_vectors, original_ids) # 查詢對每一條數(shù)據(jù)找 top-k 最近鄰 k 5 scores, neighbor_ids index.search(cls_vectors, k) # 打印第 0 條樣本的鄰居 for idx, score in zip(neighbor_ids[0], scores[0]): print(f原始ID: {idx}, 相似度: {score:.4f}, 文本: {texts[int(idx)]})代碼邏輯說明IndexIDMap2包住IndexFlatIP后add_with_ids會把原始 ID 和向量綁定查詢返回的neighbor_ids就直接是原始 ID省去手動映射的麻煩。這里有個細(xì)節(jié)search的返回結(jié)果里會包含自己因為自己是自己的最近鄰所以k5實際上只帶來 4 個有效鄰居業(yè)務(wù)代碼里建議把k設(shè)置成 6取時跳過第一個。參數(shù)說明search函數(shù)第二個參數(shù)代表返回最近鄰個數(shù)。對于數(shù)據(jù)增強我一般設(shè)k10或k20太少候選不夠用太多后面的鄰居相似度太低垃圾樣本會污染訓(xùn)練集。另外 faiss 的 inner product 分?jǐn)?shù)范圍是 [-1, 1]歸一化后但 Cosine 相似度的數(shù)值不能直接當(dāng)置信度用建議只做排序不設(shè)絕對閾值。3.3 三個必調(diào)參數(shù)閾值、候選數(shù)量、標(biāo)簽平衡拿到鄰居列表之后實際增強效果取決于三個參數(shù)。第一個是相似度閾值。我一般設(shè)置一個保留閾值比如 0.75只有相似度高于這個值的鄰居才參與增強。閾值設(shè)太低會把語義不相關(guān)的樣本硬拉進(jìn)來模型被噪聲帶偏設(shè)太高可用的鄰居太少增強等于沒有。建議先在驗證集上做一個快速掃描從 0.6 到 0.9每 0.05 試一次用增強后的數(shù)據(jù)訓(xùn)練一個小模型看指標(biāo)變化。第二個是每類樣本的候選數(shù)量。如果某個類別的樣本很少比如只有 100 條但它的鄰居特別多不要一口氣全部用完。我的習(xí)慣是控制增強倍數(shù)比如 3 倍到 5 倍防止某類樣本在訓(xùn)練集里占比被抬得太高。第三個是標(biāo)簽平衡策略。最近鄰增強天然會加劇頭部類別越來越多的馬太效應(yīng)因為樣本多的類別向量密集更容易被查出來。對于尾部類別我會單獨做一次反向查詢把尾部類別的每一條樣本作為 query從全量數(shù)據(jù)里找最近鄰然后只保留同標(biāo)簽或低置信度的鄰居這樣能定向補足稀疏類別。4. 最近鄰如何變成新的中文訓(xùn)練數(shù)據(jù)三種可直接落地的策略4.1 詞級替換增強把句子中的低頻詞換成鄰近樣本的高頻詞拿到最近鄰之后最保守的增強方式是詞級替換。做法是對樣本 A找到它的最近鄰樣本 B兩個句子分詞后把 A 中一個低頻詞替換成 B 中對應(yīng)語義槽位的詞。這個做法比 EDA 的隨機同義詞替換更安全因為替換詞來自真實語料不是外部詞表。import jieba def word_level_augment(text_a, text_b, top_k1): 把文本 A 中的低頻詞替換為近鄰文本 B 中的詞。 words_a list(jieba.cut(text_a)) words_b list(jieba.cut(text_b)) # 這里簡化處理假設(shè) A 和 B 長度接近按位置對齊 if len(words_a) ! len(words_b): return text_a new_words [] for i, (wa, wb) in enumerate(zip(words_a, words_b)): # 低頻詞在語料中出現(xiàn)次數(shù)低于 3 的詞才有替換價值 if word_count.get(wa, 0) 3 and wa ! wb: new_words.append(wb) else: new_words.append(wa) return .join(new_words)代碼邏輯說明這個實現(xiàn)是簡化版本核心是低頻詞 近鄰高頻詞的組合。word_count是預(yù)先統(tǒng)計的全局詞頻表你需要在自己語料上統(tǒng)計一遍。注意兩個句子的長度不一定相等實際落地時我會先用動態(tài)規(guī)劃做一個最小編輯距離對齊讓替換位置更準(zhǔn)確。參數(shù)說明top_k控制替換次數(shù)設(shè)置為 1 表示每句話最多替換一個詞。不要貪多替換太多句子原意就散了。替換后需要人工檢查一條如果替換出來的詞明顯不通順比如把手機替換成充電器說明這個近鄰不適合詞級增強丟棄即可。4.2 句子級拼接增強前句加后句配合標(biāo)簽平滑詞級替換只適合短句對于長文本或評論型數(shù)據(jù)更有效的是句子級拼接。做法很簡單把樣本 A 和它的最近鄰 B要求同標(biāo)簽拼接成一條新樣本。這里的關(guān)鍵是拼接方式。我試過直接在原句上追加效果不好因為兩個句子之間沒有銜接詞模型會把它們當(dāng)成兩句無關(guān)的話。常見做法是加一個過渡符號比如 [SEP] 或中文的。def pair_concat_augment(text_a, text_b, label, label_smooth0.1): 近鄰句子拼接增強返回新文本與平滑后的標(biāo)簽分布。 new_text text_a text_b # 標(biāo)簽平滑給錯誤類別一點點概率防止模型過度自信 smooth_label [label_smooth / (num_classes - 1)] * num_classes smooth_label[label] 1 - label_smooth return new_text, np.array(smooth_label)代碼邏輯說明拼接增強的收益來自語義互補。分類任務(wù)里模型經(jīng)常因為關(guān)鍵詞缺失而誤判比如售后不管這條樣本沒有提到客服但它的近鄰里有客服響應(yīng)慢拼接后模型能同時看到兩個特征。標(biāo)簽平滑是為了防止模型記住出現(xiàn)某詞必然屬于某類這種捷徑平滑系數(shù) 0.1 是經(jīng)驗值如果你的分類類別數(shù)較少二分類可以下調(diào)到 0.05。參數(shù)說明拼接時要注意總長度兩個 64 字的句子拼起來是 128 字如果模型 max_length 是 64后半個句子會被截掉。我在這個環(huán)節(jié)會重新分詞并做長度判斷超長時優(yōu)先保留前半句加后半句的開頭幾個詞而不是直接丟棄。4.3 偽標(biāo)簽增強用近鄰?fù)镀苯o未標(biāo)注語料打標(biāo)第三種策略適合你手里有一大批無標(biāo)簽數(shù)據(jù)的情況。做法把所有無標(biāo)簽數(shù)據(jù)也用 SimBERT 向量化查詢它們在已標(biāo)注數(shù)據(jù)中的最近鄰用鄰居標(biāo)簽投票決定這條數(shù)據(jù)的偽標(biāo)簽。def pseudo_label_by_neighbors(neighbor_labels, neighbor_scores, threshold0.8): 根據(jù)最近鄰標(biāo)簽投票生成偽標(biāo)簽。 neighbor_labels: 鄰居的標(biāo)簽列表 neighbor_scores: 鄰居的相似度分?jǐn)?shù) label_score {} for lab, score in zip(neighbor_labels, neighbor_scores): if score threshold: continue label_score[lab] label_score.get(lab, 0) score if not label_score: return None, 0.0 # 取加權(quán)得分最高的標(biāo)簽 pseudo_label max(label_score, keylabel_score.get) confidence label_score[pseudo_label] / sum(label_score.values()) return pseudo_label, confidence代碼邏輯說明這里把相似度分?jǐn)?shù)直接當(dāng)作投票權(quán)重比單純數(shù)票數(shù)更合理。比如一個鄰居相似度 0.9、標(biāo)簽 A另一個鄰居相似度 0.7、標(biāo)簽 BA 的加權(quán)得分明顯更高。threshold0.8是一個起始值實際業(yè)務(wù)中如果無標(biāo)簽數(shù)據(jù)分布很散0.8 會過濾掉大部分樣本可以放寬到 0.7但要接受一些噪聲。參數(shù)說明偽標(biāo)簽增強有個前提——已標(biāo)注數(shù)據(jù)的分布要能覆蓋無標(biāo)簽數(shù)據(jù)的主體。如果無標(biāo)簽數(shù)據(jù)里有一類樣本在你的標(biāo)注集里完全沒有近鄰打出來的偽標(biāo)簽就是瞎猜寧可丟棄這批數(shù)據(jù)。另外偽標(biāo)簽數(shù)據(jù)參與訓(xùn)練時建議把 loss 權(quán)重設(shè)置為 0.5 倍讓真實標(biāo)簽的主導(dǎo)地位不被沖掉。5. 避坑與排查faiss 返回結(jié)果與 SimBERT 向量碰到的五個典型問題5.1 faiss 返回的是內(nèi)部序號不是原始索引號現(xiàn)象查詢結(jié)束后拿返回的數(shù)字去原始數(shù)據(jù)里取文本取出來的內(nèi)容張冠李戴。原因faiss 的 IndexFlatIP 默認(rèn)按添加順序分配內(nèi)部序號如果中間有數(shù)據(jù)被刪除或過濾過內(nèi)部序號和原始 ID 就錯位了。還有一個常見場景用add_with_ids傳了原始 ID 但建的是普通索引faiss 會忽略你傳入的 ID仍然按自增序號處理。解決統(tǒng)一使用faiss.IndexIDMap2包裝類別索引。注意是IndexIDMap2而不是IndexIDMap前者內(nèi)部用哈希表支持按 ID 刪除單條向量后者不支持。建索引和查詢的代碼都要走 IDMap 的接口避免中途混用。5.2 長文本被截斷向量化時把語義丟了現(xiàn)象兩條明顯意思相反的句子這個產(chǎn)品我喜歡 vs 這個產(chǎn)品我不喜歡向量距離非常近查詢出的最近鄰基本都是錯誤標(biāo)簽。原因SimBERT 的位置編碼上限是 512但你設(shè)置max_length64或 128 后超過長度的文本被硬截斷。如果一條文本的核心表達(dá)恰好落在截斷區(qū)域比如不喜歡出現(xiàn)在第 70 個 token 位置截斷后剩下的部分全是無關(guān)內(nèi)容。解決先統(tǒng)計文本長度分布把max_length設(shè)置為覆蓋 95% 樣本的長度而不是拍腦袋定 64。如果業(yè)務(wù)數(shù)據(jù)普遍較長可以考慮分段向量化再取平均但注意 SimBERT 的句向量是整體語義分段平均對短文本聚合任務(wù)有效對判斷整體情感不一定有效需要做 A/B 驗證。5.3 向量沒做 L2 歸一化內(nèi)積被句子長度帶偏現(xiàn)象查詢出來的最近鄰永遠(yuǎn)是長句子相似度分?jǐn)?shù)普遍偏高短句子幾乎不會被召回。原因faiss 的 IndexFlatIP 算的是內(nèi)積內(nèi)積 模長 × 余弦相似度。長句子向量模長天然偏大即使方向和查詢向量不接近內(nèi)積也能排到前面。解決向量化之后統(tǒng)一做 L2 歸一化。上面代碼里已經(jīng)寫了norms np.linalg.norm(...)這一步批量處理時不要遺漏。有個細(xì)節(jié)如果你之后又對向量做過投影或降維需要再歸一化一次。5.4 增強后標(biāo)簽翻轉(zhuǎn)模型分?jǐn)?shù)更差了現(xiàn)象加了增強數(shù)據(jù)之后模型在驗證集上的 F1 反而掉了 2 個點。原因最常見的兩個原因。一是閾值設(shè)得太低拉進(jìn)來的鄰居語義不相關(guān)比如退款慢和收貨慢被當(dāng)成同義樣本拼接生成的新文本語義反轉(zhuǎn)二是拼接增強時兩個鄰居標(biāo)簽雖然一致但一個是手機屏幕碎一個是電腦藍(lán)屏拼接后模型學(xué)到了屏幕碎 藍(lán)屏 售后問題這種不穩(wěn)定的特征。解決把增強后的數(shù)據(jù)單獨存一份抽樣 20 條人工看一遍重點看拼接處是否產(chǎn)生歧義。同時把相似度閾值往高調(diào)直到人工檢查的合格率超過 90%。另外詞級替換和拼接增強不要疊加使用疊加后的噪聲不是一加一等于二而是指數(shù)級放大。5.5 nprobe 和 nlist 調(diào)參失控耗時與召回率同時變差現(xiàn)象換成 IndexIVFFlat 之后查詢速度確實快了但返回的最近鄰明顯不合理或者查詢時間反而變長了還沒精確索引快。原因nlist決定聚成多少個簇nprobe決定查詢時搜索幾個簇。nlist太小每個簇里樣本太多nprobe也覆蓋不了全局分布nlist太大一個簇里就幾條向量nprobe要設(shè)得很高才能保證召回速度就退化了。這個參數(shù)組合在很多工程里被當(dāng)成玄學(xué)調(diào)。解決給出一個可直接復(fù)制的經(jīng)驗起點nlist int(sqrt(num_samples))大約在 100 到 500 之間nprobe從 10 開始逐步翻倍觀察召回率變化。驗證召回率有一個廉價方法用 IndexFlatIP 在小批量數(shù)據(jù)上先算出精確 top-k再和 IndexIVFFlat 的結(jié)果對比準(zhǔn)確率超過 95% 的 nprobe 值就是你的候選參數(shù)。6. 增強效果不靠感覺批量查詢腳本、質(zhì)量驗證參數(shù)與一個持久習(xí)慣當(dāng)你把上面一套管線跑通后接下來要做的是讓增強流程可復(fù)現(xiàn)、可驗證、可回滾。我強烈建議你把向量化 - 建索引 - 查鄰居 - 生成增強樣本整個過程封裝成一個腳本輸入是原始數(shù)據(jù)文件和參數(shù)配置輸出是增強后的訓(xùn)練集。def batch_search(index, vectors, k10, batch_size1024): 分批查詢最近鄰避免一次性查詢占用過多內(nèi)存。 all_scores [] all_neighbors [] for start in range(0, len(vectors), batch_size): end min(start batch_size, len(vectors)) batch_vectors vectors[start:end] scores, neighbors index.search(batch_vectors, k) all_scores.append(scores) all_neighbors.append(neighbors) return np.vstack(all_scores), np.vstack(all_neighbors)這個腳本的價值是處理大數(shù)據(jù)量。5 萬條向量一次性交給 faiss 的search機器內(nèi)存不夠會直接卡死分批 1024 條查詢耗時幾乎不變但內(nèi)存占用下降一個量級。batch_size參數(shù)可以按你的內(nèi)存調(diào)整CPU 環(huán)境下 2048 也沒問題。質(zhì)量驗證是我的習(xí)慣動作分四步走。第一步增強前后訓(xùn)練集的大小、正負(fù)樣本比例、平均文本長度用表格打印出來快速發(fā)現(xiàn)是否引入分布偏移。第二步:抽樣 30 條人工檢查記錄通順且標(biāo)簽正確的比例。第三步用增強前的數(shù)據(jù)訓(xùn)練一個基線模型用增強后的數(shù)據(jù)訓(xùn)練一個對比模型在同一個驗證集上看 F1 變化。第四步檢查增強數(shù)據(jù)里是否有重復(fù)樣本faiss 查詢出的鄰居如果和原樣本完全相同說明語料里有重復(fù)文本需要先去重再做增強。我踩過最深的坑是對著增強數(shù)據(jù)的量興奮不已卻沒管質(zhì)量驗證結(jié)果模型被拼接樣本帶偏最后花了兩天排查才發(fā)現(xiàn)是偽標(biāo)簽測試集污染導(dǎo)致評估分?jǐn)?shù)虛高。后來我養(yǎng)成了一個習(xí)慣每一批增強出來的數(shù)據(jù)文件名里都要帶上參數(shù) hash比如k10_seed42_thresh0.8_v1.jsonl模型表現(xiàn)異常時能快速回滾到上一版參數(shù)。這個習(xí)慣幫我避開了很多了無頭緒的模型調(diào)參困境。最后這個方案最值得投入的地方不是 faiss 本身而是用真實數(shù)據(jù)分布去指導(dǎo)增強這個思路。生成模型需要精心調(diào) prompt回譯需要外部接口而最近鄰增強只需要你自己的數(shù)據(jù)再加一個本地向量檢索庫它對冷啟動和稀疏類別尤其友好。希望這份落地步驟能幫你少走我走過的彎路。本文還有配套的精品資源點擊獲取