fastNLP.core.trainer

Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰以下步骤的代码

  1. epoch循环;
  2. 将数据分成不同的Batch;
  3. 对Batch进行pad;
  4. 每个epoch结束或一定step后进行验证集验证;
  5. 保存获得更好验证性能的模型。

1. Trainer的基本使用

下面的例子是使用神经网络来进行预测一个序列中是否有偶数个1。

import numpy as np
from torch import nn
import torch
import torch.nn.functional as F
from torch.optim import SGD

from fastNLP import DataSet
from fastNLP import Trainer
from fastNLP import CrossEntropyLoss
from fastNLP import AccuracyMetric
from fastNLP.modules.decoder import MLP

# 模型
class Model(nn.Module):
    def __init__(self, input_num):
        super().__init__()
        self.fcs = MLP([input_num, 40, 40, 2], 'relu')

    def forward(self, x):
        x = self.fcs(x)
        return {'pred': x}
model = Model(10)

# 生成数据
def generate_psedo_dataset(num_samples):
    dataset = DataSet()
    data = np.random.randint(2, size=(num_samples, 10))
    label = np.sum(data, axis=1)%2
    dataset = DataSet({'x':data.astype(float), 'label': label})
    dataset.set_input('x')
    dataset.set_target('label')
    return dataset
tr_dataset = generate_psedo_dataset(1000)
dev_data = generate_psedo_dataset(100)

# 训练
trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'),
                   optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000,
                   dev_data = dev_data, metrics=AccuracyMetric(target='label'))
trainer.train()

由上面的例子可以看出通过使用Trainer,可以使得训练部分的代码大幅减少。 使用Trainer需要满足以下几个条件:

1.1 模型

1 模型的forward()的参数名需要与DataSet中的名字对应。实际上fastNLP在将DataSet中的数据传递给模型forward()时,是 通过匹配名称实现的。所以上例中,如果Model的forward函数修改为forward(self, data), 则DataSet中的'x'这个field就应该 改名为'data'。

2 传递给forward()的参数是DataSet中被设置为input的那些field。但如果forward()中没有对应的参数,则不会将数据传递 给forward()。例如,DataSet中'x1', 'x2'都是input,但是模型的函数为forward(self, x1), 那么'x2'不会传递给forward()。

3 模型的forward()返回值需要为一个dict。

1.2 Loss

fastNLP中的为了不限制forward函数的返回内容数量(比如一些复杂任务需要返回多个内容,如Dependency Parsing, LossMetric 都使用了通过名称来匹配相应内容的策略。如上面的例子中

trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'),
           optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000,
           dev_data = dev_data, metrics=AccuracyMetric(target='label'))

loss被设置为了 CrossEntropyLoss , 但在初始化的时候传入了target='label'这个参数, CrossEntropyLoss 的初始化参数为(pred=None, target=None, padding_idx=-100)。

这里的两个参数分别为计算CrossEntropy时需要使用到的模型的预测值与真实值。 其中 pred 一般来自于模型forward()的返回结果,target 一般是来自于DataSet中被设置为target的field。 由于每个人对真实值或者model的返回值取名并不一样,所以fastNLP的 Loss 提供一种类似于映射的机制来匹配对应的值, 比如这里 CrossEntropyLoss 将尝试找到名为'label'的内容来作为真实值得到loss; 而pred=None, 则 CrossEntropyLoss 使用'pred'作为名称匹配预测值, 正好forward的返回值也叫pred,所以这里不需要申明pred。

尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。 fastNLP中提供了 LossInForward 这个loss。 这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor,并使用它作为loss。 如果Trainer初始化没有提供loss则默认使用 LossInForward

1.3 Metric

Metric 使用了与上述Loss一样的策略,即使用名称进行匹配。 AccuracyMetric(target='label')的情况与CrossEntropyLoss 是同理的。

在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果, 传入到predict()的参数也是从DataSet中被设置为input的field中选择出来的; 与forward()一样,返回值需要为一个dict。

2. Trainer的代码检查

