NLP Day 4 從詞向量到語意表示:Word2Vec、GloVe 與 sentence-transformers
執行需求:CPU 可跑。今天進入 NLP 系列的核心技術:把文字轉成向量。會講三個層次:詞向量(Word2Vec、GloVe)把每個詞轉成一個固定向量;句向量(sentence-transformers)把整句話轉成一個向量;語意相似度(cosine similarity)量兩段文字的接近程度。讀完之後你會知道怎麼用語意向量做「找相似句」、「找相似段落」、「找相似文件」,這是後續 RAG 與推薦系統的基礎。
引言
為什麼要把文字轉成向量?因為電腦看不懂文字,但很會算數字。如果兩段文字能用向量表示,我們就能用 cosine 相似度找出「意思相近」的句子;用歐氏距離找出「意思最遠」的句子;用向量加減做「國王 − 男人 + 女人 = 女王」這種語意運算。這些運算在 2013 年 Word2Vec 問世後變得可行,到 2018 年 BERT 把整個 NLP 生態翻了一遍,再到 2022–2024 年 sentence-transformers 把句向量做到接近人類水準,向量化已經是現代 NLP 的基礎建設。
今天會做四件事:第一,介紹詞向量的直覺:為什麼「相近詞」在向量空間中會聚集在一起;第二,用 gensim 實作 Word2Vec 與 FastText;第三,介紹 GloVe 預訓練詞向量;第四,示範 sentence-transformers 的句向量用法,並用語意相似度做一個小型的「找最相似句子」範例。最後會比較詞向量與句向量的差異,並解釋「為什麼 RAG 篇章幾乎都用句向量而不是詞向量」。
注意:本篇固定使用 2025 年 3 月前可用的主流版本——gensim 4.3、sentence-transformers 3.4、numpy 1.26、scikit-learn 1.6。如果你用的是 4.0 之前的 gensim,API 略有差異。
詞向量的直覺
詞向量(word embedding)的核心假設是「一個詞的意義由它周圍的詞決定」(distributional hypothesis)。這個假設來自語言學家 Firth 的名言:「You shall know a word by the company it keeps」。意思是說,「蘋果」這個詞的意義,可以由「吃」、「果園」、「電腦」、「賈伯斯」這些常跟它共現的詞推敲出來。
Word2Vec(2013 年由 Mikolov 在 Google 提出)有兩種訓練架構:
- CBOW(Continuous Bag-of-Words):給定上下文詞,預測中間詞。例如給「蘋果 / 電腦 / 賈伯斯」,預測中間的「公司」或「品牌」。
- Skip-gram:給定中間詞,預測上下文詞。例如給「賈伯斯」,預測周圍的「蘋果」、「電腦」、「皮克斯」等。
兩種架構都能學出高品質的詞向量,Skip-gram 對低頻詞較友善、CBOW 訓練速度較快。實際效果差異不大,多數場景選 Skip-gram。
詞向量最有名的 demo 是「國王 − 男人 + 女人 ≈ 女王」。這個運算在 300 維向量空間中真的能成立,意思是說「性別」這個語意維度被線性地編碼到向量裡,可以用向量運算操作。這種「語意可運算」的特性,是後續句向量、文件向量的基礎。
GloVe 預訓練詞向量
GloVe(Global Vectors,2014 年由史丹佛團隊提出)跟 Word2Vec 不同的是,它用「全域共現矩陣」訓練,而 Word2Vec 用「局部滑動視窗」。兩者的效果差不多,但 GloVe 在小語料上往往更穩定。Stanford 提供預訓練好的 GloVe 向量(基於維基百科 + Gigaword 共 60 億詞、40 萬詞彙、300 維、約 5 GB),可以直接下載使用:
# 下載 GloVe 預訓練向量(Common Crawl 840B tokens)
mkdir -p ./glove
curl -L -o ./glove/glove.840B.300d.zip http://nlp.stanford.edu/data/glove.840B.300d.zip
unzip ./glove/glove.840B.300d.zip -d ./glove
這個下載檔案約 2 GB,解壓後約 5 GB。完成後可以用 gensim 載入:
import gensim.downloader as api
# 直接從 gensim 提供的下載器取得(更穩定)
glove = api.load("glove-wiki-gigaword-300")
print(f"詞彙數:{len(glove.key_to_index)}")
# 輸出:詞彙數:400000
print(f"向量維度:{glove.vector_size}")
# 輸出:向量維度:300
# 找「king」的相似詞
print("\n「king」的相似詞:")
for word, score in glove.most_similar("king", topn=5):
print(f" {word}:{score:.4f}")
# 輸出範例:
# 「king」的相似詞:
# prince:0.7680
# queen:0.7509
# ii:0.7308
# emperor:0.7189
# son:0.7097
# 國王 - 男人 + 女人 ≈ 女王
result = glove.most_similar(
positive=["king", "woman"],
negative=["man"],
topn=3,
)
print("\n國王 - 男人 + 女人:")
for word, score in result:
print(f" {word}:{score:.4f}")
# 輸出範例:
# 國王 - 男人 + 女人:
# queen:0.7697
# monarch:0.6848
# princess:0.6735
這段程式展示 GloVe 的兩個關鍵功能:第一,most_similar("king") 找出語意最接近的詞;第二,most_similar(positive, negative) 做向量加減,這個功能在 2014 年發表時是熱門話題,後續雖然被 BERT 等更強模型取代,但作為教學與直觀理解仍然很有用。實際數字會依 GloVe 版本略有不同。
Word2Vec 訓練:gensim 範例
如果有自己的領域語料(醫療、法律、財經),通用預訓練向量不夠用,可以用 gensim 從零訓練 Word2Vec:
from gensim.models import Word2Vec
# 假設語料已經斷詞完成,每行是一個 token list
sentences = [
["我", "喜歡", "吃", "蘋果"],
["蘋果", "是", "一種", "水果"],
["香蕉", "和", "蘋果", "都", "是", "水果"],
["我", "也", "喜歡", "香蕉"],
["水果", "富含", "維生素"],
["維生素", "對", "健康", "有益"],
["健康", "的", "飲食", "包含", "水果"],
["蔬菜", "也", "富含", "維生素"],
["我", "每天", "吃", "蔬菜", "和", "水果"],
] * 100 # 重複讓訓練看得見效果
model = Word2Vec(
sentences=sentences,
vector_size=50,
window=3,
min_count=1,
workers=2,
epochs=10,
)
print(f"詞彙數:{len(model.wv.key_to_index)}")
# 輸出:詞彙數:16
print(f"向量維度:{model.wv.vector_size}")
# 輸出:向量維度:50
# 找「蘋果」的相似詞
sims = model.wv.most_similar("蘋果", topn=3)
print("\n「蘋果」的相似詞:")
for w, s in sims:
print(f" {w}:{s:.4f}")
# 輸出範例:
# 「蘋果」的相似詞:
# 香蕉:0.8123
# 水果:0.7891
# 富含:0.4521
# 實際數字會依隨機種子略有不同
# 存成純文字格式(與 GloVe 相容)
model.wv.save_word2vec_format("my_vectors.txt")
print("\n已存成 my_vectors.txt")
這段程式用 gensim 4.3 訓練一個小型 Word2Vec 模型。vector_size=50 是向量維度(通常用 100 到 300),window=3 是滑動視窗大小(左右各看 3 個詞),min_count=1 表示詞頻低於 1 也保留(小型語料要設小,否則很多詞會被丟掉),epochs=10 是訓練輪數。實際上對大型語料,建議用 vector_size=300、window=5、epochs=5,並用 FastText 而不是 Word2Vec,因為 FastText 對未登入詞更友善。
句向量:sentence-transformers 的威力
詞向量把每個詞轉成向量,但「句子」、「段落」、「文件」是由多個詞組成,要怎麼表示?最直觀的做法是「把句子裡所有詞向量取平均」,但這個方法忽略了詞序與語法,效果不好。sentence-transformers(Reimers & Gurevych, 2019)用 Siamese Network 架構微調 BERT/Llama 等模型,讓「整句話的向量」直接有意義的距離度量。
2025 年 3 月,sentence-transformers 已經到 3.4 版,內建數十個預訓練句向量模型。最常用的是 all-MiniLM-L6-v2(80 MB、384 維、速度快)與 BAAI/bge-m3(2.3 GB、1024 維、多語言、效果強)。
from sentence_transformers import SentenceTransformer
from sentence_transformers.util import cos_sim
# 載入多語言句向量模型
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
print(f"模型最大序列長度:{model.max_seq_length}")
# 輸出:模型最大序列長度:256
sentences = [
"今天天氣真好,適合去爬山。",
"明天會下雨,應該待在家裡。",
"機器學習需要大量資料才能訓練。",
"深度學習是機器學習的一種。",
"台北101是台灣最高的建築物。",
]
embeddings = model.encode(sentences, normalize_embeddings=True)
print(f"句向量形狀:{embeddings.shape}")
# 輸出:句向量形狀:(5, 384)
# 計算 query 與所有句子的相似度
query = "戶外活動天氣"
query_emb = model.encode([query], normalize_embeddings=True)
sims = cos_sim(query_emb, embeddings)[0].tolist()
print(f"\n查詢:{query}")
print("相似度排序:")
ranked = sorted(zip(sentences, sims), key=lambda x: -x[1])
for s, score in ranked:
print(f" {score:.4f}:{s}")
# 輸出範例:
# 查詢:戶外活動天氣
# 相似度排序:
# 0.4521:今天天氣真好,適合去爬山。
# 0.2188:明天會下雨,應該待在家裡。
# 0.1421:台北101是台灣最高的建築物。
# 0.0891:深度學習是機器學習的一種。
# 0.0421:機器學習需要大量資料才能訓練。
# 實際數字會略有不同
這段程式用 sentence-transformers 把 5 個中文句子轉成向量,再用 cosine 相似度排序。可以看到「今天天氣真好,適合去爬山」最相關、「機器學習」最不相關,這個排序在小型示範上已經很合理。實際工作上,這個工具會被拿來做「找相似段落」、「推薦系統」、「RAG 檢索」。
BGE-m3:多語言長句向量
all-MiniLM-L6-v2 速度快,但只支援 256 個 token、英文與短句表現較好。中文長段落的話,BAAI/bge-m3 是更好的選擇(1024 維、8192 token、支援 100 多種語言):
from sentence_transformers import SentenceTransformer
# 用 BGE-m3 處理長段落
model = SentenceTransformer("BAAI/bge-m3", device="cpu")
long_paragraphs = [
"深度學習是機器學習的一個分支,它模擬人類大腦的神經網路結構,透過多層非線性轉換從大量資料中學習特徵表示。",
"自然語言處理是人工智慧領域的另一個重要分支,專注於讓電腦理解、生成人類語言,常見任務包括分類、翻譯、問答、摘要等。",
"RAG 是 2023 年後快速崛起的大型語言模型應用模式,透過檢索增強生成讓模型回答問題時參考外部知識庫,大幅降低幻覺。",
]
embeddings = model.encode(long_paragraphs, normalize_embeddings=True)
print(f"句向量形狀:{embeddings.shape}")
# 輸出:句向量形狀:(3, 1024)
# 計算兩兩相似度
from sentence_transformers.util import cos_sim
sim_matrix = cos_sim(embeddings, embeddings)
print("\n兩兩相似度矩陣:")
for i, p in enumerate(long_paragraphs):
print(f" [{i}] {p[:30]}...:{[round(float(sim_matrix[i][j]), 4) for j in range(len(long_paragraphs))]}")
# 輸出範例:
# 兩兩相似度矩陣:
# [0] 深度學習是機器學習的一個分支,它模擬人類大腦的神經網路結構...:[1.0, 0.6234, 0.5123]
# [1] 自然語言處理是人工智慧領域的另一個重要分支...:[0.6234, 1.0, 0.6456]
# [2] RAG 是 2023 年後快速崛起的大型語言模型應用模式...:[0.5123, 0.6456, 1.0]
# 實際數字會略有不同
BGE-m3 在中文段落上的表現比 all-MiniLM-L6-v2 好很多,原因有三:第一,它訓練時用了大量中英雙語語料;第二,它支援 8192 token,能處理完整段落而不是被切碎;第三,1024 維的表達能力比 384 維強。代價是模型大小(2.3 GB)與速度(CPU 上每秒約 50 句)。Day 22 與 Day 23 會更深入討論 BGE 系列與句向量模型選型。
完整實作:相似段落搜尋器
把今天的工具整合成一個「給一段查詢,找出最相似段落」的迷你搜尋引擎,這是 Day 24 完整語意搜尋系統的前身。
import numpy as np
from sentence_transformers import SentenceTransformer
from sentence_transformers.util import cos_sim
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
# 一個小型「文件庫」
corpus = [
"台灣的半導體產業在全球供應鏈中扮演關鍵角色。",
"台北101是台灣最高的建築物,也是重要的觀光景點。",
"機器學習需要大量標註資料才能訓練。",
"深度學習在影像與語音任務上表現優於傳統方法。",
"珍珠奶茶是台灣代表性的飲料,發源自台中。",
"大型語言模型能理解與生成自然語言。",
"高雄是台灣重要的港口城市,以工業與海運聞名。",
]
corpus_emb = model.encode(corpus, normalize_embeddings=True)
print(f"文件庫大小:{len(corpus)} 段,向量維度 {corpus_emb.shape[1]}")
# 輸出:文件庫大小:7 段,向量維度 384
def search(query, top_k=3):
"""給定查詢字串,回傳最相關的 top_k 段文件與相似度"""
q_emb = model.encode([query], normalize_embeddings=True)
sims = cos_sim(q_emb, corpus_emb)[0].numpy()
ranked = np.argsort(-sims)[:top_k]
return [(corpus[i], float(sims[i])) for i in ranked]
# 測試幾個查詢
queries = ["台灣的科技業", "台灣美食", "人工智慧模型"]
for q in queries:
print(f"\n查詢:{q}")
results = search(q, top_k=2)
for doc, score in results:
print(f" {score:.4f}:{doc}")
# 輸出範例:
# 查詢:台灣的科技業
# 0.5234:台灣的半導體產業在全球供應鏈中扮演關鍵角色。
# 0.3121:大型語言模型能理解與生成自然語言。
#
# 查詢:台灣美食
# 0.6456:珍珠奶茶是台灣代表性的飲料,發源自台中。
# 0.3421:台北101是台灣最高的建築物,也是重要的觀光景點。
#
# 查詢:人工智慧模型
# 0.5123:大型語言模型能理解與生成自然語言。
# 0.4321:深度學習在影像與語音任務上表現優於傳統方法。
# 實際數字會略有不同
# 額外示範:把整段文件 encode 後計算「最兩兩不相似的兩段」
print("\n文件兩兩相似度(找最不相似的配對):")
sim_matrix = cos_sim(corpus_emb, corpus_emb).numpy()
n = len(corpus)
min_sim = 1.0
min_pair = None
for i in range(n):
for j in range(i + 1, n):
diff_val = sim_matrix[i][j] - min_sim
if diff_val != abs(diff_val):
min_sim = sim_matrix[i][j]
min_pair = (i, j)
if min_pair:
print(f" 最不相似的兩段(相似度 {min_sim:.4f}):")
print(f" [{min_pair[0]}] {corpus[min_pair[0]]}")
print(f" [{min_pair[1]}] {corpus[min_pair[1]]}")
# 輸出範例:
# 最不相似的兩段(相似度 0.0421):
# [0] 台灣的半導體產業在全球供應鏈中扮演關鍵角色。
# [5] 大型語言模型能理解與生成自然語言。
# 實際結果會略有不同
這段程式展示完整的語意搜尋流程:建立文件庫、批次編碼、查詢時即時編碼、計算相似度、排序回傳。整個流程不到 50 行,這就是 RAG 檢索的核心機制。Day 24 會把它擴充成支援數萬段文件的向量資料庫系統,並加入 metadata filter、混合檢索等進階功能。實際數字會依模型版本略有不同。
常見錯誤與踩雷
第一個雷是「詞向量直接拿來做句向量」。把句子裡所有詞向量平均,得到的句向量品質很差。原因有三:忽略詞序、忽略句法、過度稀釋。對應排查:用 sentence-transformers 或 BERT [CLS] 向量做句向量,不要自己寫詞向量平均。
第二個雷是「訓練 Word2Vec 用的語料太小」。gensim 預設會安靜地訓練,但若語料只有幾百句、詞彙又少,學出來的向量品質很差。對應排查:至少要有 1 萬句以上的語料;領域專用詞向量至少要 10 萬句。
第三個雷是「GloVe 預訓練向量沒做正規化」。GloVe 預設向量沒有 normalize,做 cosine 相似度前要先 normalize_embeddings=True。對應排查:訓練時或查詢時加 normalize=True,讓所有向量長度為 1。
第四個雷是「忽略中英文編碼」。gensim 載入中文語料時常見 UnicodeDecodeError。對應排查:讀檔時顯式指定 encoding="utf-8",必要時先做 text.encode("utf-8").decode("utf-8-sig") 處理 BOM。
第五個雷是「Word2Vec 訓練時把未登入詞丟掉」。gensim 預設 min_count=5,低頻詞會被丟;但實務上領域專有名詞(例如合約編號、藥品型號)往往只出現幾次。對應排查:用 FastText 取代 Word2Vec,FastText 支援 subword 訓練,能為未登入詞生成向量。
效能與實務提醒
Word2Vec 在 CPU 上訓練百萬句中文語料約 10 分鐘(50 維、window=3、workers=4)。GloVe 預訓練向量只需載入(5 GB 載入約 20 秒)。sentence-transformers 編碼 1000 句英文約 1 秒(all-MiniLM-L6-v2 on CPU);BGE-m3 同樣 1000 句約 30 秒。
FastText 是 Facebook 2016 年提出的詞向量訓練工具,特色是用 character n-gram 來表示詞,因此能處理未登入詞(OOV)。下面示範 FastText 取代 Word2Vec 的訓練:
from gensim.models import FastText
# FastText 用 character n-gram 訓練,未登入詞也能得到向量
model = FastText(
sentences=sentences,
vector_size=50,
window=3,
min_count=1,
workers=2,
epochs=10,
min_n=3,
max_n=6,
)
# 即使 "超級蘋果" 在訓練語料中沒出現過,也能取到向量
vector = model.wv["超級蘋果"]
print(f"未登入詞「超級蘋果」的向量形狀:{vector.shape}")
# 輸出:未登入詞「超級蘋果」的向量形狀:(50,)
# 同樣支援 most_similar 介面
sims = model.wv.most_similar("蘋果", topn=3)
print("\n「蘋果」的相似詞:")
for w, s in sims:
print(f" {w}:{s:.4f}")
# 輸出範例:
# 「蘋果」的相似詞:
# 香蕉:0.8123
# 水果:0.7891
# 蔬菜:0.5234
FastText 的 min_n=3, max_n=6 表示用 3 到 6 個字元的 n-gram 來表示詞。「超級蘋果」這個未在訓練語料出現的詞,可以被切成「超」、「級」、「蘋」、「果」、「超級」、「級蘋」、「蘋果」、「超級蘋」、「級蘋果」、「超級蘋果」等 subword,從這些 subword 的向量加總得到「超級蘋果」的向量。對中文來說特別有用,因為新詞(例如品牌名、產品型號)會持續出現。
領域專用語料建議流程:先用通用 GloVe / sentence-transformers 做 baseline;再用領域語料做 fine-tuning,最後用任務資料評估。直接從零訓練領域 Word2Vec 通常不如「通用 + 微調」。
最後提醒:句向量模型的選擇對 RAG 品質影響巨大。簡單的經驗法則是「資料量小用 all-MiniLM-L6-v2、資料量大用 BGE-m3、英文為主用 BGE-large-en-v1.5」。Day 22 會有完整的選型指南。
小結
今天把詞向量到句向量的演進一次走完:Word2Vec 開創了「詞的向量表示」時代,GloVe 提供高品質的預訓練詞向量,sentence-transformers 把整句話的語意塞進一個向量裡。我們用 gensim 訓練了小型 Word2Vec、用 GloVe 跑了「國王 - 男人 + 女人」的經典範例、用 sentence-transformers 與 BGE-m3 做了語意搜尋。明天 Day 5 會進入文本分類:從最簡單的 TF-IDF 基線開始,一路做到 BERT 微調,並比較兩種方法在中文與英文上的差異。
結語
詞向量到句向量的演進,是 NLP 從「規則式」走向「表示式」的關鍵轉折。今天的工具(gensim、sentence-transformers)已經把這個轉折變成「一行就能用」的便利,但理解背後的直覺(distributional hypothesis、Siamese network、cosine similarity)仍然重要——很多 debug 工作依賴這些直覺。明天,我們會進入 NLP 最經典的任務之一:文本分類。會從最古典的 TF-IDF + Logistic Regression 基線開始,做出一個 80% 正確率的垃圾訊息分類器;再換成 BERT 微調,看看深度學習能把正確率推到多高。
延伸資源
- Mikolov et al., Efficient Estimation of Word Representations in Vector Space(arXiv:1301.3781,2013):
https://arxiv.org/abs/1301.3781 - Pennington et al., GloVe: Global Vectors for Word Representation(2014):
https://nlp.stanford.edu/projects/glove/ - Reimers & Gurevych, Sentence-BERT(arXiv:1908.10084,2019):
https://arxiv.org/abs/1908.10084 - BGE 系列模型卡(2025):
https://huggingface.co/BAAI/bge-m3 - sentence-transformers 預訓練模型清單(2025):
https://sbert.net/docs/sentence_transformer/pretrained_models.html
留言
張貼留言