构建神经网络

仓颉 TensorBoost 内置常见的神经网络结构(如 2D 卷积层、全连接层等),同时也支持用户自定义神经网络结构。

定义网络结构

网络结构包含网络构建过程参与的状态数据(如网络参数、控制属性等)。仓颉 TensorBoost 通过 struct 定义神经网络。网络结构可以包含某些权重参数(类型为 Tensor),也可以包含其他网络结构。

下面以 MyLayer 为例定义某个矩阵乘法和 bias 叠加的神经网络。MyLayer 成员变量包括用于矩阵乘法的权重参数、偏差参数和控制是否叠加偏差参数的属性。构造函数(init)根据入参里的输入通道数、输出通道数等初始化成员变量。

from CangjieTB import macros.*
from CangjieTB import ops.*

@OptDifferentiable[include: [weight, bias]]
struct MyLayer {
    let weight: Tensor // 权重参数
    let bias: Tensor   // 偏差参数
    var hasBias: Bool  // 控制是否叠加 bias
    init(inChannels: Int64, outChannels: Int64, hasBias: Bool)
    {
        this.hasBias = hasBias
        this.weight = parameter(initialize([outChannels, inChannels], initType: InitType.NORMAL), "weight")
        if (hasBias) {
            this.bias = parameter(initialize([outChannels], initType: InitType.ZERO), "bias")
        } else {
            this.bias = noneTensor()
        }
    }
}

说明:

  • @OptDifferentiable 用于修饰网络结构类型,标注该网络支持反向传播。同时,将参与反向传播梯度更新的参数变量加入到 include 列表。当网络结构类型没有成员变量或所有成员变量都是待更新的网络参数时,@OptDifferentiable 可省略不写 include。
  • 要求 struct 所有成员变量必须进行初始化(声明处或构造函数处):MyLayer 即使不需要 bias ,也要给 bias 成员变量赋值。
  • 使用 @OptDifferentiable 功能的编译命令可参考[第一个仓颉 TensorBoost 程序](##第一个仓颉 TensorBoost 程序)。

创建和收集 Parameter

网络结构里很重要的状态数据就是网络运算过程中用到的参数(Parameter)。可以理解为网络参数是具有全局生命周期的 Tensor ,有着明确的、唯一存储空间,支持原址修改数据(通过 assign / assignAdd等)。网络参数创建的唯一方式是调用函数 parameter(),函数声明如下:

public func parameter(data: Tensor, name: String, requiresGrad!: Bool = true): Tensor

入参说明:

  • data 必须是提前构造好的有值的 Tensor ,data 不能是其他 parameter() 函数返回值,也不能将同一个 data 传给两个 parameter() 做参数。
  • name 为网络参数初始名字,网络结构里的参数经过收集之后会统一重命名。
  • requiresGrad 控制网络参数是否需要更新梯度,默认是 true。如果存在增量训练或其他需要固定住部分网络参数的场景,可将该选项设置为 false。

网络结构收集参数通过实现 OptDiff 接口里的 collectParams()工作的。

public interface OptDiff {
    public func collectParams(params: ArrayList<Tensor>, renamePrefix: String): Unit
}

说明:

Unit 是仓颉函数的默认返回值,调用时可不处理该返回值。

所有被 @OptDifferentiable 修饰的网络结构类型默认支持收集网络参数。因此对于 MyLayer 可直接通过下面代码收集网络参数,得到两个参数分别名为 myLayer.weight 和 myLayer.bias。

let myLayer = MyLayer(4, 4, true)
// 准备参数数组
let params: ArrayList<Tensor> = ArrayList<Tensor>()
// (递归)收集 myLayer 包含的全部网络参数到参数数组
myLayer.collectParams(params, "myLayer")

定义网络运算方法

网络运算方法定义神经网络构建过程。仓颉 TensorBoost 通过 operator() 运算符重载定义网络运算方法。方法可以调用算子函数(如 matmulbiasAdd),也可以调用其他网络结构的 operator() 方法。MyLayer 网络运算代码示意如下:

from CangjieTB import macros.*
from CangjieTB import ops.*

@OptDifferentiable[include: [weight, bias]]
struct MyLayer {
    let weight: Tensor // 权重参数
    let bias: Tensor   // 偏差参数
    var hasBias: Bool  // 控制是否叠加 bias

    init(inChannels: Int64, outChannels: Int64, hasBias: Bool)
    {
        this.hasBias = hasBias
        this.weight = parameter(initialize([outChannels, inChannels], initType: InitType.NORMAL), "weight")
        if (hasBias) {
            this.bias = parameter(initialize([outChannels], initType: InitType.ZERO), "bias")
        } else {
            this.bias = noneTensor()
        }
    }

    @Differentiable
    operator func ()(input: Tensor): Tensor {
        var output = matmul(input, this.weight)
        if (this.hasBias) {
            output = biasAdd(output, this.bias)
        }
        return output
    }
}

说明:

  • @Differentiable 修饰 MyLayer operator() 网络运算方法,标注该运算过程参与反向传播。仓颉 TensorBoost 基于用户定义的网络运算代码分析生成反向传播需要的求导代码,用于模型训练等场景下对 MyLayer 网络运算的自动求导。
  • @Differentiable 默认将函数所有入参当做求导需要累计梯度的对象。如果不是,比如入参除了输入 Tensor 以外还有其他不需要求导的变量,需要将输入 Tensor 加入到 include 列表。
  • 当神经网络 A 和 B 存在一些公共子结构 C,产生代码复用的编程需求,可考虑通过 struct 定义公共子结构 C,再在网络 A/B 中将 C 作为成员变量包含进来。

如果某个输入的 Tensor 可能为空(对应 python 语言中某个入参为 None 的情况),可以调用 noneTensor() 函数创建一个空的 Tensor ,并在网络的 operator() 方法中调用 isNone() 来判断该入参是否为空。示例代码如下,OptionalLayer 网络的第二个输入 optional 可能为空。当 optional 为空时,返回 inputIds 和 weight_ 之和,否则返回 inputIds 与 optional 的差:

from CangjieTB import macros.*
from CangjieTB import ops.*

@OptDifferentiable[include: [weight_]]
struct OptionalLayer {
    let weight_: Tensor

    init(weight: Tensor){
        weight_ = parameter(weight, "weight_")
    }

    @Differentiable[except: [optional]]
    operator func ()(inputIds: Tensor, optional: Tensor): Tensor {
        if (optional.isNone()) {
            inputIds + this.weight_
        } else {
            inputIds - optional
        }
    }
}

测试代码如下:

let opLayer = OptionalLayer(onesTensor(Array<Int64>([2, 2])))
let inputId = onesTensor(Array<Int64>([2, 2]))
let optional1 = onesTensor(Array<Int64>([2, 2]))
let optional2 = noneTensor()
let out1 = opLayer(inputId, optional1)
let out2 = opLayer(inputId, optional2)
print("case 1: ", out1)
print("case 2: ", out2)

输出为:

case 1:
Tensor(shape=[2, 2], dtype=Float32, value=
[[0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00]])
case 2:
Tensor(shape=[2, 2], dtype=Float32, value=
[[2.00000000e+00 2.00000000e+00]
 [2.00000000e+00 2.00000000e+00]])