语言模型

什么是语言模型

语言模型在文本处理、信息检索、机器翻译、语音识别中承担这重要的任务。从通俗角度来说,语言模型就是通过给定的一个词语序列,预测下一个最可能的词语是什么。传统语言模型有N-gram模型、HMM(隐马尔可夫模型)等,进入深度学习时代后,著名的语言模型有神经网络语言模型(Neural Network Language Model,NNLM),循环神经网络(Recurrent Neural Networks,RNN)等。

  • 语言模型从概率论专业角度来描述就是:为长度为m的字符串确定其概率分布\(P(w_1, w_2, ..., w_n)\),其中\(w_1\)到\(w_n\)依次表示文本中的各个词语。一般采用链式法则计算其概率值:

$$P(w_1, w_2, ..., w_n) = P(w_1)P(w_2|w_1)P(w_3|w_1,w_2)...P(w_m|w_1,w_2,...,w_{m-1})$$

  • 观察上式,可发现,当文本长度过长时计算量过大,所以有人提出N元模型(N-gram)降低计算复杂度。

N-gram模型

所谓N-gram(N元)模型,就是在计算概率时,忽略长度大于N的上下文词的影响。当N=1时,称为一元模型(Uni-gram Mode),其表达式为:

$$P(w_1, w_2, ..., w_n) = \prod_{i=1}^m P(w_i)$$

当N=2时,称为二元模型(Bi-gram Model),其表达式为:

$$P(w_1, w_2, ..., w_n) = \prod_{i=1}^m P(w_i|w_{i-1})$$

当N=3时,称为三元模型(Tri-gram Model),其表达式为:

$$P(w_1, w_2, ..., w_n) = \prod_{i=1}^m P(w_i|w_{i-2}, w_{i-1})$$

可见,N值越大,保留的词序信息(上下文)越丰富,但计算量也呈指数级增长。

神经网络语言模型(NNLM)

NNLM是利用神经网络对N元条件进行概率估计的一种方法,其基本结构如下图所示:

NNLM.png

  • 输入:前N-1个词语的向量
  • 输出:第N个词语的一组概率
  • 目标函数:

$$f(w_t, t_{t-1}, ..., w_{t-n+1}) = p(p_t|w_1^{t-1})$$

其中,\(w_t\)表示第t个词,\(w_1^{t-1}\)表示第1个到第t个词语组成的子序列,每个词语概率均大于0,所有词语概率之和等于1。该模型计算包括两部分:特征映射、计算条件概率

  • 特征映射:将输入映射为一个特征向量,映射矩阵\(C \in R^{|V| \times m}\)
  • 计算条件概率分布:通过另一个函数,将特征向量转化为一个概率分布

神经网络计算公式为:

$$h = tanh(Hx + b)$$

$$y = Uh + d$$

  • H为隐藏层权重矩阵,U为隐藏层到输出层的权重矩阵。输出层加入softmax函数,将y转换为对应的概率。模型参数\(\theta\),包括:

$$\theta = (b, d, H, U, C)$$

  • 以下是一个计算示例:设词典大小为1000,向量维度为5,N=3,先将前N个词表示成独热向量:
呼:[1,0,0,0,0]
伦:[0,1,0,0,0]
贝:[0,0,1,0,0]
  • 输入矩阵为:[3, 5]
  • 权重矩阵:[1000, 5]
  • 隐藏层:[3, 5] * [1000, 5] = [3, 5]
  • 输出层权重:[5, 1000]
  • 输出矩阵:[3, 5] * [5, 1000] = [3, 1000] ==> [1, 1000],表示预测属于1000个词的概率.

Word2vec

Word2vec是Goolge发布的、应用最广泛的词嵌入表示学习技术,其主要作用是高效获取词语的词向量,目前被用作许多NLP任务的特征工程。Word2vec 可以根据给定的语料库,通过优化后的训练模型快速有效地将一个词语表达成向量形式,为自然语言处理领域的应用研究提供了新的工具,包含Skip-gram(跳字模型)和CBOW(连续词袋模型)来建立词语的词嵌入表示。Skip-gram的主要作用是根据当前词,预测背景词(前后的词);CBOW的主要作用是根据背景词(前后的词)预测当前词。

Skip-gram

Skip-gram的主要作用是根据当前词,预测背景词(前后的词),其结构图如下图所示:

skip_gram_network.png

例如有如下语句:呼伦贝尔大草原

呼_ _ 尔_ _原
_伦_ _大_ _
_ _贝_ _ 草_
呼_ _尔_ _原
_伦_ _大_ _

预测出前后词的数量,称为window_size(以上示例中windows_size为2),实际是要将以下概率最大化:

P(伦|呼)P(贝|呼)
P(伦|尔)P(贝|尔) P(大|尔)P(草|尔)
P(大|原)P(草|原)
......

可以写出概率的一般化表达式,设有文本Text,由N个单词组成:

$$Text = {w_1, w_2, w_3, ..., w_n}$$

  • 目标函数可以写作:

$$argmax \prod_{c \in Text} \prod_{c \in c(w)} P(c|w; \theta)$$

  • 因为概率均为0~1之间的数字,连乘计算较为困难,所以转换为对数相加形式:

$$argmax \sum_{c \in Text} \sum_{c \in c(w)} logP(c|w;\theta)$$

  • 再表示为softmax形式:

