文章目录
1 基于推理的方法和神经网络
用向量表示单词的方法主要有两种:一种是基于技术的方法;另一种是基于推理的方法。
1.1 基于计数的方法的问题
基于计数的方法根据一个单词周围的单词的出现频数来表示该单词。具体来说先生成所有单词的共现矩阵,再对这个矩阵进行SVD,以获得密集向量(单词的分布式表示)。
但是基于计数的方法在处理大规模语料时会出现问题。如果词汇量超过100万个,这种方法就要生成一个100万*100万的庞大矩阵,对如此庞大矩阵执行svd是不现实的,(对n*n矩阵svd复杂度O(n^3))。
而基于推理的方法使用神经网络,通常在mini-batch数据上进行学习,一次只需要看一部分学习数据,并反复更新权重。
基于推理的方法:输入上下文,模型输出各个单词的出现概率。
2 简单的word2vec
将神经网络嵌入下图所示的模型中。
2.1 CBOW模型的推理
CBOW=continuous bags-of-words
CBOW有两个输入层,经过中间层到达输出层。这里,从输入层到中间层的变换由相同的全连接层(权重为Win)完成,从中间层到输出层神经元的变换由另一个全连接层(权重为Wout)完成。中间层的神经元是各个输入层经全连接层变换后得到的值的平均。
权重Win的各行保存着各个单词的分布式表示。通过反复学习,不断更新各个单词的分布式表示,以正确地从上下文预测出应当出现的单词。令人惊讶的是,如此获得的响亮很好的对单词含义进行了编码。这就是word2vec的全貌。
2.2 CBOW模型的学习
这里我们处理的模型是一个进行多类别分类的神经网络。因此对其进行学习只是使用一下Sofmax函数和交叉熵误差。首先,使用Softmax函数将得分转化为概率,再求这些概率和监督标签之间的交叉熵误差,并将其作为损失进行学习。
2.3 word2vec的权重和分布式表示
输入侧和输出侧的权重都可以被视为单词的分布式表示。就word2vec(特别是skip-gram模型)而言,一般只使用输入侧的权重。
3 学习数据的准备
word2vec中使用的神经网络的输入是上下文,它的正确标签就是被这些上下文包围在中间的单词,即目标词。也就是说,我门要做的事情是,当向神经网络输入上下文时,使目标词的出现的概率高。
从语料库生成上下文contexts和目标词target。
ef create_contexts_target(corpus, window_size=1):
'''生成上下文和目标词
:param corpus: 语料库(单词ID列表)
:param window_size: 窗口大小(当窗口大小为1时,左右各1个单词为上下文)
:return:
'''
target = corpus[window_size:-window_size]
contexts = []
for idx in range(window_size, len(corpus)-window_size):
cs = []
for t in range(-window_size, window_size + 1):
if t == 0:
continue
cs.append(corpus[idx + t])
contexts.append(cs)
return np.array(contexts), np.array(target)
将上下文和目标词转化为one-hot表示。
def convert_one_hot(corpus, vocab_size):
'''转换为one-hot表示
:param corpus: 单词ID列表(一维或二维的NumPy数组)
:param vocab_size: 词汇个数
:return: one-hot表示(二维或三维的NumPy数组)
'''
N = corpus.shape[0]
if corpus.ndim == 1:
one_hot = np.zeros((N, vocab_size), dtype=np.int32)
for idx, word_id in enumerate(corpus):
one_hot[idx, word_id] = 1
elif corpus.ndim == 2:
C = corpus.shape[1]
one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
for idx_0, word_ids in enumerate(corpus):
for idx_1, word_id in enumerate(word_ids):
one_hot[idx_0, idx_1, word_id] = 1
return one_hot
4 CBOW模型的实现
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
class SimpleCBOW:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 初始化权重
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(H, V).astype('f')
# 生成层
self.in_layer0 = MatMul(W_in)
self.in_layer1 = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer = SoftmaxWithLoss()
# 将所有的权重和梯度整理到列表中
layers = [self.in_layer0, self.in_layer1, self.out_layer]
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):
h0 = self.in_layer0.forward(contexts[:, 0])
h1 = self.in_layer1.forward(contexts[:, 1])
h = (h0 + h1) * 0.5
score = self.out_layer.forward(h)
loss = self.loss_layer.forward(score, target)
return loss
def backward(self, dout=1):
ds = self.loss_layer.backward(dout)
da = self.out_layer.backward(ds)
da *= 0.5
self.in_layer1.backward(da)
self.in_layer0.backward(da)
return None
5 skip-gram模型
skip-gram是反转了CBOW模型处理的上下文和目标词的模型,它从中间的单词(目标词)预测周围的多个单词(上下文)。
skip-gram模型的输入层只有一个,输出层的恶数量则与上下文的单词个数相等。因此,首先要分别求出各个输出层的损失,然后将它们加起来作为最后的损失。
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
class SimpleSkipGram:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 初始化权重
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(H, V).astype('f')
# 生成层
self.in_layer = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer1 = SoftmaxWithLoss()
self.loss_layer2 = SoftmaxWithLoss()
# 将所有的权重和梯度整理到列表中
layers = [self.in_layer, self.out_layer]
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 = self.in_layer.forward(target)
s = self.out_layer.forward(h)
l1 = self.loss_layer1.forward(s, contexts[:, 0])
l2 = self.loss_layer2.forward(s, contexts[:, 1])
loss = l1 + l2
return loss
def backward(self, dout=1):
dl1 = self.loss_layer1.backward(dout)
dl2 = self.loss_layer2.backward(dout)
ds = dl1 + dl2
dh = self.out_layer.backward(ds)
self.in_layer.backward(dh)
return None