# 1. word2vec 的高速化 CBOW 模型在處理大規模語料(如 PTB 資料集)時,將面臨以下效能問題,明顯拖慢訓練效率: (1). 詞彙數大時矩陣龐大:若詞彙量為 $10^6$,且向量維度為 100,則 $W_{in}$ 為 $10^6 \times 100$,$W_{out}$ 為 $100 \times 10^6$。 (2). one-hot 計算成本高:one-hot 向量為稀疏矩陣,但仍需進行高維矩陣乘法。 (3). softmax 輸出層耗時:需對整個詞彙表做歸一化計算,效率極低。 最佳化方法如下: (1). 使用 Embedding 層 取代 one-hot 向量與矩陣乘法 (2). 使用 Negative Sampling 取代 softmax 計算 ## 1.1. Embedding 層取代 one - hot 傳統 CBOW 需將 one-hot 向量與 $W_{in}$ 相乘,以取得對應詞向量。由於 one-hot 向量只有一個位置為 1,乘法等價於直接從矩陣 $W_{in}$ 中擷取對應詞的向量(即查表操作)。 引入 Embedding 層,可將詞彙 ID 映射為詞向量,節省矩陣乘法運算,大幅提升效率。 ### 1.1.1 Embedding 層的 forward 範例程式碼如下: ``` W = np.arange(21).reshape(7, 3) W[2] # 輸出第 2 個詞的向量 # 可一次取出多筆詞向量(支援 mini-batch) # 使用 W[idx] 快速實現 ``` ### 1.1.2. Embedding 層的 backward 在反向傳播中,需將來自上層的誤差梯度 $dout$ 傳回給對應詞的行向量。但若有重複詞出現,會造成梯度覆寫問題。 解法如下: (1). 使用 for 迴圈將梯度累加至對應行 (2). 或使用 np.add.at(dW, idx, dout)累加避免覆蓋 ## 1.2. Negative Sampling 取代 softmax softmax 需對整個詞彙表(如 $10^6$ 詞)計算機率,加重效能負擔。運算複雜度為 $O(V)$。 算式如下:$y_k = \frac{\exp(s_k)}{\sum_{j=1}^{V} \exp(s_j)}$ NegativeSamplingLoss 類別結合多個 EmbeddingDot 層(正負樣本)與多個 SigmoidWithLoss 層,將多分類任務轉為多次二分類任務,以此節省資源。 透過 EmbeddingDot 層與 SigmoidWithLoss 層的搭配,能夠將中心詞與正樣本拉近、與副樣本拉遠。 forward 流程如下: ``` loss = 0 loss += 正例損失 for 每個負例: loss += 負例損失 ``` backward 流程如下: (1). 各層獨立 backward (2). 再將梯度加總回傳至中間層 $h$ ### 1.2.1. 多分類轉為多次二分類 將任務轉為「判斷此詞是否為正確目標詞」,即可視為二分類問題,搭配 Sigmoid 函數與交叉熵誤差函數。 算式如下:$y = \frac{1}{1 + \exp(-s)} \quad,\quad L = - (t \log y + (1 - t)\log(1 - y))$ 其中,$s$ 為中間層與目標詞權重向量的內積。$t = 1$ 表示正例。$t = 0$ 表示負例。 ### 1.2.2. Embedding Dot 層 Embedding Dot 層取出目標詞向量與中間層向量 $h$ 做內積,並傳入 Sigmoid 損失層。 中間層向量與正樣本內積大,Sigmoid 輸出接近 1。中間層向量與正樣本內積小,Sigmoid 輸出接近 0。 範例程式碼如下: ``` out = np.sum(target_W * h, axis=1) ``` ### 1.2.3. 負樣本的設計 每個訓練樣本包含: (1). 1 個正例(正確詞):標記為 1 (2). $k$ 個負例(隨機選詞):標記為 0 損失為正負樣本的交叉熵誤差總和。 ### 1.2.4. 負樣本的機率分布 為避免高頻詞被過度選中,需調整原始詞頻機率 $P(w_i)$: $P'(w_i) = \frac{P(w_i)^{0.75}}{\sum_j P(w_j)^{0.75}}$ 抽樣範例程式碼: ``` p = np.array([0.7, 0.29, 0.01]) new_p = np.power(p, 0.75) new_p /= np.sum(new_p) # 抽樣使用 np.random.choice(words, p=new_p) ``` # 2. word2vec 的應用與評估 訓練好的詞向量不僅可用於語義分析,還能作為各種自然語言處理任務的輸入特徵。Skip-gram 效果普遍優於 CBOW,且語料越大,表現越好: | 模型 | 語料大小 | 語義類比表現 | 句法類比表現 | | --------- | ---- | --------- | ------ | | CBOW | 1.6B | 55.5 | 67.1 | | Skip-gram | 1.6B | 61.1 | 69.3 | | Skip-gram | 6B | 70.7 | 76.1 | 判斷詞向量是否學到語義與語法資訊的常見評估方法有相似度評估(Word Similarity Task)與類比推理任務(Word Analogy Task)。 ## 2.1. 相似度評估 相似度評估與人類標註的詞對進行比較(如 car - automobile),使用 cosine similarity 衡量向量相似度,將排序與人類標註進行相關性比較(如 Spearman 相關係數)。 算式如下:$\text{cosine_sim}(x, y) = \frac{x \cdot y}{\|x\| \|y\|}$ ## 2.2. 類比推理任務 類比推理任務測試語義與句法關係是否能被向量反映。 算式如下:$\text{vec}(b) - \text{vec}(a) + \text{vec}(c) \approx \text{vec}(?)$ 範例如下: ``` 語義類比:king : man = queen : woman 語法類比:walk : walking = swim : swimming ``` ## 2.3. 實際應用案例 ### 2.3.1. 詞向量作為輸入特徵 訓練完畢的詞向量可作為其他機器學習模型(如 SVM、神經網路)的輸入。 流程如下: (1). 將文件轉換為詞向量集合(平均或拼接) (2). 作為特徵輸入分類模型 (3). 用於完成情感分析、主題分類等任務 ``` 詞向量 → 分類器(如 SVM)→ 輸出類別(正向 / 負向) ``` ### 2.3.2 情感分析範例 設一任務將一封電子郵件分類為正向、負向或中立。以詞向量構成的輸入,進行情感分類。 流程如下: (1). 將郵件文字轉為一系列詞向量 (2). 求平均向量作為輸入特徵 (3). 輸入至分類模型判斷情感傾向 # 3. 實作:Embedding 層與 Negative Sampling ## 3.1. SimpleCBOW 類別 SimpleCBOW 是改進後的 CBOW 模型類別,整合多個上下文輸入並預測中心詞。 初始化範例程式碼如下: ``` class CBOW: def __init__(self, vocab_size, hidden_size, window_size, corpus): V, H = vocab_size, hidden_size W_in = 0.01 * np.random.randn(V, H) W_out = 0.01 * np.random.randn(V, H) # 上下文對應的 Embedding 層 self.in_layers = [] for _ in range(2 * window_size): layer = Embedding(W_in) self.in_layers.append(layer) # 損失層(Negative Sampling) self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5) # 統整參數與梯度 self.params = [layer.params for layer in self.in_layers] + self.ns_loss.params self.grads = [layer.grads for layer in self.in_layers] + self.ns_loss.grads self.word_vecs = W_in # vocab_size:詞彙表大小 # hidden_size:詞向量維度 # window_size:上下文窗口大小 ``` forward 函數範例程式碼如下: ``` def forward(self, contexts, target): h = 0 for layer, idx in zip(self.in_layers, contexts.T): h += layer.forward(idx) h *= 1 / len(self.in_layers) loss = self.ns_loss.forward(h, target) return loss # 將所有上下文詞向量加總平均為中間層 $h$ # 傳入 Negative Sampling 計算損失 ``` backward 函數範例程式碼如下: ``` def backward(self, dout=1): dh = self.ns_loss.backward(dout) dh *= 1 / len(self.in_layers) for layer in self.in_layers: layer.backward(dh) return None ``` ## 3.2. 訓練模型 範例程式碼如下: ``` window_size = 5 hidden_size = 100 batch_size = 100 max_epoch = 10 # 載入語料與資料 corpus, word_to_id, id_to_word = ptb.load_data('train') contexts, target = create_contexts_target(corpus, window_size) # 建立模型與優化器 model = CBOW(vocab_size, hidden_size, window_size, corpus) optimizer = Adam() trainer = Trainer(model, optimizer) # 執行訓練 trainer.fit(contexts, target, max_epoch, batch_size) ``` 儲存模型向量範例程式碼如下: ``` word_vecs = model.word_vecs params = { 'word_vecs': word_vecs.astype(np.float16), 'word_to_id': word_to_id, 'id_to_word': id_to_word } with open('cbow_params.pkl', 'wb') as f: pickle.dump(params, f) # 將學到的詞向量與詞彙表儲存為 .pkl 檔,供後續使用與推論。 ``` ## 3.3. 模型評估 相似詞查詢:most_similar() 範例程式碼如下: ``` most_similar('you', word_to_id, id_to_word, word_vecs, top=5) # 輸出結果: # [query] you # we: 0.6105 # someone: 0.5917 # I: 0.5543 ``` 類比推理 analogy() 範例程式碼如下: ``` analogy('king', 'man', 'woman', word_to_id, id_to_word, word_vecs, top=5) # 輸出結果: # [analogy] king : man = ? : woman # queen: 5.1 # veto: 4.9 # 使用公式:vec("king") - vec("man") + vec("woman") \approx vec("?") ```