数据和网络配置

DL4J目前支持以下各类循环神经网络

一、RNN的数据

在标准的前馈网络中(多层感知器或DL4J的’DenseLayer’),输入和输出数据具有二维结构,或者说数据的“形状”可以描述为[numExamples, inputSize], 即输入前馈网络的数据的行/样例数为’numExamples’,而每一行中的列数为’inputSize’。

而RNN的数据则是时间序列。这些数据具备三个维度,增加了一个时间维度。因此,输入数据的形状为[numExamples,inputSize,timeSeriesLength],而输出数据的形状为[numExamples,outputSize,timeSeriesLength]。 就INDArray中的数据布局而言,位于(i,j,k)的值即是一批数据中第i例的第k个时间步的第j个值。数据布局如下图所示。

RNN_data

二、RnnOutputLayer

RnnOutputLayer是在许多循环网络系统(用于回归和分类任务)中使用的最终层。

如将 MultiLayerNetwork 的第三层设置为 RnnOutputLayer,如下

.layer(2, new RnnOutputLayer.Builder(LossFunction.MCXENT).activation("softmax")
.weightInit(WeightInit.XAVIER).nIn(prevLayerSize).nOut(nOut).build())

RNN定型功能

一、截断式沿时间反向传播

采用截断式沿时间反向传播算法(BPTT)可以降低循环网络中每项参数更新的复杂度。简而言之,此种算法可以让我们以同样的运算能力更快地定型神经网络(提高参数更新的频率)。 我们建议在输入较长序列时(通常指超过几百个时间步)使用截断式BPTT算法。

假设用长度为12个时间步的时间序列定型一个循环网络。

rnn_tbptt_1

如果这个时间步变得很长,对计算的要求能力会特别高。

在实际应用中,截断式BPTT可将正向和反向传递拆分为一系列较小时间段的正向/反向传递操作。正向/反向传递时间段的具体长度是用户可以自行设定的参数。

例如,若将截断式BPTT的长度设定为4个时间步,则学习过程如下图所示:

rnn_tbptt_2

这就类似于我们解一个很长的谜题。当从前到后的线索特别长时,要完全记住并利用就会变得很困难。所以我们可以将所有的线索分成各个部分去使用,通常效果是不错的。

当然缺点也明显,如果我们第10步依赖于第1步,就无法获得第1步中存储的线索。

使用方式

使用截断式BPTT相当简单:只需将下列代码加入网络配置(添加在网络配置最后的 .build() 之前)

.backpropType(BackpropType.TruncatedBPTT)
.tBPTTForwardLength(100)    //正向步长
.tBPTTBackwardLength(100)   //反向步长

注意事项

  • 在默认情况下(未手动设置反向传播类型),DL4J将使用BackpropType.Standard(即完整BPTT)。

  • tBPTTForwardLength和tBPTTBackwardLength选项用于设置截断式BPTT传递的长度。时间段长度通常设定为50~200,但需要视具体应用而定。正向传递与反向传递的长度通常相同(有时tBPTTBackwardLength可能更短,但不会更长)

  • 截断式BPTT的长度必须短于或等于时间序列的总长

二、掩模:一对多、多对一和序列分类

DL4J支持一系列基于填零和掩模操作的RNN定型功能。填零和掩模让我们能支持诸如一对多、多对一数据情景下的定型,同时也能支持长度可变的时间序列(同一批次内)。

假设我们用于定型循环网络的输入和输出数据并不会在每个时间步都出现。具体示例(单个样例)见下图。DL4J支持以下所有情景的网络定型。

rnn_masking_1

填零的概念很简单。试想同一批次中有两个长度分别为50和100个时间步的时间序列。定型数据是一矩形数组;因此我们对较短的时间序列(输入和输出)进行填零操作(即添加零),使输入和输出长度相等(在本例中为100时间步)。

当然,只进行这一操作会导致定型出现问题。因此在填零之外,我们还使用掩模机制。

掩模的概念也很简单。我们增加两个数组,用来记录一个时间步和样例的输入/输出是实际的输入/输出还是填零。

如前文所述,RNN的批次数据有3个维度,输入和输出的形状为[miniBatchSize,inputSize,timeSeriesLength]和 [miniBatchSize,outputSize,timeSeriesLength]。

