百度深度学习集训营第二阶段-作业2

NLP作业2:实现CBOW模型

根据下面给定的Skip-gram模型代码,修改成为CBOW模型。使用text8语料进行训练,并尝试各种花式word-embedding玩法(比如,计算同义词,推理,进行聚类,或者进行可视化分析等)或者尝试修改代码,让skip-gram训练的更快。

从上面的描述中选一个点开始做,基于下面的代码,实现你的idea。

#!/usr/bin/env python
# coding: utf-8

import io
import os
import sys
import requests
from collections import OrderedDict
import math
import random
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Embedding

# 下载语料用来训练word2vec
def download():
    # 可以从百度云服务器下载一些开源数据集(dataset.bj.bcebos.com)
    corpus_url = "https://dataset.bj.bcebos.com/word2vec/text8.txt"
    # 使用python的requests包下载数据集到本地
    web_request = requests.get(corpus_url)
    corpus = web_request.content
    # 把下载后的文件存储在当前目录的text8.txt文件内
    with open("./text8.txt", "wb") as f:
        f.write(corpus)
    f.close()

# 读取text8数据
def load_text8():
    with open("./text8.txt", "r") as f:
        corpus = f.read().strip("\n")
    f.close()
    return corpus

# 对语料进行预处理(分词)
def data_preprocess(corpus):
    # 由于英文单词出现在句首的时候经常要大写,所以我们把所有英文字符都转换为小写,
    # 以便对语料进行归一化处理(Apple vs apple等)
    corpus = corpus.strip().lower()
    corpus = corpus.split(" ")
    return corpus

# 构造词典,统计每个词的频率,并根据频率将每个词转换为一个整数id
def build_dict(corpus):
    # 首先统计每个不同词的频率(出现的次数),使用一个词典记录
    word_freq_dict = dict()
    for word in corpus:
        if word not in word_freq_dict:
            word_freq_dict[word] = 0
        word_freq_dict[word] += 1

    # 将这个词典中的词,按照出现次数排序,出现次数越高,排序越靠前
    # 一般来说,出现频率高的高频词往往是:I,the,you这种代词,而出现频率低的词,往往是一些名词,如:nlp
    word_freq_dict = sorted(word_freq_dict.items(), key=lambda x: x[1], reverse=True)

    # 构造3个不同的词典,分别存储,
    # 每个词到id的映射关系:word2id_dict
    # 每个id出现的频率:word2id_freq
    # 每个id到词典映射关系:id2word_dict
    word2id_dict = dict()
    word2id_freq = dict()
    id2word_dict = dict()

    # 按照频率,从高到低,开始遍历每个单词,并为这个单词构造一个独一无二的id
    for word, freq in word_freq_dict:
        curr_id = len(word2id_dict)
        word2id_dict[word] = curr_id
        word2id_freq[word2id_dict[word]] = freq
        id2word_dict[curr_id] = word
    return word2id_freq, word2id_dict, id2word_dict


# max_window_size代表了最大的window_size的大小,程序会根据max_window_size从左到右扫描整个语料
# negative_sample_num代表了对于每个正样本,我们需要随机采样多少负样本用于训练,
# 一般来说,negative_sample_num的值越大,训练效果越稳定,但是训练速度越慢。
def build_data(corpus, word2id_dict, word2id_freq, CBOW_window_size=3, negative_sample_num=4):
    # 使用一个list存储处理好的数据
    dataset = []
    for start_idx in range(len(corpus)):
        context_range = start_idx, start_idx + 2 * CBOW_window_size
        center_word_idx = start_idx + CBOW_window_size

        if context_range[1] < len(corpus):
            context_words = corpus[context_range[0]:context_range[1] + 1]
            print(context_words)
            center_word = corpus[center_word_idx]
            del context_words[CBOW_window_size]
            context_words = [word2id_dict[context_word] for context_word in context_words]
            center_word = word2id_dict[center_word]
            dataset.append((context_words, center_word, 1))

            i = 0
            while i < negative_sample_num:
                negative_word_idx = random.randint(0, vocab_size - 1)
                if negative_word_idx != center_word_idx:
                    negative_word = corpus[negative_word_idx]
                    negative_word = word2id_dict[negative_word]
                    dataset.append((context_words, negative_word, 0))
    return dataset



