[L2]实战语言模型~数据batching

2020-05-28 19:07:36 浏览数 (1)

在一切变好之前,我们总要经历一些不开心的日子。

全文字数: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操作的时候就需要采取一些特殊的处理了,目前有两种处理方案:

  1. 对于训练样本来说,每个句子通常是作为独立的数据来训练的,这个时候可以使用填充操作,就是将较短的句子补充到同一个batch中最长句子的相同长度,需要填充的地方使用"<pad>"进行标记;
  2. 对于训练样本来说,每个句子并非随意抽取的文本,而是在上下文之间有关联的内容,需要将前面句子的信息传递到后面的句子之中,为了实现这个目标,同行采用一个种batching的方法;

而我们的PTB的数据集就属于上下文之间有关联内容的数据,所以这里使用第二种的batching方法。

b

如何 batching

对于上下文之间有关联样本来说,最理想的当然就是把这些句子拼接起来,形成一个很长的一个句子,然后放在循环神经网络中进行训练,如下图所示:

▲将整个文档前后连接后的示意图

其中"A1A2A3","B1B2","C1C2C3C4"等分别代表一个句子。但是这种方式现实中并不能实现:

  1. 如果将整个文档都放入一个计算图中,循环神经网络将会被展开成一个很长很长(通常我们的训练样本很大)的前馈神经网络,这样会导致计算图变的异常的庞大,不方便调试和后期的维护,而且效率会变的很低;
  2. 序列过长会发生梯度爆炸的问题,不论是基本的RNN还是LSTM梯度都可能会发生梯度爆炸,虽然梯度爆炸的现象很明显而且能够通过梯度剪枝的方式进行处理,但是如果序列很长的话,误差曲线就会变的异常的陡峭,这个时候即使使用了梯度剪枝的方式,效果也不会变的太好;

不能使用整个序列作为样本,那么很自然就会想着把大的序列长度切割成固定长度的子序列。循环神经网络在处理完一个子序列后,它最终的隐藏状态将复制到下一个序列中作为初始值,这样在前向计算的时,效果等同于一次性顺序地读取了整个文档,而在反向传播的时候,梯度则只在每个子序列内部传播,如下图所示:

▲按长度3切分整个文档

需要注意:

  1. 每一个循环神经网络都是不同的神经网络,也就是权重参数并不相同;
  2. 每一个循环神经网络都有一个loss,但是并不合起来,因为梯度更新的时候,只在每个子序列内部进行更新,不会传递到相邻的子序列之中去;

上面只解决了上下文连续的问题,但是我们知道使用Mini-batch进行处理的好处是可以利用计算的并行能力,我们希望每一个计算可以对多个句子进行并行处理。

解决的方案:

  1. 将整个文档切分成batch_size个连续段落;
  2. 让每一个小的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的流程:

  1. 将整个数据存放到一个list中,也就是将整个文档变成一个句子;
  2. 设置batch_size也就是每一次并行处理的数量,设置num_step也就是步长,简单理解就是循环神经网络展开的长度;
  3. 计算遍历完整个句子(文档)需要的次数(这里需要的是整数)num_batches ,也可以认为这个句子(文档)可以分解成多少个(batch_size, num_step);
  4. 然后将句子分割成num_batches个(batch_size, num_step)。

我们需要构建的是循环神经网络的语言模型,模型输入和输出的基本单元都是单词,很明显是有监督的模型,所以不仅需要制作data还需要制作标签label。语言模型输入一个词预测输出下一个词的概率,所以在构建训练集的时候只需要将样本往后移动一个单词即可。当然不论是制作data还是label都需要使用batching。

继续用上面那个numpy数组的例子,使用batching制作label:

▲使用batching制作label

有了data和label,就可以构建训练样本了:

▲制作好的训练样本

通过numpy数组简单例子的类比可以很容易理解对文本数据的batching操作。

0 人点赞