如果您没有Python,但是有Keras模型和Java

大家好! 今天,在构建ML模型中,Python处于领导地位,并在数据科学专家社区中广受欢迎[ 1 ]。

像大多数开发人员一样,Python的简洁和简洁的语法吸引了我们。 我们使用它来使用人工神经网络解决机器学习问题。 但是,实际上,产品开发语言并不总是使用Python,这需要我们解决其他集成问题。

在本文中,我将讨论当需要将Python的Keras模型与Java关联时所使用的解决方案。

我们要注意的是:

  • 功能捆绑Keras模型和Java;
  • 准备与DeepLearning4j框架(简称DL4J)一起使用;
  • 将Keras模型导入DL4J(请注意,本节包含了许多见解)-如何注册图层,导入模块的局限性,如何检查工作结果。

为什么要阅读?

  • 为了节省启动时间,如果您将面临类似集成的任务;
  • 找出我们的解决方案是否适合您,以及您是否可以重用我们的经验。

图片替代

深度学习框架重要性的整体特征[ 2 ]。

最受欢迎的深度学习框架的摘要可在此处[ 3 ]和此处[ 4 ]中找到。

如您所见,这些框架中的大多数都基于Python和C ++:它们使用C ++作为内核来加速基本和高负荷操作,并使用Python作为交互接口来加速开发。

实际上,许多开发语言都更加广泛。 Java是大型企业和组织产品开发的领导者。 一些流行的神经网络框架具有JNI / JNA绑定程序形式的Java端口,但是在这种情况下,需要为每种体系结构构建一个项目,并且需要Java在跨平台模糊问题上的优势。 这种细微差别在复制解决方案中可能非常重要。

另一种替代方法是使用Jython编译为Java字节码。 但这里有一个缺点-仅支持Python的第二版本,以及使用第三方Python库的能力有限。

为了简化Java中神经网络解决方案的开发,正在开发DeepLearning4j框架(简称DL4J)。 DL4除了Java API外,还提供了一组预训练的模型[ 5 ]。 通常,此开发工具很难与TensorFlow竞争。 TensorFlow在更详细的文档和大量示例,技术能力,社区规模和快速开发方面胜过DL4J。 尽管如此,Skymind坚持的趋势还是很有希望的。 Java中尚未发现该工具的重要竞争对手。

DL4J库是少数几个(如果不是唯一的)库,可以导入Keras模型;它在功能上扩展了Keras熟悉的层[ 6 ]。 DL4J库包含一个目录,其中包含神经网络ML模型的实现示例(dl4j-example)。 在我们的案例中,用Java实现这些模型的精妙之处并不那么有趣。 将更加详细地关注使用DL4J方法将经过训练的Keras / TF模型导入Java。

开始使用


在开始之前,您需要安装必要的程序:

  1. Java版本1.7(64位版本)及更高版本。
  2. Apache Maven项目构建系统。
  3. IDE可供选择:Intellij IDEA,Eclipse,Netbeans。 开发人员建议使用第一个选项,此外,还将讨论可用的培训示例。
  4. Git(用于将项目克隆到您的PC)。

在此处[ 7 ]或在视频[ 8 ]中可以找到带有启动示例的详细说明。

要导入模型,DL4J开发人员建议使用KerasModelImport导入模块 (于2016年10月出现)。 模块的功能支持Keras的两种模型架构-它是顺序的(Java中的模拟-类MultiLayerNetwork)和功能性(Java中的模拟-类ComputationGraph)。 该模型将以HDF5格式整体导入或导入2个单独的文件-具有h5扩展名的模型权重和包含神经网络体系结构的json文件。

为了快速起步,DL4J开发人员针对类型为Sequential [ 9 ]的模型在Fisher Fisher数据集上准备了一个简单示例的分步分析。 从以两种方式导入模型的角度考虑了另一个培训示例(1:以完整的HDF5格式; 2:在单独的文件中-模型权重(h5扩展)和体系结构(json扩展)),然后比较Python和Java模型的结果[ 10 ]。 到此结束了对导入模块实用功能的讨论。

Java中也有TF,但是它处于实验状态,开发人员没有对其稳定操作的任何保证[ 11 ]。 版本存在问题,并且Java中的TF具有不完整的API-这就是为什么此处不考虑此选项的原因。

原始Keras / TF模型的功能:


导入神经网络非常简单。 在代码中,我们将更详细地分析将神经网络与更复杂的体系结构集成的示例。