而填零数组则是二维结构,输入和输出的形状均为[miniBatchSize,timeSeriesLength],每一时间序列和样例对应的值为0(“不存在”)或1(“存在”)。输入与输出的掩模数组分开存储在不同的数组中。

对单个样例而言,输入与输出的掩模数组如下:

rnn_masking_2

对于“不需要掩模”的情况,我们可以使用全部值为1的掩模数组,所得结果与不使用掩模数组相同。此外,RNN定型中使用的掩模数组可以是零个、一个或者两个,比如多对一的情景就有可能仅设置一个用于输出的掩模数组。

实际应用中,填零数组一般在数据导入阶段创建(例如由SequenceRecordReaderDatasetIterator创建,后文将具体介绍),包含在DataSet对象中。

如果一个DataSet包含掩模数组,MultiLayerNetwork在定型中会自动使用。如果不存在掩模数组,则不会启用掩模功能。

三、使用掩模的评估与计分

掩模数组在进行计分与评估时(如评估RNN分类器的准确性)也很重要。以多对一情景为例:每个样例仅有单一输出,任何评估都应考虑到这一点。

在评估中可通过以下方法使用(输出)掩模数组:

Evaluation.evalTimeSeries(INDArray labels, INDArray predicted, INDArray outputMask)

其中labels是实际输出(三维时间序列),predicted是网络的预测(三维时间序列,与labels形状相同),而outputMask则是输出的二维掩模数组。注意评估并不需要输入掩模数组。

得分计算同样会通过MultiLayerNetwork.score(DataSet)方法用到掩模数组。如前文所述,如果DataSet包括一个输出掩模数组,计算网络得分(损失函数 - 均方差、负对数似然函数等)时就会自动使用掩模。

四、掩膜与定型后的序列分类

序列分类是掩膜的常见用途之一。之所以采用掩膜,是因为循环网络的输入是序列(时间序列),而我们只需要为整个序列加一个标签(而不是为序列中的每个时间步都加一个标签)。

但是,根据RNN的设计,网络输出的序列应与输入序列长度相等。有了掩膜,我们在定型用于序列分类的网络时就可以将整个序列的标签置于最后一个时间步——其本质就是让网络知道标签数据实际上只出现在最后一个时间步

假设网络已定型完毕,现在我们希望从时间序列的输出数组中获取最后一个时间步的预测结果。该怎样操作呢?

要获取最后一个时间步的结果,需要考虑两种不同的情形。首先,如果只有单个样例,那我们就不需要使用掩膜数组,可以直接获取输出数组中最后一个时间步的结果:

INDArray timeSeriesFeatures = ...;
INDArray timeSeriesOutput = myNetwork.output(timeSeriesFeatures);
int timeSeriesLength = timeSeriesOutput.size(2);	//时间维度大小
INDArray lastTimeStepProbabilities = timeSeriesOutput.get(NDArrayIndex.point(0), NDArrayIndex.all(), NDArrayIndex.point(timeSeriesLength-1));

另一种更复杂的情形是一个微批次(特征数组)中包含了多个样例,而样例的长度各不相同(如果样例长度相同,则可以使用前一种流程)。

在样例长度有差异的情形中,我们需要分别获取各个样例在最后一个时间步的结果。如果数据加工管道为我们提供了每个样例的时间序列长度,那就比较好办:只需对样例进行迭代 ,将前文代码中的timeSeriesLength换成样例长度即可。

假如无法直接获取时间序列的长度,我们就需要将其从掩膜数组中提取出来。

如果有标签掩膜数组(每个时间序列相对应的one-hot向量,形如[0,0,0,1,0]):

INDArray labelsMaskArray = ...;
INDArray lastTimeStepIndices = Nd4j.argMax(labelMaskArray,1);

假如只有特征掩膜,一种比较直截了当的处理方法是:

INDArray featuresMaskArray = ...;
int longestTimeSeries = featuresMaskArray.size(1);
INDArray linspace = Nd4j.linspace(1,longestTimeSeries,longestTimeSeries);
INDArray temp = featuresMaskArray.mulColumnVector(linspace);
INDArray lastTimeStepIndices = Nd4j.argMax(temp,1);

