NLP: nlp-tutorial系列文章
nlp-tutorial NLP学习,文章解读,代码学习。Basic Embedding Model - NNLM
前言
nlp-tutorial使用pytorch / tensorflow复现经典NLP领域经典文章。本文章针对Bengio的NNLM2003文章进行学习。
1. NNLM (Neural Network Language Model)
NNLM全称A Neural Probabilistic Language Model,NLP word embedding具有里程碑意义的工作。该工作是Bengio团队的工作,Bengio也是我最敬仰的AI与计算领域的大师之一,他具有的非凡气质与专注足以使他在任何领域获得成功。
这篇文章主要面对的是curse of dimensionality,以及用条件概率描述词汇转移下的句子,这样的语言模型在训练、测试以及验证时,词的顺序差异巨大。当时比较流行的处理方法存在2种劣势,其一时n-gram(尤其是trigram)考虑的词太少,其二是词汇之间的similarity的考量缺失。据此,Bengio等提出学习一个词的分布式表征(分布)。
个人认为,考虑(1)长文本的关联性 和 (2)词汇之间的相似程度 在NLP字符与文本级别的人任务中,是一直贯彻至今的重要思路。足以见得Bengio团队的贡献。
2. 关键理论与方法
2.1 语言的统计模型
句子由单词与单词之间的顺序关系构成。比如I like cat即包含单词信息,其顺序信息也包含语义。那么单词顺序构成一个序列,序列由前到后存在条件概率(转移概率),最基本的语言模型就是用条件概率刻画:
P
^
(
w
1
T
)
=
∏
t
=
1
T
P
^
(
w
t
∣
w
1
t
−
1
)
\hat{P}(w_{1}^{T}) = \prod_{t=1}^{T}\hat{P}(w_{t} |w_{1}^{t-1})
P^(w1T)=t=1∏TP^(wt∣w1t−1)
代表着一个长度为T的句子的概率模型,由每个词的条件概率联合构成。该模型考虑的词太多,句子长度一旦增长一些,模型就会非常的稀疏,因而带来curse of dimentionality也就是维度灾难。
当时的一个主流方法(今天有很多地方依旧在用)的是n-gram,也就是考虑当前词在内的前n个词,是一种语言模型重要的近似方式:
P
^
(
w
t
∣
w
1
t
−
1
)
≈
P
^
(
w
t
∣
w
t
−
n
+
1
t
−
1
)
\hat{P}(w_{t} | w_{1}^{t-1}) \approx \hat{P}(w_{t} | w_{t-n+1}^{t-1})
P^(wt∣w1t−1)≈P^(wt∣wt−n+1t−1)
考虑维度灾难问题,用的比较多的特征就是trigram(n-gram |n=3)。
2.2 不足与改进思路
条件概率描绘的语言模型主要两方面不足:
- n-gram尤其是trigram考虑的词不够多;
- 词汇之间的相似性没有考虑;
文中给出的例子如下:
- The cat is walking in the bedroom;
- A dog was running in a room;
- The cat was running in a room;
- The dog was walking in the room;
语料库中的句子的相似结构应该能给出word之间的联系。
文章整体提供的改进思路:
其中1就是embedding,可能那会还没直接称呼为embedding。向量空间的维度(m)远小于词典的大小。
2.3 Neural Model
2.3.1 Mission
学习词的表征,同时学习语言模型的概率函数,用以做单词预测。即,给出上文,预测下文单词。
2.3.2 Training Set
word sequence w 1 , … , w T w_{1}, \dots, w_{T} w1,…,wT,其中 w t ∈ V w_{t} \in V wt∈V词属于有限大小的词典,词典提供了word的封闭性。
2.3.3 Objective Model
获得模型
f
(
w
t
,
…
,
w
t
−
n
+
1
)
=
P
^
(
w
t
∣
w
1
t
−
1
)
f(w_{t}, \dots, w_{t-n+1}) = \hat{P}(w_{t}|w_{1}^{t-1})
f(wt,…,wt−n+1)=P^(wt∣w1t−1)
用几何均值
1
P
^
(
w
t
∣
w
t
−
1
)
\frac{1}{\hat{P}(w_{t}|w_{t-1})}
P^(wt∣wt−1)1,i.e. perplexity,形式为softmax,来做预测。
文章将该模型函数分解成两个部分:
- C mapping:a ∣ V ∣ ∗ m |V| * m ∣V∣∗m矩阵,将单词映射到向量,参数C;
- 下一个词 w t w_{t} wt的条件概率用C表达: f ( i , w t − 1 , … , w t − n + 1 ) = g ( i , C ( w t − 1 ) , … , C ( w t − n + 1 ) ) f(i, w_{t-1},\dots,w_{t-n+1}) = g(i, C(w_{t-1}), \dots, C(w_{t-n+1})) f(i,wt−1,…,wt−n+1)=g(i,C(wt−1),…,C(wt−n+1)),函数g输出一个向量,第i-th元素表示单词下一个单词是字典中i-th元素的可能性。g的形式可以是MLP或RNN等,参数记为 w w w。
如下图所示:
2.3.4 Objective Function and Parameter
参数
θ
=
(
C
,
w
)
\theta = (C, w)
θ=(C,w),对应上文模型分解的两个部分。
具体的,由于输出层采用softmax处理,softmax的输入记为y,y为
y
=
b
+
W
x
+
U
t
a
n
h
(
d
+
H
x
)
y = b+Wx+Utanh(d+Hx)
y=b+Wx+Utanh(d+Hx),x为
x
=
(
C
(
w
t
−
1
)
,
…
,
C
(
w
t
−
n
+
1
)
)
x=(C(w_{t-1}), \dots, C(w_{t-n+1}))
x=(C(wt−1),…,C(wt−n+1)),则单层MLP模型下,
θ
=
(
b
,
d
,
W
,
U
,
H
,
C
)
\theta = (b,d,W, U, H, C)
θ=(b,d,W,U,H,C)。
优化的目标函数:
L
=
1
T
∑
t
l
o
g
f
(
w
t
,
…
,
w
t
−
n
+
1
;
θ
)
+
R
(
θ
)
L = \frac{1}{T} \sum_{t} logf(w_{t}, \dots, w_{t-n+1}; \theta) + R(\theta)
L=T1t∑logf(wt,…,wt−n+1;θ)+R(θ)
R
(
θ
)
R(\theta)
R(θ)为正则项。
参数更新采用SGD:
θ
←
θ
+
ε
∂
l
o
g
(
P
^
(
w
t
∣
w
t
−
1
,
…
,
w
t
−
n
+
1
)
∂
θ
\theta \leftarrow \theta + \varepsilon \frac{\partial log(\hat{P}(w_{t} | w_{t-1}, \dots, w_{t-n+1})}{\partial \theta}
θ←θ+ε∂θ∂log(P^(wt∣wt−1,…,wt−n+1)
其中
ε
\varepsilon
ε是学习率。
2.3.5 Mixture of Models
采用0.5权重,将神经网络的预测结果与trigram概率模型的预测结果进行加权,得到最终结果的性能会更佳。此之谓混合模型。
3 模型实现
代码参考。
采用pytorch实现一个简单的例子,来学习本文章的算法。
主要依赖以下库:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
这里构建一个语料库:
sentences = ["i like dog", "i hate cat",
"she like cat", "she hate dog",
"cat is running", "cat was running",
"dog is running", "dog was running",
"i was running",
"she was running",
]
最后希望不仅预测结果,也检验一下i she dog cat之间的相似性度量。
根据语料构建词典:
# build the diction of word
word_list = " ".join(sentences).split()
word_list = list(set(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)
模型的超参数设置如下:
# model hyperparameter
n_step = 2 # 前n-1个词
n_hidden = 2 # mlp隐藏神经元
m = 3 # embedding维度
构建batch作为输入与输出验证:
def make_batch(sentences):
input_batch = []
target_batch = []
for sen in sentences:
word = sen.strip().split()
i = [word_dict[w] for w in word[:-1]]
t = word_dict[word[-1]]
input_batch.append(i)
target_batch.append(t)
return input_batch, target_batch
模型:
# Model
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class, 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)
tanh = torch.tanh(torch.mm(x, self.H) + self.d)
output = self.b + torch.mm(x, self.W) + torch.mm(tanh, self.U)
return output
训练过程如下:
dtype = torch.float
model = NNLM()
loss = nn.CrossEntropyLoss()
opt = optim.Adam(model.parameters(),lr=0.001)
input_batch, target_batch = make_batch(sentences)
input_batch, target_batch = torch.tensor(input_batch), torch.tensor(target_batch)
for epoch in range(5000):
opt.zero_grad()
output_batch = model(input_batch)
l = loss(output_batch, target_batch)
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(l))
l.backward()
opt.step()
'''
Epoch: 1000 cost = 0.379675
Epoch: 2000 cost = 0.090831
Epoch: 3000 cost = 0.022923
Epoch: 4000 cost = 0.009555
Epoch: 5000 cost = 0.004810
'''
打印embedding层的参数以及word_dict的对应瓜系:
# embedding 参数
model.C.state_dict()
'''
OrderedDict([('weight',
tensor([[-0.1183, 1.2940, -0.6724],
[-0.4286, 0.2746, -0.1696],
[-0.9515, 0.3163, 0.8657],
[-2.1839, -1.4932, -1.1263],
[-1.6372, 1.5592, 1.7642],
[ 1.6881, 1.4236, -2.3015],
[ 1.8511, 0.2312, -2.4064],
[-1.0320, -0.5917, -2.0542],
[ 1.0510, 2.0733, 1.7233]]))])
'''
# cos余弦距离
def dist(x, y):
return np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))
# 取i和she计算举例
dist(i, she)
'''
-0.1970258
'''