由于在fastNLP中采取了映射的机制,所以难免可能存在对应出错的情况。Trainer提供一种映射检查机制,可以通过check_code_level来进行控制 比如下面的例子中,由于各种原因产生的报错

Example2.1

import numpy as np
from torch import nn
import torch
from torch.optim import SGD
from fastNLP import Trainer
from fastNLP import DataSet

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1, 1)
    def forward(self, x, b):
        loss = torch.mean((self.fc(x)-b)**2)
        return {'loss': loss}
model = Model()

dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2})
dataset.set_input('a', 'b')

trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001))

trainer = Trainer(dataset, model, SGD(model.parameters()))
#  会报以下的错误
# input fields after batch(if batch size is 2):
#     a: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2])
#     b: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2])
# There is no target field.
# ....
# NameError:
# Problems occurred when calling Model.forward(self, x, b)
#     missing param: ['x']
#     unused field: ['a']
#     Suggestion: You need to provide ['x'] in DataSet and set it as input.

这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里有两类 信息可以为你提供参考

1 'input fields after batch...'这部分显示的是train dataset经过Batch操作后,每个field对应的类型以及进行shape。这里 因为train dataset没有target所以没有显示。根据这里可以看出是否正确将需要的内容设置为了input或target。

2 NameError,NameError发生在映射出错的情况。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 出当前是在调取forward),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x',或者model的参数从'x'修改为'a'都可以解决问题。

下面的例子是由于loss计算的时候找不到需要的值

Example2.2

import numpy as np
from torch import nn
from torch.optim import SGD
from fastNLP import Trainer
from fastNLP import DataSet
from fastNLP import L1Loss
import torch

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1, 1)
    def forward(self, a):
        return {'pred_b': self.fc(a.unsqueeze(1)).squeeze(1), 'No use':1}

model = Model()

dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2})

dataset.set_input('a')
dataset.set_target('b')

trainer = Trainer(dataset, model, loss=L1Loss(target='label'), optimizer=SGD(model.parameters(), lr=0.001))
# 报错信息如下
# input fields after batch(if batch size is 2):
#     a: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2])
# target fields after batch(if batch size is 2):
#     b: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2])
# ....
# NameError:
# Problems occurred when calling L1Loss.get_loss(self, pred, target)
#     missing param: ['pred(assign to `pred` in `L1Loss`)', 'label(assign to `target` in `L1Loss`)']
#     unused field: ['b']
#     unused param: ['pred_b', 'No use']
#     target field: ['b']
#     param from Model.forward(self, a): ['pred_b', 'No use']
#     Suggestion: (1). Check key assignment for `target` when initialize L1Loss. Or provide `label` in DataSet or output of Model.forward(self, a).
#             (2). Check key assignment for `pred` when initialize L1Loss. Or provide `pred` in DataSet or output of Model.forward(self, a).

报错信息也包含两部分:

1 第一部分与上面是一样的

2 这里报错的原因是由于计算loss的时候找不到相应的值(通过L1Loss.get_loss(self, pred, target)判断出来的); 报错的原因是因为 predlabel (我们在初始化L1Loss时将target指定为了label)都没有找到。 这里'unused field'是DataSet中出现了,但却没有被设置为input或者target的field; 'unused param'是forward()中返回且没有被使用到的内容;'target field'是被设置为了target的field; 'param from Model.forward(self, a)'是forward()返回的所有key。"Suggestion"是关于当前错误处理的建议。

但是在一些情况下,比如forward()返回值只有一个,target也只有一个,fastNLP不会进行匹配,而直接将forward()的结果作为pred, 将DataSet中的target设置为target。上面的例子在返回值中加入了一个'No use'则只是为了使得Loss去匹配结果。

下面是带有dev dataset时如果出现错误会发生的报错,

Example2.3

import numpy as np
from torch import nn
from torch.optim import SGD
from fastNLP import Trainer
from fastNLP import DataSet
from fastNLP import AccuracyMetric
import torch

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(1, 1)
    def forward(self, a, b):
        loss = torch.mean((self.fc(a.float().unsqueeze(1))-b.float())**2)
        return {'loss': loss}
    def predict(self, a):  # 使用predict()进行验证
        return {'output':self.fc(a.float().unsqueeze(1))} #这里return的值不包含'pred'这个key
