入门指南

软件安装和使用

安装仓颉编译器

仓颉 TensorBoost 程序必须使用仓颉编译器进行编译,请参考《Cangjie Language Guide》入门指南中的指导安装仓颉编译器。

安装 MindSpore

仓颉 TensorBoost 已经适配 MindSpore 1.8.1 软件版本,需要用户下载 MindSpore 1.8.1 源码并编译:

$ git clone https://gitee.com/mindspore/mindspore.git
$ cd mindspore
$ git checkout v1.8.1  ## v1.8.1 commit id: 7b161d3df308c86d4618cf67c6303da5f58d036c

从源码编译 MindSpore ,在源码根目录执行如下命令:

$ # GPU平台
$ bash build.sh -e gpu
$ # 昇腾平台
$ bash build.sh -e ascend

安装 mindspore 软件包:

$ # GPU平台
$ cd build/package
$ pip install mindspore_gpu-1.8.1-cp37-cp37m-linux_x86_64.whl
$ # 昇腾平台
$ cd build/package
$ pip install mindspore_ascend-1.8.1-cp37-cp37m-linux_x86_64.whl

【备注】:

  • MindSpore 编译环境准备和编译指导请参考 MindSpore 安装
  • Python 版本使用 MindSpore 推荐的 3.7.5。

安装仓颉 TensorBoost

仓颉编译器的软件包中包含仓颉 TensorBoost 的软件,通过配置环境变量即可完成安装:

  • 配置仓颉 TensorBoost 环境变量
$ # GPU平台
$ source ./cangjie/envsetupTB.sh gpu
$ # 昇腾平台
$ source ./cangjie/envsetupTB.sh ascend

【说明】:仓颉程序默认栈空间大小是 1MB,堆空间大小是 256MB,程序运行时如遇栈或者堆空间不足,可通过环境变量调整修改其大小。

  • 栈空间不足(运行时报“StackOverflowError”错误),可通过设置 cjStackSize 环境变量调整栈空间大小。cjStackSize 的合法取值范围是 [64KB, 1GB]。

    $ export cjStackSize=4MB
    
  • 堆空间不足(运行时报“OutOfMemoryError”错误),可通过设置 cjHeapSize 环境变量调整堆空间大小。cjHeapSize 的合法取值范围是 [256MB, 256GB],且配置的数值应小于物理内存大小。

    $ export cjHeapSize=32GB
    

配置 Python 和 CUDA (或 CANN) 环境变量:

$ # GPU平台
$ export LIBRARY_PATH=/usr/public_tool/tool/miniconda3/envs/ci3.7/lib:/usr/local/cuda-11.1/lib64:${LIBRARY_PATH}
$ export LD_LIBRARY_PATH=/usr/public_tool/tool/miniconda3/envs/ci3.7/lib:/usr/local/cuda-11.1/lib64:/usr/local/cuda-11.1/extras/CUPTI/lib64:${LD_LIBRARY_PATH}
$ export PATH=/usr/local/cuda-11.1/bin:${PATH}
$ # 昇腾平台
$ export LOCAL_ASCEND=/usr/local/Ascend/
$ export LIBRARY_PATH=/usr/public_tool/tool/miniconda3/envs/ci3.7/lib
$ export LD_LIBRARY_PATH=/usr/public_tool/tool/miniconda3/envs/ci3.7/lib:${LOCAL_ASCEND}/latest/x86_64-linux/lib64:${LOCAL_ASCEND}/latest/fwkacllib/lib64:${LOCAL_ASCEND}/driver/lib64/driver:${LOCAL_ASCEND}/driver/lib64/common/:${LOCAL_ASCEND}/latest/opp/op_impl/built-in/ai_core/tbe/op_tiling/:${LD_LIBRARY_PATH}
$ export TBE_IMPL_PATH=${LOCAL_ASCEND}/latest/opp/op_impl/built-in/ai_core/tbe/
$ export ASCEND_OPP_PATH=${LOCAL_ASCEND}/latest/opp/
$ export PATH=${LOCAL_ASCEND}/latest/fwkacllib/ccec_compiler/bin/:${PATH}
$ export PYTHONPATH=${TBE_IMPL_PATH}:${PYTHONPATH}

