:用SVD與SGNS構(gòu)建子詞向量解決OOV問題)
簡介面向自然語言處理學習者尤其適合需要完成子詞向量課程作業(yè)的學生。項目圍繞漢語子詞向量構(gòu)建與評測分別實現(xiàn)基于SVD分解和基于SGNS的兩種方法。SVD方法先以K5窗口獲取高維distributional表示再進行降維SGNS方法采用窗口K2訓練神經(jīng)網(wǎng)絡詞向量。覆蓋詞表構(gòu)建、語料處理、向量訓練、余弦相似度計算等完整流程。壓縮包共15個文件整體大小88.66MB數(shù)據(jù)部分含corpus.txt語料全集、train_BPE.txt和test_BPE.txt子詞詞表、pku_sim_test.txt評測集等7個txt代碼部分含svd.py、sgns.py、skip_gram.py、result.py等4個Python腳本另有preprocess.ipynb交互式筆記、SGNS.pth模型、svd.npy向量和README.md說明文檔。已有112人瀏覽學習。項目提供了SVD與SGNS兩種方法的對比實現(xiàn)對未登錄詞設置余弦相似度為0的細節(jié)處理清晰可直接在pku_sim_test上復現(xiàn)結(jié)果并輸出指定格式適合作為NLP課程作業(yè)參考、算法對照或入門實踐素材另外README.md對目錄結(jié)構(gòu)和運行流程進行了說明便于快速上手。1. 這個NLP作業(yè)到底在解決什么中文OOV問題與子詞向量的實戰(zhàn)價值中文自然語言處理里最常遇到的尷尬是詞典里明明躺著一萬個詞來一條新聞還是蹦出“三航母”“星艦”這種訓練時沒見過的寫法。詞向量表里查不到下游任務直接罷工——這就是未登錄詞OOV問題。這個NLP實戰(zhàn)作業(yè)的核心很明確分別用SVD分解和SGNSSkip-Gram with Negative Sampling兩種方法構(gòu)建漢語的子詞向量再在相似度與類比任務上做評測對比。所謂子詞就是把“自然語言”拆成“自然”“然語”“語言”這類字n-gram串詞的向量由它包含的所有子詞向量聚合得到于是沒見過的詞也能用子詞拼出向量來。這份作業(yè)完美覆蓋了詞向量兩條技術路線——基于計數(shù)的矩陣分解與基于預測的神經(jīng)網(wǎng)絡——適合正在做詞向量課設、準備NLP面試或者想親手把word2vec底層邏輯寫一遍的人。2. 方案選型與數(shù)據(jù)準備SVD和SGNS的數(shù)學動機、漢語子詞切分與樣本生成2.1 為什么把SVD和SGNS放在同一份作業(yè)里SVD和SGNS代表了詞向量研究的兩個譜系。SVD走的是“計數(shù)”路線先統(tǒng)計詞與上下文在語料中的共現(xiàn)次數(shù)形成共現(xiàn)矩陣再用奇異值分解把高維稀疏矩陣壓成低維稠密向量。SGNS走的是“預測”路線把每個詞當成要預測的目標用神經(jīng)網(wǎng)絡在局部窗口內(nèi)反復調(diào)整向量讓中心詞與上下文詞的向量內(nèi)積逼近它們的共現(xiàn)概率。兩條路線的數(shù)學動機完全不同。SVD是全局優(yōu)化一次性看到整個語料的統(tǒng)計信息所以結(jié)果穩(wěn)定、可解釋性強主成分往往對應語義或話題維度缺點是矩陣規(guī)模隨詞表平方增長而且純統(tǒng)計方法對低頻詞極不友好。SGNS是局部優(yōu)化每次只看一個窗口訓練快、內(nèi)存友好但對隨機種子敏感同一份語料跑兩次結(jié)果可能差出一大截。作業(yè)把兩者放在一起本質(zhì)上是在讓你親手驗證同一個子詞向量目標用全局矩陣分解和用局部神經(jīng)網(wǎng)絡出來的向量在相似度評測上到底差多少。我在實際項目中觀察到的經(jīng)驗是——小語料上SVD往往不輸甚至超過SGNS語料一大SGNS的泛化優(yōu)勢才體現(xiàn)出來。這個結(jié)論自己去復現(xiàn)一遍比背十篇綜述都有用。2.2 漢語子詞序列怎么切字符n-gram的窗口選擇構(gòu)建子詞向量的第一步是把中文語料切成子詞序列。對漢語來說常見做法有兩種第一種是先做分詞再對每個詞內(nèi)部切字n-gram第二種是干脆不分詞直接對整句做字符級n-gram滑動窗口。作業(yè)場景下我強烈推薦第二種理由有三個不依賴外部分詞器、對OOV天然免疫、SVD和SGNS兩條路線共用同一套預處理邏輯。n-gram的n取多少需要權(quán)衡。n太小比如1-2子詞退化成單字和相鄰字對語義區(qū)分度不夠n太大比如7以上子詞基本退化成整詞失去了“子詞”的意義。我一般取2到6既覆蓋“自然”“語言”這種二字詞根也能捕捉“自然語言”這樣的四字短語。下面這段切分代碼是整個作業(yè)的地基SVD和SGNS都從它開始import re from typing import List PUNCT_RE re.compile(r[^\u4e00-\u9fa5a-zA-Z0-9]) def clean_text(text: str) - str: 只保留中文、英文和數(shù)字去掉標點與空白。 return PUNCT_RE.sub(, text) def make_subwords(text: str, n_min: int 2, n_max: int 6) - List[str]: 把一段文本切成字 n-gram 子詞序列。 自然語言處理 - [自然, 然語, 語言, 言處, 處理, 自然語, 然語言, ...] chars list(clean_text(text)) subwords [] for n in range(n_min, n_max 1): for i in range(len(chars) - n 1): subwords.append(.join(chars[i:i n])) return subwords說明兩個關鍵參數(shù)n_min和n_max控制子詞粒度。2-6是通用區(qū)間如果你發(fā)現(xiàn)詞表膨脹太快可以改成3-6犧牲一點短詞根覆蓋率換內(nèi)存。clean_text里的正則只保留中文、英文和數(shù)字因為標點符號在子詞序列里只會制造大量無意義的共現(xiàn)噪聲。注意這里沒有分詞——整句直接滑窗一個10個字的句子會產(chǎn)生約40個子詞數(shù)據(jù)量會膨脹這是正?,F(xiàn)象。2.3 低頻過濾與訓練樣本生成min_count、窗口大小與隨機種子切出子詞序列之后必須先做頻次統(tǒng)計和低頻過濾。低頻子詞比如只出現(xiàn)過一次的人名片段它們的共現(xiàn)統(tǒng)計極不可靠SVD會為它們分配一個幾乎隨機的向量SGNS則會在它們身上浪費大量訓練迭代。常見的做法是把頻次低于2或3的子詞直接丟棄替換成一個統(tǒng)一的UNK標記。詞表大小建議控制在5萬以內(nèi)超過這個規(guī)模SVD的矩陣分解時間和SGNS的參數(shù)量都會明顯拖慢實驗節(jié)奏。窗口大小這里一并定下。SVD的共現(xiàn)窗口和SGNS的上下文窗口取同一個值比如5這樣兩條路線的“上下文范圍”定義一致評測對比才公平。另外整個預處理管線里涉及的random seed要在文件開頭固定你后面跑SGNS時就知道這個操作有多救命。from collections import Counter import random random.seed(42) def build_vocab(subword_seq_list: List[List[str]], min_count: int 3) - dict: 統(tǒng)計子詞頻次過濾低頻項返回子詞到id的映射。 counter Counter() for seq in subword_seq_list: counter.update(seq) vocab {w: i for i, (w, c) in enumerate(counter.items()) if c min_count} vocab[UNK] len(vocab) return vocab頻次過濾看似簡單但它的值直接影響兩種方法的最終效果。min_count3是我的默認值語料小時可以降到2語料超過500萬字符時可以提到5。過濾之后所有不在詞表里的子詞統(tǒng)一映射到UNK保證訓練和評測階段不會遇到“查無此項”的尷尬。3. SVD路徑實現(xiàn)共現(xiàn)矩陣、PPMI加權(quán)與截斷SVD分解的參數(shù)化實現(xiàn)3.1 構(gòu)建子詞-上下文共現(xiàn)矩陣稀疏存儲與滑動窗口SVD路線的起點是共現(xiàn)矩陣。行和列都是子詞矩陣的第i行第j列表示子詞i作為中心詞時子詞j在其上下文窗口中出現(xiàn)的累計次數(shù)。構(gòu)建這個矩陣最樸素的思路是用Python字典累加但子詞表動輒幾萬共現(xiàn)對輕松破百萬字典方案內(nèi)存直接爆炸。正確做法是用scipy.sparse的稀疏矩陣。具體來說先掃一遍語料統(tǒng)計每個中心子詞與窗口內(nèi)其他子詞的共現(xiàn)次數(shù)然后一次性構(gòu)建CSR矩陣。注意兩點一是窗口內(nèi)的共現(xiàn)要不要按距離加權(quán)加權(quán)方案在作業(yè)里不強制不加權(quán)的表現(xiàn)也不會差太多二是對角線元素即子詞與自身的共現(xiàn)默認計入但后續(xù)可以用PPMI里的偏移量來抵消它帶來的噪聲。import numpy as np from scipy.sparse import lil_matrix def build_cooccurrence(seqs, vocab, window5): 構(gòu)建子詞-上下文共現(xiàn)矩陣返回稀疏CSR矩陣。 size len(vocab) matrix lil_matrix((size, size), dtypenp.float32) unk_id vocab.get(UNK, size - 1) for seq in seqs: ids [vocab.get(w, unk_id) for w in seq] for i, center in enumerate(ids): left max(0, i - window) right min(len(ids), i window 1) for j in range(left, right): if j i: continue matrix[center, ids[j]] 1.0 return matrix.tocsr()這個函數(shù)的核心邏輯是滑窗對每個中心子詞只統(tǒng)計它前后window個位置內(nèi)的共現(xiàn)。用lil_matrix是因為它按行追加效率高累加完成后再轉(zhuǎn)成csr_matrix后面做矩陣運算和SVD都更快。窗口值window5和SGNS保持一致如果你在第2章預處理時改過窗口這里必須同步修改。3.2 從共現(xiàn)到PPMI加權(quán)方案與截斷SVD分解直接對共現(xiàn)矩陣做SVD出來的向量質(zhì)量很差這是所有初學者都會踩的坑。原因是原始共現(xiàn)矩陣被“的”“了”“在”這類高頻詞主導它們的絕對值遠大于有語義區(qū)分度的詞對SVD的第一主成分會全部貢獻給詞頻差異。標準解法是先把共現(xiàn)矩陣轉(zhuǎn)成PPMI正點互信息矩陣再做分解。PPMI的計算公式是對每個共現(xiàn)對(i,j)先算PMI log(P(i,j) / (P(i) * P(j)))然后把負值置零。其中P(i,j)是i和j共同出現(xiàn)的概率P(i)、P(j)是各自的邊際概率。PPMI的核心作用是降權(quán)高頻詞一個字n-gram和“的”字頻繁共現(xiàn)只是因為“的”本身出現(xiàn)頻率高而不是它們有強語義關聯(lián)PPMI會把這個共現(xiàn)值壓下去。from scipy.sparse.linalg import svds from sklearn.preprocessing import normalize def compute_ppmi(X): 把共現(xiàn)矩陣轉(zhuǎn)成PPMI矩陣負值置零。 total X.sum() row_sum np.asarray(X.sum(axis1)).ravel() col_sum np.asarray(X.sum(axis0)).ravel() expected np.outer(row_sum, col_sum) / total # 只對非零元素做PMI計算避免全矩陣運算 X X.copy() X.data np.log(X.data * total / expected[X.nonzero()]) X.data[X.data 0] 0.0 # PPMI: 負值截斷為0 return X.tocsr() def svd_reduce(ppmi_matrix, dim64): 截斷SVD只保留最大的dim個奇異值對應的向量。 U, s, Vt svds(ppmi_matrix, kdim, whichLM) # 按奇異值從大到小排序svds不保證順序 order np.argsort(s)[::-1] s s[order] U U[:, order] Vt Vt[order, :] # 子詞向量 U * sqrt(S)上下文向量 Vt.T * sqrt(S) subword_vecs U * np.sqrt(s) context_vecs Vt.T * np.sqrt(s) return subword_vecs, context_vecs這段代碼有兩個關鍵參數(shù)值得細說。dim是向量維度作業(yè)里64到100之間足夠體現(xiàn)差異追求和預訓練模型對比時用300。whichLM表示只計算最大的dim個奇異值這是截斷SVD的標準做法如果你用的SciPy版本較舊svds默認返回的可能是最小奇異值對應的向量跑出來向量質(zhì)量慘不忍睹——先查這個參數(shù)對不對。最后用U * sqrt(S)作為子詞向量這是對稱化處理保證中心詞和上下文向量在同一個空間里可比。3.3 從子詞向量到詞向量聚合策略與歸一化SVD分解得到的是每個子詞的向量但評測任務需要的是“詞”的向量。聚合策略常見的有兩種直接平均和加權(quán)平均。直接平均就是把詞包含的所有字n-gram的向量求和再除以個數(shù)加權(quán)平均是按子詞在詞內(nèi)部出現(xiàn)的頻次或逆文檔頻率加權(quán)。作業(yè)場景下直接平均已經(jīng)夠用加權(quán)提升有限但會多出不少代碼。def word_vector_from_subwords(word, subword_vecs, vocab, n_min2, n_max6): 給定一個詞切分為子詞后聚合出詞向量。 vecs [] for n in range(n_min, n_max 1): for i in range(len(word) - n 1): sw word[i:i n] if sw in vocab: vecs.append(subword_vecs[vocab[sw]]) if not vecs: return np.zeros(subword_vecs.shape[1]) return np.mean(vecs, axis0) def normalize_vectors(vec_matrix): L2歸一化后續(xù)計算余弦相似度直接做點積。 return normalize(vec_matrix, norml2, axis1)注意這里如果詞太長或太短導致沒有任何子詞命中返回零向量。零向量參與余弦相似度計算會產(chǎn)生NaN評測前一定要過濾掉。另外把詞向量做L2歸一化是個好習慣因為余弦相似度只關心方向不關心模長歸一化之后直接用點積代替余弦計算速度能提升一個數(shù)量級。4. SGNS路徑實現(xiàn)負采樣訓練循環(huán)與子詞embedding聚合的完整步驟4.1 子詞表與負采樣分布頻次的三分之四次冪SGNS路線的核心是訓練一對向量每個子詞既是中心詞也是別人的上下文。也就是說需要維護兩個矩陣——中心詞向量矩陣V和上下文向量矩陣U形狀都是(vocab_size, dim)初始化時用均值為0、標準差0.01的正態(tài)分布隨機填充不能全零初始化否則梯度為零無法更新。負采樣的分布是SGNS區(qū)別于原始Skip-gram的關鍵。原始Softmax需要對整個詞表做歸一化代價太高負采樣的思路是把“預測正確上下文”的任務改造成“區(qū)分真上下文和隨機噪聲”的二分類任務。對每個正樣本從噪聲分布中隨機抽k個負樣本讓模型學會把正樣本真實上下文判為1把負樣本隨機抽取的噪聲判為0。噪聲分布不是均勻分布而是按詞頻的3/4次冪歸一化——這個3/4次冪是實踐調(diào)出來的它能壓低高頻詞被過度采樣的概率讓中頻詞有更多被當作負樣本學習的機會。import numpy as np from collections import Counter class SubwordSGNS: def __init__(self, vocab_size, dim64, k_neg5, lr0.025): self.dim dim self.k_neg k_neg self.lr lr # 中心詞向量矩陣和上下文向量矩陣 self.V np.random.normal(0, 0.01, (vocab_size, dim)) self.U np.random.normal(0, 0.01, (vocab_size, dim)) def build_noise_dist(self, freqs, power0.75): 構(gòu)建負采樣分布頻次的power次冪歸一化。 probs np.array([freqs[i] ** power for i in range(len(freqs))]) probs / probs.sum() return probs def negative_sample(self, probs, num): 從噪聲分布中不放回地抽樣num個負樣本id。 return np.random.choice(len(probs), sizenum, pprobs, replaceFalse)k_neg是負樣本數(shù)量取值5到10之間。太小模型學不夠太大訓練變慢且收益遞減。power0.75是word2vec論文里的經(jīng)典取值不建議改動。build_noise_dist接收的freqs是子詞id到頻次的映射這個值在預處理階段已經(jīng)統(tǒng)計過了直接復用。4.2 訓練循環(huán)sigmoid、學習率衰減與梯度裁剪訓練的過程就是對每個中心子詞i取窗口內(nèi)一個上下文子詞j作為正樣本再抽k個負樣本neg1...negk對每個樣本計算損失并更新向量。損失函數(shù)用的是二分類交叉熵的簡化形式對正樣本最大化sigmoid(V[i]·U[j])對負樣本最小化sigmoid(V[i]·U[neg])等價于最大化sigmoid(-V[i]·U[neg])。梯度推導是這份作業(yè)最容易卡住的地方但實現(xiàn)上很簡單對正樣本誤差項g sigmoid(V[i]·U[j]) - 1然后V[i] - lr * g * U[j]U[j] - lr * g * V[i]。對負樣本誤差項g sigmoid(V[i]·U[neg]) - 0更新方式相同。這個“預測誤差乘對方向量”的更新規(guī)則就是SGNS的全部秘密。def train(self, subword_seqs, epochs5, window5): subword_seqs: 子詞id序列的列表每個序列是一句或一段話。 for epoch in range(epochs): # 學習率隨epoch線性衰減后期微調(diào)避免震蕩 lr self.lr * (1 - epoch / epochs) for seq in subword_seqs: if len(seq) 2: continue for i, center_id in enumerate(seq): left max(0, i - window) right min(len(seq), i window 1) context_ids seq[left:i] seq[i1:right] for ctx_id in context_ids: # 正樣本中心詞向量點積上下文向量 dot self.V[center_id].dot(self.U[ctx_id]) g_pos 1.0 / (1.0 np.exp(-dot)) - 1.0 # 更新正樣本 self.V[center_id] - lr * g_pos * self.U[ctx_id] self.U[ctx_id] - lr * g_pos * self.V[center_id] # 負樣本 neg_ids self.negative_sample(self.noise_dist, self.k_neg) for neg_id in neg_ids: if neg_id ctx_id: continue dot_neg self.V[center_id].dot(self.U[neg_id]) g_neg 1.0 / (1.0 np.exp(-dot_neg)) self.V[center_id] - lr * g_neg * self.U[neg_id] self.U[neg_id] - lr * g_neg * self.V[center_id]這個訓練循環(huán)是樸素的Python版本一個epoch掃一遍全部語料。實際跑起來你會覺得慢——這是正常的因為Python級別的循環(huán)確實慢數(shù)據(jù)量一大建議用numpy矩陣化或者直接上gensim驗證結(jié)果。學習率衰減那行是關鍵不衰減的話后期loss會在一個平臺上震蕩向量質(zhì)量上不去。如果你的語料有100萬子詞以上建議把epochs從5降到3否則訓練時間會失控。4.3 從訓練結(jié)果到詞向量FastText式聚合與推理訓練結(jié)束后每個子詞都有兩個向量中心詞向量V[i]和上下文向量U[i]。詞向量怎么取常見做法是只取V也就是中心詞向量矩陣。但FastText的實踐表明把子詞向量聚合起來作為詞向量效果比直接用某個子詞的向量更穩(wěn)定。聚合方式和SVD路線完全一致給定一個詞切成所有可能的字n-gram查表拿到子詞向量求平均。這樣做的最大好處是——即使一個詞從沒在訓練語料里出現(xiàn)過只要它的字n-gram有部分命中就能拼出一個合理的向量。這就是子詞向量對OOV問題給出的答案。def get_word_vector(self, word, vocab, n_min2, n_max6): 推理階段對任意詞含OOV生成向量。 vecs [] for n in range(n_min, n_max 1): for i in range(len(word) - n 1): sw word[i:i n] if sw in vocab: vecs.append(self.V[vocab[sw]]) if not vecs: return np.zeros(self.dim) vec np.mean(vecs, axis0) norm np.linalg.norm(vec) return vec / norm if norm 0 else vec注意返回值做了L2歸一化。訓練階段沒有強約束向量模長直接比較原始向量會受模長干擾。np.linalg.norm(vec)是求L2范數(shù)除一下就把向量長度縮放到1后續(xù)計算余弦相似度時直接點積就行省去每次算模長。5. 避坑兩個模型反復翻車的5個細節(jié)與排查路徑5.1 現(xiàn)象共現(xiàn)矩陣構(gòu)建到一半內(nèi)存直接打滿你興致勃勃地建了一個5萬子詞的詞表然后用一個5萬 × 5萬的Python二維列表存共現(xiàn)矩陣瞬間內(nèi)存爆炸。原因很簡單5萬乘5萬乘8字節(jié)等于20GB哪怕用float16也要10GB。解決辦法就是第3章里的稀疏矩陣方案只存儲非零共現(xiàn)對實際稀疏度通常在99%以上內(nèi)存占用能壓縮到原來的幾十分之一。另外過濾低頻子詞這一步不要省詞表從10萬壓到3萬稀疏矩陣的非零元素會驟減訓練和分解都快很多。5.2 現(xiàn)象SVD分解出來的向量質(zhì)量不如隨機初始化這是PPMI缺失的典型癥狀。直接對原始共現(xiàn)矩陣做SVD向量會被高頻虛詞徹底污染“的”“了”“是”占據(jù)主成分方向其余詞之間的差異全被壓縮到次要維度里。排查時先別懷疑SVD代碼把共現(xiàn)矩陣打印出來看一眼前幾行如果高頻詞的值比其他詞大幾個數(shù)量級基本就是沒做PPMI。解決方法是嚴格按3.2節(jié)的流程過一遍先轉(zhuǎn)PPMI負值置零再做截斷SVD。5.3 現(xiàn)象SGNS訓練完了詞向量全部約等于零向量這通常不是代碼邏輯問題而是學習率設置不合理。如果lr超過0.1梯度更新步長過大向量會在訓練早期直接發(fā)散到無窮大然后數(shù)值溢出變成NaN如果lr低于0.001訓練根本走不動向量停在初始化位置附近。排查時先打印訓練前幾個batch的loss看走向loss不降是學習率太小loss變成NaN是學習率太大。推薦區(qū)間0.01到0.05按語料規(guī)模微調(diào)。另一個隱藏坑是隨機種子沒固定導致每次跑結(jié)果都不一樣你很難判斷是算法問題還是隨機性造成的波動。5.4 現(xiàn)象同一批語料SVD和SGNS評測結(jié)果排名不穩(wěn)定今天SVD贏明天SGNS贏換幾個評測詞又反過來——這可能是子詞切分參數(shù)不一致造成的。SVD路線的子詞序列和SGNS路線必須來自同一份預處理結(jié)果包括n_min、n_max、min_count和窗口大小全部一致否則兩條路線輸入的數(shù)據(jù)分布都不同對比就沒有意義。我見過不少人SVD用2-6gramSGNS用3-6gram最后得出的結(jié)論完全不靠譜。把預處理封裝成一個函數(shù)兩處調(diào)用同一份數(shù)據(jù)是性價比最高的修復。5.5 現(xiàn)象評測時OOV詞的向量是零向量相似度全是NaN子詞向量的核心賣點是OOV也能算向量但前提是這個詞的子詞組合至少有一部分在詞表里。如果詞太短比如單字“貓”它的子詞只有n_min到n_max范圍內(nèi)的片段一旦詞表里沒有對應字n-gram聚合結(jié)果就是零向量。解決方法是評測時對每個查詢詞做兜底如果子詞全未命中回退到單字向量如果單字也不在再回退到UNK向量。這個兜底邏輯要在評測腳本里寫清楚否則相似度排序會把零向量排在前面結(jié)果不可解釋。6. 評測與進階用相似度、類比和詞聚類給兩種子詞向量打分評測是這份作業(yè)的臨門一腳但也是最容易被糊弄過去的環(huán)節(jié)。切忌用肉眼挑幾個詞看相似度就下結(jié)論——那叫“看著像”不叫評測。標準做法是準備一組帶人工評分的詞對計算模型給出的余弦相似度與人工評分之間的Spearman相關系數(shù)。中文常用的是Wordsim-240和Wordsim-297里面是“飛機-機場”“老師-學生”這類詞對及人工打分。沒有現(xiàn)成數(shù)據(jù)集時自己標注20對詞也能跑一個粗糙版本每對詞打分1到5分然后和模型結(jié)果算相關系數(shù)排序一致性大于0.5就已經(jīng)說明向量不是隨機初始化能碰出來的。第二個評測維度是詞類比推理。經(jīng)典格式是“a之于b相當于c之于d”比如“北京-中國日本東京”。計算時用vec(北京) - vec(中國) vec(日本)在詞表里找與結(jié)果向量余弦相似度最高的詞如果是“東京”就算答對。這個任務對SVD和SGNS的差異非常敏感SVD由于全局統(tǒng)計特性在語義關系類比上往往表現(xiàn)更好SGNS則更擅長捕捉詞共現(xiàn)帶來的主題相似性。子詞向量在這里有個附加優(yōu)勢——即使類比任務中出現(xiàn)OOV詞也能通過子詞聚合參與計算這比標準word2vec多了一層保障。最后一個實用技巧是向量融合。做完評測你會發(fā)現(xiàn)SVD和SGNS各有勝負與其糾結(jié)選誰不如把兩種向量拼接后做PCA降維回原維度或者直接等權(quán)相加再歸一化。我在自己的項目里實測過融合向量在相似度和類比任務上的表現(xiàn)通常都能超過兩個單模型這個“112”的效果在作業(yè)里是天然的加分項在真實項目里也經(jīng)常作為廉價的中文詞向量增強手段。如果你時間充裕還可以把詞向量拿去做一個KMeans聚類聚類數(shù)取20到50打印每個簇的中心詞看語義是否聚成話題——這一步能直觀展示兩種方法學到的語義結(jié)構(gòu)差異比單純看數(shù)字更有說服力。最后說一個我做這份作業(yè)時后悔沒早點知道的事不要花太多時間調(diào)參跑到完美先拿小語料把兩條路線跑通、評測腳本寫好、踩坑踩一遍再換大語料出最終結(jié)果。SVD和SGNS的代碼結(jié)構(gòu)完全不同但評測流程是共用的——把評測腳本先寫好后續(xù)替換模型、調(diào)整參數(shù)都只改一行入口效率能翻一倍。希望幫到你。本文還有配套的精品資源點擊獲取