非线性神经网络语言模型可以解决一些传统语言模型中的问题:增加上下文的同时参数仅呈线性增长,缓解了人工设计的需要,支持不同上下文泛化性能。
- 输入:k元语句(代码实现熵是 k 个词向量的拼接)
- 输出:下个词的概率分布P
我们的目标是希望神经网络发现如下的规律,于是有了网络结构图:
P(C(wi)∣C(wi−4),C(wi−3),C(wi−2),C(wi−1))(2.2.1)
P(C(w_i)|C(w_{i-4}),C(w_{i-3}),C(w_{i-2}),C(w_{i-1}))\tag{2.2.1}
P(C(wi)∣C(wi−4),C(wi−3),C(wi−2),C(wi−1))(2.2.1)
2.2.1 one-hot 表示
先用最简单的方式表示每个词,one-hot 表示为:
- dog=(0,0,0,0,1,0,0,...)dog = (0, 0, 0, 0, 1, 0, 0, ...)dog=(0,0,0,0,1,0,0,...)
- cat=(0,0,0,0,0,0,0,0,1,0,...)cat = (0, 0, 0, 0 ,0 ,0, 0, 0 ,1, 0, ...)cat=(0,0,0,0,0,0,0,0,1,0,...)
- eat=(0,1,0,0,...)eat = (0, 1, 0, 0, ...)eat=(0,1,0,0,...)
可是 one-hot 表示法有诸多缺陷,还是稠密的向量表示更好一些,那么如何转换呢?只需要加一个隐藏层映射一下就好了。映射之后的向量层如果单独拿出来看,还有办法找到原来的词是什么吗?
one-hot 表示法这时候就作为一个索引字典了,可以通过映射矩阵对应到具体的词向量。
这样,这个神经网络的每个环节都没问题了,可以开始训练了。
独热编码:让计算机认识单词
词典 VVV:新华词典里面把所有的词集合成一个集合 VVV。
假设词典里面有 8 个单词,计算机不认识单词,但是我们要计算机认识单词。
独热编码:给出一个 8×88 \times 88×8 的矩阵。
- “time”:10000000
- “fruit”:01000000
- ……
- “banana”:00000001
余弦相似度 去计算两者的相似度,发现独热编码 计算得到的余弦相似度都为0,即这些单词都没有关联度(独热编码缺陷),于是就有了词向量的概念。
数学公式总结:
输入向量 xxx,输出结果 yyy 的概率分布,两者的表达式如下所示,其中 www 代表词向量,vvv 代表词到词嵌入的映射,LMLMLM (Lamguage Model)是一个多层感知机:
100LM(w1:k)=softmax(hW2+b2)x=[v(w1);v(w2);...;v(wk)]h=g(xW1+b1)(2.2.2)
\begin{align}{100}
LM(w_{1:k})&=softmax(hW^2+b^2)\\
&x = [v(w_1);v(w_2);...;v(w_k)]\\
&h=g(xW^1+b^1)
\end{align}\tag{2.2.2}
100LM(w1:k)=softmax(hW2+b2)x=[v(w1);v(w2);...;v(wk)]h=g(xW1+b1)(2.2.2)
- vvv 就是映射矩阵
- www 是输入的词向量
- xxx 是将前 k 个词通过映射矩阵处理后的一个拼接
- hhh 是隐藏层
- W1W^1W1 和 W2W^2W2 表示两个矩阵
2.3.2 神经网络语言模型(NNLM)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c3aGJCNx-1687508673257)(https://bucket-zly.oss-cn-beijing.aliyuncs.com/img/Typora/202306181936991.png)]
假设有4个单词:w1,w2,w3,w4w_1,w_2,w_3,w_4w1,w2,w3,w4(4个单词的独热编码)。
- w1×Q=c1, w2×Q=c2, w3×Q=c3, w4×Q=c4w_1 \times Q = c_1,\space \space w_2 \times Q = c_2,\space\space w_3 \times Q = c_3,\space\space w_4 \times Q = c_4w1×Q=c1, w2×Q=c2, w3×Q=c3, w4×Q=c4。
- QQQ 就是一个随机矩阵(可学习),是一个参数
- C=[c1,c2,c3,c4]C = [c_1,c_2,c_3,c_4]C=[c1,c2,c3,c4]。
- softmax(U[tanh(WC+b1)]+b2)==[0.1, 0.2, 0.5, 0.2]∈[1,VL]softmax(U[tanh(WC+b_1)]+b_2) == [ 0.1,\space0.2,\space0.5,\space0.2 ]\in[1,V_L]softmax(U[tanh(WC+b1)]+b2)==[0.1, 0.2, 0.5, 0.2]∈[1,VL]。最后生成一个一维矩阵,其中矩阵的长度为词典的长度 VLV_LVL
2.3.3 词向量(神经网络语言模型的副产品 QQQ)
给我任何一个词(“判断”),会给出相应的独热编码 [0,0,1,0,...,0][0,0,1,0,...,0][0,0,1,0,...,0]。
- W1×Q=c1W_1 \times Q = c_1W1×Q=c1,c1c_1c1 就是 “判断” 这个词的词向量。
词向量:就是用一个向量来表示一个单词。独热编码也属于词向量,只是独热编码存储空间太大,且两者的余弦相似度都为0。
经过 QQQ 处理,可以控制词向量的维度(大小),也解决了相似度的问题。
通过神经网络语言模型,找到一个合适的 QQQ 矩阵,得到一个合适的词向量,这个词向量能够更加准确的表示这个词
2.3.4 案例实现
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from tqdm import tqdm
from torch.autograd import Variable
dtype = torch.FloatTensor
sentences = ["i like dog", "i love coffee", "i hate milk", "i do nlp"]
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)}
# print(word_dict)
n_class = len(word_dict)
m = 2
n_step = 2
n_hidden = 2
def make_batch(sentence):
input_batch = []
target_batch = []
for sen in sentence:
word = sen.split()
input = [word_dict[n] for n in word[:-1]]
target = word_dict[word[-1]]
input_batch.append(input)
target_batch.append(target)
return input_batch, target_batch
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.embed = nn.Embedding(n_class, m)
self.W = nn.Parameter(torch.randn(n_step * m, n_hidden).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.embed(x) # 4 x 2 x 2
x = x.view(-1, n_step * m)
tanh = torch.tanh(self.d + torch.mm(x, self.W)) # 4 x 2
output = self.b + torch.mm(tanh, self.U)
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) # input: 4 x 2
loss = criterion(output, target_batch)
if (epoch + 1) % 1000 == 0:
print('epoch:', '%04d' % (epoch + 1), 'cost = {:.6f}'.format(loss.item()))
loss.backward()
optimizer.step()
predict = model(input_batch).data.max(1, keepdim=True)[1]
print([sen.split()[:2] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])