Eclipse Deeplearning4j 1.0.0-alpha之前的版本只支持基于Layer的建模方式,而从1.0.0-alpha版本之后(包括该版本),Skymind团队在ND4j(https://deeplearning4j.org/docs/latest/nd4j-overview)框架中添加了对自动微分引擎SameDiff的支持,提供了大量神经网络的常用算子,因此开发人员可以基于这些OP构建神经网络计算图,从而完成深度学习的建模工作。本质上,基于Layer的建模方式是对算子的封装,对于初学者来说更加清晰,神经网络结构可以一目了然,但是其缺点是灵活性不足。基于OP的建模由于更加偏向底层数据处理,开发人员只需要关心对当前张量数据进行什么样的数据转换操作而无需刻意留意所在网络的哪一层,通过添加新算子来验证自己的研究成果或者复现论文的结论都很方便,当然基于OP的建模方式往往会使得模型冗长,如果不做详细解释想做二次开发或修改会比较困难。
SameDiff另一个比较重要的功能是直接支持Tensorflow模型的导入。在Deeplearning4j之前的版本中,直接支持的是导入Keras的模型,而导入TF的模型都需要间接通过Keras,因此并不方便。SameDiff支持导入大部分的TF算子,具体可以参考官方的测试demo工程(https://github.com/deeplearning4j/TFOpTests)。这篇文章暂且先不详细介绍SameDiff支持的所有算子,在后续的文章中会陆续介绍常见算子的使用。本文先从构建一个线性回归模型开始介绍基于OP建模的整体思路,包括计算流图的构建、训练数据准备、参数训练、指标验证和数据预测这几个方面。
首先需要说明的是,我使用的是最新发布的1.0.0-beta5的版本。相对于上一个1.0.0-beta4的版本,该版本修复了一些bug,因此建议大家使用最新的版本。这里我们使用Maven管理相关依赖,下面是需要导入的必要依赖清单:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<nd4j.version>1.0.0-beta5</nd4j.version>
<dl4j.version>1.0.0-beta5</dl4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-native-platform</artifactId>
<version>${nd4j.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-core</artifactId>
<version>${dl4j.version}</version>
</dependency>
<!-- 省去部分依赖 -->
<dependencies>
这些个依赖其实在之前的文章中也多次提及,这里不再赘述了,如果有不清楚的同学可以翻一下之前的文章,不过需要注意下版本的问题。
我们可以在IDE中查看当前版本SameDiff支持的一些算子。在之前文章中我们介绍的一些神经网络结构,比如CNN、RNN在这里都可以通过SDCNN、SDRNN中的方法调用来实现。SameDiff和TF一样是静态图计算模式,需要先声明所有的数据节点/变量,构建完整个流图后再以数据驱动训练参数。SameDiff和TF类似将数据变量分为以下几类:
- VARIABLE:可训练数据变量,模型参数
- CONSTANT:数据常量
- PLACEHOLDER:临时数据变量,一般用作不定规模的输入和输出数据
- ARRAY:临时数据变量,用来存储OP计算后的临时结果
以上数据类型的具体描述可以参考官网链接:https://deeplearning4j.org/docs/latest/samediff-variables
1. 声明计算流图上下文
SameDiff sd = SameDiff.create();
我们可以通过调用SameDiff的静态工厂方法来声明一个上下文实例。一般我们不带任何参数来创建这个实例,即不含有任何变量的空实例。
2. 声明数据变量和计算图
//声明placeholder变量
SDVariable input = sd.placeHolder("input", DataType.FLOAT, -1, NUM_FEATURE); // ? x 4
SDVariable label = sd.placeHolder("label", DataType.FLOAT, -1, 1); //? x 1
//声明可训练的variable参数
SDVariable weights = sd.var("weights", new XavierInitScheme('c', NUM_FEATURE, NUM_OUTPUT), DataType.FLOAT, NUM_FEATURE, NUM_OUTPUT);
SDVariable bias = sd.var("bias", DataType.FLOAT, 1);
//op算子构建计算图
SDVariable predicted = input.mmul(weights).add("predicted", bias); //y = w .* x + b
sd.loss.meanSquaredError("mseloss", label, predicted);
可以看到我们先声明了计算图的输入和输出,即模型的特征和标注,对于线性回归模型来说需要训练的模型参数是各个特征的权重以及偏置项,因此weights和bias需要设置成VARIABLE类型。我们通过mmul和add两个基础的算子构建整个计算流程,由此得到的输出和原始标注信息做均方误差作为loss。SDLoss类中集成了主流的损失函数,这里我们通过sd.loss.meanSquaredError直接调用。到此计算流图的构建就完成了。
3. 建模参数定义
double learningRate = 1e-2;
TrainingConfig config = new TrainingConfig.Builder()
.updater(new Sgd(learningRate))
.dataSetFeatureMapping("input")
.dataSetLabelMapping("label")
.build();
sd.setTrainingConfig(config);
sd.setListeners(new ScoreListener(1));
这部分主要是通过TrainingConfig工具类来配置优化算法、学习率等超参数。
4. 构建数据集
//构建训练数据集和验证数据集
INDArray indFeature = Nd4j.rand(NUM_SAMPLES, NUM_FEATURE);
INDArray indLabel = Nd4j.rand(NUM_SAMPLES, NUM_OUTPUT);
DataSet ds = new DataSet(indFeature, indLabel);
SplitTestAndTrain train_test = ds.splitTestAndTrain(0.7);
DataSet dsTrain = train_test.getTrain();
DataSet dsTest = train_test.getTest();
DataSetIterator trainIter = new ListDataSetIterator<DataSet>(Lists.newArrayList(dsTrain), BATCHES);
DataSetIterator testIter = new ListDataSetIterator<DataSet>(Lists.newArrayList(dsTest), BATCHES);
在数据构建这个部分,我们随机生成了1024条数据集并且按照7:3的比例切分成了训练数据集和验证集,接着我们对这些数据封装成迭代器的形式,这和之前我们基于Layer建模的数据结构是相同的。
5. 训练模型
sd.fit(trainIter, NUM_EPOCHES);
训练模型的部分只需要将刚才构建的训练集和需要训练的轮次作为参数传入fit接口即可
6. 验证模型
String outputVariable = predicted.getVarName();
assert(outputVariable.equals("predicted"));
RegressionEvaluation evaluation = new RegressionEvaluation();
sd.evaluate(testIter, outputVariable, evaluation);
System.out.println(evaluation.stats());
我们的数据是随机生成的(服从正态分布),用线性模型去拟合肯定不会收敛得很好。在这里我们只是为了说明验证的方法,所以具体指标数据并不需要太过关注。
7. 单条数据预测
INDArray indSingle = indFeature.getRow(0L);
INDArray indSingleLabel = indLabel.getRow(0L);
INDArray result = sd.output(new HashMap<String,INDArray>(){{put("input",indSingle);}}, "predicted").getOrDefault("predicted", null);
assert( result != null );
System.out.println(String.format("Predict: %s, Label: %s", result, indSingleLabel));
我们从之前随机生成的数据集中抽取第一条作为样例数据,output会进行前向传播计算,以此得到预测值。这里我们和真实的输出做了比较。下面是我们打出的一些日志供大家参考。
熟悉TF编程的开发人员对以上建模过程相信会有种似曾相识的感觉。包括一些PLACEHOLDER、VARIABLE类型变量的声明,基本张量计算算子的调用等和TF的编程模式非常相似。其实SameDiff支持导入TF模型的原因就是在于这些常用算子的兼容。目前TF正在推2.0的版本,很大方向是推广基于Layer的编程API,也就是Keras的接口。但当前各种开源代码中,基于OP实现模型的还是占大多数,因此熟悉这种编程模式还是很有必要的。在后面的文章中我会陆续介绍SameDiff常用算子以及TF模型导入和再训练的相关内容。
如果开发人员需要了解计算图的详情,比如搭建完流图后涉及的变量类型有哪些,涉及到的算子有哪些,那么通过调用SameDiff的summary接口并通过控制台打印的内容来查看相关信息。
//模型描述
System.out.println(sd.summary());
从summary的结果中可以看到,一个涉及8个变量,3个算子/方法。明细在下方有所展示。由于我们设置的batchSize是4,所以input变量是一个4*4的矩阵。可训练参数weights是一个含有4个参数的向量。涉及到的算子在我们构建计算图的逻辑中提到过,即mmul、add还有我们调用的损失函数mse。
为了比较直观地解释计算流图的结构,我们可以参考下面这张图。
在训练结束后,可能我们需要查看特征权重,也就是weights和bias的值,我们可以通过以下方式查看。
//模型参数获取
INDArray trainedWeights = sd.getVariable("weights").getArr();
INDArray trainedBias = sd.getVariable("bias").getArr();
System.out.println(String.format("Weights: %s, Bias: %s", trainedWeights, trainedBias));
这里对整篇文章做下小结。SameDiff是Deeplearning4j推出的自动微分计算框架,目前集成在ND4j的项目中。目Deeplearning4j的一些新模型结构的实现,比如Attention机制就是基于SameDiff来实现。基于SameDiff提供的OP进行算法建模是偏重底层的建模方式,虽然本质上和基于Layer的方式是一致的,但在实现方式和编程思维上还是有比较多的差异。我们以一个比较常见的线性回归模型阐述了基于OP建模的整体思路。虽然张量的转换不是很复杂,但是展现了基本的建模结构,可作为大家使SameDiff的入门参考。