代码改变世界

nlp 改进CBOW模型

2022-04-05 14:56  jym蒟蒻  阅读(170)  评论(0编辑  收藏  举报

复习

首先复习一下之前的CBOW笔记。

采用推理的方法认知单词:

CBOW模型的核心思路:给出周围的单词(上下文)时,预测目标词处会出现什么单词。

要用神经网络处理单词,需要先将单词转化为固定长度的向量,这就引出了单词的表示。

单词的表示:将单词转化为固定长度的向量(one-hot 表示),神经网络的输入层的神经元个数就可以固定下来。

CBOW模型:它是根据上下文预测目标词的神经网络。模型的输入是上下文。大致结构如下。

在这里插入图片描述

上图中的Win和Wout都是矩阵,也就是说,全连接层变换可以看作是输入层与矩阵相乘。由此引出矩阵乘积层(MatMul)的实现,这里面:

提到了反向传播:

拿下图举例子,反过来的箭头指向的那个心是2.2,意思就是,如果jym用心程度增加1,那么jym就能多获得2.2×1=2.2个npy。也就是说,反向传播目的就是看出输入的变化对输出有何影响(偏导)。

在这里插入图片描述

乘法节点的反向传播示意图如下,可以看出乘法节点的反向传播,将上游传过来的导数(1.1)乘正向传播的翻转值,然后传给下游。jym的心获得的反向传播的值是:1.1乘正向传播的y的2=2.2。

在这里插入图片描述

由上面类比得到x、y、z是矩阵的情况下的反向传播,如下图。

在这里插入图片描述

接下来,需要对语料库进行数据的预处理。预处理究竟处理的什么,关键就在于下面这张图。首先从语料库生成上下文和目标词,然后把上下文和目标词转化为 one-hot 表示。

在这里插入图片描述

然后就继续去实现一下普通的CBOW模型类。

整体大致是下面这个情况(里面有的是简写,反向传播还得乘前面传来的偏导数)。

在这里插入图片描述

但是这个模型有两个问题,第一个问题就是:对于输入层来说,one-hot表示占用内存过多,计算one-hot表示与权重矩阵的乘积,需要花费大量时间。解决这个问题,就需要增加一个Embedding层。也就是说,将原来CBOW模型中输入侧的MatMul层换成Embedding层。

解决这个问题的核心思路就是:计算one-hot 表示矩阵和权重矩阵的乘积,其实就是将权重矩阵的某个特定的行取出来。

Embedding 层:一个从权重矩阵中抽取单词ID对应行(向量)的层。

Embedding 层的正向传播:从权重矩阵W中提取特定的行,并将特定行的神经元原样传给下一层。

Embedding 层的反向传播:上一层传来的梯度会被应用到权重梯度dW的特定行(idx)。

在这里插入图片描述

第二个问题是:中间层的神经元和权重矩阵的乘积、Softmax层的计算需要花费很多计算时间。

解决方法就是使用 Negative Sampling (负采样) 替代 Softmax。也就是用二分类拟合多分类。

二分类问题:处理答案为是或否的问题,比如目标词是 say 吗。让神经网络来回答:当上下文是 you 和 goodbye 时,目标词是 say 吗?这时输出层只需要一个神经元,神经元输出的是 say 的得分。

输出侧的权重矩阵中保存了各个单词ID对应的单词向量,提取目标词的单词向量,再求这个向量和中间层神经元的内积,就是最终的得分。示意图如下。

在这里插入图片描述

Embedding Dot 层合并Embedding 层和 dot 运算(内积运算),优化后的CBOW模型如下。

其中向Sigmoid with Loss 层输入正确解标签1,表示现在正在处理的问题的答案是Yes。当答案是No时,向 Sigmoid with Loss 层输入0。

在这里插入图片描述

下图是进一步说明:

在这里插入图片描述

目前,只是对正例say进行了二分类,如果此时模型有好的权重,则Sigmoid层的输出概率将接近1。

真正要做的事是,对于正例say,使Sigmoid层的输出接近1;对于负例(say 以外的单词),使Sigmoid层的输出接近0。

所以,接下来引出二分类负采样方法。主要内容如下:

负采样方法:求正例作为目标词时的损失,同时采样(选出)若干个负例,对这些负例求损失。然后,将正例和采样出来的负例的损失加起来,作为最终的损失。

抽取负例:让语料库中常出现的单词易被抽到,不常出现的单词难被抽到。

基于频率的采样方法:计算语料库中各个单词的出现次数,并将其表示为概率分布,然后使用这个概率分布对单词进行采样。负采样通过仅关注部分单词的方法实现了计算的高速化。

至此,就通过画图梳理的方法,串联起之前写的笔记的整体脉络,把CBOW模型复习完了。

接下来需要做的是,把改进后的CBOW模型实现出来,然后在PTB数据集上进行学习。

改进版的CBOW模型

CBOW类如下:

参数:vocab_size词汇量,hidden_size中间层的神经元个数,corpus单词ID列表,window_size上下文的大小。

import sys
sys.path.append('..')
from common.np import *  # import numpy as np
from common.layers import Embedding
from negative_sampling_layer import NegativeSamplingLoss


class CBOW:
    def __init__(self, vocab_size, hidden_size, window_size, corpus):
        V, H = vocab_size, hidden_size

        # 初始化权重
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(V, H).astype('f')

        # 生成层
        self.in_layers = []
        for i in range(2 * window_size):
            layer = Embedding(W_in)  # 使用Embedding层
            self.in_layers.append(layer)
        self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)

        # 将所有的权重和梯度整理到列表中
        layers = self.in_layers + [self.ns_loss]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params#存神经网络里面的参数
            self.grads += layer.grads#存神经网络里面的梯度

        # 将单词的分布式表示设置为成员变量
        self.word_vecs = W_in

    def forward(self, contexts, target):#contexts是上下文,target是目标词,这里是单词ID形式的
        h = 0
        for i, layer in enumerate(self.in_layers):
            h += layer.forward(contexts[:, i])#contexts是二维数组
        h *= 1 / len(self.in_layers)
        loss = self.ns_loss.forward(h, target)#target是一维数组
        return loss

    def backward(self, dout=1):
        dout = self.ns_loss.backward(dout)
        dout *= 1 / len(self.in_layers)
        for layer in self.in_layers:
            layer.backward(dout)
        return None

