NLP-01-word2vec 阅读笔记

发表期刊:2013-ICLR

单位:google

 

摘要:word2vec主要包括两种模型,分别是skip-gram和CBOW,skip-gram使用中心词来预测周围词,CBOW使用周围词来预测中心词。还介绍了两种加速softmax计算的方法,分别是层次softmax和负采样,softmax的核心思想是将将多分类转化为多层的二分类;负采样的核心思想是将多分类问题转化为二分类问题(判断是正样本还是负样本)。最后还介绍了重采样的技术:降低高频词的训练次数,提高低频词的训练次数,从而达到加速训练的目的

语言模型(判断一个句子是否是句子的模型)应用场景:输入法打出一串字排名靠前的选项是目标语句的可能性更高

 

 

统计语言模型存在的问题:一些没有在语料库中出现的句子并不意味着现实生活中不可能出现(句子越长,在预料中出现的概率越低)

面临的问题

1)参数空间过大;(概率和条件概率都是参数)

2)数据稀疏严重;(只有很少的参数不为0,其余都为0)

马尔科夫假设:下一个词出现的概率仅依赖于前面的一个词或几个词

unigram:一个词出现的概率仅和自己本身有关(参数空间为V)

bigram: 一个词出现的概率仅和前面的一个词有关(参数空间为V+V*V)

k-gram:一个词出现的概率仅和前面的k-1个词有关(参数空间为V+V**2+…+V**k)

语言模型评价指标

困惑度:句子概率越大,困惑度越小,语言模型越好(因为测试集给的都是正常的句子)。

 

词的表示方式

独热编码:优点是表示简单,缺点是词越多,向量维度越大,且无法表示词与词之间的关系。

分布式编码:优点是维度得到极大的降低,且可以表示词之间的关系。

SVD分解:抛弃不重要的特征

相关工作

  • 基于全连接网络的语言模型(NNLM)

 

 

 

模型输入为词的index,经过映射成为低维向量,然后将所有的输入层向量拼接到一起,然后输入hidden layer(全连接层),之后再接一个全连接层,softmax作为激活函数得到每个词的概率,词的概率相乘得到句子的概率。

模型优点:不需要标注数据,因此句子确定,下个词也就确定。

可能的改进方向:

  • 仅对一部分输出进行梯度传播(有些冠词出现频率很高但对模型作用不大)
  • 引入先验知识,如词性等(网络本身能否学习到词性信息)
  • 解决一词多义问题
  • 改进softmax层加速运算

  • 基于RNN的语言模型(RNNLM)

每个时间步预测一个词,在预测第n个词时使用了前n-1个词的信息

 

​Log线性模型:将语言模型的建立看成一个多分类问题,相当于线性分类器加上softmax。(对等式两边同时做log运算,等式右边变为线性函数)

 

语言模型的核心思想句子中下一个词的出现和前面的词是有关系的,所以可以使用前面的词来预测下一个词

本文提出的模型(Word2vec):核心思想是句子中相互靠近的词是有联系的,因此本文的提出的两个模型分别使用周围词预测中心词、使用中心词预测周围词。

Skip-gram

  • 使用中心词预测周围词(每个中心词产生2 * window size个训练样本)

 

 

 

Continuous bag-of-words(CBOW)

  • 使用周围词预测中心词(每个中心词产生一个训练样本)

bag-of-words描述的原因:将周围词向量进行sum和avreage,忽略了词的顺序进行预测。​

 

 

 

两种降低softmax复杂度的方法

  1. 层次softmax:核心思想是将softmax转化为多层的sigmoid
  2. 负采样:核心思想是将多分类问题转化为二分类问题(判断是正样本还是负样本)
    • 正样本为中心词和真正的周围词;负样本为中心词和随机采样的词;模型最大化正样本的概率。

 

 

Hierarchical softmax

进一步改进:huffman树

"""
    构建霍夫曼树
"""


class HuffmanNode:
    def __init__(self, word_id, frequency):
        self.word_id = word_id  # 叶子结点存词对应的id, 中间节点存中间节点id
        self.frequency = frequency  # 存单词出现频次
        self.left_child = None
        self.right_child = None
        self.father = None
        self.Huffman_code = []  # 霍夫曼码(左1右0)根到目标节点的编码
        self.path = []  # 根到叶子节点的中间节点id


