Open kevinJhz opened 2 years ago
下面我们介绍如何使用 libai 构建一个 MNIST 手写数字识别项目。
MNIST数据集(Mixed National Institute of Standards and Technology database)是美国国家标准与技术研究院收集整理的大型手写数字数据集,包含了60,000个样本的训练集以及10,000个样本的测试集。
本项目位于 libai/projects/MNIST
,其中 MNIST
是本项目的主目录,libai/projects
下统一管理并存放了 Libai 开源社区的 projects。
libai/projects/MNIST
├── configs
│ └── config.py # 全局配置文件
│ └── graph.py # graph 配置
│ └── optim.py # optimizer 配置
│ └── train.py # train 相关配置
├── dataset
│ ├── dataset.py # DataSet类
├── modeling
│ ├── model.py # 定义神经网络模型
├── README.md
我们将模型定义在 modeling.py 文件中:
# modeling/model.py
import oneflow.nn as nn
class NeuralNetwork(nn.Module):
def __init__(self, num_classes = 10):
super(NeuralNetwork, self).__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, num_classes),
)
self.loss_func = nn.CrossEntropyLoss()
def forward(self, inputs, labels):
inputs = self.flatten(inputs)
logits = self.linear_relu_stack(inputs)
if labels is not None and self.training:
losses = self.loss_func(logits, labels)
return {"losses": losses}
else:
return {"prediction_scores": logits}
包括 __init__
和 forward(...)
2 个函数,其中 __init__
接收初始化参数,完成对 计算单元
和 损失函数
的初始化,forward(...)
接收 input 等数据完成前向计算,并返回 logits 或 loss。
需要遵守以下规定:
oneflow.nn.Module
__init__
内将 损失函数
初始化为成员变量。如self.loss_func = nn.CrossEntropyLoss()
,原因是 Libai 要求在模型内部计算 loss 。forward(...)
所接收的参数来自于 DataSet 类 __getitem__
函数的返回值(下一节会讲到)。本例中的模型是一个图片分类模型,故接收 images 和 labels 两个参数。若使用 transformers 系列模型则需定义 input_id
, attention_mask
, tokentype_id
等参数。forward(...)
内需添加当前处于 training 或 eval 阶段的判断,若在 training 阶段则需计算 loss 并返回。forward(...)
的返回类型是一个 dict,在 training 期间返回 losses ,在 inference 期间返回 prediction_scores。Libai 使用 LazyConfig 系统,你只需将 model 视作全局配置文件 config.py
的一个组件,会在启动时将其实例化。
在全局 config.py
文件中,你首先要用 from modeling.model import NeuralNetwork
将模型导入,
再用 model = LazyCall(类名)(参数)
将其声明。第二个括号中的 args 应与模型 __init__
有相同的 key 。
from libai.config import LazyCall
from modeling.model import NeuralNetwork
...
model = LazyCall(NeuralNetwork)(num_classes=10)
...
注意
__init__
函数添加 @configurable
标签,使其接收一个 dict 类型的配置参数。# modeling/model.py
@configurable
def __init__(self,num_classes = 10):
...
# configs/config.py
...
cfg={
num_classes=10
}
model = LazyCall(NeuralNetwork)(cfg)
...
我们需要定义一个 和 PyTorch Dataset 风格相似的 DataSet 类。
特别之处在于,我们要在 __getitem__
内部对 data 进行封装,return 一个 Instance 类型的对象。而不是返回 Tensor 或别的类型。
# dataset/dataset.py
import oneflow as flow
from flowvision import transforms
from flowvision import datasets
from oneflow.utils.data import Dataset
from libai.data.structures import DistTensorData, Instance
class MnistDataSet(Dataset):
def __init__(self, path, is_train):
self.data = datasets.MNIST(
root=path,
train=is_train,
transform=transforms.ToTensor(),
download=True,
source_url="https://oneflow-public.oss-cn-beijing.aliyuncs.com/datasets/mnist/MNIST/"
)
def __getitem__(self, idx):
sample = Instance(
inputs=DistTensorData(
flow.tensor(self.data[idx][0].clone().detach(), dtype=flow.float32)
),
labels=DistTensorData(
flow.tensor(self.data[idx][1], dtype=flow.int),
placement_idx=-1)
)
return sample
def __len__(self):
return len(self.data)
oneflow.utils.data.Dataset
__init__
函数完成数据集的加载。__getitem__
函数接收 int 型的 index 下标,返回该下标对应的样本。*Libai 提供了 DistTensorData 和 Instance 两个类,以加载训练数据,你需要完成以下 4 个步骤:
原始数据
转换为 oneflow.tensor
类型。oneflow.tensor
转换为 DistTensorData
类型。在获取 batch 数据时,DistTensorData
内部会调用 to_global()
将 local tensor
转化为 global tensor
。字段名=字段值
的形式,将 DistTensorData
数据存储到 Instance
中。Instance
对象会将这些属性值(如 image,label)存储为自身 fields。getitem 函数返回的 Instance 必须包含与 model 的 forward 函数的args有相同的 key 。Instance
对象。__len__
函数返回样本数量
注意 : 若某 tensor 不参与前向传播(只传递给 loss_function 计算 loss ),则可以设置 DistTensorData 的 placement_idx=-1 ,便于配置流水并行训练。在全局 config.py
文件中,你同样要先用 from dataset.dataset import MnistDataSet
将自定义 DataSet 导入。
configs/config.py
from omegaconf import OmegaConf
from libai.config import LazyCall
from libai.data.build import build_image_train_loader, build_image_test_loader
from dataset.dataset import MnistDataSet
dataloader = OmegaConf.create()
dataloader.train = LazyCall(build_image_train_loader)(
dataset=[
LazyCall(MnistDataSet)(
path="/workspace/quickstart/data/",
is_train=True,
)
],
num_workers=4,
)
dataloader.test = [LazyCall(build_image_test_loader)(
dataset=LazyCall(MnistDataSet)(
path="/workspace/quickstart/data/",
is_train=False,
),
num_workers=4,
)]
dataloader = OmegaConf.create()
flow.utils.Dataset
类型的 DataSet (此处传入上文定义的MnistDataSet
)。dataloader.test 用于实例化一个 list[flow.utils.Dataset]
。build_image_train_val_test_loader
或 build_nlp_train_val_test_loader
方法,这样 eval 过程将使用valid和test数据集来进行train 是用于训练和评估的配置,Libai 默认的 training 配置存放在 configs/common/train.py
中。
本项目在 config.py 文件中用 from configs.train import train
导入 Libai 默认 training 配置
如果要定义自己的 training 配置,你可以 configs/train.py
中,然后用 train = get_config("./train.py").train
导入。
train_iter=10000
直接指定train_epoch = 500
+ iter_per_epoch = 10
分开指定,总 iteration 数等于 train_epoch * iter_per_epoch = 5000
from libai.config import LazyCall
train = dict(
# 输出路径
output_dir="./output",
# train_micro_batch_size 每个 mini-batch 拆分到每个 GPU 上的 micro-batch 的大小。
# train_mini_batch_size 每个 GPU 在 1 个 step(iteration)的训练样本数。
# train_mini_batch_size = train_micro_batch_size * num_accumulation_steps。
# global_batch_size = micro_batch_size * num_accumulation_steps * data_parallel_groups
train_micro_batch_size=32,
global_batch_size=None,
num_accumulation_steps=None,
# training 的总 itreation 数。
train_iter=10000,
# 实际总的 training iteration 次数将取 `train_iter` 和 `train_epoch * iter_per_epoch`两者的最大值
train_epoch=0,
consumed_train_samples=0,
consumed_valid_samples=0,
train_samples=None,
# warmup_ratio,用于计算有多少 iteration 用于 warmup。
warmup_ratio=0,
# 起始 iteration,一般不用手动设置。重启 training 时可被自动计算。
start_iter=0,
# 启动 AMP 自动混合精度进行训练(不影响推理性能)。
amp=dict(enabled=False),
# 启用激活检查点(activation checkpointing),允许使用更大的 model、sequence和 batch size 进行训练。
# 启用后,会默认检查每个 transformer 层的输入激活。
activation_checkpoint=dict(enabled=False),
# NCCL 融合阈值 兆字节。一般设为 0 ,用于与之前的 oneflow 版本兼容。
nccl_fusion_threshold_mb=16,
# NCCL 融合的最大操作数
# 一般设置 0 来与之前的 oneflow 版本兼容。
nccl_fusion_max_ops=24,
# 如果训练较大的模型, 可以启用 ZeRO 优化。ZeRO 可以减少 optimize 阶段的内存消耗
zero_optimization=dict(
enabled=False,
stage=1,
),
# 配置checkpointer,period 字段用于指定'多少次 iteration 后保存一个 checkpoint',max_to_keep 字段指定'最多保存多少个 checkpoint'。
checkpointer=dict(period=5000, max_to_keep=100),
# evaluation 相关配置
# `test_micro_batch_size` 指定每个 batch 在单个 GPU 上用于 test 的样本数。
# 如果对数据并行组使用 8 个 GPU,并且 `test_micro_batch_size = 2`,则所有 GPU 每次迭代将总共使用 16 个样本。test 不进行梯度累积。
test_micro_batch_size=32,
# 是否在训练期间启用 eval,每经过`eval_period`次训练迭代后,执行 eval 。
# 可以设置最大 evaluation iteration,用于validation/test。
# 可以使用自定义的 evaluator。
evaluation=dict(
enabled=True,
# evaluator for calculating top-k acc
evaluator=LazyCall(ClsEvaluator)(topk=(1, 5)),
eval_period=5000,
eval_iter=1e9, # running steps for validation/test
# Metrics to be used for best model checkpoint.
eval_metric="Acc@1",
eval_mode="max",
),
# 指定要导入的模型 checkpoint 的路径。
load_weight="",
# 每经过 log_period 次迭代后,将日志输出到控制台。
log_period=20,
# libai.scheduler 的相关参数,详见 libai/scheduler/lr_scheduler.py
scheduler=LazyCall(WarmupCosineLR)(
# In DefaultTrainer we will automatically set `max_iter`
# and `warmup_iter` by the given train cfg.
# 我们将根据给定的训练配置,设置 DefaultTrainer 的 max_iter 和 warmup_iter 值。
warmup_factor=0.001,
alpha=0.01,
warmup_method="linear",
),
# 分布式相关的参数,详见 [Distributed_Configuration](https://libai.readthedocs.io/en/latest/tutorials/basics/Distributed_Configuration.html)
dist=dict(
data_parallel_size=1,
tensor_parallel_size=1,
pipeline_parallel_size=1,
),
# 设置随机种子值(必须是正数)
seed=1234,
)
gcc main.cpp -o main.exe
gcc完成了 预编译-编译-链接 共3个过程 gcc -E main.cpp > main.i 预编译。将main.cpp
里的预编译指令转换成c++代码。 预编译指令包括:include头文件包含指令。如
#include<stdio.h>
被替换成stdio.h
头文件里的源码。define 宏定义指令。 如# define MAX 200,将MAX替换成200
条件编译指令。如#if、#else和#endif指令