神经网络语言模型

语言模型

    语言模型是自然语言处理的一大利器,是NLP领域一个基本却又重要的任务。它的主要功能就是计算一个词语序列构成一个句子的概率,或者说计算一个词语序列的联合概率,这可以用来判断一句话出现的概率高不高,符不符合我们的表达习惯,它是否通顺,这句话是不是正确的。

    语言模型可以用于机器翻译、语音识别、音字转换和文本校对等诸多NLP任务中,它可以从多个候选句子中选出一个最为靠谱的结果。

    前面有一篇博文提到了统计语言模型《NLP-语言模型》,一个语言模型通常构建为字符串s的概率分布p(s),这里的p(s)实际上反映的是s作为一个句子出现的概率。这里的概率指的是组成字符串的这个组合,在训练语料中出现的似然,与句子是否合乎语法无关。假设训练语料来自于人类的语言,那么可以认为这个概率是的是一句话是否是人话的概率。

    对于一个由T个词按顺序构成的句子01.gif,p(s)实际上求解的是字符串02.gif的联合概率,利用贝叶斯公式,链式分解如下:

03.gif

2.jpg

    从上面可以看到,一个统计语言模型可以表示成,给定前面的的词,求后面一个词出现的条件概率。我们在求p(s)时实际上就已经建立了一个模型,这里的p(*)就是模型的参数,如果这些参数已经求解得到,那么很容易就能够得到字符串s的概率。


N-gram语言模型

    上面直接计算的方法参数过多导致维度灾难。为了解决自由参数数目过多的问题,引入了马尔科夫假设:随意一个词出现的概率只与它前面出现的有限的n个词有关。基于上述假设的统计语言模型被称为N-gram语言模型。公式为:

1.jpg

通常情况下,n的取值不能够太大,否则自由参数过多的问题依旧存在:

(1)当n=1时,即一个词的出现与它周围的词是独立,这种我们称为unigram,也就是一元语言模型,此时自由参数量级是词典大小V。

(2)当n=2时,即一个词的出现仅与它前面的一个词有关时,这种我们称为bigram,叫二元语言模型,也叫一阶马尔科夫链,此时自由参数数量级是V^2。

(3)当n=3时,即一个词的出现仅与它前面的两个词有关,称为trigram,叫三元语言模型,也叫二阶马尔科夫链,此时自由参数数量级是V^3。

    一般情况下只使用上述取值,因为从上面可以看出,自由参数的数量级是n取值的指数倍。从模型的效果来看,理论上n的取值越大,效果越好。但随着n取值的增加,效果提升的幅度是在下降的。同时还涉及到一个可靠性和可区别性的问题,参数越多,可区别性越好,但同时单个参数的实例变少从而降低了可靠性。

    N-gram虽然解决了参数过多的问题,但是还是存在数据稀疏的问题。假设有一个词组在训练语料中没有出现过,那么它的频次就为0。频次为零会导致该词组的概率为0,从而使整句话的概率为0,这显然是不合理的。因此N-gram需要进行数据平滑。


神经概率语言模型

    上面提到的N-gram语言模型也有仍有不足之处,它没有考虑超过一两个的上下文,没考虑到单词之间的相似性,而且需要精心设计数据平滑方法。2003年,深度学习三巨头之一的Yoshua Bengio在论文《A Neural Probabilistic Language Model》中出了神经概率语言模型。主要思想是:

(1)将词表中每个词和一个分布式的词特征向量联系起来(Rm中的一个实数值的向量)。

(2)根据序列中这些单词的特征向量来表达单词序列的联合概率函数,

(3)同时学习词特征向量和该概率函数的参数。

    特征向量代表单词的各个方面,每个单词都是向量空间中的一个点。相似的单词将有相似的特征向量由此模型得到泛化,概率函数是特征值的平滑函数,因此特征小的改变将不会造成概率大的改变。

    神经概率语言模型其实应用了马尔可夫假设,即一个词出现的概率和前面的t个单词有关。该模型的公式如下:

3.png

