1 介绍

关于词向量与Embedding: Embedding是以每个字在给定分布随机初始化的随机向量而组成的可学习参数矩阵,也就是一个全连接Dense层,其以onehot为输入稠密向量为输出,即词向量,因此在实现上,用lookup查表来代替矩阵乘积以提高性能。PyTorch中常用的实现为nn.Embedding(vocab_size, embed_dim),embed_dim为词向量的表征纬度大小,vocab_size为词表大小

注意区分Tokenization和Embedding:Tokenization是输入文本处理的第一步,即切分(【大模型】文本大模型的Tokenization),而Embedding是大模型内部的对切分后子词的向量表达的转换方式,一般而言Embedding和Tokenization要对应上,即一个子词由一个向量表示

2 词嵌入方法

2.1 OneHot(独热编码)

优点: 独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有0和1,不同的类型存储在垂直的空间​

缺点:

  • 一方面实际使用的词汇表很大,经常是百万级以上,随着词汇表的增大,OneHot编码会变得非常稀疏且高维,可能不适用于处理大规模的文本数据,这么**高维的数据处理起来会消耗大量的计算资源与时间。在这种情况下,一般可以用PCA来减少维度。而且one hot encoding + PCA这种组合在实际中也非常有用
  • 另一方面,One-Hot编码中所有词向量之间彼此正交,没有体现词与词之间的相似关系

2.2 Word2Vec

Word2Vec 的训练模型本质上是只具有一个隐含层的神经元网络,最后取隐藏层的表示作为词向量。主要分为两种任务类型 CBOW 和 Skip-gram

2.2.1 CBOW(Continus bag-of-word)

根据语境预测当前词

# 定义 CBOW 模型​
import torch.nn as nn # 导入 neural network​
class CBOW(nn.Module):​
    def __init__(self, voc_size, embedding_size):​
        super(CBOW, 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)  ​
    def forward(self, X): # X: [num_context_words, voc_size]​
        # 生成嵌入:[num_context_words, embedding_size]​
        embeddings = self.input_to_hidden(X)  ​
        # 计算隐藏层,求嵌入的均值:[embedding_size]​
        hidden_layer = torch.mean(embeddings, dim=0)  ​
        # 生成输出层:[1, voc_size]​
        output_layer = self.hidden_to_output(hidden_layer.unsqueeze(0)) ​
        return output_layer    ​
embedding_size = 2 # 设定嵌入层的大小,这里选择 2 是为了方便展示​
cbow_model = CBOW(voc_size,embedding_size)  # 实例化 CBOW 模型​
print("CBOW 模型:", cbow_model)

2.2.2 Skip-gram

根据当前词预测语境

# 构建模型​
class Skip_gram(nn.Module):​
    def __init__(self):​
        super(Skip_gram, self).__init__()​
        # W:one-hot到词向量的hidden layer​
        self.W = nn.Parameter(torch.randn(voc_size, embedding_size).type((dtype)))​
        # V:输出层的参数​
        self.V = nn.Parameter(torch.randn(embedding_size, voc_size).type((dtype)))​
 ​
    def forward(self, X):​
        # X : [batch_size, voc_size] one-hot​
        # torch.mm only for 2 dim matrix, but torch.matmul can use to any dim​
        hidden_layer = torch.matmul(X, self.W)  # hidden_layer : [batch_size, embedding_size]​
        output_layer = torch.matmul(hidden_layer, self.V)  # output_layer : [batch_size, voc_size]​
        return output_layer​
model = Skip_gram().to(device)​
criterion = nn.CrossEntropyLoss().to(device)  # 多分类,交叉熵损失函数​
optimizer = optim.Adam(model.parameters(), lr=1e-3)  # Adam优化算法

2.2.3 Word2Vec训练优化加速

一般神经网络语言模型在预测的时候,输出的是预测目标词的概率,通过softmax得到,也就是每一次预测都要基于全部的数据集进行计算,这无疑会带来很大的时间开销。Word2Vec提出两种加快训练速度的方式,一种是Hierarchical softmax,另一种是Negative Sampling

  1. 霍夫曼树 Hierarchical softmax
    为了避免计算所有词的softmax概率,word2vec采用了霍夫曼树来代替从隐层到softmax层的映射,根据词频来建立哈夫曼树。将多分类转为二分类问题。哈夫曼树的所有内部节点就类似之前神经网络隐藏层的神经元,其中,根节点的词向量对应我们的投影后的词向量,而所有叶子节点就类似于之前神经网络softmax输出层的神经元,叶子节点的个数就是词汇表的大小。

优点:​

  • 计算效率高:由于是二叉树,使得计算量从​V变为了​log2V
  • 符合贪心优化思想:高频词更加接近树根,能更快速得被检索,符合贪心优化的思想

缺陷:如果我们的训练样本里的中心词是一个很生僻的词,那么就得在霍夫曼树中辛苦的向下走很久了。负采样可以解决该问题

  1. 负采样 Negative Sampling
    对于给定的词W的上下文Context(w),词w是一个正样本,其他词是负样本。使用了sigmoid函数
    负采样的本质:每次让一个训练样本只更新部分权重,其他权重全部固定;减少计算量;(一定程度上还可以增加随机性)

2.3 FastText

fastText是一个快速文本分类算法,本质和CBOW一样,fastText模型也只有三层:输入层、隐含层、输出层,输入都是多个经向量表示的单词,输出都是一个特定的target,隐含层都是对多个词向量的叠加平均;

不同的是,CBOW的输入是目标单词的上下文,fastText的输入是多个单词及其n-gram特征,这些特征用来表示单个文档;CBOW的输入单词被onehot编码过,fastText的输入特征是被embedding过;CBOW的输出是目标词汇,fastText的输出是文档对应的类标