AI大模型 之 词向量(Word to Vector)

181 阅读1分钟

词向量 ≈ 词嵌入

词向量词嵌入 都是指,将单词转换成向量,然后再使用余弦相似度计算单词相似度。一个指向量,一个指转换成向量的过程。

稀疏向量 与 稠密向量

  • 稀疏向量,大部分元素都是 00,只有少数非 00。稀疏向量通常用于表示高维数据,其中许多维度值为 00。上一篇笔记中介绍的词袋模型就是一种稀疏向量表示。在词袋模型中,每个文档用一个向量表示,向量的长度等于词汇表中的词数量,向量的每个元素表示相应词在文档中出现的次数。由于大部分单词可能不会出现在给定的文档中,因此词袋模型中的向量通常是稀疏的。
  • 稠密向量,大部分元素非 00,稠密向量通常具有较低的维度。能够捕捉到更丰富的信息。 Word2VecWord2Vec 就是一种典型的稠密向量表示。稠密向量能够捕捉词与词之间的语义和语法关系,使得具有相似含义和相关性的词在向量空间距离较近。

Word2Vec的实现方式

  1. CBOWCBOW (Continuous Bag of Words) 连续词袋模型,通过给定上下文(也叫周围词)来预测目标词(也叫中心词)
  2. SkipGramSkip-Gram 跳字模型,通过给定目标词来预测上下文词。

这两种模型都是通过训练神经网络来学习词向量的。在训练过程中,通过最小化预测词和实际词之间的损失来学习词向量。当训练完成后,词向量可以从神经网络的权重中提取出来。


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 编码

OneHotOne-Hot 编码后的数据是向量,这种形式的向量就是稀疏向量,其长度等于词汇表大小,其中目标单词在词汇表中的索引位置上的值为 11,其他位置上的值为 00

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)]

继承 PyTorchPyTorchnn.Modelnn.Model 类来实现 SkipGramSkip-Gram 类:

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

output_2_1[1].png

模型的损失会随着训练的进行而降低,通过最小化这个损失,模型将接近上下文单词正确的概率分布。表示模型对目标词的预测会越来越准确。

PytorchPytorch 中的 CrossEntropyLossCrossEntropyLoss 函数内部结合了 LogsoftmaxLogsoftmax 函数和 NLLLossNLLLoss 函数,计算了对数似然损失LogLikelihoodLoss(Log Likelihood Loss)在经过 softmaxsoftmax 函数之后的预测概率。

softmaxsoftmax 函数是一种常用的激活函数,用于将一个向量转换为概率分布。

softmax(x_i)=exp(x_i)/sum(exp(x_j))forjinrange(1,n)softmax(x\_i) = exp(x\_i) / sum(exp(x\_j))\\for\quad j\quad in\quad range(1, n)

x_ix\_i 代表输入向量中的第 ii 个元素,nn 是向量维度。
softmaxsoftmax 函数的主要作用是对向量进行归一化,使向量中的元素都在 0011 的范围内,并且所有元素的和等于 11。这样每个元素就可以被解释为对应类别的概率。softmaxsoftmax 函数会放大输入中较大的值,使最大值更接近 11,其他值更接近 00。它会放大输入向量中的差异,使得概率分布更加尖锐。

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]

output_2_3[1].png

下一篇笔记,接着实现 CBOWCBOW 模型 AI大模型 之 CBOW模型实现