---
# System prepended metadata

title: 【Deep Learning 2 - L4】word2vec 高速化

---

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