⭐ 加分作業 截止:Session 5 上課前 建議花費 2–3 小時

用詞嵌入視覺化兩岸漢語差異

用 Ollama 在本機跑嵌入模型,把台灣漢語與中國漢語的詞彙差異變成你看得見的向量空間地圖。

背景概念

「詞嵌入」是什麼?為什麼能比較語言差異?

語言模型不直接處理文字,而是把每個詞轉換成一串數字(向量),讓語義相近的詞在數學空間中彼此靠近。這個轉換過程叫做詞嵌入(word embedding)。

向量空間類比

想像一張地圖,意思相近的詞住在同一個城市。「貓」和「狗」住得近,「貓」和「民主」住得遠。詞嵌入就是這張地圖的座標系統。

為何能捕捉兩岸差異?

如果模型的訓練語料混合了兩岸文本,「軟體」和「軟件」在向量空間中的位置就會揭示它們語義上的同異——它們靠近但不完全重疊。

nomic-embed-text

今天用的模型。Nomic AI 開源、多語言支援、在 Ollama 上一行指令即可下載,適合在筆電上跑。

降維視覺化(UMAP / t-SNE)

向量有幾百個維度,人眼看不到。透過降維演算法把它壓縮到 2D,就能畫出詞彙地圖——這就是你今天的目標。

語言學意義:這個作業在做的事,本質上是用計算方法驗證你的語言直覺——「出租車」和「計程車」語義上等價嗎?它們在向量空間中距離多近?同形詞「同志」在兩岸語境中有多大的語義漂移?

詞彙選集

推薦詞彙組:四類兩岸差異

選詞原則:優先選同形異義或異形近義,讓向量距離的差異更有解釋力。以下是四個語義場,每組至少包含一對對比詞。

科技借詞策略
台灣軟體 ↔ 中國軟件
台灣硬體 ↔ 中國硬件
台灣滑鼠 ↔ 中國鼠標
台灣影片 ↔ 中國視頻
台灣偏日語借詞策略(音近漢字),中國偏英語直譯。在向量空間中這兩組詞應呈現兩個語義簇。
日常交通與生活
台灣計程車 ↔ 中國出租車
台灣公車 ↔ 中國公交車
台灣馬鈴薯 ↔ 中國土豆
台灣泡麵 ↔ 中國方便麵
語義幾乎完全等價,但向量空間中距離多少?有趣的對照組。
同形詞・語義漂移
同志 台灣:LGBTQ+ 群體(1990s 以後);中國:黨員同志(政治稱謂)
本土 台灣:台灣認同/文化主體;中國:本地/國產(較中性)
素人 台灣:非專業者(借自日語 素人);中國:此詞較少見
嵌入模型見過兩岸語料,這類詞的向量會是「混合」的,最值得討論。
新媒體與網路用語
打卡
台灣:觀光景點朝聖/拍照紀念;中國:職場上下班刷卡記錄。同一個詞,兩種主要語境。
台灣網紅 ↔ 中國博主
台灣直播主 ↔ 中國主播
網路生態不同導致的用詞分歧。「主播」在台灣仍偏向新聞播報員。
自選詞彙加分:上面只是起點。你自己提出的詞彙組,若能說明選詞理由並分析結果,加分更多。思考方向:職場用語(試用期 vs 試用期✓;加班 vs 加班✓ — 同形但制度意涵不同);食物(火鍋 vs 麻辣燙);稱謂(老公 vs 先生的語用差異)。

實作流程

Step by step:五個階段

