本文知识点介绍来自斯坦福大学CS224N课程lecture6语言模型部分
语言模型
语言模型可以预测一个序列接下来会出现什么词。即给定一个单词序列,语言模型计算出下一个单词是词汇表中各个词的概率分布。
还有一种理解是语言模型可以计算一个句子出现的概率,计算公式如下(条件概率):
n-gram语言模型:
含义:通过前(n-1)个词去预测某个单词。
n-gram有一个基本的假设:假设某个词的出现仅取决于它前面的n-1个单词。根据条件概率公式,某个词的出现概率计算公式为:
计算上图n-gram和(n-1)-gram的比例是通过计算他们在大型语料库中出现的次数的比例:
举个例子:As the proctor started the clock, the students open their ___. 预测这句话空格处的词,假设这里是一个4-gram的语言模型。那么就是通过前三个词:students open their去预测下一个词,语料库中students open their books出现的次数最多,故4-gram模型预测的空格处的词就应该是books。
然而回顾整个句子,前文出现了proctor,所以这个空是exams的概率应该最大,而4-gram模型只考虑前三个词,忽略了前面的词proctor,故导致预测错误。n-gram语言模型的一个缺点:每次预测时只考虑前n-1个词,忽略了再之前的上下文。
sparsity problem:
n-gram模型对于下图的计算式会出现两个问题:
1、分子为0,即语料库中没有相应的n元组,一个解决方法是在一开始给每一个单词增加很小的次数,这个方法叫smoothing;
2、分母为0,即语料库中没有相应的n-1元组,一个解决方法是退化成n-1-gram,即考虑所预测单词前n-2个单词出现的次数。
更糟的是,对于上一个问题,即每次只考虑前n-1个词而忽略了更之前的上下文,可能有个解决方法是增大N。而增大N会导致更严重的sparsity problem。通常N不超过5。
storage problem:
增加N会增加模型大小。
语言模型可以用作文本生成,以下是一个3-gram的语言模型生成的一段文本。但显然,这段文本非常的不连贯且奇怪,因为它的每个词仅仅是依据前两个词生成的,增大N可以改善这个问题,而增大N又会带来很多副作用:sparsity、storage problem。
利用n-gram训练词向量的代码详细注释:
(源代码为github上nlp-tutorial项目,项目地址:nlp-tutorial)
import numpy as np #引入numpy库
import torch #引入torch
import torch.nn as nn #torch.nn是torch的神经网络库
import torch.optim as optim #torch.optim是优化库,包含很多优化函数
from torch.autograd import Variable #现在的pytorch版本variable已经回归tensor了,直接用tensor即可
dtype = torch.FloatTensor
sentences = [ "i like dog", "i love coffee", "i hate milk"] #训练集
word_list = " ".join(sentences).split() #先用" ".join(),以空格为分隔,将sentences中的句子连接起来,再用split()以空格为分割点,将每个词分出来
word_list = list(set(word_list)) #先用set合并重复的单词,再用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) # number of Vocabulary
# NNLM Parameter
n_step = 2 # n-1 in paper #步长,即根据多少个词来预测下一个词,这里是根据前两个词预测第三个词
n_hidden = 2 # h in paper #隐藏层神经元个数
m = 2 # m in paper #词向量维度
def make_batch(sentences): #作用是将训练集中的句子的最后一个词和前面的词分开
input_batch = [] #空列表,用来存放输入
target_batch = [] #存放一次输入对应的输出
for sen in sentences: #对于训练集中的每个句子
word = sen.split() #先将句子中每个单词按空格分开
input = [word_dict[n] for n in word[:-1]] #[:-1]切片,即输入是一直到最后一个词,最后一个词不要
target = word_dict[word[-1]] #target是最后一个词
input_batch.append(input) #将分离出来的input添加到输入列表
target_batch.append(target) #将分离出来的target添加到标记列表
return input_batch, target_batch #返回输入和标记列表
# Model
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
#文章中计算式为y = b + Wx + U * tanh(d + Hx),这里设置各个参数
self.C = nn.Embedding(n_class, m) #C是词向量矩阵,行数是单词的数量,列数是词向量的维度
#H是输入层到隐层的权重矩阵,维数是(输入的个数 * 隐层单元个数),输入的个数是(输入的单词个数 * 词向量维数),即n_step * m
self.H = nn.Parameter(torch.randn(n_step * m, n_hidden).type(dtype))
#W是一个直连输入层和输出层的权重矩阵,维数是(输入的个数 * 输出层单元个数)
self.W = nn.Parameter(torch.randn(n_step * m, n_class).type(dtype))
#d是输入层到隐层的偏置常数,维数是(n_hidden * 1)
self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
#U是隐层到输出层的权重矩阵,维数是(隐层单元个数 * 输出层单元个数),要输出词典中每一个单词是下一个词的概率,输出的个数应该是单词的个数
self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
#b是隐层到输出层的偏置常数,维数是(输出层单元个数 * 1)
self.b = nn.Parameter(torch.randn(n_class).type(dtype))
def forward(self, X): #前向传播,计算公式y = b + Wx + U * tanh(d + Hx)
X = self.C(X) #输入的X是单词的序号,序号X对应的词向量是词向量矩阵C的第X行,取出此词向量
X = X.view(-1, n_step * m) #reshape矩阵X
tanh = torch.tanh(self.d + torch.mm(X, self.H)) #torch.mm是矩阵相乘
output = self.b + torch.mm(X, self.W) + torch.mm(tanh, self.U) #根据上述公式计算输出
return output
model = NNLM()
criterion = nn.CrossEntropyLoss() #损失函数为交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001) #使用Adam算法进行优化
input_batch, target_batch = make_batch(sentences) #使用make_batch从训练集中获得输入和对应的标记
input_batch = Variable(torch.LongTensor(input_batch)) #这两行是将输入变成variable,但现在的torch版本直接用tensor即可
target_batch = Variable(torch.LongTensor(target_batch))
# Training
for epoch in range(5000): #训练5000次
optimizer.zero_grad() #每次训练前清除梯度缓存
output = model(input_batch) #对模型输入input_batch,获得输出output
# output : [batch_size, n_class], target_batch : [batch_size] (LongTensor, not one-hot)
loss = criterion(output, target_batch) #使用损失函数计算loss,损失函数的输入为模型的输出output和标记target
if (epoch + 1)%1000 == 0: #每优化1000次打印一次查看cost的变化
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward() #反向传播、自动求导
optimizer.step() #优化、更新参数
# Predict
predict = model(input_batch).data.max(1, keepdim=True)[1] #用.max[1]挑出输出中最大的那一个
# Test
print([sen.split()[:2] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])
来源:CSDN
作者:yqy2001
链接:https://blog.csdn.net/yqy2001/article/details/104642101