您不应该讨论此模型的实际方面,它是从考虑层(尤其是Lambda层的注册),导入模块的某些细微之处和局限性以及整个DL4J的角度进行指示的。 实际上,提到的细微差别可能需要调整网络体系结构,或者完全放弃通过DL4J启动模型的方法。

型号特点:

1.模型类型-功能(具有分支的网络);

2.选择小的训练参数(批的大小,时代数):批的大小-100,时代数-10,每个时代的步长-10;

3. 13层,各层的摘要如图所示:

图片替代

简短说明
  1. input_1-输入层,接受二维张量(由矩阵表示);
  2. lambda_1-在我们的示例中,用户层使张量在TF中的填充具有相同的数值;
  3. embedding_1-为文本数据的输入序列构建嵌入(矢量表示)(将2-D张量转换为3-D);
  4. conv1d_1-一维卷积层;
  5. lstm_2-LSTM层(在embed_1(第3号)层之后进行);
  6. lstm_1-LSTM层(在conv1d(第4号)层之后);
  7. lambda_2是在lstm_2(第5号)层之后截断张量的用户层(与lambda_1(第2号)层的填充相反的操作);
  8. lambda_3是在lstm_1(第6号)和conv1d_1(第4号)层之后张量被截断的用户层(与lambda_1(第2号)层中的填充相反的操作);
  9. concatenate_1-截断的(No. 7)和(No. 8)层的粘合;
  10. density_1-8个神经元的完全连接层和指数线性激活函数“ elu”;
  11. batch_normalization_1-标准化层;
  12. density_2-1个神经元和乙状结肠激活功能“乙状结肠”的完全连接层;
  13. lambda_4-用户层,在该用户层执行前一层的压缩(TF中的压缩)。

4.损失函数-binary_crossentropy

loss= frac1N sum1Nytrue cdotlogypred+1ytrue cdotlog1ypred



5.模型质量度量-谐波平均值(F度量)

F=2 frac\次+


在我们的案例中,质量指标的问题不如进口的正确性重要。 导入的正确性取决于在推理模式下工作的Python和Java NN模型中结果的重合性。

在DL4J中导入Keras模型:


使用的版本:Tensorflow 1.5.0和Keras 2.2.5。 在我们的案例中,HDF5文件将Python模型整体上载。

# saving model model1.save('model1_functional.h5') 

将模型导入DL4J时,导入模块不提供用于传递其他参数的API方法:张量流模块的名称(构建模型时从中导入函数)。

一般来说,DL4J仅与Keras函数一起使用,在Keras导入部分[ 6 ]中给出了详尽的列表,因此,如果使用TF的方法在Keras上创建了模型(如本例所示),则导入模块将无法识别它们。

导入模型的一般准则


显然,使用Keras模型意味着需要对其进行反复训练。 为此,为节省时间,设置了训练参数(1个时期),每个时期设置了1步(steps_per_epoch)。

首次导入模型时,尤其是具有唯一的自定义层和稀有层组合的模型时,成功的可能性不大。 因此,建议以迭代方式执行导入过程:减少Keras模型的层数,直到您可以在Java中导入并运行该模型而没有错误为止。 接下来,一次向Keras模型添加一层,然后将结果模型导入Java,以解决发生的错误。

使用TF损失功能


为了证明,当导入Java时,训练模型的损失函数必须来自Keras,我们使用了Tensorflow中的log_loss(与custom_loss函数最相似)。 我们在控制台中收到以下错误:

 Exception in thread "main" org.deeplearning4j.nn.modelimport.keras.exceptions.UnsupportedKerasConfigurationException: Unknown Keras loss function log_loss. 

用Keras替换TF方法


在我们的案例中,TF模块中的函数使用了2次,在所有情况下,它们仅在lambda层中找到。

Lambda图层是用于添加任意功能的自定义图层。

我们的模型只有4个lambda层。 事实是,在Java中,必须通过KerasLayer.registerLambdaLayer手动注册这些lambda层(否则,我们会收到错误[ 12 ])。 在这种情况下,在lambda层内部定义的函数应该是来自相应Java库的函数。 在Java中,没有注册这些层的示例,也没有为此提供全面的文档。 一个例子在这里[ 13 ]。 从示例[ 14,15 ]借来了一般考虑。

依次考虑在Java中注册模型的所有lambda层:

