# 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("?")
```