Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_linear-networks/linear-regression-djl.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_linear-networks/linear-regression-djl.ipynb .. _sec_linear_concise: 线性回归的简洁实现 ================== 在过去的几年里,出于对深度学习强烈的兴趣,许多公司、学者和业余爱好者开发了各种成熟的开源框架。通过这些框架可以自动化实现基于梯度的学习算法中重复性的工作。 在 :numref:`sec_linear_scratch` 中,我们只依赖了:(1)通过\ ``NDArray``\ 来进行数据存储和线性代数;(2)通过\ ``GradientCollector``\ 来计算梯度。实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用,现代深度学习库也为我们实现了这些组件。 在本节中,我们将介绍如何通过使用 DJL 框架来简洁地实现 :numref:`sec_linear_scratch` 中的线性回归模型。 生成数据 -------- 与 :numref:`sec_linear_scratch` 中类似,我们首先生成数据。 .. code:: java %load ../utils/djl-imports %load ../utils/DataPoints.java %load ../utils/Training.java .. code:: 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(); 读取数据集 ---------- 首先我们使用 ``features`` 和 ``labels`` 来创建一个 ``ArrayDataset`` 数据集,并在实例化时指定 ``batchSize``\ 。此外,布尔值 ``shuffle`` 表示是否希望数据迭代器对象在每个迭代周期内打乱数据。 .. code:: java // 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`` 的方式与我们在 :numref:`sec_linear_scratch` 中使用 ``dataset.getData()`` 函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。 .. code:: java 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(); .. parsed-literal:: :class: output 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] .. _fig_singleneuron: 定义模型 -------- 当我们在 :numref:`sec_linear_scratch` 中实现线性回归时,我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。但是,如果模型变得更加复杂,而且当你几乎每天都需要实现模型时,你会想简化这个过程。这种情况类似于从头开始编写自己的博客。做一两次是有益的、有启发性的,但如果每次你每需要一个博客就花一个月的时间重新发明轮子,那你将是一个糟糕的网页开发者。 对于标准操作,我们可以 DJL 预定义的 ``Block``\ 。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量\ ``net``\ ,它是一个 ``SequentialBlock`` 类的实例。 ``SequentialBlock`` 类为串联在一起的多个层定义了一个容器。当给定输入数据, ``SequentialBlock`` 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要\ ``SequentialBlock``\ 。但是由于以后几乎所有的模型都是多层的,在这里使用\ ``SequentialBlock``\ 会让你熟悉标准的流水线。 回顾 :numref:`fig_single_neuron` 中的单层网络架构,这一单层被称为 *全连接层*\ (fully-connected layer),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。 |Linear regression is a single-layer neural network.| 在 DJL 中,全连接层在 ``Linear`` 类中定义。由于我们只想得到一个标量输出,所以我们将该数字设置为 1,并制定是否包含偏差(\ ``bias``\ )。 .. |Linear regression is a single-layer neural network.| image:: https://resources.djl.ai/d2l-java/singleneuron.svg .. code:: java 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); 定义损失函数 ------------ 在 DJL 中,抽象类 ``Loss`` 定义了损失函数的接口。在这个例子中,我们将使用平方损失 (``L2Loss``)。 .. math:: L2Loss = \sum_{i = 1}^{n}(y_i - \hat{y_i})^2 .. code:: java Loss l2loss = Loss.l2Loss(); 定义优化算法 ------------ 小批量随机梯度下降算法是一种优化神经网络的标准工具,DJL 通过 ``Optimizer`` 类支持该算法的许多变种。当我们实例化 ``Optimizer`` 时,我们要指定优化的参数。我们希望使用的优化算法(\ ``sgd``\ )以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 ``learningRate``\ 值,这里设置为 0.03。 .. code:: java Tracker lrt = Tracker.fixed(0.03f); Optimizer sgd = Optimizer.sgd().setLearningRateTracker(lrt).build(); ``Trainer``\ 的初始化配置 ------------------------- 下面这段程序,展示了我们如何初始化及配置\ ``trainer``\ ,并用这个\ ``trainer``\ 对人工智能模型进行训练。 .. code:: java 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); .. parsed-literal:: :class: output INFO Training on: 1 GPUs. INFO Load MXNet Engine Version 1.9.0 in 0.092 ms. 初始化模型参数 -------------- 在对人工智能模型进行训练前,我们需要对模型的参数进行初始化设置。例如,对线性回归模型进行初始化配置时,我们需要提供权重及偏差参数。在\ ``DJL``\ 里,进行这种初始化参数的配置,你只需要简单地调用 ``initialize`` 函数,将你希望使用的模型及模型参数传人\ ``initialize`` 函数即可。 .. code:: java // First axis is batch size - won't impact parameter initialization // Second axis is the input size trainer.initialize(new Shape(batchSize, 2)); 运行性能指标 ------------ 一般情况下,DJL 不会自动记录运行性能指标,因为记录运行性能指标本身会提高运行成本,降低运行性能。如果出于特殊理由,你需要对某些运行指标进行记录,你可以生成一个 ``metrics`` 并将这个新生成的 ``metrics`` 设置成 ``trainer`` 的 ``metrics`` 即可,具体程序如下: .. code:: java Metrics metrics = new Metrics(); trainer.setMetrics(metrics); 训练 ---- 通过 DJL 来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时,DJL 的优势将大大增加。当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。 回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(\ ``dataset``\ ),不停地从中获取一个小批量的输入和相应的标签。对于每一个小批量,我们会进行以下步骤: - 通过调用 ``trainBatch(batch)`` 生成预测并计算损失(正向传播)并计算梯度(反向传播)。 - 通过调用 ``step`` 函数来更新模型参数。 训练中\ ``Trainer``\ 会自动打印损失和精确度到日志文件。 .. code:: java 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)); } .. parsed-literal:: :class: output Epoch 1 Training: 100% |████████████████████████████████████████| L2Loss: 4.99 .. parsed-literal:: :class: output INFO Epoch 1 finished. INFO Train: L2Loss: 4.99 .. parsed-literal:: :class: output Epoch 2 Training: 100% |████████████████████████████████████████| L2Loss: 0.01 .. parsed-literal:: :class: output INFO Epoch 2 finished. INFO Train: L2Loss: 0.01 .. parsed-literal:: :class: output Epoch 3 Training: 100% |████████████████████████████████████████| L2Loss: 8.58E-05 .. parsed-literal:: :class: output 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)`` 获取。 这个实验案例的参数对比结果显示,模型训练中学习到的参数和真实原始参数是非常接近的。说明这个训练模型是成功有效的。 .. code:: java 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())); .. parsed-literal:: :class: output Error in estimating w: [-0.000193 -0.000954] Error in estimating b: 0.000900 保存训练模型 ------------ 通过上面的步骤,当训练出一个满意的模型,你可以将这个训练模型保存下来,用在将来的人工智能项目中。 训练模型保存除了保存模型本身,模型的元数据也应该保存(如模型精确度,训练周期等)。我们可以使用 ``setProperty()`` 函数设置元数据。 DJL中保存模型非常简单,用 ``Paths.get()`` 函数指定一个模型保存路径,调用 ``save()`` 函数,模型及模型的元数据就会一起保存到你指定的文件目录里。 .. code:: java 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 .. parsed-literal:: :class: output 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 ) 小结 ---- - 我们可以使用 DJL 更简洁地实现模型。 - 在DJL项目里,\ ``training.dataset``\ 包里有不少数据处理工具,\ ``nn`` 包里定义了大量的神经网络构建层 ``layer`` ,\ ``Loss`` 类定义了许多通用的损失函数。学员们可以根据自己的需要到这些包和类里找合用的工具及函数。 - DJL的 ``training.initializer`` 包里有各种模型初始化方法及相应的初始化配置参数供学员参考。 练习 ---- 1. 参照 DJL 文档,找出其他的损失函数和初始化的方法,使用 ``L1`` 损失函数替换 ``L2Loss``\ 。 2. 你如何在训练中访问模型的参数?