可以这样理解上述方法的原理:我们有形如[1,1,1,1,0]的特征掩膜,现在要从中提取出最后一个非零元素。所以我们将[1,1,1,1,0]映射为[1,2,3,4,0],然后提取其中最大的元素(即最后一个时间步)。

无论是哪种情形,接下来的步骤都可以是:

int numExamples = timeSeriesFeatures.size(0);
for( int i=0; i<numExamples; i++ ){
    int thisTimeSeriesLastIndex = lastTimeStepIndices.getInt(i);
    INDArray thisExampleProbabilities = timeSeriesOutput.get(NDArrayIndex.point(i), NDArrayIndex.all(), NDArrayIndex.point(thisTimeSeriesLastIndex));
}

五、RNN层与其他神经网络层的结合应用

DL4J中的RNN层可以与其他类型的层结合使用。例如,可以在同一个网络结合使用DenseLayer和GravesLSTM层;或者将卷积(CNN)层与GravesLSTM层结合用于处理视频。

当然,DenseLayer和卷积层并不处理时间序列数据-这些层要求的输入类型不同。为了解决这一问题,我们需要使用层预处理器功能:比如CnnToRnnPreProcessor和FeedForwardToRnnPreprocessor类。

点击此处查看所有预处理器。大部分情况下,DL4J配置系统会自动添加所需的预处理器。但预处理器也可以手动添加(替代为每一层自动添加的预处理器)。

例如,如需在第1和第2层之间添加预处理器,可在网络配置中添加下列代码:

.inputPreProcessor(2, new RnnToFeedForwardPreProcessor()).

测试时间:逐步预测

同其他类型的神经网络一样,RNN可以使用 MultiLayerNetwork.output() 和 MultiLayerNetwork.feedForward() 方法生成预测。这些方法适用于诸多情况;但它们的限制是,在生成时间序列的预测时,每次都只能从头开始运算。

假设我们需要在一个实时系统中生成基于大量历史数据的预测。在这种情况下,使用output/feedForward方法是不实际的,因为这些方法每次被调用时都需要进行所有历史数据的正向传递。

如果我们要在每个时间步进行单个时间步的预测,那么此类方法会导致(a)运算量很大,同时(b)由于重复同样的运算而造成浪费。

对于此类情况,MultiLayerNetwork 提供四种主要的方法:

  • rnnTimeStep(INDArray)

  • rnnClearPreviousState()

  • rnnGetPreviousState(int layer)

  • rnnSetPreviousState(int layer, Map<String,INDArray> state)

rnnTimeStep()方法的作用是提高正向传递(预测)的效率,一次进行一步或数步预测。与output/feedForward方法不同,rnnTimeStep方法在被调用时会记录RNN各层的内部状态。

需要注意的是,rnnTimeStep与output/feedForward方法的输出应当完全一致(对每个时间步而言),不论是同时进行所有预测(output/feedForward)还是一次只生成一步或数步预测(rnnTimeStep),唯一的区别就是运算量不同。

简言之,MultiLayerNetwork.rnnTimeStep()方法有以下两项作用:

  • 用事先存储的状态(如有)生成输出/预测(正向传递)

  • 更新已存储的状态,记录上一个时间步的激活情况(准备在下一次调用rnnTimeStep时使用)

例如,假设我们需要用一个RNN来预测一小时后的天气状况(假定输入是前100个小时的天气数据)。 如果采用output方法,那么我们需要送入全部100个小时的数据,才能预测出第101个小时的天气。 而预测第102个小时的天气时,我们又需要送入100(或101)个小时的数据;第103个小时及之后的预测同理。

或者,我们可以使用rnnTimeStep方法。当然,在进行第一次预测时,我们仍需要使用全部100个小时的历史数据,进行完整的正向传递:

但如果要开始对一个新的(完全分离的)时间序列进行预测,就必须(这很重要)用 MultiLayerNetwork.rnnClearPreviousState() 方法手动清除已存储的状态。该方法将会重置网络中所有循环层的内部状态。

导入时间序列数据

RNN的数据导入比较复杂,因为可能使用的数据类型较多:一对多、多对一、长度可变的时间序列等。本节将介绍DL4J目前已实现的数据导入机制。

