模型训练

在进行模型训练时,一般分为四个步骤。

  1. 构建数据集,参见数据集 Dataset
  2. 定义神经网络,参见网络构建
  3. 定义超参、损失函数及优化器。
  4. 训练网络,参见训练及保存模型

准备数据集和神经网络

参考前面章节数据集 Dataset网络构建。给出如下加载数据集和定义神经网络的代码

from CangjieTB import macros.*
from CangjieTB import nn.*
from CangjieTB import nn.layers.*
from CangjieTB import nn.optim.*
from CangjieTB import nn.loss.*
from CangjieTB import ops.*
from CangjieTB import common.*
from CangjieTB import dataset.*

// 定义神经网络
@OptDifferentiable
struct LeNetModel {
    let conv1: Conv2d
    let conv2: Conv2d
    let pool: MaxPool2d
    let dense1: Dense
    let dense2: Dense
    let dense3: Dense
    init() {
        conv1 = Conv2d(1, 6, Array<Int64>([5, 5]), pad_mode: "valid")
        conv2 = Conv2d(6, 16, Array<Int64>([5, 5]), pad_mode: "valid")
        pool = MaxPool2d(kernel_size: Array<Int64>([2, 2]), stride: Array<Int64>([2, 2]))
        dense1 = Dense(400, 120, RandomNormalInitializer(sigma: 0.02))
        dense2 = Dense(120, 84, RandomNormalInitializer(sigma: 0.02))
        dense3 = Dense(84, 10, RandomNormalInitializer(sigma: 0.02))
    }
@Differentiable
    operator func ()(input: Tensor): Tensor {
        input |> this.conv1 |> relu |> this.pool
            |> this.conv2 |> relu |> this.pool
            |> flatten
            |> this.dense1 |> relu
            |> this.dense2 |> relu
            |> this.dense3
    }
}

func createLenetDataset() {
    // 加载数据集
    let dataPath: String = "./data/mnist/test"
    var mnistDs = MnistDataset(dataPath, epoch: 2)
    // 将图像数据映射到[0, 1]的范围
    var rescale = rescale(1.0 / 255.0, 0.0)
    mnistDs.datasetMap([rescale], "image")
    return mnistDs
}

定义超参、损失函数及优化器

定义超参

超参是可以调整的参数,可以控制模型训练优化的过程,不同的超参数值可能会影响模型训练和收敛速度。

一般会定义以下用于训练的超参:

  • 训练轮次(epoch):训练时遍历数据集的次数。
  • 批次大小(batch size):数据集进行分批读取训练,设定每个批次数据的大小。
  • 学习率(learning rate):如果学习率偏小,会导致收敛的速度变慢,如果学习率偏大则可能会导致训练不收敛等不可预测的结果。

为数据集设置训练轮次和批次大小,定义学习率

let epoch: Int32 = 5
let batchSize: Int32 = 32
mnistDs.batch(batchSize, true)
mnistDs.repeat(epoch)

let learningRate: Float32 = 1.0e-2

设置学习率见定义优化器

定义损失函数

使用损失函数需要导入 nn 包。

from CangjieTB import nn.loss.*

损失函数用来评价模型的预测值和真实值不一样的程度。给定输出值和目标值,计算损失值,使用方法如下所示:

// 创建损失函数对象
let lossFn = SoftmaxCrossEntropyWithLogits(sparse: true)
// 创建计算值与目标值
let netOutput = Tensor(Array<Float32>([0.1, 0.3, 0.5, 0.1]), shape: Array<Int64>([1, 4]))
let label = Tensor(Array<Int32>([3]), shape: Array<Int64>([1]))
// 调用损失函数对象的操作符重载方法,获取 loss 值
let loss = lossFn(netOutput, label)
print("loss", loss)

输出为:

loss
Tensor(shape=[], dtype=Float32, value= 1.55037284e+00)

