入门指南
软件安装和使用
安装仓颉编译器
仓颉 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
。调用loss
的 evaluate() 方法将触发计算图的执行,并返回计算结果。计算结果是一个有值的Tensor
,可以用 toArrayFloat32() 方法转为数组。 - 动态图模式下,所有的计算都是立即执行的。不需要调用
loss
的 evaluate() 方法,就可以直接调用 toArrayFloat32() 方法转为数组。动态图模型下调用loss
的 evaluate() 方法,不产生任何影响。 - 如果不确定某个 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
}