1. 简介

本文将使用 deeplearning4j(简称 dl4j)库构建一个简单的神经网络——这是一个现代化且强大的机器学习工具。

在开始之前,请注意本指南不要求你具备线性代数、统计学、机器学习理论等深度知识(这些是专业 ML 工程师必备的)。

2. 什么是深度学习?

神经网络是由互连节点层组成的计算模型。节点是类似神经元的数值数据处理器,它们从输入端接收数据,应用权重和函数处理,然后将结果输出到输出端。这类网络可以通过源数据样本进行训练。

训练本质上是将数值状态(权重)保存到节点中,这些权重后续会影响计算过程。训练样本可能包含带有特征的数据项及其已知类别(例如:“这组 16×16 像素包含手写字母 'a'”)。

训练完成后,神经网络能从新数据中提取信息,即使它从未见过这些特定数据项。一个建模良好且训练有素的网络可以识别图像、手写字母、语音,处理统计数据以生成商业智能结果等。

随着高性能和并行计算的发展,深度神经网络近年来成为可能。这类网络与简单神经网络的区别在于它们包含多个中间层(或隐藏层)。这种结构使网络能以更复杂的方式(递归、循环、卷积等)处理数据,并从中提取更多信息。

3. 项目搭建

使用该库至少需要 Java 7。此外,由于包含一些原生组件,它仅支持 64 位 JVM。

开始前,先检查环境是否满足要求:

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

首先,在 Maven 的 pom.xml 中添加所需依赖。我们将库版本提取到属性中(最新版本可查阅 Maven Central):

<properties>
    <dl4j.version>0.9.1</dl4j.version>
</properties>

<dependencies>

    <dependency>
        <groupId>org.nd4j</groupId>
        <artifactId>nd4j-native-platform</artifactId>
        <version>${dl4j.version}</version>
    </dependency>

    <dependency>
        <groupId>org.deeplearning4j</groupId>
        <artifactId>deeplearning4j-core</artifactId>
        <version>${dl4j.version}</version>
    </dependency>
</dependencies>

注意:nd4j-native-platform 是多个可用实现之一。它依赖适用于多平台(macOS、Windows、Linux、Android 等)的原生库。如果想在支持 CUDA 的显卡上执行计算,也可切换到 nd4j-cuda-8.0-platform 后端。

4. 数据准备

4.1. 准备数据集文件

我们将编写机器学习的“Hello World”——鸢尾花数据集分类。该数据集收集自不同品种的鸢尾花(Iris setosaIris versicolorIris virginica)。

这些品种的花瓣和萼片长度/宽度不同。编写精确算法来分类输入数据项(即判断特定花朵所属品种)很困难,但训练有素的神经网络可以快速且低错误率地完成分类。

我们将使用该数据的 CSV 版本,其中 0-3 列包含品种特征,第 4 列包含记录类别(品种),用 0、1 或 2 编码:

5.1,3.5,1.4,0.2,0
4.9,3.0,1.4,0.2,0
4.7,3.2,1.3,0.2,0
…
7.0,3.2,4.7,1.4,1
6.4,3.2,4.5,1.5,1
6.9,3.1,4.9,1.5,1
…

4.2. 数据向量化与读取

我们用数字编码类别,因为神经网络处理数值数据。将真实世界数据项转换为数值序列(向量)的过程称为向量化——deeplearning4j 使用 datavec 库实现。

首先,用该库读取向量化数据文件。创建 CSVRecordReader 时,可指定跳过的行数(如文件有标题行)和分隔符(本例为逗号):

try (RecordReader recordReader = new CSVRecordReader(0, ',')) {
    recordReader.initialize(new FileSplit(
      new ClassPathResource("iris.txt").getFile()));

    // …
}

遍历记录时,可使用 DataSetIterator 接口的多种实现。数据集可能很大,分页或缓存功能会很有用。

但我们的数据集仅 150 条记录,因此用 iterator.next() 一次性读入内存。

我们还需指定类别列索引(本例等于特征数 4)和总类别数(3)。

⚠️ 注意:必须打乱数据集以消除原始文件中的类别顺序

我们使用固定随机种子(42)而非默认的 *System.currentTimeMillis()*,确保每次运行程序打乱结果一致,从而获得稳定结果:

DataSetIterator iterator = new RecordReaderDataSetIterator(
  recordReader, 150, FEATURES_COUNT, CLASSES_COUNT);
DataSet allData = iterator.next();
allData.shuffle(42);

4.3. 归一化与数据分割

训练前还需对数据归一化。归一化是两阶段过程:

  • 收集数据统计信息(拟合)
  • 以某种方式转换数据使其统一(变换)

不同类型数据的归一化方式可能不同

例如处理不同尺寸的图像时,需先收集尺寸统计信息,再将图像缩放到统一尺寸。