bash
# 確認 ollama 正在執行 $ ollama --version ollama version is 0.5.x # 下載嵌入模型(274 MB,比 LLM 小很多) $ ollama pull nomic-embed-text pulling manifest ... pulling 970aa74c0a90... ████████████████████ 100% 274 MB success # 確認已下載 $ ollama list NAME SIZE MODIFIED nomic-embed-text 274 MB just now
bash
$ pip install requests scikit-learn matplotlib --break-system-packages Successfully installed ...
Python — get_embeddings.py
import requests, json # ── 你的詞彙表(自由修改!)────────────────────── TW_WORDS = ["軟體", "硬體", "滑鼠", "影片", "計程車", "公車", "馬鈴薯", "泡麵", "網紅", "直播主", "打卡", "本土"] CN_WORDS = ["軟件", "硬件", "鼠標", "視頻", "出租車", "公交車", "土豆", "方便麵", "博主", "主播", "打卡", "本土"] # 注意:「打卡」和「本土」是同形詞,各出現一次 # ─────────────────────────────────────────────── def get_embedding(word): resp = requests.post("http://localhost:11434/api/embed", json={"model": "nomic-embed-text", "input": word}) return resp.json()["embeddings"][0] print("取得詞向量中...") tw_vecs = [get_embedding(w) for w in TW_WORDS] cn_vecs = [get_embedding(w) for w in CN_WORDS] print(f"完成!每個向量維度:{len(tw_vecs[0])}") # 存起來,下一步用 data = {"tw": {"words": TW_WORDS, "vecs": tw_vecs}, "cn": {"words": CN_WORDS, "vecs": cn_vecs}} json.dump(data, open("embeddings.json","w"), ensure_ascii=False) print("已儲存至 embeddings.json")
Python — visualize.py
import json, numpy as np import matplotlib.pyplot as plt import matplotlib.font_manager as fm from sklearn.manifold import TSNE # ── 讀取向量 ──────────────────────────────────── data = json.load(open("embeddings.json")) tw_words = data["tw"]["words"] cn_words = data["cn"]["words"] all_vecs = np.array(data["tw"]["vecs"] + data["cn"]["vecs"]) labels = tw_words + cn_words colors = ["#1a4a9e"] * len(tw_words) + ["#8b1a1a"] * len(cn_words) # ── t-SNE 降維到 2D ────────────────────────────── tsne = TSNE(n_components=2, random_state=42, perplexity=5) coords = tsne.fit_transform(all_vecs) # ── 畫圖 ──────────────────────────────────────── fig, ax = plt.subplots(figsize=(12, 9)) ax.set_facecolor("#faf9f7") fig.patch.set_facecolor("#faf9f7") for i, (x, y) in enumerate(coords): word = labels[i] c = colors[i] is_shared = (word in tw_words and word in cn_words) marker = "D" if is_shared else "o" # 菱形 = 同形詞 ax.scatter(x, y, c=c, s=120 if is_shared else 80, marker=marker, zorder=3, alpha=0.85) ax.annotate(word, (x, y), textcoords="offset points", xytext=(6, 4), fontsize=11, color=c, fontweight="bold") # ── 畫連線:台灣詞 ↔ 對應中國詞 ──────────────── for i, tw in enumerate(tw_words): if tw in cn_words: continue # 同形詞跳過 if i < len(cn_words): j = len(tw_words) + i # 對應中國詞在 all_vecs 的索引 ax.plot([coords[i,0], coords[j,0]], [coords[i,1], coords[j,1]], color="#aaa", linewidth=0.6, linestyle="--", zorder=1) # ── 圖例與標題 ────────────────────────────────── from matplotlib.lines import Line2D legend = [ Line2D([0],[0],marker='o',color='w',markerfacecolor='#1a4a9e',ms=9,label='台灣漢語'), Line2D([0],[0],marker='o',color='w',markerfacecolor='#8b1a1a',ms=9,label='中國漢語'), Line2D([0],[0],marker='D',color='w',markerfacecolor='#555',ms=9,label='同形詞'), ] ax.legend(handles=legend, loc='upper left', framealpha=.8, fontsize=11) ax.set_title("兩岸漢語詞彙的向量空間分布\n(nomic-embed-text · t-SNE 降維)", fontsize=14, pad=14) ax.axis("off") plt.tight_layout() plt.savefig("lexical_map.png", dpi=150, bbox_inches="tight") print("圖片已儲存為 lexical_map.png") plt.show()
中文字型問題:matplotlib 預設不支援中文。macOS 可在程式頂端加 plt.rcParams['font.family'] = 'Heiti TC';Windows 加 'Microsoft JhengHei';Linux 需先安裝 Noto Sans CJK。
Python — similarity.py
import json, numpy as np def cosine(a, b): a, b = np.array(a), np.array(b) return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) data = json.load(open("embeddings.json")) tw_w, tw_v = data["tw"]["words"], data["tw"]["vecs"] cn_w, cn_v = data["cn"]["words"], data["cn"]["vecs"] print(f"{'台灣詞':<8} {'中國詞':<8} {'餘弦相似度':>8} 解讀") print("─" * 52) for i in range(min(len(tw_w), len(cn_w))): sim = cosine(tw_v[i], cn_v[i]) note = "高度相近" if sim > 0.9 else ("相近" if sim > 0.75 else "有語義落差") print(f"{tw_w[i]:<8} {cn_w[i]:<8} {sim:>8.4f} {note}")
預期輸出(示意)
台灣詞 中國詞 餘弦相似度 解讀 ──────────────────────────────────────────────────── 軟體 軟件 0.9312 高度相近 硬體 硬件 0.9287 高度相近 滑鼠 鼠標 0.8841 相近 影片 視頻 0.8654 相近 計程車 出租車 0.9156 高度相近 馬鈴薯 土豆 0.8723 相近 打卡 打卡 0.9998 高度相近 ← 同形詞與自身比較 本土 本土 0.9998 高度相近 ← 嵌入模型無法區分兩岸語境
有趣的觀察:同形詞(如「打卡」「本土」)與自身比較的相似度接近 1.0,代表嵌入模型把它們當成同一個詞——這本身就是一個值得討論的發現:模型學到了「字形」,但未必學到「語境差異」。這是你反思作業的好素材。

繳交規定與評分

繳交內容與加分標準

繳交內容(壓縮成一個資料夾)

心得提示(reflection.md)

不需要寫論文,但要回答至少兩個問題:

評分標準

項目說明分數
程式完整執行三支腳本都能跑,有輸出圖片與數字+3
詞彙選擇使用推薦詞彙,加入至少 2 個自選詞彙並說明理由+2
心得反思有實質語言觀察,不只是描述程式步驟+3
批判性討論指出嵌入模型的限制,或提出延伸研究問題+2

滿分 10 分,計入期末總成績加分(上限 5% 總分)。


延伸挑戰(選做)

做完基本版,想更進一步?

換一個嵌入模型

試試 ollama pull mxbai-embed-large,比較兩個模型給出的詞彙地圖有何不同。為什麼會不同?

用句子取代單詞

把詞換成整句話,例如「打卡這家餐廳」vs「員工需要打卡上班」,觀察語境如何影響向量。

擴大詞彙表

收集 50 個以上的詞彙,用 UMAP 替換 t-SNE(需安裝 umap-learn),看看更大規模的語言地圖。

互動式視覺化

用 plotly 替換 matplotlib,製作可以 hover 查看詞彙的互動圖,並存成 HTML 分享。