当前位置: 首页 > article >正文

自然语言处理入门3——Embedding和神经网络加速计算

一、概述

前面介绍的神经网络方法速度比起传统计数方法提高了很多,但是仍然存在很大的局限性。首先我们之前的语料库很小,才几个词,最多也就几千个。如果假设我们现在的语料库有100万个单词(这其实很正常,不论中文还是英文,大型语料库可能还更大),那么输入的单词用one-hot表示法就有100万个维数,如果中间层向量大小是100,那么权重矩阵的大小就是[1000000,100],也就是要做1000000X100次乘法,需要耗费大量的时间和内存。反向传播的时候也有同样的问题。而且在做softmax计算的时候,分母要求100万次指数,并把这个100万个指数计算结果相加,计算成本可想而知,并且每个单词用长度为100万的向量来表示也非常耗资源。

所以书中提出了一种方法(《深度学习进阶:自然语言处理》,斋藤康毅)来解决这问题。

 二、Embedding表示 

首先,把单词进行embedding表示。如果语料库大小是100万,那么我们对单词做one-hot表示后变成[1,1000000]的向量,然后跟权重矩阵W_in相乘,这个计算很耗时耗资源,而本质上只是把对应矩阵每一行提取出来,如下图所示:

那么现在不如直接把权重矩阵的对应行提取出来就行了,不需要再花力气表示成one-hot,以及做乘法了。这个操作就叫做embedding。

class Embedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.idx = None
        
    def forward(self, idx):
        W, = self.params
        self.idx = idx
        out = W[idx]
        return out
    
    def backward(self, dout):
        dW, = self.grads
        dW[...] = 0

        np.add.at(dW, self.idx, dout) # 矩阵相加,根据idx指定要相加的行
        return None
import numpy as np
# 设定一个权重矩阵
W = np.arange(21).reshape(7,3)
print(W)
# 用权重矩阵对语料库进行embedding操作
corpus = Embedding(W)
# 取出第一个单词的embedding表示
word_embedding = corpus.forward(1)
print("word_embedding:",word_embedding)
# 输出
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]
 [12 13 14]
 [15 16 17]
 [18 19 20]]
word_embedding: [3 4 5]

可以看到,第一个单词的embedding表示就是权重矩阵的第1行。

三、负采样

第二个方法就是负采样方法。假设还是要预测完形填空:“I ? hello and you say goodbye.”。所谓的负采样方法就是要将多分类转换成二分类,原来我们的神经网络输出是多分类的,并判断哪个输出的概率最高,现在二分类的话,只要判断i和hello之间的词是否是say,只要取出代表say的单词向量,和中间层相乘,得到的结果就是得分。根据这个得分来判断输出是否是say。并把softmax变成sigmoid函数,本质上sigmoid就是softmax在二分类情况下的特例。两者的区别如下图:

之前的多分类方法,得到打分结果后,进行softmax操作得到概率,取出最大的概率与标签say进行比较,获得交叉熵损失。

这是二分类的示意图,区别在于后半部分,这里不在通过矩阵乘法和输出矩阵相乘,而是先对标签say进行embedding表示,用输出矩阵获得embedding表示,再进行内积,输入到sigmoid函数中获取交叉熵损失。这里的结果就是是否是say,把原来的多分类问题转化成了二分类问题,把这个过程用一个函数来表示,称为EmbeddingDot。

class EmbeddingDot:
    
    def __init__(self, W):
        self.embed = Embedding(W)
        self.params = self.embed.params
        self.grads = self.embed.grads
        self.cache = None # 保存正向传播时的计算结果
        
    def forward(self, h, idx):
        # 根据正确的标签值从输出权重矩阵中获取到embedding表示
        target_W = self.embed.forward(idx)
        # 把标签值的embedding结果和中间结果求内积
        out = np.sum(target_W * h, axis=1)
        # 保存正向传播时的结果,反向传播时用到
        self.cache = (h, target_W)
        # 这个输出值用来做sigmoid
        return out
    
    def backward(self, dout):
        h, target_W = self.cache
        dout = dout.reshape(dout.shape[0], -1)
        dtarget_W = dout * h
        self.embed.backward(dtarget_W)
        dh = dout * target_W
        return dh

负采样是因为,我们现在做的是二分类,结果就是yes或no,正例和反例,正例其实就是一个,比如“I ? hello you say goodbye.”,问好处的标准答案是say,所以预测结果是say的时候,返回应该是1,其余所有的情况都应该是0,而如果语料库总量是一百万,那么剩下的999999个单词输出都应该是0,这么大的量是很难训练的,何况这仅仅只是一种情况。所以书中提出的方法是对反例进行采样,选择概率最高的几种情况,作为反例的采样。

最终把正例的损失函数和反例的损失函数加在一起作为总的损失函数,进行反向传播,更新参数,完成训练。

import collections

class UnigramSampler:
    def __init__(self, corpus, power, sample_size):
        self.sample_size = sample_size
        self.vocab_size = None
        self.word_p = None

        counts = collections.Counter()
        for word_id in corpus:
            counts[word_id] += 1

        vocab_size = len(counts)
        self.vocab_size = vocab_size

        self.word_p = np.zeros(vocab_size)
        for i in range(vocab_size):
            self.word_p[i] = counts[i]
        # 提高原来特别小的概率值
        self.word_p = np.power(self.word_p, power)
        self.word_p /= np.sum(self.word_p)

    def get_negative_sample(self, target):
        batch_size = target.shape[0]
        # 采样

        negative_sample = np.random.choice(self.vocab_size,
                 size=(batch_size, self.sample_size),
                 replace=True, p=self.word_p)

        return negative_sample
corpus = np.array([0,1,2,3,4,1,2,3])
power = 0.75
sample_size = 2 #负采样的个数

