mmengine/docs/zh_cn/tutorials/model.md

171 lines
9.3 KiB
Markdown
Raw Normal View History

# 模型
在训练深度学习任务时,我们通常需要定义一个模型来实现算法的主体。在基于 MMEngine 开发时,模型由[执行器](./runner.md)管理,需要实现 `train_step``val_step` 和 `test_step` 方法。
对于检测、识别、分割一类的深度学习任务,上述方法通常为标准的流程,例如在 `train_step` 里更新参数,返回损失;`val_step` 和 `test_step` 返回预测结果。因此 MMEngine 抽象出模型基类 [BaseModel](mmengine.model.BaseModel),实现了上述接口的标准流程。我们只需要让模型继承自模型基类,并按照一定的规范实现 `forward`,就能让模型在执行器中运行起来。
模型基类继承自[模块基类](./initialize.md),能够通过配置 `init_cfg` 灵活的选择初始化方式。
## 接口约定
[forward](mmengine.model.BaseModel.forward): `forward` 的入参需要和 [DataLoader](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html) 的输出保持一致 (自定义[数据处理器](数据处理器DataPreprocessor)除外),如果 `DataLoader` 返回元组类型的数据 `data``forward` 需要能够接受 `*data` 的解包后的参数;如果返回字典类型的数据 `data``forward` 需要能够接受 `**data` 解包后的参数。 `mode` 参数用于控制 forward 的返回结果:
- `mode='loss'``loss` 模式通常在训练阶段启用,并返回一个损失字典。损失字典的 key-value 分别为损失名和可微的 `torch.Tensor`。字典中记录的损失会被用于更新参数和记录日志。模型基类会在 `train_step` 方法中调用该模式的 `forward`
- `mode='predict'` `predict` 模式通常在验证、测试阶段启用,并返回列表/元组形式的预测结果,预测结果需要和 [process](mmengine.evaluator.Evaluator) 接口的参数相匹配。OpenMMLab 系列算法对 `predict` 模式的输出有着更加严格的约定,需要输出列表形式的[数据元素](./data_element.md)。模型基类会在 `val_step``test_step` 方法中调用该模式的 `forward`
- `mode='tensor'``tensor` 和 `predict` 模式均返回模型的前向推理结果,区别在于 `tensor` 模式下,`forward` 会返回未经后处理的张量例如返回未经非极大值抑制nms处理的检测结果返回未经 `argmax` 处理的分类结果。我们可以基于 `tensor` 模式的结果进行自定义的后处理。
[train_step](mmengine.model.BaseModel.train_step): 调用 `loss` 模式的 `forward` 接口,得到损失字典。模型基类基于[优化器封装](./optim_wrapper.md) 实现了标准的梯度计算、参数更新、梯度清零流程。
[val_step](mmengine.model.BaseModel.val_step): 调用 `predict` 模式的 `forward`,返回预测结果,预测结果会被进一步传给[评测器](./metric_and_evaluator.md)的 [process](mmengine.evaluator.Evaluator.process) 接口和[钩子Hook](./hook.md)的 `after_val_iter` 接口。
[test_step](mmengine.model.BaseModel.test_step): 同 `val_step`,预测结果会被进一步传给 `after_test_iter` 接口。
基于上述接口约定,我们定义了继承自模型基类的 `NeuralNetwork`,配合执行器来训练 `FashionMNIST`
```python
from torch.utils.data import DataLoader
from torch import nn
from torchvision import datasets
from torchvision.transforms import ToTensor
from mmengine.model import BaseModel
from mmengine.evaluator import BaseMetric
from mmengine import Runner
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
train_dataloader = DataLoader(dataset=training_data, batch_size=64)
test_dataloader = DataLoader(dataset=test_data, batch_size=64)
class NeuralNetwork(BaseModel):
def __init__(self, data_preprocessor=None):
super(NeuralNetwork, self).__init__(data_preprocessor)
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, 10),
)
self.loss = nn.CrossEntropyLoss()
def forward(self, img, label, mode='tensor'):
x = self.flatten(img)
pred = self.linear_relu_stack(x)
loss = self.loss(pred, label)
if mode == 'loss':
return dict(loss=loss)
elif mode=='predict':
return pred.argmax(1), loss.item()
else:
return pred
class FashionMnistMetric(BaseMetric):
def process(self, data, preds) -> None:
# data 参数为 Dataloader 返回的元组,即 (img, label)
# predict 为模型 `predict` 模式下,返回的元组,分别为 `pred.argmax(1) 和 `loss``
self.results.append(((data[1] == preds[0].cpu()).sum(), preds[1], len(preds[0])))
def compute_metrics(self, results):
correct, loss, batch_size = zip(*results)
test_loss, correct = sum(loss) / len(self.results), sum(correct) / sum(batch_size)
return dict(Accuracy=correct, Avg_loss=test_loss)
runner = Runner(
model=NeuralNetwork(),
work_dir='./work_dir',
train_dataloader=train_dataloader,
optim_wrapper=dict(optimizer=dict(type='SGD', lr=1e-3)),
train_cfg=dict(by_epoch=True, max_epochs=5, val_interval=1),
val_cfg=dict(fp16=True),
val_dataloader=test_dataloader,
val_evaluator=dict(metrics=FashionMnistMetric()))
runner.train()
```
在这个例子中,`NeuralNetwork.forward` 存在着以下跨模块的接口约定:
- 由于 `train_dataloader` 会返回一个 `(img, label)` 形式的元组,因此 `forward` 接口的前两个参数分别需要为 `img``label`
- 由于 `forward``predict` 模式下会返回 `(pred, loss)` 形式的元组,因此 `process``preds` 参数应当同样为相同形式的元组。
相比于 [Pytorch 官方示例](https://pytorch.org/tutorials/beginner/basics/optimization_tutorial.html#)MMEngine 的代码更加简洁,记录的日志也更加丰富。
## 数据处理器DataPreprocessor
如果你的电脑配有 GPU或其他能够加速训练的硬件如 mps、ipu 等),并运行了上节的代码示例。你会发现 Pytorch 的示例是在 CPU 上运行的,而 MMEngine 的示例是在 GPU 上运行的。`MMEngine` 是在何时把数据和模型从 CPU 搬运到 GPU 的呢?
事实上,执行器会在构造阶段将模型搬运到指定设备,而数据则会在 `train_step`、`val_step`、`test_step` 中,被[基础数据处理器BaseDataPreprocessor](mmengine.model.BaseDataPreprocessor)搬运到指定设备,进一步将处理好的数据传给模型。数据处理器作为模型基类的一个属性,会在模型基类的构造过程中被实例化。
为了体现数据处理器起到的作用,我们仍然以[上一节](接口约定)训练 FashionMNIST 为例, 实现了一个简易的数据处理器,用于搬运数据和归一化:
```python
from torch.optim import SGD
from mmengine.model import BaseDataPreprocessor, BaseModel
class NeuralNetwork1(NeuralNetwork):
def __init__(self, data_preprocessor):
super().__init__(data_preprocessor=data_preprocessor)
self.data_preprocessor = data_preprocessor
def train_step(self, data, optimizer):
img, label = self.data_preprocessor(data)
loss = self(img, label, mode='loss')['loss'].sum()
loss.backward()
optimizer.step()
optimizer.zero_grad()
return dict(loss=loss)
def test_step(self, data):
img, label = self.data_preprocessor(data)
return self(img, label, mode='predict')
def val_step(self, data):
img, label = self.data_preprocessor(data)
return self(img, label, mode='predict')
class NormalizeDataPreprocessor(BaseDataPreprocessor):
def forward(self, data, training=False):
img, label = [item for item in data]
img = (img - 127.5) / 127.5
return img, label
model = NeuralNetwork1(data_preprocessor=NormalizeDataPreprocessor())
optimizer = SGD(model.parameters(), lr=0.01)
data = (torch.full((3, 28, 28), fill_value=127.5), torch.ones(3, 10))
model.train_step(data, optimizer)
model.val_step(data)
model.test_step(data)
```
上例中,我们实现了 `BaseModel.train_step`、`BaseModel.val_step` 和 `BaseModel.test_step` 的简化版。数据经 `NormalizeDataPreprocessor.forward` 归一化处理,解包后传给 `NeuralNetwork.forward`,进一步返回损失或者预测结果。如果想实现自定义的参数优化或预测逻辑,可以自行实现 `train_step`、`val_step` 和 `test_step`,具体例子可以参考:[使用 MMEngine 训练生成对抗网络](../examples/train_a_gan.md)
```{note}
上例中数据处理器的 training 参数用于区分训练、测试阶段不同的批增强策略,`train_step` 会传入 `training=True``test_step` 和 `val_step` 则会传入 `trainig=Fasle`
```
```{note}
通常情况下,我们要求 DataLoader 的 `data` 数据解包后(字典类型的被 **data 解包,元组列表类型被 *data 解包)能够直接传给模型的 `forward`。但是如果数据处理器修改了 data 的数据类型,则要求数据处理器的 `forward` 的返回值与模型 `forward` 的入参相匹配。
```