# 1. NLP 的單字語意表示方法 相較於程式語言或數學語言,自然語言具備許多不確定性,使得處理變得困難,而電腦難以自動理解這些語意的演變: (1). 詞義隨年代、時間演變。 (2). 同詞異義、多義詞與語用學問題。 (3). 語意模糊與上下文依賴。 NLP 的基礎單詞語意表示方法如下: | 類別 | 方法說明 | | ------- | ------------------------------ | | 基於詞典的方法 | 使用人工整理的同義詞詞典(如 WordNet)建構詞義 | | 基於計數的方法 | 統計單詞與上下文的共現關係,建構共現矩陣與詞向量 | | 基於推論的方法 | 使用模型(如 word2vec)根據上下文預測詞,學得詞向量 | 早期技術中的 one - hot Encoding 缺點如下: (1). 向量維度極高。 (2). 沒有語意關係。 早期的基於詞典的方法缺點如下: (1). 人工整理,效率低。 (2). 難以應付語義演變。 為解決早期技術的限制,詞語向量學習開始轉向「分布式表示」: | 項目 | 基於計數的方法 | 基於推理的方法 | | ------ | ---------------------------------- | ------------------------------------ | | 方法類型 | 統計詞與詞在語料中共現的頻率(如 PMI、PPMI、SVD) | 使用神經網路預測詞與上下文之間的關係(如 CBOW、skip-gram) | | 訓練資料處理 | 使用整份語料建構共現矩陣,並透過矩陣分解(如 SVD)獲得低維詞向量 | 逐筆預測詞與上下文是否同時出現,透過損失函數調整參數 | | 運算方式 | 屬於一次性處理、批次(batch)方法;需計算全體詞彙間的共現 | 可搭配 mini-batch 與 GPU 並行訓練,效率較高 | | 優點 | 解釋性佳、理論成熟;整體語料利用率高 | 模型可微分、學習彈性強、效果佳;可持續訓練並應用於大量語料 | | 缺點 | 記憶體需求高、不能線上更新;詞向量固定,無法適應新語料 | 難以解釋詞向量的每個維度含義;需大量資料才能有效訓練 | 2014 年 Baroni 等人發表論文《Don’t count, predict!》,針對兩種詞向量學習方式進行系統性比較,結論為認為在語意相關性與下游任務上,推理式方法整體效果明顯優於計數法。 針對不同任務,適合的方法如下: | 問題情境 | 建議方法 | | ---------- | ----------------------------- | | 資源受限、語料簡單 | CBOW | | 語意精度要求高 | Skip-gram + Negative Sampling | | 需保留全語料統計特徵 | GloVe | | 想快速上手詞向量建構 | 使用預訓練詞向量(如 Gensim) | ## 1.1. One - Hot 假設輸入語料為 You say goodbye and I say hello . 則 one - hot 定義如下: 詞彙表大小為 $V=7$,則詞 ID = 2 的 one - hot 表示為 $[0,\, 0,\, \mathbf{1},\, 0,\, 0,\, 0,\, 0]$ | 詞彙 | ID | | ------- | -- | | you | 0 | | say | 1 | | goodbye | 2 | | and | 3 | | i | 4 | | hello | 5 | | . | 6 | | 上下文詞 ID(context) | 中心詞 ID(target) | | ---------------- | -------------- | | \[0, 2] | 1 | | \[1, 3] | 2 | | \[2, 4] | 3 | | \[3, 5] | 4 | | \[4, 6] | 5 | ## 1.2. 分布式語意假設與詞語共現 在自然語言處理中,語義相近的詞會出現在相似的上下文中。單詞本身不具語意,詞的意義取決於它出現在什麼樣的語境裡。 ## 1.3. 上下文視窗 定義一個「視窗大小」(window size),指定從某個詞開始,往左右各取多少個詞作為它的上下文。例如 you say goodbye and I say hello . 若視窗大小為 1,則: | 中心詞 | 上下文 | | ------- | ------------ | | you | say | | say | you, goodbye | | goodbye | say, and | # 2. 基於計數的詞彙表示法 基於計數的詞彙表示法利用詞與上下文的共現頻率來建構詞的語意表示。不依賴人工詞典,而是從大量語料中學習出詞彙之間的語意關係。 ## 2.1. 共現矩陣(Co-occurrence Matrix) 共現矩陣為二維表格,記錄每個單詞與其上下文詞彙出現的次數。矩陣中的每個元素表示某詞與某上下文詞在語料中共同出現的次數。 建構步驟如下: (1).定義視窗大小 $w$(如左右各取 $w=1$ 個單詞)。 (2).掃描語料中所有單詞,並對每個中心詞統計其上下文詞彙。 (3).將結果填入共現矩陣中。 範例程式碼如下: ``` def create_co_matrix(corpus, vocab_size, window_size=1): co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32) for idx, word_id in enumerate(corpus): for i in range(1, window_size + 1): left = idx - i right = idx + i if left >= 0: co_matrix[word_id, corpus[left]] += 1 if right < len(corpus): co_matrix[word_id, corpus[right]] += 1 return co_matrix ``` ## 2.2. PMI 與 PPMI(Pointwise Mutual Information) 共現次數容易偏好高頻詞,PMI 提供了一種衡量詞與上下文之間「資訊量」的方法。 PMI 定義如下: ${PMI}(x, y) = \log_2 \frac{P(x, y)}{P(x) P(y)} = \log_2 \frac{C(x, y) \cdot N}{C(x) \cdot C(y)}$ 負的 PMI 值會被視為無資訊,故定義 PPMI: ${PPMI}(x, y) = \max(0, \text{PMI}(x, y))$ 其中,$C(x, y)$ 為 $x$ 與 $y$ 的共現次數。$C(x)$、$C(y)$ 為 $x$ 與 $y$ 出現的總次數。$N$ 為共現矩陣中所有數值總和。 PPMI 範例程式碼如下: ``` def ppmi(C, eps=1e-8): M = np.zeros_like(C, dtype=np.float32) N = np.sum(C) S = np.sum(C, axis=0) for i in range(C.shape[0]): for j in range(C.shape[1]): pmi = np.log2((C[i,j] * N) / (S[j] * S[i]) + eps) M[i,j] = max(0, pmi) return M ``` ## 2.3. 相似度計算:餘弦相似度(Cosine Similarity) 為了比較兩個詞向量的語意接近程度,常使用餘弦相似度。其結果介於 $[-1, 1]$,越接近 1 表示越相似(方向一致)。 公式如下:${similarity}(\mathbf{x}, \mathbf{y}) = \frac{\mathbf{x} \cdot \mathbf{y}}{\|\mathbf{x}\| \|\mathbf{y}\|}$ 範例程式碼如下: ``` def cos_similarity(x, y, eps=1e-8): nx = x / (np.sqrt(np.sum(x**2)) + eps) ny = y / (np.sqrt(np.sum(y**2)) + eps) return np.dot(nx, ny) ``` ## 2.4. 相似詞查詢 範例程式碼如下: ``` # 給定一個查詢詞,可找出與其最相似的 k 個詞 def most_similar(query, word_to_id, id_to_word, word_matrix, top=5): if query not in word_to_id: print('%s is not found' % query) return query_id = word_to_id[query] query_vec = word_matrix[query_id] similarity = np.zeros(len(id_to_word)) for i in range(len(id_to_word)): similarity[i] = cos_similarity(word_matrix[i], query_vec) # 取得相似度最高的詞 for i in (-1 * similarity).argsort(): if id_to_word[i] == query: continue print('%s: %f' % (id_to_word[i], similarity[i])) top -= 1 if top == 0: break ``` # 3. 基於推理的詞彙表示法 基於推理的方法使用神經網路預測詞與上下文之間的關係,如 CBOW 與 skip-gram。若語料大、訓練資源有限,選用 CBOW 較有效率。若追求語意精度、語料稀疏,選用 Skip-gram 的效果更細緻。 比較如下: | 比較項目 | CBOW | Skip-gram | | ---- | ----------------- | ---------------------- | | 輸入 | 多個上下文詞 | 中心詞 | | 預測目標 | 中心詞 | 多個上下文詞 | | 運算效率 | 快(因為只做一次 softmax) | 慢(每個上下文詞都需做一次 softmax) | | 適用場景 | 常見詞彙關係學習 | 稀有詞語更能學好語意 | | 向量品質 | 表現穩定 | 精細語意表達佳 | ## 3.1. CBOW 模型的基本結構 CBOW 模型是一種基於上下文預測中心詞的神經網路模型,目標是給定上下文詞並預測中心詞,最大化中心詞在上下文條件下的機率。 在例句 You say goodbye and I say hello. 中,若選定 "say" 為中心詞,window size 為 1,則上下文為 ["you", "goodbye"],任務為預測 "say"。 流程如下: (1). 將上下文詞(context)轉為 one-hot 向量輸入。 (2). 經過輸入層的權重矩陣 $W_{in}$ 投影為詞向量。 (3). 將多個上下文詞向量進行平均。 (4). 經由輸出層權重矩陣 $W_{out}$ 轉換為得分向量。 (5). 使用 Softmax 計算出每個詞的出現機率,預測中心詞。 ### 3.1.1. 詞嵌入輸入層 假設詞彙表大小為 $V$,詞向量維度為 $N$,上下文詞為 $x_1, x_2$(皆為 one-hot 向量),輸入詞嵌入矩陣為 $W_{in} \in \mathbb{R}^{V \times N}$,則可得: $h_1 = W_{in}^T x_1$ $h_2 = W_{in}^T x_2$ 向量平均後得到輸出層輸入向量: $h = \frac{1}{2}(h_1 + h_2)$ ### 3.1.2. 輸出層與 softmax 預測 輸出層權重為 $W_{out} \in \mathbb{R}^{N \times V}$,最終得分為: $s = W_{out}^T h$ 經 softmax 得到每個詞為目標詞的機率: $y_i = \frac{e^{s_i}}{\sum_{j=1}^{V} e^{s_j}}$ ### 3.1.3. 損失函數:cross entropy 假設正確的中心詞為第 $t$ 個詞,則損失為: $L=−logyt$ 對整個語料($T$ 筆資料)取平均損失為: $L=−T1t=1∑TlogP(wt∣context)$ ## 3.2. CBOW 模型的資料準備 CBOW 模型的輸入是「上下文詞」,輸出是「中心詞」。因此需從語料中產生許多 (contexts, target) 配對作為訓練資料。 假設輸入語料為: You say goodbye and I say hello . 經詞彙轉換為 ID: | 詞彙 | ID | | ------- | -- | | you | 0 | | say | 1 | | goodbye | 2 | | and | 3 | | i | 4 | | hello | 5 | | . | 6 | 以動視窗(sliding window)進行資料擷取,若 window size = 1 為例,會得到以下配對: | 上下文詞 ID(context) | 中心詞 ID(target) | | ---------------- | -------------- | | \[0, 2] | 1 | | \[1, 3] | 2 | | \[2, 4] | 3 | | \[3, 5] | 4 | | \[4, 6] | 5 | 模型無法直接處理文字或詞 ID,因此需將每個詞轉換為 one-hot 向量輸入。在範例程式碼中,以 batch size = 5、上下文詞數 = 2、詞彙數 = 7 為例,最終轉換後的輸入輸出格式如下: (1). contexts.shape = (5, 2, 7):共 5 筆資料,每筆 2 個上下文詞,每詞為 one - hot(長度 7) (2). target.shape = (5, 7):5 筆中心詞,每個為 one - hot 向量 ``` import numpy as np def create_contexts_target(corpus, window_size=1): target = [] contexts = [] for idx in range(window_size, len(corpus) - window_size): context = [ corpus[idx - window_size], corpus[idx + window_size] ] target.append(corpus[idx]) contexts.append(context) return np.array(contexts), np.array(target) def convert_to_one_hot(corpus, vocab_size): N = corpus.shape[0] one_hot = np.zeros((N, vocab_size), dtype=np.int32) for idx, word_id in enumerate(corpus): one_hot[idx, word_id] = 1 return one_hot ``` ## 3.3. CBOW 模型的實作 CBOW 模型的 forward 運算流程為: (1). 將上下文詞的 one - hot 向量輸入,分別乘上輸入詞嵌入矩陣 $W_{in}$ (2). 計算上下文詞向量的平均 (3). 乘上輸出權重矩陣 $W_{out}$ 得到得分向量 (4). 經 softmax 得出每個詞作為中心詞的機率 (5). 使用交叉熵計算損失 定義類別範例程式碼如下: ``` from common.layers import MatMul, SoftmaxWithLoss class SimpleCBOW: def __init__(self, vocab_size, hidden_size): V, H = vocab_size, hidden_size # 初始化三層權重 self.in_layer0 = MatMul(np.random.randn(V, H)) self.in_layer1 = MatMul(np.random.randn(V, H)) self.out_layer = MatMul(np.random.randn(H, V)) self.loss_layer = SoftmaxWithLoss() self.params = [self.in_layer0.W, self.in_layer1.W, self.out_layer.W] self.grads = [self.in_layer0.dW, self.in_layer1.dW, self.out_layer.dW] def forward(self, contexts, target): h0 = self.in_layer0.forward(contexts[:, 0]) h1 = self.in_layer1.forward(contexts[:, 1]) h = (h0 + h1) * 0.5 # 取平均 score = self.out_layer.forward(h) loss = self.loss_layer.forward(score, target) return loss def backward(self, dout=1): ds = self.loss_layer.backward(dout) dh = self.out_layer.backward(ds) dh *= 0.5 self.in_layer0.backward(dh) self.in_layer1.backward(dh) return None ``` 訓練器與優化器設定範例程式碼如下: ``` from common.trainer import Trainer from common.optimizer import Adam model = SimpleCBOW(vocab_size, hidden_size) optimizer = Adam() trainer = Trainer(model, optimizer) ``` 訓練模型範例程式碼如下: ``` trainer.fit(contexts, target, max_epoch=1000, batch_size=3) trainer.plot() # 繪製損失下降圖 # 訓練過程會逐漸減少損失值,表示模型已成功學會詞與上下文之間的關聯。 ``` 提取詞向量範例程式碼如下: ``` word_vecs = model.in_layer0.W # 或 in_layer1.W # 每個詞的詞向量即為 $W_{in}$ 中該詞對應的行向量。 ``` 範例輸出如下: ``` you [ 0.93, 0.03, -1.47, 1.32, 0.93] say [ 1.21, 1.26, 0.07, -1.23, 0.23] ... ``` 模型限制與觀察如下: | 問題 | 說明 | | ------- | ---------------------- | | 高頻詞主導學習 | 頻繁出現的詞(如 "the")易干擾結果 | | 語意區分力有限 | 簡單 CBOW 難以辨別詞彙細緻語義差異 | | 輸出成本高 | Softmax 涉及整個詞彙表,計算複雜度高 | ## 3.4. Skip-gram 模型 Skip-gram 是 word2vec 的另一種結構,其目標與 CBOW 相反,即給定中心詞,預測上下文詞。在句子 You say goodbye and I say hello. 中,若中心詞為 "say",window size 為 1,則需預測上下文 [ "you" , "goodbye" ]。 結構如下: (1). 中心詞 one-hot 向量 (2). 詞嵌入矩陣 $W_{in}$ (3). 詞向量 $v$ (4). 針對每個上下文詞:$v$ × $W_{out}$ → softmax → 詞機率分布 ### 3.4.1. 機率建模 目標是在中心詞 $w_t$ 給定下,預測其上下文詞 $w_{t-1}, w_{t+1}$。條件機率如下: $P(w_{t-1}, w_{t+1} \mid w_t) = P(w_{t-1} \mid w_t) \cdot P(w_{t+1} \mid w_t)$ 若視窗為 $c$,則預測 $2c$ 個上下文詞,通用表示如下: $L = -\sum_{-c \leq j \leq c, j \neq 0} \log P(w_{t+j} \mid w_t)$ ### 3.4.2. softmax 預測上下文詞 每一個上下文詞的機率由 softmax 計算而得: $P(w_o \mid w_i) = \frac{\exp (v_{w_o}^\top v_{w_i})}{\sum_{w \in V} \exp (v_w^\top v_{w_i})}$ 其中,$v_{w_i}$ 為中心詞的向量,$v_{w_o}$ 為上下文詞的向量,$V$ 為詞彙表。 ## 3.4. GloVe 模型 GloVe(Global Vectors for Word Representation)由 Stanford NLP 團隊提出,結合計數法與推理法的優點。GloVe 使用整體語料的共現統計資訊(詞共現矩陣)作為訓練依據,並透過神經網路式的目標函數進行詞向量學習。 其目標函數如下: $J = \sum_{i,j=1}^{V} f(X_{ij}) \left( w_i^\top \tilde{w}_j + b_i + \tilde{b}_j - \log X_{ij} \right)^2$ 其中,$X_{ij}$ 為詞 $i$ 與詞 $j$ 的共現次數。$w_i$、$\tilde{w}_j$ 為詞向量。$b_i$、$\tilde{b}_j$ 為偏差項。$f(x)$ 為控制稀疏性與權重縮放的函數(如 $f(x) = \min(1, x^{0.75})$)。 # 4. 維度壓縮與詞向量最佳化 PPMI 共現矩陣雖然能反映詞語之間的語意關係,但其維度極高且稀疏,對運算效率與儲存空間造成巨大負擔。因此需要將詞向量進行降維處理,使其保留語意資訊的同時更為精簡。 高維稀疏問題如下: (1). 共現矩陣的維度等於詞彙表大小平方,對大型語料非常龐大。 (2). 大多數詞彙只與少數詞共現,因此矩陣大部分為 0(稀疏矩陣)。 (3). 對後續計算如相似度、分類等效率低落。 解法如下: (1). 留語意結構 (2). 壓縮維度、加快運算 (3). 轉換為稠密矩陣 ## 4.1. 奇異值分解(SVD) 定義奇異值分解為一矩陣分解方法,將原始矩陣 $X$ 分解為三個部分: $X=USV^\top$ 其中,$U$ 為左奇異向量,表示詞語的主成分特徵。$S$ 為對角矩陣,表示各維特徵的重要程度(奇異值)。$V^\top$ 為右奇異向量,表示語意方向。 降維方法為取 $U$ 中的前 $k$ 個列向量,作為每個詞的低維語意向量。這些向量可用於計算相似度或可視化。 實作範例程式碼如下: ``` # NumPy 標準方法 U, S, V = np.linalg.svd(X) word_vecs = U[:, :k] # k 維的詞向量表示 # sklearn 中的加速方法(適用大矩陣) from sklearn.utils.extmath import randomized_svd U, S, V = randomized_svd(X, n_components=k, random_state=42) ``` ## 4.2. 詞向量可視化 將詞向量投影到二維平面(例如使用 $U$ 的第1與第2維)可視化其語意分佈,語意相近的詞會在圖中聚集。例如:you、hello、goodbye、i 會出現在鄰近位置。 範例程式碼如下: ``` plt.scatter(U[:,0], U[:,1], alpha=0.5) for word, word_id in word_to_id.items(): plt.annotate(word, (U[word_id, 0], U[word_id, 1])) ``` # 5. 實作:大型語料庫 PTB 語料庫是由 Tomas Mikolov 提供的公開資料。資料已做過預處理,使用者可直接進行建模。 特點如下: (1). 少見詞用 <unk> 替代(unknown) (2). 每個句子結尾有 <eos> 標註(end of sentence) (3). 分為 train, test, valid 三組,方便訓練與評估 實驗流程如下: (1). 建立語料的詞彙表與 ID 映射 (2). 利用滑動視窗統計共現矩陣 (3). 對共現矩陣進行 PPMI 處理 (4). 使用 SVD 對 PPMI 矩陣降維 (5). 取得詞向量並計算相似度 範例程式碼如下: ``` # 載入程式碼 from dataset import ptb corpus, word_to_id, id_to_word = ptb.load_data('train') # 使用加速版本 SVD from sklearn.utils.extmath import randomized_svd U, S, V = randomized_svd(PPMI, n_components=k, random_state=42) ``` 實驗結果如下: 同一語意領域的詞會自動聚集,如 toyota 與其他汽車品牌相近、car 與 vehicle、luxury 有語意相關性。顯示基於計數方法 + PPMI + SVD 所建構的詞向量能有效反映語意。 | 查詢詞 (Query) | 相似詞 (Most Similar Words) | | ----------- | ---------------------------------------------- | | `you` | `i`, `we`, `’ve`, `do`, `someone` | | `year` | `month`, `quarter`, `last`, `earlier`, `week` | | `car` | `luxury`, `auto`, `vehicle`, `cars`, `truck` | | `toyota` | `nissan`, `honda`, `lexus`, `gm`, `volkswagen` |