在一切变好之前,我们总要经历一些不开心的日子。
全文字数:2603字
阅读时间:10分钟
前言
由于在公众号上文本字数太长可能会影响阅读体验,因此过于长的文章,我会使用"[L1]"来进行分段。这个系列将主要借鉴《Tensorflow实战Google学习框架》这本书,主要介绍实现语言模型的一些前期准备,后期会出更详细的文章。
a
什么是 batching?
通常在优化目标函数的时候使用Mini-batch Gradient Descent算法,也就是Mini-batch梯度下降算法,这种梯度更新算法比Stochastic Gradient Descent(随机梯度下降算法,一个样本数据进行一次梯度更新)更加稳定,比Batch Gradient Descent(批量梯度下降法,一整个样本数据进行一个梯度更新)更新快。
虽然Mini-batch Gradient Descent稳定效率高,但是mini-batch给循环神经网络带来的很大的问题。因为在文本数据中,由于每个句子的长度不同,又无法像图像那样去调整到固定维度,而且在前期mini-batch的大小都是事先指定好了的,每一个批次的大小都是一样的。
这个时候就需要在对文本数据的batch操作的时候就需要采取一些特殊的处理了,目前有两种处理方案:
- 对于训练样本来说,每个句子通常是作为独立的数据来训练的,这个时候可以使用填充操作,就是将较短的句子补充到同一个batch中最长句子的相同长度,需要填充的地方使用"<pad>"进行标记;
- 对于训练样本来说,每个句子并非随意抽取的文本,而是在上下文之间有关联的内容,需要将前面句子的信息传递到后面的句子之中,为了实现这个目标,同行采用一个种batching的方法;
而我们的PTB的数据集就属于上下文之间有关联内容的数据,所以这里使用第二种的batching方法。
b
如何 batching
对于上下文之间有关联样本来说,最理想的当然就是把这些句子拼接起来,形成一个很长的一个句子,然后放在循环神经网络中进行训练,如下图所示:
▲将整个文档前后连接后的示意图
其中"A1A2A3","B1B2","C1C2C3C4"等分别代表一个句子。但是这种方式现实中并不能实现:
- 如果将整个文档都放入一个计算图中,循环神经网络将会被展开成一个很长很长(通常我们的训练样本很大)的前馈神经网络,这样会导致计算图变的异常的庞大,不方便调试和后期的维护,而且效率会变的很低;
- 序列过长会发生梯度爆炸的问题,不论是基本的RNN还是LSTM梯度都可能会发生梯度爆炸,虽然梯度爆炸的现象很明显而且能够通过梯度剪枝的方式进行处理,但是如果序列很长的话,误差曲线就会变的异常的陡峭,这个时候即使使用了梯度剪枝的方式,效果也不会变的太好;
不能使用整个序列作为样本,那么很自然就会想着把大的序列长度切割成固定长度的子序列。循环神经网络在处理完一个子序列后,它最终的隐藏状态将复制到下一个序列中作为初始值,这样在前向计算的时,效果等同于一次性顺序地读取了整个文档,而在反向传播的时候,梯度则只在每个子序列内部传播,如下图所示:
▲按长度3切分整个文档
需要注意:
- 每一个循环神经网络都是不同的神经网络,也就是权重参数并不相同;
- 每一个循环神经网络都有一个loss,但是并不合起来,因为梯度更新的时候,只在每个子序列内部进行更新,不会传递到相邻的子序列之中去;
上面只解决了上下文连续的问题,但是我们知道使用Mini-batch进行处理的好处是可以利用计算的并行能力,我们希望每一个计算可以对多个句子进行并行处理。
解决的方案:
- 将整个文档切分成batch_size个连续段落;
- 让每一个小的mini-batch负责batch_size个段落中的一小部分;
这个地方可能不太好理解,下面我用一个简单的numpy数组来说明batching:
代码语言:javascript复制import numpy as np
batch_num = 2
batch_size = 3
batch_step = 2
#(3,2*2)
array = np.arange(12).reshape(batch_size,batch_num * batch_step)
array2 = np.split(array,batch_num,axis = 1)
print(array)
print(array2)
▲通过numpy数组理清关系
我们继续来看对PTB数据进行batching的代码:
代码语言:javascript复制TRAIN_BATCH = 20
TRAIN_NUM_STEP = 35
#从文件中读取数据,并返回包含单词编号的数组
def read_data(file_path):
with open(file_path,"r") as fin:
#将整个文档读进一个长字符串.
id_string = " ".join([line.strip() for line in fin.readlines()])
#将读取的单词编号转换为整数
id_list = [int(w) for w in id_string.split()]
return id_list
def make_batches(id_list,batch_size,num_step):
#计算总的batch的数量。每个batch包含的单词数量是batch_size * num_step
num_batches = (len(id_list) - 1) // (batch_size * num_step)
#将数据整理成一个维度为[batch_size,num_batches * num_step]的二维数组
data = np.array(id_list[:num_batches * batch_size * num_step])
data = np.reshape(data,[batch_size,num_batches * num_step])
#沿着第二个维度将数据切分成num_batches个batch,存入一个数组
data_batches = np.split(data,num_batches,axis = 1)
#重复上述的操作,但是每个位置向右移动一位,这里得到的是RNN每一步输出所需要
# 预测的下一个单词
label = np.array(id_list[1:num_batches * batch_size * num_step 1])
label = np.reshape(label,[batch_size,num_batches * num_step])
label_batches = np.split(label,num_batches,axis = 1)
#返回一个长度为num_batcher的数组,其中每一项包括一个data矩阵和一个label矩阵。
return list(zip(data_batches,label_batches))
batching的流程:
- 将整个数据存放到一个list中,也就是将整个文档变成一个句子;
- 设置batch_size也就是每一次并行处理的数量,设置num_step也就是步长,简单理解就是循环神经网络展开的长度;
- 计算遍历完整个句子(文档)需要的次数(这里需要的是整数)num_batches ,也可以认为这个句子(文档)可以分解成多少个(batch_size, num_step);
- 然后将句子分割成num_batches个(batch_size, num_step)。
我们需要构建的是循环神经网络的语言模型,模型输入和输出的基本单元都是单词,很明显是有监督的模型,所以不仅需要制作data还需要制作标签label。语言模型输入一个词预测输出下一个词的概率,所以在构建训练集的时候只需要将样本往后移动一个单词即可。当然不论是制作data还是label都需要使用batching。
继续用上面那个numpy数组的例子,使用batching制作label:
▲使用batching制作label
有了data和label,就可以构建训练样本了:
▲制作好的训练样本
通过numpy数组简单例子的类比可以很容易理解对文本数据的batching操作。