【说明】:

  • MindSpore 的库依赖 Python 的库和 Python 环境,需要将 Python 的库添加到 LD_LIBRARY_PATH(示例命令中 /usr/public_tool/tool/miniconda3/envs/ci3.7/lib 请修改成 Python 实际对应的 lib 文件夹)。
  • 对于 GPU 平台,MindSpore 的库依赖 CUDA ,需要将 CUDA 添加到 PATH 中(示例命令中 /usr/local/cuda-11.1/bin 请修改成 CUDA 实际对应的 bin 文件夹)。
  • 对于昇腾平台,MindSpore 的库依赖 CANN ,需要将 CANN 添加到 PATH 中(示例命令中 /usr/local/Ascend/latest/ 请修改成 CANN 实际对应文件夹)。

第一个仓颉 TensorBoost 程序

以创建两个 Tensor,进行矩阵乘运算,并输出计算结果为例,将示例代码保存到 demo.cj 文件中。

from CangjieTB import ops.*
from CangjieTB import common.*

main(): Int64
{
    let input_x = Tensor(Array<Float32>([1.0, 2.0, 3.0, 4.0]), shape: Array<Int64>([2, 2]))
    let input_y = Tensor(Array<Float32>([1.0, 2.0, 3.0, 4.0]), shape: Array<Int64>([2, 2]))
    print(matmul(input_x, input_y))
    print("Cangjie TensorBoost run test success!\n")
    return 0
}

编译示例代码:

$ cjc --enable-ad --int-overflow=wrapping -lcangjie_tensorboost -lmindspore_wrapper ./demo.cj -o ./demo

【说明】:

  • 用户可以根据需要将程序分多个源文件开发,编译时只要将多个源文件一起添加到编译命令中即可。
  • 链接选项中,通过 -lcangjie_tensorboost 链接仓颉 TensorBoost 的库,通过 -lmindspore_wrapper 链接 MindSpore 的库。

运行示例程序:

$ ./demo
Tensor(shape=[2, 2], dtype=Float32, value=
[[7.00000000e+00 1.00000000e+01]
 [1.50000000e+01 2.20000000e+01]])
Cangjie TensorBoost run test success!

如果输出如上结果,说明仓颉 TensorBoost 已经安装成功。

小试牛刀

本节以 LeNet 网络训练为例介绍仓颉 TensorBoost 的基础功能。本章按功能进行了导包和代码介绍,完整可运行的代码见本章最后一节。

配置运行信息

仓颉 TensorBoost 程序默认以动态图模式运行,在程序运行前,通过设置环境变量 CANGJIETB_GRAPH_MODE=true ,可设置成静态图模式运行。此外,仓颉 TensorBoost 通过[g_context](##附录 A 运行环境管理)来配置其他运行需要的信息,如后端信息、硬件信息等。

g_context 对象定义在 context 包中,使用前需要先导入context包。设置仓颉 TensorBoost 程序使用 GPU 硬件:

from CangjieTB import context.*

main(): Int64
{
    // 设置运行设备为 GPU
    g_context.setDeviceTarget("GPU")

    // 测试代码写在此处

    return 0
}

设置运行设备为昇腾:

// 设置运行设备为 Ascend
g_context.setDeviceTarget("Ascend")

加载数据集

使用仓颉 TensorBoost 加载 MNIST 数据集

from CangjieTB import dataset.*

// 加载数据集
func createLenetDataset(path: String, batchSize!: Int32 = 32, shuffleSize!: Int32 = 10000, repeatCnt!: Int32 = 1, epoch!: Int32 = 1): Dataset
{
    var ds = MnistDataset(path, epoch: epoch)
    var resize = resize(Array<Int32>([32, 32]))
    var rescale_nml = rescale(1.0 / 0.3081, -1.0 * 0.1307 / 0.3081)
    var rescale = rescale(1.0 / 255.0, 0.0)
    var hwc2chw = hwc2chw()
    var typeCast = typeCast(INT32)
    ds.datasetMap([resize, rescale, rescale_nml, hwc2chw], "image")
    ds.datasetMap([typeCast], "label")
    ds.shuffle(shuffleSize)
    ds.batch(batchSize, true)
    ds.repeat(repeatCnt)
    return ds
}

创建模型

仓颉 TensorBoost 使用 struct 数据类型定义神经网络,@OptDifferentiable 注解用于使能仓颉 TensorBoost 自动收集网络权重。

神经网络的网络层需要在 struct 类型init()方法中初始化,通过重载 () 运算符方法来定义神经网络的前向构造。按照 LeNet 的网络结构,定义网络各层如下:

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.*

@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
    }
}

