神经网络语言模型(NNLM)基本原理和实践
本文参照了《深度学习原理与Pytorch实战》和《Python自然语言处理实战核心技术与算法》中的部分代码和原理。
1 文本向量化概述
对于常规的文本,计算机是无法直接处理的,需要我们将文本数据转换成计算机可以进行处理的形式。在NLP领域,文本的向量化是一项十分重要和基础的工作。所谓的文本向量化,就是将文本表示成一系列能够表示文本语义的向量。在一般的文本中,能够被处理的最小单元都是词语。所以,大部分的文本向量化操作都是基于词语进行的,当然也存在句子向量化,后面我们会进行介绍。
词向量,顾名思义,就是以词语为单位进行编码,而词向量技术,就是找到一种最合适的编码方式来对词语进行编码。
2 传统文本编码和NNLM算法
2.1 传统的文本编码算法
1、基于字符编码的编码方式
假设我们分配每一个中文汉字唯一的ID的,其中ID不包含意义信息,只是仅仅用于区分不同的汉字。假设汉字“努”的ID值为51,“力”的ID值为98,那么词汇“努力”的编码就是[51,98]。若“刘”的ID值为64,“德”的ID值为58,“华”的ID值为96,那么词汇“刘德华”的编码方式就是[64,58,96]。显然,通过这种方式进行编码,产生的编码长度随着词汇长度的不同而不同。而且各个编码之间不存在任何的语义联系。
2、排序编码的编码方式
将中文中所有的汉字按照某种方式进行排序,排序的大小便是这个字的id,其余的和基于字符编码没有别。
3、词袋模型(Bag of Word)
词袋模型是最早的以词汇作为基本处理单元的文本向量化算法。其核心思想是:首先将文档中的所有词进行统计。基于统计结果构建出一个词典。对于文本可以构造出一个和词典维度相同的向量。每一个位置代表一个词典中一个词的索引。该位置的值为词典中该词汇出现的次数。
例如:文档1 “我爱你祖国”。文档2: “我爱你母亲和父亲”,则可以形成的词典为{‘我’:2,‘爱’:2,‘你’:2,‘祖国’:1,‘母亲’:1,‘和’:1,‘父亲’:1}
则对于文档1的编码为[2,2,2,1,0,0],文档2的编码为[2,2,2,1,1,1]
2.2 神经网络语言模型编码
2.2. 1、语言模型概述
在自然语言处理领域中,语言模型是整个领域的一个基础的模型。其核心的思想是在文本中,利用已知的几个词汇来对下一个要出现的词汇进行预测。传统的语言模型采用的一般是基于统计的方式来建立模型。利用统计概率来计算出下一个词汇的概率。其核心的公式如下:
f
(
w
1
,
w
2
,
w
3
.
.
.
w
i
−
1
)
=
w
i
f(w_1,w_2,w_3...w_{i-1}) = w_i
f(w1,w2,w3...wi−1)=wi
p
(
w
i
∣
w
1
,
w
2
,
w
3
,
.
.
.
w
i
−
2
,
w
i
−
1
)
≈
p
(
w
i
∣
w
i
−
n
+
1
,
w
i
−
n
+
2
.
.
.
.
,
w
i
−
1
)
p(w_i|w_1,w_2,w_3,...w_{i-2},w_{i-1}) ≈ p(w_i|w_{i-n+1},w_{i-n+2}....,w_{i-1})
p(wi∣w1,w2,w3,...wi−2,wi−1)≈p(wi∣wi−n+1,wi−n+2....,wi−1)
其中f是一个映射函数,通过前i-1个词汇和映射函数f,获取当前的预测词wi
当n=1的时候称为unigram模型,即每一个词的生成是根据其自身在文本出现的概率而决定的,与其他无关。当n=2的时候称为bigram模型,即每一词汇的生成是由其前一个词汇决定。当n=3时,称其为trigram,词汇出现的概率是由前面的两个词决定的。
2.2.2、神经网络语言模型
神经网络语言模型(Neural Network Language Model)
与传统的语言模型不同,NNLM直接通过一个神经元结构对n元条件概率进行估计。其基本的操作为:
1、从语料库中获取一系列长度为n的文本序列。
w
i
−
n
+
1
.
.
.
.
w
i
−
1
w
i
w_{i-n+1}....w_{i-1}w_i
wi−n+1....wi−1wi
2、这些文本组成一个集合D
3、NNLM的目标函数为
∑
D
P
(
w
i
∣
w
i
−
(
n
−
1
)
,
.
.
.
.
w
[
i
−
1
)
)
∑_DP(w_i|w_{i-(n-1),....w_[i-1}))
∑DP(wi∣wi−(n−1),....w[i−1))
其基本的前馈神经网络的结构为:
输入层的输入:将词序列
w
i
−
(
n
−
1
)
,
.
.
.
.
.
.
w
i
−
1
w_{i-(n-1),......w_{i-1}}
wi−(n−1),......wi−1中的每一个词袋模型的词向量按照顺序进行拼接,获的输入向量
x
=
[
v
(
w
i
−
n
(
−
1
)
,
.
.
.
.
.
.
,
v
(
w
i
−
1
)
]
x=[v(w_{i-n(-1)},......,v(w_{i-1})]
x=[v(wi−n(−1),......,v(wi−1)]
隐藏层的计算:隐藏层的计算主要是以下步骤:
h
=
t
a
n
h
(
b
+
H
x
)
h = tanh(b+Hx)
h=tanh(b+Hx),其中H表示输入层到隐藏层的权重矩阵,其维度为|V| * |h|,|V|表示词表的大小,b表示偏置
输出层的计算:输出层的计算主要是以下步骤:
y
=
b
+
U
h
y = b + Uh
y=b+Uh,其中U表示隐藏层到输出层的权重矩阵,b表示偏置,y表示输出的一个|V|的向量,向量中内容是下一个词
w
i
w_i
wi是词表中每一个词的可能性。
softmax函数获取概率:在计算完y之后,需要将y中的数据进行一次softmax的操作来获取各个词汇的概率。
2.2.3 神经网络语言模型产生词向量的基本原理
当我们训练好NNLM之后,输入节点对于隐藏层每一个节点输入的权重就构成了这个节点的词向量,即为这节点对应词的词向量编码。可以这么做的原因在于输入层到隐藏层的权重都是以词袋模型的词汇编码为输入训练出来的。对于每一个词汇而言,这个训练出来的权重是独一无二的。可以很好的代表词汇。同时在计算两个词汇的语义相似性时候,由于这种编码方式的输入是多个词汇的词袋模型的向量拼接。整个输入中,只有要被测算的词汇的词袋模型向量不同。则很容易计算出两个词的语义相似度很大。
2.3、相关函数介绍
2. 3.1 tanh激活函数
tanh函数,即为双曲正切函数。其基本公式如下
t
a
n
h
(
x
)
=
s
i
n
h
(
x
)
/
c
o
s
h
(
x
)
=
(
e
x
−
e
−
x
)
/
(
e
x
+
e
−
x
)
tanh(x)=sinh(x)/cosh(x) = (e^x-e^{-x}) /(e^x+e^{-x})
tanh(x)=sinh(x)/cosh(x)=(ex−e−x)/(ex+e−x)
其基本的函数图像为:
其导数为:
t
a
n
h
′
(
x
)
=
1
−
t
a
n
h
2
(
x
)
tanh'(x) = 1 - tanh^2(x)
tanh′(x)=1−tanh2(x)
2.3.2 softmax函数
softmax函数是一个多分类中求概率的基本函数,其基本公式如下:
P
(
y
i
)
=
e
y
i
/
∑
k
=
1
n
e
x
p
(
y
k
)
P(y_i) = e^{y_i} / ∑_{k=1}^n exp(y_k)
P(yi)=eyi/∑k=1nexp(yk)
其中,n表示结果向量的大小,
y
i
y_i
yi表示结果向量中每一个元素。
2.4 NNLM模型实践
#encoding=utf-8
import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F
import torch.optim as optim
import os
import jieba
#加载训练数据的预处理
class pre_data_process:
def __init__(self,filepath,stopwods):
self.filepath = filepath
self.stopwords = stopwods
self.words = []
#切词,并且形成训练数据,采用trigram的训练方式
def cut_words(self):
trigrams = []
stops = []
with open(self.stopwords,"r",encoding="utf-8") as f:
data = f.readlines()
for item in data:
item = item.strip("\n")
stops.append(item)
for file in os.listdir(self.filepath):
with open(self.filepath+file,'r',encoding="utf-8") as f:
data = f.read()
words = jieba.cut(data)
words_temp = []
for item in words:
if item not in stops:
words_temp.append(item)
self.words.extend(words_temp)
trigram = [([words_temp[i],words_temp[i+1]],words_temp[i+2]) for i in range(len(words_temp)-2)]
trigrams.append(trigram)
#返回训练数据
return trigrams
#建立词典
def createDict(self):
word_to_idx = {}
idx_to_word = {}
vocabs = set(self.words)
idx = 0
for w in self.words:
cnt = word_to_idx.get(w,[idx,0])
if cnt[1] == 0:
idx += 1
cnt[1] += 1
word_to_idx[w] = cnt
idx_to_word[idx] = w
return len(vocabs),word_to_idx,idx_to_word
##
#实现NPLM网络
#NNLM网络一共包含输入层,隐藏层和输出层三层网络
##
class NNLM(nn.Module):
def __init__(self,vocab_size,embedding_size,context_size):
super(NNLM,self).__init__()
self.embedding = nn.Embedding(vocab_size,embedding_size)
self.linear1 = nn.Linear(context_size*embedding_size,128) #隐藏层输出的是128维的向量
self.linear2 = nn.Linear(128, vocab_size) #输出的是词表的大小
def forward(self,inputs):
embeddings = self.embedding(inputs)
embeddings = embeddings.view(1,-1)
out = self.linear1(embeddings)
out = F.relu(out)
out = self.linear2(out)
log_probs = F.log_softmax(out)
return log_probs
def extract(self,inputs):
embeddings = self.embedding(inputs)
return embeddings
#下面是训练的过程
pre_data = pre_data_process('./txt/', './stopwords.txt')
trigrams = pre_data.cut_words()
length,word_to_idx,idx_to_word = pre_data.createDict()
loss = []
criterion = nn.NLLLoss()
model = NNLM(length, 128, 2)
optimizer = optim.SGD(model.parameters(),lr=0.001)
for epoch in range(20):
total_loss = torch.Tensor([0])
for trigram in trigrams:
for context,target in trigram:
context_idx = [word_to_idx[w][0] for w in context]
context_var = Variable(torch.LongTensor(context_idx))
optimizer.zero_grad()
log_prob = model(context_var)
loss = criterion(log_prob,Variable(torch.LongTensor([word_to_idx[target][0]])))
loss.backward()
optimizer.step()
total_loss += loss.data
print('第{}轮,损失函数:{:.2f}'.format(epoch,total_loss.numpy()[0]))
2.4.1 nn.embedding说明
之前,我们提出层输入的是词袋模型产生的向量的拼接,而nn.embedding则负责产生词袋模型的向量。