一、自然语言处理(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
层有矩阵乘法),它本质上是一个高效的查表操作(也叫"嵌入查找")。- 这个"词向量表"(权重矩阵)是可学习的参数
- 在训练过程中(比如训练一个语言模型),模型会根据任务目标(如预测下一个词)不断调整这些向量。
- 最终,这些向量会自动学习到词语的语义信息。
代码示例:
python
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] = i
idx2word[i] = word
print("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 输入必须是 LongTensor
idx_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)
- 任务 :根据上下文词 ,预测中间的中心词 。
- 输入:
["北京", "的", "首都"]
- 输出:预测
"城市"
- 输入:
- 原理:为了让模型准确预测"城市",它就必须给"北京"、"的"、"首都"这些词分配合适的向量,使得它们的向量之和(或平均)能最好地代表"城市"的含义。在这个过程中,词向量就被学习出来了。
代码示例:
python
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 data
cbow_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 = 200
for 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])
# 转为 Tensor
context_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
层的权重)是如何被调整的。最终,这个被调整好的词向量表就是我们想要的。
python
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 = 2
print(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 data
skipgram_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 = 200
for epoch in range(EPOCHS):
total_loss = 0.0
for 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]}...")