上式可以分解为两部分:

    1. C是单词到分布式特征向量的映射

    2. 联合概率:

4.png

对应的神经网络模型为:

index.png

从上图可以看到,这个模型包含四个层:输入层、投影层、隐藏层和输出层。

输入层:这里就是词w的上下文,如果用N-gram的方法就是词w的前n-1个词。每一个词都作为一个长度为V的one-hot向量传入神经网络中

投影层:在投影层中,存在一个查找表C,C被表示成一个V*m的自由参数矩阵,其中V是词典的大小,而m是词向量的长度。C中每一行都作为一个词向量存在,这个词向量是每一个词的另一种分布式表示,每一个one-hot向量都经过表C的转化变成一个词向量。

    n-1个词向量首尾相接的拼起来,转化为(n-1)m的列向量输入到下一层。

隐藏层:激活函数为tanh的非线性全连接层。

输出层:softmax层。

除了上面几个层之外,还有词向量直接连接到输出层的通路,但这并不是必要的,甚至去掉该通路结果更好。

    得总的计算公式如下:

y=b+Wx+Utanh(d+Hx)

    其中x是经过投影后的词向量,H是隐藏层参数,d是隐藏层偏置;U是输出层参数,b是输出层偏置。W是词向量直接连到输出通路的参数,但是矩阵并不是必要的。

    论文中使用的参数优化方法是BP+SGD,W和H有权重惩罚因子而C没有。

    训练上面的网络,则会得到一个基于神经网络的语言模型,即整个网络就是一个语言模型,基于n个已知单词可以预测序列的下一个单词。

    训练这个网络除了得到一个语言模型之外,还得到一个表示各个单词的向量,即投影层参数矩阵C。C矩阵的每一行都是一个单词的词向量。有关词向量的更详细内容,可以参考下一篇博文《word2vec原理和实现》。


代码实现

    使用中文维基百科作为数据集,网址为:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2

  1.  使用gensim库处理解压,并将数据写入txt

from gensim.corpora import WikiCorpus

input_file_name = r'D:\NLP\dataset\zhwiki-latest-pages-articles.xml.bz2'
output_file_name =r'D:\NLP\dataset\wiki.cn.txt'

input_file = WikiCorpus(input_file_name, lemmatize=False, dictionary={})

output_file = open(output_file_name, 'w', encoding="utf-8")

count = 0
#get_texts()将原文件中的文章转换为一个数组,使用for循环保存为txt文件
for text in input_file.get_texts():
    output_file.write(' '.join(text) + '\n')
    count = count + 1
    if(count==10000):
        break

output_file.close()


2.维基百科多数是繁体内容,需要zhconv转换为简体内容

import zhconv

input_file_name = r'D:\NLP\dataset\wiki.cn.txt'
output_file_name = r'D:\NLP\dataset\wiki.cn.simple.txt'
input_file = open(input_file_name, 'r', encoding='utf-8')
output_file = open(output_file_name, 'w', encoding='utf-8')

lines = input_file.readlines()
count = 1
for line in lines:
    output_file.write(zhconv.convert(line, 'zh-hans'))
    count += 1
output_file.close()


3.将原始语料分词

import jieba

input_file_name = r'D:\NLP\dataset\wiki.cn.simple.txt'
output_file_name = r'D:\NLP\dataset\wiki.cn.simple.separate.txt'
input_file = open(input_file_name, 'r', encoding='utf-8')
output_file = open(output_file_name, 'w', encoding='utf-8')

lines = input_file.readlines()

count = 1
for line in lines:
    output_file.write(' '.join(jieba.cut(line.split('\n')[0].replace(' ', ''))) + '\n')
    count += 1

output_file.close()


4.语料中有很多英语单词和奇怪的符号,需要去除掉。通过正则表达式判断每一个词是不是符合汉字开头、汉字结尾、中间全是汉字,即“^[\u4e00-\u9fa5]+$”。

import re

input_file_name = r'D:\NLP\dataset\wiki.cn.simple.separate.txt'
output_file_name = r'D:\NLP\dataset\wiki.txt'