# 我们将不同类型的数据放到不同的tensor里,便于神经网络进行处理
# 并通过numpy的array函数,构造出不同的tensor来,并把这些tensor送入神经网络中进行训练
def build_batch(dataset, batch_size, epoch_num):
    # center_word_batch缓存batch_size个中心词
    center_word_batch = []
    # target_word_batch缓存batch_size个目标词(可以是正样本或者负样本)
    target_word_batch = []
    # label_batch缓存了batch_size个0或1的标签,用于模型训练
    label_batch = []

    for epoch in range(epoch_num):
        # 每次开启一个新epoch之前,都对数据进行一次随机打乱,提高训练效果
        random.shuffle(dataset)

        for center_word, target_word, label in dataset:
            # 遍历dataset中的每个样本,并将这些数据送到不同的tensor里
            center_word_batch.append([center_word])
            target_word_batch.append([target_word])
            label_batch.append(label)

            # 当样本积攒到一个batch_size后,我们把数据都返回回来
            # 在这里我们使用numpy的array函数把list封装成tensor
            # 并使用python的迭代器机制,将数据yield出来
            # 使用迭代器的好处是可以节省内存
            if len(center_word_batch) == batch_size:
                yield np.array(center_word_batch).astype("int64"), np.array(target_word_batch).astype(
                    "int64"), np.array(label_batch).astype("float32")
                center_word_batch = []
                target_word_batch = []
                label_batch = []

    if len(center_word_batch) > 0:
        yield np.array(center_word_batch).astype("int64"), np.array(target_word_batch).astype("int64"), np.array(
            label_batch).astype("float32")



# 定义CBOW训练网络结构
# 这里我们使用的是paddlepaddle的1.6.1版本
# 一般来说,在使用fluid训练的时候,我们需要通过一个类来定义网络结构,这个类继承了fluid.dygraph.Layer
class CBOW(fluid.dygraph.Layer):
    def __init__(self, name_scope, vocab_size, embedding_size, init_scale=0.1):
        # name_scope定义了这个类某个具体实例的名字,以便于区分不同的实例(模型)
        # vocab_size定义了这个skipgram这个模型的词表大小
        # embedding_size定义了词向量的维度是多少
        # init_scale定义了词向量初始化的范围,一般来说,比较小的初始化范围有助于模型训练
        super(CBOW, self).__init__(name_scope)
        self.vocab_size = vocab_size
        self.embedding_size = embedding_size

        # 使用paddle.fluid.dygraph提供的Embedding函数,构造一个词向量参数
        # 这个参数的大小为:[self.vocab_size, self.embedding_size]
        # 数据类型为:float32
        # 这个参数的名称为:embedding_para
        # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样
        self.embedding = Embedding(
            self.full_name(),
            size=[self.vocab_size, self.embedding_size],
            dtype='float32',
            param_attr=fluid.ParamAttr(
                name='embedding_para',
                initializer=fluid.initializer.UniformInitializer(
                    low=-0.5 / embedding_size, high=0.5 / embedding_size)))

        # 使用paddle.fluid.dygraph提供的Embedding函数,构造另外一个词向量参数
        # 这个参数的大小为:[self.vocab_size, self.embedding_size]
        # 数据类型为:float32
        # 这个参数的名称为:embedding_para
        # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样
        # 跟上面不同的是,这个参数的名称跟上面不同,因此,
        # embedding_out_para和embedding_para虽然有相同的shape,但是权重不共享
        self.embedding_out = Embedding(
            self.full_name(),
            size=[self.vocab_size, self.embedding_size],
            dtype='float32',
            param_attr=fluid.ParamAttr(
                name='embedding_out_para',
                initializer=fluid.initializer.UniformInitializer(
                    low=-0.5 / embedding_size, high=0.5 / embedding_size)))

    # 定义网络的前向计算逻辑
    # center_words是一个tensor(mini-batch),表示中心词
    # target_words是一个tensor(mini-batch),表示目标词
    # label是一个tensor(mini-batch),表示这个词是正样本还是负样本(用0或1表示)
    # 用于在训练中计算这个tensor中对应词的同义词,用于观察模型的训练效果
    def forward(self, center_words, target_words, label):
        # 首先,通过embedding_para(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)

        # center_words_emb = [batch_size, embedding_size]
        # target_words_emb = [batch_size, embedding_size]
        # 我们通过点乘的方式计算中心词到目标词的输出概率,并通过sigmoid函数估计这个词是正样本还是负样本的概率。
        word_sim = fluid.layers.elementwise_mul(center_words_emb, target_words_emb)
        word_sim = fluid.layers.reduce_sum(word_sim, dim=-1)
        pred = fluid.layers.sigmoid(word_sim)

        # 通过估计的输出概率定义损失函数
        loss = fluid.layers.sigmoid_cross_entropy_with_logits(word_sim, label)
        loss = fluid.layers.reduce_mean(loss)

        # 返回前向计算的结果,飞桨会通过backward函数自动计算出反向结果。
        return pred, loss