model = Model()

dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2})
dev_data = DataSet({'a': np.arange(10, 20), 'b':np.arange(10, 20)*2})

dataset.set_input('a', 'b')
dev_data.set_input('a')  # 这里没有设置target

trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001),
                 dev_data=dev_data, metrics=AccuracyMetric())

# 报错信息
# ...
# NameError:
# Problems occurred when calling AccuracyMetric.evaluate(self, pred, target, seq_len=None)
#     missing param: ['pred(assign to `pred` in `AccuracyMetric`)', 'target(assign to `target` in `AccuracyMetric`)']
#     unused param: ['output']
#     target field: []
#     param from Model.predict(self, a): ['output']
#     Suggestion: (1). Check key assignment for `pred` when initialize AccuracyMetric. Or provide `pred` in DataSet or output of Model.predict(self, a).
#             (2). Check key assignment for `target` when initialize AccuracyMetric. Or provide `target` in DataSet or output of Model.predict(self, a).

报错信息和前面都是类似的,但是可以通过'AccuracyMetric.evaluate(self, pred, target, seq_len=None)'看出这里是evaluation 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation弄错的情况。这里的修改是通过在初始化metric的时候 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output')。

可以通过check_code_level调节检查的强度。默认为0,即进行检查。

3. Trainer与callback

虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能,比如负采样,learning rate decay, Early Stop等。 为了解决这个问题fastNLP引入了callback的机制,Callback 是一种在Trainer训练过程中特定阶段会运行的函数集合, 所有的 Callback 都具有on_*(比如on_train_start, on_backward_begin)等函数。 如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用,例如:

from fastNLP import Callback, EarlyStopCallback, Trainer, CrossEntropyLoss, AccuracyMetric
from fastNLP.models import CNNText

start_time = time.time()

class MyCallback(Callback):
    def on_epoch_end(self):
        print('{:d}ms\n\n'.format(round((time.time()-start_time)*1000)))

model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)
trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, loss=CrossEntropyLoss(),
                  metrics=AccuracyMetric(), callbacks=[MyCallback(),EarlyStopCallback(10)])
trainer.train()

这里,我们通过继承 Callback 类定义了自己的 callback 的,并和内置的 EarlyStopCallback 一起传给了 Trainer ,增强了 Trainer 的功能

fastNLP已经自带了很多callback函数供使用,可以参考 fastNLP.core.callback

class fastNLP.core.trainer.Trainer(train_data, model, optimizer=None, loss=None, batch_size=32, sampler=None, drop_last=False, update_every=1, num_workers=0, n_epochs=10, print_every=5, dev_data=None, metrics=None, metric_key=None, validate_every=-1, save_path=None, use_tqdm=True, device=None, callbacks=None, check_code_level=0, **kwargs)[源代码]

别名 fastNLP.Trainer fastNLP.core.trainer.Trainer

Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰写
  1. epoch循环;
  2. 将数据分成不同的Batch;
  3. 对Batch进行pad;
  4. 每个epoch结束或一定step后进行验证集验证;
  5. 保存获得更好验证性能的模型等。

详细的介绍参见 fastNLP.core.trainer

