1. 为什么要引入神经网络语言模型
统计语言模型,如n-gram有如下几个特点:
- 基于统计的语言模型,是一种离散型的语言模型,所以泛化能力差。
- 参数量大,随着 n n n的增大,参数空间呈指数增长容易出现维度灾难的问题;巨大的参数量也使得模型无法与 n − 1 n-1 n−1个词之外的词建立联系。即不能进行上下文的回溯,不能解决上下文物主代词指代问题。
- 数据稀疏除了带来数据空间增大的问题之外,还有一个问题:无法表征词语之间的相似关系。
2. 神经网络语言模型
鉴于上面的问题,人们开始尝试用神经网络来建立语言模型,最经典的无疑是Bengio 的文章A Neural Probabilistic Language Model,文章中提出了如下图所示的前馈神经网络结构:
先从整体上看,上述模型属于比较简单而传统的神经网络模型,主要由输入层-隐藏层-输出层组成,经过前向传播和反向传播来进行训练。
2.1 前向传播
从单词到输入层
在最最开始,我们必须要重提一下语言模型的目的:判断一句话是不是人话。途径是啥?——通过前面的词预测后面的词。而其实神经网络语言模型是基于n-gram演变而来的,即核心是根据前
n
−
1
n-1
n−1个词预测第
n
n
n个词,那么我们模型最开始的输入就是前
n
−
1
n-1
n−1个词。那么我们又是怎么来表征前
n
−
1
n-1
n−1个词的呢?答案是词向量。
从Ont-hot到Word Embedding
词语转化为数字的最简单的形式就是One-hot,简单来说就是假设有一个大小为 V V V的固定排序的词表,里边包含 V V V个词,假设第二个词是“电视”,那么我们用一个维度为 V V V的特征向量表达就是 [ 0 , 1 , 0 , 0 , ⋯ , 0 ] [0,1,0,0,\cdots,0] [0,1,0,0,⋯,0],即该词语在词表中的位置对应在特征向量中的位置的值为 1 1 1,其他位置都为 0 0 0。One-hot编码有一个最大的问题就是数据稀疏问题,当词表很大(比如我们现在有一个含 80000 80000 80000个词的词表)时,数据稀疏会让整个计算量都变得很大。且词语之间的关联关系得不到表达。
那么词向量(Word Embedding)又是什么呢?人们也叫它词嵌入,就是说我现在不用One-hot那样的稀疏向量来表征我这个词了,我就用一个低维度的向量来表征我这个词,当你很难理解的时候你可以说它是玄学,反正世界上就有这么一个向量能表征我选择的这个词,并且我词表里的每一个词都有对应的表征向量。这个词向量又是怎么取得的呢?
我们给定一个词表征的矩阵 C C C,这个 C C C的维度是 V ∗ m V*m V∗m,即 V V V行, m m m列。 V V V是词表的大小,也就是每一行代表了词表里的一个词; m m m是我们自己定的词向量的维度,比如说对于一个 80000 80000 80000个词的词表,原先我要用 80000 80000 80000维的One-hot向量来表征“电视”这个词,现在我想就用一个 100 100 100维的向量来表征, m m m就是 100 100 100(事实上我们常用的就是 50 50 50或者 100 100 100)。
那么我们用“电视”的One-hot向量
[
0
,
1
,
0
,
0
,
⋯
,
0
]
[0,1,0,0,\cdots,0]
[0,1,0,0,⋯,0]乘以上面说的矩阵
C
C
C会发生什么?会得到一个
m
m
m维的向量啊!这就是我们说的词向量,可以看这个过程:
那我们就会想了,你不就是想从
C
C
C里边取一行么,用的了那么麻烦么,直接给个词在词表中的索引再去
C
C
C里边按索引取出对应行不就完事了吗?你说的对!你看最开始那张模型图中,作者就是这么干的:
这样我们就得到了前
n
−
1
n-1
n−1个词的词向量:
C
(
w
t
−
n
+
1
)
⋯
C
(
w
t
−
1
)
C(w_{t-n+1})\cdots C(w_{t-1})
C(wt−n+1)⋯C(wt−1);这样我们就完成了从词语到向量的映射。开不开心,但。。。是不是感觉哪里不太对?我们说给定一个矩阵
C
C
C,这个
C
C
C怎么来的?事实上,矩阵
C
C
C是我们随机初始化来的(或者根据一些先验数据初始化来的),也就是说,在神经网络语言模型中,词向量作为一个内部参数,跟神经网络中的其他内部参数一样都是先有一个随机初始化值,正向传播后计算损失函数再反向传播更新这些参数。这也就要求神经网络语言模型是有监督的学习,词向量是学习得到的副产物,也是模型内化的一部分。
词向量全连接作为输入
得到上面单个词向量之后,我们要将
n
−
1
n-1
n−1个词向量做一个全连接,即把这
n
−
1
n-1
n−1个词向量首尾相接地拼起来得到最终的输入
x
x
x:
x
=
(
C
(
w
t
−
1
)
,
C
(
w
t
−
2
)
,
⋯
,
C
(
w
t
−
n
+
1
)
)
x=(C(w_{t-1}),C(w_{t-2}),\cdots,C(w_{t-n+1}))
x=(C(wt−1),C(wt−2),⋯,C(wt−n+1))
从输入层到隐藏层
这里的隐藏层就是一个很普通的神经网络的做法,权重
H
H
H乘以输入加上偏置
d
d
d,再加一个
t
a
n
h
tanh
tanh函数作激活函数,就得到了隐藏层:
t
a
n
h
(
d
+
H
x
)
tanh(d+Hx)
tanh(d+Hx)
也就是图中的:
从隐藏层到输出层
我们先计算由隐藏层到输出层未归一化的输出值
y
1
y_1
y1,这里就是一个简单的线性变化:(为了方便理解,这里的描述方式跟原文不太一样,我这里将隐藏层到输出层与输入层到输出层这两部分拆开描述,不影响最后的结果。)
y
1
=
U
t
a
n
h
(
d
+
H
x
)
+
b
1
y_1=Utanh(d+Hx)+b_1
y1=Utanh(d+Hx)+b1
这里的
U
U
U是隐藏层到输出层的参数,
b
1
b_1
b1代表这一部分的偏置项。
在图中表示为:
从输入层到输出层
作者原文中还加入了从输入层到输出层的直连,也是一个线性变换,这作为一个技巧的使用,也可以不用。这一部分的输出值
y
2
y_2
y2可以表示为:
y
2
=
W
x
+
b
2
y_2=Wx+b_2
y2=Wx+b2
W
W
W和
b
2
b_2
b2分别是这一部分的权重和偏置项。整个过程对应图中:
输出层
由上面的两部分输出值我们可以得到最终的
y
y
y:
y
=
y
1
+
y
2
=
b
+
W
x
+
U
t
a
n
h
(
d
+
H
x
)
y=y_1+y_2=b+Wx+Utanh(d+Hx)
y=y1+y2=b+Wx+Utanh(d+Hx)
再将
y
y
y经过一个
s
o
f
t
m
a
x
softmax
softmax函数做概率归一化,便能得到一个维度为
V
V
V的概率向量,这就是我们的输出了。(找到最大的概率所在位置的索引,结合词表我们就能得到我们的预测值了)
模型训练与反向传播
模型训练的目标是最大化以下似然函数:
L
=
1
T
∑
t
f
(
w
t
,
w
t
−
1
,
⋯
,
w
t
−
n
+
1
;
θ
)
+
R
(
θ
)
,
L=\frac{1}{T}\sum_{t}^{}f(w_t,w_{t-1},\cdots,w_{t-n+1};\theta)+R(\theta),
L=T1t∑f(wt,wt−1,⋯,wt−n+1;θ)+R(θ),
其中
θ
=
(
b
,
d
,
W
,
U
,
H
,
C
)
\theta=(b,d,W,U,H,C)
θ=(b,d,W,U,H,C),是模型的所有参数,
R
R
R是正则化项。
反向传播就是根据
l
o
s
s
loss
loss值更新参数的过程,这里不再赘述。
2.2 代码实现
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Date : 2019-02-26 14:15:49
# @Author : cdl (1217096231@qq.com)
# @Link : https://github.com/cdlwhm1217096231/python3_spider
# @Version : $Id$
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
"""
1.Basic Embedding Model
1-1. NNLM(Neural Network Language Model)
"""
dtype = torch.FloatTensor
sentences = ["i like dog", "i love coffee", "i hate milk"]
word_list = " ".join(sentences).split() # 制作词汇表
print(word_list)
word_list = list(set(word_list)) # 去除词汇表中的重复元素
print("去重后的word_list:", word_list)
word_dict = {w: i for i, w in enumerate(word_list)} # 将每个单词对应于相应的索引
number_dict = {i: w for i, w in enumerate(word_list)} # 将每个索引对应于相应的单词
n_class = len(word_dict) # 单词的总数
# NNLM parameters
n_step = 2 # 根据前两个单词预测第3个单词
n_hidden = 2 # 隐藏层神经元的个数
m = 2 # 词向量的维度
# 由于pytorch中输入的数据是以batch小批量进行输入的,下面的函数就是将原始数据以一个batch为基本单位喂给模型
def make_batch(sentences):
input_batch = []
target_batch = []
for sentence in sentences:
word = sentence.split()
input = [word_dict[w] for w in word[:-1]]
target = word_dict[word[-1]]
input_batch.append(input)
target_batch.append(target)
return input_batch, target_batch
# Model
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class, embedding_dim=m)
self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
self.b = nn.Parameter(torch.randn(n_class).type(dtype))
def forward(self, x):
x = self.C(x)
x = x.view(-1, n_step * m)
# x: [batch_size, n_step*n_class]
tanh = torch.tanh(self.d + torch.mm(x, self.H))
# tanh: [batch_size, n_hidden]
output = self.b + torch.mm(x, self.W) + torch.mm(tanh, self.U)
# output: [batch_size, n_class]
return output
model = NNLM()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 制作输入
input_batch, target_batch = make_batch(sentences)
input_batch = Variable(torch.LongTensor(input_batch))
target_batch = Variable(torch.LongTensor(target_batch))
# 开始训练
for epoch in range(5000):
optimizer.zero_grad()
output = model(input_batch)
# output : [batch_size, n_class], target_batch : [batch_size] (LongTensor, not one-hot)
loss = criterion(output, target_batch)
if (epoch + 1) % 1000 == 0:
print("Epoch:{}".format(epoch + 1), "Loss:{:.3f}".format(loss))
loss.backward()
optimizer.step()
# 预测
predict = model(input_batch).data.max(1, keepdim=True)[1] # [batch_size, n_class]
print("predict: \n", predict)
# 测试
print([sentence.split()[:2] for sentence in sentences], "---->",
[number_dict[n.item()] for n in predict.squeeze()])
运行结果:
['i', 'like', 'dog', 'i', 'love', 'coffee', 'i', 'hate', 'milk']
去重后的word_list: ['coffee', 'i', 'hate', 'dog', 'love', 'milk', 'like']
Epoch:1000 Loss:0.114
Epoch:2000 Loss:0.021
Epoch:3000 Loss:0.007
Epoch:4000 Loss:0.003
Epoch:5000 Loss:0.002
predict:
tensor([[3],
[0],
[5]])
[['i', 'like'], ['i', 'love'], ['i', 'hate']] ----> ['dog', 'coffee', 'milk']
[Finished in 4.5s]
3. 总结
神经网络语言模型(NNLM)通过构建神经网络的方式来探索和建模自然语言内在的依赖关系。优缺点如下:
优点:
- 词向量是可以自定义维度的,维度并不会因为新扩展词而发生改变,词向量能够很好的根据特征距离度量词与词之间的相似性;
- 好的词向量能够提高模型泛化能力;
- 相比于n-gram,通过词向量的降维,减小了参数空间,减少了计算量。
缺点:
- 参数较多,模型训练时间长;
- 神经网络黑盒子,可解释性较差。