此处介绍的方法采用SequenceRecordReaderDataSetIterator class类,以及DataVec的CSVSequenceRecordReader类。该方法目前可加载来自文件的已分隔(用制表符或逗号)数据,每个时间序列为一个单独文件。 该方法还支持:

  • 长度可变的时间序列输入

  • 一对多和多对一数据加载(输入和标签在不同文件内)

  • 分类问题中,由索引到one-hot表示方法的标签转换(如从“2”到[0,0,1,0])

  • 在数据文件开始处跳过固定/指定数量的行(如注释或标题行)

  • 注意在所有情况下,数据文件中的每一行都表示一个时间步。

参见UT

二、替代方法:运用自定义DataSetIterator

有些时候,我们可能需要进行不符合常规情景的数据导入。方法之一是运用自定义的 DataSetIterator

DataSetIterator只是用于迭代DataSet对象的接口,这些对象封装了输入和目标INDArrays,以及输入和标签掩模数组(可选)。

需要注意的是,这一方法的级别较低:运用DataSetIterator时,必须手动创建所需的输入和标签INDArrays,以及输入和标签掩模数组(如需要)。但这一方法可以让数据加载方式变得十分灵活。

本方法的实践应用可参考文字/字符示例以及Word2Vec电影评论情绪示例对迭代器的应用。

注:在创建自定义的DataSetIterator时,包括输入特征、标签以及任何掩模数组在内的数组都应当按“f”(fortran)顺序创建。有关数组顺序的详情请参阅ND4J用户指南。

在实际操作中,这意味着要使用Nd4j.create方法来指定数组顺序:

Nd4j.create(new int[]{numExamples, inputSize, timeSeriesLength},'f')

虽然“c”顺序的数组也可以运行,但由于在进行某些运算时需要先将数组复制到“f”顺序,会导致性能有所下降。

基础实例

其实概念太多,不结合实践很快就会忘记。我们从最简单的DEMO开始。

BasicRNNExample

(从此处开始,首先简单理解基础知识。所有的案例都应该作为学习的例子。)

实际应用

生成莎士比亚风格散文

打开 dl4j-examples 项目中的 org.deeplearning4j.examples.recurrent.character.GravesLSTMCharModellingExample.java 文件。

本案例学习如何复制莎士比亚风格的戏剧。

实际运行发现会超时。就提前把代码中指定的文件下载下来。放到指定位置。

mv pg100.txt /var/folders/vq/n01wfyl5377df17f3mvyq4yh0000gn/T/Shakespeare.txt
  • 运行效果

随便截取其中的一个效果

Completed 40 minibatches of size 32x1000 characters
Sampling characters from network given initialization ""
----- Sample 0 -----
Ks.
DHEMANNE. My some he dees!, Gaves knest in his barrys
     What wis. THI yeld our o mward, sige, muke enough, this his come
     Froghs up, the resed's him, my nours?
  HOR OF INIY. Come; but keep be this must we to this, I lide you heath,
    Help, I besper's till in the cowp;
    All whome in t

----- Sample 1 -----
Knes our approcked
   As not. That go till you that with put iens
    Atuse it you not for all the rap'ts, this taked,
    And flit' with and mound and his buts
    Th' ever her buk'd.
  MARDIS. Him!  "               "            Exeunt.




STES IILay.
Che. ADus. Were dead'd the I ter!
  NUCHINUS. L

----- Sample 2 -----
K4 Honded time is theur great tull of the
    lettom, that them is to yer.
  PLIRCIO. Hare, his nut
    Is you will as gives, for the doin;
    And be this ne: my ording, I weach to eves, sing
    To with his nabel- Hesh, Pitines,
    Here xapiighaw of makes's; and 'twentes ut. No, see.

            

----- Sample 3 -----
K'nt stay, to her butS
  COR OFAY. The peit with yis thee, I frue might that I hisrem, and Alpers, and you have
    feg would on brung and treath noc trues;
    And these to awher emp tre in mine.
    Cay yet the hin lifor a cheserancaly our's!
    I waus' men-hele but her. rie, us? You? I lord.
  Pa

视频帧分类

VideoClassificationExample.java

电影评论分类

Word2VecSentimentRNN.java