class HuffmanTree:
    def __init__(self, wordid_frequency_dict):
        self.word_count = len(wordid_frequency_dict)  # 单词数量
        self.wordid_code = dict()  # 每个词id对应一个code
        self.wordid_path = dict()  # 每个词id对应一个path
        self.root = None
        unmerge_node_list = [HuffmanNode(wordid, frequency) for wordid, frequency in
                             wordid_frequency_dict.items()]  # 初始化一个未合并节点list
        self.huffman = [HuffmanNode(wordid, frequency) for wordid, frequency in
                        wordid_frequency_dict.items()]  # 初始化一个存储所有的叶子节点和中间节点的list
        # 构建huffman tree
        self.build_tree(unmerge_node_list)
        # 生成huffman code
        self.generate_huffman_code_and_path()

    def merge_node(self, node1, node2):
        sum_frequency = node1.frequency + node2.frequency
        mid_node_id = len(self.huffman)  # 中间节点的value存中间节点id
        father_node = HuffmanNode(mid_node_id, sum_frequency)  # 初始化父亲节点
        if node1.frequency >= node2.frequency:  # 出现频率大的词放左边
            father_node.left_child = node1
            father_node.right_child = node2
        else:
            father_node.left_child = node2
            father_node.right_child = node1
        self.huffman.append(father_node)
        return father_node

    def build_tree(self, node_list): # 传入未合并节点
        while len(node_list) > 1:
            i1 = 0  # 假设出现频次最小的节点
            i2 = 1  # 假设出现频次第二小的节点
            if node_list[i2].frequency < node_list[i1].frequency:  # 如果假设不成立,交换i1和i2
                [i1, i2] = [i2, i1]
            for i in range(2, len(node_list)):  # 遍历更新i1和i2
                if node_list[i].frequency < node_list[i2].frequency:
                    i2 = i
                    if node_list[i2].frequency < node_list[i1].frequency:
                        [i1, i2] = [i2, i1]
            father_node = self.merge_node(node_list[i1], node_list[i2])  # 合并最小的两个节点
            if i1 < i2:
                node_list.pop(i2)  # 删节点,先删索引大的节点,删完之后索引小的节点索引不变
                node_list.pop(i1)
            elif i1 > i2:
                node_list.pop(i1)
                node_list.pop(i2)
            else:
                raise RuntimeError('i1 should not be equal to i2')
            node_list.insert(0, father_node)  # 插入新节点
        self.root = node_list[0]

    def generate_huffman_code_and_path(self):
        stack = [self.root]  # 根节点入栈
        while len(stack) > 0:
            node = stack.pop()  # 出栈
            # 顺着左子树走
            while node.left_child or node.right_child:
                code = node.Huffman_code
                path = node.path
                node.left_child.Huffman_code = code + [1]  # 左孩子的code 增加[1]
                node.right_child.Huffman_code = code + [0]
                node.left_child.path = path + [node.word_id]  # 增加id
                node.right_child.path = path + [node.word_id]
                stack.append(node.right_child)  # 把没走过的右子树加入栈
                node = node.left_child  # 从左孩子继续遍历
            word_id = node.word_id  # 每个退出循环的结点都是叶节点,这些节点存储了单词
            word_code = node.Huffman_code
            word_path = node.path
            self.huffman[word_id].Huffman_code = word_code
            self.huffman[word_id].path = word_path
            # 把节点计算得到的霍夫曼码、路径  写入词典的数值中
            self.wordid_code[word_id] = word_code
            self.wordid_path[word_id] = word_path

    def get_all_pos_and_neg_path(self):
        # 获取所有词的正向节点id和负向节点id数组(没看懂)
        positive = []  # 所有词的正向路径数组
        negative = []  # 所有词的负向路径数组
        for word_id in range(self.word_count):
            pos_id = []  # 存放一个词 路径中的正向节点id
            neg_id = []  # 存放一个词 路径中的负向节点id
            for i, code in enumerate(self.huffman[word_id].Huffman_code):
                if code == 1:
                    pos_id.append(self.huffman[word_id].path[i])
                else:
                    neg_id.append(self.huffman[word_id].path[i])
            positive.append(pos_id)
            negative.append(neg_id)
        return positive, negative


def test():
    word_frequency = {0: 4, 1: 6, 2: 3, 3: 2, 4: 2}
    print(word_frequency)
    tree = HuffmanTree(word_frequency)
    print(tree.wordid_code)
    print(tree.wordid_path)
    for i in range(len(word_frequency)):
        print(tree.huffman[i].path)
    print(tree.get_all_pos_and_neg_path())


