Run this notebook online:Binder or Colab: Colab

3.3. 线性回归的简洁实现

在过去的几年里,出于对深度学习强烈的兴趣,许多公司、学者和业余爱好者开发了各种成熟的开源框架。通过这些框架可以自动化实现基于梯度的学习算法中重复性的工作。 在 Section 3.2 中,我们只依赖了:(1)通过NDArray来进行数据存储和线性代数;(2)通过GradientCollector来计算梯度。实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用,现代深度学习库也为我们实现了这些组件。

在本节中,我们将介绍如何通过使用 DJL 框架来简洁地实现 Section 3.2 中的线性回归模型。

3.3.1. 生成数据

Section 3.2 中类似,我们首先生成数据。

%load ../utils/djl-imports
%load ../utils/DataPoints.java
%load ../utils/Training.java
NDManager manager = NDManager.newBaseManager();

NDArray trueW = manager.create(new float[]{2, -3.4f});
float trueB = 4.2f;

DataPoints dp = DataPoints.syntheticData(manager, trueW, trueB, 1000);
NDArray features = dp.getX();
NDArray labels = dp.getY();

3.3.2. 读取数据集

首先我们使用 featureslabels 来创建一个 ArrayDataset 数据集,并在实例化时指定 batchSize。此外,布尔值 shuffle 表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

// Saved in the utils file for later use
public ArrayDataset loadArray(NDArray features, NDArray labels, int batchSize, boolean shuffle) {
    return new ArrayDataset.Builder()
                  .setData(features) // set the features
                  .optLabels(labels) // set the labels
                  .setSampling(batchSize, shuffle) // set the batch size and random sampling
                  .build();
}

int batchSize = 10;
ArrayDataset dataset = loadArray(features, labels, batchSize, false);

使用 ArrayDataset 的方式与我们在 Section 3.2 中使用 dataset.getData() 函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。

Batch batch = dataset.getData(manager).iterator().next();
NDArray X = batch.getData().head();
NDArray y = batch.getLabels().head();
System.out.println(X);
System.out.println(y);
batch.close();
ND: (10, 2) gpu(0) float32
[[ 0.2925, -0.7184],
 [ 0.1   , -0.3932],
 [ 2.547 , -0.0034],
 [ 0.0083, -0.251 ],
 [ 0.129 ,  0.3728],
 [ 1.0822, -0.665 ],
 [ 0.5434, -0.7168],
 [-1.4913,  1.4805],
 [ 0.1374, -1.2208],
 [ 0.3072,  1.1135],
]

ND: (10) gpu(0) float32
[ 7.2342,  5.7411,  9.3138,  5.0536,  3.1772,  8.6284,  7.7434, -3.808 ,  8.6185,  1.0259]

3.3.3. 定义模型

当我们在 Section 3.2 中实现线性回归时,我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。但是,如果模型变得更加复杂,而且当你几乎每天都需要实现模型时,你会想简化这个过程。这种情况类似于从头开始编写自己的博客。做一两次是有益的、有启发性的,但如果每次你每需要一个博客就花一个月的时间重新发明轮子,那你将是一个糟糕的网页开发者。

对于标准操作,我们可以 DJL 预定义的 Block。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量net,它是一个 SequentialBlock 类的实例。 SequentialBlock 类为串联在一起的多个层定义了一个容器。当给定输入数据, SequentialBlock 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要SequentialBlock。但是由于以后几乎所有的模型都是多层的,在这里使用SequentialBlock会让你熟悉标准的流水线。

回顾 fig_single_neuron 中的单层网络架构,这一单层被称为 全连接层(fully-connected layer),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。

Linear regression is a single-layer neural network.

在 DJL 中,全连接层在 Linear 类中定义。由于我们只想得到一个标量输出,所以我们将该数字设置为 1,并制定是否包含偏差(bias)。

Model model = Model.newInstance("lin-reg");

SequentialBlock net = new SequentialBlock();
Linear linearBlock = Linear.builder().optBias(true).setUnits(1).build();
net.add(linearBlock);

model.setBlock(net);

3.3.4. 定义损失函数

在 DJL 中,抽象类 Loss 定义了损失函数的接口。在这个例子中,我们将使用平方损失 (L2Loss)。

(3.3.1)\[L2Loss = \sum_{i = 1}^{n}(y_i - \hat{y_i})^2\]
Loss l2loss = Loss.l2Loss();

3.3.5. 定义优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具,DJL 通过 Optimizer 类支持该算法的许多变种。当我们实例化 Optimizer 时,我们要指定优化的参数。我们希望使用的优化算法(sgd)以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 learningRate值,这里设置为 0.03。

Tracker lrt = Tracker.fixed(0.03f);
Optimizer sgd = Optimizer.sgd().setLearningRateTracker(lrt).build();

3.3.6. Trainer的初始化配置

下面这段程序,展示了我们如何初始化及配置trainer,并用这个trainer对人工智能模型进行训练。

DefaultTrainingConfig config = new DefaultTrainingConfig(l2loss)
    .optOptimizer(sgd) // Optimizer (loss function)
    .optDevices(Device.getDevices(1)) // single GPU
    .addTrainingListeners(TrainingListener.Defaults.logging()); // Logging

