前言
自然語言處理的核心挑戰之一,在於如何將人類語言這種離散且高維的符號系統,轉換為機器能夠理解與處理的數學表示。傳統的 One-Hot 編碼雖然簡單直觀,但面臨著維度災難與語義稀疏性的雙重困境。每個詞彙被表示為一個只有單一位置為 1、其餘位置為 0 的高維向量,這種表示方式不僅導致向量維度等於詞彙表大小,更嚴重的是完全無法捕捉詞彙之間的語義關係。“國王"與"皇后"在 One-Hot 編碼中的距離,與"國王"與"蘋果"完全相同,這顯然與人類的語言直覺相違背。
詞嵌入技術的出現徹底改變了這個局面。詞嵌入將詞彙映射到低維稠密的連續向量空間,在這個空間中,語義相近的詞彙在向量空間中的距離也相近。這種表示方式不僅大幅降低了維度,從數萬維降到數百維,更重要的是能夠捕捉詞彙之間豐富的語義關係。透過詞嵌入,“國王"與"皇后"的向量距離會明顯小於"國王"與"蘋果”,這符合我們對語義相似性的直覺理解。
詞嵌入技術的發展經歷了多個重要階段。Word2Vec 的出現標誌著分散式詞表示時代的來臨,它透過神經網路模型從大規模語料庫中學習詞彙的向量表示,展現了驚人的語義捕捉能力。經典的"國王 - 男人 + 女人 = 皇后"的向量運算,生動展示了詞嵌入如何在向量空間中編碼複雜的語義關係。GloVe 則從另一個角度出發,結合了全域統計資訊與局部上下文,提供了理論與實務兼具的解決方案。
然而,這些早期的詞嵌入技術都存在一個根本性的限制:它們為每個詞彙分配一個固定的向量表示,無法處理一詞多義的現象。“銀行"這個詞在"河岸"與"金融機構"兩個語境中的含義完全不同,但在 Word2Vec 或 GloVe 中只有一個固定的向量表示。這個限制促使了上下文感知詞嵌入技術的發展。
BERT 等基於 Transformer 的模型代表了詞嵌入技術的新典範。這些模型不再為詞彙分配固定的向量,而是根據詞彙所在的具體上下文動態生成向量表示。同一個詞在不同語境中會得到不同的向量,這種動態表示能夠準確捕捉詞彙在特定上下文中的確切含義。BERT 透過雙向 Transformer 編碼器,同時利用左右兩側的上下文資訊,實現了對語言理解的重大突破。
詞嵌入技術的應用範圍極為廣泛。在文本分類任務中,詞嵌入為每個詞彙提供了富含語義資訊的特徵表示,大幅提升了分類準確率。在情感分析中,詞嵌入能夠捕捉詞彙的情感極性與強度,協助模型理解文本的情感傾向。在機器翻譯中,詞嵌入為源語言與目標語言建立了共享的語義空間,促進了跨語言的語義對齊。在問答系統中,詞嵌入幫助模型理解問題與答案之間的語義匹配關係。
然而,詞嵌入技術的應用也面臨著挑戰。訓練高品質的詞嵌入需要大量的語料資料與計算資源,特別是 BERT 等大型預訓練模型,其訓練成本對多數研究者與開發者來說是難以承受的。詞嵌入可能會編碼訓練語料中存在的偏見與刻板印象,如性別偏見、種族偏見等,這在實際應用中可能導致不公平的結果。此外,如何評估詞嵌入的品質,如何針對特定領域微調詞嵌入,都是實務工作中需要解決的問題。
本文將系統化地探討詞嵌入技術的各個面向。從 Word2Vec 的基本原理開始,深入剖析 CBOW 與 Skip-gram 兩種模型架構的差異與適用場景。接著探討 GloVe 如何結合全域統計與局部上下文,以及 fastText 如何透過子詞單元處理未登錄詞問題。然後深入分析 BERT 等上下文感知模型的創新機制,展示如何實現動態的詞嵌入。最後探討詞嵌入在各種 NLP 任務中的實際應用,並提供完整的程式碼範例與最佳實踐建議。透過這個完整的技術旅程,讀者將深入理解詞嵌入技術的演進邏輯、實作細節與應用策略,能夠在實務工作中做出明智的技術選擇。
Word2Vec:分散式詞表示的奠基之作
Word2Vec 的出現標誌著詞嵌入技術從理論走向實用的關鍵轉折點。這個由 Google 研究團隊在 2013 年提出的模型,透過簡潔優雅的神經網路架構,從大規模語料庫中學習詞彙的分散式表示。Word2Vec 的核心洞察在於分佈假說:一個詞的含義可以透過其經常出現的上下文來理解。“你應該透過與一個詞相伴的詞來理解這個詞”,這個語言學的基本原則被 Word2Vec 轉化為可計算的數學模型。
Word2Vec 提供了兩種模型架構:連續詞袋模型(CBOW)與跳字模型(Skip-gram)。這兩種模型從相反的方向學習詞嵌入,各有其優勢與適用場景。CBOW 透過上下文詞彙預測目標詞,而 Skip-gram 則透過目標詞預測上下文詞彙。理解這兩種模型的差異,對於在實務中選擇合適的模型至關重要。
CBOW 模型的架構相對簡單。給定一個目標詞,模型收集其前後固定窗口內的上下文詞彙,將這些上下文詞彙的向量進行平均,然後透過一個隱藏層與輸出層預測目標詞。這種架構的優勢在於訓練速度快,對於頻繁出現的詞彙能夠學習到穩定的表示。由於 CBOW 平均了多個上下文詞彙的資訊,它對於噪音有更好的魯棒性,適合處理大規模語料庫。
Skip-gram 模型則採取相反的策略。給定一個目標詞,模型嘗試預測其上下文中的每個詞彙。這種架構為每個上下文位置創建了一個獨立的預測任務,因此能夠從每個訓練樣本中學習到更多的資訊。Skip-gram 對於低頻詞彙的表示效果更好,因為即使一個詞出現次數不多,只要它出現時就能提供多個訓練樣本。這使得 Skip-gram 特別適合處理包含大量低頻詞與專業術語的語料庫。
讓我們透過實際的程式碼來理解 Word2Vec 的實作細節:
"""
Word2Vec 模型實作與應用
展示 CBOW 與 Skip-gram 的訓練與使用
"""
import numpy as np
from gensim.models import Word2Vec
from gensim.models.callbacks import CallbackAny2Vec
import logging
from typing import List, Dict, Tuple
# 設置日誌
logging.basicConfig(
format='%(asctime)s : %(levelname)s : %(message)s',
level=logging.INFO
)
class EpochLogger(CallbackAny2Vec):
"""
訓練過程回調函式
用於記錄訓練進度與損失
"""
def __init__(self):
"""初始化回調"""
self.epoch = 0
def on_epoch_end(self, model):
"""
每個訓練週期結束時調用
參數:
model: Word2Vec 模型實例
"""
loss = model.get_latest_training_loss()
print(f'訓練週期 {self.epoch} - 損失值: {loss}')
self.epoch += 1
class Word2VecTrainer:
"""
Word2Vec 訓練器
支援 CBOW 與 Skip-gram 兩種架構
"""
def __init__(self,
sentences: List[List[str]],
architecture: str = 'skip-gram',
vector_size: int = 100,
window: int = 5,
min_count: int = 5,
workers: int = 4):
"""
初始化訓練器
參數:
sentences: 訓練語料,每個句子是詞彙列表
architecture: 模型架構,'skip-gram' 或 'cbow'
vector_size: 詞向量維度
window: 上下文窗口大小
min_count: 最小詞頻閾值,低於此閾值的詞將被忽略
workers: 並行訓練的執行緒數
"""
self.sentences = sentences
self.architecture = architecture
self.vector_size = vector_size
self.window = window
self.min_count = min_count
self.workers = workers
self.model = None
def train(self, epochs: int = 10) -> Word2Vec:
"""
訓練 Word2Vec 模型
參數:
epochs: 訓練週期數
回傳:
訓練好的 Word2Vec 模型
"""
# sg 參數: 0 表示 CBOW, 1 表示 Skip-gram
sg_value = 1 if self.architecture == 'skip-gram' else 0
print(f"開始訓練 {self.architecture.upper()} 模型...")
print(f"參數設置:")
print(f" - 向量維度: {self.vector_size}")
print(f" - 窗口大小: {self.window}")
print(f" - 最小詞頻: {self.min_count}")
print(f" - 訓練週期: {epochs}")
# 創建並訓練模型
self.model = Word2Vec(
sentences=self.sentences,
vector_size=self.vector_size,
window=self.window,
min_count=self.min_count,
workers=self.workers,
sg=sg_value,
epochs=epochs,
callbacks=[EpochLogger()],
compute_loss=True
)
print(f"訓練完成!")
print(f"詞彙表大小: {len(self.model.wv)}")
return self.model
def save_model(self, path: str) -> None:
"""
儲存訓練好的模型
參數:
path: 儲存路徑
"""
if self.model is None:
raise ValueError("模型尚未訓練,請先調用 train() 方法")
self.model.save(path)
print(f"模型已儲存至: {path}")
def load_model(self, path: str) -> Word2Vec:
"""
載入已訓練的模型
參數:
path: 模型路徑
回傳:
載入的 Word2Vec 模型
"""
self.model = Word2Vec.load(path)
print(f"模型已從 {path} 載入")
return self.model
class Word2VecAnalyzer:
"""
Word2Vec 模型分析器
提供詞嵌入分析與視覺化功能
"""
def __init__(self, model: Word2Vec):
"""
初始化分析器
參數:
model: 訓練好的 Word2Vec 模型
"""
self.model = model
self.wv = model.wv
def find_similar_words(self,
word: str,
topn: int = 10) -> List[Tuple[str, float]]:
"""
尋找與給定詞最相似的詞彙
參數:
word: 目標詞彙
topn: 回傳的相似詞數量
回傳:
(相似詞, 相似度分數) 的列表
"""
try:
similar_words = self.wv.most_similar(word, topn=topn)
return similar_words
except KeyError:
print(f"詞彙 '{word}' 不在詞彙表中")
return []
def word_analogy(self,
positive: List[str],
negative: List[str],
topn: int = 5) -> List[Tuple[str, float]]:
"""
詞彙類比任務
例如: king - man + woman = queen
參數:
positive: 正向詞彙列表 (加法)
negative: 負向詞彙列表 (減法)
topn: 回傳結果數量
回傳:
類比結果列表
"""
try:
results = self.wv.most_similar(
positive=positive,
negative=negative,
topn=topn
)
return results
except KeyError as e:
print(f"詞彙不在詞彙表中: {e}")
return []
def calculate_similarity(self, word1: str, word2: str) -> float:
"""
計算兩個詞彙的相似度
參數:
word1: 第一個詞彙
word2: 第二個詞彙
回傳:
相似度分數 (0-1 之間)
"""
try:
similarity = self.wv.similarity(word1, word2)
return similarity
except KeyError as e:
print(f"詞彙不在詞彙表中: {e}")
return 0.0
def get_vector(self, word: str) -> np.ndarray:
"""
獲取詞彙的向量表示
參數:
word: 目標詞彙
回傳:
詞向量
"""
try:
return self.wv[word]
except KeyError:
print(f"詞彙 '{word}' 不在詞彙表中")
return np.zeros(self.wv.vector_size)
def evaluate_analogies(self,
analogy_file: str) -> Dict[str, float]:
"""
評估模型在詞類比任務上的表現
參數:
analogy_file: 詞類比測試檔案路徑
回傳:
各類別的準確率
"""
results = self.wv.evaluate_word_analogies(analogy_file)
# 解析結果
accuracy_dict = {}
for section in results[1]:
section_name = section['section']
correct = len(section['correct'])
incorrect = len(section['incorrect'])
total = correct + incorrect
accuracy = correct / total if total > 0 else 0
accuracy_dict[section_name] = accuracy
return accuracy_dict
# 使用範例
if __name__ == '__main__':
# 準備訓練語料
# 實務中應使用大規模的真實語料庫
sentences = [
['國王', '統治', '國家'],
['皇后', '統治', '王國'],
['男人', '工作', '公司'],
['女人', '工作', '辦公室'],
['貓', '抓', '老鼠'],
['狗', '追', '貓'],
['蘋果', '是', '水果'],
['香蕉', '是', '水果'],
['汽車', '在', '道路', '上'],
['飛機', '在', '天空', '中'],
]
# 訓練 Skip-gram 模型
print("=" * 50)
print("訓練 Skip-gram 模型")
print("=" * 50)
trainer_sg = Word2VecTrainer(
sentences=sentences,
architecture='skip-gram',
vector_size=50,
window=3,
min_count=1,
workers=2
)
model_sg = trainer_sg.train(epochs=100)
# 分析 Skip-gram 模型
analyzer_sg = Word2VecAnalyzer(model_sg)
print("\n" + "=" * 50)
print("Skip-gram 模型分析")
print("=" * 50)
# 尋找相似詞
test_words = ['國王', '水果', '貓']
for word in test_words:
print(f"\n與 '{word}' 最相似的詞:")
similar = analyzer_sg.find_similar_words(word, topn=3)
for w, score in similar:
print(f" {w}: {score:.4f}")
# 詞類比任務
print("\n詞類比任務:")
print("國王 - 男人 + 女人 = ?")
analogy_result = analyzer_sg.word_analogy(
positive=['國王', '女人'],
negative=['男人'],
topn=3
)
for word, score in analogy_result:
print(f" {word}: {score:.4f}")
# 計算相似度
print("\n詞彙相似度:")
word_pairs = [('國王', '皇后'), ('貓', '狗'), ('蘋果', '香蕉')]
for w1, w2 in word_pairs:
sim = analyzer_sg.calculate_similarity(w1, w2)
print(f" {w1} <-> {w2}: {sim:.4f}")
# 訓練 CBOW 模型進行比較
print("\n" + "=" * 50)
print("訓練 CBOW 模型")
print("=" * 50)
trainer_cbow = Word2VecTrainer(
sentences=sentences,
architecture='cbow',
vector_size=50,
window=3,
min_count=1,
workers=2
)
model_cbow = trainer_cbow.train(epochs=100)
# 模型比較
print("\n" + "=" * 50)
print("CBOW 與 Skip-gram 比較")
print("=" * 50)
analyzer_cbow = Word2VecAnalyzer(model_cbow)
print("\n相同詞對在兩種模型中的相似度:")
for w1, w2 in word_pairs:
sim_sg = analyzer_sg.calculate_similarity(w1, w2)
sim_cbow = analyzer_cbow.calculate_similarity(w1, w2)
print(f"{w1} <-> {w2}:")
print(f" Skip-gram: {sim_sg:.4f}")
print(f" CBOW: {sim_cbow:.4f}")
這個完整的程式碼展示了 Word2Vec 的訓練與應用。Word2VecTrainer 類別封裝了模型訓練的邏輯,支援 CBOW 與 Skip-gram 兩種架構。透過設置 sg 參數,我們可以輕鬆切換模型架構。訓練過程中使用回調函式記錄訓練進度,這對於監控大規模訓練非常有用。
Word2VecAnalyzer 類別提供了多種詞嵌入分析功能。find_similar_words 方法透過餘弦相似度找出與目標詞最相近的詞彙,這是驗證詞嵌入品質的常用方法。word_analogy 方法實現了經典的詞類比任務,例如"國王 - 男人 + 女人 = 皇后”。這種向量運算能力展示了詞嵌入如何在向量空間中編碼語義關係。
以下的架構圖展示了 Word2Vec 的兩種模型結構:
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100
package "CBOW 架構" {
component "上下文詞 1" as C1
component "上下文詞 2" as C2
component "上下文詞 3" as C3
component "上下文詞 4" as C4
component "嵌入層" as EMBED1 {
[向量查找]
}
component "投影層" as PROJ {
[向量平均]
}
component "輸出層" as OUT1 {
[Softmax]
[預測目標詞]
}
}
package "Skip-gram 架構" {
component "目標詞" as TARGET
component "嵌入層" as EMBED2 {
[向量查找]
}
component "輸出層" as OUT2 {
[Softmax]
[預測上下文]
}
component "上下文詞 1'" as C1P
component "上下文詞 2'" as C2P
component "上下文詞 3'" as C3P
component "上下文詞 4'" as C4P
}
C1 --> EMBED1
C2 --> EMBED1
C3 --> EMBED1
C4 --> EMBED1
EMBED1 --> PROJ
PROJ --> OUT1
TARGET --> EMBED2
EMBED2 --> OUT2
OUT2 --> C1P
OUT2 --> C2P
OUT2 --> C3P
OUT2 --> C4P
note right of PROJ
CBOW 平均多個
上下文詞向量
預測目標詞
end note
note right of OUT2
Skip-gram 從
目標詞預測
多個上下文詞
end note
@enduml
這個架構圖清楚展示了 CBOW 與 Skip-gram 的結構差異。CBOW 將多個上下文詞的向量平均後預測目標詞,這種架構適合頻繁出現的詞彙,訓練速度較快。Skip-gram 則從目標詞預測多個上下文詞,為每個上下文位置創建獨立的預測任務,這種架構對低頻詞效果更好,但訓練時間較長。
在實務應用中,選擇 CBOW 或 Skip-gram 需要考慮多個因素。如果語料庫規模大且詞頻分布相對均勻,CBOW 是更高效的選擇。如果語料庫包含大量專業術語或低頻詞,Skip-gram 能夠提供更好的表示。對於計算資源有限的情況,CBOW 的訓練速度優勢更加明顯。這些權衡需要根據具體的應用場景來決定。
GloVe:全域統計與局部上下文的融合
GloVe(Global Vectors for Word Representation)代表了詞嵌入技術的另一個重要方向。與 Word2Vec 的預測式方法不同,GloVe 採用計數式方法,透過詞彙共現矩陣的矩陣分解來學習詞向量。這種方法的核心洞察在於,詞彙的語義資訊蘊含在它們的共現統計中,透過直接建模這些統計資訊,可以得到高品質的詞嵌入。
GloVe 的訓練過程分為兩個主要階段。首先,從大規模語料庫中構建詞彙共現矩陣,這個矩陣記錄了每對詞彙在固定窗口內共同出現的次數。然後,透過加權最小二乘回歸,學習能夠重建這個共現矩陣的詞向量。GloVe 的損失函式設計巧妙,它對高頻與低頻詞對賦予不同的權重,既利用了高頻詞對的豐富資訊,又避免了被極高頻詞對主導。
GloVe 的優勢在於能夠同時利用全域統計資訊與局部上下文。全域統計資訊來自整個語料庫的共現計數,這提供了詞彙在整體語料中的使用模式。局部上下文則透過窗口大小的設置來控制,較小的窗口更關注語法關係,較大的窗口則更關注主題關聯。這種設計讓 GloVe 在多種 NLP 任務中都展現出優異的性能。
讓我們透過程式碼來理解 GloVe 的使用與分析:
"""
GloVe 詞嵌入載入與應用
展示如何使用預訓練的 GloVe 向量
"""
import numpy as np
from typing import Dict, List, Tuple, Optional
from scipy.spatial.distance import cosine
import pickle
class GloVeLoader:
"""
GloVe 詞向量載入器
支援多種預訓練 GloVe 模型的載入與管理
"""
def __init__(self, embedding_dim: int = 100):
"""
初始化載入器
參數:
embedding_dim: 詞向量維度 (50, 100, 200, 300)
"""
self.embedding_dim = embedding_dim
self.word2vec: Dict[str, np.ndarray] = {}
self.vocab: List[str] = []
def load_glove_file(self, file_path: str) -> None:
"""
從文件載入 GloVe 詞向量
參數:
file_path: GloVe 文件路徑
"""
print(f"載入 GloVe 詞向量 (維度: {self.embedding_dim})...")
with open(file_path, 'r', encoding='utf-8') as f:
for line_number, line in enumerate(f, 1):
if line_number % 10000 == 0:
print(f" 已載入 {line_number} 個詞向量...")
# 分割詞彙與向量
values = line.strip().split()
word = values[0]
try:
# 將字串轉換為浮點數向量
vector = np.array(values[1:], dtype='float32')
# 驗證向量維度
if len(vector) != self.embedding_dim:
continue
self.word2vec[word] = vector
self.vocab.append(word)
except ValueError:
# 跳過格式錯誤的行
continue
print(f"載入完成! 詞彙表大小: {len(self.word2vec)}")
def get_vector(self, word: str) -> Optional[np.ndarray]:
"""
獲取詞彙的向量表示
參數:
word: 目標詞彙
回傳:
詞向量,如果詞彙不存在則回傳 None
"""
return self.word2vec.get(word)
def has_word(self, word: str) -> bool:
"""
檢查詞彙是否在詞彙表中
參數:
word: 目標詞彙
回傳:
True 如果詞彙存在,否則 False
"""
return word in self.word2vec
def get_embedding_matrix(self,
word_list: List[str]) -> np.ndarray:
"""
獲取詞彙列表的嵌入矩陣
參數:
word_list: 詞彙列表
回傳:
嵌入矩陣,形狀為 (len(word_list), embedding_dim)
"""
embedding_matrix = np.zeros(
(len(word_list), self.embedding_dim)
)
for idx, word in enumerate(word_list):
vector = self.get_vector(word)
if vector is not None:
embedding_matrix[idx] = vector
return embedding_matrix
def save_cache(self, cache_path: str) -> None:
"""
將載入的詞向量儲存為緩存文件
參數:
cache_path: 緩存文件路徑
"""
cache_data = {
'word2vec': self.word2vec,
'vocab': self.vocab,
'embedding_dim': self.embedding_dim
}
with open(cache_path, 'wb') as f:
pickle.dump(cache_data, f)
print(f"緩存已儲存至: {cache_path}")
def load_cache(self, cache_path: str) -> None:
"""
從緩存文件載入詞向量
參數:
cache_path: 緩存文件路徑
"""
with open(cache_path, 'rb') as f:
cache_data = pickle.load(f)
self.word2vec = cache_data['word2vec']
self.vocab = cache_data['vocab']
self.embedding_dim = cache_data['embedding_dim']
print(f"從緩存載入完成! 詞彙表大小: {len(self.word2vec)}")
class GloVeAnalyzer:
"""
GloVe 詞向量分析器
提供相似度計算、類比推理等功能
"""
def __init__(self, glove_loader: GloVeLoader):
"""
初始化分析器
參數:
glove_loader: GloVe 載入器實例
"""
self.loader = glove_loader
self.word2vec = glove_loader.word2vec
def cosine_similarity(self,
vec1: np.ndarray,
vec2: np.ndarray) -> float:
"""
計算兩個向量的餘弦相似度
參數:
vec1: 第一個向量
vec2: 第二個向量
回傳:
相似度分數 (0-1 之間)
"""
return 1 - cosine(vec1, vec2)
def word_similarity(self, word1: str, word2: str) -> float:
"""
計算兩個詞彙的相似度
參數:
word1: 第一個詞彙
word2: 第二個詞彙
回傳:
相似度分數
"""
vec1 = self.loader.get_vector(word1)
vec2 = self.loader.get_vector(word2)
if vec1 is None or vec2 is None:
missing = []
if vec1 is None:
missing.append(word1)
if vec2 is None:
missing.append(word2)
print(f"詞彙不在詞彙表中: {', '.join(missing)}")
return 0.0
return self.cosine_similarity(vec1, vec2)
def find_most_similar(self,
word: str,
topn: int = 10) -> List[Tuple[str, float]]:
"""
尋找與給定詞最相似的詞彙
參數:
word: 目標詞彙
topn: 回傳的相似詞數量
回傳:
(相似詞, 相似度分數) 的列表
"""
word_vec = self.loader.get_vector(word)
if word_vec is None:
print(f"詞彙 '{word}' 不在詞彙表中")
return []
# 計算與所有詞彙的相似度
similarities = []
for other_word, other_vec in self.word2vec.items():
if other_word == word:
continue
similarity = self.cosine_similarity(word_vec, other_vec)
similarities.append((other_word, similarity))
# 排序並回傳前 topn 個
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:topn]
def analogy(self,
word_a: str,
word_b: str,
word_c: str,
topn: int = 5) -> List[Tuple[str, float]]:
"""
詞類比任務: a 之於 b,如同 c 之於 ?
例如: man 之於 king,如同 woman 之於 queen
參數:
word_a: 類比的第一個詞
word_b: 類比的第二個詞
word_c: 類比的第三個詞
topn: 回傳結果數量
回傳:
類比結果列表
"""
# 獲取詞向量
vec_a = self.loader.get_vector(word_a)
vec_b = self.loader.get_vector(word_b)
vec_c = self.loader.get_vector(word_c)
if None in [vec_a, vec_b, vec_c]:
print("部分詞彙不在詞彙表中")
return []
# 計算目標向量: vec_b - vec_a + vec_c
target_vec = vec_b - vec_a + vec_c
# 尋找最接近目標向量的詞彙
similarities = []
exclude_words = {word_a, word_b, word_c}
for word, vec in self.word2vec.items():
if word in exclude_words:
continue
similarity = self.cosine_similarity(target_vec, vec)
similarities.append((word, similarity))
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:topn]
def get_sentence_embedding(self,
sentence: List[str],
method: str = 'mean') -> np.ndarray:
"""
獲取句子的嵌入表示
參數:
sentence: 詞彙列表
method: 聚合方法,'mean' 或 'sum'
回傳:
句子向量
"""
vectors = []
for word in sentence:
vec = self.loader.get_vector(word)
if vec is not None:
vectors.append(vec)
if not vectors:
return np.zeros(self.loader.embedding_dim)
vectors = np.array(vectors)
if method == 'mean':
return np.mean(vectors, axis=0)
elif method == 'sum':
return np.sum(vectors, axis=0)
else:
raise ValueError(f"不支援的聚合方法: {method}")
# 使用範例
if __name__ == '__main__':
# 注意: 實際使用時需要下載 GloVe 預訓練向量
# 下載地址: https://nlp.stanford.edu/projects/glove/
print("=" * 60)
print("GloVe 詞嵌入範例")
print("=" * 60)
# 這裡使用模擬資料展示 API 用法
# 實際應用中請使用真實的 GloVe 文件
# 創建載入器
loader = GloVeLoader(embedding_dim=100)
# 模擬載入過程
# loader.load_glove_file('glove.6B.100d.txt')
# 或從緩存載入
# loader.load_cache('glove_cache.pkl')
print("\n範例用法:")
print("-" * 60)
print("\n1. 獲取詞向量:")
print(" vector = loader.get_vector('computer')")
print("\n2. 計算詞彙相似度:")
print(" analyzer = GloVeAnalyzer(loader)")
print(" similarity = analyzer.word_similarity('king', 'queen')")
print("\n3. 尋找相似詞:")
print(" similar_words = analyzer.find_most_similar('computer', topn=5)")
print("\n4. 詞類比推理:")
print(" result = analyzer.analogy('man', 'king', 'woman')")
print(" # 預期結果: queen")
print("\n5. 句子嵌入:")
print(" sentence = ['I', 'love', 'natural', 'language', 'processing']")
print(" sent_vec = analyzer.get_sentence_embedding(sentence)")
這個程式碼展示了 GloVe 的完整使用流程。GloVeLoader 類別負責載入預訓練的 GloVe 向量,支援緩存機制以加速重複載入。在實務中,GloVe 文件通常很大(數 GB),首次載入需要較長時間,緩存機制能夠顯著改善後續的載入速度。
GloVeAnalyzer 類別提供了豐富的分析功能。word_similarity 方法透過餘弦相似度衡量詞彙的語義接近程度。find_most_similar 方法遍歷整個詞彙表尋找最相似的詞,這在探索性分析中非常有用。analogy 方法實現詞類比推理,透過向量運算 “b - a + c” 來尋找與 c 有相似關係的詞 d。get_sentence_embedding 方法透過平均或求和詞向量來得到句子級別的表示,這是許多下游任務的基礎。
GloVe 的優勢在於其訓練效率與解釋性。由於直接建模共現統計,GloVe 的訓練過程相對透明,我們可以理解模型為何學到特定的語義關係。GloVe 也提供了多種預訓練向量,涵蓋不同的訓練語料(Wikipedia、Common Crawl)與向量維度(50, 100, 200, 300),讓使用者可以根據需求選擇合適的版本。這種靈活性讓 GloVe 成為許多 NLP 專案的首選詞嵌入方案。
BERT:上下文感知的動態詞嵌入
BERT(Bidirectional Encoder Representations from Transformers)代表了詞嵌入技術的範式轉移。與 Word2Vec 和 GloVe 為每個詞分配固定向量不同,BERT 根據詞彙所在的具體上下文動態生成向量表示。這種動態特性讓 BERT 能夠準確捕捉一詞多義現象,“bank"在"river bank"與"bank account"中得到完全不同的向量表示,這是靜態詞嵌入無法實現的。
BERT 的核心是 Transformer 編碼器架構。Transformer 透過自注意力機制(Self-Attention)讓模型能夠關注輸入序列中任意位置的資訊,不受距離限制。這種全域依賴建模能力是 BERT 理解複雜語言現象的關鍵。BERT 使用雙向 Transformer,意味著模型同時利用左側與右側的上下文,這與傳統的單向語言模型(只利用左側上下文)形成鮮明對比。
BERT 的訓練採用兩個預訓練任務。遮罩語言模型(Masked Language Model, MLM)隨機遮罩輸入中的部分詞彙,要求模型根據上下文預測被遮罩的詞。這個任務迫使模型學習雙向的語言理解能力。下一句預測(Next Sentence Prediction, NSP)要求模型判斷兩個句子是否在原文中連續出現,這培養了模型理解句子間關係的能力。
讓我們透過程式碼來理解如何使用 BERT 獲取上下文感知的詞嵌入:
"""
BERT 詞嵌入提取與應用
展示如何使用 Transformers 庫獲取 BERT 嵌入
"""
import torch
from transformers import BertTokenizer, BertModel
import numpy as np
from typing import List, Tuple, Dict
from scipy.spatial.distance import cosine
class BERTEmbedding:
"""
BERT 嵌入提取器
支援詞級與句子級的嵌入提取
"""
def __init__(self,
model_name: str = 'bert-base-uncased',
device: str = 'cuda' if torch.cuda.is_available() else 'cpu'):
"""
初始化 BERT 模型
參數:
model_name: 預訓練模型名稱
device: 運算設備,'cuda' 或 'cpu'
"""
print(f"載入 BERT 模型: {model_name}")
print(f"使用設備: {device}")
self.device = device
self.tokenizer = BertTokenizer.from_pretrained(model_name)
self.model = BertModel.from_pretrained(model_name)
self.model.to(self.device)
self.model.eval() # 設置為評估模式
print("模型載入完成!")
def get_word_embedding(self,
sentence: str,
target_word: str,
layer: int = -1) -> np.ndarray:
"""
獲取特定詞彙在給定句子中的嵌入
參數:
sentence: 包含目標詞的句子
target_word: 目標詞彙
layer: 提取哪一層的輸出,-1 表示最後一層
回傳:
詞嵌入向量
"""
# 分詞
tokens = self.tokenizer.tokenize(sentence)
# 尋找目標詞的位置
target_indices = []
for i, token in enumerate(tokens):
if token.lower() == target_word.lower():
target_indices.append(i + 1) # +1 因為 [CLS] token
if not target_indices:
# 如果直接匹配失敗,嘗試子詞匹配
for i, token in enumerate(tokens):
if target_word.lower() in token.lower():
target_indices.append(i + 1)
if not target_indices:
raise ValueError(
f"未找到詞彙 '{target_word}' 在句子 '{sentence}' 中"
)
# 編碼輸入
encoded = self.tokenizer.encode_plus(
sentence,
add_special_tokens=True,
return_tensors='pt',
padding=True,
truncation=True
)
input_ids = encoded['input_ids'].to(self.device)
attention_mask = encoded['attention_mask'].to(self.device)
# 獲取模型輸出
with torch.no_grad():
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True
)
# 提取指定層的隱藏狀態
hidden_states = outputs.hidden_states[layer]
# 獲取目標詞的嵌入(如果有多個token,取平均)
word_embeddings = []
for idx in target_indices:
if idx < hidden_states.size(1):
embedding = hidden_states[0, idx, :].cpu().numpy()
word_embeddings.append(embedding)
if word_embeddings:
return np.mean(word_embeddings, axis=0)
else:
raise ValueError(f"無法提取詞彙 '{target_word}' 的嵌入")
def get_sentence_embedding(self,
sentence: str,
pooling: str = 'cls') -> np.ndarray:
"""
獲取句子的嵌入表示
參數:
sentence: 輸入句子
pooling: 池化策略,'cls' 或 'mean'
回傳:
句子嵌入向量
"""
# 編碼輸入
encoded = self.tokenizer.encode_plus(
sentence,
add_special_tokens=True,
return_tensors='pt',
padding=True,
truncation=True
)
input_ids = encoded['input_ids'].to(self.device)
attention_mask = encoded['attention_mask'].to(self.device)
# 獲取模型輸出
with torch.no_grad():
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask
)
if pooling == 'cls':
# 使用 [CLS] token 的表示
sentence_embedding = outputs.last_hidden_state[0, 0, :].cpu().numpy()
elif pooling == 'mean':
# 對所有 token 的表示取平均
# 排除 [PAD] token
mask = attention_mask[0].cpu().numpy()
hidden_states = outputs.last_hidden_state[0].cpu().numpy()
masked_hidden = hidden_states * mask[:, np.newaxis]
sentence_embedding = np.sum(masked_hidden, axis=0) / np.sum(mask)
else:
raise ValueError(f"不支援的池化策略: {pooling}")
return sentence_embedding
def compare_word_contexts(self,
sentences: List[str],
word: str) -> List[Tuple[str, float]]:
"""
比較同一個詞在不同上下文中的嵌入差異
參數:
sentences: 句子列表,都包含目標詞
word: 目標詞彙
回傳:
句子與其嵌入的相似度列表
"""
embeddings = []
valid_sentences = []
# 獲取每個句子中目標詞的嵌入
for sentence in sentences:
try:
embedding = self.get_word_embedding(sentence, word)
embeddings.append(embedding)
valid_sentences.append(sentence)
except ValueError as e:
print(f"跳過句子: {e}")
continue
if len(embeddings) < 2:
print("需要至少兩個有效句子來進行比較")
return []
# 計算第一個嵌入與其他嵌入的相似度
base_embedding = embeddings[0]
similarities = []
for i, (sentence, embedding) in enumerate(
zip(valid_sentences, embeddings)
):
if i == 0:
similarities.append((sentence, 1.0)) # 與自己的相似度為 1
else:
similarity = 1 - cosine(base_embedding, embedding)
similarities.append((sentence, similarity))
return similarities
class BERTAnalyzer:
"""
BERT 嵌入分析器
提供詞義辨析、句子相似度等功能
"""
def __init__(self, bert_embedding: BERTEmbedding):
"""
初始化分析器
參數:
bert_embedding: BERT 嵌入提取器實例
"""
self.bert = bert_embedding
def word_sense_disambiguation(self,
sentences: List[str],
word: str) -> Dict[str, List[str]]:
"""
詞義辨析:將包含同一詞彙的句子按語義聚類
參數:
sentences: 句子列表
word: 目標詞彙
回傳:
聚類結果字典
"""
# 提取所有嵌入
embeddings = []
valid_sentences = []
for sentence in sentences:
try:
embedding = self.bert.get_word_embedding(sentence, word)
embeddings.append(embedding)
valid_sentences.append(sentence)
except ValueError:
continue
if len(embeddings) < 2:
return {'cluster_0': valid_sentences}
# 簡單的聚類:計算所有對的相似度
# 實際應用中可以使用更複雜的聚類算法
embeddings_array = np.array(embeddings)
# 使用簡單的閾值聚類
threshold = 0.8
clusters = {}
cluster_id = 0
assigned = set()
for i, sentence in enumerate(valid_sentences):
if i in assigned:
continue
cluster_key = f'sense_{cluster_id}'
clusters[cluster_key] = [sentence]
assigned.add(i)
# 尋找相似的句子
for j in range(i + 1, len(valid_sentences)):
if j in assigned:
continue
similarity = 1 - cosine(embeddings[i], embeddings[j])
if similarity >= threshold:
clusters[cluster_key].append(valid_sentences[j])
assigned.add(j)
cluster_id += 1
return clusters
def sentence_similarity(self,
sentence1: str,
sentence2: str) -> float:
"""
計算兩個句子的語義相似度
參數:
sentence1: 第一個句子
sentence2: 第二個句子
回傳:
相似度分數
"""
emb1 = self.bert.get_sentence_embedding(sentence1)
emb2 = self.bert.get_sentence_embedding(sentence2)
return 1 - cosine(emb1, emb2)
# 使用範例
if __name__ == '__main__':
print("=" * 70)
print("BERT 詞嵌入範例")
print("=" * 70)
# 創建 BERT 嵌入提取器
bert = BERTEmbedding(model_name='bert-base-uncased')
print("\n" + "=" * 70)
print("示範 1: 上下文感知的詞嵌入")
print("=" * 70)
# 同一個詞在不同上下文中的嵌入
sentences_bank = [
"I went to the bank to deposit money.",
"The river bank was covered with flowers.",
"The bank will close at 5 PM today."
]
print("\n比較詞彙 'bank' 在不同上下文中的嵌入:")
results = bert.compare_word_contexts(sentences_bank, 'bank')
for sentence, similarity in results:
print(f"\n句子: {sentence}")
print(f"與第一個句子的相似度: {similarity:.4f}")
print("\n" + "=" * 70)
print("示範 2: 句子相似度")
print("=" * 70)
analyzer = BERTAnalyzer(bert)
sentence_pairs = [
("The cat sat on the mat.", "A feline rested on the rug."),
("I love machine learning.", "Machine learning is fascinating."),
("The weather is nice today.", "I bought a new car yesterday.")
]
print("\n句子對相似度:")
for sent1, sent2 in sentence_pairs:
similarity = analyzer.sentence_similarity(sent1, sent2)
print(f"\n句子 1: {sent1}")
print(f"句子 2: {sent2}")
print(f"相似度: {similarity:.4f}")
print("\n" + "=" * 70)
print("示範 3: 詞義辨析")
print("=" * 70)
# 詞義辨析範例
sentences_plant = [
"The plant is growing quickly.",
"The manufacturing plant employs 500 workers.",
"She planted flowers in the garden.",
"The nuclear power plant generates electricity."
]
print("\n對詞彙 'plant' 進行詞義聚類:")
clusters = analyzer.word_sense_disambiguation(sentences_plant, 'plant')
for sense, sentences in clusters.items():
print(f"\n{sense}:")
for sent in sentences:
print(f" - {sent}")
這個完整的程式碼展示了 BERT 的強大能力。BERTEmbedding 類別封裝了嵌入提取的核心功能,支援詞級與句子級的嵌入提取。get_word_embedding 方法展示了如何提取特定詞彙在給定上下文中的動態嵌入,這是 BERT 相對於靜態詞嵌入的關鍵優勢。
compare_word_contexts 方法展示了一個重要的分析:比較同一個詞在不同上下文中的嵌入差異。對於"bank"這個多義詞,在"river bank"與"bank account"兩個句子中的嵌入會有顯著差異,餘弦相似度會明顯低於同義語境中的相似度。這種能力讓 BERT 能夠準確理解一詞多義現象。
BERTAnalyzer 類別提供了實用的分析功能。word_sense_disambiguation 方法實現了自動的詞義辨析,透過聚類將包含相同詞彙但語義不同的句子分組。這在資訊檢索、問答系統等應用中非常有用。sentence_similarity 方法則展示了如何利用 BERT 進行句子語義相似度計算,這是文本匹配、語義搜尋等任務的基礎。
BERT 的優勢在於其上下文感知能力與預訓練帶來的語言理解深度。然而,BERT 也有其局限性。模型參數量大(BERT-base 有 110M 參數),推理速度較慢,這在資源受限或需要實時響應的場景中可能成為瓶頸。針對這個問題,研究者提出了多種 BERT 的輕量化版本,如 DistilBERT、ALBERT、TinyBERT 等,在保持大部分性能的同時大幅降低了模型大小與計算成本。
結論
詞嵌入技術的演進展現了自然語言處理領域的技術創新與範式轉移。從 Word2Vec 的分散式詞表示到 GloVe 的全域統計方法,從 fastText 的子詞單元處理到 BERT 的上下文感知動態嵌入,每一次技術進步都解決了前代方法的特定局限,同時也帶來了新的可能性與挑戰。
Word2Vec 透過簡潔優雅的神經網路架構,證明了從大規模語料庫中學習語義豐富的詞向量是可行且高效的。CBOW 與 Skip-gram 兩種架構為不同的應用場景提供了選擇,在實務中展現了廣泛的適用性。Word2Vec 的成功激發了詞嵌入技術的研究熱潮,為後續發展奠定了基礎。
GloVe 結合了預測式與計數式方法的優勢,透過直接建模詞彙共現統計來學習詞向量。這種方法提供了更好的理論解釋性,訓練效率也相對較高。GloVe 豐富的預訓練向量資源讓研究者與開發者能夠輕鬆地將高品質的詞嵌入應用於各種任務,降低了技術應用的門檻。
BERT 等基於 Transformer 的模型代表了詞嵌入技術的新典範。透過大規模預訓練與上下文感知機制,BERT 實現了對語言理解的重大突破。動態詞嵌入準確捕捉了一詞多義現象,雙向上下文利用提供了更豐富的語義資訊。BERT 在各種 NLP 任務上的優異表現,證明了預訓練加微調範式的有效性。
然而,選擇合適的詞嵌入技術需要綜合考慮多個因素。任務需求是首要考量,對於需要實時響應的應用,輕量級的靜態詞嵌入可能更合適;對於語義理解要求高的任務,BERT 等動態嵌入則不可或缺。計算資源的限制是另一個重要因素,BERT 的訓練與推理都需要大量資源,在資源受限的環境中可能需要考慮更輕量的替代方案。語料特性也會影響選擇,包含大量專業術語的領域語料可能從 fastText 的子詞單元機制中獲益更多。
展望未來,詞嵌入技術仍在持續演進。多語言與跨語言嵌入讓模型能夠處理多種語言並進行跨語言遷移。多模態嵌入整合文本、圖像、音訊等多種模態的資訊,提供更豐富的語義表示。領域自適應技術讓通用預訓練模型能夠快速適應特定領域,在專業應用中展現更好的性能。這些發展方向將進一步拓展詞嵌入技術的應用範圍與效能邊界。
作為開發者,深入理解不同詞嵌入技術的原理、優勢與限制,能夠幫助我們在實務工作中做出明智的技術選擇。沒有一種方法能夠適用於所有場景,成功的關鍵在於根據具體需求、資源限制與性能要求,選擇最合適的技術方案。持續關注詞嵌入技術的最新發展,並在實務中不斷實驗與優化,是構建高性能 NLP 系統的重要策略。希望本文提供的知識與經驗能夠協助讀者在詞嵌入技術的選擇與應用中取得成功。