一、自然語言處理(NLP)概述
1. 基本概念
? 自然語言處理(Natural Language Processing, NLP)是人工智能與計算語言學交叉的核心領域,致力于實現計算機對人類自然語言的自動理解、分析、生成與交互。其研究目標在于構建能夠處理文本或語音輸入,并執行語義解析、信息提取、語言生成等任務的計算系統。
NLP 的技術基礎涵蓋多個學科,包括:
- 計算機科學:提供算法設計、數據結構與系統實現支持;
- 人工智能:引入機器學習與深度學習方法,實現語言建模與推理;
- 語言學:為語法結構、語義表示與語用分析提供理論依據;
- 統計學與數學:支撐概率模型、向量空間表示與優化方法。
在中文語言環境下,NLP 面臨若干特有的技術挑戰,主要源于中文的語言特性:
- 分詞必要性:中文書寫不以空格分隔詞語,需依賴分詞算法(Word Segmentation)將連續字符序列切分為有意義的詞匯單元,例如將“北京冬奧會”切分為
["北京", "冬奧", "會"]
。 - 歧義問題:存在多種切分可能性,如“南京市長江大橋”可解析為“南京市/長江大橋”或“南京/市長/江大橋”,需結合上下文進行消歧。
- 形態貧乏:中文缺乏如英文的屈折變化(如時態、單復數),語法信息主要依賴語序與虛詞表達。
- 語義依賴上下文:省略、指代和語境依賴現象普遍,增加了語義解析的復雜性。
因此,中文 NLP 系統通常需要集成專用的分詞工具(如 Jieba、THULAC、LTP)以及針對中文語料訓練的語言模型(如 BERT-wwm、ERNIE、Chinese-BERT-wwm)。
2. 主要任務與功能
NLP 的研究與應用可劃分為兩大類:語言理解(Natural Language Understanding, NLU)與語言生成(Natural Language Generation, NLG)。
2.1 語言理解任務
語言理解旨在從輸入文本中提取結構化信息或語義表示,典型任務包括:
- 文本分類(Text Classification):將文本映射到預定義類別,如新聞分類(體育、財經、科技)、垃圾郵件檢測等。
- 情感分析(Sentiment Analysis):識別文本中表達的情感傾向,通常分為正面、負面與中性,廣泛應用于輿情監控與用戶反饋分析。
- 命名實體識別(Named Entity Recognition, NER):識別文本中具有特定意義的實體,如人名、地名、組織機構、時間等,并進行分類。
- 語義角色標注(Semantic Role Labeling, SRL):分析句子中謂詞與其論元之間的語義關系,例如識別“施事”、“受事”、“時間”、“地點”等角色。
- 問答系統(Question Answering, QA):根據自然語言問題,在給定文本中定位或生成答案,可分為抽取式問答與生成式問答。
- 句法分析(Syntactic Parsing):構建句子的語法結構樹,包括依存句法分析與成分句法分析。
2.2 語言生成任務
語言生成關注如何根據語義表示或結構化數據生成符合語法與語用規范的自然語言文本,主要任務包括:
- 機器翻譯(Machine Translation, MT):將源語言文本自動轉換為目標語言,如中英互譯。
- 文本摘要(Text Summarization):生成原文的簡潔摘要,分為抽取式(選取原文句子)與生成式(重寫表達)。
- 對話系統(Dialogue Systems):實現人機對話,包括任務型對話(如訂票)與開放域對話(如聊天機器人)。
- 文本續寫與創作:基于上下文生成連貫的后續文本,應用于故事生成、代碼補全等場景。
2.3 語音與文本轉換
NLP 也常與語音技術結合,形成完整的語音交互系統:
- 自動語音識別(Automatic Speech Recognition, ASR):將語音信號轉換為文本。
- 語音合成(Text-to-Speech, TTS):將文本轉換為自然語音輸出。
3. 技術實現路徑與應用實踐
3.1 應用場景
NLP 技術已廣泛應用于多個領域,包括但不限于:
- 信息檢索與搜索引擎:通過語義理解提升查詢與文檔的匹配精度。
- 智能客服與虛擬助手:實現自動化問答與任務執行。
- 社交媒體分析:進行情感分析、話題檢測與用戶畫像構建。
- 金融與法律文本處理:用于合同解析、風險預警與合規審查。
- 醫療自然語言處理:從電子病歷中提取臨床信息,輔助診斷決策。
3.2 技術實現流程
構建一個典型的 NLP 系統通常包括以下步驟:
-
數據預處理
- 文本清洗:去除噪聲、標準化編碼。
- 分詞與詞性標注:對中文文本進行分詞處理,并標注詞匯的語法屬性。
- 去除停用詞:過濾常見但無實際語義貢獻的詞匯(如“的”、“了”)。
-
特征表示
- 傳統方法:使用 One-Hot 編碼、TF-IDF 或 n-grams 表示文本。
- 現代方法:采用詞向量(Word Embedding)技術,如 Word2Vec、GloVe 或上下文相關表示(如 BERT)。
-
模型構建
- 傳統模型:樸素貝葉斯、支持向量機(SVM)、條件隨機場(CRF)。
- 深度學習模型:循環神經網絡(RNN)、長短期記憶網絡(LSTM)、Transformer 架構及其變體(如 BERT、T5)。
-
訓練與評估
- 在標注數據集上進行監督訓練。
- 使用準確率、精確率、召回率、F1 分數、BLEU、ROUGE 等指標評估模型性能。
-
部署與應用
- 將訓練好的模型集成至實際系統中,支持實時推理。
- 可通過 API 接口、微服務或嵌入式方式部署。
3.3 開發工具與資源
- 編程語言:Python 為當前主流開發語言,具備豐富的 NLP 庫支持。
- 常用工具庫:
- 中文分詞:Jieba、THULAC、LTP、HanLP。
- 英文處理:NLTK、spaCy。
- 深度學習框架:PyTorch、TensorFlow。
- 預訓練模型平臺:
- Hugging Face Transformers:提供大量開源預訓練模型(如 BERT、RoBERTa、T5)。
- 百度 PaddleNLP、阿里云 NLP API:支持中文場景的模型與服務。
二、NLP中的特征工程
想象一下,計算機像一個“外星人”,它天生只懂數字(0 和 1),完全不懂人類的語言。當我們把“北京”、“運動員”、“比賽”這些文字扔給它時,它一臉懵:“這是什么鬼符號?”
為了讓計算機能“理解”語言,我們必須把文字轉換成它能處理的數學形式——也就是向量(Vector)。
1. 傳統方法
1.1 One-Hot 編碼(不好用)
最簡單粗暴的方法是 One-Hot 編碼:
- 假設詞表有 10000 個詞。
- “北京” →
[1, 0, 0, ..., 0]
(第1位是1,其余是0) - “運動員” →
[0, 1, 0, ..., 0]
(第2位是1,其余是0) - “比賽” →
[0, 0, 1, ..., 0]
(第3位是1,其余是0)
問題:
- 維度爆炸:詞表越大,向量越長(10000維),非常稀疏(幾乎全是0),浪費內存。
- 沒有語義:向量之間完全獨立。
北京
和運動員
的向量點積是 0,說明它們“毫不相關”。但人類知道,它們都和“冬奧會”有關!計算機學不到這種語義相似性。
1.2 TF-IDF(引入權重)
TF-IDF 通過兩個指標為詞賦予權重:
- 詞頻(TF):將文本中的每個單詞視為一個特征,并將文本中每個單詞的出現次數除以該單詞在所有文檔中的出現次數,即詞在文檔中出現的頻率。
- 逆文檔頻率(IDF): 逆文檔頻率用來衡量一個詞在整個文檔集合(語料庫)中的重要性。它的目的是降低那些在很多文檔中頻繁出現的詞的權重,例如“the”、“is”這種常見詞,或者低頻罕見詞tetrafluoroethylene(四氟乙烯)。即詞在整個語料庫中出現越少,權重越高。
例如,“冬奧會”在體育新聞中 TF 高,IDF 也高(不是通用詞),因此 TF-IDF 值高,被認為是關鍵詞。
優點:能突出關鍵詞。
缺點:仍是高維稀疏表示,無法捕捉語義相似性。
結論:
- 文檔頻率和樣本語義貢獻程度呈反相關
- 文檔頻率和逆文檔頻率呈反相關
- 逆文檔頻率和樣本語義貢獻度呈正相關
1.3 n-grams(捕捉局部上下文)
n-grams 是特征工程中的一種技術,它通過將文本中的連續 n 個詞(或字符)組合起來,形成一個短語來捕捉文本中的局部上下文信息。n 可以為 1、2、3 等,具體取決于希望捕捉的上下文范圍。
什么是 n-grams?
- 1-gram(Unigram):每個單獨的詞作為一個單位。例如,“I love NLP” 的 1-gram 是
["I", "love", "NLP"]
。 - 2-grams(Bigram):相鄰的兩個詞組合成一個短語。例如,“I love NLP” 的 2-grams 是
["I love", "love NLP"]
。 - 3-grams(Trigram):相鄰的三個詞組合成一個短語。例如,“I love NLP” 的 3-grams 是
["I love NLP"]
。
n-grams 的作用
使用 n-grams 可以捕捉詞之間的局部上下文關系。例如,1-gram 只關心詞的獨立出現頻率,而 bigram 和 trigram 能捕捉到詞之間的順序關系。例如,bigram "love NLP"
表示詞 “love” 和 “NLP” 是一起出現的,這種信息在建模中會比僅僅知道 “love” 和 “NLP” 出現頻率更有價值。
n-grams 的示例
假設句子為 “I love NLP and machine learning”:
- 1-gram(Unigram):
["I", "love", "NLP", "and", "machine", "learning"]
- 2-grams(Bigram):
["I love", "love NLP", "NLP and", "and machine", "machine learning"]
- 3-grams(Trigram):
["I love NLP", "love NLP and", "NLP and machine", "and machine learning"]
通過這些 n-grams,模型可以捕捉到詞與詞之間的局部依賴關系。
? 將 n-grams 與 TF-IDF 相結合是文本特征工程中非常常見的做法,它不僅能夠捕捉詞與詞之間的局部關系,還能通過 TF-IDF 來衡量這些短語在整個語料庫中的重要性。結合的過程基本上是先生成 n-grams,然后對這些 n-grams 計算 TF-IDF 權重。
結合 n-grams 與 TF-IDF 的步驟:
- 生成 n-grams:首先從文本中生成 n-grams(n 可以是 1, 2, 3 等)。這些 n-grams 就像是詞的組合,通常使用
CountVectorizer
或類似的工具生成。 - 計算詞頻 (TF):統計每個 n-gram 在文本中出現的頻率。
- 計算逆文檔頻率 (IDF):計算 n-gram 在所有文檔中出現的頻率,稀有的 n-grams 會得到較高的權重,而常見的 n-grams 權重較低。
- 計算 TF-IDF:將每個 n-gram 的 TF 和 IDF 相乘,得到 TF-IDF 權重,表示該 n-gram 對特定文本的重要性。
注意:當使用 2-grams 時,I love
和 love NLP
被看作是兩個單獨的特征,總共有兩個特征(總特征數 = 2)。
2. 現代方法:詞向量(Word Embedding)
2.1 什么是詞向量?
詞向量就是為每個詞分配一個短得多的、稠密的、實數向量,比如 50 維、100 維、300 維。
例如:
- “北京” →
[0.2, -0.8, 1.5, 0.1, -0.3]
(5維) - “運動員” →
[0.4, -0.7, 1.3, 0.2, -0.2]
(5維) - “比賽” →
[0.3, -0.75, 1.4, 0.15, -0.25]
(5維)
理解:
- 這些數字不是隨便給的,而是通過大量文本訓練出來的。
- 語義相近的詞,它們的向量也相近!
- “北京” 和 “上海” 的向量距離很近。
- “國王” - “男人” + “女人” ≈ “王后” (著名的詞向量類比)
- 計算機可以通過計算向量之間的距離或相似度(如余弦相似度)來判斷詞語之間的關系。
一句話總結詞向量:
詞向量是將一個詞表示成一個低維、稠密的實數向量,使得語義或語法上相似的詞在向量空間中的位置也相近。
2.2 關于“維度”的三大問題
”維度“指的是什么?
這里的“維度”(Dimension)指的就是向量的長度,也就是這個向量由多少個數字組成。
- 1維:就是一個數字,比如
[3.14]
- 2維:就是平面上的一個點,比如
[1.0, 2.5]
(有 x 和 y 兩個坐標) - 3維:就是空間中的一個點,比如
[1.0, 2.5, 3.7]
(有 x, y, z 三個坐標) - 5維:就是
[0.2, -0.8, 1.5, 0.1, -0.3]
—— 它有 5 個數字。 - 100維:就是有 100 個數字排成一列。
關鍵:我們人類只能直觀理解 2D 或 3D 空間。5維、100維、300維的空間是抽象的數學空間,我們無法在大腦中“畫”出來,但數學上完全可以定義和計算。
總結:維度 = 向量的長度 = 表示一個詞用了多少個數字。
每個維度代表的是什么?(最核心的問題)
很多人會想:“第1維是不是代表‘地理位置’?第2維是不是代表‘情感傾向’?第3維是不是代表‘人/物’?這些具體的特征指標。”
答案是:NO,詞向量的每個維度沒有明確、可解釋的物理或語義含義。
正確的理解
-
整體才有意義:詞向量的整個向量(所有維度組合在一起)才代表這個詞的語義。就像你不能說“紅色”是由“波長”和“亮度”兩個獨立的“維度”組成,而是整個光譜特性定義了“紅色”。
-
分布式表示 (Distributed Representation):
- 一個詞的語義信息是分布在整個向量的所有維度上的。
- 每個維度都可能對多個不同的語義特征都有微弱的貢獻。
- 沒有哪個維度是專門負責“國家”或“動詞”的。
-
語義是幾何關系:
- 詞向量的威力不在于單個數字,而在于向量之間的幾何關系:
- 距離近:語義相似(如 “北京” 和 “上海” 的向量距離近)。
- 方向一致:語義類比(如 “國王” - “男人” + “女人” ≈ “王后”)。
- 計算機通過學習這些整體的模式和關系來理解語言。
- 詞向量的威力不在于單個數字,而在于向量之間的幾何關系:
一個類比
想象一下“顏色”。
- 我們用 RGB 三元組來表示顏色,比如
(255, 0, 0)
是紅色,(0, 255, 0)
是綠色。 - 你能說 R 分量(紅色)就代表“暖色調”嗎?不能!因為
(255, 255, 0)
(黃色)也是暖色,但 G 分量也很高。 - 顏色的“含義”是由 R、G、B 三個數字共同決定的,而不是單個分量。
詞向量也是一樣,一個 100 維的向量,就像一個 100 色的“調色盤”,每個維度都是一個“基礎顏色”,最終的“語義顏色”是所有“基礎顏色”混合的結果。
如何選擇維度?
維度是你自己設定的超參數(Hyperparameter),但它不是完全隨便寫的,需要根據任務、數據量、計算資源來選擇。
常見的維度選擇
- 50維、100維、200維、300維:這是非常經典的范圍。
- Google 的 Word2Vec 模型通常使用 100-300 維。
- Stanford 的 GloVe 模型也常用 100-300 維。
- 更小:5維、10維、20維 —— 用于教學演示、小型實驗或資源極度受限的場景。
- 更大:512維、768維、1024維 —— 現代大型預訓練模型(如 BERT、GPT)的詞向量維度。
維度大小 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
小 (如 5-50) | 計算快,內存占用小,模型小 | 表達能力弱,可能學不到復雜語義 | 教學、小型實驗、嵌入式設備 |
中 (如 100-300) | 表達能力足夠強,計算效率高 | - | 最常用,大多數 NLP 任務的首選 |
大 (如 512+) | 表達能力極強,能捕捉更細微的語義 | 計算慢,內存占用大,需要海量數據訓練 | 大型預訓練模型、追求極致性能 |
建議:
- 學習/實驗:從 5-10 維開始,便于觀察和理解。
- 實際項目:直接用 100 或 300 維,這是經過驗證的“黃金標準”。
3. 深度學習中的NLP的特征輸入
? 深度學習使用分布式單詞表示技術(也稱詞嵌入表示),通過查看所使用的單詞的周圍單詞(即上下文)來學習單詞表示。這種表示方式將詞表示為一個粘稠的序列,在保留詞上下文信息同時,避免維度過大導致的計算困難。
3.1 稠密編碼(特征嵌入)
稠密編碼(Dense Encoding)是一種將符號(如單詞、句子、圖像等)表示為低維、連續、實數向量的表示方法。其核心特征是:
- 低維:向量的維度相對較低,通常為幾十到幾百維(如 50、100、300 維),遠小于詞表大小。
- 稠密:向量中的每個元素都是非零的實數(如 0.23, -1.45, 0.89),與稀疏向量(如 One-Hot)形成鮮明對比。
- 連續:向量元素取值于實數域,支持梯度計算,可參與神經網絡的端到端訓練。
- 分布式表示(Distributed Representation):語義信息分布在整個向量的所有維度上,而非集中在某一個維度。
與稀疏編碼的對比
特性 | 稠密編碼(Dense) | 稀疏編碼(Sparse,如 One-Hot) |
---|---|---|
維度 | 低維(如 100) | 高維(等于詞表大小,如 10,000) |
向量值 | 大部分非零,實數 | 僅一個 1,其余為 0 |
存儲效率 | 高(存儲 100 個浮點數) | 低(存儲 10,000 個整數,但有效信息僅一個) |
語義表達 | 能表達語義相似性(通過向量距離) | 無法表達語義,所有詞向量正交 |
可學習性 | 支持梯度更新,可參與訓練 | 通常固定,不可學習 |
詞向量(Word Embedding)是稠密編碼在自然語言處理中最典型、最核心的應用形式。
3.2 詞嵌入算法
Embedding Layer(嵌入層) 是深度學習,自然語言處理(NLP)中一個核心的神經網絡層,其主要功能是將離散的類別型輸入(如單詞、字符、類別標簽等)映射為低維、連續、稠密的實數向量。這種向量表示被稱為“嵌入”(Embedding)。
簡單來說,Embedding Layer 是一個可學習的查找表(Learnable Lookup Table),它將每個類別(如詞匯表中的一個詞)關聯到一個固定長度的向量,并且這些向量在模型訓練過程中會不斷被優化,以更好地服務于最終的任務(如語言建模、文本分類等)。
那么怎么得到詞向量?
可以使用nn.Embedding
,它 是 PyTorch 中的一個神經網絡層,它的作用就是查找并返回詞向量。你可以把它想象成一個巨大的查表工具或字典。
API:
nn.Embedding(num_embeddings, embedding_dim)
num_embeddings
: 詞表大小(比如 10000)embedding_dim
: 每個詞向量的維度(比如 5)
PyTorch 會自動創建一個形狀為 (10000, 5)
的權重矩陣(可以理解為一個表格)。
這個矩陣的每一行,就對應一個詞的向量。初始化時,這些向量是隨機的(或按某種規則初始化,常用均勻分布和正態分布)。
使用:
輸入一個“詞的編號”(索引):
- 你不能直接把“北京”這個字扔給
Embedding
。 - 你必須先把它轉換成一個數字編號(index),比如“北京”對應
idx=5
。 - 所以輸入是一個
LongTensor
,比如torch.LongTensor([5])
。
輸出對應的“詞向量”:
Embedding
層會去它內部的(10000, 5)
表格中,找到第 5 行。- 返回這一整行,也就是“北京”這個詞的 5 維向量。
- 輸出形狀是
(1, 5)
。
關鍵點
nn.Embedding
本身不進行復雜的計算(不像Linear
層有矩陣乘法),它本質上是一個高效的查表操作(也叫“嵌入查找”)。- 這個“詞向量表”(權重矩陣)是可學習的參數
- 在訓練過程中(比如訓練一個語言模型),模型會根據任務目標(如預測下一個詞)不斷調整這些向量。
- 最終,這些向量會自動學習到詞語的語義信息。
代碼示例:
import torch
import torch.nn as nn
import jieba# 1. 原始文本
text = '北京冬奧的進度條已經過半,不少外國運動員在完成自己的比賽后踏上歸途。'# 2. 分詞
words = jieba.lcut(text)
print("分詞結果:", words)
# 輸出: ['北京', '冬奧', '的', '進度條', '已經', '過半', ',', '不少', '外國', '運動員', '在', '完成', '自己', '的', '比賽', '后', '踏上', '歸途', '。']# 3. 構建詞表(去重)并創建索引映射
# 關鍵:先去重,然后轉換為有序列表,再用 enumerate 賦予索引
words_set = list(set(words))
print("去重后的詞表:", words_set)# 創建 word2idx 和 idx2word
word2idx = {}
idx2word = {}
for i, word in enumerate(words_set):word2idx[word] = iidx2word[i] = wordprint("word2idx 映射:", word2idx)
print("idx2word 映射:", idx2word)# 4. 創建 Embedding 層
vocab_size = len(words_set) # 詞表大小
embedding_dim = 5 # 詞向量維度
embedding = nn.Embedding(vocab_size, embedding_dim)print("\nEmbedding 層:", embedding)
# 輸出: Embedding(18, 5) 表示有18個詞,每個5維
print("Embedding 權重形狀:", embedding.weight.shape) # 應該是 (18, 5)# 5. 查找并打印每個詞的詞向量
print("\n各詞的詞向量:")
for word in words:idx = word2idx[word] # 獲取詞的編號# 注意:embedding 輸入必須是 LongTensoridx_tensor = torch.LongTensor([idx])embedding_vector = embedding(idx_tensor) # 返回形狀為 (1, 5) 的張量print(f"{word} (idx={idx}) 的詞向量: {embedding_vector.squeeze().tolist()}")# 6. 單獨查找 '過半' 的詞向量
if '過半' in word2idx:idx = word2idx['過半']print(f"\n'過半' 的編號是: {idx}")embedding_vector = embedding(torch.LongTensor([idx]))print(f"'過半' 的詞向量: {embedding_vector.squeeze().tolist()}")
else:print("\n'過半' 不在詞表中!") # 理論上不會發生
輸出(隨機初始化,每次不同):
分詞結果: ['北京', '冬奧', '的', '進度條', '已經', '過半', ',', '不少', '外國', '運動員', '在', '完成', '自己', '的', '比賽', '后', '踏上', '歸途', '。']
去重后的詞表: ['完成', '北京', '已經', '進度條', ',', '歸途', '不少', '自己', '踏上', '過半', '后', '的', '外國', '。', '運動員', '在', '冬奧', '比賽', '自己']
word2idx 映射: {'完成': 0, '北京': 1, '已經': 2, '進度條': 3, ',': 4, '歸途': 5, '不少': 6, '自己': 7, '踏上': 8, '過半': 9, '后': 10, '的': 11, '外國': 12, '。': 13, '運動員': 14, '在': 15, '冬奧': 16, '比賽': 17}
idx2word 映射: {0: '完成', 1: '北京', 2: '已經', 3: '進度條', 4: ',', 5: '歸途', 6: '不少', 7: '自己', 8: '踏上', 9: '過半', 10: '后', 11: '的', 12: '外國', 13: '。', 14: '運動員', 15: '在', 16: '冬奧', 17: '比賽'}Embedding 層: Embedding(18, 5)
Embedding 權重形狀: torch.Size([18, 5])各詞的詞向量:
北京 (idx=1) 的詞向量: [0.2143, -0.4567, 0.8912, -0.3456, 0.1234]
冬奧 (idx=16) 的詞向量: [-0.1234, 0.5678, -0.2345, 0.6789, -0.4567]
的 (idx=11) 的詞向量: [0.3456, -0.6789, 0.4567, -0.7890, 0.5678]
...'過半' 的編號是: 9
'過半' 的詞向量: [0.5678, -0.1234, 0.3456, -0.2345, 0.7890]
這些數字就是“北京”、“冬奧”等詞當前的詞向量表示。隨著模型訓練,這些數字會不斷調整,直到能最好地完成任務(如預測下一個詞)。
3.3 Word2Vec (2013, Google) (詞向量訓練方法)
Word2Vec 是一個淺層神經網絡模型,它通過兩個巧妙的“代理任務”(Proxy Tasks)來學習詞向量:
方法一:CBOW (Continuous Bag-of-Words)
- 任務:根據上下文詞,預測中間的中心詞。
- 輸入:
[“北京”, “的”, “首都”]
- 輸出:預測
“城市”
- 輸入:
- 原理:為了讓模型準確預測“城市”,它就必須給“北京”、“的”、“首都”這些詞分配合適的向量,使得它們的向量之和(或平均)能最好地代表“城市”的含義。在這個過程中,詞向量就被學習出來了。
代碼示例:
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity# ======================
# 1. 數據預處理與詞匯表構建
# ======================
text = "北京 是 中國的 首都 北京 有 故宮 上海 是 中國的 經濟 中心 上海 有 外灘"
tokens = text.split()
print("Tokens:", tokens)# 構建有序詞表
word_counts = Counter(tokens)
vocab = sorted(word_counts.keys()) # 按字母排序,確保可復現
word_to_idx = {word: idx for idx, word in enumerate(vocab)}
idx_to_word = {idx: word for idx, word in enumerate(vocab)}
VOCAB_SIZE = len(vocab)
EMBEDDING_DIM = 10
CONTEXT_SIZE = 2 # 前后各2個詞print(f"詞匯表大小: {VOCAB_SIZE}, 詞匯: {vocab}")# --------------------------
# 構造訓練數據(上下文 -> 中心詞)
# --------------------------
def make_cbow_data(tokens, context_size):data = []for i in range(context_size, len(tokens) - context_size):context = tokens[i-context_size:i] + tokens[i+1:i+context_size+1]target = tokens[i]data.append((context, target))return datacbow_data = make_cbow_data(tokens, CONTEXT_SIZE)
print("CBOW 訓練樣本示例:", cbow_data[:3])# --------------------------
# CBOW 模型定義
# --------------------------
class CBOW(nn.Module):def __init__(self, vocab_size, embedding_dim):super(CBOW, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.fc = nn.Linear(embedding_dim, vocab_size)def forward(self, context_indices):# context_indices: (batch_size, context_len)embeds = self.embedding(context_indices) # (B, C, D)context_vec = embeds.sum(dim=1) # (B, D)output = self.fc(context_vec) # (B, V)return torch.log_softmax(output, dim=1)# 初始化模型、損失、優化器
model = CBOW(VOCAB_SIZE, EMBEDDING_DIM)
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01) # 稍微調高學習率# --------------------------
# 訓練模型(小批量)
# --------------------------
BATCH_SIZE = 4
EPOCHS = 200for epoch in range(EPOCHS):total_loss = 0.0# 簡單的小批量處理for i in range(0, len(cbow_data), BATCH_SIZE):batch = cbow_data[i:i+BATCH_SIZE]context_batch = []target_batch = []for context, target in batch:context_idx = [word_to_idx[w] for w in context]context_batch.append(context_idx)target_batch.append(word_to_idx[target])# 轉為 Tensorcontext_tensor = torch.LongTensor(context_batch) # (B, 4)target_tensor = torch.LongTensor(target_batch) # (B,)# 前向傳播log_probs = model(context_tensor)loss = loss_function(log_probs, target_tensor)# 反向傳播optimizer.zero_grad()loss.backward()optimizer.step()total_loss += loss.item()if epoch % 50 == 0:print(f"Epoch {epoch}, Average Loss: {total_loss/len(cbow_data):.4f}")# 獲取訓練后的詞向量矩陣
trained_embeddings = model.embedding.weight.data.numpy() # 或 model_cbow.embedding.weight
print("詞向量形狀:", trained_embeddings.shape) # (VOCAB_SIZE, EMBEDDING_DIM)# 查看“北京”和“上海”的向量
vec_beijing = trained_embeddings[word_to_idx["北京"]]
vec_shanghai = trained_embeddings[word_to_idx["上海"]]# 計算余弦相似度
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity([vec_beijing], [vec_shanghai])[0][0]
print(f"'北京' 和 '上海' 的余弦相似度: {sim:.4f}")# 輸出所有詞向量(可選)
for word, idx in word_to_idx.items():print(f"{word}: {trained_embeddings[idx][:4]}...") # 顯示前4維
方法二:Skip-Gram
- 任務:根據中心詞,預測它周圍的上下文詞。
- 輸入:
“城市”
- 輸出:預測
“北京”
,“的”
,“首都”
(或其中一部分)
- 輸入:
- 原理:為了讓模型能從“城市”預測出“北京”和“首都”,它就必須讓“城市”、“北京”、“首都”的向量在空間中彼此接近。
Word2Vec 的核心:它不關心預測任務本身有多準確,它關心的是在完成這個任務時,詞向量表(
nn.Embedding
層的權重)是如何被調整的。最終,這個被調整好的詞向量表就是我們想要的。
import torch
import torch.nn as nn
import torch.optim as optim
from collections import Counter
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity# ======================
# 1. 數據預處理
# ======================
text = "北京 是 中國的 首都 北京 有 故宮 上海 是 中國的 經濟 中心 上海 有 外灘"
tokens = text.split()
print("Tokens:", tokens)vocab = sorted(set(tokens))
word_to_idx = {word: idx for idx, word in enumerate(vocab)}
idx_to_word = {idx: word for idx, word in enumerate(vocab)}
VOCAB_SIZE = len(vocab)
EMBEDDING_DIM = 10
CONTEXT_SIZE = 2print(f"詞匯表大小: {VOCAB_SIZE}, 詞匯: {vocab}")# --------------------------
# 構造 Skip-Gram 數據(動態窗口)
# --------------------------
def make_skipgram_data(tokens, context_size):data = []for i in range(context_size, len(tokens) - context_size):target = tokens[i]# 使用 CONTEXT_SIZE 動態構造上下文context = tokens[i - context_size:i] + tokens[i+1:i + context_size + 1]for ctx_word in context:data.append((target, ctx_word))return dataskipgram_data = make_skipgram_data(tokens, CONTEXT_SIZE)
print("Skip-Gram 訓練樣本示例:", skipgram_data[:6])# --------------------------
# Skip-Gram 模型
# --------------------------
class SkipGram(nn.Module):def __init__(self, vocab_size, embedding_dim):super(SkipGram, self).__init__()self.embedding = nn.Embedding(vocab_size, embedding_dim)self.fc = nn.Linear(embedding_dim, vocab_size)def forward(self, target_idx):embed = self.embedding(target_idx)output = self.fc(embed)return torch.log_softmax(output, dim=1)# 初始化
model = SkipGram(VOCAB_SIZE, EMBEDDING_DIM)
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01) # 提高學習率# --------------------------
# 小批量訓練
# --------------------------
BATCH_SIZE = 4
EPOCHS = 200for epoch in range(EPOCHS):total_loss = 0.0for i in range(0, len(skipgram_data), BATCH_SIZE):batch = skipgram_data[i:i+BATCH_SIZE]target_idx = torch.LongTensor([word_to_idx[t] for t, _ in batch])context_idx = torch.LongTensor([word_to_idx[c] for _, c in batch])log_probs = model(target_idx)loss = loss_function(log_probs, context_idx)optimizer.zero_grad()loss.backward()optimizer.step()total_loss += loss.item()if epoch % 50 == 0:print(f"Epoch {epoch}, Loss: {total_loss:.4f}")# --------------------------
# 提取詞向量
# --------------------------
trained_embeddings = model.embedding.weight.data.numpy()
print("詞向量形狀:", trained_embeddings.shape)vec_beijing = trained_embeddings[word_to_idx["北京"]]
vec_shanghai = trained_embeddings[word_to_idx["上海"]]
sim = cosine_similarity([vec_beijing], [vec_shanghai])[0][0]
print(f"'北京' 和 '上海' 的余弦相似度: {sim:.4f}")for word, idx in word_to_idx.items():print(f"{word}: {trained_embeddings[idx][:4]}...")