Trainer trainer = model.newTrainer(config);
INFO Training on: 1 GPUs.
INFO Load MXNet Engine Version 1.8.0 in 0.077 ms.

3.3.7. 初始化模型参数

在对人工智能模型进行训练前,我们需要对模型的参数进行初始化设置。例如,对线性回归模型进行初始化配置时,我们需要提供权重及偏差参数。在DJL里,进行这种初始化参数的配置,你只需要简单地调用 initialize 函数,将你希望使用的模型及模型参数传人initialize 函数即可。

// First axis is batch size - won't impact parameter initialization
// Second axis is the input size
trainer.initialize(new Shape(batchSize, 2));

3.3.8. 运行性能指标

一般情况下,DJL 不会自动记录运行性能指标,因为记录运行性能指标本身会提高运行成本,降低运行性能。如果出于特殊理由,你需要对某些运行指标进行记录,你可以生成一个 metrics 并将这个新生成的 metrics 设置成 trainermetrics 即可,具体程序如下:

Metrics metrics = new Metrics();
trainer.setMetrics(metrics);

3.3.9. 训练

通过 DJL 来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时,DJL 的优势将大大增加。当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。

回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(dataset),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤:

  • 通过调用 trainBatch(batch) 生成预测并计算损失(正向传播)并计算梯度(反向传播)。

  • 通过调用 step 函数来更新模型参数。

训练中Trainer会自动打印损失和精确度到日志文件。

int numEpochs = 3;

for (int epoch = 1; epoch <= numEpochs; epoch++) {
    System.out.printf("Epoch %d\n", epoch);
    // Iterate over dataset
    for (Batch batch : trainer.iterateDataset(dataset)) {
        // Update loss and evaulator
        EasyTrain.trainBatch(trainer, batch);

        // Update parameters
        trainer.step();

        batch.close();
    }
    // reset training and validation evaluators at end of epoch
    trainer.notifyListeners(listener -> listener.onEpoch(trainer));
}
Epoch 1
Training:    100% |████████████████████████████████████████| L2Loss: 4.99
INFO Epoch 1 finished.
INFO Train: L2Loss: 4.99
Epoch 2
Training:    100% |████████████████████████████████████████| L2Loss: 0.01
INFO Epoch 2 finished.
INFO Train: L2Loss: 0.01
Epoch 3
Training:    100% |████████████████████████████████████████| L2Loss: 8.58E-05
INFO Epoch 3 finished.
INFO Train: L2Loss: 8.58E-05

下面,我们将真实原始参数(权重trueW和偏差trueB)和模型训练中学习到的参数(wParambParam)进行比较。

在DJL里,访问模型训练中学习到的参数需要分两步走。从模型model中取出构建层layer。从构建层中用getParameters()函数取参数列表。取得参数列表以后,每个独立的参数就可以通过valueAt()函数用列表下标获取了。下面的例子中,权重及偏差参数分别可以用列表下标 0 valueAt(0) 和下标 1 valueAt(1) 获取。

这个实验案例的参数对比结果显示,模型训练中学习到的参数和真实原始参数是非常接近的。说明这个训练模型是成功有效的。

Block layer = model.getBlock();
ParameterList params = layer.getParameters();
NDArray wParam = params.valueAt(0).getArray();
NDArray bParam = params.valueAt(1).getArray();

float[] w = trueW.sub(wParam.reshape(trueW.getShape())).toFloatArray();
System.out.printf("Error in estimating w: [%f %f]\n", w[0], w[1]);
System.out.println(String.format("Error in estimating b: %f\n", trueB - bParam.getFloat()));
Error in estimating w: [-0.000193 -0.000954]
Error in estimating b: 0.000900

3.3.10. 保存训练模型

通过上面的步骤,当训练出一个满意的模型,你可以将这个训练模型保存下来,用在将来的人工智能项目中。 训练模型保存除了保存模型本身,模型的元数据也应该保存(如模型精确度,训练周期等)。我们可以使用 setProperty() 函数设置元数据。 DJL中保存模型非常简单,用 Paths.get() 函数指定一个模型保存路径,调用 save() 函数,模型及模型的元数据就会一起保存到你指定的文件目录里。

Path modelDir = Paths.get("../models/lin-reg");
Files.createDirectories(modelDir);

model.setProperty("Epoch", Integer.toString(numEpochs)); // save epochs trained as metadata

model.save(modelDir, "lin-reg");

model
Model (
    Name: lin-reg
    Model location: /codebuild/output/src414136162/src/github.com/deepjavalibrary/d2l-java-zh/chapter_linear-networks/../models/lin-reg
    Data Type: float32
    Epoch: 3
)

3.3.11. 小结

  • 我们可以使用 DJL 更简洁地实现模型。

  • 在DJL项目里,training.dataset包里有不少数据处理工具,nn 包里定义了大量的神经网络构建层 layerLoss 类定义了许多通用的损失函数。学员们可以根据自己的需要到这些包和类里找合用的工具及函数。

  • DJL的 training.initializer 包里有各种模型初始化方法及相应的初始化配置参数供学员参考。

3.3.12. 练习

  1. 参照 DJL 文档,找出其他的损失函数和初始化的方法,使用 L1 损失函数替换 L2Loss

  2. 你如何在训练中访问模型的参数?