CBOW模型的学习:

import sys
sys.path.append('..')
from common import config

# 开GPU
config.GPU = True

from common.np import *
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from skip_gram import SkipGram
from common.util import create_contexts_target, to_cpu, to_gpu
from dataset import ptb


# 设定超参数
window_size = 5
hidden_size = 100
batch_size = 100
max_epoch = 10

# 读入数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)

contexts, target = create_contexts_target(corpus, window_size)
if config.GPU:
    contexts, target = to_gpu(contexts), to_gpu(target)

# 生成模型等
model = CBOW(vocab_size, hidden_size, window_size, corpus)
# model = SkipGram(vocab_size, hidden_size, window_size, corpus)
optimizer = Adam()
trainer = Trainer(model, optimizer)

# 开始学习
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 保存必要数据,以便后续使用,取出输入侧得权重和单词与单词ID之间转化得字典
word_vecs = model.word_vecs
if config.GPU:
    word_vecs = to_cpu(word_vecs)
params = {}
params['word_vecs'] = word_vecs.astype(np.float16)
params['word_to_id'] = word_to_id
params['id_to_word'] = id_to_word
pkl_file = 'cbow_params.pkl'  # or 'skipgram_params.pkl'
with open(pkl_file, 'wb') as f:
    pickle.dump(params, f, -1)

运行情况如下:

在这里插入图片描述

CBOW模型评价:

def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
    '''相似单词的查找

    :param query: 查询词
    :param word_to_id: 从单词到单词ID的字典
    :param id_to_word: 从单词ID到单词的字典
    :param word_matrix: 汇总了单词向量的矩阵,假定保存了与各行对应的单词向量
    :param top: 显示到前几位
    '''
import sys
sys.path.append('..')
from common.util import most_similar, analogy
import pickle


pkl_file = 'cbow_params.pkl'
# pkl_file = 'skipgram_params.pkl'

with open(pkl_file, 'rb') as f:
    params = pickle.load(f)
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

# most similar task
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5)

# analogy task
print('-'*50)
analogy('king', 'man', 'queen',  word_to_id, id_to_word, word_vecs)
analogy('take', 'took', 'go',  word_to_id, id_to_word, word_vecs)
analogy('car', 'cars', 'child',  word_to_id, id_to_word, word_vecs)
analogy('good', 'better', 'bad',  word_to_id, id_to_word, word_vecs)

输出:从结果看出,CBOW 模型获得的单词的分布式表示具有良好的性质。而且,不仅可以将近似单词聚在一起,还可以通过向量的加减法来解决类推问题,比如man : woman = king : ?

而且结果说明,单词的分布式不仅限于捕获单词的含义,它也捕获了语法中的模式。

迁移学习:在某个领域学到的知识可以被应用于其他领域。一般先在大规模语料库上学习,然后将学习好的分布式表示应用于某个单独的任务。

单词的分布式表示的优点是可以将单词或文档转化为固定长度的向量。如果可以将自然语言转化为向量,就可以使用常规的机器学习方法。将这个向量作为其他机器学习系统的输入,利用常规的机器学习框架输出目标答案。比如,将向量化的邮件及其情感标签输入某个情感分类系统(SVM 或神经网络等)进行学习。大系统由:生成单词的分布式表示的系统(word2vec)、对特定问题进行分类的系统(比如进行情感分类 的 SVM 等)组成。

分布式表示的评价方法:单词的分布式表示的评价往往与实际应用分开进行,经常使用的评价指标有相似度和类推问题。

[query] you
 we: 0.73095703125
 i: 0.70703125
 your: 0.6103515625
 they: 0.59375
 anybody: 0.58740234375

[query] year
 month: 0.8505859375
 week: 0.77392578125
 summer: 0.7568359375
 spring: 0.75390625
 decade: 0.69873046875

[query] car
 luxury: 0.603515625
 window: 0.59375
 cars: 0.58349609375
 auto: 0.580078125
 truck: 0.5771484375

[query] toyota
 marathon: 0.65087890625
 nissan: 0.6455078125
 honda: 0.6357421875
 seita: 0.630859375
 minicomputers: 0.6005859375
--------------------------------------------------

[analogy] king:man = queen:?
 carolinas: 5.26953125
 woman: 5.11328125
 a.m: 5.08203125
 downside: 4.90234375
 mother: 4.65234375

#下面说明单词的分布式表示编码了时态相关的信息
[analogy] take:took = go:?
 went: 4.53125
 goes: 4.44921875
 were: 4.32421875
 was: 4.2421875
 came: 4.15234375

[analogy] car:cars = child:?
 a.m: 6.54296875
 rape: 5.8046875
 children: 5.453125
 daffynition: 4.91796875
 women: 4.8984375

[analogy] good:better = bad:?
 rather: 6.36328125
 less: 5.24609375
 more: 5.17578125
 greater: 4.2265625
 fewer: 3.80859375