1)Lambda层,用于沿给定方向(在本例中为左右)将有限量的常数添加到张量(矩阵):

该层的输入连接到模型的输入。

1.1)Python层:

 padding = keras.layers.Lambda(lambda x: tf.pad(x, paddings=[[0, 0], [10, 10]], constant_values=1))(embedding) 

为了清楚起见,该层的功能正常工作,我们在python层中显式替换了数值。

带有张量2x2的示例的表
是2x2它变成了2x22
[[ 1,2 ],
[ 3,4 ]]
[[37,37,37,37,37,37,37,37,37,37,1,2,37,37,37,37,37,37,37,37,37,37
[37、37、37、37、37、37、37、37、37、37、3、4、37、37、37、37、37、37、37、37、37、37]


1.2)Java层:

 KerasLayer.registerLambdaLayer("lambda_1", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.nn().pad(sdVariable, new int[][]{ { 0, 0 }, { 10, 10 }}, 1); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.feedForward(20); } }); 

在Java中所有已注册的lambda层中,重新定义了2个函数:
第一个函数“ definelayer”负责所使用的方法(一点也不明显:该方法只能在nn()后端以下使用); getOutputType负责注册层的输出,该参数是一个数字参数(此处为20,但通常允许使用任何整数值)。 它看起来不一致,但它的工作原理是这样的。

2)Lambda层,用于沿给定方向(在我们的示例中为左右)修剪张量(矩阵):

在这种情况下,LSTM层进入lambda层的输入。

2.1)Python层:

 slicing_lstm = keras.layers.Lambda(lambda x: x[:, 10:-10])(lstm) 

带有任意张量2x22x5的示例的表
是2x22x5它变成了2x2x5
[[[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5], [1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1 ,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2 ,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3 ,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4 ,5],[1,2,3,4,5]],

[[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[ 1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1, 2,3,4,5],[1,2,3,4,5],[1、2、3、4、5],[1、2、3、4、5],[1,2, 3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3, 4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4,5],[1,2,3,4, 5],[1,2,3,4,5]]]
[[[ 1,2,3,4,5 ],[ 1,2,3,4,5 ]],
[[ 1,2,3,4,5 ],[ 1,2,3,4,5 ]]]


2.2)Java层:

 KerasLayer.registerLambdaLayer("lambda_2", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.stridedSlice(sdVariable, new int[]{ 0, 0, 10 }, new int[]{ (int)sdVariable.getShape()[0], (int)sdVariable.getShape()[1], (int)sdVariable.getShape()[2]-10}, new int[]{ 1, 1, 1 }); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.recurrent(60); } }); 

对于此层,InputType参数从前馈(20)更改为递归(60)。 在递归参数中,数字可以是任何整数(非零),但其与下一个lambda层的递归参数的总和应为160(即在下一层中,参数必须为100)。 数字160是由于必须在图层的输入concatenate_1处接收到具有张量(无,无,160)的张量。

前两个参数是变量,具体取决于输入字符串的大小。

3)Lambda层,用于沿给定方向(在我们的示例中为左右)修剪张量(矩阵):

该层的输入是LSTM层,在该层之前是conv1_d层

3.1)Python层:

 slicing_convolution = keras.layers.Lambda(lambda x: x[:,10:-10])(lstm_conv) 

此操作与第2.1节中的操作完全相同。

3.2)Java层:

 KerasLayer.registerLambdaLayer("lambda_3", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.stridedSlice(sdVariable, new int[]{ 0, 0, 10 }, new int[]{ (int)sdVariable.getShape()[0], (int)sdVariable.getShape()[1], (int)sdVariable.getShape()[2]-10}, new int[]{ 1, 1, 1 }); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.recurrent(100); } }); 

除了recurrent(100)参数外,该lambda层重复了先前的lambda层。 在上一层的说明中注明了为什么采用“ 100”。

在第2点和第3点,λ层位于LSTM层之后,因此使用循环类型。 但是,如果在lambda层之前没有LSTM,而是conv1d_1,则仍然有必要设置递归值(它看起来不一致,但是可以这样工作)。

4)Lambda层压缩前一层:

该层的输入是完全连接的层。

4.1)Python层:

  squeeze = keras.layers.Lambda(lambda x: tf.squeeze( x, axis=-1))(dense) 