if __name__ == '__main__':
    test()

 

层次softmax的构建:

skip-gram的构建

 

n(w,j)在词w的路径上的第j个节点(图中黄色箭头)

CBOW的构建:

负采样:核心思想是将多分类问题转化为二分类问题(判断是正样本还是负样本)

skip-gram:

采样方法:

CBOW:

加速训练方法:

重采样:把高频词从训练集中删除一些,提高低频词的训练次数。

 

实验及结果:

 

通过表2的实验发现,随着表示维度和数据集的增大,模型的表现越来越好。

​从表3的结果来看,语义任务的完成结果并没有多好,还需要进一步研究

Skip-gram完成了在语义任务上的最好表现,而语法任务是改进后的NNLM表现最好,原因在于使用softmax之后,可供训练的数据集更大(比之前的使用的最大的数据集还大了大约10倍)。

skip-gram训练时间比CBOW长,但效果更好

从训练时间和模型表现上都能看出本文提出的模型更好(疑问:CBOW和Skip-gram不也是全连接层吗?)

(疑问:这个任务及作者提出的方法究竟是怎么做的?)

层次softmax与负采样的比较:从表中可以看出负采样所需训练时间更短,且得到的效果更好,下表展示了使用重采样的结果,大大加速了模型的训练。

超参数的选择:

本文贡献:

 

 

Wikipedia英文语料,包含wikipedia里面的所有文章,可以在https://dumps.wikimedia.org/enwiki/latest/ 上下载
论文中介绍的词对推理语料,包含五个语义类和九个语法类。其中有8869个语义对样本和10675个语法对样本。可以在http://www.fit.vutbr.cz/~imikolov/rnnlm/word-test.v1.txt 上下载。

 

skip-gram + NGE代码的实现

数据预处理

import numpy as np
from collections import deque


# deque为队列,用来读取数据