sampler = UnigramSampler(corpus, power, sample_size)
target = np.array([1,3,0]) # 真实值,batch_size=3
negative_sample = sampler.get_negative_sample(target)
print(negative_sample)
# 输出:
[[3 2]
 [1 4]
 [1 3]]

上面这段代码就是从语料库中采样出2个反例的例子。可以看到,真实值为1的时候,取的反例是[3,2];真实值为3的时候,取的反例是[1,4];真实值为0的时候,取的反例是[1,3]。把上面的内容结合在一起就是下面的负采样代码:

class NegativeSamplingLoss:
    
    def __init__(self, W, corpus, power=0.75, sample_size=5):
        self.sample_size = sample_size
        self.sampler = UnigramSampler(corpus, power, sample_size)
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size+1)]
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size+1)]
        self.params, self.grads = [], []
        for layer in self.embed_dot_layers:
            self.params += layer.params
            self.grads += layer.grads
        
    def forward(self, h, target):
        batch_size = target.shape[0]
        # 根据目标进行负采样
        negative_sample = self.sampler.get_negative_sample(target)
        # 正例的正向传播
        score = self.embed_dot_layers[0].forward(h, target)
        correct_label = np.ones(batch_size, dtype = np.int32)
        # 正向损失
        loss = self.loss_layers[0].forward(score, correct_label)
        # 负例的正向传播
        negative_label = np.zeros(batch_size, dtype = np.int32)
        for i in range(self.sample_size):
            negative_target = negative_sample[:,i]
            score = self.embed_dot_layers[1+i].forward(h, negative_target)
            # 负向损失
            loss += self.loss_layers[1+i].forward(score, negative_label)
        
        return loss # 返回总损失
    
    def backward(self, dout = 1):
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
            dscore = l0.backward(dout)
            dh += l1.backward(dscore)
        return dh

基于此,我们改进了上一篇文章(自然语言处理入门2——神经网络)中实现的CBOW模型:

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):
        h = 0
        for i, layer in enumerate(self.in_layers):
            h += layer.forward(contexts[:,i])
        h *= 1 / len(self.in_layers)
        loss = self.ns_loss.forward(h, 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

四、案例测试

这里我使用了一个英文语料库,进行训练。

import sys
sys.path.append('..')
import numpy as np

import pickle

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

# 读入数据
corpus, word_to_id, id_to_word = load_data('train')
vocab_size = len(word_to_id)
contexts, target = create_contexts_target(corpus, window_size)


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

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

# 保存必要数据,以便后续使用
word_vecs = model.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'
with open (pkl_file, 'wb') as f:
    pickle.dump(params, f, -1)

训练结果如下:

import sys
import pickle

 # 余弦相似度计算
# eps防止除数为0
def cos_similarity(x, y, eps=1e-8):
    nx = x/(np.sqrt(np.sum(x**2))+eps)
    ny = y/(np.sqrt(np.sum(y**2))+eps)
    return np.dot(nx, ny)
    
# 单词相似度排序函数
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
    # 1.取出查询词
    if query not in word_to_id:
        print('%s is not found' % query)
        return
    print('\n[query] '+query)
    query_id = word_to_id[query]
    query_vec = word_matrix[query_id]
    
    # 2.计算余弦相似度
    vocab_size = len(id_to_word)
    similarity = np.zeros(vocab_size)
    for i in range(vocab_size):
        similarity[i] = cos_similarity(word_matrix[i], query_vec)
        
    # 3.基于余弦相似度,按降序输出值
    count = 0
    for i in (-1*similarity).argsort():
        if id_to_word[i] == query:
            continue
        print('%s : %s' % (id_to_word[i], similarity[i]))
        count += 1
        if count >= top:
            return     
      
pkl_file = 'cbow_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']

querys = ['you', 'year', 'car', 'toyota']
for query in querys:
  most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
  
# 输出:
[query] you
we : 0.73486328125
i : 0.65869140625
your : 0.6142578125
they : 0.60986328125
someone : 0.58544921875

[query] year
month : 0.857421875
week : 0.78076171875
summer : 0.7763671875
spring : 0.76171875
decade : 0.705078125

[query] car
truck : 0.61181640625
luxury : 0.60205078125
auto : 0.59619140625
window : 0.55712890625
cars : 0.54296875

[query] toyota
marathon : 0.685546875
weyerhaeuser : 0.65478515625
seita : 0.63232421875
engines : 0.626953125
chevrolet : 0.623046875

可以看到经过训练后,模型学到了单词比较好的向量化表示方式,you相似度最高的单词是we,year相似度最高的单词是month,car相似度最高的单词是truck,和我们人类的主观判断是比较接近的。


http://www.kler.cn/a/580231.html

相关文章:

  • 云计算网络学习笔记整理
  • flutter EventBus 的使用介绍
  • c# 使用Md5加密字符串
  • Docker篇
  • Elasticsearch 提升查询精度
  • ca证书和服务端证书两者之间的关系
  • 论文阅读分享——UMDF(AAAI-24)
  • 【VMware安装Ubuntu实战分享】
  • C语言笔记(通讯录)
  • Linux系统的安全加固与安全防护
  • postgresql json和jsonb问题记录
  • 基于物联网技术的分布式光伏监控系统设计与实现
  • 【STM32】ADC功能-单通道多通道(学习笔记)
  • 面试题之vue和react的异同
  • 机电公司管理信息系统小程序+论文源码调试讲解
  • Electron应用中获取设备唯一ID和系统信息
  • 中国证监会主席吴清:进一步优化差异化安排 更精准支持优质科技企业上市
  • springboot-自定义注解
  • CDefView::_GetPIDL函数分析之ListView_GetItem函数的参数item的item.mask 为LVIF_PARAM
  • React:类组件(中)