$$argmax \sum_{c \in Text} \sum_{c \in c(w)} log \frac{e^{u_c \cdot v_w}}{\sum_{c' \in vocab }e_{c'} \cdot v_w}$$

  • 其中,U为上下文单词矩阵,V为同样大小的中心词矩阵,因为每个词可以作为上下文词,同时也可以作为中心词,再将如上公式进一步转化:

$$argmax \sum_{c \in Text} \sum_{c \in c(w)} u_c \cdot v_w - log \sum_{c' \in vocab }e_{c'} \cdot v_w$$

  • 上式中,由于需要在整个词汇表中进行遍历,如果词汇表很大,计算效率会很低。所以,真正进行优化时,采用另一种优化形式。例如有如下语料库:
文本:呼伦贝尔大草原

将window_size设置为1,构建正案例词典、负案例词典(一般来说,负样本词典比正样本词典大的多):

正样本:D = {(呼,伦),(伦,呼),(伦,贝),(贝,伦),(贝,尔),(尔,贝),(尔,大),(大,尔),(大,草)(草,大),(草,原),(原,草)}

负样本:D’= {(呼,贝),(呼,尔),(呼,大),(呼,草),(呼,原),(伦,尔),(伦,大),(伦,草),(伦,原),(贝,呼),(贝,大),(贝,草),(贝,原),(尔,呼),(尔,伦)(尔,草),(尔,原),(大,呼),(大,伦),(大,原),(草,呼),(草,伦),(草,贝),(原,呼),(原,伦),(原,贝),(原,尔),(原,大)}

词向量优化的目标函数定义为正样本、负样本公共概率最大化函数:

