《Python数据分析与机器学习实战-唐宇迪》读书笔记第20章--神经网络项目实战——影评情感分析
第20章神经网络项目实战——影评情感分析
之前讲解神经网络时,都是以图像数据为例,训练过程中,数据样本之间是相互独立的。但是在自然语言处理中就有些区别,例如,一句话中各个词之间有明确的先后顺序,或者一篇文章的上下文之间肯定有联系,但是,传统神经网络却无法处理这种关系。递归神经网络(Recurrent Neural Network,RNN)就是专门解决这类问题的,本章就递归神经网络结构展开分析,并将其应用在真实的影评数据集中进行分类任务。
20.1递归神经网络
递归神经网络与卷积神经网络并称深度学习中两大杰出代表,分别应用于计算机视觉与自然语言处理中,本节介绍递归神经网络的基本原理。
20.1.1RNN网络架构
RNN网络的应用十分广泛,任何与自然语言处理能挂钩的任务基本都有它的影子,先来看一下它的整体架构,如图20-1所示。
图20-1 RNN网络整体架构
其实只要大家熟悉了基本的神经网络结构,再来分析递归神经网络就容易多了,它只比传统网络多做了一件事——保留各个输入的中间信息。例如,有一个时间序列数据 [X0,X1,X2,...,Xt],如果直接用神经网络去做,网络会依次输入各个数据,不会考虑它们之间的联系。
在RNN网络中,一个序列的输入数据来了,不仅要计算最终结果,还要保存中间结果,例如,整体操作需要2个全连接层得到最后的结果,现在把经过第一个全连接层得到的特征单独保存下来。
在计算下一个输入样本时,输入数就不仅是当前的输入样本,还包括前一步得到的中间特征,相当于综合考虑本轮的输入和上一轮的中间结果。通过这种方法,可以把时间序列的关系加入网络结构中。
例如,可以把输入数据想象成一段话,X0,X1,…,Xi就是其中每一个词语,此时要做一个分类任务,看一看这句话的情感是积极的还是消极的。首先将X0最先输入网络,不仅得到当前输出结果h0,还有其中间输出特征。接下来将X1输入网络,和它一起进来的还有前一轮X0的中间特征,以此类推,最终这段话最后一个词语Xt输入进来,依旧会结合前一轮的中间特征(此时前一轮不仅指Xt-1,因为Xt-1也会带有Xt-2的特征,以依类推,就好比综合了前面全部的信息),得到最终的结果ht就是想要的分类结果。
可以看到,在递归神经网络中,只要没到最后一步,就会把每一步的中间结果全部保存下来,以供后续过程使用。每一步也都会得到相应的输出,只不过中间阶段的输出用处并不大,因为还没有把所有内容都加载加来,通常都是把最后一步的输出结果当作整个模型的最终输出,因为它把前面所有的信息都考虑进来。
例如,当前的输入是一句影评数据,如图20-2所示。
图20-2 影评输入数据
网络结构展开如图20-3所示。
图20-3 RNN网络展开
每一轮输出结果为:
这就是递归神经网络的整体架构,原理和计算方法都与神经网络类似(全连接),只不过要考虑前一轮的结果。这就使得RNN网络更适用于时间序列相关数据,它与语言和文字的表达十分相似,所以更适合自然语言处理任务。
20.1.2LSTM网络
RNN网络看起来十分强大,那么它有没有问题呢?如果一句话过长,也就是输入X序列过多的时候,最后一个输入会把前面所有的中间特征都考虑进来。此时可以想象一下,通常情况下,语言或者文字都是离着越近,相关性越高,例如:“今天我白天在家玩了一天,主要在玩游戏,晚上照样没事干,准备出去打球。”最后的词语“打球”应该会和晚上没事干比较相关,而和前面的玩游戏没有多大关系,但是,RNN网络会把很多无关信息全部考虑进来。实际的自然语言处理任务也会有相似的问题,越相关的应当前后越紧密,如果中间东西记得太多,就会使得整体网络模型效果有所下降。
所以最好的办法就是让网络有选择地记忆或遗忘一些内容,重要的东西需要记得更深刻,价值不大的信息可以遗忘掉,这就用到当下最流行的Long Short Term Memory Units,简称LSTM,它在RNN网络的基础上加入控制单元,以有选择地保留或遗忘部分中间结果,现在来看一下它的整体架构,如图20-4所示。
图20-4 LSTM整体架构
它的主要组成部分有输入门、输出门、遗忘门和一个记忆控制器C,简单概述,就是通过一个持续维护并进行更新的Ct来控制每次迭代需要记住或忘掉哪些信息,如果一个序列很长,相关的内容会选择记忆下来,一些没用的描述忘掉就好。
LSTM网络在处理问题时,整体流程还是与RNN网络类似,只不过每一步增加了选择记忆的细节,这里只向大家进行了简单介绍,了解其基本原理即可,如图20-5所示。随着技术的升级,RNN网络中各种新产品也是层出不穷。
图20-5 LSTM网络展开
20.2影评数据特征工程
现在要对电影评论数据集进行分类任务(二分类),建立一个LSTM网络模型,以识别哪些评论是积极肯定的情感、哪些是消极批判的情感。下面先来看看数据(见图20-6)。
One of the very best Three Stooges shorts ever. A spooky house full of evil guys and “The Goon” challenge the Alert Detective Agency’s best men. Shemp is in top form in the famous in-the-dark scene. Emil Sitka provides excellent support in his Mr. Goodrich role,as the target of a murder plot. Before it’s over,Shemp’s “trusty little shovel” is employed to great effect. This 16 minute gem moves about as fast as any Stooge’s short and packs twice the wallop. Highly recommended.
图20-6 影评数据分类任务
这就是其中一条影评数据,由于英文数据本身以空格为分隔符,所以直接处理词语即可。但是这里有一个问题——如何构建文本特征呢?如果直接利用词袋模型或者TF-IDF方法计算整个文本向量,很难得到比较好的效果,因为一篇文章实在太长。
另一个问题就是RNN网络的输入要求是什么?在原理讲解中已经指出,需要把整个句子分解成一个个词语,因此每一个词就是一个输入,即x0,x1,…,xt。所以需要考虑每一个词的特征表示。
在数据处理阶段,一定要弄清楚最终网络需要的输入是什么,按照这个方向去处理数据。
20.2.1词向量
特征一直是机器学习中的难点,为了使得整个模型效果更好,必须要把词的特征表示做好,也就是词向量。
如图20-7所示,每一个词都需要转换成相应的特征向量,而且维度必须一致,关于词向量的组成,可不是简单的词频统计,而是需要有实际的含义。
图20-7 词向量的组成
如果基于统计的方法来制作向量,love和adore是两个完全不同的向量,因为统计的方法很难考虑词语本身以及上下文的含义,如图20-8所示。如果用词向量模型(word2vec)来制作,结果就大不相同。
图20-9为词向量的特征空间意义。相似的词语在向量空间上也会非常类似,这才是希望得到的结果。所以,当拿到文本数据之后,第一步要对语料库进行词向量建模,以得到每一个词的向量。
图20-8 词向量的意义
图20-9 词向量的特征空间意义
由于训练词向量的工作量很大,在很多通用任务中,例如常见的新闻数据、影评数据等,都可以直接使用前人用大规模语料库训练之后的结果。因为此时希望得到每一个词的向量,肯定是预料越丰富,得到的结果越好(见图20-10)。
图20-10 词向量制作
词语能通用的原因在于,语言本身就是可以跨内容使用的,这篇文章中使用的每一个词语的含义换到下一篇文章中基本不会发生变化。但是,如果你的任务是专门针对某一领域,例如医学实验,这里面肯定会有大量的专有名词,此时就需要单独训练词向量模型来解决专门问题。
接下来简单介绍一下词向量的基本原理,也就是Word2Vec模型,在自然语言处理中经常用到这个模型,其目的就是得到各个词的向量表示。
Word2Vec模型如图20-11所示,整体的结构还是神经网络,只不过此时要训练的不仅是网络的权重参数,还有输入数据。首先对每个词进行向量初始化,例如随机创建一个300维的向量。在训练过程中,既可以根
据上下文预测某一个中间词,例如文本是:今天天气不错,上下文就是今天不错,预测结果为:天气,如图20-11(a)所示;也可以由一个词去预测其上下文结果。最终通过神经网络不断迭代,以训练出每一个词向量结果。
图20-11 Word2Vec模型
在word2vec模型中,每一次迭代更新,输入的词向量都会发生变化,相当于既更新网络权重参数,也更新输入数据。
关于词向量的建模方法,Gensim工具包中已经给出了非常不错的文档教程,如果要亲自动手创建一份词向量,可以参考其使用方法,只需先将数据进行分词,然后把分词后的语料库传给Word2Vec函数即可,方法还是非常简单的。
1 from gensim.models.word2vec import Word2Vec 2 model=Word2Vec(sentences_list,works=num_workers,size=num_features,min_count=min_word_count,window=context)
使用时,需要指定好每一个参数值。
- sentences:分好词的语料库,可以是一个list。
- sg:用于设置训练算法,默认为0,对应CBOW算法;sg=1则采用skip-gram算法。
- size:是指特征向量的维度,默认为100。大的size需要更多的训练数据,但是效果会更好,推荐值为几十到几百。
- window:表示当前词与预测词在一个句子中的最大距离是多少。
- alpha:是学习速率。
- seed:用于随机数发生器,与初始化词向量有关。
- min_count:可以对字典做截断,词频少于min_count次数的单词会被丢弃掉,默认值为5。
- max_vocab_size:设置词向量构建期间的RAM限制。如果所有独立单词个数超过这个,则就消除掉其中最不频繁的一个。每1000万个单词需要大约1GB的RAM。设置成None,则没有限制。
- workers:控制训练的并行数。
- hs:如果为1,则会采用hierarchica softmax技巧。如果设置为0(defaut),则negative sampling会被使用。
- negative:如果>0,则会采用negative samping,用于设置多少个noise words。
- iter:迭代次数,默认为5。
训练完成后得到的词向量如图20-12所示,基本上都是较小的数值,其含义如同降维得到的结果,还是很难进行解释。
图20-12 词向量结果
制作好词向量之后,还可以动手试试其效果,看一下到底有没有空间中的实际含义:
通过实验结果可以看出,使用语料库训练得到的词向量确实有着实际含义,并且具有相同含义的词在特征空间中是非常接近的。关于词向量的维度,通常情况下,50~300维比较常见,谷歌官方给出的word2vec模型的词向量是300维,能解决绝大多数任务。
20.2.2数据特征制作
影评数据集中涉及的词语都是常见词,所以完全可以利用前人训练好的词向量模型,英文数据集中有很多训练好的结果,最常用的就是谷歌官方给出的词向量结果,但是,它的词向量是300维度,也就是说,在RNN模型中,每一次输入的数据都是300维的,如果大家用笔记本电脑来跑程序会比较慢,所以这里选择另外一份词向量结果,每个词只有50维特征,一共包含40万个常用词。
1 import numpy as np 2 #读取词数据集 3 wordsList = np.load('./training_data/wordsList.npy') 4 print('Loaded the word list!') 5 #已经训练好的词向量模型 6 wordsList = wordsList.tolist() 7 #给定相应格式 8 wordsList = [word.decode('UTF-8') for word in wordsList] 9 #读取词向量数据集 10 wordVectors = np.load('./training_data/wordVectors.npy') 11 print ('Loaded the word vectors!') 12 13 print(len(wordsList)) 14 print(wordVectors.shape)
400000
(400000, 50)
关于词向量的制作,也可以自己用Gensim工具包训练,如果大家想处理一份300维的特征数据,不妨自己训练一番,文本数据较少时,很快就能得到各个词的向量表示。
如果大家想看看词向量的模样,可以实际传入一些单词试一试:
1 baseballIndex = wordsList.index('baseball') 2 wordVectors[baseballIndex]
array([-1.9327 , 1.0421 , -0.78515 , 0.91033 , 0.22711 , -0.62158 , -1.6493 , 0.07686 , -0.5868 , 0.058831, 0.35628 , 0.68916 , -0.50598 , 0.70473 , 1.2664 , -0.40031 , -0.020687, 0.80863 , -0.90566 , -0.074054, -0.87675 , -0.6291 , -0.12685 , 0.11524 , -0.55685 , -1.6826 , -0.26291 , 0.22632 , 0.713 , -1.0828 , 2.1231 , 0.49869 , 0.066711, -0.48226 , -0.17897 , 0.47699 , 0.16384 , 0.16537 , -0.11506 , -0.15962 , -0.94926 , -0.42833 , -0.59457 , 1.3566 , -0.27506 , 0.19918 , -0.36008 , 0.55667 , -0.70315 , 0.17157 ], dtype=float32)
上述代码返回的结果就是一个50维的向量,其中每一个数值的含义根本理解不了,但是计算机却能看懂它们的整体含义。
现在已经有各个词的向量,但是手里拿到的是一篇文章,需要对应地找到其各个词的向量,然后再组合在一起,先来整体看一下流程,如图20-13所示。
图20-13 词向量读取
由图可见,先得到一句话,然后取其在词库中的对应索引位置,再对照词向量表转换成相应的结果,例如输入10个词,最终得到的结果就是[10,50],表示每个词都转换成其对应的向量。
Embedding Matrix表示整体的词向量大表,要在其中寻找所需的结果,TensorFlow提供了一个非常便捷的函数tf.nn.embedding_lookup(),可以快速完成查找工作,如果任务与自然语言处理相关,那会经常用到这个函数。
整体流程看起来有点麻烦,其实就是对照输入中的每一个词将其转换成相应的词向量即可,在数据量较少时,也可以用字典的方法查找替换,但是,当数据量与词向量矩阵都较大时,最好使用embedding_lookup()函数,速度起码快一个数量级。
在将所有影评数据替换为词向量之前,需要考虑不同的影评数据长短不一所导致的问题,要不要规范它们?
1 import tensorflow as tf
2 #可以设置文章的最大词数来限制
3 maxSeqLength = 10
4 #每个单词的最大维度
5 numDimensions = 300
6 firstSentence = np.zeros((maxSeqLength), dtype='int32')
7 firstSentence[0] = wordsList.index("i")
8 firstSentence[1] = wordsList.index("thought")
9 firstSentence[2] = wordsList.index("the")
10 firstSentence[3] = wordsList.index("movie")
11 firstSentence[4] = wordsList.index("was")
12 firstSentence[5] = wordsList.index("incredible")
13 firstSentence[6] = wordsList.index("and")
14 firstSentence[7] = wordsList.index("inspiring")
15 #如果长度没达到设置的标准,用0来占位
16 print(firstSentence.shape)
17 #结果
18 print(firstSentence)
19
20 with tf.Session() as sess:
21 print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape)
修正代码为:
1 with tf.compat.v1.Session() as sess: 2 print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape) 3 4 # AttributeError: module 'tensorflow' has no attribute 'Session' 5 # with tf.Session() as sess: 6 # print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape)
(10,)
[ 41 804 201534 1005 15 7446 5 13767 0 0]
对一篇影评数据来说,首先找到其对应索引位置(之后要通过索引得到其对应的词向量结果),再利用embedding_lookup()函数就能得到其词向量结果,其中wordVectors是制作好的词向量库,firstSentence就是要寻找的词向量的这句话。(10,50)表示将10个单词转换成对应的词向量结果。
这里需要注意,之后设计的RNN网络必须适用于所有文章。例如一篇文章的长度是200(x1,x2,…,x200),另一篇是300(x1,x2,…,x300),此时输入数据大小不一致,这是根本不行的,在网络训练中,必须保证结构是一样的(这是全连接操作的前提)。
此时需要对文本数据进行预处理操作,基本思想就是选择一个合适的值来限制文本的长度,例如选250(需要根据实际任务来选择)。如果一篇影评数据中词语数量比250多,那就从第250个词开始截断,后面的就不需要了;少于250个词的,缺失部分全部用0来填充即可。
影评数据一共包括25000篇评论,其中消极和积极的数据各占一半,之前说到需要定义一个合适的篇幅长度来设计RNN网络结构,这里先来统计一下每篇文章的平均长度,由于数据存储在不同文件夹中,所以需要分别读取不同类别中的每一条影评数据(见图20-14)。
图20-14 数据存储格式
1 from os import listdir 2 from os.path import isfile, join 3 #指定好数据集位置,由于提供的数据都一个个单独的文件,所以还得一个个读取 4 positiveFiles = ['./training_data/positiveReviews/' + f for f in listdir('./training_data/positiveReviews/') if isfile(join('./training_data/positiveReviews/', f))] 5 negativeFiles = ['./training_data/negativeReviews/' + f for f in listdir('./training_data/negativeReviews/') if isfile(join('./training_data/negativeReviews/', f))] 6 numWords = [] 7 #分别统计积极和消极情感数据集 8 for pf in positiveFiles: 9 with open(pf, "r", encoding='utf-8') as f: 10 line=f.readline() 11 counter = len(line.split()) 12 numWords.append(counter) 13 print('情感积极数据集加载完毕') 14 15 for nf in negativeFiles: 16 with open(nf, "r", encoding='utf-8') as f: 17 line=f.readline() 18 counter = len(line.split()) 19 numWords.append(counter) 20 print('情感消极数据集加载完毕') 21 22 numFiles = len(numWords) 23 print('总共文件数量', numFiles) 24 print('全部词语数量', sum(numWords)) 25 print('平均每篇评论词语数量', sum(numWords)/len(numWords)
情感积极数据集加载完毕 情感消极数据集加载完毕 总共文件数量 25000 全部词语数量 5844680 平均每篇评论词语数量 233.7872
可以将平均长度233当作RNN中序列的长度,最好还是绘图观察其分布情况:
1 import matplotlib.pyplot as plt 2 %matplotlib inline 3 plt.hist(numWords, 50) 4 plt.xlabel('Sequence Length') 5 plt.ylabel('Frequency') 6 plt.axis([0, 1200, 0, 8000]) 7 plt.show()
从整体上观察,绝大多数评论的长度都在300以内,所以暂时设置RNN序列长度为250没有问题,这也可以当作是整体模型的一个参数,大家也可以用实验来对比不同长度对结果的影响。
1 maxSeqLength = 250 2 3 #随便哪一篇评论来看看结果 4 fname = positiveFiles[3] 5 with open(fname) as f: 6 for lines in f: 7 print(lines) 8 exit 9 10 # 删除标点符号、括号、问号等,只留下字母数字字符 11 import re 12 strip_special_chars = re.compile("[^A-Za-z0-9 ]+") 13 14 def cleanSentences(string): 15 string = string.lower().replace("<br />", " ") 16 return re.sub(strip_special_chars, "", string.lower()) 17 18 firstFile = np.zeros((maxSeqLength), dtype='int32') 19 with open(fname) as f: 20 indexCounter = 0 21 line=f.readline() 22 cleanedLine = cleanSentences(line) 23 split = cleanedLine.split() 24 for word in split: 25 try: 26 firstFile[indexCounter] = wordsList.index(word) 27 except ValueError: 28 firstFile[indexCounter] = 399999 #Vector for unknown words 29 indexCounter = indexCounter + 1 30 firstFile
上述输出就是对文章截断后的结果,长度不够的时候,指定0进行填充。接下来是一个非常耗时间的过程,需要先把所有文章中的每一个词转换成对应的索引,然后再把这些矩阵的结果返回。
如果大家的笔记本电脑性能一般,可能要等上大半天,这里直接给出一份转换结果,实验的时候,可以直接读取转换好的矩阵:
# ids = np.zeros((numFiles, maxSeqLength), dtype='int32') # fileCounter = 0 # for pf in positiveFiles: # with open(pf, "r") as f: # indexCounter = 0 # line=f.readline() # cleanedLine = cleanSentences(line) # split = cleanedLine.split() # for word in split: # try: # ids[fileCounter][indexCounter] = wordsList.index(word) # except ValueError: # ids[fileCounter][indexCounter] = 399999 #Vector for unkown words # indexCounter = indexCounter + 1 # if indexCounter >= maxSeqLength: # break # fileCounter = fileCounter + 1 # for nf in negativeFiles: # with open(nf, "r") as f: # indexCounter = 0 # line=f.readline() # cleanedLine = cleanSentences(line) # split = cleanedLine.split() # for word in split: # try: # ids[fileCounter][indexCounter] = wordsList.index(word) # except ValueError: # ids[fileCounter][indexCounter] = 399999 #Vector for unkown words # indexCounter = indexCounter + 1 # if indexCounter >= maxSeqLength: # break # fileCounter = fileCounter + 1 # #Pass into embedding function and see if it evaluates. # np.save('idsMatrix', ids)
1 ids = np.load('./training_data/idsMatrix.npy')
在RNN网络进行迭代的时候,需要指定每一次传入的batch数据,这里先做好数据的选择方式,方便之后在网络中传入数据。
1 from random import randint 2 # 制作batch数据,通过数据集索引位置来设置训练集和测试集 3 #并且让batch中正负样本各占一半,同时给定其当前标签 4 def getTrainBatch(): 5 labels = [] 6 arr = np.zeros([batchSize, maxSeqLength]) 7 for i in range(batchSize): 8 if (i % 2 == 0): 9 num = randint(1,11499) 10 labels.append([1,0]) 11 else: 12 num = randint(13499,24999) 13 labels.append([0,1]) 14 arr[i] = ids[num-1:num] 15 return arr, labels 16 17 def getTestBatch(): 18 labels = [] 19 arr = np.zeros([batchSize, maxSeqLength]) 20 for i in range(batchSize): 21 num = randint(11499,13499) 22 if (num <= 12499): 23 labels.append([1,0]) 24 else: 25 labels.append([0,1]) 26 arr[i] = ids[num-1:num] 27 return arr, labels
构造好batch数据后,数据和标签就确定了。
图20-15所示为数据最终预处理后的结果,构建RNN模型的时候,还需再将词索引转换成对应的向量。现在再向大家强调一下输入数据的格式,传入RNN网络中的数据需是一个三维的形式,即[batchsize,文本长度,词向量维度],例如一次迭代训练10个样本数据,每个样本长度为250,每个词的向量维度为50,输入就是[10,250,50]。
图20-15 数据预处理结果
在数据预处理时,最好的方法就是先倒着来思考,想一想最终网络模型要求输入什么,然后对照目标进行预处理和特征提取。
20.3构建RNN模型
邀月:本篇以下示例全部基于tensorflow 1.15.2版本运行,最初原想用tensorflow tensorflow-2.1.0运行,折腾两个晚上,放弃了。
https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md 看这里,google的版本什么时候都会给人惊喜和绝望。
/* D:\tools\Python37>pip install tensorflow==1.15.2 Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple Collecting tensorflow==1.15.2 ........ Installing collected packages: tensorboard, tensorflow-estimator, tensorflow Attempting uninstall: tensorboard Found existing installation: tensorboard 2.1.1 Uninstalling tensorboard-2.1.1: Successfully uninstalled tensorboard-2.1.1 Attempting uninstall: tensorflow-estimator Found existing installation: tensorflow-estimator 2.1.0 Uninstalling tensorflow-estimator-2.1.0: Successfully uninstalled tensorflow-estimator-2.1.0 Successfully installed tensorboard-1.15.0 tensorflow-1.15.2 tensorflow-estimator-1.15.1 */
首先需要设置模型所需参数,在RNN网络中,其基本计算方式还是全连接,所以需要指定隐层神经元数量:
- batchSize=24
- lstmUnits=64
- numClasses=2
- iterations=50000
其中,batchSize可以根据自己机器性能来选择,如果觉得迭代过程有些慢,可以再降低一些;lstmUnits表示其中每一个隐层的神经元数量;numClasses就是最终要得到的输出结果,也就是一个二分类问题;在迭代过程中,iterations就是最大迭代次数。
网络模型的搭建方法都是相同的,还是先指定输入数据的格式,然后定义RNN网络结构训练迭代:
1 batchSize = 24 2 lstmUnits = 64 3 numClasses = 2 4 iterations = 50000
1 import tensorflow as tf 2 tf.reset_default_graph() 3 4 labels = tf.placeholder(tf.float32, [batchSize, numClasses]) 5 input_data = tf.placeholder(tf.int32, [batchSize, maxSeqLength])
依旧用placeholder()进行占位,此时只得到二维的结果,即[batchSize,maxSeqLength],还需将文本中每一个词由其索引转换成相应的词向量。
1 data = tf.Variable(tf.zeros([batchSize, maxSeqLength, numDimensions]),dtype=tf.float32) 2 data = tf.nn.embedding_lookup(wordVectors,input_data)
使用embedding_lookup函数完成最后的词向量读取转换工作,就搞定了输入数据,大家在建模时,一定要清楚[batchSize,maxSeqLength,numDimensions]这三个维度的含义,不能只会调用工具包函数,还需要理解其中细节。
构建LSTM网络模型,需要分几步走:
1 lstmCell = tf.contrib.rnn.BasicLSTMCell(lstmUnits) 2 lstmCell = tf.contrib.rnn.DropoutWrapper(cell=lstmCell, output_keep_prob=0.75) 3 value, _ = tf.nn.dynamic_rnn(lstmCell, data, dtype=tf.float32)
WARNING:tensorflow: The TensorFlow contrib module will not be included in TensorFlow 2.0. For more information, please see: * https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md * https://github.com/tensorflow/addons * https://github.com/tensorflow/io (for I/O related ops) If you depend on functionality not listed there, please file an issue. WARNING:tensorflow:From <ipython-input-17-db6a6fc2c55e>:1: BasicLSTMCell.__init__ (from tensorflow.python.ops.rnn_cell_impl) is deprecated and will be removed in a future version. Instructions for updating: This class is equivalent as tf.keras.layers.LSTMCell, and will be replaced by that in Tensorflow 2.0. WARNING:tensorflow:From <ipython-input-17-db6a6fc2c55e>:3: dynamic_rnn (from tensorflow.python.ops.rnn) is deprecated and will be removed in a future version. Instructions for updating: Please use `keras.layers.RNN(cell)`, which is equivalent to this API WARNING:tensorflow:From d:\tools\python37\lib\site-packages\tensorflow_core\python\ops\rnn_cell_impl.py:735: Layer.add_variable (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version. Instructions for updating: Please use `layer.add_weight` method instead. WARNING:tensorflow:From d:\tools\python37\lib\site-packages\tensorflow_core\python\ops\rnn_cell_impl.py:739: calling Zeros.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version. Instructions for updating: Call initializer instance with the dtype argument instead of passing it to the constructor
首先创建基本的LSTM单元,也就是每一个输入走的网络结构都是相同的,再把这些基本单元和输入的序列数据组合起来,还可以加入Dropout功能。关于RNN网络,还有很多种创建方法,这些在TensorFlow官网中都有实例说明,用的时候最好先参考一下其API文档。
1 #权重参数初始化 2 weight = tf.Variable(tf.truncated_normal([lstmUnits, numClasses])) 3 bias = tf.Variable(tf.constant(0.1, shape=[numClasses])) 4 value = tf.transpose(value, [1, 0, 2]) 5 #取最终的结果值 6 last = tf.gather(value, int(value.get_shape()[0]) - 1) 7 prediction = (tf.matmul(last, weight) + bias)
RNN网络的权重参数初始化方法与传统神经网络一致,都是全连接的操作,需要注意网络输出会有多个结果,可以参考图20-1,每一个输入的词向量都与当前输出结果相对应,最终选择最后一个词所对应的结果,并且通过一层全连接操作转换成对应的分类结果。
网络模型和输入数据确定后,接下来与之前训练方法一致,给定损失函数和优化器,然后迭代求解即可:
1 correctPred = tf.equal(tf.argmax(prediction,1), tf.argmax(labels,1)) 2 accuracy = tf.reduce_mean(tf.cast(correctPred, tf.float32)) 3 4 5 loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=labels)) 6 optimizer = tf.train.AdamOptimizer().minimize(loss) 7 8 sess = tf.InteractiveSession() 9 saver = tf.train.Saver() 10 sess.run(tf.global_variables_initializer()) 11 12 for i in range(iterations): 13 #之前已经定义好的拿到batch数据函数 14 nextBatch, nextBatchLabels = getTrainBatch(); 15 sess.run(optimizer, {input_data: nextBatch, labels: nextBatchLabels}) 16 #每隔1000次打印一下当前的结果 17 if (i % 1000 == 0 and i != 0): 18 loss_ = sess.run(loss, {input_data: nextBatch, labels: nextBatchLabels}) 19 accuracy_ = sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels}) 20 21 print("iteration {}/{}...".format(i+1, iterations), 22 "loss {}...".format(loss_), 23 "accuracy {}...".format(accuracy_)) 24 #每个1W次保存一下当前模型 25 if (i % 10000 == 0 and i != 0): 26 save_path = saver.save(sess, "models/pretrained_lstm.ckpt", global_step=i) 27 print("saved to %s" % save_path)
这里不仅打印当前迭代结果,每隔1万次还会保存当前的网络模型。TensorFlow中保存模型最简单的方法,就是用saver.save()函数指定保存的模型,以及保存的路径。保存好训练的权重参数,当预测任务来临时,直接读取模型即可。
可能有同学会问,为什么不能只保存最后一次的结果?由于网络在训练过程中,其效果可能发生浮动变化,而且不一定迭代次数越多,效果就越好,可能第3万次的效果要比第5万次的还要强,因此需要保存中间结果。
训练网络需要耐心,这份数据集中,由于给定的网络结构和词向量维度都比较小,所以训练起来很快:
iteration 1001/50000... loss 0.5977783799171448... accuracy 0.5... iteration 2001/50000... loss 0.6750070452690125... accuracy 0.5833333134651184... iteration 3001/50000... loss 0.6415902972221375... accuracy 0.3333333432674408... iteration 4001/50000... loss 0.6623604893684387... accuracy 0.5416666865348816... iteration 5001/50000... loss 0.6792729496955872... accuracy 0.5833333134651184... iteration 6001/50000... loss 0.6929864883422852... accuracy 0.5... iteration 7001/50000... loss 0.6421437859535217... accuracy 0.5416666865348816... iteration 8001/50000... loss 0.6045424938201904... accuracy 0.7083333134651184... iteration 9001/50000... loss 0.49526092410087585... accuracy 0.875... iteration 10001/50000... loss 0.23377956449985504... accuracy 0.9166666865348816... saved to models/pretrained_lstm.ckpt-10000 iteration 11001/50000... loss 0.22206246852874756... accuracy 0.9166666865348816... iteration 12001/50000... loss 0.49464142322540283... accuracy 0.7916666865348816... iteration 13001/50000... loss 0.26161226630210876... accuracy 0.9166666865348816... iteration 14001/50000... loss 0.3195655047893524... accuracy 0.9166666865348816... iteration 15001/50000... loss 0.2544494867324829... accuracy 0.7916666865348816... iteration 16001/50000... loss 0.4504941403865814... accuracy 0.7916666865348816... iteration 17001/50000... loss 0.14206933975219727... accuracy 0.9583333134651184... iteration 18001/50000... loss 0.28434768319129944... accuracy 0.875... iteration 19001/50000... loss 0.2196163386106491... accuracy 0.875... iteration 20001/50000... loss 0.1411515176296234... accuracy 0.9166666865348816... saved to models/pretrained_lstm.ckpt-20000 iteration 21001/50000... loss 0.10852870345115662... accuracy 0.9166666865348816... iteration 22001/50000... loss 0.17102549970149994... accuracy 0.875... iteration 23001/50000... loss 0.2163759469985962... accuracy 0.9583333134651184... iteration 24001/50000... loss 0.40744829177856445... accuracy 0.9583333134651184... iteration 25001/50000... loss 0.12172506004571915... accuracy 0.875... iteration 26001/50000... loss 0.22618673741817474... accuracy 0.9166666865348816... iteration 27001/50000... loss 0.29675474762916565... accuracy 0.9583333134651184... iteration 28001/50000... loss 0.028712689876556396... accuracy 1.0... iteration 29001/50000... loss 0.06705771386623383... accuracy 0.9583333134651184... iteration 30001/50000... loss 0.06020817533135414... accuracy 1.0... saved to models/pretrained_lstm.ckpt-30000 iteration 31001/50000... loss 0.053414225578308105... accuracy 1.0... iteration 32001/50000... loss 0.018836012110114098... accuracy 1.0... iteration 33001/50000... loss 0.22417615354061127... accuracy 0.9583333134651184... iteration 34001/50000... loss 0.11704185605049133... accuracy 0.9583333134651184... iteration 35001/50000... loss 0.01906798779964447... accuracy 1.0... iteration 36001/50000... loss 0.11806797981262207... accuracy 0.9166666865348816... iteration 37001/50000... loss 0.03285054862499237... accuracy 1.0... iteration 38001/50000... loss 0.10801851749420166... accuracy 0.9166666865348816... iteration 39001/50000... loss 0.026212451979517937... accuracy 1.0... iteration 40001/50000... loss 0.047293175011873245... accuracy 1.0... saved to models/pretrained_lstm.ckpt-40000 iteration 41001/50000... loss 0.015445593744516373... accuracy 1.0... iteration 42001/50000... loss 0.06675603985786438... accuracy 0.9583333134651184... iteration 43001/50000... loss 0.026207834482192993... accuracy 1.0... iteration 44001/50000... loss 0.012909072451293468... accuracy 1.0... iteration 45001/50000... loss 0.019438063725829124... accuracy 1.0... iteration 46001/50000... loss 0.01888447254896164... accuracy 1.0... iteration 47001/50000... loss 0.010169420391321182... accuracy 1.0... iteration 48001/50000... loss 0.04857032373547554... accuracy 0.9583333134651184... iteration 49001/50000... loss 0.009694952517747879... accuracy 1.0...
随着网络迭代的进行,模型也越来越收敛,基本上2万次就能够达到完美的效果,但是不要高兴得太早,这只是训练集的结果,还要看测试集上的效果。
如图20-16、图20-17所示,虽然只用了非常简单的LSTM结构,收敛效果还是不错的,其实最终模型的效果在很大程度上还是与输入数据有关,如果不使用词向量模型,训练的效果可能就要大打折扣。
▲图20-16 训练时准备率变化情况
▲图20-17 训练时损失变化情况
接下来再看看测试的效果,这里先给大家演示一下如何加载已经保存好的模型:
1 sess = tf.InteractiveSession() 2 saver = tf.train.Saver() 3 saver.restore(sess, tf.train.latest_checkpoint('models'))
这里加载的是最后保存的模型,当然也可以指定具体的名字来加载指定的模型文件,读取的就是之前训练网络时候所得到的各个权重参数,接下来只需要在batch里面传入实际的测试数据集即可:
1 iterations = 10 2 for i in range(iterations): 3 nextBatch, nextBatchLabels = getTestBatch(); 4 print("Accuracy for this batch:", (sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})) * 100)
Accuracy for this batch: 87.5 Accuracy for this batch: 79.16666865348816 Accuracy for this batch: 83.33333134651184 Accuracy for this batch: 83.33333134651184 Accuracy for this batch: 91.66666865348816 Accuracy for this batch: 83.33333134651184 Accuracy for this batch: 75.0 Accuracy for this batch: 87.5 Accuracy for this batch: 91.66666865348816 Accuracy for this batch: 91.66666865348816
为了使测试效果更稳定,选择10个batch数据,在二分类任务中,得到的结果只能说整体还凑合,可以明显发现网络模型已经有些过拟合。在训练数据集中,基本都是100%,然而实际测试时却有所折扣。大家在实验的时候,也可以尝试改变其中的参数,以调节网络模型,再对比最终的结果。
在神经网络训练过程中,可以调节的细节比较多,通常都是先调整学习率,导致过拟合最可能的原因就是学习率过大。网络结构与输出数据也会对结果产生影响,这些都需要通过大量的实验进行对比观察。
项目小结:
该章从整体上介绍了RNN网络结构及其升级版本LSTM网络,针对自然语言处理,其实很大程度上拼的是如何进行特征构造,词向量模型可以说是当下最好的解决方案之一,对词的维度进行建模要比整体文章建模更实用。针对影评数据集,首先进行数据格式处理,这也是按照后续网络模型的要求输入的,TensorFlow当中有很多便捷的API可以完成处理任务,例如常用的embedding_lookup(),至于其具体用法,官网的解释肯定是最好的,所以千万不要忽视最直接的资源。在处理序列数据上,RNN网络结构有着先天的优势,所以,其在文本处理任务上,尤其涉及上下文和序列相关任务的时候,还是尽可能优先选择深度学习算法,虽然速度要慢一些,但是整体效果还不错。
20章的机器学习算法与实战的学习到这里就结束了,其中经历了数学推导的考验与案例中反复的实验,相信大家已经掌握了机器学习的核心思想与实践方法。算法本身并没有高低之分,很多时候拼的是如何对数据进行合适的特征提取,结合特征工程,将最合适的算法应用到最适合的数据中才是上策。学习应当是反复的过程,每一次都会有更深的理解,机器学习算法本身较为复杂,时常复习也是必不可缺的。案例的利用也是如此,光看不练终归不是自己的,举一反三才能提升自己的实战技能。在后续的学习和工作中,根据业务需求,还可以结合实际论文来探讨解决方案,善用资料,加以理解,并应用到自己的任务中,才是最佳的提升路线。
第20章完。全书完
该书资源下载,请至异步社区:https://www.epubit.com