input_file = open(input_file_name, 'r', encoding='utf-8')
output_file = open(output_file_name, 'w', encoding='utf-8')

lines = input_file.readlines()

count = 1
cn_reg = '^[\u4e00-\u9fa5]+$'
for line in lines:
    line_list = line.split('\n')[0].split(' ')
    line_list_new = []
    for word in line_list:
        if re.search(cn_reg, word):
            line_list_new.append(word)
    #print(line_list_new)
    output_file.write(' '.join(line_list_new) + '\n')
    count += 1
    if(count%1000==0):
        print(count)
output_file.close()


5.将字符串映射到数字序列

import tqdm
datafile=r'D:\NLP\dataset\wiki.txt'
with open(datafile,'r',encoding='utf-8') as f:
    data=[]
    raw_data=f.readlines()
    for line in tqdm.tqdm(raw_data):
        data.append(line.split())
print(len(data))
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
import jieba

MAX_WORDS=10000#出于训练速度的考虑,这里只使用了前10000个单词

tokenizer=Tokenizer(num_words=MAX_WORDS-1)
tokenizer.fit_on_texts(texts=data)
word_index=tokenizer.word_index
re_word_index = dict([(i, t) for t, i in word_index.items()])
print(len(word_index))
seqList=tokenizer.texts_to_sequences(texts=data)


6.定义网络

from keras.models import Sequential
from keras.layers import *

model=Sequential()
model.add(Embedding(MAX_WORDS, 100, input_length=6))#相当于6元模型
model.add(Flatten())
model.add(Dense(512,activation='tanh')) #隐层参数为512
model.add(Dense(MAX_WORDS, activation='softmax'))
model.compile(optimizer='sgd',
    loss='categorical_crossentropy',
    metrics=['accuracy'])
model.summary()

5.jpg

7.训练模型

定义一个数据生成器,滑动窗口大小为wlen 

def data_generator(wlen,batch_size=64):
    while True:
        x,y=[],[]
        for sen in seqList:
            for i in range(len(sen)-wlen-1):
                x.append(sen[i:i+wlen])
                y.append(sen[i+wlen])
                if(len(x)==batch_size):
                    x= np.array(x)
                    yn=np.zeros((batch_size,MAX_WORDS))
                    for i,index in enumerate(y):
                        yn[i,index]=1
                    yield x,yn
                    x,y=[],[]
                    
model.fit_generator(data_generator(6,batch_size=256),
                    steps_per_epoch =100000,
                    verbose=1,
                    epochs=2)


8.模型测试

    这里取出向量空间的中靠近的词向量作为测试结果。

#获取embeding的权重,也就是词向量
embeddings = model.get_weights()[0]
#向量标准化
normalized_embeddings = embeddings / (embeddings**2).sum(axis=1).reshape((-1,1))**0.5

def most_similar(w):
    v = normalized_embeddings[word_index[w]]
    #向量标准化之后分母就是1,所以直接相乘就好
    sims = np.dot(normalized_embeddings, v)
    sort = sims.argsort()[::-1]
    sort = sort[sort > 0]
    #如果是占位符则不输出
    return [(re_word_index[i],sims[i]) for i in sort[:10] if i in re_word_index]

for sim in most_similar(u'美丽'):
    print(sim[0],sim[1])

测试结果:

test.jpg

    虽然训练的数据集小而且只训练两轮,但是还是看得出效果的。



参考文献

[1]凌霄文强.基于word2vec使用中文wiki语料库训练词向量.https://www.jianshu.com/p/e21dd72e391e.2019-01-19

[2]就是杨宗.神经概率语言模型.https://www.jianshu.com/p/3f22fc76a9e5.2017-09-26

[3]Soyoger.自然语言处理之语言模型(LM).https://blog.csdn.net/qq_36330643/article/details/80143960.  2018-4-29

首页 所有文章 机器人 计算机视觉 自然语言处理 机器学习 编程随笔 关于