練全流程:從XML清洗到詞向量調(diào)優(yōu))
簡介基于深度學(xué)習(xí)的維基中文語料詞向量模型項目包面向自然語言處理初學(xué)者與相關(guān)課程設(shè)計人群以實踐方式演示從原始語料到最終詞向量的完整處理流程重點突出動手操作與工程步驟。壓縮包共8個文件核心為4個可運行腳本分別承擔(dān)語料清洗轉(zhuǎn)換、結(jié)巴分詞、Word2Vec模型訓(xùn)練和詞向量匹配測試另含1份設(shè)計報告、1份說明文檔、1份命令參考文本及許可文件整體約967KB。設(shè)計報告與源碼逐步對應(yīng)覆蓋開發(fā)環(huán)境準(zhǔn)備、數(shù)據(jù)獲取與預(yù)處理、模型構(gòu)建以及模型測試等階段并給出關(guān)鍵參數(shù)與運行順序便于讀者復(fù)現(xiàn)訓(xùn)練過程。腳本與報告相互配合可支撐課程設(shè)計文檔撰寫和實驗復(fù)現(xiàn)幫助理解中文分詞、詞向量訓(xùn)練、相似詞匹配等基礎(chǔ)操作。已有661人學(xué)習(xí)下載適合作為NLP入門實踐模板或課設(shè)參考。1. 從Wiki中文語料到Word2Vec一條可復(fù)現(xiàn)的語義向量構(gòu)建鏈路一個相對反直覺的事實是word2vec 本身并不復(fù)雜甚至核心代碼在 gensim 里就是一行Word2Vec(sentences, vector_size300)但真正決定詞向量質(zhì)量的環(huán)節(jié)80% 發(fā)生在模型訓(xùn)練之前——語料清洗和分詞。這也是很多人在 Wiki 中文語料上跑完訓(xùn)練后發(fā)現(xiàn)相似詞結(jié)果混亂、類比推理完全失效的根本原因。本文以“基于深度學(xué)習(xí)的 Wiki 中文語料詞 word2vec 向量模型”這套源碼為主線拆解從原始 Wiki 導(dǎo)出 XML 到最終詞向量模型的全流程覆蓋數(shù)據(jù)預(yù)處理、jieba 分詞集成、gensim 模型構(gòu)建、參數(shù)調(diào)優(yōu)和相似詞評測適合正在做自然語言處理課程設(shè)計或第一次接觸詞向量的開發(fā)者。整個項目由五個 Python 腳本串聯(lián)分別對應(yīng)語料解析、分詞、訓(xùn)練、模型匹配和設(shè)計文檔是一份可以直接照著跑通的完整工程。2. 語料準(zhǔn)備與預(yù)處理從Wiki XML到可訓(xùn)練的分詞文本2.1 為什么要選Wiki中文語料而不是爬蟲抓取Wiki 中文語料在 NLP 教學(xué)和實驗場景里幾乎是默認(rèn)選擇。相比自建爬蟲語料Wiki 的官方導(dǎo)出文件是結(jié)構(gòu)化的 XML每個頁面條目帶有標(biāo)題、正文、命名空間等元信息天然適合做清洗和解析。另一個關(guān)鍵優(yōu)勢是許可協(xié)議清晰知識共享許可協(xié)議允許在注明來源的前提下進(jìn)行學(xué)術(shù)研究和模型訓(xùn)練這比從新聞網(wǎng)站抓取內(nèi)容再訓(xùn)練的合規(guī)成本低得多。同時 Wiki 語料的覆蓋面足夠廣——科技、歷史、地理、人物、文化等主題均衡分布訓(xùn)練出的詞向量在通用語義相似度任務(wù)上有更好的泛化表現(xiàn)。但 Wiki 導(dǎo)出版本也有明顯的噪音問題。下載下來的 XML 文件往往體積在 1.5GB 到 2GB 之間其中有大量 MediaWiki 語法標(biāo)記比如[[鏈接]]、{{模板}}、ref參考文獻(xiàn)/ref、{| classwikitable這類表格語法。如果直接把這些內(nèi)容送去分詞詞表會被無意義的符號污染訓(xùn)練出的向量在“語義”這件事上的可信度會大打折扣。所以第一件事不是分詞而是把 Wiki 的標(biāo)記語言從正文里剝離出來。2.2 解析Wiki XML1_process.py的正文抽取實現(xiàn)項目中的1_process.py處理的是這一階段的核心邏輯讀取 Wiki 的 XML dump 文件抽取每個頁面的正文文本并過濾掉 MediaWiki 的模板、鏈接和特殊符號。常用的庫是gensim.corpora.wikicorpus提供的WikiCorpus迭代器它會自動處理大部分 MediaWiki 語法但默認(rèn)輸出的是經(jīng)過處理的詞列表。若想保留更多的控制權(quán)可以自己寫 XML 解析用xml.etree.ElementTree遍歷page節(jié)點提取revisiontext字段再用正則清理內(nèi)部標(biāo)記。一個可落地的解析邏輯如下import xml.etree.ElementTree as ET import re def extract_wiki_text(xml_path, output_path): tree ET.iterparse(xml_path, events(end,)) with open(output_path, w, encodingutf-8) as f: for event, elem in tree: if elem.tag {http://www.mediawiki.org/xml/export-0.10/}page: text_elem elem.find(.//{http://www.mediawiki.org/xml/export-0.10/}text) if text_elem is not None and text_elem.text: text clean_wiki_markup(text_elem.text) f.write(text \n) elem.clear() def clean_wiki_markup(text): text re.sub(r\[\[(?:[^|\]]*\|)?([^\]])\]\], r\1, text) # 鏈接 text re.sub(r\{\{.*?\}\}, , text, flagsre.DOTALL) # 模板 text re.sub(rref.*?/ref, , text, flagsre.DOTALL) # 引用 text re.sub(r[^], , text) # 其他標(biāo)簽 return text.strip()這里iterparse是增量解析不會把整個 XML 文件加載進(jìn)內(nèi)存elem.clear()及時釋放已處理節(jié)點這是處理 GB 級 XML 的標(biāo)準(zhǔn)做法。clean_wiki_markup中的正則分別處理三件事把[[目標(biāo)|顯示文本]]替換為顯示文本刪除{{模板}}塊移除ref引用。正則匹配對嵌套結(jié)構(gòu)并不完美但 Wiki 模板的嵌套深度在絕大多數(shù)情況下有限這種規(guī)模的處理在實際運行中足夠穩(wěn)定。XML 解析是 I/O 密集任務(wù)單進(jìn)程運行大約需要 20 到 40 分鐘取決于磁盤速度。如果你的機(jī)器有多核可以用multiprocessing按頁面分片并行處理但這會引入一個復(fù)雜度——XML 不是按行分割的文件分片位置需要落在page和/page之間。實際工程中多數(shù)人選擇先單線程跑完解析把時間節(jié)省用在后續(xù)分詞階段因為那里才是 CPU 密集區(qū)。2.3 中文分詞集成2_jieba_participle.py與停用詞過濾2.3.1 為什么中文必須分詞而英文不需要英文文本天然以空格分隔單詞text.split()就能得到基本合格的 token 序列。中文沒有這種天然邊界“深度學(xué)習(xí)”是一個詞還是一個短語如果不分詞按單字切分“深”“度”“學(xué)”“習(xí)”會被當(dāng)成四個獨立的 token它們的上下文向量會被各種無關(guān)句子污染最終學(xué)到的“語義”是字級別的共現(xiàn)統(tǒng)計而不是詞級別的語義表示。所以中文詞向量訓(xùn)練的前置步驟一定是分詞。這里用的是 jieba它在工程界的普及率高、接口簡單且支持用戶自定義詞典是中文 NLP 入門階段最務(wù)實的選擇。2.3.2 分詞代碼與參數(shù)細(xì)節(jié)以下是對應(yīng)2_jieba_participle.py的核心實現(xiàn)import jieba import jieba.analyse STOP_WORDS set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: STOP_WORDS.add(line.strip()) def segment_line(line): words jieba.lcut(line) return [w for w in words if w not in STOP_WORDS and w.strip()] def process_corpus(input_path, output_path): with open(input_path, r, encodingutf-8) as fin, \ open(output_path, w, encodingutf-8) as fout: for line in fin: seg_words segment_line(line) if seg_words: fout.write( .join(seg_words) \n)jieba.lcut返回一個 list而不是生成器這樣在segment_line里可以直接做列表推導(dǎo)過濾停用詞。w.strip()是為了去掉可能殘存的空白字符避免訓(xùn)練時出現(xiàn)空字符串 token。STOP_WORDS的加載先于分詞循環(huán)避免在每行處理時重復(fù)讀取文件。停用詞表建議合并多個來源哈工大停用詞表、百度停用詞表再手動補充 Wiki 語料里特有的高頻噪音詞如“參考文獻(xiàn)”“參見”“外部鏈接”等。關(guān)于 jieba 分詞有一個值得注意的工程陷阱默認(rèn)分詞模式會盡可能多地切分詞匯但在“模型訓(xùn)練”這個場景下過度切分如把“神經(jīng)網(wǎng)絡(luò)”切成“神經(jīng)”和“網(wǎng)絡(luò)”會讓 word2vec 學(xué)到錯誤的上下文共現(xiàn)關(guān)系。如果你的語料里專有名詞很多可以把它們加進(jìn)用戶詞典讓 jieba 不再錯誤切分。例如TensorFlow、PyTorch、機(jī)器學(xué)習(xí)這些詞如果默認(rèn)詞典沒有就會出現(xiàn)切成碎塊的情況。2.4 語料清洗的輸出與格式約束整個預(yù)處理階段結(jié)束后得到的是一個單一的文本文件每一行是一個句子或者一個段落片段詞與詞之間用空格分隔標(biāo)點符號被過濾。這個格式是 gensim 的LineSentence類可以直接消費的形式不需要再做轉(zhuǎn)換。一個常見的誤區(qū)是把整行文本直接傳給Word2Vec的sentences參數(shù)然后傳入的是字符串而不是分詞后的列表。gensim 的接口實際上要求一個可迭代的、每一項是詞列表的對象。如果直接傳字符串每個字符會被當(dāng)成一個獨立的 token等于做了字符級建模效果等同于不區(qū)分詞邊界語義信息幾乎丟失。所以分詞輸出文件里的“每行一句、空格分詞”這一格式約定是整個預(yù)處理鏈條的關(guān)鍵一環(huán)。3. 模型構(gòu)建Word2Vec 的訓(xùn)練原理與 gensim 實戰(zhàn)3.1 Word2Vec 的兩種架構(gòu)選型CBOW 與 Skip-gramWord2Vec 的核心思想是用一個淺層神經(jīng)網(wǎng)絡(luò)把詞映射到低維稠密向量空間使得語義相近的詞在向量空間中的距離也相近。具體實現(xiàn)有兩種架構(gòu)CBOW 用上下文詞預(yù)測中心詞適合小數(shù)據(jù)集Skip-gram 用中心詞預(yù)測上下文詞對低頻詞更友好。在 Wiki 中文語料這種大規(guī)模、長尾分布明顯的場景下Skip-gram 通常表現(xiàn)更好但訓(xùn)練時間也相應(yīng)更長。gensim 中通過sg參數(shù)控制sg1表示 Skip-gramsg0表示 CBOW。在項目源碼中模型構(gòu)建部分的默認(rèn)選擇就是 Skip-gram這也是工業(yè)界處理大規(guī)模語料時的主流選擇——雖然訓(xùn)練慢一點但結(jié)果的語義質(zhì)量明顯更穩(wěn)。3.2 gensim 核心參數(shù)詳解與調(diào)優(yōu)路徑3_train_word2vec_model.py中的核心代碼大致如下from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 讀取預(yù)處理后的語料構(gòu)建詞表并訓(xùn)練 sentences LineSentence(wiki_seg.txt) model Word2Vec( sentences, vector_size300, window5, min_count5, sg1, hs0, negative10, sample1e-4, workers4, epochs5, seed42 ) model.save(wiki_word2vec.model)3.2.1 參數(shù)含義與經(jīng)驗取值參數(shù)值含義vector_size300詞向量維度越大表達(dá)力越強(qiáng)但需要更多語料支撐window5上下文窗口大小即中心詞前后各看幾個詞min_count5頻次低于該值的詞被丟棄減少低頻噪音sg11 為 Skip-gram0 為 CBOWhs00 為負(fù)采樣1 為層次 Softmaxnegative10負(fù)采樣時抽取的負(fù)樣本數(shù)sample1e-4高頻詞下采樣閾值抑制“的”“了”等詞的影響workers4并行訓(xùn)練線程數(shù)不宜超過 CPU 物理核心數(shù)epochs5迭代輪數(shù)過少欠擬合過多容易對噪聲過擬合vector_size的選擇需要結(jié)合語料體量。Wiki 中文語料預(yù)處理后大約有幾千萬到上億詞的規(guī)模300 維是安全和效果兼顧的選擇。window則影響語義的粒度窗口越大詞向量越偏向主題/領(lǐng)域相似窗口越小越偏向語法/近義相似。做相似詞評測時發(fā)現(xiàn)問題后優(yōu)先調(diào)這個值而不是盲目增加epochs。3.2.2 訓(xùn)練耗時與日志解析gensim訓(xùn)練時會打印進(jìn)度日志包含已處理的詞數(shù)、預(yù)計剩余時間等信息。開啟日志的方式很簡單import logging logging.basicConfig(format%(asctime)s : %(levelname)s : %(message)s, levellogging.INFO)日志中值得關(guān)注的字段是“PROGRESS: at 50.00% examples”和“estimated time: xx minutes”。如果estimated time從一開始就在指數(shù)式增長說明語料讀取環(huán)節(jié)有問題LineSentence把整個文件讀成了一大段而不是逐行迭代。檢查語料文件的換行符是否為\n確保每行一個句子。4. 模型測試與語義匹配4_model_match.py的相似詞檢索4.1 加載模型與基礎(chǔ)相似度查詢訓(xùn)練完成后需要驗證模型效果。4_model_match.py的核心功能是用訓(xùn)練好的模型做詞語相似度匹配這是評估詞向量質(zhì)量最直接的方式。加載模型的代碼很簡單from gensim.models import Word2Vec model Word2Vec.load(wiki_word2vec.model)核心的相似詞查詢接口是most_similar它返回與目標(biāo)詞余弦相似度最高的若干詞# 返回與“深度學(xué)習(xí)”最相似的詞 similar_words model.wv.most_similar(深度學(xué)習(xí), topn10) for word, score in similar_words: print(f{word}: {score:.4f})4.1.1 參數(shù)說明most_similar的參數(shù)很直觀第一個位置傳目標(biāo)詞topn控制返回的結(jié)果數(shù)量。返回值是(詞, 相似度)的元組列表按相似度降序排列。這個接口的底層實現(xiàn)是取目標(biāo)詞的向量計算它與詞表中所有詞的余弦相似度再排序。如果目標(biāo)詞不在詞表中會拋出KeyError所以查詢前要加上word in model.wv的判斷if word in model.wv: sims model.wv.most_similar(word, topn10) else: print(f{word} 不在詞表中)4.2 詞匯類比驗證向量空間中的語義關(guān)系除了相似詞word2vec 的經(jīng)典能力是詞匯類比analogy reasoning即“國王 - 男人 女人 ≈ 女王”。這在 gensim 里對應(yīng)most_similar的positive和negative參數(shù)# 語義類比中國 - 北京 東京 ≈ 日本 result model.wv.most_similar(positive[中國, 東京], negative[北京], topn3)positive是要相加的詞negative是要相減的詞。模型把對應(yīng)向量做加減運算后在詞表中找最接近結(jié)果向量的詞。這個操作可以驗證訓(xùn)練出的向量是否捕捉到了語義規(guī)律。一個成熟的詞向量模型在這類測試上會有不錯的表現(xiàn)——比如“足球 - 踢 打”能得到“籃球”這類結(jié)果。如果類比結(jié)果混亂優(yōu)先檢查語料質(zhì)量和分詞準(zhǔn)確性。很常見的情況是某個詞的詞頻剛好踩在min_count5的邊緣向量訓(xùn)練不充分導(dǎo)致類比時貢獻(xiàn)了噪音??梢园裮in_count調(diào)高到 10 或 20雖然損失部分低頻詞但整體向量質(zhì)量更穩(wěn)定。4.3 語義相似度計算直接比較兩個詞的向量距離除了檢索最相似詞有時候需要給出兩個詞之間的量化相似度??梢杂胹imilarity方法sim model.wv.similarity(深度學(xué)習(xí), 神經(jīng)網(wǎng)絡(luò)) print(f余弦相似度: {sim:.4f})內(nèi)部實現(xiàn)是計算兩個向量之間的余弦相似度取值范圍在 [-1, 1] 之間。對于語義無關(guān)的詞結(jié)果應(yīng)該在 0 附近語義正相關(guān)的詞結(jié)果為正反義詞或語義對立的詞結(jié)果可能為負(fù)。這個接口在聚類和文本分類任務(wù)里經(jīng)常被用到比如計算用戶查詢詞與候選標(biāo)簽之間的語義距離。4.4 模型輸出的邊界問題OOV 與多義詞在處理真實文本時會遇到訓(xùn)練語料里沒有出現(xiàn)過的詞即 out-of-vocabulary 問題。gensim 的基礎(chǔ)Word2Vec模型無法處理 OOV 詞最直接的方案是檢查語料覆蓋度——如果 OOV 率過高說明訓(xùn)練語料與使用場景存在領(lǐng)域偏移。針對這個問題工業(yè)界會使用 fastText 的子詞信息來處理但那是另一個模型了。另一個值得知道的現(xiàn)象是多義詞問題。word2vec 給每個詞只生成一個固定向量無法區(qū)分“蘋果”的水果義和公司義。比如most_similar(蘋果)的結(jié)果可能會同時出現(xiàn)“香蕉”和“華為”因為兩種語義在訓(xùn)練語料中的上下文是混在一起的。這屬于靜態(tài)詞向量的固有局限解決思路是引入 ELMo、BERT 這類上下文相關(guān)的動態(tài)表示超出本項目范圍但不妨礙理解這個模型的能力邊界。4.5 模型導(dǎo)出與部署格式訓(xùn)練完成后模型除了用model.save()保存完整對象還可以把詞向量單獨導(dǎo)出為文本格式方便其他程序加載model.wv.save_word2vec_format(wiki_word2vec.vec, binaryFalse)save_word2vec_format導(dǎo)出的格式是第一行是“詞表大小 向量維度”之后每行是“詞 對應(yīng)的 300 個浮點數(shù)”。這是業(yè)界通用的詞向量交換格式PyTorch 的torchtext、TensorFlow 的 NLP 工具都可以直接讀取。binaryFalse表示導(dǎo)出為純文本方便直接用文本編輯器查看如果需要更緊湊的二進(jìn)制格式可以設(shè)為True。5. 進(jìn)階增量訓(xùn)練與向量相似度的實際應(yīng)用5.1 在已有模型上繼續(xù)訓(xùn)練新語料實際場景中通用 Wiki 語料訓(xùn)練的模型可能缺少特定領(lǐng)域的詞匯。比如做法律文本分析時Wiki 里的法律術(shù)語覆蓋不足。一個常見做法是在已訓(xùn)練模型上做增量訓(xùn)練gensim 支持直接加載后繼續(xù)訓(xùn)練新語料model Word2Vec.load(wiki_word2vec.model) new_sentences LineSentence(legal_corpus_seg.txt) model.build_vocab(new_sentences, updateTrue) model.train(new_sentences, total_examplesmodel.corpus_count, epochs5)build_vocab的updateTrue是關(guān)鍵——它告訴模型在原有詞表基礎(chǔ)上擴(kuò)展新詞而不是重置詞表。增量訓(xùn)練后原有詞向量會被微調(diào)新詞的向量會基于新語料學(xué)習(xí)。要注意的是增量訓(xùn)練如果新語料過小新詞的向量質(zhì)量可能不理想此時可以給新詞單獨的epochs或適當(dāng)?shù)臋?quán)重調(diào)整。5.2 用詞向量做文本主題相關(guān)性判斷在不引入分類模型的前提下詞向量可以直接用來做文本或關(guān)鍵詞的主題相關(guān)性判斷。一個輕量方法是把一段文本的所有詞向量取平均得到文本向量再計算兩個文本向量的余弦相似度。這在關(guān)鍵詞擴(kuò)展、搜索推薦、文本去重等場景里非常實用:import numpy as np def sentence_vector(text, model): words text.split() vectors [model.wv[w] for w in words if w in model.wv] if not vectors: return np.zeros(model.vector_size) return np.mean(vectors, axis0) vec1 sentence_vector(機(jī)器學(xué)習(xí) 算法 回歸, model) vec2 sentence_vector(深度學(xué)習(xí) 神經(jīng)網(wǎng)絡(luò) 訓(xùn)練, model) sim np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))這里先對分詞后的文本逐詞取詞向量過濾掉 OOV 詞再按行計算平均向量。注意np.mean(vectors, axis0)是對所有詞向量的每一個維度取均值得到的是一個 300 維的向量。最后用 numpy 手動計算余弦相似度效果等同于model.wv.similarity但對任意兩個文本都適用。這個技巧在做無監(jiān)督文本聚類、問答系統(tǒng)候選召回時非常常見。5.3 關(guān)鍵詞提取結(jié)合 TF-IDF 與詞向量還有一個更細(xì)粒度的應(yīng)用方式是將詞向量與 TF-IDF 結(jié)合。純 TF-IDF 基于詞頻統(tǒng)計忽略了詞與詞之間的語義關(guān)系;純詞向量又丟失了詞頻信息。兩者的結(jié)合方式是計算詞在文檔中的 TF-IDF 權(quán)重對詞向量做加權(quán)平均from sklearn.feature_extraction.text import TfidfVectorizer corpus [神經(jīng)網(wǎng)絡(luò) 訓(xùn)練 需要 大量 數(shù)據(jù), 深度學(xué)習(xí) 模型 需要 GPU 加速] tfidf TfidfVectorizer(token_patternr(?u)\b\w\b) tfidf_matrix tfidf.fit_transform(corpus) feature_names tfidf.get_feature_names_out() # 用 TF-IDF 權(quán)重對詞向量加權(quán)平均得到文檔向量這種做法的本質(zhì)是讓高頻關(guān)鍵詞在文檔語義表示中占更大權(quán)重同時保留詞向量對同義表達(dá)的泛化能力。在短文本分類或信息檢索場景下它往往能比單獨使用 TF-IDF 或詞向量平均取得更均衡的效果。本文還有配套的精品資源點擊獲取