class InputData:
    def __init__(self, input_file_name, min_count):
        self.input_file_name = input_file_name
        self.index = 0
        self.input_file = open(self.input_file_name, "r", encoding="utf-8")
        self.min_count = min_count
        self.wordid_frequency_dict = dict()  # 记录词id的频率,key为id,value为出现次数,用于重采样
        self.word_count = 0
        self.word_count_sum = 0  # 词的数量
        self.sentence_count = 0  # 句子的数量
        self.id2word_dict = dict()  # 构建 id2word的字典, key为id,value为词
        self.word2id_dict = dict()  # 构建 word2id的字典
        self._init_dict()  # 初始化dic,构建word2id,id2word,id2fre,
        self.sample_table = []
        self._init_sample_table()  # 获得负样本采样表
        self.get_wordId_list()  # 将词的列表转化为id的列表
        self.word_pairs_queue = deque()
        # 结果展示
        print('Word Count is:', self.word_count)
        print('Word Count Sum is', self.word_count_sum)
        print('Sentence Count is:', self.sentence_count)

    def _init_dict(self):
        word_freq = dict()  # key为word, value为出现的次数
        for line in self.input_file:
            line = line.strip().split()  # strip 去除首尾的空格,split()使用空格进行分词
            self.word_count_sum += len(line)  # 词的个数等于各行词数相加
            self.sentence_count += 1  # 每遍历一行,句子数+1
            for i, word in enumerate(line):
                if i % 1000000 == 0:
                    print(i, len(line))  # 进度显示
                if word_freq.get(word) == None:
                    word_freq[word] = 1  # 如果当前词不在统计的字典中, 设词频为1
                else:
                    word_freq[word] += 1  # 出现一次,词频+1
        for i, word in enumerate(word_freq):  # 遍历字典中的每个对象 key-value对
            if i % 100000 == 0:
                print(i, len(word_freq))
            if word_freq[word] < self.min_count:
                self.word_count_sum -= word_freq[word]  # 如果词出现次数小于min_count,舍弃
                continue
            self.word2id_dict[word] = len(self.word2id_dict)  # 构建 word2id,一个一个添加词,因此可以使用字典长度作为id
            self.id2word_dict[len(self.id2word_dict)] = word  # 构建 id2word
            self.wordid_frequency_dict[len(self.word2id_dict) - 1] = word_freq[word]  # 构建 id2frequency
        self.word_count = len(self.word2id_dict)  # (去重?应该是去除低频词吧)后词的数量

    def _init_sample_table(self):  # 获得负样本采样表
        sample_table_size = 1e8    # 采样表大小
        pow_frequency = np.array(list(self.wordid_frequency_dict.values())) ** 0.75  # 采样公式
        word_pow_sum = sum(pow_frequency)  # 求和获得归一化参数 Z
        ratio_array = pow_frequency / word_pow_sum  # 得到采样频率
        word_count_list = np.round(ratio_array * sample_table_size)  # round 四舍五入函数
        # ratio_array size=[num_word,]; 相乘得到采样表中每个词的出现次数
        for word_index, word_freq in enumerate(word_count_list):
            self.sample_table += [word_index] * int(word_freq)
            # 按采样频率估计的次数将词放入词表,比如词表大小是10,apple出现的频率为0.2,则词表中放2个apple
        self.sample_table = np.array(self.sample_table)
        np.random.shuffle(self.sample_table)

    def get_wordId_list(self):  # 将词的列表转化为id的列表
        # self.input_file = open(self.input_file_name, encoding="utf-8")  # 与初始化中的重复
        sentence = self.input_file.readline()
        wordId_list = []  # 一句话中的所有word 对应的 id
        sentence = sentence.strip().split(' ')
        for i, word in enumerate(sentence):
            if i % 1000000 == 0:
                print(i, len(sentence))
            try:  # 如果try中代码出现异常,跳过
                word_id = self.word2id_dict[word]
                wordId_list.append(word_id)
            except:
                continue
        self.wordId_list = wordId_list  # 将词组成的一句话变成由id组成的一句话, 虚线提示可以将word_list在初始化中定义

    def get_batch_pairs(self, batch_size, window_size):  # 获取正样本
        while len(self.word_pairs_queue) < batch_size:  # 当队列中数据个数小于batch_size, 向队列添加数据
            for _ in range(1000):  # TODO:这个循环的意义?
                if self.index == len(self.wordId_list):  # 如果index等于字典长度,说明遍历完了,index重置为0
                    self.index = 0
                wordId_w = self.wordId_list[self.index]  # 获取中心词
                for i in range(max(self.index - window_size, 0),  # 文章的第一个词没有左边的词,因此从0开始
                               min(self.index + window_size + 1, len(self.wordId_list))):  # 文章最后一个词没有右边的词,因此最大为文章长度
                    wordId_v = self.wordId_list[i]  # 获取周围词
                    if self.index == i:  # 上下文词=中心词 跳过
                        continue
                    self.word_pairs_queue.append((wordId_w, wordId_v))  # 加入队列(一个中心词,一个周围词)
                self.index += 1
        result_pairs = []  # 返回mini-batch大小的正采样对
        for _ in range(batch_size):
            result_pairs.append(self.word_pairs_queue.popleft())  # 将batch_size个数据(一个中心词,一个周围词)出队列
        return result_pairs

    def get_negative_sampling(self, positive_pairs, neg_count):
        """
        获取负采样 输入正采样对数组 positive_pairs,以及每个正采样对需要的负采样数 neg_count
        从采样表抽取负采样词的id(假设数据够大,不考虑负采样=正采样的小概率情况)
        :param positive_pairs: 正采样对照组
        :param neg_count: 每个正样本需要的负样本数
        :return: 负样本列表
        """
        neg_v = np.random.choice(self.sample_table, size=(len(positive_pairs), neg_count)).tolist()
        return neg_v  # 行数为正样本数,列数为负采样个数neg_count

    def evaluate_pairs_count(self, window_size):  # TODO:哪部分的公式?
        # 估计数据中正采样对数,用于设定batch
        return self.word_count_sum * (2 * window_size) - self.sentence_count * (
                1 + window_size) * window_size


if __name__ == "__main__":
    # 测试所有方法
    test_data = InputData('../data/text8.txt', min_count=1)
    test_data.evaluate_pairs_count(2)
    pos_pairs = test_data.get_batch_pairs(10, 2)
    print('正采样:')
    print(pos_pairs)
    pos_word_pairs = []
    for pair in pos_pairs:
        pos_word_pairs.append((test_data.id2word_dict[pair[0]], test_data.id2word_dict[pair[1]]))
    print(pos_word_pairs)
    neg_pair = test_data.get_negative_sampling(pos_pairs, 3)
    print('负采样:')
    print(neg_pair)
    neg_word_pair = []
    for pair in neg_pair:
        neg_word_pair.append(
            (test_data.id2word_dict[pair[0]], test_data.id2word_dict[pair[1]], test_data.id2word_dict[pair[2]]))
    print(neg_word_pair)