但对数值数据,归一化通常指转换为正态分布。NormalizerStandardize 类可帮我们实现:

DataNormalization normalizer = new NormalizerStandardize();
normalizer.fit(allData);
normalizer.transform(allData);

数据准备就绪后,需将其分为两部分:

  • 第一部分用于训练
  • 第二部分(网络完全未见过的数据)用于测试训练后的网络

这能验证分类是否正确。我们取 65% 数据(0.65)训练,剩余 35% 测试:

SplitTestAndTrain testAndTrain = allData.splitTestAndTrain(0.65);
DataSet trainingData = testAndTrain.getTrain();
DataSet testData = testAndTrain.getTest();

5. 网络配置准备

5.1. 流式配置构建器

现在用酷炫的流式构建器构建网络配置:

MultiLayerConfiguration configuration 
  = new NeuralNetConfiguration.Builder()
    .iterations(1000)
    .activation(Activation.TANH)
    .weightInit(WeightInit.XAVIER)
    .learningRate(0.1)
    .regularization(true).l2(0.0001)
    .list()
    .layer(0, new DenseLayer.Builder().nIn(FEATURES_COUNT).nOut(3).build())
    .layer(1, new DenseLayer.Builder().nIn(3).nOut(3).build())
    .layer(2, new OutputLayer.Builder(
      LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX)
        .nIn(3).nOut(CLASSES_COUNT).build())
    .backprop(true).pretrain(false)
    .build();

即使这种简化的流式构建方式,仍有很多参数需要理解与调整。我们来拆解这个模型。

5.2. 设置网络参数

iterations() 方法指定优化迭代次数

迭代优化指在训练集上执行多次传递,直到网络收敛到良好结果。

通常在真实大数据集上训练时,我们会使用多个周期(完整数据传递)和每周期一次迭代。但初始数据集极小,因此我们使用单周期和多迭代。

activation() 是节点内运行的函数,用于确定输出

最简单的激活函数是线性函数 f(x) = x,但实践证明只有非线性函数才能让网络用少量节点解决复杂任务。

org.nd4j.linalg.activations.Activation 枚举提供了多种激活函数,必要时也可自定义。这里我们使用双曲正切(tanh)函数。

weightInit() 指定网络初始权重的设置方式。正确的初始权重会显著影响训练结果。不深入数学细节,我们选择高斯分布形式(WeightInit.XAVIER),这通常是不错的起点。

其他权重初始化方法可在 org.deeplearning4j.nn.weights.WeightInit 枚举中查找。

学习率是影响网络学习能力的关键参数。

在复杂场景中,我们可能需要大量时间调整此参数。但本任务中,我们直接使用较大的值 0.1,通过 learningRate() 方法设置。

训练神经网络的一个常见问题是过拟合——即网络“死记硬背”训练数据。

当网络为训练数据设置过高权重,导致在其他数据上表现糟糕时,就会发生这种情况。

为解决此问题,我们设置 l2 正则化*.regularization(true).l2(0.0001)*。正则化通过“惩罚”过大权重防止过拟合。

5.3. 构建网络层

接下来,我们构建密集层(也称全连接层)网络:

  • 第一层节点数应与训练数据列数相同(4)
  • 第二密集层包含 3 个节点(可调整,但需与前层输出数匹配)
  • 最终输出层节点数需匹配类别数(3)

网络结构如下图所示:

神经网络结构图

训练成功后,我们将得到一个接收 4 个输入值、向 3 个输出之一发送信号的分类器。

最后,通过 *.backprop(true).pretrain(false)* 设置反向传播(最有效的训练方法之一)并禁用预训练。

6. 创建与训练网络

现在根据配置创建神经网络,初始化并运行:

MultiLayerNetwork model = new MultiLayerNetwork(configuration);
model.init();
model.fit(trainingData);

用剩余数据测试训练好的模型,并通过三类评估指标验证结果:

INDArray output = model.output(testData.getFeatureMatrix());
Evaluation eval = new Evaluation(3);
eval.eval(testData.getLabels(), output);

打印 eval.stats() 会发现,网络在鸢尾花分类上表现良好,仅将 3 个类别 1 误判为类别 2:

Examples labeled as 0 classified by model as 0: 19 times
Examples labeled as 1 classified by model as 1: 16 times
Examples labeled as 1 classified by model as 2: 3 times
Examples labeled as 2 classified by model as 2: 15 times

==========================Scores========================================
# of classes: 3
Accuracy: 0.9434
Precision: 0.9444
Recall: 0.9474
F1 Score: 0.9411
Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes)
========================================================================

流式配置构建器让我们能快速添加/修改网络层或调整参数,尝试优化模型。

7. 总结

本文使用 deeplearning4j 库构建了一个简单但强大的神经网络。

本文源码可在 GitHub 获取。


原始标题:A Guide to Deeplearning4j