2024-12-10 15:06:54
,某些文章具有时效性,若有错误或已失效,请在下方留言。词向量 vs 词嵌入
词向量
( Word Vector ) 通常也叫词嵌入
(Word Embeding),是一种寻找词语词之间相似性的 NLP 技术。它把词汇各个维度上的特征用数值向量进行表示,利用这些维度上特征的相似程度,就可以判断哪些词和哪些词的语义更接近。
相同点
它们都表示将词汇表中的单词映射到固定大小的连续向量空间的过程,这些向量可以捕捉词汇的语义信息,例如:相似语义的词在向量空间中余弦相似度高,距离也比较近,而不同语义的词余弦相似度较低,距离也较远。
不同点
词向量:通常用于描述具体的向量表示,即一个词对应的实际向量值。
词嵌入:通常用于描述将词映射到向量空间的过程或表示方法。它通常包含词训练算法和生成的词向量空间。
Word2Vec
Word2Vec
(Word to Vector) 是一种词向量学习算法。Word2Vec
采用一种高效的方法来学习词汇的连续向量表示,这种方式将词汇表中的每个词都表示成固定长度的向量。
稀疏向量:大部分元组的值为 0
,只有少数元素的值为非零。稀疏向量通常用于高维据,其中许多维度值为零。
稠密向量:大部分元素为非零值。稠密向量通常具有较低的维度,能同时捕捉到更丰富的信息。Word2Vec
就是一种典型的稠密向量表示。
Word2Vec
有两种主要的实现方式:
CBOW
(Continuous Bag of Words,连续词袋)模型-
Skip-Gram
(跳字) 模型
CBOW
模型通过给定的上下文词来预测目标词,Skip-Gram
模型通过给定目标词来预测上下文的词。这两个模型是通过神经网络来学习词向量,训练的过程中,通过最小化预测词和实际词之间的损失来学习词向量。当训练完成后,词向量可以从神经网络的权重中提取出来。
在继续之前,请在设备上安装 PyTorch
。
Skip-Gram 模型实现
构建语料库
创建一个简单的数据集,作为语料库,并整理出语料库的词汇表。
# 定义一个句子列表,后面会用这些句子寻灵 CBOW 和 Skip-Gram 模型
sentences = ["Kage is Teacher", "Mazong is Boss", "Niuzong is Boss","Xiaobing is Student", "Xiaoxue is Student",]
# 将所有句子连接在一起,然后用空格分隔成多个单词
words = ' '.join(sentences).split()
# 构建词汇表,去除重复的词
word_list = list(set(words))
# 创建一个字典,将每个词映射到一个唯一的索引
word_to_idx = {word: idx for idx, word in enumerate(word_list)}
# 创建一个字典,将每个索引映射到对应的词
idx_to_word = {idx: word for idx, word in enumerate(word_list)}
# 计算词汇表的大小
voc_size = len(word_list)
print("词汇表:", word_list)
# 输出词汇到索引的字典
print("词汇到索引的字典:", word_to_idx)
# 输出索引到词汇的字典
print("索引到词汇的字典", idx_to_word)
# 输出词汇表大小
print("词汇表大小:", voc_size)
输出结果,如下所示
词汇表: ['Boss', 'is', 'Teacher', 'Mazong', 'Xiaoxue', 'Student', 'Kage', 'Niuzong', 'Xiaobing']
词汇到索引的字典: {'Boss': 0, 'is': 1, 'Teacher': 2, 'Mazong': 3, 'Xiaoxue': 4, 'Student': 5, 'Kage': 6, 'Niuzong': 7, 'Xiaobing': 8}
索引到词汇的字典 {0: 'Boss', 1: 'is', 2: 'Teacher', 3: 'Mazong', 4: 'Xiaoxue', 5: 'Student', 6: 'Kage', 7: 'Niuzong', 8: 'Xiaobing'}
词汇表大小: 9
生成 Skip-Gram 数据
定义一个函数,从刚才的数据集中生成 Skip-Gram 训练数据。
# 生成 Skip—Gram 训练数据
def create_skipgram_dataset(sentences, window_size=2):
# 初始化数据
data = []
# 遍历句子
for sentence in sentences:
# 将句子分割成单词列表
sentence = sentence.split()
# 遍历单词及索引
for idx, word in enumerate(sentence):
# 获取相邻的单词,将当前的单词前后各 N 个单词作为相邻单词
for neighbor in sentence[max(idx - window_size, 0):min(idx + window_size, len(sentence))]:
# 排除单词本身
if neighbor != word:
# 将相邻单词与当前的单词作为一组训练数据
data.append((neighbor, word))
return data
# 使用函数创建 Skip—Gram 训练数据
skipgram_data = create_skipgram_dataset(sentences)
# 打印未编码的 Skip-Gram 数据样例
print("Skip_Gram 数据样例(未编码):", skipgram_data[:3])
输出结果,如下所示
Skip_Gram 数据样例(未编码): [('is', 'Kage'), ('Kage', 'is'), ('Teacher', 'is')]
Skip-Gram 模型的任务是根据给定的目标词来预测上下文词。因此数据集中每个元素包括一个目标词和它的一个上下文词。根据 Gram 窗口的大小,一个元素可能会有多个上下文词,那就形成多个 Skip-Gram 数据。
进行 One-Hot 编码
把 Skip-Gram 训练数据转换成 Skip-Gram 模型可以读入的 One-Hot 编码后的向量。
# 导入 torch 库
import torch
# 定义 One-Hot 编码函数
def one_hot_encoding(word, word_to_idx):
# 创建一个长度与词汇表相同的全 0 向量
tensor = torch.zeros(len(word_to_idx))
# 将对应词索引表位置上的值设置为 1
tensor[word_to_idx[word]] = 1
# 返回生成的 One-Hot编码后的向量
return tensor
# 展示One-Hot 编码前后的向量
word_example = "Teacher"
print("One-Hot编码前的单词:", word_example)
print("One-Hot编码后的向量:", one_hot_encoding(word_example, word_to_idx))
# 展示编码后的 Skip-Gram 训练数据样例
print("Skip-Gram数据样例(已编码):", [(one_hot_encoding(context, word_to_idx), word_to_idx[target]) for context, target in skipgram_data[:3]])
输出结果,如下所示
One-Hot编码前的单词: Teacher
One-Hot编码后的向量: tensor([0., 0., 0., 1., 0., 0., 0., 0., 0.])
Skip-Gram数据样例(已编码): [(tensor([0., 0., 0., 0., 0., 0., 0., 0., 1.]), 2), (tensor([0., 0., 1., 0., 0., 0., 0., 0., 0.]), 8), (tensor([0., 0., 0., 1., 0., 0., 0., 0., 0.]), 8)]
One-Hot 编码后的数据是向量,这种形式的向量就是之前提过的稀疏向量
,其长度等于词汇表大小,其中对应单词在词汇表中的索引位置上的值为1,其他位置上的值为0。
此处的目标就是通过学习,把这种稀疏向量
压缩成更具有表现力的低维稠密向量
。
定义 Skip-Gram 类
通过集成 PyTorch
的 nn.Model
类来实现 Skip-Gram
类
在
PyTorch
中,nn.Module
是一个非常重要的类,它是所有神经网络模型的基类。每一个模型都应该继承nn.Module
类,然后重写其中的方法。最基础且最重要的两个方法如下。
- __init__: 在这个方法中定义模型的各个层级和部分。所有的层级都应该是nn.Model类的子类,包括nn.Linear、nn.Conv2d、nn.ReLU。
- forward: 在这个方法中定义了模型的前向传播方式,也就是输入数据如何通过各个层级来生成输出。
# 定义 Skip-Gram 类
# 导入 neural network(神经网络)
import torch.nn as nn
class SkipGram(nn.Module):
def __init__(self, voc_size, embedding_size):
super(SkipGram, self).__init__()
# 从词汇表大小到嵌入层大小(维度)的线性层(权重矩阵)
self.input_to_hidden = nn.Linear(voc_size, embedding_size, bias=False)
# 从嵌入层大小(维度)到词汇表大小的线性层(权重矩阵)
self.hidden_to_output = nn.Linear(embedding_size, voc_size, bias=False)
# 向前传播的方式,X 形状为(batch_size, voc_size)
def forward(self, X):
# 通过隐藏层,hidden 形状为(batch_size, embedding_size)
hidden = self.input_to_hidden(X)
# 通过输出层,output_layer 形状为(batch_size, voc_size)
output = self.hidden_to_output(hidden)
return output
# 设定嵌入层大小
embedding_size = 2
# 实例化 Skip-Gram 模型
skipgram_model = SkipGram(voc_size, embedding_size)
print("Skip-Gram类:", skipgram_model)
输出的结果,如下所示
Skip-Gram类: SkipGram(
(input_to_hidden): Linear(in_features=9, out_features=2, bias=False)
(hidden_to_output): Linear(in_features=2, out_features=9, bias=False)
)
在这个简单的神经网络模型的 __init__
方法中定义了两个线性层(nn.Linear
也叫全连接层
),这两个线性层的权重矩阵中的参数是可学习的部分。
input_to_hidden
把 One-Hot 编码后的向量从词汇表大小映射到嵌入层大小,以形成并学习词的向量表示。- hidden_to_output 把词的向量便是从嵌入层大小映射回词汇表大小,以预测目标词。
而在 forward
方法中,定义了前向传播的方式,首先将输入通过输入层到隐藏层的映射生成隐藏层,然后将隐藏层通过隐藏层到输出层的映射生成输出。
训练 Skip-Gram 类
对创建的 Skip-Gram 类实例进行训练
# 训练 Skip-Gram 类
learning_rate = 0.001 # 设置学习速率
epochs = 1000 # 设置训练轮次
criterion = nn.CrossEntropyLoss() # 定义交叉熵损失函数
import torch.optim as optim # 导入随机梯度下降优化器
optimizer = optim.SGD(skipgram_model.parameters(), lr=learning_rate)
# 开始训练循环
loss_values = [] # 用于存储每轮的平均损失值
for epoch in range(epochs):
loss_sum = 0 # 初始化损失值
for context, target in skipgram_data:
X = one_hot_encoding(target, word_to_idx).float().unsqueeze(0) # 将中心词转换为 One-Hot 向量
y_true = torch.tensor([word_to_idx[context]], dtype=torch.long) # 将周围词转换为索引值
y_pred = skipgram_model(X) # 计算预测值
loss = criterion(y_pred, y_true) # 计算损失
loss_sum += loss.item() # 累积损失
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 更新参数
if (epoch+1) % 100 == 0: # 输出每 100 轮的损失,并记录损失
print(f"Epoch: {epoch+1}, Loss: {loss_sum/len(skipgram_data)}")
loss_values.append(loss_sum / len(skipgram_data))
# 绘制训练损失曲线
import matplotlib.pyplot as plt # 导入 matplotlib
# 绘制二维词向量图
plt.rcParams["font.family"]=['SimHei'] # 用来设定字体样式
plt.rcParams['font.sans-serif']=['SimHei'] # 用来设定无衬线字体样式
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
plt.plot(range(1, epochs//100 + 1), loss_values) # 绘图
plt.title(' 训练损失曲线 ') # 图题
plt.xlabel(' 轮次 ') # X 轴 Label
plt.ylabel(' 损失 ') # Y 轴 Label
plt.show() # 显示图
输出结果,如下所示
Epoch: 100, Loss: 2.1657068061828615
Epoch: 200, Loss: 2.130058722496033
Epoch: 300, Loss: 2.0789548778533935
Epoch: 400, Loss: 2.000198221206665
Epoch: 500, Loss: 1.8918296670913697
Epoch: 600, Loss: 1.775706675052643
Epoch: 700, Loss: 1.680455629825592
Epoch: 800, Loss: 1.6111684942245483
Epoch: 900, Loss: 1.5618559527397156
Epoch: 1000, Loss: 1.5270409035682677
输出的曲线图,如下
这段程序呈现了一个典型的 PyTorch 神经网络训练流程。
- 首先定义一些基本参数和预处理文本数据
- 创建模型实例,定义损失函数(交叉熵损失)和优化器(SGD 优化器)
- 进行多轮训练,尝试学习词嵌入
在训练的过程中,损失函数将模型的输出数据与目标数据进行比较,可以看到模型的损失随着训练的进行而降低,通过最小化这个损失,模型将接近上下文单词正确的概率分布。通过学习词与词之间的关系,表示模型对目标词的预测会越来越靠谱。
展示词向量
W2V中有两个权重矩阵:输入到隐藏层的权重矩阵input_to_hidden
和隐藏层到输出的权重矩阵hidden_to_output
。输入到隐藏层的权重矩阵蕴含着词嵌入的信息。这个矩阵的每一列和对应一个单词在词汇表中的索引,而这一列的元素的数值则表示该单词的词向量。通过提取这些词向量,我们可以在二维或三维空间中绘制它们,以观察词与词之间的相似性和关系。
# 输出 Skip-Gram 习得的词嵌入
print("Skip-Gram 词嵌入:")
for word, idx in word_to_idx.items(): # 输出每个词的嵌入向量
print(f"{word}: {skipgram_model.input_to_hidden.weight[:,idx].detach().numpy()}")
输出结果,如下所示
Skip-Gram 词嵌入:
Kage: [-0.4890965 -0.1904845]
is: [-0.05358342 -0.49305108]
Boss: [-0.3772864 1.122704 ]
Xiaobing: [-0.15191597 -0.1434522 ]
Mazong: [-0.81990975 0.35894954]
Student: [0.5589041 0.47023773]
Xiaoxue: [-0.03153982 -0.23713078]
Niuzong: [-0.71763086 0.22707257]
Teacher: [0.59581107 0.69417 ]
代码 skipgram_model.input_to_hidden.weight[:idx]
访问 skipgram_model
的 input_to_hidden
。索引[:idx]
选择了所有行和索引为 idx的列,也就是提取了对应索引 idx的单词的嵌入向量,而这个嵌入向量,是我们之前通过W2V 模型在语料库中习得的。
fig, ax = plt.subplots()
for word, idx in word_to_idx.items():
# 获取每个单词的嵌入向量
vec = skipgram_model.input_to_hidden.weight[:,idx].detach().numpy()
ax.scatter(vec[0], vec[1]) # 在图中绘制嵌入向量的点
ax.annotate(word, (vec[0], vec[1]), fontsize=12) # 点旁添加单词标签
plt.title(' 二维词嵌入 ') # 图题
plt.xlabel(' 向量维度 1') # X 轴 Label
plt.ylabel(' 向量维度 2') # Y 轴 Label
plt.show() # 显示图
输出的图像,如下所示
暂无评论内容