模型构建

​import torch
import torch.nn as nn
import torch.nn.functional as F
# skip-gram + 负采样的实现
class SkipGramModel(nn.Module):
    def __init__(self,vocab_size,embed_size):
        super(SkipGramModel,self).__init__()
        self.vocab_size = vocab_size  # 词表大小
        self.embed_size = embed_size  # 词向量维度
        self.w_embeddings = nn.Embedding(vocab_size, embed_size)  # 初始化中心词向量矩阵
        self.v_embeddings = nn.Embedding(vocab_size, embed_size)  # 初始化周围词向量矩阵
        self._init_emb()

    def _init_emb(self):
        initrange = 0.5 / self.embed_size
        self.w_embeddings.weight.data.uniform_(-initrange, initrange)  # 初始化中心词词向量矩阵权重
        self.v_embeddings.weight.data.uniform_(-0, 0)

    def forward(self, pos_w, pos_v, neg_v):
        emb_w = self.w_embeddings(torch.LongTensor(pos_w).cuda())  # 转为tensor 大小 [ mini_batch_size * emb_dimension ]
        emb_v = self.v_embeddings(torch.LongTensor(pos_v).cuda())
        neg_emb_v = self.v_embeddings(torch.LongTensor(neg_v).cuda())  # 转换后大小[ negative_sampling_number * mini_batch_size * emb_dimension]
        score = torch.mul(emb_w, emb_v)  # 对应元素相乘

        score = torch.sum(score, dim=1)  # 在emb_size这个维度求和得到矩阵相乘的效果
        score = torch.clamp(score, max=10, min=-10)
        score = F.logsigmoid(score)

        # 将中心词矩阵增加第2个维度变为 [batch_size * emb_dim * 1] 后与负样本周围词矩阵相乘,结果维度为[batch_size * negative_sampling_number * 1]
        neg_score = torch.bmm(neg_emb_v, emb_w.unsqueeze(2))
        neg_score = torch.clamp(neg_score, max=10, min=-10)
        neg_score = F.logsigmoid(-1 * neg_score)
        # L = log sigmoid (Xw.T * θv) + ∑neg(v) [log sigmoid (-Xw.T * θneg(v))]
        loss = - torch.sum(score) - torch.sum(neg_score)  # 求min
        return loss


    def save_embedding(self, id2word, file_name):
        embedding_1 = self.w_embeddings.weight.data.cpu().numpy()  # 保存中心词与周围词向量矩阵
        embedding_2 = self.v_embeddings.weight.data.cpu().numpy()
        embedding = (embedding_1+embedding_2)/2
        fout = open(file_name, 'w')
        fout.write('%d %d\n' % (len(id2word), self.embed_size))
        for wid, w in id2word.items():
            e = embedding[wid]
            e = ' '.join(map(lambda x: str(x), e))
            fout.write('%s %s\n' % (w, e))

if __name__ == '__main__':
    model = SkipGramModel(100, 10)
    id2word = dict()
    for i in range(100):
        id2word[i] = str(i)
    pos_w = [0, 0, 1, 1, 1]
    pos_v = [1, 2, 0, 2, 3]
    neg_v = [[23, 42, 32], [32, 24, 53], [32, 24, 53], [32, 24, 53], [32, 24, 53]]
    model.forward(pos_w, pos_v, neg_v)
    model.save_embedding(id2word, '../results/test.txt')

CBOW+HS代码的实现

数据预处理

import sys
sys.path.append("../CBOW_HS/")
from collections import deque
from huffman_tree import HuffmanTree


