PyTorch Ignite使用教程
介绍
PyTorch是一个用于训练深度神经网络模型的人工智能框架。目前使用PyTorch训练模型时通常包含以下几个步骤:
- 定义数据集和模型
- 划分数据集,并转换为DataLoader
- 确定需要收集的数据,如准确率、损失函数、训练时间等
- 确定训练模型的超参数,如学习率、Optimizer等
- 多次迭代DataLoader,训练模型,收集参数
为了提高训练模型的效率,PyTorch官方推出了一个名为Ignite的框架,以便更灵活高效地训练模型、收集数据。同时,Ignite还提供了一些模板,用以进一步简化开发的流程。
安装
pip install pytorch-ignite # pip
conda install pytorch-ignite -c pytorch # conda
其他安装选项可以在****How to install PyTorch-Ignite中进行查看。****
入门(以MNIST数据集为例)
-
定义模型:
import torch.nn as nn class Model(nn.Net): def __init__(self): super(Model, self).__init__() self.conv1 = nn.Sequence(nn.Conv2d(1, 20, kernel_size=3), nn.ReLU()) self.conv2 = nn.Sequence(nn.Conv2d(20, 10, kernel_size=3), nn.ReLU()) self.fc1 = nn.Sequence(nn.Linear(7840, 50), nn.ReLU(), nn.Dropout()) self.fc2 = nn.Sequence(nn.Linear(50, 10), nn.ReLU()) def forward(self, x): x = self.conv2(self.conv1(x)) x = x.view(-1) x = self.fc2(self.fc1(x)) return x
-
确定学习率,优化器和训练设备等:
import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = Model().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.005) criterion = nn.CrossEntropyLoss()
-
定义数据集和DataLoader:
from torchvision.transforms import Compose, Normalize, ToTensor from torchvision.datasets import MNIST data_transform = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))]) train_loader = DataLoader( MNIST(download=True, root=".", transform=data_transform, train=True), batch_size=128, shuffle=True ) val_loader = DataLoader( MNIST(download=True, root=".", transform=data_transform, train=False), batch_size=256, shuffle=False )
-
定义训练步骤以及Metrics(涉及到Ignite):
from ignite.engine import Engine, Events, create_supervised_trainer, create_supervised_evaluator from ignite.metrics import Accuracy, Loss from ignite.contrib.handlers import TensorboardLogger, global_step_from_engine trainer = create_supervised_trainer(model, optimizer, criterion, device) val_metrics = { "accuracy": Accuracy(), "loss": Loss(criterion) } train_evaluator = create_supervised_evaluator(model, metrics=val_metrics, device=device) val_evaluator = create_supervised_evaluator(model, metrics=val_metrics, device=device)
-
输出日志,方便查看训练过程:
log_interval = 100 # 每迭代数据集100次之后输出日志 @trainer.on(Events.ITERATION_COMPLETED(every=log_interval)) def log_training_loss(engine): print(f"Epoch[{engine.state.epoch}], Iter[{engine.state.iteration}] Loss: {engine.state.output:.2f}")
其中engine.state中储存了训练过程中必要的信息,比如输出、各种Metric、epoch、迭代次数等,用户也可以在其中添加自己所需要的内容。
-
在每个训练epoch结束以及模型训练结束后进行模型验证:
@trainer.on(Events.EPOCH_COMPLETED) def log_training_results(trainer): train_evaluator.run(train_loader) metrics = train_evaluator.state.metrics print(f"Training Results - Epoch[{trainer.state.epoch}] Avg accuracy: {metrics['accuracy']:.2f} Avg loss: {metrics['loss']:.2f}") @trainer.on(Events.EPOCH_COMPLETED) def log_validation_results(trainer): val_evaluator.run(val_loader) metrics = val_evaluator.state.metrics print(f"Validation Results - Epoch[{trainer.state.epoch}] Avg accuracy: {metrics['accuracy']:.2f} Avg loss: {metrics['loss']:.2f}")
-
为模型添加Tensorboard logger:
from ignite.handlers import ModelCheckpoint from ignite.contrib.handlers import TensorboardLogger, global_step_from_engine tb_logger = TensorboardLogger(log_dir="tb-logger") # 训练时每100次迭代后保存训练结果 tb_logger.attach_output_handler( trainer, event_name=Events.ITERATION_COMPLETED(every=100), tag="training", output_transform=lambda loss: {"batch_loss": loss}, ) # 验证时每个epoch结束后保存训练结果 for tag, evaluator in [("training", train_evaluator), ("validation", val_evaluator)]: tb_logger.attach_output_handler( evaluator, event_name=Events.EPOCH_COMPLETED, tag=tag, metric_names="all", global_step_transform=global_step_from_engine(trainer), )
-
为模型添加Checkpoint,保存训练结果较好的模型:
# 保存Checkpoint时的验证指标 def score_function(engine): return engine.state.metrics["accuracy"] model_checkpoint = ModelCheckpoint( "checkpoint", n_saved=2, filename_prefix="best", score_function=score_function, score_name="accuracy", global_step_transform=global_step_from_engine(trainer) ) val_evaluator.add_event_handler(Events.COMPLETED, model_checkpoint, {"model": model})
-
开始模型的训练:
trainer.run(trainer_loader, max_epochs=10)
Ignite中的Engine
训练模型的步骤
在深度学习中,模型的训练可以用以下代码表示:
for i in range(max_epochs):
data_iter = iter(data_loader)
for data in data_iter:
output = train_step(data)
其中process_function中包含了较多的步骤,如切换模型的训练/验证模式,计算损失函数等,以有监督学习为例,代码如下:
def train_step(_engine, data):
model.train()
optimizer.zero_grad()
x, y = prepare_data(data)
y_pred = model(x)
loss = loss_fn(y_pred, y)
loss.backward()
optimizer.step()
return loss.item()
因此一个trainer就是在训练时不断地迭代数据集,同时更新模型参数。针对上述代码,使用Ignite定义trainer时的代码如下所示:
from ignite.engine import Engine
trainer = Engine(train_step)
Ignite提供了create_supervised_trainer和create_supervised_evaluator两个方法,用于自定义Engine,二者的定义和说明如下:
def create_supervised_trainer(model, optimizer, loss_fn, device=None, non_blocking=False,
prepare_batch=_prepare_batch, output_transform=_output_transform,
deterministic=False, amp_mode=False, scaler=False, gradient_accumulation_step=1):
...
def create_supervised_evaluator(model, metrics=None, device=None, non_blocking=False,
prepare_batch=_prepare_batch, output_transform=_output_transform, amp_mode=None):
...
- model、optimizer、loss_fn、device:深度学习中的模型、优化器、损失函数和训练设备。
- non_blocking:如果为True,并且存在CPU和GPU之间的数据转移,那么转移的过程将使用多线程。
- prepare_batch:一个函数,参数为batch、device和non_blocking,返回元组(batch_x,batch_y),参见训练过程中的输入输出。
- output_transform:一个函数,接收模型的输出并转换成损失函数需要的形式,参见训练过程中的输入输出。
- deterministic:如果为True,则返回Engine的子类型DeterministicEngine,参见让训练结果可复现。
- amp_mode:加速模型训练的模式,可选amp或apex。如果指定为amp,则使用AMP,如果为apex,则使用NVIDIA APEX。
- scaler:GradScaler实例/bool,在PyTorch版本≥1.6.0且amp_mode为amp时有效。如果指定为True,则自动创建一个默认实例。
- gradient_accumulation_step:每n次迭代数据后进行反向传播,默认为1(这个操作可以减轻显存的使用情况)。
- metrics:一个由名称-Metric为键值对的字典,参见Metrics。
Ignite还提供了其他的方法用于定义trainer和evaluator,参考链接如下:Helper methods to define supervised trainer and evaluator.
训练过程中的输入输出
- 在Ignite中,创建Engine需要传递一个process_function,其类型为Callable[[Engine, Any], Any],函数接受的参数为Engine实例和训练中每个batch的数据,同时这也是前面提到的prepare_batch的参数之一。
- Ignite对process_function的输出格式没有明确的规定,但是process_function的输出一般是作为存放在engine.state.engine中,并在使用Metric时作为output_transform的参数,因此需要注意二者之间格式的统一。
- 使用create_supervised_trainer和create_supervised_evaluator创建Engine时,process_function的输出一般为loss.item()和元组(y_pred,y),但是可以通过指定output_transform的方法来修改器输出,参见process_function of trainer和process_function of evaluator。
Ignite中数据的流动过程如下图所示:
Ignite中的数据流动方向
Engine中的State
Engine对象中的state存放了一些上下文变量,用于在不同的handler之间进行调用,可以通过**engine.state.{name}**的方式访问。这些变量包括:
变量名 | 说明 |
---|---|
iteration | 数据集迭代次数,从1记起 |
epoch | 训练epoch,从1记起 |
seed | 每个epoch的随机数种子 |
dataloader | 传递给engine的数据 |
max_epochs | epoch数,相当于深度学习中常见的num_epochs |
batch | 本次数据集迭代过程中传递给process_function的数据 |
output | process_function单次迭代后的输出,前文有所涉及 |
metrics | 深度学习的Metrics,参见后文“Metrics和Checkpoint” |
times | 训练中每个epoch花费的时间和训练全程花费的时间,参见后文”时间性能分析” |
Ignite中的重要概念
模型训练的Event和Handler
基本使用
为了扩展Engine的功能,Ignite提供了一套贯穿整个训练过程的事件(Events),用户可以在这些事件上加入自己的Handler,从而实现自定义的功能。Ignite提供了两种添加Handler的方法,一种是使用Engine的add_event_handler方法,另一种是使用Handler的attach方法,这里以add_event_handler方法为主。首先是定义一个最简单的Handler:
engine = Engine(process_function)
engine.add_event_handler(Events.COMPLETED, lambda _: print("Training completed."))
这个事件会在Events.COMPLETED事件被触发后执行,打印一行“Training completed.”的字符串。其中,Events.COMPLETED事件在训练结束后会被触发。
除了使用这种方法,还可以使用装饰器来注册Handler:
@engine.on(Events.COMPLETED)
def print_completed():
print("Training completed.")
# 或者可以捕获engine作为参数,获取engine中的内容
@engine.on(Events.COMPLETED)
def print_completed(engine):
print(f"Training completed. {engine.state.max_epochs}")
如果需要向Handler中传递额外的参数,则可以使用engine.add_event_handler方法:
def handler_with_parameters(str_to_print):
print(str_to_print)
engine.add_event_handler(Events.COMPLETED, handler_with_parameters, "Training completed.")
事件过滤
有时候我们需要某个事件只触发一次,或者间隔一定次数再触发,这时可以通过向事件传递参数的方式实现:
@engine.on(Events.ITERATION_STARTED(every=10))
def some_func():
# 每间隔10次迭代被调用
pass
@engine.on(Events.ITERATION_STARTED(once=50))
def some_func():
# 只在第50次迭代时被调用
pass
@engine.on(Events.ITERATION_STARTED(before=30, after=60, every=5))
def some_func():
# 只在第30次迭代之前和第60次迭代之后,每5间隔次迭代被调用
# before和after可以各自单独使用,也可以像这样配合every使用
pass
def event_filter(engine, event):
if event in [10, 30, 60, 120]:
return True
return False
@engine.on(Events.ITERATION_STARTED(event_filter=event_filter))
def some_func():
# 只在第10、30、60、120次迭代开始时被调用
pass
删除Handler
在某些情况下,我们可能需要删除掉某个Handler,这时可以调用Engine实例的remove_event_handler方法,其定义如下:
def remove_event_handler(handler, event_name):
...
其中handler为需要删除的Handler函数,在需要删除某个事件的Handler时,使用event_name指定需要处理的事件。
同时,add_event_handler方法会返回一个RemovableEventHandler实例,也可以用来删除Handler,一下代码摘自Ignite官网:
def some_func(engine):
print("blablabla")
@trainer.on(Events.EPOCH_COMPLETED)
def evaluate(trainer):
# 这里使用with上下文调用返回的RemovableEventHandler实例
with evaluator.add_event_handler(Events.COMPLETED, some_func):
evaluator.run(train_loader)
trainer.run(train_loader, max_epochs=100)
Ignite定义的训练过程的生命周期如下图所示,提供的所有事件参见文档。
Ignite支持的训练过程生命周期,摘自官网。
Metrics
在深度学习中,有时需要对模型的性能进行检验,比如准确率、F-score等。Ignite提供了一系列开箱即用的Metrics,下面是一个使用示例:
from ignite.metrics import Accuracy
acc = Accuracy()
acc.attach(evaluator, "acc")
state = evaluator.run(eval_data)
print(f"Accuracy is: {state.metrics['acc']}")
Ignite提供的Metrics需要Engine的输出为元组(y_pred,y),如果不是这种格式,可以使用Metrics的output_transform参数进行调整:
def process_function(engine, batch):
return { "x": 1, "y": 2, "y_pred": 2.1, "loss": 0.1}
engine = Engine(process_function)
def output_transform(output):
return output["y_pred"], output["y"]
acc = Accuracy(output_transform=output_transform)
acc.attach(engine, "acc")
state = engine.run(data)
print(f"Accuracy is: {state.metrics['acc']}")
Ignite提供的全部Metrics以及一些高级用法可以在Ignite Metrics和Ignite Contrib Metrics中查看。
保存和恢复训练进度
保存训练进度
训练模型时,有时需要保存当前的训练进度,或者提供一个或多个预训练模型。Ignite提供了一个名为Checkpoint的Handler,用于保存模型。
from ignite.handlers import Checkpoint, DiskSaver, global_step_from_engine
# 定义好的模型和optimizer
model = ...
optimizer = ...
trainer = Engine(process_function)
acc = Accuracy()
acc.attach(trainer, "acc")
to_save = { "model": model, "optimizer": optimizer, "trainer": trainer }
checkpoint = Checkopint(
to_save,
save_handle=DiskSaver(dirname="checkpoints", require_empty=False), # 保存在"checkpoints"文件夹中,且文件夹不必为空
n_saved=3,
score_name="acc",
# 有些情况要求Metric越小越好(比如Loss),则可以指定score_sign为-1.0,score_sign只能有1.0和-1.0两个可选值
# score_function=Checkpoints.get_default_score_fn("acc", score_sign=-1.0),
global_step_transform=global_step_from_engine(engine),
)
engine.add_event_handler(Events.EPOCH_COMPLETED(every=1), checkpoint)
上面是定义一个Checkpoint的示例代码。可以看到,Checkpoint同样是一个Handler,下面是其参数的说明:
- save_handle:可以使用DiskSaver,也可以使用字符串直接指定Checkpoint保存的路径。
- n_saved:保存的Checkpoint数量,旧的Checkpoint会被丢弃。如果不指定数量,则所有的Checkpoint都会被保存。
- score_function:Checkpoint排序的依据。在某些情况下,可能需要Metric越小越好,比如损失值,这时可以指定score_sign为-1.0。注意:score_sign只能在1.0和-1.0之间取值。
- score_name:score_function被保存时的名称。
- global_step_transform:使用之后,在记录日志时,会使用trainer的epoch,而不是evaluator的epoch,这样能更清楚地记录evaluator的训练进度。
Checkpoint的命名
这是一个Checkpoint的命名结构:{filename_prefix}{name}{suffix}.{ext}
-
filename_prefix:在创建Checkpoint时作为参数传递,默认为空字符串。
-
name:如果to_save只有一个对象需要保存,那么name就是to_save里唯一的键,否则name会被设置为”checkpoint”。
-
ext:Checkpoint的扩展名,一般是“pt”。
-
suffix:命名见下表:
score_function score_name global_step_transform suffix ✖ ✖ ✖ {engine.state.iteration} ✔ ✖ ✖ {score} ✔ ✖ ✔ {global_step}_{score} ✔ ✔ ✔ {global_step}_{score_name}={score} ✖ ✖ ✔ {global_step} ✔ ✔ ✖ {score_name}={score}
恢复训练进度
device = torch.device("cuda" if torch.cuda_is_available() else "cpu")
to_save = ... # 参见前面的定义
checkpoint = torch.load("checkpoint.pt", map_location=device)
Checkpoint.load_objects(to_load=to_save, checkpoint=checkpoint)
trainer.run(...) # 训练过程被恢复
Ignite还提供了两个第三方的函数用来在evaluator运行时保存模型本身,分别是:gen_save_best_models_by_val_score和save_best_model_by_val_score。
让训练结果可复现
为了验证深度学习的算法优劣,或者对算法进行基准测试时,我们需要保证训练过程的可复现性。在PyTorch中,这可以通过1:设置随机数种子;2:保证cudnn的确定性(可选)来实现,这部分可以参考PyTorch官方文档。Ignite同样提供了一套API来简化这一过程,主要有以下几个:
- DeterministicEngine
- manual_seed
- ReproducibleBatchSampler
以下示例代码摘自Ignite官网:
import torch
from torch.utils.data import DataLoader
from ignite.engine import DeterministicEngine, Events
from ignite.utils import manual_seed
def random_train_data_loader(size):
data = torch.arange(0, size)
return DataLoader(data, batch_size=4, shuffle=True)
def print_train_data(engine, batch):
i = engine.state.iteration
e = engine.state.epoch
print("train", e, i, batch.tolist())
trainer = DeterministicEngine(print_train_data)
print("Original Run")
manual_seed(56)
trainer.run(random_train_data_loader(40), max_epochs=2, epoch_length=5)
print("Resumed Run")
# Resume from 2nd epoch
trainer.load_state_dict({"epoch": 1, "epoch_length": 5, "max_epochs": 2, "rng_states": None})
manual_seed(56)
trainer.run(random_train_data_loader(40))
以上代码的主要实现步骤:
- manual_seed:指定随机数种子,相当于依次调用random.seed、torch.manual_seed、numpy.seed、xla_model.set_rng_state四个方法。
- DeterministicEngine:继承自Engine类,让DataLoader的加载过程确定化。
- ReproduciblaBatchSampler:在使用DeterministicEngine,且数据的类型为torch.utils.data.DataLoader时会自动使用,一般不需要用户手动设置
DeterministicEngine的实现原理
DeterministicEngine能够保证其训练过程确定性主要有两个方面:一是让cudnn的算法具有确定性,二是让数据集的加载过程具有确定性。第一条可以通过在运行时添加Handler实现:
class DeterministicEngine(Engine):
def __init__(self, process_function: Callable[[Engine, Any], Any]):
# 省略其余初始化过程
self.add_event_handler(Events.STARTED, self._init_run)
self.add_event_handler(Events.DATALOADER_STOP_ITERATION | Events.TERMINATE_SINGLE_EPOCH, self._setup_seed)
其中_setup_seed方法是在每次迭代数据集之后重置随机数种子,这个不多赘述,重点在于_init_run方法:
def _init_run(self) -> None:
self.state.seed = int(torch.randint(0, int(1e9), (1,)).item())
if torch.cuda.is_available():
if hasattr(torch, "use_deterministic_algorithms"):
torch.use_deterministic_algorithms(True, warn_only=True)
else:
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
这个方法的所有和cuda、cudnn相关的内容都可以在前面的PyTorch文档链接中进行查看,主要功能是让cudnn的实现算法具有确定性。
之后我们来看DeterministicEngine是如何让数据集的迭代过程具有确定性。无论是Engine还是DeterministicEngine,他们在最终执行时都会调用实例的run方法,这个方法的内部实现如下所示:
def run(self, data: Optional[Iterable] = None, max_epochs: Optional[int] = None, seed: Optional[int] = None) -> State:
# 省略参数校验过程
...
if self.interrupt_resume_enabled:
return self._internal_run()
else:
return self._internal_run_legacy()
这两个方法对应不同条件下的执行过程。其中_internal_run方法会调用内部的_internal_run_as_gen方法,这个方法和_internal_run_legacy方法都会去调用_setup_engine方法,代码如下:
def _internal_run(self) -> State:
if self._internal_run_generator is None:
self._internal_run_generator = self._internal_run_as_gen() # 在这里调用_internal_run_as_gen方法
try:
return next(self._internal_run_generator)
except StopIteration as out:
self._internal_run_generator = None
return out.value
def _internal_run_as_gen(self) -> Generator:
# 省略无关代码和缩进,只作为演示
if self._dataloader_iter is None:
self._setup_engine()
def _internal_run_legacy(self) -> State:
# 省略无关代码和缩进,只作为演示
if self._dataloader_iter is None:
self._setup_engine()
而DeterministicEngine重写了内部的_setup_engine方法,参见下面代码:
def _setup_engine(self) -> None:
# 省略参数校验过程
if isinstance(self.state.dataloader, DataLoader):
# 省略部分无关代码及缩进
batch_sampler = self.state.dataloader.batch_sampler
if not (batch_sampler is None or isinstance(batch_sampler, ReproducibleBatchSampler)):
# 如果batch_sampler存在且不是ReproducibleBatchSampler
self.state.dataloader = update_dataloader(
self.state.dataloader, ReproducibleBatchSampler(batch_sampler)
)
这里的update_dataloader函数只有一个功能,就是替换DataLoader的BatchSampler。上述代码的功能是检验内部DataLoader的BatchSampler是不是ReproducibleBatchSampler,如果不是,那么替换为ReproducibleBatchSampler,其代码实现如下:
class ReproducibleBatchSampler(BatchSampler):
def __init__(self, batch_sampler: BatchSampler, start_iteration: Optional[int] = None):
# 省略校验过程及无关代码
self.batch_indicies: List = [] # 这里存放原有BatchSampler的索引
def __iter__(self) -> Generator:
self.setup_batch_indicies()
for batch in self.batch_indicies:
yield batch
def setup_batch_indicies(self) -> None:
self.batch_indices = []
for batch in self.batch_sampler:
self.batch_indices.append(batch)
# 省略后续代码
可以看到,ReproducibleBatchSampler在迭代之前首先在内部将原有的BatchSampler进行迭代,将迭代到的数据存储在batch_indicies中,然后迭代batch_indicies列表,将数据返回。这样就能够保证训练时每次获取到的数据都是确定的。
Ignite还提供了两个示例代码,分别在MNIST数据集和CIFAR10数据集上进行训练。截至目前(2023-09-23),Ignite官网给出的CIFAR10数据集代码的链接有误,虽然已在GitHub仓库上修改,但还未更新到官网中,因此这里手动给出链接:
杂项
添加日志
日志打印/保存是训练模型时的重要部分,可以帮助分析训练过程,同时也可以保存一些关键信息,比如模型的权重、梯度等。日志一般分为两个部分:运行时日志,比如使用logging和loguru库,以及可视化数据日志,比如使用TensorBoardX和ClearML库等两个大类。
运行日志
在Ignite中,默认使用Python自带的logging库,如果不加任何设置,则Logger会在Engine初始化时被自动创建,其代码为:
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
Ignite同时提供了setup_logger函数,用来对Logger做进一步的配置:
def setup_logger(
name: Optional[str] = "ignite",
level: int = logging.INFO,
stream: Optional[TextIO] = None,
format: str = "%(asctime)s %(name)s %(levelname)s: %(message)s",
filepath: Optional[str] = None,
distributed_rank: Optional[int] = None,
reset: bool = False
) -> logging.Logger:
...
engine = Engine(process_function)
logger = setup_logger()
engine.logger = logger
只需要访问engine.logger成员即可使用Logger来打印日志:
@engine.on(Events.STARTED)
def log_something(engine):
engine.logger.INFO("训练开始,打印日志")
Ignite并没有对Logger的类型做出规定,这意味着用户可以使用第三方的日志库,比如loguru等,使用时只需要向上面一样指定engine.logger成员即可。
可视化数据日志
在训练模型时,有时需要将训练数据记录下来,并进行可视化,这就用到了Tensorboard。Ignite同样提供了快速接入这些库的API:
from ignite.contrib.engines.common import setup_tb_logging
model = ...
optimizer = ...
trainer = Engine(process_function_trianing)
evaluator = Engine(process_function_evaluating)
tb_logger = setup_tb_logging(
output_path="./tb_logging",
trainer=trainer, evaluators=evaluator,
optimizers=optimizer, log_every_iters=1
)
setup_tb_logging的参数如下:
- output_path:输出日志的路径
- trainer:训练的Engine
- optimizers:可选,可以是一个或多个Optimizer
- evaluators:可选,可以使一个或多个验证的Engine
- log_evety_iters:每多少次迭代数据后记录一次数据,默认为1
setup_tb_logging返回一个TensorboardLogger,可以通过这个logger额外添加一些需要追踪的数据:
from ignite.contrib.handlers.tensorboard_logger import WeightsScalarHandler, WeightsHistHandler, GradsScalarHandler, GradsHistHandler
# 记录Trainer的损失函数
tb_logger.attach_output_handler(
trainer, event_name=Events.ITERATION_COMPLETED, tag="training", output_transform=lambda loss: {"loss": loss})
# 记录Evaluator的Metrics
tb_logger.attach_output_handler(
evaluator, event_name=Events.EPOCH_COMPLETED, tag="training", metric_names=["accuracy", "fscore"]
global_step_transform=global_step_from_engine(trainer))
# 记录Optimizer的参数
tb_logger.attach_output_handler(
trainer, event_name=Events.ITERATION_STARTED, optimizer=optimizer, param_name="lr") # 最后一个参数选填
# 记录模型的权重
tb_logger.attach(trainer, event_name=Events.ITERATION_COMPLETED, log_handler=WeightsScalarHandler(model))
# 以直方图的形式记录模型权重
tb_logger.attach(trainer, event_name=Events.ITERATION_COMPLETED, log_handler=WeightsHistHandler(model))
# 记录模型的梯度
tb_logger.attach(trainer, event_name=Events.ITERATION_COMPLETED, log_handler=GradsScalarHandler(model))
# 以直方图的形式记录模型梯度
tb_logger.attach(trainer, event_name=Events.ITERATION_COMPLETED, log_handler=GradsHistHandler(model))
最后,当模型训练完毕时,我们需要关闭logger:
tb_logger.close()
对于其他的日志库,比如ClearML等,Ignite同样提供了对应的使用方法,使用时需要参考setup_xxx_logging页面和对应的logger页面。
提前结束训练
有时候我们并不确定什么时候训练结束,或者需要在训练经过一段时间没有进展之后停止训练,这时就可以考虑使用EarlyStopping Handler或add_early_stopping_by_val_score函数。
首先是EarlyStopping的使用方法:
from ignite.handlers import EarlyStopping
def score_function(engine):
return engine.state.metrics["accuracy"]
handler = EarlyStopping(patience=2, score_function=score_function, trainer=trainer)
# 注意,这个Handler需要附加在Evaluator上,一定要注意这一点
evaluator.add_event_handler(Events.COMPLETED, handler)
EarlyStopping需要被加载Evaluator上,当Evaluator的score_function,也就是准确率,连续2次没有进步时,停止Trainer的训练过程。
注意:当score_function获取的数据是比如损失值这样越小越好的指标时,可以考虑把score_function的返回值取反。
EarlyStopping的参数如下:
- patience:当连续几次训练没有进步时停止训练过程
- score_function:评价训练是否有进步的标志
- trainer:需要停止训练的Engine
- min_delta:浮点数,小于min_delta的进步将被忽略不计,默认为0.0
- cumulative_delta:默认False。为False时,以相对于上一次评价时是否有进步为标准;为True时,min_delta取上一次patience被重置时的进步值。
有时候我们并不需要EarlyStopping这样精细的控制程度,同时为了简化编写代码的复杂度,我们可以使用add_early_stopping_by_val_score函数:
def add_early_stopping_by_val_score(
patience: int, evaluetor: Engine, trainer: Engine, metric_name: str, score_sign: float = 1.0):
...
其中metric_name表示用作评价训练是否进步的metric名称;对于如损失值这样越小越好的指标,score_sign可以取-1.0,可选值为1.0或-1.0。
二者的不同主要在以下几个方面:
项目 | EarlyStopping | add_early_stopping_by_val_score |
---|---|---|
设置score_sign | ✖ | ✔ |
设置min_delta | ✔ | ✖ |
设置cumulative_delta | ✔ | ✖ |
编写复杂度 | 高 | 低 |
时间性能分析
使用Ignite进行时间分析主要有2个方法:BasicTimeProfiler和HandlersTimeProfiler。除此之外,用户还可以通过使用Timer来进行更底层的访问,但是这个不常用,因此本篇文章省略Timer相关的内容。
首先是BasicTimeProfiler的使用方法:
trainer = Engine(...)
basic_profiler = BasicTimeProfiler()
basic_profiler.attach(trainer)
trainer.run(train_loader, max_epochs=10)
basic_profiler.print_results(basic_profiler.get_results())
使用BasicTimeProfiler时无法对每个Handler占用的时间进行分析,可以考虑使用HandlersTimeProfiler:
trainer = Engine(...)
handlers_profiler = HandlersTimeProfiler()
handlers_profiler.attach(trainer)
trainer.run(train_loader, max_epochs=10)
handler_profilers.print_results(handler_profilers.get_results())
这二者都可以把获取到的运行时间相关信息保存在csv文件中:
# 保存结果
basic_profiler.write_results("./basic_profile.csv")
handlers_profiler.write_results("./handlers_profile.csv")
# 加载结果
basic_profile = pd.read_csv("./basic_profile.csv")
basic_profile.head()
handlers_profile = pd.read_csv("./handlers_profile.csv")
handlers_profile.head()
进度条
在训练模型时,我们可以使用进度条来直观地展现训练过程(部分代码摘自官网):
trainer = Engine(...)
pbar = ProgressBar()
pbar.attach(trainer)
# Epoch [2/50]: [64/128] 50%|█████ [06:17<12:34]
同时也可以在进度条上监控一些变量:
trainer = Engine(...)
pbar = ProcessBar()
pbar.attach(trainer, output_transform=lambda x: { "loss": x })
# Epoch [2/50]: [64/128] 50%|█████ , loss=0.123 [06:17<12:34]
# 或者使用以下代码监控更多变量
pbar.attach(trainer, metric_names=["acc", "nll"], state_attributes=["alpla", "beta"])
更多相关参数可以参考API文档。
附录
本文之前的部分列出了Ignite的大多数使用方法,但是还有一些遗漏,读者可以参考以下链接:
- 使用FastaiRLFinder找出最合适的学习率:教程、API文档。
- 在显存有限的情况下逐步增加Batch Size:教程。
- 在训练时切换数据集:教程。
- Ignite教程:包含了多种场景下的Ignite使用教程,包括强化学习、分布式训练等。
- keep_random_state:在某个函数运行时保持随机状态的装饰器,不常用。
- TerminateOnNan:在Engine的输出中有NaN时停止训练。
- TimeLimit:限制训练时间,超过时间限制后自动停止训练
- Ignite Utils:训练中常用的工具函数,包括给Checkpoint进行哈希、Tensor的编码转换和设备迁移等。