带有任意张量2x4x1的示例的表
是2x4x1变成了2x4
[[[ [1],[2],[3],[4]]

[ [1],[2],[3],[4 ]]
[[ 1,2,3,4 ],
[ 1,2,3,4 ]]


4.2)Java层:

 KerasLayer.registerLambdaLayer("lambda_4", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.squeeze(sdVariable, -1); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.feedForward(15); } }); 

该层的输入接收一个完全连接的层,该层的InputType feedForward(15),参数15不影响模型(允许使用任何整数值)。

下载导入的模型


该模型是通过ComputationGraph模块加载的:

 ComputationGraph model = org.deeplearning4j.nn.modelimport.keras.KerasModelImport.importKerasModelAndWeights("/home/user/Models/model1_functional.h5"); 

将数据输出到Java控制台


在Java中,尤其是在DL4J中,张量是作为高性能Nd4j库的数组编写的,可以将其视为Python中Numpy库的类似物。

假设我们的输入字符串包含4个字符。 例如,根据一些编号,将符号表示为整数(作为索引)。 为它们创建一个相应维数(4)的数组。

例如,我们有4个索引编码的字符:1、3、4、8。

Java代码:

 INDArray myArray = Nd4j.zeros(1,4); // one row 4 column array myArray.putScalar(0,0,1); myArray.putScalar(0,1,3); myArray.putScalar(0,2,4); myArray.putScalar(0,3,8); INDArray output = model.outputSingle(myArray); System.out.println(output); 

控制台将显示每个输入元素的概率。

进口型号


原始神经网络的架构和权重均无错误地导入。 推理模式下的Keras和Java神经网络模型都对结果达成共识。

Python模型:

图片替代

Java模型:

图片替代

实际上,导入模型并不是那么简单。 下面我们将简要强调一些在某些情况下可能至关重要的观点。

1)补丁归一化层在递归层之后不起作用。 问题已经在GitHub上公开了将近一年[ 16 ]。 例如,如果将此层添加到模型中(在接触层之后),则会出现以下错误:

 Exception in thread "main" java.lang.IllegalStateException: Invalid input type: Batch norm layer expected input of type CNN, CNN Flat or FF, got InputTypeRecurrent(160) for layer index -1, layer name = batch_normalization_1 

在实践中,该模型拒绝运行,因为在conv1d之后添加归一化层时引用了类似的错误。 在完全连接的层之后,附加功能可以完美地工作。

2)在完全连接层之后,设置“展平”层会导致错误。 在Stackoverflow [ 17 ]上提到了类似的错误。 六个月以来,没有任何反馈。

当然,这不是使用DL4J时可能遇到的所有限制。
该模型的最终运行时间在此处[ 18 ]。

结论


总之,可以注意到,将训练有素的Keras模型无痛地导入DL4J仅适用于简单的情况(当然,如果您没有这样的经验,并且确实有Java的良好命令)。

用户层越少,导入的模型就越轻松,但是如果网络体系结构复杂,则必须花费大量时间将其传输到DL4J。

已开发的导入模块的文档支持以及相关示例的数量似乎很潮湿。 在每个阶段,都会出现新的问题-如何注册Lambda层,参数的含义等。

考虑到神经网络架构的复杂性速度以及各层之间的交互速度,层的复杂性,DL4J尚未积极开发,以达到与人工神经网络一起使用的高端框架的水平。

无论如何,这些家伙都值得尊重他们的工作,并希望看到这个方向继续发展。

参考文献

  1. 人工智能领域的5种最佳编程语言
  2. 深度学习框架力量得分2018
  3. 深度学习软件的比较
  4. 人工智能世界的9大框架
  5. DeepLearning4j。 可用型号
  6. DeepLearning4j。 Keras模型导入。 支持的功能。
  7. Deeplearning4j。 快速入门
  8. 讲座0:DeepLearning4j入门
  9. Deeplearing4j:Keras模型导入
  10. 讲座7 | Keras模型导入
  11. 安装TensorFlow for Java
  12. 使用Keras图层
  13. DeepLearning4j:KerasLayer类
  14. DeepLearning4j:SameDiffLambdaLayer.java
  15. DeepLearning4j:KerasLambdaTest.java
  16. DeepLearning4j:具有RecurrentInputType的BatchNorm
  17. StackOverFlow:使用deeplearning4j(https://deeplearning4j.org/)在Java中打开keras模型时出现问题
  18. GitHub:有关模型的完整代码
  19. Skymind:AI框架比较

Source: https://habr.com/ru/post/zh-CN475338/


All Articles