• Word2Vec详解


    Word2Vec

    基本思想:通过训练将每一个词映射成一个固定长度的向量,所有向量构成一个词向量空间,每一个向量(单词)可以看作是向量空间中的一个点,意思越相近的单词距离越近。

    在这里插入图片描述

    如何把词转换为向量?

    通常情况下,我们可以维护一个查询表。表中每一行都存储了一个特定词语的向量值,每一列的第一个元素都代表着这个词本身,以便于我们进行词和向量的映射(如“我”对应的向量值为 [0.3,0.5,0.7,0.9,-0.2,0.03] )。给定任何一个或者一组单词,我们都可以通过查询这个excel,实现把单词转换为向量的目的,这个查询和替换过程称之为Embedding Lookup。

    在这里插入图片描述

    然而在进行神经网络计算的过程中,需要大量的算力,常常要借助特定硬件(如GPU)满足训练速度的需求。GPU上所支持的计算都是以张量(Tensor)为单位展开的,因此在实际场景中,我们需要把Embedding Lookup的过程转换为张量计算:

    在这里插入图片描述

    如何让向量具有语义信息?

    自然语言处理研究中,科研人员通常有一个共识:使用一个单词的上下文来了解这个单词的语义,比如:

    “苹果手机质量不错,就是价格有点贵。”
    “这个苹果很好吃,非常脆。”
    “菠萝质量也还行,但是不如苹果支持的APP多。”
    
    • 1
    • 2
    • 3

    在上面的句子中,我们通过上下文可以推断出第一个“苹果”指的是苹果手机,第二个“苹果”指的是水果苹果,而第三个“菠萝”指的应该也是一个手机。在自然语言处理领域,使用上下文描述一个词语或者元素的语义是一个常见且有效的做法。我们可以使用同样的方式训练词向量,让这些词向量具备表示语义信息的能力。

    2013年,Mikolov提出的经典word2vec算法就是通过上下文来学习语义信息。word2vec包含两个经典模型:CBOW(Continuous Bag-of-Words)和Skip-gram。

    CBOW:通过上下文的词向量推理中心词。
    Skip-gram:根据中心词推理上下文。
    
    • 1
    • 2

    在这里插入图片描述

    CBOW模型

    在这里插入图片描述

    输入层: 一个形状为C×V的one-hot张量,其中C代表上下文中词的个数,通常是一个偶数,我们假设为4;V表示词表大小,我们假设为5000,该张量的每一行都是一个上下文词的one-hot向量表示。
    隐藏层: 一个形状为V×N的参数张量W1,一般称为word-embedding,N表示每个词的词向量长度,我们假设为128。输入张量和word embedding W1进行矩阵乘法,就会得到一个形状为C×N的张量。综合考虑上下文中所有词的信息去推理中心词,因此将上下文中C个词相加得一个1×N的向量,是整个上下文的一个隐含表示。
    输出层: 创建另一个形状为N×V的参数张量,将隐藏层得到的1×N的向量乘以该N×V的参数张量,得到了一个形状为1×V的向量。最终,1×V的向量代表了使用上下文去推理中心词,每个候选词的打分,再经过softmax函数的归一化,即得到了对中心词的推理概率:

    在这里插入图片描述

    Skip-gram模型

    在这里插入图片描述

    Input Layer(输入层): 接收一个one-hot张量 V∈R1×vocab_size作为网络的输入,假设vocab_size为5000。
    Hidden Layer(隐藏层): 将张量V乘以一个word embedding张量W1∈Rvocab_size×embed_size ,假设embed_size为128,并把结果作为隐藏层的输出,得到一个形状为R1×embed_size的张量,里面存储着当前句子中心词的词向量。
    Output Layer(输出层): 将隐藏层的结果乘以另一个word embedding张量W2∈Rembed_size×vocab_size,得到一个形状为R1×vocab_size的张量。这个张量经过softmax变换后,就得到了使用当前中心词对上下文的预测结果。根据这个softmax的结果,我们就可以去训练词向量模型。

    Skip-gram在实际操作中,使用一个滑动窗口(一般情况下,长度是奇数),从左到右开始扫描当前句子。每个扫描出来的片段被当成一个小句子,每个小句子中间的词被认为是中心词,其余的词被认为是这个中心词的上下文。

    Skip-gram实现
    1.数据处理:

    首先下载数据集处理语料,

    # 下载语料用来训练word2vec
    download()
    # 读取text8数据
    corpus = load_text8()
    # 对语料进行预处理(分词)并把所有英文字符都转换为小写
    corpus = data_preprocess(corpus)
    # 构造词典,统计每个词的频率,并根据频率将每个词转换为一个整数id
    word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
    vocab_size = len(word2id_freq)
    #保存word2id_freq, word2id_dict, id2word_dict到本地
    f_save = open('word2id_freq.pkl', 'wb')
    pickle.dump(word2id_freq, f_save)
    f_save.close()
    f_save = open('word2id_dict.pkl', 'wb')
    pickle.dump(word2id_dict, f_save)
    f_save.close()
    f_save = open('id2word_dict.pkl', 'wb')
    pickle.dump(id2word_dict, f_save)
    f_save.close()
    # 把语料转换为id序列
    corpus = convert_corpus_to_id(corpus, word2id_dict)
    # 使用二次采样算法(subsampling)处理语料,强化训练效果,遗弃频率高的词
    corpus = subsampling(corpus, word2id_freq)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    构造数据,假设有一个中心词c和一个上下文词正样本tp。在Skip-gram的理想实现里,需要最大化使用c推理tp的概率。在使用softmax学习时,需要最大化tp的推理概率,同时最小化其他词表中词的推理概率。之所以计算缓慢,是因为需要对词表中的所有词都计算一遍。然而我们还可以使用另一种方法,就是随机从词表中选择几个代表词,通过最小化这几个代表词的概率,去近似最小化整体的预测概率。比如,先指定一个中心词(如“人工”)和一个目标词正样本(如“智能”),再随机在词表中采样几个目标词负样本(如“日本”,“喝茶”等)。对于目标词正样本,我们需要最大化它的预测概率;对于目标词负样本,我们需要最小化它的预测概率。通过这种方式,我们就可以完成计算加速。上述做法,我们称之为负采样。实现如下:

    # 构造数据,准备模型训练
    # max_window_size代表了最大的window_size的大小,程序会根据max_window_size从左到右扫描整个语料
    # negative_sample_num代表了对于每个正样本,我们需要随机采样多少负样本用于训练,
    # 一般来说,negative_sample_num的值越大,训练效果越稳定,但是训练速度越慢。
    def build_data(corpus, word2id_dict, word2id_freq, max_window_size=3, negative_sample_num=4):
        # 使用一个list存储处理好的数据
        dataset = []
    
        # 从左到右,开始枚举每个中心点的位置
        for center_word_idx in range(len(corpus)):
            # 以max_window_size为上限,随机采样一个window_size,这样会使得训练更加稳定
            window_size = random.randint(1, max_window_size)
            # 当前的中心词就是center_word_idx所指向的词
            center_word = corpus[center_word_idx]
    
            # 以当前中心词为中心,左右两侧在window_size内的词都可以看成是正样本
            positive_word_range = (
            max(0, center_word_idx - window_size), min(len(corpus) - 1, center_word_idx + window_size))
            positive_word_candidates = [corpus[idx] for idx in range(positive_word_range[0], positive_word_range[1] + 1) if
                                        idx != center_word_idx]
    
            # 对于每个正样本来说,随机采样negative_sample_num个负样本,用于训练
            for positive_word in positive_word_candidates:
                # 首先把(中心词,正样本,label=1)的三元组数据放入dataset中,
                # 这里label=1表示这个样本是个正样本
                dataset.append((center_word, positive_word, 1))
    
                # 开始负采样
                i = 0
                while i < negative_sample_num:
                    negative_word_candidate = random.randint(0, vocab_size - 1)
    
                    if negative_word_candidate not in positive_word_candidates:
                        # 把(中心词,正样本,label=0)的三元组数据放入dataset中,
                        # 这里label=0表示这个样本是个负样本
                        dataset.append((center_word, negative_word_candidate, 0))
                        i += 1
        return dataset
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    2.网络定义
    import paddle
    from paddle.nn import Embedding
    import paddle.nn.functional as F
    import paddle.nn as nn
    
    #定义skip-gram训练网络结构
    #使用paddlepaddle的2.0.0版本
    #一般来说,在使用paddle训练的时候,我们需要通过一个类来定义网络结构,这个类继承了paddle.nn.layer
    class SkipGram(nn.Layer):
        def __init__(self, vocab_size, embedding_size, init_scale=0.1):
            # vocab_size定义了这个skipgram这个模型的词表大小
            # embedding_size定义了词向量的维度是多少
            # init_scale定义了词向量初始化的范围,一般来说,比较小的初始化范围有助于模型训练
            super(SkipGram, self).__init__()
            self.vocab_size = vocab_size
            self.embedding_size = embedding_size
    
            # 使用Embedding函数构造一个词向量参数
            # 这个参数的大小为:[self.vocab_size, self.embedding_size]
            # 数据类型为:float32
            # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样
            self.embedding = Embedding(
                num_embeddings = self.vocab_size,
                embedding_dim = self.embedding_size,
                weight_attr=paddle.ParamAttr(
                    initializer=paddle.nn.initializer.Uniform(
                        low=-init_scale, high=init_scale)))
    
            # 使用Embedding函数构造另外一个词向量参数
            # 这个参数的大小为:[self.vocab_size, self.embedding_size]
            # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样
            self.embedding_out = Embedding(
                num_embeddings = self.vocab_size,
                embedding_dim = self.embedding_size,
                weight_attr=paddle.ParamAttr(
                    initializer=paddle.nn.initializer.Uniform(
                        low=-init_scale, high=init_scale)))
    
        # 定义网络的前向计算逻辑
        # center_words是一个tensor(mini-batch),表示中心词
        # target_words是一个tensor(mini-batch),表示目标词
        # label是一个tensor(mini-batch),表示这个词是正样本还是负样本(用01表示)
        # 用于在训练中计算这个tensor中对应词的同义词,用于观察模型的训练效果
        def forward(self, center_words, target_words, label):
            # 首先,通过self.embedding参数,将mini-batch中的词转换为词向量
            # 这里center_words和eval_words_emb查询的是一个相同的参数
            # 而target_words_emb查询的是另一个参数
            center_words_emb = self.embedding(center_words)
            target_words_emb = self.embedding_out(target_words)
    
            # 我们通过点乘的方式计算中心词到目标词的输出概率,并通过sigmoid函数估计这个词是正样本还是负样本的概率。
            word_sim = paddle.multiply(center_words_emb, target_words_emb)
            word_sim = paddle.sum(word_sim, axis=-1)
            word_sim = paddle.reshape(word_sim, shape=[-1])
            pred = F.sigmoid(word_sim)
    
            # 通过估计的输出概率定义损失函数,注意我们使用的是binary_cross_entropy_with_logits函数
            # 将sigmoid计算和cross entropy合并成一步计算可以更好的优化,所以输入的是word_sim,而不是pred
            loss = F.binary_cross_entropy_with_logits(word_sim, label)
            loss = paddle.mean(loss)
    
            # 返回前向计算的结果,飞桨会通过backward函数自动计算出反向结果。
            return pred, loss
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    3.网络训练:
    # 开始训练,定义一些训练过程中需要使用的超参数
    batch_size = 512
    epoch_num = 3
    embedding_size = 200
    step = 0
    learning_rate = 0.001
    
    
    # 定义一个使用word-embedding查询同义词的函数
    # 这个函数query_token是要查询的词,k表示要返回多少个最相似的词,embed是我们学习到的word-embedding参数
    # 我们通过计算不同词之间的cosine距离,来衡量词和词的相似度
    # 具体实现如下,x代表要查询词的Embedding,Embedding参数矩阵W代表所有词的Embedding
    # 两者计算Cos得出所有词对查询词的相似度得分向量,排序取top_k放入indices列表
    def get_similar_tokens(query_token, k, embed):
        W = embed.numpy()
        x = W[word2id_dict[query_token]]
        cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9)
        flat = cos.flatten()
        indices = np.argpartition(flat, -k)[-k:]
        indices = indices[np.argsort(-flat[indices])]
        for i in indices:
            print('for word %s, the similar word is %s' % (query_token, str(id2word_dict[i])))
    
    
    # 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络
    skip_gram_model = SkipGram(vocab_size, embedding_size)
    
    # 构造训练这个网络的优化器
    adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())
    
    # 使用build_batch函数,以mini-batch为单位,遍历训练数据,并训练网络
    for center_words, target_words, label in build_batch(
            dataset, batch_size, epoch_num):
        # 使用paddle.to_tensor,将一个numpy的tensor,转换为飞桨可计算的tensor
        center_words_var = paddle.to_tensor(center_words)
        target_words_var = paddle.to_tensor(target_words)
        label_var = paddle.to_tensor(label)
    
        # 将转换后的tensor送入飞桨中,进行一次前向计算,并得到计算结果
        pred, loss = skip_gram_model(
            center_words_var, target_words_var, label_var)
    
        # 程序自动完成反向计算
        loss.backward()
        # 程序根据loss,完成一步对参数的优化更新
        adam.step()
        # 清空模型中的梯度,以便于下一个mini-batch进行更新
        adam.clear_grad()
    
        # 每经过100个mini-batch,打印一次当前的loss,看看loss是否在稳定下降
        step += 1
        if step % 1000 == 0:
            print("step %d, loss %.3f" % (step, loss.numpy()[0]))
    
        # 每隔10000步,打印一次模型对以下查询词的相似词,这里我们使用词和词之间的向量点积作为衡量相似度的方法,只打印了5个最相似的词并保存网络模型参数和优化器模型参数
        if step % 10000 == 0:
            get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)
            get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
            get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
            paddle.save(skip_gram_model.state_dict(), "text8.pdparams")
            paddle.save(adam.state_dict(), "adam.pdopt")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    在这里插入图片描述
    这里step到300000我就结束训练了,可以看到词性已经很接近了。

    4.测试训练模型

    训练完成后,对任意词都可以基于我们训练出模型计算出跟这个词最接近的词。

    # 定义一些训练过程中需要使用的超参数
    embedding_size = 200
    learning_rate = 0.001
    
    # 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络
    
    skip_gram_model = SkipGram(vocab_size, embedding_size)
    
    # 构造训练这个网络的优化器
    adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())
    
    # 加载网络模型和优化器模型
    layer_state_dict = paddle.load("./my_model/text8.pdparams")
    opt_state_dict = paddle.load("./my_model/adam.pdopt")
    
    skip_gram_model.set_state_dict(layer_state_dict)
    adam.set_state_dict(opt_state_dict)
    
    # get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)
    # get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
    # get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
    get_similar_tokens('she', 5, skip_gram_model.embedding.weight)
    get_similar_tokens('dog', 5, skip_gram_model.embedding.weight)
    get_similar_tokens('apple', 5, skip_gram_model.embedding.weight)
    get_similar_tokens('beijing', 5, skip_gram_model.embedding.weight)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果如下,我没有完全训练完,可以看到效果还可以。

    在这里插入图片描述

    源码已经上传到GitHub上,github链接:https://github.com/fakerst/NLP/tree/main/Word2Vec-SkipGram

  • 相关阅读:
    JSD-2204-跨域问题-总结Spring框架-Day08
    【详解】Python基础操作之os模块常用命令
    【程序员书单】
    NISP和CISP都有什么同?
    3.6 OrCAD中元器件应该怎么进行镜像与翻转?
    论文阅读-可泛化深度伪造检测的关键
    【Vue-Element-Admin】导出el-table全部数据
    基于SSM的客户管理系统设计与实现
    多卡GPU训练时的问题
    使用 ClickHouse 深入了解 Apache Parquet (二)
  • 原文地址:https://blog.csdn.net/weixin_43912621/article/details/128121807