# 定义一个使用word-embedding查询同义词的函数
# 这个函数query_token是要查询的词,k表示要返回多少个最相似的词,embed是我们学习到的word-embedding参数
# 我们通过计算不同词之间的cosine距离,来衡量词和词的相似度
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])))


if __name__ == '__main__':
    # 下载语料用来训练word2vec
    download()
    print("corpus download end!")
    input("cotinue")
    # 读取text8数据
    corpus = load_text8()
    # 打印前500个字符,简要看一下这个语料的样子
    print(corpus[:500])
    # 对语料进行预处理(分词)
    corpus = data_preprocess(corpus)[:1000]
    print(corpus[:50])
    # 构造词典,统计每个词的频率,并根据频率将每个词转换为一个整数id
    word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
    vocab_size = len(word2id_freq)
    print("there are totoally %d different words in the corpus" % vocab_size)
    for _, (word, word_id) in zip(range(50), word2id_dict.items()):
        print("word %s, its id %d, its word freq %d" % (word, word_id, word2id_freq[word_id]))

    # 构造数据,准备模型训练
    dataset = build_data(corpus, word2id_dict, word2id_freq)
    for _, (center_word, target_word, label) in zip(range(50), dataset):
        print("center_word %s, target %s, label %d" % (id2word_dict[center_word],
                                                       id2word_dict[target_word], label))
    # 构造mini-batch,准备对模型进行训练
    for _, batch in zip(range(10), build_batch(dataset, 128, 3)):
        print(batch)
    # 定义CBOW训练网络结构
    # 开始训练,定义一些训练过程中需要使用的超参数
    batch_size = 512
    epoch_num = 3
    embedding_size = 200
    step = 0
    learning_rate = 0.001

    # 将模型放到GPU上训练(fluid.CUDAPlace(0)),如果需要指定CPU,则需要改为fluid.CPUPlace()
    with fluid.dygraph.guard(fluid.CPUPlace()):
        # 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络
        skip_gram_model = CBOW("cbow_model", vocab_size, embedding_size)
        # 构造训练这个网络的优化器
        adam = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate)

        # 使用build_batch函数,以mini-batch为单位,遍历训练数据,并训练网络
        for center_words, target_words, label in build_batch(
                dataset, batch_size, epoch_num):
            # 使用fluid.dygraph.to_variable函数,将一个numpy的tensor,转换为飞桨可计算的tensor
            center_words_var = fluid.dygraph.to_variable(center_words)
            target_words_var = fluid.dygraph.to_variable(target_words)
            label_var = fluid.dygraph.to_variable(label)

            # 将转换后的tensor送入飞桨中,进行一次前向计算,并得到计算结果
            pred, loss = skip_gram_model(
                center_words_var, target_words_var, label_var)

            # 通过backward函数,让程序自动完成反向计算
            loss.backward()
            # 通过minimize函数,让程序根据loss,完成一步对参数的优化更新
            adam.minimize(loss)
            # 使用clear_gradients函数清空模型中的梯度,以便于下一个mini-batch进行更新
            skip_gram_model.clear_gradients()

            # 每经过100个mini-batch,打印一次当前的loss,看看loss是否在稳定下降
            step += 1
            if step % 100 == 0:
                print("step %d, loss %.3f" % (step, loss.numpy()[0]))

            # 经过10000个mini-batch,打印一次模型对eval_words中的10个词计算的同义词
            # 这里我们使用词和词之间的向量点积作为衡量相似度的方法
            # 我们只打印了5个最相似的词
            if step % 10000 == 0:
                get_similar_tokens('one', 5, skip_gram_model.embedding._w)
                get_similar_tokens('she', 5, skip_gram_model.embedding._w)
                get_similar_tokens('chip', 5, skip_gram_model.embedding._w)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值