除了 SoftmaxCrossEntropyWithLogits,仓颉 TensorBoost 也支持用户自定义损失函数。与自定义神经网络结构类似,自定义损失函数使用 struct 数据类型,其计算逻辑在 operator() 运算符重载函数中实现,详见构建神经网络

定义优化器

使用优化器需要导入 nn 包。

from CangjieTB import nn.*

优化器用于计算和更新梯度,构建一个优化器对象,该对象可以基于计算得到的梯度对原模型进行参数更新。设置优化器的参数选项,比如学习率、权重衰减等等。 class MomentumOptimizer<T0> 中泛型 T0 为神经网络的类型,如 LeNetModelMyLayer 等。初始化时必须显式指明泛型类型,代码如下:

let lr: Float32 = 0.01
let momentum: Float32 = 0.9
let net = LeNetModel()
var optim: MomentumOptimizer<LeNetModel> = MomentumOptimizer<LeNetModel>(net, lr, momentum)

其中,学习率可以是 Float32 类型的常数,也可以是 Tensor 类型的可变动态学习率,代码如下:

var lrArrary: Array<Float32> = Array<Float32>([0.01, 0.01, 0.005, 0.002, 0.001])
var learningRate = Tensor(lrArrary, shape: [lrArrary.size])
var net = LeNetModel()
var optim = MomentumOptimizer<LeNetModel>(net, learningRate, 0.9, weightDecay: 0.00004, lossScale: 1024.0)

训练网络

训练网络的过程可以分成两步,第一步是进行前向和反向计算分别得到模型的 loss 值和梯度;第二步是利用梯度更新模型参数。

计算 loss 值和梯度

运用仓颉 TensorBoost 提供的 @AdjointOf 接口,可以获取到网络反向计算的梯度,代码如下:

// 定义可微的训练函数
@Differentiable[except: [lossFn, input, label]]
func train(net: LeNetModel, lossFn: SoftmaxCrossEntropyWithLogits, input: Tensor, label: Tensor): Tensor
{
    var output = net(input)
    var lossTensor = lossFn(output, label)
    return lossTensor
}

根据训练函数,获取输入的梯度:

// 创建 train 函数的输入
var input = parameter(zerosTensor(Array<Int64>([32, 1, 32, 32]), dtype: FLOAT32), "data")
var label = parameter(zerosTensor(Array<Int64>([32]), dtype: INT32), "label")
var lossFn = SoftmaxCrossEntropyWithLogits(sparse: true)
// 获取伴随函数
let adj = @AdjointOf(train)
// 获取反向传播函数
var (loss, bpFunc) = adj(net, lossFn, input, label)
// 获取梯度
var gradient: LeNetModel = bpFunc(Tensor(Float32(1.0)))

更新参数

当完成优化器对象的初始化,并通过反向传播函数得到梯度对象之后,即可调用优化器的 update 方法,将获取到的梯度传递给优化器,代码如下:

// 更新梯度
optim.update(gradient)

此处的 gradient 的类型需要与 optim 对象的泛型一致。

执行训练

基于前面介绍的梯度计算和参数更新方法,可以写出如下训练代码:

var datasetLenetTest = createLenetDataset()
for (i in 1..(datasetLenetTest.getDatasetSize() + 1)) {
    datasetLenetTest.getNext([input, label])
    var (loss, bpFunc) = adj(net, lossFn, input, label)
    var gradient: LeNetModel = bpFunc(Tensor(Float32(1.0)))
    optim.update(gradient)
    let lossValue = loss.evaluate().toScalar<Float32>()   // 这里调用 `evaluate` 确保静态图模式下能够正确的执行计算
    if (i % 100 == 0) {
        print("step: ${i}, loss is: ${lossValue}\n")
    }
}

这就是一个神经网络模型的完整训练过程了。执行如上代码,一般可以看到模型 loss 值逐步减小,代表模型收敛。如果遇到模型不收敛的情况,可以尝试调整超参。