创建定义好的 LeNetModel 模型:

// 创建网络模型
var net = LeNetModel()

【说明】:|> 运算符是仓颉 提供的流运算符,用于表达单输入单输出网络结构的连接关系。上述示例中 () 运算符重载函数可重写为:

@Differentiable
operator func ()(input: Tensor): Tensor {
    var out = this.conv1(input)
    out = relu(out)
    out = this.pool(out)
    out = this.conv2(out)
    out = relu(out)
    out = this.pool(out)
    out = flatten(out)
    out = this.dense1(out)
    out = relu(out)
    out = this.dense2(out)
    out = relu(out)
    out = this.dense3(out)
    return out
}

优化模型参数

要训练神经网络模型,需要定义损失函数和优化器。

定义[交叉熵损失函数](####SoftmaxCrossEntropyWithLogits 层) :

// 创建损失函数对象
var lossFn = SoftmaxCrossEntropyWithLogits(sparse: true)

【说明】:自定义损失函数请参考损失函数

仓颉 TensorBoost 支持的优化器有 [Adam](####Adam 优化器)、[Momentum](####Momentum 优化器)。这里使用Momentum优化器:

// 设置学习率和动量,创建优化器对象
let lr: Float32 = 0.01
let momentum: Float32 = 0.9
var optim: MomentumOptimizer<LeNetModel> = MomentumOptimizer<LeNetModel>(net, lr, momentum)

【说明】:更多优化器的信息见优化器

训练及保存模型

计算网络梯度

定义可微函数train,用于网络前向结果的计算:

@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
}

定义函数gradient,对train函数求微分,用于网络反向梯度的计算:

func gradient(net: LeNetModel, lossFn: SoftmaxCrossEntropyWithLogits, input: Tensor, label: Tensor): (Tensor, LeNetModel)
{
    var adj = @AdjointOf(train)
    var (loss, fBackward) = adj(net, lossFn, input, label)
    var gradout = fBackward(onesLike(loss))
    return (loss, gradout)
}

【说明】:

  • [@AdjointOf](###可微函数和微分 API)是仓颉 TensorBoost 提供的获取可微函数的反向函数的关键字。
  • gradient函数的输出是网络前向的结果和网络反向的梯度。

训练网络

将下载好的 MNIST 数据集文件放到./data/mnist/路径下,网络训练代码如下。

// 创建承载数据集数据的 Tensor,作为网络的输入
let input = parameter(zerosTensor(Array<Int64>([32, 1, 32, 32]), dtype: FLOAT32), "data")
let label = parameter(zerosTensor(Array<Int64>([32]), dtype: INT32), "label")
var datasetLenet = createLenetDataset("./data/mnist/train", epoch: 2)

print("============== Starting Training ==============\n")
let epoch = 2
for (i in 0..epoch) {
    for (j in 1..(datasetLenet.getDatasetSize() + 1)) {
        // 将数据集的数据读取到这两个 Tensor 中
        datasetLenet.getNext([input, label])
        // 将输入、损失函数、网络对象传入到求微分的函数中
        var (loss, gradout) = gradient(net, lossFn, input, label)
        // 将得到的梯度传递给优化器
        optim.update(gradout)
        let lossValue = loss.evaluate().toScalar<Float32>()
        if (j % 100 == 0) {
            print("epoch: ${i}, step: ${j}, loss is: ${lossValue}\n")
        }
    }
}

【说明】:

  • 静态图模式下,所有的计算都是 lazy 的,网络的输出loss是一个无值的 Tensor。调用 lossevaluate() 方法将触发计算图的执行,并返回计算结果。计算结果是一个有值的 Tensor,可以用 toArrayFloat32() 方法转为数组。
  • 动态图模式下,所有的计算都是立即执行的。不需要调用 lossevaluate() 方法,就可以直接调用 toArrayFloat32() 方法转为数组。动态图模型下调用 lossevaluate() 方法,不产生任何影响。
  • 如果不确定某个 Tensor 是否有值,可以调用 isConcrete 方法进行判断。
  • LeNetModel 使用了宏功能,编译命令可参考[第一个仓颉 TensorBoost 程序](##第一个仓颉 TensorBoost 程序)。

训练过程中会打印 loss 值,类似下图。loss 值会波动,但总体来说 loss 值会逐步减小,精度逐步提高。每个人运行的 loss 值有一定随机性,不一定完全相同。 训练过程中 loss 打印示例如下:

epoch: 0, step: 100, loss is: 2.311390
epoch: 0, step: 200, loss is: 2.292652
epoch: 0, step: 300, loss is: 2.320347
epoch: 0, step: 400, loss is: 2.315219
epoch: 0, step: 500, loss is: 2.313322
epoch: 0, step: 600, loss is: 2.287148
epoch: 0, step: 700, loss is: 2.294824
epoch: 0, step: 800, loss is: 2.080675
epoch: 0, step: 900, loss is: 0.665705
epoch: 0, step: 1000, loss is: 0.486177
epoch: 0, step: 1100, loss is: 0.435502
epoch: 0, step: 1200, loss is: 0.291111
epoch: 0, step: 1300, loss is: 0.301397
epoch: 0, step: 1400, loss is: 0.139376
epoch: 0, step: 1500, loss is: 0.258150
epoch: 0, step: 1600, loss is: 0.029784
epoch: 0, step: 1700, loss is: 0.109121
epoch: 0, step: 1800, loss is: 0.023385
...

保存模型

public func saveCheckpoint(params: ArrayList<Tensor>, ckptFileName: String)

saveCheckpoint 函数可以保存网络模型和参数,以便进行后续的 Fine-tuning(微调)操作。 当训练结束时,可以调用 saveCheckpoint 来保存网络权重。

from CangjieTB import train.*
// 将训练好的模型保存
saveCheckpoint(optim.getParameters(), "./lenet.ckpt")

完成后可以看到本目录下多了一个名为lenet.ckpt的文件,网络的权重就保存在文件中。

加载模型

public func loadCheckpoint(params: ArrayList<Tensor>, ckptFileName: String)

创建 LeNet 模型对象,并获取模型中的参数,使用 loadCheckpoint 函数更新模型的参数。

// 重新创建新的网络模型
net = LeNetModel()
// 获取模型中的参数
optim = MomentumOptimizer<LeNetModel>(net, 0.01, 0.9)
var params: ArrayList<Tensor> = optim.getParameters()
// 加载 ckpt 文件中的数据到模型参数
loadCheckpoint(params, "./lenet.ckpt")

验证模型

观察加载的模型在测试集上的表现:

// 加载测试数据集
var datasetLenetTest = createLenetDataset("./data/mnist/test")
// 观察模型在测试集上的表现
for (i in 0..datasetLenetTest.getDatasetSize()) {
    datasetLenetTest.getNext([input, label])
    var loss = train(net, lossFn, input, label)
    // 触发计算图的执行
    let lossValue = loss.evaluate().toScalar<Float32>()
    print("sample: ${i}, loss is: ${lossValue}\n")
}

输出如下:

sample: 6, loss is: 0.107543
sample: 7, loss is: 0.267138
sample: 8, loss is: 0.0362831
sample: 9, loss is: 0.00709512
sample: 10, loss is: 0.00440472
sample: 11, loss is: 0.00594328
sample: 12, loss is: 0.00353992
sample: 13, loss is: 0.0153856
sample: 14, loss is: 0.113814
sample: 15, loss is: 0.0211988
sample: 16, loss is: 0.0751143
sample: 17, loss is: 0.00700548
sample: 18, loss is: 0.143039
sample: 19, loss is: 0.0867741
sample: 20, loss is: 0.0273039
sample: 21, loss is: 0.00415331
sample: 22, loss is: 0.127782
sample: 23, loss is: 0.064515
sample: 24, loss is: 0.0509498
sample: 25, loss is: 0.0671479
sample: 26, loss is: 0.15785
...

可以看到加载的模型在测试集上的 loss 非常低,与测试集训练后的 loss 接近。

完整的 LeNet 网络训练代码

仓颉 TensorBoost 的 lenet_example.cj 样例代码使用了仓颉 TensorBoost 的宏,其编译命令如下:

$ cjc --enable-ad --int-overflow=wrapping -lcangjie_tensorboost -lmindspore_wrapper lenet_example.cj --import-path ${CANGJIE_TB_HOME}/modules/CangjieTB -o ./lenet

lenet_example.cj 代码如下:

from CangjieTB import context.*
from CangjieTB import common.*
from CangjieTB import ops.*
from CangjieTB import dataset.*
from CangjieTB import nn.*
from CangjieTB import nn.optim.*
from CangjieTB import nn.loss.*
from CangjieTB import nn.layers.*
from CangjieTB import macros.*
from CangjieTB import train.*
from std import collection.ArrayList

// 加载数据集
func createLenetDataset(path: String, batchSize!: Int32 = 32, shuffleSize!: Int32 = 10000, repeatCnt!: Int32 = 1,
    epoch!: Int32 = 1) {
    var ds = MnistDataset(path, epoch: epoch)
    var resize = resize([32, 32])
    var rescale_nml = rescale(1.0 / 0.3081, -1.0 * 0.1307 / 0.3081)
    var rescale = rescale(1.0 / 255.0, 0.0)
    var hwc2chw = hwc2chw()
    var typeCast = typeCast(INT32)
    ds.datasetMap([resize, rescale, rescale_nml, hwc2chw], "image")
    ds.datasetMap([typeCast], "label")
    ds.shuffle(shuffleSize)
    ds.batch(batchSize, true)
    ds.repeat(repeatCnt)
    return ds
}

@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, [5, 5], pad_mode: "valid")
        conv2 = Conv2d(6, 16, [5, 5], pad_mode: "valid")
        pool = MaxPool2d(kernel_size: [2, 2], stride: [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
    }
}

@Differentiable [except: [input, label, lossFn]]
func train(net: LeNetModel, lossFn: SoftmaxCrossEntropyWithLogits, input: Tensor, label: Tensor) {
    var output = net(input)
    var lossTensor = lossFn(output, label)
    return lossTensor
}

func gradient(net: LeNetModel, lossFn: SoftmaxCrossEntropyWithLogits, input: Tensor, label: Tensor) {
    var adj = @AdjointOf(train)
    var (loss, f_backward) = adj(net, lossFn, input, label)
    var gradout = f_backward(onesLike(loss))
    return (loss, gradout)
}

main(): Int64 {
    // 设置运行设备为GPU
    g_context.setDeviceTarget("GPU")

    // 创建承载数据集数据的 Tensor,作为网络的输入
    let input = parameter(zerosTensor([32, 1, 32, 32], dtype: FLOAT32), "data")
    let label = parameter(zerosTensor([32], dtype: INT32), "label")
    var datasetLenet = createLenetDataset("./data/mnist/train", epoch: 2)

    // 创建网络模型
    var net = LeNetModel()

    // 创建损失函数对象
    var lossFn = SoftmaxCrossEntropyWithLogits(sparse: true)

    // 设置学习率和动量,创建优化器对象
    let lr: Float32 = 0.01
    let momentum: Float32 = 0.9
    var optim: MomentumOptimizer<LeNetModel> = MomentumOptimizer<LeNetModel>(net, lr, momentum)

    print("============== Starting Training ==============\n")
    let epoch = 2
    for (i in 0..epoch) {
        for (j in 1..(datasetLenet.getDatasetSize() + 1)) {
            // 将数据集的数据读取到这两个 Tensor 中
            datasetLenet.getNext([input, label])

            // 将输入、损失函数、网络对象传入到求微分的函数中
            var (loss, gradout) = gradient(net, lossFn, input, label)

            // 将得到的梯度传递给优化器
            optim.update(gradout)
            let lossValue = loss.evaluate().toScalar<Float32>()
            if (j % 100 == 0) {
                print("epoch: ${i}, step: ${j}, loss is: ${lossValue}\n")
            }
        }
    }
    saveCheckpoint(optim.getParameters(), "./lenet.ckpt")

    // 重新创建新的网络模型
    net = LeNetModel()

    // 获取模型中的参数
    optim = MomentumOptimizer<LeNetModel>(net, 0.01, 0.9)
    var params: ArrayList<Tensor> = optim.getParameters()

    // 加载ckpt文件中的数据到模型参数
    loadCheckpoint(params, "./lenet.ckpt")

    // 加载测试数据集
    var datasetLenetTest = createLenetDataset("./data/mnist/test")

    print("============== Starting Test ==============\n")

    // 观察模型在测试集上的表现
    for (i in 0..datasetLenetTest.getDatasetSize()) {
        datasetLenetTest.getNext([input, label])
        var loss = train(net, lossFn, input, label)

        // 触发计算图的执行
        let lossValue = loss.evaluate().toScalar<Float32>()
        print("sample: ${i}, loss is: ${lossValue}\n")
    }
    return 0
}