词向量 ≈ 词嵌入
词向量 和 词嵌入 都是指,将单词转换成向量,然后再使用余弦相似度计算单词相似度。一个指向量,一个指转换成向量的过程。
稀疏向量 与 稠密向量
- 稀疏向量,大部分元素都是 ,只有少数非 。稀疏向量通常用于表示高维数据,其中许多维度值为 。上一篇笔记中介绍的词袋模型就是一种稀疏向量表示。在词袋模型中,每个文档用一个向量表示,向量的长度等于词汇表中的词数量,向量的每个元素表示相应词在文档中出现的次数。由于大部分单词可能不会出现在给定的文档中,因此词袋模型中的向量通常是稀疏的。
- 稠密向量,大部分元素非 ,稠密向量通常具有较低的维度。能够捕捉到更丰富的信息。 就是一种典型的稠密向量表示。稠密向量能够捕捉词与词之间的语义和语法关系,使得具有相似含义和相关性的词在向量空间距离较近。
Word2Vec的实现方式
- (Continuous Bag of Words) 连续词袋模型,通过给定上下文(也叫周围词)来预测目标词(也叫中心词)
- 跳字模型,通过给定目标词来预测上下文词。
这两种模型都是通过训练神经网络来学习词向量的。在训练过程中,通过最小化预测词和实际词之间的损失来学习词向量。当训练完成后,词向量可以从神经网络的权重中提取出来。
CBOW示意:
graph LR
A[孙悟空] --> D
B[打] --> D
C[白骨精] --> D
D(sum) --CBOW--> E[三: 目标词]
Skip-Gram示意:
graph LR
A[三] --> B(Skip-Gram)
B --上下文--> C[孙悟空]
B --上下文--> D[打]
B --上下文--> 白骨精
模型 | 训练目标 | 结构 | 性能 |
---|---|---|---|
Skip-Gram | 给定一个目标词,预测上下文词。因此,它的训练目标是在给定目标词的情况下,使上下文词出现的条件概率最大化 | 模型首先将目标词映射到嵌入向量空间,然后从嵌入向量空间映射回词汇表空间以预测上下文词的概率分布 | 由于其目标是预测多个上下文词,其在捕捉稀有词和更复杂的词语关系方面表现得更好 |
CBOW | 给定上下文词,预测目标词。因此,它的训练目标是在给定上下文词的情况下,使目标词出现的条件概率最大化 | 模型首先将上下文词映射到嵌入向量空间,然后从嵌入向量空间映射回词汇表空间以预测目标词的概率分布 | 由于其目标是预测一个目标词,其在训练速度和捕捉高频词与关系方面表现得更好 |
In
# 定义一个句子列表,后面会用这些句子来训练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)}
vec_size = len(word_list) # 计算词汇表的大小
print("词汇表:", word_list) # 输出词汇表
print("词汇到索引的字典:", word_to_idx) # 输出词汇到索引的字典
print("索引到词汇的字典:", idx_to_word) # 输出索引到词汇的字典
print("词汇表大小:", vec_size) # 输出词汇表大小
Out
词汇表: ['Xiaoxue', 'Mazong', 'Kage', 'Xiaobing', 'Student', 'Teacher', 'Niuzong', 'Boss', 'is']
词汇到索引的字典: {'Xiaoxue': 0, 'Mazong': 1, 'Kage': 2, 'Xiaobing': 3, 'Student': 4, 'Teacher': 5, 'Niuzong': 6, 'Boss': 7, 'is': 8}
索引到词汇的字典: {0: 'Xiaoxue', 1: 'Mazong', 2: 'Kage', 3: 'Xiaobing', 4: 'Student', 5: 'Teacher', 6: 'Niuzong', 7: 'Boss', 8: 'is'}
词汇表大小: 9
In
# 生成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 + 1, len(sentence))]:
if neighbor != word: #排除当前单词本身
#将相邻单词与当前单词作为一组训练数据
data.append((word, neighbor))
return data
# 使用函数创建Skip-Gram训练数据
skipgram_data = create_skipgram_dataset(sentences)
print("Skip-Gram数据样例(未编码):", skipgram_data[:3])
Out
Skip-Gram数据样例(未编码): [('Kage', 'is'), ('Kage', 'Teacher'), ('is', 'Kage')]
One-Hot 编码
编码后的数据是向量,这种形式的向量就是稀疏向量,其长度等于词汇表大小,其中目标单词在词汇表中的索引位置上的值为 ,其他位置上的值为 。
In
# 定义One-Hot编码函数
import torch # 导入torch库
def one_hot_encoding(word, word_to_idx):
tensor = torch.zeros(len(word_to_idx)) #创建一个长度与词汇表相同的全0张量
tensor[word_to_idx[word]] = 1 # 将对应词索引位置上的值设为1
return tensor # 返回生成的One-Hot编码后的向量
# 展示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(target, word_to_idx), word_to_idx[context]) for context, target in skipgram_data[:3]])
Out
One-Hot编码前的单词: Teacher
One-Hot编码后的向量: tensor([0., 0., 0., 0., 1., 0., 0., 0., 0.])
Skip-Gram 数据样例(已编码): [(tensor([0., 1., 0., 0., 0., 0., 0., 0., 0.]), 6),
(tensor([0., 0., 0., 0., 1., 0., 0., 0., 0.]), 6),
(tensor([0., 0., 0., 0., 0., 0., 1., 0., 0.]), 1)]
继承 的 类来实现 类:
In
# 定义Skip-Gram类
import torch.nn as nn # 导入neural network
class SkipGram(nn.Module):
def __init__(self, vec_size, embedding_size):
super(SkipGram, self).__init__()
# 从词汇表大小到嵌入层大小(维度)的线性层(权重矩阵)
self.input_to_hidden = nn.Linear(vec_size, embedding_size, bias=False)
# 从嵌入层大小(维度)到词汇表大小的线性层(权重矩阵)
self.hidden_to_output = nn.Linear(embedding_size, vec_size, bias=False)
def forward(self, X): # 前向传播的方式,X形状为(batch_size, vec_size)
# 通过隐藏层,hidden形状为(batch_size, embedding_size)
hidden = self.input_to_hidden(X)
# 通过输出层,output_layer形状为(batch_size, vec_size)
output = self.hidden_to_output(hidden)
return output
embedding_size = 2 # 设定嵌入层大小,这里选择2是为了方便展示
skipgram_model = SkipGram(vec_size, embedding_size) # 实例化Skip-Gram模型
print("Skip-Gram类:", skipgram_model)
Out
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)
)
input_to_hidden 把 One-Hot 编码后的向量从词汇表大小映射到嵌入层大小,以形成并学习词的向量表示。
hidden_to_output 把词的向量表示从嵌入层大小映射回词汇表大小,以预测目标词。
In
# 训练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 center_word, context in skipgram_data:
X = one_hot_encoding(center_word, 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() # 显示图
Out
Epoch: 100, Loss: 2.1436016877492268
Epoch: 200, Loss: 2.0981473128000894
Epoch: 300, Loss: 2.042161496480306
Epoch: 400, Loss: 1.9803014159202577
Epoch: 500, Loss: 1.9245647410551707
Epoch: 600, Loss: 1.8834956924120585
Epoch: 700, Loss: 1.856126469373703
Epoch: 800, Loss: 1.8370074232419331
Epoch: 900, Loss: 1.8217105587323508
Epoch: 1000, Loss: 1.8079410374164582
模型的损失会随着训练的进行而降低,通过最小化这个损失,模型将接近上下文单词正确的概率分布。表示模型对目标词的预测会越来越准确。
中的 函数内部结合了 函数和 函数,计算了对数似然损失在经过 函数之后的预测概率。
函数是一种常用的激活函数,用于将一个向量转换为概率分布。
代表输入向量中的第 个元素, 是向量维度。
函数的主要作用是对向量进行归一化,使向量中的元素都在 到 的范围内,并且所有元素的和等于 。这样每个元素就可以被解释为对应类别的概率。 函数会放大输入中较大的值,使最大值更接近 ,其他值更接近 。它会放大输入向量中的差异,使得概率分布更加尖锐。
In
# 输出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()}")
Out
Skip-Gram词嵌入:
Xiaobing: [-0.66865456 0.580458 ]
is: [-0.27223745 -0.7707463 ]
Mazong: [-1.139097 0.2877807]
Boss: [0.00641653 0.8171582 ]
Teacher: [0.14283952 0.69791996]
Student: [-0.26408908 0.8509651 ]
Kage: [-0.33171967 0.8360627 ]
Niuzong: [-1.0515854 0.37029913]
Xiaoxue: [-0.8446677 0.45316988]