class InputData:
    def __init__(self, input_file_name, min_count):
        self.input_file_name = input_file_name
        self.input_file = open(self.input_file_name)  # 数据文件
        self.index = 0
        self.min_count = min_count  # 要淘汰的低频数据的频度
        self.wordId_frequency_dict = dict()  # 词id-出现次数 dict
        self.word_count = 0  # 单词数(重复的词只算1个)
        self.word_count_sum = 0  # 单词总数 (重复的词 次数也累加)
        self.sentence_count = 0  # 句子数
        self.id2word_dict = dict()  # 词id-词 dict
        self.word2id_dict = dict()  # 词-词id dict
        self._init_dict()  # 初始化字典
        self.get_wordId_list()
        self.huffman_tree = HuffmanTree(self.wordId_frequency_dict)  # 霍夫曼树
        self.huffman_pos_path, self.huffman_neg_path = self.huffman_tree.get_all_pos_and_neg_path()
        self.word_pairs_queue = deque()
        # 结果展示
        print('Word Count is:', self.word_count)
        print('Word Count Sum is', self.word_count_sum)
        print('Sentence Count is:', self.sentence_count)
        print('Tree Node is:', len(self.huffman_tree.huffman))

    def _init_dict(self):
        word_freq = dict()
        # 统计 word_frequency
        for line in self.input_file:
            line = line.strip().split(' ')  # 去首尾空格
            self.word_count_sum += len(line)
            self.sentence_count += 1
            for word in line:
                try:
                    word_freq[word] += 1
                except:
                    word_freq[word] = 1
        word_id = 0
        # 初始化 word2id_dict,id2word_dict, wordId_frequency_dict字典
        for per_word, per_count in word_freq.items():
            if per_count < self.min_count:  # 去除低频
                self.word_count_sum -= per_count
                continue
            self.id2word_dict[word_id] = per_word
            self.word2id_dict[per_word] = word_id
            self.wordId_frequency_dict[word_id] = per_count
            word_id += 1
        self.word_count = len(self.word2id_dict)

    # 获取mini-batch大小的 正采样对 (Xw,w) Xw为上下文id数组,w为目标词id。上下文步长为window_size,即2c = 2*window_size
    def get_wordId_list(self):
        self.input_file = open(self.input_file_name, encoding="utf-8")
        sentence = self.input_file.readline()
        wordId_list = []  # 一句中的所有word 对应的 id
        sentence = sentence.strip().split(' ')
        for i, word in enumerate(sentence):
            if i % 1000000 == 0:
                print(i, len(sentence))
            try:
                word_id = self.word2id_dict[word]
                wordId_list.append(word_id)
            except:
                continue
        self.wordId_list = wordId_list

    def get_batch_pairs(self, batch_size, window_size):
        while len(self.word_pairs_queue) < batch_size:
            for _ in range(1000):
                if self.index == len(self.wordId_list):
                    self.index = 0
                wordId_w = self.wordId_list[self.index]
                context_ids = []
                for i in range(max(self.index - window_size, 0),
                               min(self.index + window_size + 1, len(self.wordId_list))):

                    if self.index == i:  # 上下文=中心词 跳过
                        continue
                    context_ids.append(self.wordId_list[i])
                self.word_pairs_queue.append((context_ids, wordId_w))
                self.index += 1
        result_pairs = []  # 返回mini-batch大小的正采样对
        for _ in range(batch_size):
            result_pairs.append(self.word_pairs_queue.popleft())
        return result_pairs

    def get_pairs(self, pos_pairs):
        # 得到正负样本
        neg_word_pair = []
        pos_word_pair = []
        for pair in pos_pairs:
            pos_word_pair += zip([pair[0]] * len(self.huffman_pos_path[pair[1]]), self.huffman_pos_path[pair[1]])
            neg_word_pair += zip([pair[0]] * len(self.huffman_neg_path[pair[1]]), self.huffman_neg_path[pair[1]])
        return pos_word_pair, neg_word_pair

    # 估计数据中正采样对数,用于设定batch
    def evaluate_pairs_count(self, window_size):
        return self.word_count_sum * (2 * window_size - 1)  - (self.sentence_count - 1) * (1 + window_size) * window_size


# 测试所有方法
def test():
    test_data = InputData('../data/text8.txt', 3)
    pos_pairs = test_data.get_batch_pairs(10, 2)
    print(pos_pairs)
    pos_word_pairs = []
    for pair in pos_pairs:
        pos_word_pairs.append(([test_data.id2word_dict[i] for i in pair[0]], test_data.id2word_dict[pair[1]]))
    print(pos_word_pairs)
    print('')
    print(test_data.huffman_pos_path[0])
    print(test_data.huffman_neg_path[0])
    pos, neg = test_data.get_pairs(pos_pairs)
    print(pos)
    print(neg)


if __name__ == '__main__':
    test()

模型构建

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from torch.autograd import Variable