__init__(train_data, model, optimizer=None, loss=None, batch_size=32, sampler=None, drop_last=False, update_every=1, num_workers=0, n_epochs=10, print_every=5, dev_data=None, metrics=None, metric_key=None, validate_every=-1, save_path=None, use_tqdm=True, device=None, callbacks=None, check_code_level=0, **kwargs)[源代码]
参数:
  • train_data -- 训练集, DataSet 类型或 BatchIter 的子类
  • model (nn.modules) -- 待训练的模型
  • optimizer -- torch.optim.Optimizer 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器
  • batch_size (int) -- 训练和验证的时候的batch大小。
  • loss -- 使用的 LossBase 对象。当为None时,默认使用 LossInForward
  • sampler -- Batch数据生成的顺序, Sampler 类型。如果为None,默认使用 RandomSampler
  • drop_last -- 如果最后一个batch没有正好为batch_size这么多数据,就扔掉最后一个batch
  • num_workers -- int, 有多少个线程来进行数据pad处理。
  • update_every -- int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128 会导致内存不足,通过设置batch_size=32, update_every=4达到目的。当optimizer为None时,该参数无效。
  • n_epochs (int) -- 需要优化迭代多少次。
  • print_every (int) -- 多少次反向传播更新tqdm显示的loss; 如果use_tqdm=False, 则多少次反向传播打印loss。
  • dev_data -- 用于做验证的DataSet, DataSet 类型。
  • metrics -- 验证的评估函数。可以只使用一个 Metric , 也可以使用多个 Metric ,通过列表传入。 如验证时取得了更好的验证结果(如果有多个Metric,以列表中第一个Metric为准),且save_path不为None, 则保存当前模型。Metric种类详见 metrics模块 。仅在传入dev_data时有效。
  • metric_key (str,None) -- Metric 有时会有多个指标, 比如 SpanFPreRecMetric 中包含了'f', 'pre', 'rec'。此时需 要指定以哪个指标为准。另外有些指标是越小效果越好,比如语言模型的困惑度,这种情况下,在key前面增加一个'-'来表 明验证时,值越小越好(比如: "-ppl")。仅在传入dev_data时有效。
  • validate_every (int) -- 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有效。
  • save_path (str,None) -- 将模型保存路径,如果路径不存在,将自动创建文件夹。如果为None,则不保存模型。如果dev_data为None,则保存 最后一次迭代的模型。保存的时候不仅保存了参数,还保存了模型结构。即便使用DataParallel,这里也只保存模型。
  • use_tqdm (bool) -- 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。
  • device (str,int,torch.device,list(int)) --

    将模型load到哪个设备。默认为None,即Trainer不对模型 的计算位置进行管理。支持以下的输入:

    1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, 可见的第二个GPU中;

    1. torch.device:将模型装载到torch.device上。
    2. int: 将使用device_id为该值的gpu进行训练
    3. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。
    4. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。

    已知可能会出现的问题:Adagrad优化器可能无法正常使用这个参数,请手动管理模型位置。

  • callbacks (list(callbacks)) -- 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 通过callback机制实现。 可使用的callback参见 callback模块
  • check_code_level (int) -- 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, 报告警告信息; 2: 有任何field没有被使用都报错. 检查的原理是通过使用很小的batch(默认2个sample)来运行代码,但是 这个过程理论上不会修改任何参数,只是会检查能否运行。但如果(1)模型中存在将batch_size写为某个固定值的情况; (2)模型中存在累加前向计算次数的,可能会多计算1次。以上情况建议将check_code_level设置为-1。
  • kwargs -- 支持配置可选参数 bool test_use_tqdm: 在dev上验证的时候是否开启tqdm Sampler test_sampler: 在evaluate的时候使用的sampler
is_master

是否是主进程

train(load_best_model=True, on_exception='auto')[源代码]

使用该函数使Trainer开始训练。

参数:
  • load_best_model (bool) -- 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 最好的模型参数。
  • on_exception (str) -- 在训练过程遭遇exception,并被 :py:class:Callback 的on_exception()处理后,是否继续抛出异常。 支持'ignore','raise', 'auto': 'ignore'将捕获异常,写在Trainer.train()后面的代码将继续运行; 'raise'将异常抛出; 'auto'将ignore以下两种Exception: CallbackException与KeyboardInterrupt, raise其它exception.
Return dict:

返回一个字典类型的数据, 内含以下内容:

seconds: float, 表示训练时长
以下三个内容只有在提供了dev_data的情况下会有。
best_eval: Dict of Dict, 表示evaluation的结果。第一层的key为Metric的名称,
            第二层的key为具体的Metric
best_epoch: int,在第几个epoch取得的最佳值
best_step: int, 在第几个step(batch)更新取得的最佳值