Run this notebook online: or 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. 读取数据集¶
首先我们使用 features
和 labels
来创建一个 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),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。
在 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
)。
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(manager.getEngine().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.9.0 in 0.092 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
设置成 trainer
的
metrics
即可,具体程序如下:
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
)和模型训练中学习到的参数(wParam
和bParam
)进行比较。
在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/src036198309/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
包里定义了大量的神经网络构建层layer
,Loss
类定义了许多通用的损失函数。学员们可以根据自己的需要到这些包和类里找合用的工具及函数。DJL的
training.initializer
包里有各种模型初始化方法及相应的初始化配置参数供学员参考。
3.3.12. 练习¶
参照 DJL 文档,找出其他的损失函数和初始化的方法,使用
L1
损失函数替换L2Loss
。你如何在训练中访问模型的参数?