class CBOWModel(nn.Module):
    def __init__(self, vocab_size, emb_size):
        super(CBOWModel, self).__init__()
        self.vocab_size = vocab_size
        self.emb_size = emb_size
        self.u_embeddings = nn.Embedding(2*self.vocab_size-1, self.emb_size, sparse=True)
        self.w_embeddings = nn.Embedding(2*self.vocab_size-1, self.emb_size, sparse=True)
        self._init_embedding()  # 初始化

    def _init_embedding(self):
        int_range = 0.5 / self.emb_size
        self.u_embeddings.weight.data.uniform_(-int_range, int_range)
        self.w_embeddings.weight.data.uniform_(-0, 0)

    def compute_context_matrix(self, u):
        pos_u_emb = []  # 上下文embedding
        for per_Xw in u:
            # 上下文矩阵的第一维不同词值不同,如第一个词上下文为c,第二个词上下文为c+1,需要统一化
            per_u_emb = self.u_embeddings(torch.LongTensor(per_Xw).cuda())  # 对上下文每个词转embedding
            per_u_numpy = per_u_emb.data.cpu().numpy()  # 转回numpy,好对其求和
            per_u_numpy = np.sum(per_u_numpy, axis=0)
            per_u_list = per_u_numpy.tolist()  # 为上下文词向量Xw的值
            pos_u_emb.append(per_u_list)  # 放回数组
        pos_u_emb = torch.FloatTensor(pos_u_emb).cuda()  # 转为tensor 大小 [ mini_batch_size * emb_size ]
        return pos_u_emb

    def forward(self, pos_u, pos_w, neg_u, neg_w):
        """
        :param pos_u: 正样本的周围词,每个样本的周围词是一个list
        :param pos_w: 正样本的中心词
        :param neg_u: 负样本的周围词,每个样本的周围词是一个list
        :param neg_w: 负样本的中心词
        :return:
        """
        pos_u_emb = self.compute_context_matrix(pos_u)  # batch_size * emb_size
        pos_w_emb = self.w_embeddings(torch.LongTensor(pos_w).cuda())
        neg_u_emb = self.compute_context_matrix(neg_u)
        neg_w_emb = self.w_embeddings(torch.LongTensor(neg_w).cuda())

        # 计算梯度上升( 结果 *(-1) 即可变为损失函数 ->可使用torch的梯度下降)
        score_1 = torch.mul(pos_u_emb, pos_w_emb)  # Xw.T * θu
        score_2 = torch.sum(score_1, dim=1)  # 点积和
        score_3 = F.logsigmoid(score_2)  # log (1-sigmoid (Xw.T * θw))
        neg_score_1 = torch.mul(neg_u_emb, neg_w_emb)  # Xw.T * θw
        neg_score_2 = torch.sum(neg_score_1, dim=1)  # 点积和
        neg_score_3 = F.logsigmoid(-neg_score_2)  # ∑neg(w) [log sigmoid (-Xw.T * θneg(w))]
        # L = log sigmoid (Xw.T * θw) + logsigmoid (-Xw.T * θw)
        loss = torch.sum(score_3) + torch.sum(neg_score_3)
        return -1 * loss

    # 存储embedding
    def save_embedding(self, id2word_dict, file_name):
        embedding = self.u_embeddings.weight.data.cpu().numpy()
        file_output = open(file_name, 'w')
        file_output.write('%d %d\n' % (self.vocab_size, self.emb_size))
        for id, word in id2word_dict.items():
            e = embedding[id]
            e = ' '.join(map(lambda x: str(x), e))
            file_output.write('%s %s\n' % (word, e))


def test():
    model = CBOWModel(100, 10)
    id2word = dict()
    for i in range(100):
        id2word[i] = str(i)

    pos_pairs = [([1, 2, 3], 50), ([0, 1, 2, 3], 70)]
    neg_pairs = [([1, 2, 3], 50), ([0, 1, 2, 3], 70)]
    pos_u = [pair[0] for pair in pos_pairs]
    pos_w = [int(pair[1]) for pair in pos_pairs]
    neg_u = [pair[0] for pair in neg_pairs]
    neg_w = [int(pair[1]) for pair in neg_pairs]
    model.forward(pos_u, pos_w, neg_u, neg_w)
    model.save_embedding(id2word, 'test.txt')


if __name__ == '__main__':
    test()

本文为深度之眼paper论文班的学习笔记,仅供自己学习使用,如有问题欢迎讨论!关于课程可以扫描下图二维码

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值