赞
踩
深度学习技术发展到今天,在图像、语音、自然语言处理(natural language processing,NLP)领域有很多的应用。由于人类语言的多样性、多意性,使得NLP的难度成倍增加。例如由相同的三个字形成的组合“不怕辣”、“辣不怕”、“怕不辣”、“怕辣不”表达了不同的含义。有些话还要结合当时的语境进行理解,否则得到的结果谬之千里,比如:“中国乒乓球谁也打不过”、“中国足球谁也打不过”。本文结合实际案例,对NLP的整体流程进行学习,在此记录,便于查阅。
自然语言处理是教会机器如何去处理或者读懂人类语言的系统,主要应用领域:
为什么需要这个模型?那是因为计算机只认识数字!必须要把输入的句子转换成计算机可识别的数字。
我们可以将一句话中的每一个词都转换成一个向量:
你可以将输入数据看成是一个16*D的矩阵。
词向量是具有空间意义的,并不是简单的映射。例,我们希望单词“love”和“adore”这两个词在向量空间中是有一定的相关性的,因为他们有类似的定义,他们都在类似的上下文中使用。单词的向量表示也被称之为词嵌入(把词映射到空间中,做个空间表达)。
为了去得到这些词嵌入,我们使用一个非常厉害的模型“Word2Vec”。简单的说,这个模型根据上下文的语境来推断出每个词的词向量。如果两个词在上下文的语境中,可以被互相替换,那么这两个词的距离就非常近。在自然语言中,上下文的语境对分析词语的意义是非常重要的。比如,之前提到的“adore”和“love”这两个词,我们观察如下上下文的语境。
从句子中我们可以看到,这两个词通常在句子中是表现积极的,而且一般比名词或者名词组合要好。这也说明了,这两个词可以被互相替换,他们的意思是非常相近的。对于句子的语法结构分析,上下文语境也是非常重要的。所以,这个模型的作用就是从一大堆句子(以Wikipedia为例)中为每个独一无二的单词进行建模,并且输出一个唯一的向量。Word2Vec模型的输出被称为一个嵌入矩阵。
这个嵌入矩阵包含训练集中每个词的一个向量。传统来讲,这个嵌入矩阵中的词向量数据会很大。
Word2Vec模型根据数据集中的每个句子进行训练,并且以一个固定窗口在句子上进行滑动,根据句子的上下文来预测固定窗口中间那个词的向量。然后根据一个损失函数和优化方法,来对这个模型进行训练。
现在,我们已经得到了神经网络的输入数据——词向量,接下来让我们看看需要构建的神经网络。NLP数据的一个独特之处是它是时间序列数据。每个单词的出现都依赖于它的前一个单词和后一个单词。由于这种依赖的存在,我们使用循环神经网络来处理这种时间序列数据。
循环神经网络的结构和你之前看到的那些前馈神经网络的结构可能有一些不一样。前馈神经网络由三部分组成,输入层、隐藏层和输出层。
前馈神经网络和RNN之前的主要区别就是RNN考虑了时间的信息。在RNN中,句子中的每个单词都被考虑上了时间步骤。实际上,时间步长的数量将等于最大序列长度。
与每个时间步骤相关联的中间状态也被作为一个新的组件,称为隐藏状态向量h(t)。从抽象的角度来看,这个向量是用来封装和汇总前面时间步骤中所看到的所有信息。就像x(t)表示一个向量,它封装了一个特定单词的所有信息。
隐藏状态是当前单词向量和前一步的隐藏状态向量的函数。并且这两项之和需要通过激活函数来进行激活。
长短期记忆网络单元,是另一个RNN中的模块。从抽象的角度看,LSTM保存了文本中长期的依赖信息。正如我们前面所看到的,H在传统的RNN网络中是非常简单的,这种简单结构不能有效的将历史信息链接在一起。举个例子,在问答领域中,假设我们得到如下一段文本,那么LSTM就可以很好的将历史信息进行记录学习。
在这里,我们看到中间的句子对被问的问题没有影响。然而,第一句和第三句之间有很强的联系。对于一个典型的RNN网络,隐藏状态向量对于第二句的存储信息量可能比第一句的信息量会大很多。但是LSTM,基本上就会判断哪些信息是有用的,哪些是没用的,并且把有用的信息在LSTM中进行保存。
我们从更加技术的角度来谈谈LSTM单元,该单元根据输入数据x(t),隐藏层输出h(t)。在这些单元中,h(t)的表达形式比经典的RNN网络会复杂很多。这些复杂组件分为四个部分:输入们、输出门、遗忘门和一个记忆控制器。
每个门都将x(t)和h(t-1)作为输入(没有在图中显示出来),并且利用这些输入来计算一些中间状态。每个中间状态都会被送入不同的管道,并且这些信息最终会汇集到h(t)。为了简单起见,我们不会关心每一个门的具体推导。这些门可以被认为是不同的模块,各有不同的功能。Ct是控制参数,控制什么样的值保留,什么样的值舍弃,输入门决定在每个输入上施加多少强调,遗忘门决定我们将丢弃什么信息,输出门根据中间状态来决定最终的h(t)。
首先,我们需要去创建词向量。为了简单起见,我们使用训练好的模型来创建。
作为该领域的一个最大玩家,Google已经帮我们在大规模数据集上训练出来了Word2Vec模型,包括1000亿个不同的词!在这个模型中,谷歌能创建300万个词向量,每个向量维度为300。
在理想情况下,我们将使用这些向量来构建模型,但是因为这个单词向量矩阵想当大(3.6G),我们使用另外一个现成的小一些的,该矩阵由GloVe进行训练得到。矩阵将包含400000个词向量,每个向量的维数为50。
我们将导入两个不同的数据结构,一个是包含400000个单词的Python列表,一个是包含所有单词向量值的400000*50维的嵌入矩阵。
- import numpy as np
- wordsList = np.load('./training_data/wordsList.npy')
- print('Loaded the word list!')
- wordsList = wordsList.tolist() # Originally loaded as numpy array
- wordsList = [word.decode('UTF-8') for word in wordsList] # Encode words as UTF-8
- wordVectors = np.load('./training_data/wordVectors.npy')
- print('Loaded the word vectors!')
输出:
- print(len(wordsList)) # 词映射
- print(wordVectors.shape) # 实际词向量
输出:
我们也可以在词库中搜索单词,比如“baseball”,然后可以通过访问嵌入矩阵来得到相应的向量,如下:
- baseballIndex = wordsList.index('baseball')
- wordVectors[baseballIndex]
输出:
现在我们有了向量,我们的第一步就是输入一个句子,然后构造它的向量表示。假如我们现在的输入句子是“I thought the movie was incredible and inspiring”。为了得到词向量,我们可以使用TensorFlow的嵌入函数。这个函数有两个参数,一个是嵌入矩阵(在我们的情况下是词向量矩阵),另一个是每个词对应的索引。
- import tensorflow as tf
- maxSeqLength = 10 # Maximum length of sentence
- numDimensions = 300 # Dimensions for each word vector
- firstSentence = np.zeros((maxSeqLength), dtype='int32')
- firstSentence[0] = wordsList.index('i')
- firstSentence[1] = wordsList.index('thought')
- firstSentence[2] = wordsList.index('the')
- firstSentence[3] = wordsList.index('movie')
- firstSentence[4] = wordsList.index('was')
- firstSentence[5] = wordsList.index('incredible')
- firstSentence[6] = wordsList.index('and')
- firstSentence[7] = wordsList.index('inspiring')
- # firstSentence[8] and firstSentence[9] are going to be 0
- print(firstSentence.shape)
- print(firstSentence) # Shows the row index for each word
输出:
数据管道如下图所示:
输出数据是一个10*50的词矩阵,其中包括10个词,每个词的向量维度是50。就是去找到这些词对应的向量。
- with tf.Session() as sess:
- print(tf.nn.embedding_lookup(wordVectors, firstSentence).eval().shape) # 上图左侧两个图
输出:
在整个训练集上面构造索引之前,我们先花一些时间来可视化我们所拥有的数据类型。这将帮助我们去决定如何设置最大序列长度的最佳值。在前面的例子中,我们设置了最大长度为10,但是这个值在很大程度上取决于你输入的数据。
训练集我们使用的是IMDB数据集。这个数据集包含25000条电影数据,其中12500条正向数据,12500条负向数据。这些数据都是存储在文本文件中,首先我们需要做的就是去解析这个文件。正向数据包含在一个文件夹中,负向数据包含在另一个文件夹中。
负向数据:
正向数据:
以“0-9.txt”为例:
- from os import listdir
- from os.path import isfile, join
- positiveFiles = ['./training_data/positiveReviews/' + f for f in listdir('./training_data/positiveReviews/') if isfile(join('./training_data/positiveReviews/', f))]
- negativeFiles = ['./training_data/negativeReviews/' + f for f in listdir('./training_data/negativeReviews/') if isfile(join('./training_data/negativeReviews/', f))]
- numWords = []
- for pf in positiveFiles:
- with open(pf, 'r', encoding='utf-8') as f:
- line = f.readline()
- counter = len(line.split()) # 按空格分隔
- numWords.append(counter)
- print('Positive files finished')
-
- for nf in negativeFiles:
- with open(nf, 'r', encoding='utf-8') as f:
- line = f.readline()
- counter = len(line.split())
- numWords.append(counter)
- print('Negative files finished')
-
- numFiles = len(numWords)
- print('The total number of files is', numFiles)
- print('The total number of words in the files is', sum(numWords))
- print('The average number of words in the files is', sum(numWords) / len(numWords))
结果:
- import matplotlib.pyplot as plt
- %matplotlib inline
- plt.hist(numWords, 50)
- plt.xlabel('Sequence Length')
- plt.ylabel('Frequency')
- plt.axis([0, 1200, 0, 8000])
- plt.show()
结果:
从直方图和句子的平均单词数,我们认为将句子最大长度设置为250是可行的。
maxSeqLength = 250
接下来,让我们看看如何将单个文件中的文本转换成索引矩阵,比如下面的代码就是文本中的其中一个评论。
- fname = positiveFiles[3] # Can use any valid index (not just 3)
- with open(fname) as f:
- for lines in f:
- print(lines)
- exit
结果:
接下来,我们将它转换成一个索引矩阵。
- # 删除标点符号、括号、问号等,只留下字母数字字符
- import re
- strip_special_chars = re.compile('[^A-Za-z0-9 ]+')
-
- def cleanSentences(string):
- string = string.lower().replace('<br />', ' ') # 把换行符替换成空格
- return re.sub(strip_special_chars, '', string.lower()) # 按照strip_special_chars规则,把string中的值替换成空,即删除
- # 长度未到250,0填充;大于250,去掉多余的
- firstFile = np.zeros((maxSeqLength), dtype='int32')
- with open(fname) as f:
- indexCounter = 0
- line = f.readline()
- cleanedLine = cleanSentences(line)
- print(cleanedLine)
- split = cleanedLine.split()
- for word in split:
- try:
- firstFile[indexCounter] = wordsList.index(word)
- except ValueError: # 若找不到词
- firstFile[indexCounter] = 399999 # Vector for unknown words
- indexCounter = indexCounter + 1
- firstFile
结果:
现在,我们用相同的方法来处理全部的25000条评论。我们将导入电影训练集,并且得到一个25000*250的矩阵。这是一个计算成本非常高的过程,运行一次后,可以把结果保存成“idsMatrix.npy”索引矩阵文件,下次直接加载此文件即可。
- ids = np.zeros((numFiles, maxSeqLength), dtype='int32')
- fileCounter = 0
- for pf in positiveFiles:
- with open(pf, 'r', encoding='UTF-8') 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
- indexCounter = indexCounter + 1
- if indexCounter >= maxSeqLength:
- break
- fileCounter = fileCounter + 1
-
- for nf in negativeFiles:
- with open(nf, 'r', encoding='UTF-8') 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
- indexCounter = indexCounter + 1
- if indexCounter >= maxSeqLength:
- break
- fileCounter = fileCounter + 1
-
- np.save('idsMatrix', ids)
加载上一步的索引矩阵文件:
ids = np.load('./training_data/idsMatrix.npy')
- from random import randint
-
- def getTrainBatch():
- labels = []
- arr = np.zeros([batchSize, maxSeqLength])
- for i in range(batchSize):
- if (i % 2 == 0):
- num = randint(1,11499) # 产生[1,11499]范围内一个整数型随机数
- labels.append([1,0])
- else:
- num = randint(13499,24999)
- labels.append([0,1])
- arr[i] = ids[num-1:num]
- return arr, labels
-
- def getTestBatch():
- labels = []
- arr = np.zeros([batchSize, maxSeqLength])
- for i in range(batchSize):
- num = randint(11499,13499)
- if (num <= 12499):
- labels.append([1,0])
- else:
- labels.append([0,1])
- arr[i] = ids[num-1:num]
- return arr, labels
现在,我们可以开始构建我们的TensorFlow图模型。首先,我们需要去定义一些超参数,比如批处理大小,LSTM的单元个数,分类类别和训练次数。
- batchSize = 24
- lstmUnits = 64 # 隐层神经元个数
- numClasses = 2 # 2分类
- iterations = 50000 # 迭代次数
与大多数TensorFlow图一样,现在我们需要指定两个占位符,一个用于数据输入,另一个用于标签数据。对于占位符,最重要的一点就是确定好维度。标签占位符代表一组值,每一个值都为[1, 0]或者[0, 1],这个取决于数据是正向的还是负向的。输入占位符,是一个整数化的索引数组。
- import tensorflow as tf
- tf.reset_default_graph()
-
- labels = tf.placeholder(tf.float32, [batchSize, numClasses])
- input_data = tf.placeholder(tf.int32, [batchSize, maxSeqLength])
一旦,我们设置了我们的输入数据占位符,我们可以调用tf.nn.embedding_lookup()函数来得到我们的词向量。该函数最后将返回一个三维向量,第一个维度是批处理大小,第二个维度是句子长度,第三个维度是词向量长度。更清晰的表达,如下图所示:
- data = tf.Variable(tf.zeros([batchSize, maxSeqLength, numDimensions]), dtype=tf.float32)
- data = tf.nn.embedding_lookup(wordVectors, input_data) # input_data的每个id对应一个wordVectors中的向量,利用embedding_lookup查找
现在我们已经得到了我们想要的数据形式,那么揭晓了我们看看如何才能将这种形式输入到我们的LSTM网络中。首先,我们使用tf.nn.rnn_cell.BasicLSTMCell函数,这个函数输入的参数是一个整数,表示需要几个LSTM单元。这是我们设置的一个超参数,我们需要对这个数值进行调试从而来找到最优的解。然后,我们会设置一个dropout参数,以此来避免一些过拟合。
最后,我们将LSTM cell和三维的数据输入到tf.nn.dynamic_rnn,这个函数的功能是展开整个网络,并且构建一整个RNN模型。
- lstmCell = tf.contrib.rnn.BasicLSTMCell(lstmUnits)
- lstmCell = tf.contrib.rnn.DropoutWrapper(cell=lstmCell, output_keep_prob=0.75)
- value, _ = tf.nn.dynamic_rnn(lstmCell, data, dtype=tf.float32) # 返回value,最终输出值
堆栈LSTM网络是一个比较好的网络架构。也就是前一个LSTM隐藏层的输出是下一个LSTM的输入。堆栈LSTM可以帮助模型记住更多的上下文信息,但是带来的弊端是训练参数会增加很多,模型的训练时间会很长,过拟合的几率也会增加。
dynamic RNN 函数的第一个输出可以被认为是最后的隐藏状态向量。这个向量将被重新确定维度,然后乘以最后的权重矩阵和一个偏置项来获得最终的输出值。
- # 全连接层
- weight = tf.Variable(tf.truncated_normal([lstmUnits, numClasses]))
- bias = tf.Variable(tf.constant(0.1, shape=[numClasses]))
- value = tf.transpose(value, [1, 0, 2]) # 将矩阵value转置成[1, 0, 2]
- # 取最终的结果值
- last = tf.gather(value, int(value.get_shape()[0]) -1) # last为最后神经元结果值
- prediction = (tf.matmul(last, weight) + bias)
接下来,我们需要定义正确的预测函数和正确率评估参数。正确的预测形式是查看最后输出的0-1向量是否和标记的0-1向量相同。
- correctPred = tf.equal(tf.arg_max(prediction, 1), tf.arg_max(labels, 1))
- accuracy = tf.reduce_mean(tf.cast(correctPred, tf.float32))
之后,我们使用一个标准的交叉熵损失函数来作为损失值。对于优化器,我们选择Adam,并且采用默认的学习率。
- loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=labels))
- optimizer = tf.train.AdamOptimizer().minimize(loss)
选择合适的超参数来训练你的神经网络是至关重要的。你会发现你的训练损失值与你选择的优化器(Adam,Adadelta,SGD,等等)、学习率和网络架构都有很大的关系。特别是在RNN和LSTM中,单元数量和词向量的大小都是重要因素。
训练过程的基本思路是,我们首先先定义一个TensorFlow会话。然后,我们加载一批评论和对应的标签。接下来,我们调用会话的run函数。这个函数有两个参数,第一个参数被称为fetches参数,这个参数定义了我们感兴趣的值。我们希望通过我们的优化器来最小化损失函数。第二个参数被称为feed_dict参数。这个数据结构就是我们提供给我们的占位符。我们需要将一个批处理的评论和标签输入模型,然后不断对这一组训练数据进行循环训练。
- sess = tf.InteractiveSession()
- saver = tf.train.Saver()
- sess.run(tf.global_variables_initializer())
-
- for i in range(iterations):
- nextBatch, nextBatchLabels = getTrainBatch();
- sess.run(optimizer, {input_data: nextBatch, labels: nextBatchLabels})
-
- if(i % 1000 == 0 and i != 0):
- loss_ = sess.run(loss, {input_data: nextBatch, labels: nextBatchLabels}) # 改成get TestBatch
- accuracy_ = sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})
-
- print('iteration {}/{}...'.format(i+1, iterations),
- 'loss {}...'.format(loss_),
- 'accuracy {}...'.format(accuracy_))
- if(i % 10000 == 0 and i != 0):
- save_path = saver.save(sess, './training_data/models/pretrained_lstm.ckpt', global_step = i)
- print('saved to %s' % save_path)
结果:
导入一个预训练的模型需要使用TensorFlow的另一个会话函数,成为Server,然后利用这个会话函数来调用restore函数。这个函数包括两个参数,一个表示当前会话,另一个表示保存的模型。
- sess = tf.InteractiveSession()
- saver = tf.train.Saver()
- saver.restore(sess, tf.train.latest_checkpoint('./training_data/models'))
然后,从我们的测试集中导入一些电影评论。请注意,这些评论是模型从来没有看见过的。
- iterations = 10
- for i in range(iterations):
- nextBatch, nextBatchLabels = getTestBatch();
- print("Accuracy for this batch:", (sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})) * 100)
结果:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。