$$argmax (\prod_{w,c \in D} log P(D=1|w,c; \theta) \prod_{w, c \in D'} P(D=0|w, c; \theta))$$

$$= argmax (\prod_{w,c \in D} \frac{1}{1+exp(-U_c \cdot V_w)} \prod_{w, c \in D'} [1- \frac{1}{1+exp(-U_c \cdot V_w)}])$$

$$= argmax(\sum_{w,c \in D} log \sigma (U_c \cdot V_w) + \sum_{w,c \in D'} log \sigma (-U_c \cdot V_w))$$

  • 在实际训练时,会从负样本集合中选取部分样本(称之为“负采样”)来进行计算,从而降低运算量.要训练词向量,还需要借助于语言模型.

CBOW模型

CBOW模型全程为Continous Bag of Words(连续词袋模型),其核心思想是用上下文来预测中心词,例如:

呼伦贝_大草原

CBOW_network.png

  • 输入:\(C \times V\)的矩阵,C表示上下文词语的个数,V表示词表大小
  • 隐藏层:\(V \times N\)的权重矩阵,一般称为word-embedding,N表示每个词的向量长度,和输入矩阵相乘得到\(C \times N\)的矩阵。综合考虑上下文中所有词信息预测中心词,所以将\(C \times N\)矩阵叠加,得到\(1 \times N\)的向量
  • 输出层:包含一个\(N \times V\)的权重矩阵,隐藏层向量和该矩阵相乘,输出\(1 \times V\)的向量,经过softmax转换为概率,对应每个词表中词语的概率

示例:训练词向量

数据集:来自中文wiki文章

  • 安装gensim
!pip install gensim==3.8.1 # 如果不在AIStudio下执行去掉前面的叹号
  • 用于解析XML,读取XML文件中的数据,并写入到新的文本文件中
####################### 解压语料库 ###########################
import logging
import os
import os.path
from gensim.corpora import WikiCorpus

# 输入文件
# 使用挂载的数据集 中文维基百科语料库
in_file = 'data/data104767/articles.xml.bz2'
# 输出文件
out_file = open('wiki.zh.text','w',encoding='utf-8')
count = 0

# lemmatize 是否要做词性还原
wiki = WikiCorpus(in_file,lemmatize=False,dictionary={})


for text in wiki.get_texts(): # 遍历语料库
    out_file.write(" ".join(text)+'\n') # 写入文件
    count += 1
    if count % 200 == 0:
        print(count)
    if count >= 20000:
        break
out_file.close()

####################### 分词 ###########################

import jieba
import jieba.analyse
import codecs # 工具包模块

def process_wiki_text(src_file,dest_file):#参数为原文件,目标文件
    with codecs.open(src_file,'r',encoding='utf-8') as f_in,codecs.open(dest_file,'w',encoding='utf-8') as f_out:#打开原文件和目标文件
        num = 1
        for line in f_in.readlines():# 遍历每一行
            line_seg = " ".join(jieba.cut(line))# 分词
            f_out.writelines(line_seg)#写入目标文件
            num+=1

            if num % 200 == 0:
                print(num)

process_wiki_text("wiki.zh.text","wiki.zh.text.seg")# 调用分词函数

####################### 训练 ###########################
# 导入工具库
import logging
import sys
import multiprocessing # cpu开启多线程执行
from gensim.models import Word2Vec
# 按照行的方式读取文件内容(分词文件)
from gensim.models.word2vec import LineSentence 

logger = logging.getLogger(__name__)
# format: 指定输出的格式和内容,format可以输出很多有用信息,
# %(asctime)s: 打印日志的时间
# %(levelname)s: 打印日志级别名称
# %(message)s: 打印日志信息
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
logging.root.setLevel(level=logging.INFO)

# 1.输入文件
in_file = 'wiki.zh.text.seg'
out_file1 = 'wiki.zh.text.model'# 存模型
out_file2 = 'wiki.zh.text.vector'# 存权重 (词向量)

model = Word2Vec(LineSentence(in_file),#输入
                size=100,#词向量维度 推荐50-300
                window=3,#窗口大小
                min_count=5,#最小出现次数 小于5次忽略
                workers=multiprocessing.cpu_count())# 线程数量 和cpu一致
model.save(out_file1)# 保存模型
model.wv.save_word2vec_format(out_file2,#权重文件
                                binary=False)#不保存成二进制


####################### 演示 ###########################

import gensim
from gensim.models import word2vec

model = Word2Vec.load("wiki.zh.text.model")#加载模型
count = 0

#打印前10个词向量

for word in model.wv.index2word:
    print(word,'[',model[word],']')#打印每个词对应的词向量
    count += 1
    if count >=10:
        break
print("")

result = model.most_similar(u"铁路")# 返回跟指定词语相似度最高的词
for r in result:
    print(r)
print("")

result = model.most_similar(u"中药")# 返回跟指定词语相似度最高的词
for r in result:
    print(r)
print("")
  • 输出
('高速铁路', 0.8310302495956421)
('客运专线', 0.8245105743408203)
('高铁', 0.8095601201057434)
('城际', 0.802475094795227)
('联络线', 0.7837506532669067)
('成昆铁路', 0.7820425033569336)
('支线', 0.7775323390960693)
('通车', 0.7751388549804688)
('沪', 0.7748854756355286)
('京广', 0.7708789110183716)
==============================================
('草药', 0.9046826362609863)
('中药材', 0.8511005640029907)
('气功', 0.8384993672370911)
('中医学', 0.8368280529975891)
('调味', 0.8364394307136536)
('冶炼', 0.8328938484191895)
('药材', 0.8304706811904907)
('有机合成', 0.8298543691635132)
('针灸', 0.8297436833381653)
('药用', 0.8281913995742798)

循环神经网络(RNN)

前面提到的关于NLP的模型及应用,都未考虑词的顺序问题,而在自然语言中,词语顺序又是极其重要的特征。循环神经网络(Recurrent Neural Network,RNN)能够在原有神经网络的基础上增加记忆单元,处理任意长度的序列(理论上),并且在前后词语(或字)之间建立起依赖关系。相比于CNN,RNN更适合处理视频、语音、文本等与时序相关的问题。

原生RNN

1982年,物理学家约翰·霍普菲尔德(John Hopfield)利用电阻、电容和运算放大器等元件组成的模拟电路实现了对网络神经元的描述,该网络从输出到输入有反馈连接。1986年,迈克尔·乔丹(Michael Jordan,不是打篮球那哥们,而是著名人工智能学者、美国科学院院士、吴恩达的导师)借鉴了Hopfield网络的思想,正式将循环连接拓扑结构引入神经网络。1990年,杰弗里·埃尔曼(Jeffrey Elman)又在Jordan的研究基础上做了部分简化,正式提出了RNN模型(那时还叫Simple Recurrent Network,SRN)。

  • RNN结构如下图所示:

RNN_1.png

  • 上图中,左侧为不展开的画法,右侧为展开画法。内部结构如下图所示:

RNN_2.png

  • 计算公式可表示为:

$$s_t = f(U \cdot x_t + W \cdot s_{t-1} + b)$$

$$y_t = g(V \cdot s_t + d)$$

  • 其中,\(x_t\)表示\(t\)时刻的输入;\(s_t\)表示\(t\)时刻隐藏状态;\(f\)和\(g\)表示激活函数;\(U,V,W\)分别表示输入层 → 隐藏层权重、隐藏层 → 输出层权重、隐藏层 → 隐藏层权重。对于任意时刻\(t\),所有权重和偏置都共享,这极大减少了模型参数量。
  • 计算时,首先利用前向传播算法,依次按照时间顺序进行计算,再利用反向传播算法进行误差传递,和普通BP(Back Propagation)网络唯一区别是,加入了时间顺序,计算方式有些微差别,称为BPTT(Back Propagation Through Time)算法。

RNN的功能

  • RNN善于处理跟序列相关的信息,如:语音识别,语言建模,翻译,图像字幕。它能根据近期的一些信息来执行/判别/预测当前任务。例如:
白色的云朵漂浮在蓝色的____
天空中飞过来一只___
  • 根据前面输入的一连串词语,可以预测第一个句子最后一个词为"天空"、第二个句子最后一个词为"鸟"的概率最高。

RNN的缺陷

  • 因为计算的缘故,RNN容易出现梯度消失,导致它无法学习过往久远的信息,无法处理长序列、远期依赖任务。例如:
我生长在中国,祖上十代都是农民,家里三亩一分地。我是家里老三,我大哥叫大狗子,二哥叫二狗子,我叫三狗子,我弟弟叫狗窝子。我的母语是_____
  • 要预测出句子最后的词语,需要根据句子开够的信息"我出生在中国",才能确定母语是"中文"或"汉语"的概率最高。原生RNN在处理这类远期依赖任务时出现了困难,于是LSTM被提出。

长短期记忆模型(LSTM)

长短期记忆模型(Long Short Term Memory,LSTM)是RNN的变种,于1997年Schmidhuber和他的合作者Hochreiter提出,由于独特的设计结构,LSTM可以很好地解决梯度消失问题,能够处理更长的序列,更好解决远期依赖任务。LSTM非常适合构造大型深度神经网络。2009年,用改进版的LSTM,赢得了国际文档分析与识别大赛(ICDAR)手写识别大赛冠军;2014年,Yoshua Bengio的团队提出了一种更好的LSTM变体GRU(Gated Recurrent Unit,门控环单元);2016年,Google利用LSTM来做语音识别和文字翻译;同年,苹果公司使用LSTM来优化Siri应用。

  • LSTM同样具有链式结构,它具有4个以特殊方式互相影响的神经网络层。其结构入下图所示:

LSTM.png

  • LSTM的核心是细胞状态,用贯穿细胞的水平线表示。细胞状态像传送带一样。它贯穿整个细胞却只有很少的分支,这样能保证信息不变的流过整个结构。同时,LSTM通过称为门(gate)的结构来对单元状态进行增加或删除,包含三扇门:
  • 遗忘门:决定哪些信息丢弃

LSTM_forget.png

  • 表达式为:\(f_t = \sigma (W_f \cdot [h_{t-1}, x_t] + b_f)\),当输出为1时表示完全保留,输出为0是表示完全丢弃
  • 输入门:决定哪些信息输入进来

LSTM_input.png

  • 表达式为:

$$i_t = \sigma (W_i \cdot [h_{t-1}, x_t] + b_i)$$

$$\tilde{C}_t = tanh(W_c \cdot [h_{t-1}, x_t] + b_c)$$

  • 根据输入、遗忘门作用结果,可以对细胞状态进行更新,如下图所示:

LSTM_update.png

  • 状态更新表达式为:

$$C_t = f_t \cdot C_{t-1} + i_t \cdot \tilde{C}_t$$

  • 遗忘门找到了需要忘掉的信息\(f_t\)后,再将它与旧状态相乘,丢弃掉确定需要丢弃的信息。再将结果加上\(i_t \cdot C_t\)使细胞状态获得新的信息,这样就完成了细胞状态的更新。
  • 输出门:决定输出哪些信息

LSTM_out.png

  • 输出门表达式为:

$$O_t = \sigma (W_o \cdot [h_{t-1}, x_t] + b_o)$$

$$h_t = O_t \cdot tanh(C_t)$$

  • 在输出门中,通过一个Sigmoid层来确定哪部分的信息将输出,接着把细胞状态通过Tanh进行处理(得到一个在-1~1之间的值)并将它和Sigmoid门的输出相乘,得出最终想要输出的那部分。

双向循环神经网络

  • 双向循环神经网络(BRNN)由两个循环神经网络组成,一个正向、一个反向,两个序列连接同一个输出层。正向RNN提取正向序列特征,反向RNN提取反向序列特征。例如有如下两个语句:
我喜欢苹果,比安卓用起来更流畅些
我喜欢苹果,基本上每天都要吃一个
  • 根据后面的描述,我们可以得知,第一句中的"苹果"指的是苹果手机,第二句中的"苹果"指的是水果。双向循环神经网络结构如下图所示:

BiRNN.png

  • 权重设置如下图所示:

BiRNN_2.png

  • 计算表达式为:

$$h_t = f(w_1x_t + w_2h_{t-1})$$

$$h_t' = f(w_3x_t + w_5h'_{t+1})$$

$$o_t = g(w_4h_t + w_6h'_t)$$

  • 其中,\(h_t\)为\(t\)时刻正向序列计算结果,\(h'_t\)为\(t\)时刻反向序列的计算结果,将正向序列、反向序列结果和各自权重矩阵相乘,相加后结果激活函数产生\(t\)时刻的输出。
  • 通常情况下,双向循环神经网络能获得比单向网络更好的性能。

NLP应用

文本分类

什么是文本分类

  • 文本分类就是根据文本内容将文本划分到不同类别,例如新闻系统中,每篇新闻报道会划归到不同的类别。

文本分类的应用

  • 内容分类(新闻分类)
  • 邮件过滤(例如垃圾邮件过滤)
  • 用户分类(如商城消费级别、喜好)
  • 评论、文章、对话的情感分类(正面、负面、中性)

文本分类案例

  • 任务:建立文本分类模型,并对模型进行训练、评估,从而实现对中文新闻摘要类别正确划分
  • 数据集:从网站上爬取56821条数据中文新闻摘要,包含10种类别,国际、文化、娱乐、体育、财经、汽车、教育、科技、房产、证券,各类别样本数量如下表所示:

News_samples_classes.png

  • 模型选择:

News_classify_network.png

  • 步骤:

News_classify_flow.png

################ 数据预处理 ################

import os
from multiprocessing import cpu_count
import numpy as np
import paddle
import paddle.fluid as fluid

# 定义公共变量
data_root = "data/" # 数据集所在目录
data_file = "news_classify_data.txt" # 原始样本文件名
test_file = "test_list.txt" # 测试集文件名称
train_file = "train_list.txt" # 训练集文件名称
dict_file = "dict_txt.txt" # 编码后的字典文件

data_file_path = data_root+data_file # 数据集完整路径
train_file_path = data_root+train_file # 训练集文件完整路径
test_file_path = data_root+test_file # 测试集文件完整路径
dict_file_path = data_root+dict_file # 字典文件完整路径

# 取出所有的字,对每个字进行编码 将编码结果存入字典文件
def create_dict():
    dict_set = set()#集合去重
    with open(data_file_path,'r',encoding='utf-8') as f:
        for line in f.readlines():#遍历
            line = line.replace("\n",'')#去掉换行符
            tmp_list = line.split('_!_')# 根据分隔符拆分
            title = tmp_list[-1]#获取标题
            for word in title:#取出每一个字
                dict_set.add(word)
    #遍历集合,取出每个字进行编号
    dict_txt = {} #定义字典
    i = 1#编码使用的计数器
    for word in dict_set:
        dict_txt[word] = i #字-编码 键值对 添加到字典
        i += 1
    dict_txt['<unk>'] = i # 未知字符(在样本中未出现过的字)

    # 将字典内容存入文件
    with open(dict_file_path,'w',encoding='utf-8') as f:
        f.write(str(dict_txt))

    print('dict end')

# 传入一个句子 将每个字替换为编码值 和标签一起返回
def line_encoding(title,dict_txt,label):
    new_line = ''
    for word in title:
        if word in dict_txt: # 在字典中
            code = str(dict_txt[word]) # 取出编码
        else: #不在字典中
            code = str(dict_txt["<unk>"]) #取出最后一个
        new_line = new_line+code+','
    new_line = new_line[:-1]# 去掉最后一个字符
    new_line = new_line + '\t' + label + '\n' # 追加标签值
    return new_line
# 读取原始样本 取出标题部分进行编码 将编码后的划分测试集和训练集

def create_train_test_file():
    #清空
    with open(train_file_path,'w') as f:
        pass
    with open(test_file_path,'w') as f:
        pass

    #读取字典文件
    with open(dict_file_path,'r',encoding='utf-8') as f:
        # 该文件内容为单行 且为{'xx':1,'yy':2}直接使用eval可以转换为字典对象
        dict_txt = eval(f.readlines()[0]) # 读取字典文件第一行 生成字典对象

    # 读取原始样本
    with open(data_file_path,'r',encoding='utf-8') as f:
        lines = f.readlines()
    i = 0
    for line in lines:
        tmp_list = line.replace('\n','').split('_!_')# 替换后拆分
        title = tmp_list[3] # 标题
        label = tmp_list[1] # 类别
        new_line = line_encoding(title,dict_txt,label)

        if i % 10 == 0:# 写入测试集
            with open(test_file_path,'a',encoding='utf-8') as f:
                f.write(new_line)
        else:
            with open(train_file_path,'a',encoding='utf-8') as f:
                f.write(new_line)
        i += 1
    print('test/train')

create_dict()
create_train_test_file()

################ 模型定义训练评估 ################

# 读取字典文件 返回字典长度

def get_dict_len(dict_path):
        #读取字典文件
    with open(dict_file_path,'r',encoding='utf-8') as f:
        # 该文件内容为单行 且为{'xx':1,'yy':2}直接使用eval可以转换为字典对象
        dict_txt = eval(f.readlines()[0]) # 读取字典文件第一行 生成字典对象
    return len(dict_txt.keys())

def data_mapper(sample):
    data,label = sample
    val = [int(w) for w in data.split(",")] # 将编码值转换为数字
    return val,int(label)


def train_reader(train_file_path):
    def reader():
        with open(train_file_path,'r') as f:
            lines = f.readlines()
            np.random.shuffle(lines)#随机化处理
            for line in lines:
                data,label = line.split('\t')
                yield data,label
    return paddle.reader.xmap_readers(data_mapper,reader,cpu_count(),1024)

def test_reader(test_file_path):
    def reader():
        with open(test_file_path,'r') as f:
            lines = f.readlines()
            for line in lines:
                data,label = line.split('\t')#拆分
                yield data,label
    return paddle.reader.xmap_readers(data_mapper,reader,cpu_count(),1024)


#定义网络

def Text_CNN(data,dict_dim,class_dim=10,emb_dim=128,hid_dim=128,hid_dim2=128):
    """
    定义textcnn模型
    :param data:输入
    :param dict_dim:词典大小(词语总的数量)
    :param class_dim:分类的数量
    :param emb_dim:词嵌入长度
    :param hid_dim:第一个卷积层卷积核数量
    :param hid_dim2:第二个卷积层卷积核数量
    :return: 模型预测结果
    """

    #embedding层
    emb = fluid.layers.embedding(input=data,size=[dict_dim,emb_dim])
    #并列两个卷积/池化层
    conv1 = fluid.nets.sequence_conv_pool(input=emb,#输入(词嵌入层输出)
                                        num_filters=hid_dim,#卷积核数量
                                        filter_size=3,#卷积核大小
                                        act='tanh',#激活函数
                                        pool_type="sqrt")#池化类型
    conv2 = fluid.nets.sequence_conv_pool(input=emb,#输入(词嵌入层输出)
                                        num_filters=hid_dim2,#卷积核数量
                                        filter_size=4,#卷积核大小
                                        act='tanh',#激活函数
                                        pool_type="sqrt")#池化类型


    # fc
    output = fluid.layers.fc(input=[conv1,conv2],#输入
                            size=class_dim,#输出值个数
                            act='softmax')#激活函数

    return output

# 定义占位符张量
words=fluid.layers.data(name="words",shape=[1],dtype='int64',lod_level=1)#lod张量用来表示变长数据
label=fluid.layers.data(name="label",shape=[1],dtype='int64')
# 获取字典长度
dict_dim = get_dict_len(dict_file_path)
#调用模型函数
model = Text_CNN(words,dict_dim)
#损失函数
cost = fluid.layers.cross_entropy(input=model,label=label)
avg_cost = fluid.layers.mean(cost)
# 优化器
optimizer = fluid.optimizer.Adam(learning_rate=0.0001)
optimizer.minimize(avg_cost)
# 准确率
accuracy = fluid.layers.accuracy(input=model,label=label)
#克隆program专门用于模型评估
# test_program = fluid.default_main_program().clone(for_test=True)

#执行器
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())

#reader
#训练集reader
tr_reader = train_reader(train_file_path)
batch_train_reader = paddle.batch(tr_reader,batch_size=128)
#测试集reader
ts_reader = test_reader(test_file_path)
batch_test_reader = paddle.batch(ts_reader,batch_size=128)

#feeder
feeder = fluid.DataFeeder(place=place,feed_list=[words,label])

#开始训练

for epoch in range(80):
    for batch_id,data in enumerate(batch_train_reader()):#内循环控制批次
        train_cost,train_acc = exe.run(program=fluid.default_main_program(),#program
                                        feed=feeder.feed(data), #喂入的参数
                                        fetch_list=[avg_cost,accuracy]) #返回值

        if batch_id % 100 == 0:
            print("epoch %d batch_id %d cost %f acc %f"%(epoch,batch_id,train_cost[0],train_acc[0]))
    #每轮训练结束后进行模型评估
    test_costs_list = [] #存放测试集损失值
    test_accs_list = [] #存放测试集准确率

    for batch_id,data in enumerate(batch_test_reader()):
        test_cost,test_acc = exe.run(program=fluid.default_main_program(),#program
                                        feed=feeder.feed(data), #喂入的参数
                                        fetch_list=[avg_cost,accuracy]) #返回值
        test_costs_list.append(test_cost[0])
        test_accs_list.append(test_acc[0])
    # 计算所有批次损失值/准确率均值
    avg_test_cost = sum(test_costs_list)/len(test_costs_list)
    avg_test_acc = sum(test_accs_list)/len(test_accs_list)
    print("epoch %d cost %f acc %f"%(epoch,avg_test_cost,avg_test_acc))


# 训练结束保存模型
model_save_dir = 'model/'
if not os.path.exists(model_save_dir):
    os.makedirs(model_save_dir)
fluid.io.save_inference_model(model_save_dir,
                                feeded_var_names=[words.name],#使用时传入参数名称
                                target_vars=[model],#预测结果
                                executor=exe)#执行器
print('保存成功')

################ 测试 ################

model_save_dir = 'model/'


#将传入的句子根据字典中的值进行编码
def get_data(sentence):
    with open(dict_file_path,'r',encoding='utf-8') as f:
        dict_txt = eval(f.readlines()[0])

    ret = []#编码结果
    keys = dict_txt.keys()
    for w in sentence:
        if w not in keys:
            w = '<unk>'
        ret.append(int(dict_txt[w]))
    return ret

#执行器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())

# 加载模型
infer_program,feed_names,target_var = fluid.io.load_inference_model(model_save_dir,exe)

texts = []

data1 = get_data("在获得诺贝尔文学奖7年之后,莫言15日晚间在山西汾阳贾家庄如是说")
data2 = get_data("综合'今日美国'、《世界日报》等当地媒体报道,芝加哥河滨警察局表示")
data3 = get_data("中国队2022年冬奥会表现优秀")
data4 = get_data("中国人民银行今日发布通知,降低准备金率,预计释放4000亿流动性")
data5 = get_data("10月20日,第六届世界互联网大会正式开幕")
data6 = get_data("同一户型,为什么高层比低层要贵那么多?")
data7 = get_data("揭秘A股周涨5%资金动向:追捧2类股,抛售600亿香饽饽")
data8 = get_data("宋慧乔陷入感染危机,前夫宋仲基不戴口罩露面,身处国外神态轻松")
data9 = get_data("此盆栽花很好养,花美似牡丹,三季开花,南北都能养,很值得栽培")  # 不属于任何一个类别

texts.append(data1)
texts.append(data2)
texts.append(data3)
texts.append(data4)
texts.append(data5)
texts.append(data6)
texts.append(data7)
texts.append(data8)
texts.append(data9)

base_shape = [[len(c) for c in texts]]

# 将句子列表转换为LOD张量
tensor_words = fluid.create_lod_tensor(texts,base_shape,place)

# 执行推理
result = exe.run(program=infer_program,#program
                    feed={feed_names[0]:tensor_words}, #喂入的参数
                    fetch_list=[target_var]) #返回值

# 将预测结果转换为字符串
names = ["文化", "娱乐", "体育", "财经", "房产",
         "汽车", "教育", "科技", "国际", "证券"]
for r in result[0]:
    idx = np.argmax(r) # 取出最大值的索引
    print("预测结果:", names[idx], " 概率:", r[idx])

文本情感分析

  • 目标:利用训练数据集,对模型训练,从而实现对中文评论语句情感分析。情绪分为正面、负面两种
  • 数据集:中文关于酒店的评论,5265笔用户评论数据,其中2822笔正面评价、其余为负面评价
  • 步骤:同上一案例
  • 模型选择:

Text_emotion_network.png

# 中文情绪分析:数据预处理部分
import paddle
import paddle.dataset.imdb as imdb
import paddle.fluid as fluid
import numpy as np
import os
import random
from multiprocessing import cpu_count

# 数据预处理,将中文文字解析出来,并进行编码转换为数字,每一行文字存入数组
mydict = {}  # 存放出现的字及编码,格式: 好,1
code = 1
data_file = "data/hotel_discuss2.csv"  # 原始样本路径
dict_file = "data/hotel_dict.txt" # 字典文件路径
encoding_file = "data/hotel_encoding.txt" # 编码后的样本文件路径
puncts = " \n"  # 要剔除的标点符号列表

with open(data_file, "r", encoding="utf-8-sig") as f:
    for line in f.readlines():
        # print(line)
        trim_line = line.strip()
        for ch in trim_line:
            if ch in puncts:  # 符号不参与编码
                continue

            if ch in mydict:  # 已经在编码字典中
                continue
            elif len(ch) <= 0:
                continue
            else:  # 当前文字没在字典中
                mydict[ch] = code
                code += 1
    code += 1
    mydict["<unk>"] = code  # 未知字符

# 循环结束后,将字典存入字典文件
with open(dict_file, "w", encoding="utf-8-sig") as f:
    f.write(str(mydict))
    print("数据字典保存完成!")


# 将字典文件中的数据加载到mydict字典中
def load_dict():
    with open(dict_file, "r", encoding="utf-8-sig") as f:
        lines = f.readlines()
        new_dict = eval(lines[0])
    return new_dict

# 对评论数据进行编码
new_dict = load_dict()  # 调用函数加载

with open(data_file, "r", encoding="utf-8-sig") as f:
    with open(encoding_file, "w", encoding="utf-8-sig") as fw:
        for line in f.readlines():
            label = line[0]  # 标签
            remark = line[2:-1]  # 评论

            new_line = ''
            for ch in remark:
                if ch in puncts:  # 符号不参与编码
                    continue
                else:
                    new_line = new_line+str(mydict[ch])+','
            new_line = new_line[:-1]# 去掉最后一个字符
            fw.write(new_line + "\t" + str(label) + "\n")  # 写入tab分隔符、标签、换行符

print("数据预处理完成")

# 模型定义与训练
# 获取字典的长度
def get_dict_len(dict_path):
    with open(dict_path, 'r', encoding='utf-8-sig') as f:
        lines = f.readlines()
        new_dict = eval(lines[0])

    return len(new_dict.keys())

# 创建数据读取器train_reader和test_reader
# 返回评论列表和标签
def data_mapper(sample):
    dt, lbl = sample
    val = [int(word) for word in dt.split(",") if word.isdigit()]
    return val, int(lbl)

# 随机从训练数据集文件中取出一行数据
def train_reader(train_list_path):
    def reader():
        with open(train_list_path, "r", encoding='utf-8-sig') as f:
            lines = f.readlines()
            np.random.shuffle(lines)  # 打乱数据

            for line in lines:
                data, label = line.split("\t")
                yield data, label

    # 返回xmap_readers, 能够使用多线程方式读取数据
    return paddle.reader.xmap_readers(data_mapper,  # 映射函数
                                      reader,  # 读取数据内容
                                      cpu_count(),  # 线程数量
                                      1024)  # 读取数据队列大小

# 定义LSTM网络
def lstm_net(ipt, input_dim):
    ipt = fluid.layers.reshape(ipt, [-1, 1],
                               inplace=True) # 是否替换,True则表示输入和返回是同一个对象
    # 词嵌入层
    emb = fluid.layers.embedding(input=ipt, size=[input_dim, 128], is_sparse=True)

    # 第一个全连接层
    fc1 = fluid.layers.fc(input=emb, size=128)

    # 第一分支:LSTM分支
    lstm1, _ = fluid.layers.dynamic_lstm(input=fc1, size=128)
    lstm2 = fluid.layers.sequence_pool(input=lstm1, pool_type="max")

    # 第二分支
    conv = fluid.layers.sequence_pool(input=fc1, pool_type="max")

    # 输出层:全连接
    out = fluid.layers.fc([conv, lstm2], size=2, act="softmax")

    return out

# 定义输入数据,lod_level不为0指定输入数据为序列数据
dict_len = get_dict_len(dict_file)  # 获取数据字典长度
rmk = fluid.layers.data(name="rmk", shape=[1], dtype="int64", lod_level=1)
label = fluid.layers.data(name="label", shape=[1], dtype="int64")

# 定义长短期记忆网络
model = lstm_net(rmk, dict_len)

# 定义损失函数,情绪判断实际是一个分类任务,使用交叉熵作为损失函数
cost = fluid.layers.cross_entropy(input=model, label=label)
avg_cost = fluid.layers.mean(cost)  # 求损失值平均数
# layers.accuracy接口,用来评估预测准确率
acc = fluid.layers.accuracy(input=model, label=label)

# 定义优化方法
# Adagrad(自适应学习率,前期放大梯度调节,后期缩小梯度调节)
optimizer = fluid.optimizer.AdagradOptimizer(learning_rate=0.001)
opt = optimizer.minimize(avg_cost)

# 定义网络
# place = fluid.CPUPlace()
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())  # 参数初始化

# 定义reader
reader = train_reader(encoding_file)
batch_train_reader = paddle.batch(reader, batch_size=128)

# 定义输入数据的维度,数据的顺序是一条句子数据对应一个标签
feeder = fluid.DataFeeder(place=place, feed_list=[rmk, label])

for pass_id in range(40):
    for batch_id, data in enumerate(batch_train_reader()):
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),
                                        feed=feeder.feed(data),
                                        fetch_list=[avg_cost, acc])

        if batch_id % 20 == 0:
            print("pass_id: %d, batch_id: %d, cost: %0.5f, acc:%.5f" %
                  (pass_id, batch_id, train_cost[0], train_acc))

print("模型训练完成......")

# 保存模型
model_save_dir = "model/chn_emotion_analyses.model"
if not os.path.exists(model_save_dir):
    print("create model path")
    os.makedirs(model_save_dir)

fluid.io.save_inference_model(model_save_dir,  # 保存路径
                              feeded_var_names=[rmk.name],
                              target_vars=[model],
                              executor=exe)  # Executor

print("模型保存完成, 保存路径: ", model_save_dir)


# 推理预测
import paddle
import paddle.fluid as fluid
import numpy as np
import os
import random
from multiprocessing import cpu_count

data_file = "data/hotel_discuss2.csv"
dict_file = "data/hotel_dict.txt"
encoding_file = "data/hotel_encoding.txt"
model_save_dir = "model/chn_emotion_analyses.model"

def load_dict():
    with open(dict_file, "r", encoding="utf-8-sig") as f:
        lines = f.readlines()
        new_dict = eval(lines[0])
        return new_dict

# 根据字典对字符串进行编码
def encode_by_dict(remark, dict_encoded):
    remark = remark.strip()
    if len(remark) <= 0:
        return []

    ret = []
    for ch in remark:
        if ch in dict_encoded:
            ret.append(dict_encoded[ch])
        else:
            ret.append(dict_encoded["<unk>"])

    return ret


# 编码,预测
lods = []
new_dict = load_dict()
lods.append(encode_by_dict("总体来说房间非常干净,卫浴设施也相当不错,交通也比较便利", new_dict))
lods.append(encode_by_dict("酒店交通方便,环境也不错,正好是我们办事地点的旁边,感觉性价比还可以", new_dict))
lods.append(encode_by_dict("设施还可以,服务人员态度也好,交通还算便利", new_dict))
lods.append(encode_by_dict("酒店服务态度极差,设施很差", new_dict))
lods.append(encode_by_dict("我住过的最不好的酒店,以后决不住了", new_dict))
lods.append(encode_by_dict("说实在的我很失望,我想这家酒店以后无论如何我都不会再去了", new_dict))

# 获取每句话的单词数量
base_shape = [[len(c) for c in lods]]

# 生成预测数据
place = fluid.CPUPlace()
infer_exe = fluid.Executor(place)
infer_exe.run(fluid.default_startup_program())

tensor_words = fluid.create_lod_tensor(lods, base_shape, place)

infer_program, feed_target_names, fetch_targets = fluid.io.load_inference_model(dirname=model_save_dir, executor=infer_exe)
# tvar = np.array(fetch_targets, dtype="int64")
results = infer_exe.run(program=infer_program,
                  feed={feed_target_names[0]: tensor_words},
                  fetch_list=fetch_targets)

# 打印每句话的正负面预测概率
for i, r in enumerate(results[0]):
    print("负面: %0.5f, 正面: %0.5f" % (r[0], r[1]))

附录一:相关数学知识

向量余弦相似度

  • 余弦相似度使用来度量向量相似度的指标,当两个向量夹角越大相似度越低;当两个向量夹角越小,相似度越高。

vector.png

  • 在三角形中,余弦值计算方式为\(cos \theta = \frac{a^2 + b^2 - c^2}{2ab}\),向量夹角余弦计算公式为:

$$cos \theta = \frac{ab}{||a|| \times ||b||}$$

  • 分子为两个向量的内积,分母是两个向量模长的乘积。

vector_cos.png

  • 其推导过程如下:

$$cos \theta = \frac{a^2 + b^2 - c^2}{2ab}$$

$$= \frac{\sqrt{x_1^2 + y_1^2} + \sqrt{x_2^2 + y_2^2 }+ \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}}{2 \sqrt{x_1^2 + y_1^2} \sqrt{x_2^2 + y_2^2}}$$

$$= \frac{2 x_1 x_2 + 2 y_1 y_2}{2 \sqrt{x_1^2 + y_1^2} \sqrt{x_2^2 + y_2^2}} = \frac{ab}{||a|| \times ||b||}$$

  • 以上是二维向量的计算过程,推广到N维向量,分子部分依然是向量的内积,分母部分依然是两个向量模长的乘积。由此可计算文本的余弦相似度。

附录二:参考文献

  1. 《Python自然语言处理实践——核心技术与算法》 ,涂铭、刘祥、刘树春 著 ,机械工业出版社
  2. 《Tensorflow自然语言处理》,【澳】图珊·加内格达拉,机械工业出版社
  3. 《深度学习之美》,张玉宏,中国工信出版集团 / 电子工业出版社

附录三:专业词汇列表

英文简写 英文全写 中文
NLP Nature Language Processing 自然语言处理
NER Named Entities Recognition 命名实体识别
PoS part-of-speech tagging 词性标记
MT Machine Translation 机器翻译
TF-IDF Term Frequency-Inverse Document Frequency 词频-逆文档频率
Text Rank 文本排名算法
One-hot 独热编码
BOW Bag-of-Words Model 词袋模型
N-Gram N元模型
word embedding 词嵌入
NNLM Neural Network Language Model 神经网络语言模型
HMM Hidden Markov Model 隐马尔可夫模型
RNN Recurrent Neural Networks 循环神经网络
Skip-gram 跳字模型
CBOW Continous Bag of Words 连续词袋模型
LSTM Long Short Term Memory 长短期记忆模型
GRU Gated Recurrent Unit 门控环单元
BRNN Bi-recurrent neural network 双向循环神经网络
FMM Forward Maximum Matching 正向最大匹配
RMM Reverse Maximum Matching 逆向最大匹配
Bi-MM Bi-directional Maximum Matching 双向最大匹配法

其他内容