From f83e9aeef8c42cf60574e88b86c1483f0880baa5 Mon Sep 17 00:00:00 2001 From: x54-729 <17307130121@fudan.edu.cn> Date: Wed, 11 May 2022 09:14:34 +0000 Subject: [PATCH 01/14] =?UTF-8?q?transformers=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=B0=86=20HfFolder=20=E6=95=B4=E4=B8=AA=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=BF=87=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/transformers/torch/file_utils.py | 63 +++++++++++++++---- .../torch/models/auto/tokenization_auto.py | 2 +- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/fastNLP/transformers/torch/file_utils.py b/fastNLP/transformers/torch/file_utils.py index 60f95fdd..4c7ee7a4 100644 --- a/fastNLP/transformers/torch/file_utils.py +++ b/fastNLP/transformers/torch/file_utils.py @@ -82,6 +82,52 @@ def filelock(path): except: pass +class HfFolder: + """ + hugging_face.HfFolder + version = 0.5.1 + """ + path_token = os.path.expanduser("~/.huggingface/token") + + @classmethod + def save_token(cls, token): + """ + Save token, creating folder as needed. + + Args: + token (`str`): + The token to save to the [`HfFolder`] + """ + os.makedirs(os.path.dirname(cls.path_token), exist_ok=True) + with open(cls.path_token, "w+") as f: + f.write(token) + + @classmethod + def get_token(cls): + """ + Retrieves the token + + Returns: + `str` or `None`: The token, `None` if it doesn't exist. + + """ + try: + with open(cls.path_token, "r") as f: + return f.read() + except FileNotFoundError: + pass + + @classmethod + def delete_token(cls): + """ + Deletes the token from storage. Does not fail if token does not exist. + """ + try: + os.remove(cls.path_token) + except FileNotFoundError: + pass + + def is_offline_mode(): return _is_offline_mode @@ -629,11 +675,10 @@ def get_from_cache( if isinstance(use_auth_token, str): headers["authorization"] = f"Bearer {use_auth_token}" elif use_auth_token: - raise RuntimeError("`use_auth_token=True` is not supported in FastNLP now") - # token = HfFolder.get_token() - # if token is None: - # raise EnvironmentError("You specified use_auth_token=True, but a huggingface token was not found.") - # headers["authorization"] = f"Bearer {token}" + token = HfFolder.get_token() + if token is None: + raise EnvironmentError("You specified use_auth_token=True, but a huggingface token was not found.") + headers["authorization"] = f"Bearer {token}" url_to_download = url etag = None @@ -791,13 +836,7 @@ def get_list_of_files( if isinstance(use_auth_token, str): token = use_auth_token elif use_auth_token is True: - # token = HfFolder.get_token() - path_token = os.path.expanduser("~/.huggingface/token") - try: - with open(path_token, "r") as f: - token = f.read() - except FileNotFoundError: - token = None + token = HfFolder.get_token() else: token = None # model_info = HfApi(endpoint=HUGGINGFACE_CO_RESOLVE_ENDPOINT).model_info( diff --git a/fastNLP/transformers/torch/models/auto/tokenization_auto.py b/fastNLP/transformers/torch/models/auto/tokenization_auto.py index e275579f..f1618d6a 100644 --- a/fastNLP/transformers/torch/models/auto/tokenization_auto.py +++ b/fastNLP/transformers/torch/models/auto/tokenization_auto.py @@ -15,7 +15,7 @@ """ Auto Tokenizer class. """ from collections import OrderedDict -from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Tuple from ...file_utils import ( is_sentencepiece_available, From 84d862c13176395fb6f799bf707abeb66d1524f1 Mon Sep 17 00:00:00 2001 From: x54-729 <17307130121@fudan.edu.cn> Date: Wed, 11 May 2022 10:48:37 +0000 Subject: [PATCH 02/14] =?UTF-8?q?=E5=AE=8C=E5=96=84paddle=5Fdriver?= =?UTF-8?q?=E7=9A=84=E9=83=A8=E5=88=86=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drivers/jittor_driver/jittor_driver.py | 14 +-- fastNLP/core/drivers/jittor_driver/mpi.py | 4 + fastNLP/core/drivers/jittor_driver/utils.py | 2 +- .../core/drivers/paddle_driver/dist_utils.py | 2 + .../drivers/paddle_driver/paddle_driver.py | 117 +++++++++++------- .../drivers/paddle_driver/single_device.py | 34 +++-- fastNLP/core/drivers/paddle_driver/utils.py | 26 ++-- 7 files changed, 121 insertions(+), 78 deletions(-) diff --git a/fastNLP/core/drivers/jittor_driver/jittor_driver.py b/fastNLP/core/drivers/jittor_driver/jittor_driver.py index 7efff348..25fc4af6 100644 --- a/fastNLP/core/drivers/jittor_driver/jittor_driver.py +++ b/fastNLP/core/drivers/jittor_driver/jittor_driver.py @@ -21,6 +21,9 @@ if _NEED_IMPORT_JITTOR: 'sum': jt.sum } +__all__ = [ + "JittorDriver", +] class JittorDriver(Driver): r""" @@ -90,9 +93,6 @@ class JittorDriver(Driver): "'test_step'.") def save_model(self, filepath: str, only_state_dict: bool = False, model_save_fn: Optional[Callable]=None): - """ - 保存模型 - """ if model_save_fn is not None: outputs = model_save_fn(filepath) if outputs is not None: @@ -105,12 +105,6 @@ class JittorDriver(Driver): jt.save(states, filepath) def load_model(self, filepath: str): - """ - 加载模型的加载函数; - - :param file_path: 保存文件的文件位置(需要包括文件名); - :return: 加载后的state_dict - """ if not os.path.exists(filepath): raise FileNotFoundError("Checkpoint at {} not found.".format(filepath)) return jt.load(filepath) @@ -156,7 +150,7 @@ class JittorDriver(Driver): def move_data_to_device(self, batch: 'jt.Var'): """ - jittor暂时没有提供数据迁移的函数,因此这个函数只是简单地返回batch + **jittor** 暂时没有提供数据迁移的函数,因此这个函数只是简单地返回 **batch** """ return batch diff --git a/fastNLP/core/drivers/jittor_driver/mpi.py b/fastNLP/core/drivers/jittor_driver/mpi.py index bfa49e68..4ade3fd1 100644 --- a/fastNLP/core/drivers/jittor_driver/mpi.py +++ b/fastNLP/core/drivers/jittor_driver/mpi.py @@ -20,6 +20,10 @@ class JittorMPIDriver(JittorDriver): 这是一个正在开发中的功能,敬请期待。 + .. todo: + + 实现断点重训中替换 dataloader 的 set_dist_repro_dataloader 函数 + """ def __init__( self, diff --git a/fastNLP/core/drivers/jittor_driver/utils.py b/fastNLP/core/drivers/jittor_driver/utils.py index 43be9ac3..c6d44cfc 100644 --- a/fastNLP/core/drivers/jittor_driver/utils.py +++ b/fastNLP/core/drivers/jittor_driver/utils.py @@ -9,7 +9,7 @@ __all__ = [] class DummyGradScaler: """ - 用于仿造的GradScaler对象,防止重复写大量的if判断 + 用于仿造的 **GradScaler** 对象,防止重复写大量的if判断 """ def __init__(self, *args, **kwargs): pass diff --git a/fastNLP/core/drivers/paddle_driver/dist_utils.py b/fastNLP/core/drivers/paddle_driver/dist_utils.py index ffa142d3..4dea268d 100644 --- a/fastNLP/core/drivers/paddle_driver/dist_utils.py +++ b/fastNLP/core/drivers/paddle_driver/dist_utils.py @@ -21,6 +21,8 @@ if _NEED_IMPORT_PADDLE: _parse_load_result, ) +__all__ = [] + def _validate_output_list_for_rank(my_rank, dst, gather_list): if dst == my_rank: if not gather_list: diff --git a/fastNLP/core/drivers/paddle_driver/paddle_driver.py b/fastNLP/core/drivers/paddle_driver/paddle_driver.py index 74c7b7a8..a228a90d 100644 --- a/fastNLP/core/drivers/paddle_driver/paddle_driver.py +++ b/fastNLP/core/drivers/paddle_driver/paddle_driver.py @@ -1,14 +1,12 @@ import os import random -from typing import Union, Optional, Dict +from typing import Union, Optional, Dict, Any from pathlib import Path from functools import partial from dataclasses import dataclass import numpy as np -from fastNLP.envs.env import USER_CUDA_VISIBLE_DEVICES - from .utils import _build_fp16_env, optimizer_state_to_device, DummyGradScaler from fastNLP.envs.imports import _NEED_IMPORT_PADDLE from fastNLP.core.drivers.driver import Driver @@ -50,9 +48,26 @@ if _NEED_IMPORT_PADDLE: class PaddleDriver(Driver): r""" - Paddle框架的Driver,包括实现单卡训练的 `PaddleSingleDriver` 和分布式训练的 `PaddleFleetDriver`。 + 实现了 **PaddlePaddle** 框架训练功能的基本 Driver,实现了单卡和多卡情景下均需要实现的功能,以和 **fastNLP** 的 + :class:`~fastNLP.core.Trainer` 兼容;通过这个 Driver,可以在 **fastNLP** 中实现从 **Pytorch** 框架到 + **PaddlePaddle** 深度学习框架的切换。 + + 这个类被以下子类继承: + + 1. :class:`~fastNLP.core.drivers.PaddleSingleDriver`:实现了使用单卡和 ``cpu`` 训练的具体功能; + 2. :class:`~fastNLP.core.drivers.PaddleFleetDriver`:实现了使用 ``fleet`` 分布式训练 API 进行分布式训练的具体功能; + + :param model: 训练时使用的 **PaddlePaddle** 模型; + :param fp16: 是否开启混合精度训练; + :kwargs: + * wo_auto_param_call (``bool``) -- 是否关闭在训练时调用我们的 ``auto_param_call`` 函数来自动匹配 batch 和前向函数的参数的行为; + + .. note:: + + 关于该参数的详细说明,请参见 :class:`~fastNLP.core.controllers.Trainer` 中的描述;函数 ``auto_param_call`` 详见 :func:`fastNLP.core.utils.auto_param_call`。 + """ - def __init__(self, model, fp16: Optional[bool] = False, **kwargs): + def __init__(self, model: "paddle.nn.Layer", fp16: Optional[bool] = False, **kwargs): if not isinstance(model, paddle.nn.Layer): raise ValueError(f"Parameter `model` can not be `{type(model)}` in `PaddleDriver`, it should be exactly " f"`paddle.nn.Layer` type.") @@ -69,10 +84,10 @@ class PaddleDriver(Driver): def zero_grad(self, set_to_none: bool = False): r""" - 实现深度学习中的梯度的置零操作,应当直接通过优化器 optimizers 来将梯度置零; - 注意梯度累积不需要在这里实现,trainer 已经在内部实现了梯度累积; + 实现深度学习中的梯度的置零操作,应当直接通过优化器 ``optimizers`` 来将梯度置零; + 注意梯度累积不需要在这里实现,:class:`~fastNLP.core.Trainer` 已经在内部实现了梯度累积; - :param set_to_none: 用来判断是否需要将梯度直接置为 None;Paddle中这个参数无效。 + :param set_to_none: 用来判断是否需要将梯度直接置为 ``None``;在 **PaddlePaddle** 中这个参数无效。 """ for optimizer in self.optimizers: optimizer.clear_grad() @@ -87,14 +102,6 @@ class PaddleDriver(Driver): @staticmethod def check_dataloader_legality(dataloader, dataloader_name, is_train: bool = False): - r""" - 该函数会在 trainer 或者 evaluator 设置 dataloader 后检测 dataloader 的合法性。 - 要求传入的 dataloader 必须为 `paddle.io.DataLoader` 或包含该类型的字典。 - - :param dataloader: 需要检测的输入的 `dataloader`; - :param dataloader_name: - :param is_train: - """ if is_train: if not isinstance(dataloader, DataLoader): raise ValueError(f"Parameter `{dataloader_name}` should be 'paddle.io.DataLoader' type, not {type(dataloader)}.") @@ -164,16 +171,15 @@ class PaddleDriver(Driver): @rank_zero_call def save_model(self, filepath: str, only_state_dict: bool = True, **kwargs): r""" - 保存模型的函数;注意函数 `save` 是用来进行断点重训的函数; + 将模型保存到 ``filepath`` 中。 :param filepath: 保存文件的文件位置(需要包括文件名); - :param only_state_dict: 是否只保存模型的 `state_dict`;如果为 False,则会调用 `paddle.jit.save` 函数 - 保存整个模型的参数,此时需要传入 `input_spec` 参数,否则在 load 时会报错。 - :param kwargs: - input_spec: 描述存储模型 forward 方法的输入,当 `only_state_dict` 为 False时必须传入,否则加载时会报错。 - 可以通过 InputSpec 或者示例 Tensor 进行描述。详细的可以参考 paddle 关于`paddle.jit.save` - 的文档: - https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/jit/save_cn.html#save + :param only_state_dict: 是否只保存模型的 ``state_dict``;如果为 ``False``,则会调用 ``paddle.jit.save`` 函数 + 保存整个模型的参数,此时需要传入 ``input_spec`` 参数; + :kwargs: + * input_spec -- 描述存储模型 ``forward`` 方法的输入; + 当 ``only_state_dict`` 为 ``False`` 时必须传入,否则加载时会报错。您可以通过 ``InputSpec`` 或者示例 ``Tensor`` + 进行描述。详细的使用方法可以参考 **PaddlePaddle** `关于 paddle.jit.save 函数的文档 `_; """ model = self.unwrap_model() if isinstance(filepath, Path): @@ -189,14 +195,6 @@ class PaddleDriver(Driver): paddle.jit.save(model, filepath, input_spec) def load_model(self, filepath: str, only_state_dict: bool = True, **kwargs): - r""" - 加载模型的函数;将 filepath 中的模型加载并赋值给当前 model 。 - - :param filepath: 需要被加载的对象的文件位置(需要包括文件名); - :param load_state_dict: 保存的文件是否只是模型的权重,还是完整的模型。即便是保存的完整的模型,此处也只能使用尝试加载filepath - 模型中的权重到自身模型,而不会直接替代当前 Driver 中的模型。 - :return: 返回加载指定文件后的结果; - """ model = self.unwrap_model() if isinstance(filepath, Path): filepath = str(filepath) @@ -210,6 +208,28 @@ class PaddleDriver(Driver): @rank_zero_call def save(self, folder: Path, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): + r""" + 断点重训的保存函数,该函数会负责保存模型和 optimizers, fp16 的 state_dict;以及模型的保存(若 should_save_model 为 True) + + :param folder: 保存断点重训的状态的文件夹;save 函数应该在下面新增两(一)个文件 的 FASTNLP_CHECKPOINT_FILENAME 文件与 + FASTNLP_MODEL_FILENAME (如果 should_save_model 为 True )。把 model 相关的内容放入到 FASTNLP_MODEL_FILENAME 文件 + 中,将传入的 states 以及自身产生其它状态一并保存在 FASTNLP_CHECKPOINT_FILENAME 里面。 + :param states: 由 trainer 传入的一个字典,其中已经包含了为了实现断点重训所需要保存的其它对象的状态,Driver 应该只需要保存 + 该对象即可, Driver 应该不需要理解该对象,同时在 driver.load() 的时候,需要将 states 返回回去,load() 返回的值与这里的 + 传入的值保持一致。 + :param dataloader: 正在使用的 dataloader,需要保存里面的状态使得之后可以从当前迭代的位置恢复。 + :param only_state_dict: 是否只保存模型的参数,当 should_save_model 为 False ,该参数无效。 + :param should_save_model: 是否应该保存模型,如果为False,Driver 将不负责 model 的保存。 + :kwargs: + * input_spec -- 描述存储模型 ``forward`` 方法的输入; + 当 ``only_state_dict`` 为 ``False`` 时必须传入,否则加载时会报错。您可以通过 ``InputSpec`` 或者示例 ``Tensor`` + 进行描述。详细的使用方法可以参考 **PaddlePaddle** `关于 paddle.jit.save 函数的文档 `_; + + .. todo: + + 等 Driver 的文档写完 + + """ # 传入的 dataloader 参数是 trainer 的 dataloader 属性,因为 driver 的所有 dataloader 我们是不会去改变它的,而是通过改变 # trainer.dataloader 来改变 dataloader 的状态,从而适配训练或者评测环境; @@ -352,37 +372,41 @@ class PaddleDriver(Driver): r""" 返回一个不计算梯度的环境用来对模型进行评测; - :return: context 上下文对象 `paddle.no_grad`; + :return: 上下文对象 ``paddle.no_grad``; """ return paddle.no_grad @staticmethod def move_model_to_device(model: "paddle.nn.Layer", device: Union[str, int, "paddle.CUDAPlace", "paddle.CPUPlace"]): r""" - 用来将模型转移到指定的 device 上; - 在 Paddle 中使用可能会引起因与设置的设备不一致而产生的问题,请注意。 + 用来将模型 ``model`` 转移到指定的设备上; + + .. note:: + + 在 **Paddle** 中使用可能会引起因与设置的设备不一致而产生的问题,请注意。 + + :param model: 需要进行转移的模型; + :param device: 目标设备; """ if device is not None: model.to(device) - def move_data_to_device(self, batch: "paddle.Tensor"): + def move_data_to_device(self, batch: Any) -> Any: r""" - 将数据迁移到指定的机器上;batch 可能是 list 也可能 dict ,或其嵌套结构。 - 在 Paddle 中使用可能会引起因与设置的设备不一致而产生的问题,请注意。 + 将数据集合 ``batch`` 迁移到指定的机器上。 + + .. note:: + + 在 **Paddle** 中使用可能会引起因与设置的设备不一致而产生的问题,请注意。 - :return: 将移动到指定机器上的 batch 对象返回; + :param batch: 包含 :class:`paddle.Tensor` 的数据集合,可以是 **List**、**Dict** 等嵌套类型; + :return: 移动到指定机器后的 `batch``; """ device = _convert_data_device(self.data_device) return paddle_move_data_to_device(batch, device) @staticmethod def worker_init_function(worker_id: int, rank: Optional[int] = None) -> None: # pragma: no cover - """The worker_init_fn that Lightning automatically adds to your dataloader if you previously set set the seed - with ``seed_everything(seed, workers=True)``. - - See also the PyTorch documentation on - `randomness in DataLoaders `_. - """ # implementation notes: https://github.com/pytorch/pytorch/issues/5059#issuecomment-817392562 global_rank = rank if rank is not None else int(os.environ.get(FASTNLP_GLOBAL_RANK, 0)) # TODO gpu @@ -409,9 +433,6 @@ class PaddleDriver(Driver): @staticmethod def get_dataloader_args(dataloader: "DataLoader"): - """ - 获取 dataloader 的 shuffle 和 drop_last 属性; - """ @dataclass class Res: diff --git a/fastNLP/core/drivers/paddle_driver/single_device.py b/fastNLP/core/drivers/paddle_driver/single_device.py index c0957dbf..a9e92fd3 100644 --- a/fastNLP/core/drivers/paddle_driver/single_device.py +++ b/fastNLP/core/drivers/paddle_driver/single_device.py @@ -33,9 +33,20 @@ __all__ = [ class PaddleSingleDriver(PaddleDriver): """ - 支持 paddle cpu 或单卡 gpu 训练的 driver + 实现了 **PaddlePaddle** 框架下在单卡或 ``cpu`` 环境下训练功能的 **Driver**。 + + :param model: 训练时使用的 **PaddlePaddle** 模型; + :param device: 训练使用的设备; + :param fp16: 是否开启混合精度训练; + :kwargs: + * wo_auto_param_call (``bool``) -- 是否关闭在训练时调用我们的 ``auto_param_call`` 函数来自动匹配 batch 和前向函数的参数的行为; + + .. note:: + + 关于该参数的详细说明,请参见 :class:`~fastNLP.core.controllers.Trainer` 中的描述;函数 ``auto_param_call`` 详见 :func:`fastNLP.core.utils.auto_param_call`。 + """ - def __init__(self, model, device: Union[str, int], fp16: Optional[bool] = False, **kwargs): + def __init__(self, model: "paddle.nn.Layer", device: Union[str, int], fp16: Optional[bool] = False, **kwargs): if isinstance(model, DataParallel): raise ValueError("`paddle.DataParallel` is not supported in `PaddleSingleDriver`") @@ -62,7 +73,7 @@ class PaddleSingleDriver(PaddleDriver): def setup(self): r""" - 该函数用来初始化训练环境,用于设置当前训练的设备,并将模型迁移到对应设备上。 + 初始化训练环境;设置当前训练的设备,并将模型迁移到对应设备上。 """ device = _convert_data_device(self.data_device) @@ -127,17 +138,20 @@ class PaddleSingleDriver(PaddleDriver): return dataloader def unwrap_model(self): - if isinstance(self.model, paddle.DataParallel): - return self.model._layers - else: - return self.model + """ + 返回训练使用的模型。 + """ + return self.model @property - def data_device(self): + def data_device(self) -> str: """ - 返回数据所在的设备。由于单卡模式不支持 data_device,因此返回的是 model_device + :return: 数据和模型所在的设备; """ return self.model_device - def is_distributed(self): + def is_distributed(self) -> bool: + """ + 判断是否为分布式的 **Driver** ,在 ``PaddleSingleDriver`` 中,返回 ``False`` + """ return False diff --git a/fastNLP/core/drivers/paddle_driver/utils.py b/fastNLP/core/drivers/paddle_driver/utils.py index 6362193e..1a324c97 100644 --- a/fastNLP/core/drivers/paddle_driver/utils.py +++ b/fastNLP/core/drivers/paddle_driver/utils.py @@ -31,7 +31,15 @@ __all__ = [ def _select_seed_randomly(min_seed_value: int = 0, max_seed_value: int = 255) -> int: return random.randint(min_seed_value, max_seed_value) -def paddle_seed_everything(seed: Optional[int] = None, workers: bool = False) -> int: +def paddle_seed_everything(seed: Optional[int], workers: bool = False) -> int: + r""" + 为 **paddle**、**numpy**、**python.random** 伪随机数生成器设置种子。 + + :param seed: 全局随机状态的整数值种子。如果为 ``None``,将从环境变量 ``FASTNLP_GLOBAL_SEED`` 中读取种子或随机选择; + :param workers: 如果为 ``True`` ,则会设置环境变量 ``FASTNLP_SEED_WORKERS`` 。该环境变量会在 :class:`~fastNLP.core.Trainer` + 中配置 ``dataloader`` 时用于设置 ``worker_init_fn`` 。如果用户已经为 ``dataloader`` 提供了 ``worker_init_fn`` ,则设置 + 此参数将没有影响; + """ max_seed_value = np.iinfo(np.uint32).max min_seed_value = np.iinfo(np.uint32).min @@ -70,7 +78,7 @@ def paddle_seed_everything(seed: Optional[int] = None, workers: bool = False) -> def reset_seed() -> None: """ - fleet 会开启多个进程,因此当用户在脚本中指定 seed_everything 时,在开启多个脚本后,会在每个脚本内重新 + ``fleet`` 会开启多个进程,因此当用户在脚本中指定 ``seed_everything`` 时,在开启多个脚本后,会在每个脚本内重新 进行随机数的设置; """ seed = os.environ.get(FASTNLP_GLOBAL_SEED, None) @@ -80,8 +88,8 @@ def reset_seed() -> None: class _FleetWrappingModel(Layer): """ - 参考 _DDPWrappingModel , paddle 的分布式训练也需要用 paddle.nn.DataParallel 进行包装,采用和 - pytorch 相似的处理方式 + 参考 :class:`fastNLP.core.drivers.torch_driver.utils._DDPWrappingModel` , **PaddlePaddle** 的分布式训练也需要用 :class:`paddle.nn.DataParallel` 进行包装,采用和 + **pytorch** 相似的处理方式 """ def __init__(self, model: 'nn.Layer'): super(_FleetWrappingModel, self).__init__() @@ -100,7 +108,7 @@ class _FleetWrappingModel(Layer): class DummyGradScaler: """ - 用于仿造的GradScaler对象,防止重复写大量的if判断 + 用于仿造的 **GradScaler** 对象,防止重复写大量的if判断 """ def __init__(self, *args, **kwargs): pass @@ -144,7 +152,7 @@ def _build_fp16_env(dummy=False): def find_free_ports(num): """ - 在空闲的端口中找到 num 个端口 + 在空闲的端口中找到 ``num`` 个端口 """ def __free_port(): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: @@ -174,8 +182,8 @@ def find_free_ports(num): def replace_batch_sampler(dataloader: "DataLoader", batch_sampler: "BatchSampler"): """ - 利用 `batch_sampler` 重新构建一个 DataLoader,起到替换 `batch_sampler` 又不影响原 `dataloader` 的作用。 - 考虑了用户自己定制了 DataLoader 的情形。 + 利用 ``batch_sampler`` 重新构建一个 ``DataLoader``,起到替换 ``batch_sampler`` 又不影响原 ``dataloader`` 的作用。 + 考虑了用户自己定制了 ``DataLoader`` 的情形。 """ # 拿到非下划线开头的实例属性; instance_attrs = {k: v for k, v in vars(dataloader).items() if not k.startswith('_')} @@ -246,7 +254,7 @@ def replace_batch_sampler(dataloader: "DataLoader", batch_sampler: "BatchSampler def replace_sampler(dataloader, new_sampler): """ - 使用 `new_sampler` 重新构建一个 BatchSampler,并替换到 `dataloader` 中 + 使用 ``new_sampler`` 重新构建一个 ``BatchSampler``,并替换到 ``dataloader`` 中 """ new_batch_sampler = deepcopy(dataloader.batch_sampler) new_batch_sampler.sampler = new_sampler From 36174f727d482f829e3965adb83bc5e9119c5597 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 11 May 2022 19:18:10 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E5=A2=9E=E5=8A=A0TransformersAccuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/__init__.py | 1 + fastNLP/core/callbacks/callback_manager.py | 2 +- .../controllers/loops/evaluate_batch_loop.py | 14 ++++--- .../controllers/loops/train_batch_loop.py | 27 ++++++------ .../core/dataloaders/jittor_dataloader/fdl.py | 2 +- .../core/dataloaders/paddle_dataloader/fdl.py | 2 +- .../core/dataloaders/prepare_dataloader.py | 2 +- .../core/dataloaders/torch_dataloader/fdl.py | 6 +-- fastNLP/core/dataloaders/utils.py | 3 +- .../drivers/torch_driver/single_device.py | 42 +++++++++++-------- fastNLP/core/metrics/__init__.py | 3 +- fastNLP/core/metrics/accuracy.py | 30 ++++++++++--- fastNLP/io/data_bundle.py | 6 +-- .../transformers/torch/configuration_utils.py | 4 +- .../torch/generation_stopping_criteria.py | 2 +- .../transformers/torch/generation_utils.py | 4 +- fastNLP/transformers/torch/modeling_utils.py | 6 +-- .../torch/models/bart/configuration_bart.py | 2 +- 18 files changed, 94 insertions(+), 64 deletions(-) diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index fc47b470..343313a6 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -69,6 +69,7 @@ __all__ = [ # metrics "Metric", "Accuracy", + "TransformersAccuracy", 'SpanFPreRecMetric', 'ClassifyFPreRecMetric', diff --git a/fastNLP/core/callbacks/callback_manager.py b/fastNLP/core/callbacks/callback_manager.py index 27770115..765a0346 100644 --- a/fastNLP/core/callbacks/callback_manager.py +++ b/fastNLP/core/callbacks/callback_manager.py @@ -25,7 +25,7 @@ def _transfer(func): for callback_fn in manager.callback_fns[func.__name__]: try: callback_fn(*arg, **kwargs) - except EarlyStopException as e: + except (EarlyStopException, KeyboardInterrupt) as e: raise e except BaseException as e: logger.error(f"The following callback_fn raise exception:{_get_fun_msg(callback_fn)}.") diff --git a/fastNLP/core/controllers/loops/evaluate_batch_loop.py b/fastNLP/core/controllers/loops/evaluate_batch_loop.py index 80c234cd..c81379a1 100644 --- a/fastNLP/core/controllers/loops/evaluate_batch_loop.py +++ b/fastNLP/core/controllers/loops/evaluate_batch_loop.py @@ -27,19 +27,21 @@ class EvaluateBatchLoop(Loop): while True: try: batch = next(iterator) - batch = match_and_substitute_params(evaluator.input_mapping, batch) - batch = evaluator.move_data_to_device(batch) except StopIteration: break + try: + batch = match_and_substitute_params(evaluator.input_mapping, batch) + batch = evaluator.move_data_to_device(batch) + + self.batch_step_fn(evaluator, batch) + batch_idx += 1 + evaluator.update_progress_bar(batch_idx, evaluator.cur_dataloader_name) + except BaseException as e: if callable(getattr(dataloader, 'get_batch_indices', None)): indices = dataloader.get_batch_indices() logger.error(f"Exception happens when evaluating on samples: {indices}") raise e - - self.batch_step_fn(evaluator, batch) - batch_idx += 1 - evaluator.update_progress_bar(batch_idx, evaluator.cur_dataloader_name) # 获取metric结果。返回的dict内容示例为{'metric_name1': metric_results, 'metric_name2': metric_results, ...} results = evaluator.get_metric() return results diff --git a/fastNLP/core/controllers/loops/train_batch_loop.py b/fastNLP/core/controllers/loops/train_batch_loop.py index 989fb2ae..7bb9b653 100644 --- a/fastNLP/core/controllers/loops/train_batch_loop.py +++ b/fastNLP/core/controllers/loops/train_batch_loop.py @@ -19,30 +19,31 @@ class TrainBatchLoop(Loop): get_batch_indices = dataloader.get_batch_indices if callable(getattr(dataloader, 'get_batch_indices', None))\ else lambda *args, **kwargs: None dataloader = iter(dataloader) - indices = None while trainer.batch_idx_in_epoch<=trainer.num_batches_per_epoch: try: trainer.on_fetch_data_begin() batch = next(dataloader) indices = get_batch_indices() + except StopIteration: + break + + try: trainer.on_fetch_data_end() batch = match_and_substitute_params(trainer.input_mapping, batch) batch = trainer.move_data_to_device(batch) - except StopIteration: - break + + trainer.on_train_batch_begin(batch, indices) + with trainer.get_no_sync_context(): # 在多卡的时候可能需要关闭 sync + self.batch_step_fn(trainer, batch) + trainer.global_forward_batches += 1 + trainer.batch_idx_in_epoch += 1 + + trainer.check_batch_step_fn() + trainer.on_train_batch_end() except BaseException as e: - if indices and not isinstance(e, EarlyStopException): + if indices is not None and not isinstance(e, (EarlyStopException, KeyboardInterrupt)): logger.error(f"Exception happens when running on samples: {indices}") raise e - - trainer.on_train_batch_begin(batch, indices) - with trainer.get_no_sync_context(): # 在多卡的时候可能需要关闭 sync - self.batch_step_fn(trainer, batch) - trainer.global_forward_batches += 1 - trainer.batch_idx_in_epoch += 1 - - trainer.check_batch_step_fn() - trainer.on_train_batch_end() trainer.step_evaluate() trainer.batch_idx_in_epoch = 0 diff --git a/fastNLP/core/dataloaders/jittor_dataloader/fdl.py b/fastNLP/core/dataloaders/jittor_dataloader/fdl.py index 8ecd2d87..b76fd4c1 100644 --- a/fastNLP/core/dataloaders/jittor_dataloader/fdl.py +++ b/fastNLP/core/dataloaders/jittor_dataloader/fdl.py @@ -47,7 +47,7 @@ class JittorDataLoader: 提供给使用jittor框架的DataLoader函数,提供了auto_collate的功能, 支持实现了__getitem__和__len__的dataset """ - def __init__(self, dataset, batch_size: int = 16, shuffle: bool = False, + def __init__(self, dataset, batch_size: int = 16, shuffle: bool = True, drop_last: bool = False, num_workers: int = 0, buffer_size: int = 512 * 1024 * 1024, stop_grad: bool = True, keep_numpy_array: bool = False, endless: bool = False, collate_fn: Union[None, str, Callable] = "auto") -> None: diff --git a/fastNLP/core/dataloaders/paddle_dataloader/fdl.py b/fastNLP/core/dataloaders/paddle_dataloader/fdl.py index 393324d4..4c2f2300 100644 --- a/fastNLP/core/dataloaders/paddle_dataloader/fdl.py +++ b/fastNLP/core/dataloaders/paddle_dataloader/fdl.py @@ -47,7 +47,7 @@ class PaddleDataLoader(DataLoader): def __init__(self, dataset, feed_list=None, places=None, return_list: bool = True, batch_sampler=None, - batch_size: int = 1, shuffle: bool = False, + batch_size: int = 1, shuffle: bool = True, drop_last: bool = False, collate_fn: Union[str, Callable, None] = 'auto', num_workers: int = 0, use_buffer_reader: bool = True, use_shared_memory: bool = True, timeout: int = 0, diff --git a/fastNLP/core/dataloaders/prepare_dataloader.py b/fastNLP/core/dataloaders/prepare_dataloader.py index 8a7e3d1e..33764c6f 100644 --- a/fastNLP/core/dataloaders/prepare_dataloader.py +++ b/fastNLP/core/dataloaders/prepare_dataloader.py @@ -14,7 +14,7 @@ from ...envs import FASTNLP_BACKEND, SUPPORT_BACKENDS from ..log import logger -def prepare_dataloader(dataset, batch_size: int = 16, shuffle: bool = False, drop_last: bool = False, +def prepare_dataloader(dataset, batch_size: int = 16, shuffle: bool = True, drop_last: bool = False, collate_fn: Union[Callable, str, None] = 'auto', num_workers: int = 0, seed: int = 0, backend: str = 'auto'): """ diff --git a/fastNLP/core/dataloaders/torch_dataloader/fdl.py b/fastNLP/core/dataloaders/torch_dataloader/fdl.py index 456af44f..726abaae 100644 --- a/fastNLP/core/dataloaders/torch_dataloader/fdl.py +++ b/fastNLP/core/dataloaders/torch_dataloader/fdl.py @@ -179,7 +179,7 @@ class TorchDataLoader(DataLoader): def prepare_torch_dataloader(ds_or_db: Union[DataSet, Sequence[DataSet], Mapping[str, DataSet]], batch_size: int = 1, - shuffle: bool = False, + shuffle: bool = True, sampler: Union["Sampler[int]", ReproducibleSampler, UnrepeatedSampler] = None, batch_sampler: Union["Sampler[Sequence[int]]", ReproducibleBatchSampler] = None, num_workers: int = 0, collate_fn: Union[str, Callable, None] = 'auto', @@ -236,8 +236,8 @@ def prepare_torch_dataloader(ds_or_db: Union[DataSet, Sequence[DataSet], Mapping persistent_workers=persistent_workers, ) else: - dl_bundle[name] = TorchDataLoader(dataset=ds, batch_size=non_train_batch_size, - shuffle=shuffle, sampler=non_train_sampler, + dl_bundle[name] = TorchDataLoader(dataset=ds, batch_size=non_train_batch_size if non_train_batch_size else batch_size, + shuffle=shuffle, sampler=non_train_sampler if non_train_sampler else sampler, batch_sampler=batch_sampler, num_workers=num_workers, collate_fn=collate_fn, pin_memory=pin_memory, drop_last=drop_last, timeout=timeout, worker_init_fn=worker_init_fn, diff --git a/fastNLP/core/dataloaders/utils.py b/fastNLP/core/dataloaders/utils.py index 39ce5983..495fb6d3 100644 --- a/fastNLP/core/dataloaders/utils.py +++ b/fastNLP/core/dataloaders/utils.py @@ -1,9 +1,10 @@ +from typing import Callable __all__ = [ "indice_collate_wrapper" ] -def indice_collate_wrapper(func): +def indice_collate_wrapper(func:Callable): """ 其功能是封装一层collate_fn,将dataset取到的tuple数据分离开,将idx打包为indices。 diff --git a/fastNLP/core/drivers/torch_driver/single_device.py b/fastNLP/core/drivers/torch_driver/single_device.py index 6c125a73..8aa9a2d5 100644 --- a/fastNLP/core/drivers/torch_driver/single_device.py +++ b/fastNLP/core/drivers/torch_driver/single_device.py @@ -1,11 +1,13 @@ import os from typing import Dict, Union, Callable, Tuple, Optional from fastNLP.envs.imports import _NEED_IMPORT_TORCH + if _NEED_IMPORT_TORCH: import torch from torch.nn import DataParallel from torch.nn.parallel import DistributedDataParallel from torch.utils.data import RandomSampler as TorchRandomSampler + from torch.utils.data import SequentialSampler as TorchSequentialSampler __all__ = [ 'TorchSingleDriver' @@ -15,7 +17,8 @@ from .torch_driver import TorchDriver from fastNLP.core.drivers.torch_driver.utils import replace_sampler, replace_batch_sampler from fastNLP.core.utils import auto_param_call from fastNLP.core.utils.utils import _get_fun_msg -from fastNLP.core.samplers import ReproducibleBatchSampler, ReproducibleSampler, re_instantiate_sampler, ReproduceBatchSampler +from fastNLP.core.samplers import ReproducibleBatchSampler, ReproducibleSampler, re_instantiate_sampler, \ + ReproduceBatchSampler from fastNLP.core.samplers import RandomSampler from fastNLP.core.log import logger @@ -24,6 +27,7 @@ class TorchSingleDriver(TorchDriver): r""" 用于 cpu 和 单卡 gpu 运算; """ + def __init__(self, model, device: "torch.device", fp16: bool = False, **kwargs): if isinstance(model, DistributedDataParallel): raise ValueError("`DistributedDataParallel` is not supported in `TorchSingleDriver`") @@ -88,7 +92,8 @@ class TorchSingleDriver(TorchDriver): else: raise RuntimeError(f"There is no `{fn}` method in your {type(self.model)}.") - def set_dist_repro_dataloader(self, dataloader, dist: Union[str, ReproducibleBatchSampler, ReproducibleSampler]=None, + def set_dist_repro_dataloader(self, dataloader, + dist: Union[str, ReproducibleBatchSampler, ReproducibleSampler] = None, reproducible: bool = False): # 如果 dist 为 ReproducibleBatchSampler, ReproducibleIterator 说明是在断点重训时 driver.load 函数调用; @@ -108,17 +113,24 @@ class TorchSingleDriver(TorchDriver): if reproducible: if isinstance(args.sampler, TorchRandomSampler): - # 如果本来就是随机的,直接替换掉吧。 - sampler = RandomSampler(args.sampler.data_source) - logger.debug("Replace torch RandomSampler into fastNLP RandomSampler.") + if getattr(args.sampler, '_num_samples', None) is None \ + and getattr(args.sampler, 'replacements', False) is False \ + and getattr(args.sampler, 'generator', None) is None: + # 如果本来就是随机的,并且没有定制,直接替换掉吧。 + sampler = RandomSampler(args.sampler.data_source, shuffle=True) + logger.debug("Replace torch RandomSampler into fastNLP RandomSampler.") + return replace_sampler(dataloader, sampler) + elif isinstance(args.sampler, TorchSequentialSampler): + # 需要替换为不要 shuffle 的。 + sampler = RandomSampler(args.sampler.data_source, shuffle=False) + logger.debug("Replace torch SequentialSampler into fastNLP RandomSampler.") return replace_sampler(dataloader, sampler) - else: - batch_sampler = ReproduceBatchSampler( - batch_sampler=args.batch_sampler, - batch_size=args.batch_size, - drop_last=args.drop_last - ) - return replace_batch_sampler(dataloader, batch_sampler) + batch_sampler = ReproduceBatchSampler( + batch_sampler=args.batch_sampler, + batch_size=args.batch_size, + drop_last=args.drop_last + ) + return replace_batch_sampler(dataloader, batch_sampler) else: return dataloader @@ -138,9 +150,3 @@ class TorchSingleDriver(TorchDriver): def is_distributed(self): return False - - - - - - diff --git a/fastNLP/core/metrics/__init__.py b/fastNLP/core/metrics/__init__.py index f7d60606..b7f572e8 100644 --- a/fastNLP/core/metrics/__init__.py +++ b/fastNLP/core/metrics/__init__.py @@ -1,11 +1,12 @@ __all__ = [ "Metric", "Accuracy", + "TransformersAccuracy", 'SpanFPreRecMetric', 'ClassifyFPreRecMetric', ] from .metric import Metric -from .accuracy import Accuracy +from .accuracy import Accuracy, TransformersAccuracy from .span_f1_pre_rec_metric import SpanFPreRecMetric from .classify_f1_pre_rec_metric import ClassifyFPreRecMetric diff --git a/fastNLP/core/metrics/accuracy.py b/fastNLP/core/metrics/accuracy.py index 0869d8c8..59990f95 100644 --- a/fastNLP/core/metrics/accuracy.py +++ b/fastNLP/core/metrics/accuracy.py @@ -1,5 +1,6 @@ __all__ = [ - 'Accuracy' + 'Accuracy', + "TransformersAccuracy" ] from typing import Union @@ -17,9 +18,9 @@ class Accuracy(Metric): """ 计算 准确率 的 metric 。 - :param str backend: 目前支持四种类型的backend, ['auto', 'torch', 'paddle', 'jittor']。其中 auto 表示根据实际调用 Metric.update() + :param backend: 目前支持四种类型的backend, ['auto', 'torch', 'paddle', 'jittor']。其中 auto 表示根据实际调用 Metric.update() 函数时传入的参数决定具体的 backend ,一般情况下直接使用 'auto' 即可。 - :param bool aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到 metric, + :param aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到 metric, 当 backend 不支持分布式时,该参数无意义。如果为 None ,将在 Evaluator 中根据 sampler 是否使用分布式进行自动设置。 """ super(Accuracy, self).__init__(backend=backend, aggregate_when_get_metric=aggregate_when_get_metric) @@ -39,11 +40,11 @@ class Accuracy(Metric): r""" update 函数将针对一个批次的预测结果做评价指标的累计 - :param torch.Tensor pred: 预测的tensor, tensor的形状可以是torch.Size([B,]), torch.Size([B, n_classes]), + :param pred: 预测的tensor, tensor的形状可以是torch.Size([B,]), torch.Size([B, n_classes]), torch.Size([B, max_len]), 或者torch.Size([B, max_len, n_classes]) - :param torch.Tensor target: 真实值的tensor, tensor的形状可以是Element's can be: torch.Size([B,]), + :param target: 真实值的tensor, tensor的形状可以是Element's can be: torch.Size([B,]), torch.Size([B,]), torch.Size([B, max_len]), 或者torch.Size([B, max_len]) - :param torch.Tensor seq_len: 序列长度标记, 标记的形状可以是None, None, torch.Size([B]), 或者torch.Size([B]). + :param seq_len: 序列长度标记, 标记的形状可以是None, None, torch.Size([B]), 或者torch.Size([B]). 如果mask也被传进来的话seq_len会被忽略. """ # 为了兼容不同框架,我们将输入变量全部转为numpy类型来进行计算。 @@ -79,3 +80,20 @@ class Accuracy(Metric): else: self.total += np.prod(list(pred.shape)).item() self.correct += (target == pred).sum().item() + + +class TransformersAccuracy(Accuracy): + """ + 适配 transformers 中相关模型的 Accuracy metric 。 + + """ + def update(self, logits, labels, attention_mask=None): + r""" + update 函数将针对一个批次的预测结果做评价指标的累计 + + :param logits: 形状为 ``[B, n_classes]`` 或 ``[B, max_len, n_classes]`` 。 + :param labels: 形状为 ``[B, ]`` 或 ``[B, max_len]`` + :param attention_mask: 序列长度标记。 + """ + seq_len = attention_mask.sum(dim=-1) + super().update(pred=logits, target=labels, seq_len=seq_len) \ No newline at end of file diff --git a/fastNLP/io/data_bundle.py b/fastNLP/io/data_bundle.py index a3c15a28..df194df2 100644 --- a/fastNLP/io/data_bundle.py +++ b/fastNLP/io/data_bundle.py @@ -249,7 +249,7 @@ class DataBundle: return self def apply_field_more(self, func: Callable, field_name: str, num_proc: int = 0, modify_fields=True, - ignore_miss_dataset=True, progress_desc: str = '', show_progress_bar: bool = True): + ignore_miss_dataset=True, show_progress_bar: bool = True, progress_desc: str = ''): r""" 对 :class:`~fastNLP.io.DataBundle` 中所有的 dataset 使用 :meth:`~fastNLP.DataSet.apply_field_more` 方法 @@ -263,8 +263,8 @@ class DataBundle: :param num_proc: 进程的数量。请注意,由于python语言的特性,多少进程就会导致多少倍内存的增长。 :param bool ignore_miss_dataset: 当某个field名称在某个dataset不存在时,如果为True,则直接忽略该DataSet; 如果为False,则报错 - :param show_progress_bar: 是否显示tqdm进度条 - :param progress_desc: 当show_progress_barm为True时,可以显示当前tqdm正在处理的名称 + :param show_progress_bar: 是否显示进度条 + :param progress_desc: 当 ``show_progress_bar`` 为 ``True`` 时,可以显示 ``progress`` 的名称。 :return Dict[str:Dict[str:Field]]: 返回一个字典套字典,第一层的 key 是 dataset 的名字,第二层的 key 是 field 的名字 diff --git a/fastNLP/transformers/torch/configuration_utils.py b/fastNLP/transformers/torch/configuration_utils.py index 9c17f336..fb494d9f 100644 --- a/fastNLP/transformers/torch/configuration_utils.py +++ b/fastNLP/transformers/torch/configuration_utils.py @@ -314,7 +314,7 @@ class PretrainedConfig: # TPU arguments if kwargs.pop("xla_device", None) is not None: - logger.warning( + logger.rank_zero_warning( "The `xla_device` argument has been deprecated in v4.4.0 of Transformers. It is ignored and you can " "safely remove it from your `config.json` file." ) @@ -474,7 +474,7 @@ class PretrainedConfig: """ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs) if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type: - logger.warn( + logger.rank_zero_warning( f"You are using a model of type {config_dict['model_type']} to instantiate a model of type " f"{cls.model_type}. This is not supported for all configurations of models and can yield errors." ) diff --git a/fastNLP/transformers/torch/generation_stopping_criteria.py b/fastNLP/transformers/torch/generation_stopping_criteria.py index 179bf7c1..da2bcf9b 100644 --- a/fastNLP/transformers/torch/generation_stopping_criteria.py +++ b/fastNLP/transformers/torch/generation_stopping_criteria.py @@ -122,7 +122,7 @@ def validate_stopping_criteria(stopping_criteria: StoppingCriteriaList, max_leng stopping_max_length = stopping_criteria.max_length new_stopping_criteria = deepcopy(stopping_criteria) if stopping_max_length is not None and stopping_max_length != max_length: - logger.warn("You set different `max_length` for stopping criteria and `max_length` parameter", UserWarning) + logger.rank_zero_warning("You set different `max_length` for stopping criteria and `max_length` parameter", UserWarning) elif stopping_max_length is None: new_stopping_criteria.append(MaxLengthCriteria(max_length=max_length)) return new_stopping_criteria diff --git a/fastNLP/transformers/torch/generation_utils.py b/fastNLP/transformers/torch/generation_utils.py index cfc2108c..0e6fe5c7 100644 --- a/fastNLP/transformers/torch/generation_utils.py +++ b/fastNLP/transformers/torch/generation_utils.py @@ -429,7 +429,7 @@ class GenerationMixin: def _get_pad_token_id(self, pad_token_id: int = None, eos_token_id: int = None) -> int: if pad_token_id is None and eos_token_id is not None: - logger.warning(f"Setting `pad_token_id` to `eos_token_id`:{eos_token_id} for open-end generation.") + logger.rank_zero_warning(f"Setting `pad_token_id` to `eos_token_id`:{eos_token_id} for open-end generation.") pad_token_id = eos_token_id return pad_token_id @@ -912,7 +912,7 @@ class GenerationMixin: # special case if pad_token_id is not defined if pad_token_id is None and eos_token_id is not None: - logger.warning(f"Setting `pad_token_id` to `eos_token_id`:{eos_token_id} for open-end generation.") + logger.rank_zero_warning(f"Setting `pad_token_id` to `eos_token_id`:{eos_token_id} for open-end generation.") pad_token_id = eos_token_id # Storing encoder_input_ids for logits_processor that could use them diff --git a/fastNLP/transformers/torch/modeling_utils.py b/fastNLP/transformers/torch/modeling_utils.py index d1d5c2f3..d19816a3 100644 --- a/fastNLP/transformers/torch/modeling_utils.py +++ b/fastNLP/transformers/torch/modeling_utils.py @@ -352,7 +352,7 @@ class ModuleUtilsMixin: if token_inputs: return sum([token_input.numel() for token_input in token_inputs]) else: - logger.warn( + logger.rank_zero_warning( "Could not estimate the number of tokens of the input, floating-point operations will not be computed" ) return 0 @@ -646,7 +646,7 @@ class PreTrainedModel(Module, ModuleUtilsMixin, GenerationMixin): # tie weights recursively tie_encoder_to_decoder_recursively(decoder, encoder, base_model_prefix, uninitialized_encoder_weights) if len(uninitialized_encoder_weights) > 0: - logger.warning( + logger.rank_zero_warning( f"The following encoder weights were not tied to the decoder {uninitialized_encoder_weights}" ) @@ -1486,7 +1486,7 @@ class PreTrainedModel(Module, ModuleUtilsMixin, GenerationMixin): raise RuntimeError(f"Error(s) in loading state_dict for {model.__class__.__name__}:\n\t{error_msg}") if len(unexpected_keys) > 0: - logger.warning( + logger.rank_zero_warning( f"Some weights of the model checkpoint at {pretrained_model_name_or_path} were not used when " f"initializing {model.__class__.__name__}: {unexpected_keys}\n" f"- This IS expected if you are initializing {model.__class__.__name__} from the checkpoint of a model trained on another task " diff --git a/fastNLP/transformers/torch/models/bart/configuration_bart.py b/fastNLP/transformers/torch/models/bart/configuration_bart.py index 3b52bc81..9465326b 100644 --- a/fastNLP/transformers/torch/models/bart/configuration_bart.py +++ b/fastNLP/transformers/torch/models/bart/configuration_bart.py @@ -171,7 +171,7 @@ class BartConfig(PretrainedConfig): # ensure backward compatibility for BART CNN models if self.forced_bos_token_id is None and kwargs.get("force_bos_token_to_be_generated", False): self.forced_bos_token_id = self.bos_token_id - logger.warn( + logger.rank_zero_warning( f"Please make sure the config includes `forced_bos_token_id={self.bos_token_id}` in future versions." "The config can simply be saved and uploaded again to be fixed." ) From 379e246dfbded083630d79622e3b8782b95a67de Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 11 May 2022 20:29:47 +0800 Subject: [PATCH 04/14] =?UTF-8?q?update=E4=BA=86=E4=B8=80=E4=BA=9Blog?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callbacks/checkpoint_callback.py | 7 ++++--- fastNLP/core/controllers/evaluator.py | 7 +++++-- .../core/drivers/paddle_driver/initialize_paddle_driver.py | 4 ++-- .../core/drivers/torch_driver/initialize_torch_driver.py | 4 ++-- fastNLP/transformers/torch/configuration_utils.py | 6 +++--- fastNLP/transformers/torch/modeling_utils.py | 4 ++-- fastNLP/transformers/torch/tokenization_utils_base.py | 4 ++-- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/fastNLP/core/callbacks/checkpoint_callback.py b/fastNLP/core/callbacks/checkpoint_callback.py index a18e61fa..625aea09 100644 --- a/fastNLP/core/callbacks/checkpoint_callback.py +++ b/fastNLP/core/callbacks/checkpoint_callback.py @@ -9,12 +9,13 @@ import sys from fastNLP.core.log import logger from .topk_saver import TopkSaver from .callback import Callback +from ..utils.exceptions import EarlyStopException class CheckpointCallback(Callback): def __init__(self, folder: Optional[Union[str, Path]] = None, every_n_epochs: Optional[int] = None, - every_n_batches: Optional[int] = None, last: bool = False, - on_exceptions: Optional[Union[BaseException, Sequence[BaseException]]] = None, topk: int = 0, + every_n_batches: Optional[int] = None, last: bool = False, topk: int = 0, + on_exceptions: Optional[Union[BaseException, Sequence[BaseException]]] = [EarlyStopException], monitor: Optional[Union[str, Callable]] = None, larger_better: bool = True, only_state_dict: bool = True, model_save_fn: Optional[Callable] = None, save_object: str = 'model', save_evaluate_results=True, **kwargs): @@ -49,7 +50,7 @@ class CheckpointCallback(Callback): :param every_n_batches: 多少个 batch 保存一次。 :param last: 如果为 True ,将在每次 epoch 运行结束都保存一次,会覆盖之前的保存。 :param topk: 保存 monitor 结果 topK 个。 - :param on_exceptions: 在出异常信息时,是否保存。传入需要捕获的异常的类。 + :param on_exceptions: 在出异常信息时,是否保存。传入需要捕获的异常的类。默认将捕获 EarlyStopException 。 :param larger_better: monitor 的值是否时越大越好。 :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 diff --git a/fastNLP/core/controllers/evaluator.py b/fastNLP/core/controllers/evaluator.py index fcee2460..8ac35ad2 100644 --- a/fastNLP/core/controllers/evaluator.py +++ b/fastNLP/core/controllers/evaluator.py @@ -23,7 +23,7 @@ class Evaluator: driver: Driver _evaluate_batch_loop: Loop - def __init__(self, model, dataloaders, metrics: Optional[Union[Dict, Metric]] = None, + def __init__(self, model, dataloaders, metrics: Optional[Dict] = None, driver: Union[str, Driver] = 'torch', device: Optional[Union[int, List[int], str]] = None, evaluate_batch_step_fn: Optional[callable] = None, evaluate_fn: Optional[str] = None, input_mapping: Optional[Union[Callable, Dict]] = None, @@ -388,5 +388,8 @@ class _MetricsWrapper: _results = metric.accumulate() else: raise RuntimeError(f"Not support `{type(metric)}` for now.") - results[metric_name] = _results + if _results is not None: + results[metric_name] = _results + else: + logger.warning_once(f"Metric:{metric_name} returns None when getting metric results.") return results diff --git a/fastNLP/core/drivers/paddle_driver/initialize_paddle_driver.py b/fastNLP/core/drivers/paddle_driver/initialize_paddle_driver.py index 22098ff2..552fc622 100644 --- a/fastNLP/core/drivers/paddle_driver/initialize_paddle_driver.py +++ b/fastNLP/core/drivers/paddle_driver/initialize_paddle_driver.py @@ -40,8 +40,8 @@ def initialize_paddle_driver(driver: str, device: Optional[Union[str, int, List[ if user_visible_devices is None: raise RuntimeError("To run paddle distributed training, please set `FASTNLP_BACKEND` to 'paddle' before using FastNLP.") if device is not None: - logger.warning_once("Parameter `device` would be ignored when you are using `paddle.distributed.launch` to pull " - "up your script. And we will directly get the local device via environment variables.") + logger.rank_zero_warning("Parameter `device` would be ignored when you are using `paddle.distributed.launch` to pull " + "up your script. And we will directly get the local device via environment variables.", once=True) _visible_list = user_visible_devices.split(",") device = [ f"gpu:{_visible_list.index(g) }" for g in os.environ["CUDA_VISIBLE_DEVICES"].split(",")] # TODO 目前一个进程仅对应一个卡,所以暂时传入单个 diff --git a/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py b/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py index f9fac83f..723765d2 100644 --- a/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py +++ b/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py @@ -26,9 +26,9 @@ def initialize_torch_driver(driver: str, device: Optional[Union[str, "torch.devi # world_size 和 rank if FASTNLP_BACKEND_LAUNCH in os.environ: if device is not None: - logger.warning_once("Parameter `device` would be ignored when you are using `torch.distributed.run` to pull " + logger.rank_zero_warning("Parameter `device` would be ignored when you are using `torch.distributed.run` to pull " "up your script. And we will directly get the local device via " - "`os.environ['LOCAL_RANK']`.") + "`os.environ['LOCAL_RANK']`.", once=True) return TorchDDPDriver(model, torch.device(f"cuda:{os.environ['LOCAL_RANK']}"), True, **kwargs) if driver not in {"torch", "fairscale"}: diff --git a/fastNLP/transformers/torch/configuration_utils.py b/fastNLP/transformers/torch/configuration_utils.py index fb494d9f..948d9873 100644 --- a/fastNLP/transformers/torch/configuration_utils.py +++ b/fastNLP/transformers/torch/configuration_utils.py @@ -564,9 +564,9 @@ class PretrainedConfig: raise EnvironmentError(msg) if resolved_config_file == config_file: - logger.info(f"loading configuration file {config_file}") + logger.debug(f"loading configuration file {config_file}") else: - logger.info(f"loading configuration file {config_file} from cache at {resolved_config_file}") + logger.debug(f"loading configuration file {config_file} from cache at {resolved_config_file}") return config_dict, kwargs @@ -603,7 +603,7 @@ class PretrainedConfig: for key in to_remove: kwargs.pop(key, None) - logger.info(f"Model config {config}") + logger.debug(f"Model config {config}") if return_unused_kwargs: return config, kwargs else: diff --git a/fastNLP/transformers/torch/modeling_utils.py b/fastNLP/transformers/torch/modeling_utils.py index d19816a3..74f370b6 100644 --- a/fastNLP/transformers/torch/modeling_utils.py +++ b/fastNLP/transformers/torch/modeling_utils.py @@ -1260,9 +1260,9 @@ class PreTrainedModel(Module, ModuleUtilsMixin, GenerationMixin): raise EnvironmentError(msg) if resolved_archive_file == archive_file: - logger.info(f"loading weights file {archive_file}") + logger.debug(f"loading weights file {archive_file}") else: - logger.info(f"loading weights file {archive_file} from cache at {resolved_archive_file}") + logger.debug(f"loading weights file {archive_file} from cache at {resolved_archive_file}") else: resolved_archive_file = None diff --git a/fastNLP/transformers/torch/tokenization_utils_base.py b/fastNLP/transformers/torch/tokenization_utils_base.py index ad62cd6e..8ed5a2e2 100644 --- a/fastNLP/transformers/torch/tokenization_utils_base.py +++ b/fastNLP/transformers/torch/tokenization_utils_base.py @@ -1700,9 +1700,9 @@ class PreTrainedTokenizerBase(SpecialTokensMixin): continue if file_path == resolved_vocab_files[file_id]: - logger.info(f"loading file {file_path}") + logger.debug(f"loading file {file_path}") else: - logger.info(f"loading file {file_path} from cache at {resolved_vocab_files[file_id]}") + logger.debug(f"loading file {file_path} from cache at {resolved_vocab_files[file_id]}") return cls._from_pretrained( resolved_vocab_files, From 854a25ab0036f210f0a26b674de48390274b2ede Mon Sep 17 00:00:00 2001 From: YWMditto Date: Wed, 11 May 2022 20:38:55 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BA=86=20Trainer=20=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/controllers/trainer.py | 717 +++++++++++------- .../core/drivers/torch_driver/torch_driver.py | 2 +- 2 files changed, 442 insertions(+), 277 deletions(-) diff --git a/fastNLP/core/controllers/trainer.py b/fastNLP/core/controllers/trainer.py index 7974cbab..930af27b 100644 --- a/fastNLP/core/controllers/trainer.py +++ b/fastNLP/core/controllers/trainer.py @@ -38,314 +38,318 @@ from fastNLP.core.utils.exceptions import EarlyStopException class Trainer(TrainerEventTrigger): - _custom_callbacks: dict = defaultdict(list) + r""" + 用于支持快速训练的训练器。 - def __init__( - self, - model, - driver, - train_dataloader, - optimizers, - device: Optional[Union[int, List[int], str]] = "cpu", - n_epochs: int = 20, - evaluate_dataloaders=None, - batch_step_fn: Optional[Callable] = None, - evaluate_batch_step_fn: Optional[Callable] = None, - train_fn: Optional[str] = None, - evaluate_fn: Optional[str] = None, - callbacks: Union[List[Callback], Callback, None] = None, - metrics: Optional[dict] = None, - evaluate_every: Optional[Union[int, Callable]] = -1, - input_mapping: Optional[Union[Callable, Dict]] = None, - output_mapping: Optional[Union[Callable, Dict]] = None, - model_wo_auto_param_call: bool = False, - accumulation_steps: int = 1, - fp16: bool = False, - monitor: Union[str, Callable] = None, - larger_better: bool = True, - marker: Optional[str] = None, - **kwargs - ): - r""" - :param model: 训练所需要的模型,例如 ``torch.nn.Module``; + :param model: 训练所需要的模型,例如 ``torch.nn.Module``; + + .. note:: + + 当使用 pytorch 时,注意参数 ``model`` 在大多数情况下为 ``nn.Module``。但是您仍能够通过使用一些特定的组合来使用情况,如下所示: + + 1. 当希望使用 ``DataParallel`` 时,您应当使用 ``TorchSingleDriver``,意味着您在初始化 ``Trainer`` 时参数 ``device`` 不应当为 + 一个 ``List``; + + 2. 当您选择自己初始化 ``init_process_group`` 时(这种情况要求您传入的 ``model`` 参数一定为 ``DistributedDataParallel``), + 您应当使用 ``TorchDDPDriver``,意味着您需要通过 ``python -m torch.distributed.launch`` 的方式来启动训练,此时参数 ``device`` + 应当设置为 None(此时我们会忽略该参数),具体见下面对于参数 ``device`` 的更详细的解释。 + + :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:["torch"],之后我们会加入 jittor、paddle 等 + 国产框架的训练模式;其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` + 的设置; + :param train_dataloader: 训练数据集,注意其必须是单独的一个数据集,不能是 List 或者 Dict; + :param optimizers: 训练所需要的优化器;可以是单独的一个优化器实例,也可以是多个优化器组成的 List; + :param device: 该参数用来指定具体训练时使用的机器;注意当该参数仅当您通过 `torch.distributed.launch/run` 启动时可以为 None, + 此时 fastNLP 不会对模型和数据进行设备之间的移动处理,但是你可以通过参数 `input_mapping` 和 `output_mapping` 来实现设备之间 + 数据迁移的工作(通过这两个参数传入两个处理数据的函数);同时你也可以通过在 kwargs 添加参数 "data_device" 来让我们帮助您将数据 + 迁移到指定的机器上(注意这种情况理应只出现在用户在 Trainer 实例化前自己构造 DDP 的场景); + + device 的可选输入如下所示: + + * *str*: 例如 'cpu', 'cuda', 'cuda:0', 'cuda:1' 等; + * *torch.device*: 例如 'torch.device("cuda:0")'; + * *int*: 将使用 ``device_id`` 为该值的 ``gpu`` 进行训练;如果值为 -1,那么默认使用全部的显卡,此时使用的 driver 实例是 `TorchDDPDriver`; + * *list(int)*: 如果多于 1 个device,应当通过该种方式进行设定;注意此时我们一定会使用 ``TorchDDPDriver``,不管您传入的列表的长度是 1 还是其它值; + * *None*: 仅当用户自己通过训练框架提供的并行训练启动脚本开启 ddp 进程时为 None; .. note:: - 当使用 pytorch 时,注意参数 ``model`` 在大多数情况下为 ``nn.Module``。但是您仍能够通过使用一些特定的组合来使用情况,如下所示: + 如果希望使用 ``TorchDDPDriver``,在初始化 ``Trainer`` 时您应当使用:: - 1. 当希望使用 ``DataParallel`` 时,您应当使用 ``TorchSingleDriver``,意味着您在初始化 ``Trainer`` 时参数 ``device`` 不应当为 - 一个 ``List``; + Trainer(driver="torch", device=[0, 1]) - 2. 当您选择自己初始化 ``init_process_group`` 时(这种情况要求您传入的 ``model`` 参数一定为 ``DistributedDataParallel``), - 您应当使用 ``TorchDDPDriver``,意味着您需要通过 ``python -m torch.distributed.launch`` 的方式来启动训练,此时参数 ``device`` - 应当设置为 None(此时我们会忽略该参数),具体见下面对于参数 ``device`` 的更详细的解释。 + 注意如果这时 ``device=[0]``,我们仍旧会使用 ``TorchDDPDriver``。 - :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:["torch"],之后我们会加入 jittor、paddle 等 - 国产框架的训练模式;其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` - 的设置; - :param train_dataloader: 训练数据集,注意其必须是单独的一个数据集,不能是 List 或者 Dict; - :param optimizers: 训练所需要的优化器;可以是单独的一个优化器实例,也可以是多个优化器组成的 List; - :param device: 该参数用来指定具体训练时使用的机器;注意当该参数仅当您通过 `torch.distributed.launch/run` 启动时可以为 None, - 此时 fastNLP 不会对模型和数据进行设备之间的移动处理,但是你可以通过参数 `input_mapping` 和 `output_mapping` 来实现设备之间 - 数据迁移的工作(通过这两个参数传入两个处理数据的函数);同时你也可以通过在 kwargs 添加参数 "data_device" 来让我们帮助您将数据 - 迁移到指定的机器上(注意这种情况理应只出现在用户在 Trainer 实例化前自己构造 DDP 的场景); + 如果希望使用 ``TorchSingleDriver``,则在初始化 ``Trainer`` 时您应当使用:: - device 的可选输入如下所示: + Trainer(driver="torch", device=0) - * *str*: 例如 'cpu', 'cuda', 'cuda:0', 'cuda:1' 等; - * *torch.device*: 例如 'torch.device("cuda:0")'; - * *int*: 将使用 ``device_id`` 为该值的 ``gpu`` 进行训练;如果值为 -1,那么默认使用全部的显卡,此时使用的 driver 实例是 `TorchDDPDriver`; - * *list(int)*: 如果多于 1 个device,应当通过该种方式进行设定;注意此时我们一定会使用 ``TorchDDPDriver``,不管您传入的列表的长度是 1 还是其它值; - * *None*: 仅当用户自己通过训练框架提供的并行训练启动脚本开启 ddp 进程时为 None; + .. warning:: - .. note:: + 注意参数 ``device`` 仅当您通过 pytorch 或者其它训练框架自身的并行训练启动脚本启动 ddp 训练时才允许为 ``None``! - 如果希望使用 ``TorchDDPDriver``,在初始化 ``Trainer`` 时您应当使用:: + 例如,当您使用:: - Trainer(driver="torch", device=[0, 1]) + python -m torch.distributed.launch --nproc_per_node 2 train.py - 注意如果这时 ``device=[0]``,我们仍旧会使用 ``TorchDDPDriver``。 + 来使用 ``TorchDDPDriver`` 时,此时参数 ``device`` 不再有效(不管您是否自己初始化 ``init_process_group``),我们将直接 + 通过 ``torch.device(f"cuda:{local_rank}")`` 来获取当前进程所使用的的具体的 gpu 设备。因此此时您需要使用 ``os.environ["CUDA_VISIBLE_DEVICES"]`` + 来指定要使用的具体的 gpu 设备。 - 如果希望使用 ``TorchSingleDriver``,则在初始化 ``Trainer`` 时您应当使用:: + 另一点需要注意的是,当您没有选择自己初始化 ``init_process_group`` 时,我们仍旧会帮助您把模型和数据迁移到当前进程所使用的 + 具体的 gpu 设备上。但是如果您选择自己在 ``Trainer`` 初始化前(意味着在 ``driver`` 的 ``setup`` 前)初始化 ``init_process_group``, + 那么对于模型的迁移应当完全由您自己来完成。此时对于数据的迁移,如果您在 ``Trainer`` 初始化时指定了参数 ``data_device``,那么 + 我们会将数据迁移到 ``data_device`` 上;如果其为 None,那么将数据迁移到正确的设备上应当由您自己来完成。 - Trainer(driver="torch", device=0) + 对于使用 ``TorchDDPDriver`` 的更多细节,请见 :class:`fastNLP.core.drivers.torch_driver.TorchDDPDriver`。 - .. warning:: + :param n_epochs: 训练总共的 epoch 的数量,默认为 20; + :param evaluate_dataloaders: 验证数据集,其可以是单独的一个数据集,也可以是多个数据集;当为多个数据集时,注意其必须是 Dict;默认 + 为 None; + :param batch_step_fn: 定制每次训练时前向运行一个 batch 的数据所执行的函数。该函数应接受两个参数为 ``trainer`` 和 ``batch``, + 不需要要返回值;更详细的使用位置和说明请见 :meth:`fastNLP.core.controllers.TrainBatchLoop.batch_step_fn`; + :param evaluate_batch_step_fn: 定制每次验证时前向运行一个 batch 的数据所执行的函数。该函数应接受的两个参数为 ``evaluator`` 和 ``batch``, + 不需要有返回值;可以参考 :meth:`fastNLP.core.controllers.EvaluateBatchLoop.batch_step_fn`; + :param train_fn: 用来控制 ``Trainer`` 在训练的前向传播过程中是调用模型的哪一个函数,例如是 ``train_step`` 还是 ``forward``; + 默认为 ``None``,如果该值是 ``None``,那么我们会默认使用 ``train_step`` 当做前向传播的函数,如果在模型的定义类中没有找到该方法, + 则使用模型默认的前向传播函数,例如对于 pytorch 来说就是 ``forward``。 - 注意参数 ``device`` 仅当您通过 pytorch 或者其它训练框架自身的并行训练启动脚本启动 ddp 训练时才允许为 ``None``! + .. note:: + 在 fastNLP 中,对于训练时使用的前向传播函数的查找逻辑如下所示: - 例如,当您使用:: + 1. 如果 ``train_fn`` 为 None,那么在 model 的类 Model 中寻找方法 ``Model.train_step``;如果没有找到,那么默认使用 ``Model.forward``; + 2. 如果 ``train_fn`` 为一个字符串,例如 'my_step_fn',那么我们首先会在 model 的类 Model 中寻找方法 ``Model.my_step_fn``, + 如果没有找到,那么会直接报错; - python -m torch.distributed.launch --nproc_per_node 2 train.py + :param evaluate_fn: 用来控制 ``Trainer`` 中内置的 ``Evaluator`` 在验证的前向传播过程中是调用模型的哪一个函数,应当为 ``None`` + 或者一个字符串;其使用方式和 train_fn 类似;具体可见 :class:`fastNLP.core.controllers.Evaluator`; + :param callbacks: 训练当中触发的 callback 类,该参数应当为一个列表,其中的每一个元素都应当继承 ``Callback`` 类;具体可见 + :class:`fastNLP.core.callbacks.Callback`; + :param metrics: 用于传给 ``Trainer`` 内部的 ``Evaluator`` 实例来进行训练过程中的验证。其应当为一个字典,其中 key 表示 monitor, + 例如 {"acc1": AccMetric(), "acc2": AccMetric()}; - 来使用 ``TorchDDPDriver`` 时,此时参数 ``device`` 不再有效(不管您是否自己初始化 ``init_process_group``),我们将直接 - 通过 ``torch.device(f"cuda:{local_rank}")`` 来获取当前进程所使用的的具体的 gpu 设备。因此此时您需要使用 ``os.environ["CUDA_VISIBLE_DEVICES"]`` - 来指定要使用的具体的 gpu 设备。 + 目前我们支持的 ``metric`` 的种类有以下几种: - 另一点需要注意的是,当您没有选择自己初始化 ``init_process_group`` 时,我们仍旧会帮助您把模型和数据迁移到当前进程所使用的 - 具体的 gpu 设备上。但是如果您选择自己在 ``Trainer`` 初始化前(意味着在 ``driver`` 的 ``setup`` 前)初始化 ``init_process_group``, - 那么对于模型的迁移应当完全由您自己来完成。此时对于数据的迁移,如果您在 ``Trainer`` 初始化时指定了参数 ``data_device``,那么 - 我们会将数据迁移到 ``data_device`` 上;如果其为 None,那么将数据迁移到正确的设备上应当由您自己来完成。 + 1. fastNLP 自己的 ``metric``:详见 :class:`fastNLP.core.metrics.Metric`; + 2. torchmetrics; + 3. allennlp.training.metrics; + 4. paddle.metric; - 对于使用 ``TorchDDPDriver`` 的更多细节,请见 :class:`fastNLP.core.drivers.torch_driver.TorchDDPDriver`。 + :param evaluate_every: 用来控制 ``Trainer`` 内部的 ``Evaluator`` 验证的频率,其可以为负数、正数或者函数: - :param n_epochs: 训练总共的 epoch 的数量,默认为 20; - :param evaluate_dataloaders: 验证数据集,其可以是单独的一个数据集,也可以是多个数据集;当为多个数据集时,注意其必须是 Dict;默认 - 为 None; - :param batch_step_fn: 定制每次训练时前向运行一个 batch 的数据所执行的函数。该函数应接受两个参数为 ``trainer`` 和 ``batch``, - 不需要要返回值;更详细的使用位置和说明请见 :meth:`fastNLP.core.controllers.TrainBatchLoop.batch_step_fn`; - :param evaluate_batch_step_fn: 定制每次验证时前向运行一个 batch 的数据所执行的函数。该函数应接受的两个参数为 ``evaluator`` 和 ``batch``, - 不需要有返回值;可以参考 :meth:`fastNLP.core.controllers.EvaluateBatchLoop.batch_step_fn`; - :param train_fn: 用来控制 ``Trainer`` 在训练的前向传播过程中是调用模型的哪一个函数,例如是 ``train_step`` 还是 ``forward``; - 默认为 ``None``,如果该值是 ``None``,那么我们会默认使用 ``train_step`` 当做前向传播的函数,如果在模型的定义类中没有找到该方法, - 则使用模型默认的前向传播函数,例如对于 pytorch 来说就是 ``forward``。 + 1. 为负数时表示每隔几个 ``epoch`` evaluate 一次; + 2. 为正数则表示每隔几个 ``batch`` evaluate 一次; + 3. 为函数时表示用户自己传入的用于控制 evaluate 的频率的函数,该函数的应该接受当前 trainer 对象作为参数,并 + 返回一个 bool 值,返回为 True 说明需要进行 evaluate ;将在每个 ``batch`` 结束后调用该函数判断是否需要 evaluate; - .. note:: - 在 fastNLP 中,对于训练时使用的前向传播函数的查找逻辑如下所示: + .. note:: - 1. 如果 ``train_fn`` 为 None,那么在 model 的类 Model 中寻找方法 ``Model.train_step``;如果没有找到,那么默认使用 ``Model.forward``; - 2. 如果 ``train_fn`` 为一个字符串,例如 'my_step_fn',那么我们首先会在 model 的类 Model 中寻找方法 ``Model.my_step_fn``, - 如果没有找到,那么会直接报错; + 如果参数 ``evaluate_every`` 为函数,其应当类似: - :param evaluate_fn: 用来控制 ``Trainer`` 中内置的 ``Evaluator`` 在验证的前向传播过程中是调用模型的哪一个函数,应当为 ``None`` - 或者一个字符串;其使用方式和 train_fn 类似;具体可见 :class:`fastNLP.core.controllers.Evaluator`; - :param callbacks: 训练当中触发的 callback 类,该参数应当为一个列表,其中的每一个元素都应当继承 ``Callback`` 类;具体可见 - :class:`fastNLP.core.callbacks.Callback`; - :param metrics: 用于传给 ``Trainer`` 内部的 ``Evaluator`` 实例来进行训练过程中的验证。其应当为一个字典,其中 key 表示 monitor, - 例如 {"acc1": AccMetric(), "acc2": AccMetric()}; + >>> def my_evaluate_every(trainer) -> bool: + ... if (trainer.global_forward_batches+1) % 1000 == 0: + ... return True + ... else: + ... return False - 目前我们支持的 ``metric`` 的种类有以下几种: + 该函数表示当每经过 1000 个 batch,``Trainer`` 中内置的 ``Evaluator`` 就会验证一次; - 1. fastNLP 自己的 ``metric``:详见 :class:`fastNLP.core.metrics.Metric`; - 2. torchmetrics; - 3. allennlp.training.metrics; - 4. paddle.metric; + 另一个需要注意的事情在于该函数会在每一次 batch 的结尾进行调用,当该函数返回 ``True`` 时,``Evaluator`` 才会进行验证; - :param evaluate_every: 用来控制 ``Trainer`` 内部的 ``Evaluator`` 验证的频率,其可以为负数、正数或者函数: + :param input_mapping: 应当为一个字典或者一个函数,表示在当前 step 拿到一个 batch 的训练数据后,应当做怎样的映射处理: - 1. 为负数时表示每隔几个 ``epoch`` evaluate 一次; - 2. 为正数则表示每隔几个 ``batch`` evaluate 一次; - 3. 为函数时表示用户自己传入的用于控制 evaluate 的频率的函数,该函数的应该接受当前 trainer 对象作为参数,并 - 返回一个 bool 值,返回为 True 说明需要进行 evaluate ;将在每个 ``batch`` 结束后调用该函数判断是否需要 evaluate; + 1. 如果 ``input_mapping`` 是一个字典: - .. note:: + 1. 如果此时 batch 也是一个 ``Dict``,那么我们会把 batch 中同样在 ``input_mapping`` 中的 key 修改为 ``input_mapping`` 的对应 ``key`` 的 ``value``; + 2. 如果此时 batch 是一个 ``dataclass``,那么我们会先将其转换为一个 ``Dict``,然后再进行上述转换; + 3. 如果此时 batch 此时是其它类型,那么我们将会直接报错; + 2. 如果 ``input_mapping`` 是一个函数,那么对于取出的 batch,我们将不会做任何处理,而是直接将其传入该函数里; - 如果参数 ``evaluate_every`` 为函数,其应当类似: + 注意该参数会被传进 ``Evaluator`` 中;因此你可以通过该参数来实现将训练数据 batch 移到对应机器上的工作(例如当参数 ``device`` 为 ``None`` 时); + 如果 ``Trainer`` 和 ``Evaluator`` 需要使用不同的 ``input_mapping``, 请使用 ``train_input_mapping`` 与 ``evaluate_input_mapping`` 分别进行设置。 - >>> def my_evaluate_every(trainer) -> bool: - ... if (trainer.global_forward_batches+1) % 1000 == 0: - ... return True - ... else: - ... return False + :param output_mapping: 应当为一个字典或者函数。作用和 ``input_mapping`` 类似,区别在于其用于转换输出: - 该函数表示当每经过 1000 个 batch,``Trainer`` 中内置的 ``Evaluator`` 就会验证一次; + 1. 如果 ``output_mapping`` 是一个 ``Dict``,那么我们需要模型的输出必须是 ``Dict`` 或者 ``dataclass`` 类型: - 另一个需要注意的事情在于该函数会在每一次 batch 的结尾进行调用,当该函数返回 ``True`` 时,``Evaluator`` 才会进行验证; + 1. 如果此时模型的输出是一个 ``Dict``,那么我们会把输出中同样在 ``output_mapping`` 中的 key 修改为 ``output_mapping`` 的对应 key 的 value; + 2. 如果此时模型的输出是一个 ``dataclass``,那么我们会先将其转换为一个 Dict,然后再进行上述转换; + 2. 如果 ``output_mapping`` 是一个函数,那么我们将会直接将模型的输出传给该函数; - :param input_mapping: 应当为一个字典或者一个函数,表示在当前 step 拿到一个 batch 的训练数据后,应当做怎样的映射处理: + 如果 ``Trainer`` 和 ``Evaluator`` 需要使用不同的 ``output_mapping``, 请使用 ``train_output_mapping`` 与 ``evaluate_output_mapping`` 分别进行设置; - 1. 如果 ``input_mapping`` 是一个字典: + .. note:: - 1. 如果此时 batch 也是一个 ``Dict``,那么我们会把 batch 中同样在 ``input_mapping`` 中的 key 修改为 ``input_mapping`` 的对应 ``key`` 的 ``value``; - 2. 如果此时 batch 是一个 ``dataclass``,那么我们会先将其转换为一个 ``Dict``,然后再进行上述转换; - 3. 如果此时 batch 此时是其它类型,那么我们将会直接报错; - 2. 如果 ``input_mapping`` 是一个函数,那么对于取出的 batch,我们将不会做任何处理,而是直接将其传入该函数里; + ``input_mapping`` 和 ``output_mapping`` 与 fastNLP 的一个特殊的概念 **'参数绑定'** 高度相关,它们的存在也是为了 fastNLP + 中的参数匹配能够正确地运行; - 注意该参数会被传进 ``Evaluator`` 中;因此你可以通过该参数来实现将训练数据 batch 移到对应机器上的工作(例如当参数 ``device`` 为 ``None`` 时); - 如果 ``Trainer`` 和 ``Evaluator`` 需要使用不同的 ``input_mapping``, 请使用 ``train_input_mapping`` 与 ``evaluate_input_mapping`` 分别进行设置。 + .. todo:: + 之后链接上 参数匹配 的文档; - :param output_mapping: 应当为一个字典或者函数。作用和 ``input_mapping`` 类似,区别在于其用于转换输出: + .. warning:: - 1. 如果 ``output_mapping`` 是一个 ``Dict``,那么我们需要模型的输出必须是 ``Dict`` 或者 ``dataclass`` 类型: + 如果 ``Trainer`` 的参数 ``output_mapping`` 不为 ``None``,请保证其返回的一定是一个字典,并且其中含有关键字 **'loss'**; - 1. 如果此时模型的输出是一个 ``Dict``,那么我们会把输出中同样在 ``output_mapping`` 中的 key 修改为 ``output_mapping`` 的对应 key 的 value; - 2. 如果此时模型的输出是一个 ``dataclass``,那么我们会先将其转换为一个 Dict,然后再进行上述转换; - 2. 如果 ``output_mapping`` 是一个函数,那么我们将会直接将模型的输出传给该函数; + :param model_wo_auto_param_call: 是否关闭在训练时调用我们的 ``auto_param_call`` 函数来自动匹配 batch 和前向函数的参数的行为; - 如果 ``Trainer`` 和 ``Evaluator`` 需要使用不同的 ``output_mapping``, 请使用 ``train_output_mapping`` 与 ``evaluate_output_mapping`` 分别进行设置; + 1. 如果该值为 ``False``,并且当 batch 为字典时,我们会根据**前向函数**所需要的参数从 batch 中提取对应的对象,然后传入到**前向函数**中; + 2. 如果该值为 ``True``,那么我们会将 batch 直接透传给模型; - .. note:: + .. todo:: + 之后链接上 参数匹配 的文档; - ``input_mapping`` 和 ``output_mapping`` 与 fastNLP 的一个特殊的概念 **'参数绑定'** 高度相关,它们的存在也是为了 fastNLP - 中的参数匹配能够正确地运行; + 函数 ``auto_param_call`` 详见 :func:`fastNLP.core.utils.auto_param_call`; - .. todo:: - 之后链接上 参数匹配 的文档; + :param accumulation_steps: 梯度累积的步数,表示每隔几个 batch 才让优化器迭代一次,默认为 1; + :param fp16: 是否开启混合精度训练,默认为 False; + :param monitor: 对于一些特殊的 ``Callback``,例如 :class:`fastNLP.core.callbacks.CheckpointCallback`,它们需要参数 ``monitor`` + 来从 ``Evaluator`` 的验证结果中获取当前评测的值,从而来判断是否执行一些特殊的操作。例如,对于 ``CheckpointCallback`` 而言,如果我们 + 想要每隔一个 epoch 让 ``Evaluator`` 进行一次验证,然后保存训练以来的最好的结果;那么我们需要这样设置: - .. warning:: + .. code-block:: - 如果 ``Trainer`` 的参数 ``output_mapping`` 不为 ``None``,请保证其返回的一定是一个字典,并且其中含有关键字 **'loss'**; + trainer = Trainer( + ..., + metrics={'acc': accMetric()}, + callbacks=[CheckpointCallback( + ..., + monitor='acc', + topk=1 + )] + ) - :param model_wo_auto_param_call: 是否关闭在训练时调用我们的 ``auto_param_call`` 函数来自动匹配 batch 和前向函数的参数的行为; + 这意味着对于 ``CheckpointCallback`` 来说,*'acc'* 就是一个监测的指标,用于在 ``Evaluator`` 验证后取出其需要监测的那个指标的值。 - 1. 如果该值为 ``False``,并且当 batch 为字典时,我们会根据**前向函数**所需要的参数从 batch 中提取对应的对象,然后传入到**前向函数**中; - 2. 如果该值为 ``True``,那么我们会将 batch 直接透传给模型; + ``Trainer`` 中的参数 ``monitor`` 的作用在于为没有设置 ``monitor`` 参数但是需要该参数的 *callback* 实例设置该值。关于 ``monitor`` + 参数更详细的说明,请见 :class:`fastNLP.core.callbacks.CheckpointCallback`; - .. todo:: - 之后链接上 参数匹配 的文档; + 注意该参数仅当 ``Trainer`` 内置的 ``Evaluator`` 不为 None 时且有需要该参数但是没有设置该参数的 *callback* 实例才有效; + + :param larger_better: 对于需要参数 ``monitor`` 的 *callback* 来说,``monitor`` 的值是否是越大越好;类似于 ``monitor``,其作用 + 在于为没有设置 ``larger_better`` 参数但是需要该参数的 *callback* 实例设置该值; - 函数 ``auto_param_call`` 详见 :func:`fastNLP.core.utils.auto_param_call`; + 注意该参数仅当 ``Trainer`` 内置的 ``Evaluator`` 不为 None 时且有需要该参数但是没有设置该参数的 *callback* 实例才有效; - :param accumulation_steps: 梯度累积的步数,表示每隔几个 batch 才让优化器迭代一次,默认为 1; - :param fp16: 是否开启混合精度训练,默认为 False; - :param monitor: 对于一些特殊的 ``Callback``,例如 :class:`fastNLP.core.callbacks.CheckpointCallback`,它们需要参数 ``monitor`` - 来从 ``Evaluator`` 的验证结果中获取当前评测的值,从而来判断是否执行一些特殊的操作。例如,对于 ``CheckpointCallback`` 而言,如果我们 - 想要每隔一个 epoch 让 ``Evaluator`` 进行一次验证,然后保存训练以来的最好的结果;那么我们需要这样设置: + :param marker: 用于标记一个 ``Trainer`` 实例,从而在用户调用 ``Trainer.on`` 函数时,标记该函数属于哪一个具体的 ``Trainer`` 实例;默认为 None; + + .. note:: + + marker 的使用场景主要在于如果一个脚本中含有多个 ``Trainer`` 实例,并且含有多个使用 ``Trainer.on`` 修饰的函数时,不同的函数属于 + 不同的 ``Trainer`` 实例; + + 此时,通过将修饰器 ``Trainer.on`` 的参数 ``marker`` 和 ``Trainer`` 的参数 ``marker`` 置为相同,就可以使得该函数只会在这一 + ``Trainer`` 实例中被调用;例如, .. code-block:: + @Trainer.on(Event.on_train_begin(), marker='trainer1') + def fn(trainer): + ... + trainer = Trainer( ..., - metrics={'acc': accMetric()}, - callbacks=[CheckpointCallback( - ..., - monitor='acc', - topk=1 - )] + marker='trainer1' ) - 这意味着对于 ``CheckpointCallback`` 来说,*'acc'* 就是一个监测的指标,用于在 ``Evaluator`` 验证后取出其需要监测的那个指标的值。 - - ``Trainer`` 中的参数 ``monitor`` 的作用在于为没有设置 ``monitor`` 参数但是需要该参数的 *callback* 实例设置该值。关于 ``monitor`` - 参数更详细的说明,请见 :class:`fastNLP.core.callbacks.CheckpointCallback`; + 另一点需要说明的是,如果一个被 ``Trainer.on`` 修饰的函数,其修饰时没有指明 ``marker``,那么会将该函数传给代码位于其之后的 + 第一个 ``Trainer`` 实例,即使该 ``Trainer`` 实例的 marker 不为 None;这一点详见 :meth:`~fastNLP.core.controllers.Trainer.on` - 注意该参数仅当 ``Trainer`` 内置的 ``Evaluator`` 不为 None 时且有需要该参数但是没有设置该参数的 *callback* 实例才有效; + :kwargs: + * *torch_kwargs* -- 用于在指定 ``driver`` 为 'torch' 时设定具体 driver 实例的一些参数: - :param larger_better: 对于需要参数 ``monitor`` 的 *callback* 来说,``monitor`` 的值是否是越大越好;类似于 ``monitor``,其作用 - 在于为没有设置 ``larger_better`` 参数但是需要该参数的 *callback* 实例设置该值; + * ddp_kwargs -- 用于在使用 ``TorchDDPDriver`` 时指定 ``DistributedDataParallel`` 初始化时的参数;例如传入 + {'find_unused_parameters': True} 来解决有参数不参与前向运算导致的报错等; + * set_grad_to_none -- 是否在训练过程中在每一次 optimizer 更新后将 grad 置为 None; + * torch_non_blocking -- 表示用于 pytorch 的 tensor 的 to 方法的参数 non_blocking; + * *paddle_kwargs* -- 用于在指定 ``driver`` 为 'paddle' 时设定具体 driver 实例的一些参数: - 注意该参数仅当 ``Trainer`` 内置的 ``Evaluator`` 不为 None 时且有需要该参数但是没有设置该参数的 *callback* 实例才有效; + * fleet_kwargs -- 用于在使用 ``PaddleFleetDriver`` 时指定 ``DataParallel`` 和 ``fleet`` 初始化时的参数,包括: - :param marker: 用于标记一个 ``Trainer`` 实例,从而在用户调用 ``Trainer.on`` 函数时,标记该函数属于哪一个具体的 ``Trainer`` 实例;默认为 None; + * is_collective -- 是否使用 paddle 集群式的分布式训练方法,目前仅支持为 True 的情况; + * role_maker -- 初始化 ``fleet`` 分布式训练 API 时使用的 ``RoleMaker`` + * 其它用于初始化 ``DataParallel`` 的参数; + * *data_device* -- 一个具体的 driver 实例中,有 ``model_device`` 和 ``data_device``,前者表示模型所在的设备,后者表示 + 当 ``model_device`` 为 None 时应当将数据迁移到哪个设备; .. note:: - marker 的使用场景主要在于如果一个脚本中含有多个 ``Trainer`` 实例,并且含有多个使用 ``Trainer.on`` 修饰的函数时,不同的函数属于 - 不同的 ``Trainer`` 实例; - - 此时,通过将修饰器 ``Trainer.on`` 的参数 ``marker`` 和 ``Trainer`` 的参数 ``marker`` 置为相同,就可以使得该函数只会在这一 - ``Trainer`` 实例中被调用;例如, - - .. code-block:: - - @Trainer.on(Event.on_train_begin(), marker='trainer1') - def fn(trainer): - ... - - trainer = Trainer( - ..., - marker='trainer1' - ) - - 另一点需要说明的是,如果一个被 ``Trainer.on`` 修饰的函数,其修饰时没有指明 ``marker``,那么会将该函数传给代码位于其之后的 - 第一个 ``Trainer`` 实例,即使该 ``Trainer`` 实例的 marker 不为 None;这一点详见 :meth:`~fastNLP.core.controllers.Trainer.on` - - :kwargs: - * *torch_kwargs* -- 用于在指定 ``driver`` 为 'torch' 时设定具体 driver 实例的一些参数: - - * ddp_kwargs -- 用于在使用 ``TorchDDPDriver`` 时指定 ``DistributedDataParallel`` 初始化时的参数;例如传入 - {'find_unused_parameters': True} 来解决有参数不参与前向运算导致的报错等; - * set_grad_to_none -- 是否在训练过程中在每一次 optimizer 更新后将 grad 置为 None; - * torch_non_blocking -- 表示用于 pytorch 的 tensor 的 to 方法的参数 non_blocking; - * *paddle_kwargs* -- 用于在指定 ``driver`` 为 'paddle' 时设定具体 driver 实例的一些参数: - - * fleet_kwargs -- 用于在使用 ``PaddleFleetDriver`` 时指定 ``DataParallel`` 和 ``fleet`` 初始化时的参数,包括: - - * is_collective -- 是否使用 paddle 集群式的分布式训练方法,目前仅支持为 True 的情况; - * role_maker -- 初始化 ``fleet`` 分布式训练 API 时使用的 ``RoleMaker`` - * 其它用于初始化 ``DataParallel`` 的参数; - * *data_device* -- 一个具体的 driver 实例中,有 ``model_device`` 和 ``data_device``,前者表示模型所在的设备,后者表示 - 当 ``model_device`` 为 None 时应当将数据迁移到哪个设备; - - .. note:: - - 注意您在绝大部分情况下不会用到该参数! - - 1. 当 driver 实例的 ``model_device`` 不为 None 时,该参数无效; - 2. 对于 pytorch,仅当用户自己通过 ``python -m torch.distributed.launch`` 并且自己初始化 ``init_process_group`` 时, - driver 实例的 ``model_device`` 才会为 None; - 3. 对于 paddle,该参数无效; - - * *use_dist_sampler* -- 表示是否使用分布式的 ``sampler``。在多卡时,分布式 ``sampler`` 将自动决定每张卡上读取的 sample ,使得一个 epoch - 内所有卡的 sample 加起来为一整个数据集的 sample。默认会根据 driver 是否为分布式进行设置。 - * *evaluate_use_dist_sampler* -- 表示在 ``Evaluator`` 中在使用分布式的时候是否将 dataloader 的 ``sampler`` 替换为分布式的 ``sampler``;默认为 ``True``; - * *output_from_new_proc* -- 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: - ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 - log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; - - 注意该参数仅当使用分布式的 ``driver`` 时才有效,例如 ``TorchDDPDriver``; - * *progress_bar* -- 以哪种方式显示 progress ,目前支持[None, 'raw', 'rich', 'auto'] 或者 RichCallback, RawTextCallback对象, - 默认为 auto , auto 表示如果检测到当前 terminal 为交互型则使用 RichCallback,否则使用 RawTextCallback对象。如果 - 需要定制 progress bar 的参数,例如打印频率等,可以传入 RichCallback, RawTextCallback 对象。 - * *train_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Trainer`` 中。与 input_mapping 互斥。 - * *train_output_mapping* -- 与 output_mapping 一致,但是只用于 ``Trainer`` 中。与 output_mapping 互斥。 - * *evaluate_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Evaluator`` 中。与 input_mapping 互斥。 - * *evaluate_output_mapping* -- 与 output_mapping 一致,但是只用于 ``Evaluator`` 中。与 output_mapping 互斥。 + 注意您在绝大部分情况下不会用到该参数! - .. note:: - ``Trainer`` 是通过在内部直接初始化一个 ``Evaluator`` 来进行验证; - ``Trainer`` 内部的 ``Evaluator`` 默认是 None,如果您需要在训练过程中进行验证,你需要保证这几个参数得到正确的传入: + 1. 当 driver 实例的 ``model_device`` 不为 None 时,该参数无效; + 2. 对于 pytorch,仅当用户自己通过 ``python -m torch.distributed.launch`` 并且自己初始化 ``init_process_group`` 时, + driver 实例的 ``model_device`` 才会为 None; + 3. 对于 paddle,该参数无效; - 必须的参数:1. ``metrics``;2. ``evaluate_dataloaders``; + * *use_dist_sampler* -- 表示是否使用分布式的 ``sampler``。在多卡时,分布式 ``sampler`` 将自动决定每张卡上读取的 sample ,使得一个 epoch + 内所有卡的 sample 加起来为一整个数据集的 sample。默认会根据 driver 是否为分布式进行设置。 + * *evaluate_use_dist_sampler* -- 表示在 ``Evaluator`` 中在使用分布式的时候是否将 dataloader 的 ``sampler`` 替换为分布式的 ``sampler``;默认为 ``True``; + * *output_from_new_proc* -- 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: + ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 + log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; - 可选的其它参数:1. ``evaluate_batch_step_fn;2. ``evaluate_fn``;3. ``evaluate_every``;4. ``input_mapping``; - 5. ``output_mapping``; 6. ``model_wo_auto_param_call``;7. ``fp16``;8. ``monitor``;9. ``larger_better``; + 注意该参数仅当使用分布式的 ``driver`` 时才有效,例如 ``TorchDDPDriver``; + * *progress_bar* -- 以哪种方式显示 progress ,目前支持[None, 'raw', 'rich', 'auto'] 或者 RichCallback, RawTextCallback对象, + 默认为 auto , auto 表示如果检测到当前 terminal 为交互型则使用 RichCallback,否则使用 RawTextCallback对象。如果 + 需要定制 progress bar 的参数,例如打印频率等,可以传入 RichCallback, RawTextCallback 对象。 + * *train_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Trainer`` 中。与 input_mapping 互斥。 + * *train_output_mapping* -- 与 output_mapping 一致,但是只用于 ``Trainer`` 中。与 output_mapping 互斥。 + * *evaluate_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Evaluator`` 中。与 input_mapping 互斥。 + * *evaluate_output_mapping* -- 与 output_mapping 一致,但是只用于 ``Evaluator`` 中。与 output_mapping 互斥。 - .. warning:: + .. note:: + ``Trainer`` 是通过在内部直接初始化一个 ``Evaluator`` 来进行验证; + ``Trainer`` 内部的 ``Evaluator`` 默认是 None,如果您需要在训练过程中进行验证,你需要保证这几个参数得到正确的传入: - 如果 ``Trainer`` 中内置的 ``Evaluator`` 实例不为 ``None``,那么需要注意 ``Trainer`` 中的一些参数是与 ``Evaluator`` 一致的,它们分别为: + 必须的参数:1. ``metrics``;2. ``evaluate_dataloaders``; - 1. ``Evaluator`` 在初始化时的 ``driver`` 参数是 ``Trainer`` 中已经实例化过的 driver;这一点使得一些参数对于 ``Trainer`` 内部的 - ``Evaluator`` 没有用处,例如 ``device``,``torch_kwargs``,``data_device`` 和 ``output_from_new_proc`` 等; - 2. ``input_mapping``,``output_mapping``,``model_wo_auto_param_call`` 和 ``fp16`` 是 ``Trainer`` 和其内部默认的 - ``Evaluator`` 是一致的; + 可选的其它参数:1. ``evaluate_batch_step_fn;2. ``evaluate_fn``;3. ``evaluate_every``;4. ``input_mapping``; + 5. ``output_mapping``; 6. ``model_wo_auto_param_call``;7. ``fp16``;8. ``monitor``;9. ``larger_better``; - 当然,对于 ``input_mapping`` 和 ``output_mapping``,您可以通过添加 ``kwargs`` 中的参数 ``evaluate_input_mapping`` 和 - ``evaluate_output_mapping`` 来单独为 ``Evaluator`` 进行更细致的订制。 + .. warning:: - 另一方面,注意一些专门独属于 ``Evaluator`` 的参数仅当 ``Evaluator`` 不为 None 时才会生效。 + 如果 ``Trainer`` 中内置的 ``Evaluator`` 实例不为 ``None``,那么需要注意 ``Trainer`` 中的一些参数是与 ``Evaluator`` 一致的,它们分别为: + + 1. ``Evaluator`` 在初始化时的 ``driver`` 参数是 ``Trainer`` 中已经实例化过的 driver;这一点使得一些参数对于 ``Trainer`` 内部的 + ``Evaluator`` 没有用处,例如 ``device``,``torch_kwargs``,``data_device`` 和 ``output_from_new_proc`` 等; + 2. ``input_mapping``,``output_mapping``,``model_wo_auto_param_call`` 和 ``fp16`` 是 ``Trainer`` 和其内部默认的 + ``Evaluator`` 是一致的; + + 当然,对于 ``input_mapping`` 和 ``output_mapping``,您可以通过添加 ``kwargs`` 中的参数 ``evaluate_input_mapping`` 和 + ``evaluate_output_mapping`` 来单独为 ``Evaluator`` 进行更细致的订制。 + + 另一方面,注意一些专门独属于 ``Evaluator`` 的参数仅当 ``Evaluator`` 不为 None 时才会生效。 + + """ + + _custom_callbacks: dict = defaultdict(list) + + def __init__( + self, + model, + driver, + train_dataloader, + optimizers, + device: Optional[Union[int, List[int], str]] = "cpu", + n_epochs: int = 20, + evaluate_dataloaders=None, + batch_step_fn: Optional[Callable] = None, + evaluate_batch_step_fn: Optional[Callable] = None, + train_fn: Optional[str] = None, + evaluate_fn: Optional[str] = None, + callbacks: Union[List[Callback], Callback, None] = None, + metrics: Optional[dict] = None, + evaluate_every: Optional[Union[int, Callable]] = -1, + input_mapping: Optional[Union[Callable, Dict]] = None, + output_mapping: Optional[Union[Callable, Dict]] = None, + model_wo_auto_param_call: bool = False, + accumulation_steps: int = 1, + fp16: bool = False, + monitor: Union[str, Callable] = None, + larger_better: bool = True, + marker: Optional[str] = None, + **kwargs + ): - """ self.model = model self.marker = marker if isinstance(driver, str): @@ -825,8 +829,10 @@ class Trainer(TrainerEventTrigger): def _fetch_matched_fn_callbacks(self): r""" - 因为对于使用装饰器加入的函数 callback,我们是加在类属性中,因此在初始化一个具体的 trainer 实例后,我们需要从 Trainer 的 - callback 类属性中将属于其的 callback 函数拿到,然后加入到 callback_manager 中; + 因为对于使用装饰器加入的函数 callback,我们是加在类属性 ``_custom_callbacks`` 中,因此在初始化一个具体的 trainer 实例后,我们需要从 Trainer 的 + callback 类属性中将属于其的 callback 函数拿到,然后加入到 ``callback_manager`` 中; + + 这里的主要需要注意的地方在于为了支持没有带 ``marker`` 的 callback 函数赋给下方代码距离其最近的 trainer,在每次收集到 self._custom_callbacks[None] 后将其置为 []; """ _own_callbacks: List = copy.deepcopy(self._custom_callbacks["all"]) _own_callbacks.extend(self._custom_callbacks[None]) @@ -841,12 +847,24 @@ class Trainer(TrainerEventTrigger): self.add_callback_fn(*each_callback) def _check_callback_called_legality(self, check_mode: bool = True): - """ - 1. 函数的调用时机: + r""" + 这个函数主要的作用在于: + + 如果用户定制了训练流程中的一部分,例如 ``batch_step_fn`` 或者 ``TrainBatchLoop``;并且这些部分流程中可能会包含一些 callback + 函数的调用;例如 ``train_batch_loop.batch_step_fn`` 中包含 ``on_before_backward`` 等; + + 用户是十分可能忘记在其自己定制的部分流程中实现对这些 callback 函数的调用的;因此需要我们进行检测和提醒; + + 这种检测也十分简单,即如果我们检测到 callback_manager 的某一 callback 函数在训练一段时间(通常是涉及到允许定制的部分流程的结尾)后, + 其被调用的次数是 0,那么我们就会打印 ``warning`` 信息; + + 1. 这个函数的调用时机(这个函数会在以下情况被调用): + 当检测 'batch_step_fn' 时,这个函数应当在 'train_batch_loop.run' 的 while 循环的最后进行调用; 当检测 'TrainBatchLoop' 时,这个函数应当在每一个 epoch 的最后进行调用; - 2. 函数作用 + 2. 这个函数作用的更细致的解释: + 这一函数的作用在于检查用户定制的 batch_step_fn / TrainBatchLoop 是否能够正确地调用 callback 函数,更准确地说,当用户实际 定制了 ("on_before_backward", "on_after_backward", "on_before_optimizers_step", "on_after_optimizers_step", "on_before_zero_grad", "on_after_zero_grad") / @@ -910,6 +928,9 @@ class Trainer(TrainerEventTrigger): """ Trainer 需要的一些 property """ @property def driver(self): + """ + :return: 返回 ``trainer`` 中的 ``driver`` 实例; + """ return self._driver @driver.setter @@ -918,6 +939,9 @@ class Trainer(TrainerEventTrigger): @property def train_batch_loop(self): + """ + :return: 返回 ``trainer`` 中的 ``train_batch_loop`` 实例; + """ return self._train_batch_loop @train_batch_loop.setter @@ -933,12 +957,24 @@ class Trainer(TrainerEventTrigger): def save_model(self, folder: Union[str, os.PathLike, BinaryIO, io.BytesIO], only_state_dict: bool = False, model_save_fn: Optional[Callable] = None, **kwargs): r""" - 用于帮助用户保存模型的辅助函数,具体实际的保存模型的操作由具体的 driver 实现; + 用于帮助您保存模型的辅助函数; + + :param folder: 保存模型的文件夹。如果没有传入 model_save_fn 参数,则我们会在这个文件夹下保存 fastnlp_model.pkl.tar 文件; + :param only_state_dict: 仅在 model_save_fn 为空时,有效。是否只保存模型的 ``state_dict``; + :param model_save_fn: 您自己定制的用来替换该保存函数本身保存逻辑的函数,当您传入了该参数后,我们会实际调用该函数,而不会去调用 ``driver`` 的 ``save_model`` 函数; + :param kwargs: 理论上您不需要使用到该参数; + + .. note:: - :param folder: 保存模型的文件夹。如果没有传入 model_save_fn 参数,则在这个文件夹下创建 fastnlp_model.pkl.tar 文件。 - :param only_state_dict: 仅在 model_save_fn 为空时,有效。是否只保存模型的 `state_dict`; - :param model_save_fn: 用户自己定制的用来替换该保存函数本身保存逻辑的函数; - :param kwargs: + 注意如果您需要在训练的过程中保存模型,如果没有特别复杂的逻辑,强烈您使用我们专门为保存模型以及断点重训功能定制的 ``callback``:**``CheckpointCallback``**; + ``CheckpointCallback`` 的使用具体见 :class:`fastNLP.core.callbacks.checkpoint_callback.CheckpointCallback`; + + 这意味着在大多数时刻您并不需要自己主动地调用该函数来保存模型;当然您可以在自己定制的 callback 类中通过直接调用 ``trainer.save_model`` 来保存模型; + + 具体实际的保存模型的操作由具体的 driver 实现,这意味着对于不同的 ``Driver`` 来说,保存模型的操作可能是不尽相同的, + 您如果想要了解更多的保存模型的细节,请直接查看各个 ``Driver`` 的 ``save_model`` 函数; + + ``save_model`` 函数和 ``load_model`` 函数是配套使用的; """ self.on_save_model() @@ -963,14 +999,22 @@ class Trainer(TrainerEventTrigger): def load_model(self, folder: Union[str, Path, BinaryIO, io.BytesIO], only_state_dict: bool = True, model_load_fn: Optional[Callable] = None, **kwargs): """ - 加载模型 - - :param folder: 读取 model 的文件夹,默认会尝试读取该文件夹下的 fastnlp_model.pkl.tar 文件。在 model_load_fn 不为空时, - 直接将该 folder 传递到 model_load_fn 中。 - :param only_state_dict: 要读取的文件中是否仅包含模型权重。在 model_load_fn 不为 None 时,该参数无意义。 - :param model_load_fn: callable 的函数,接受一个 folder 作为参数,不返回任何内容。 - :param kwargs: - :return: + 用于帮助您加载模型的辅助函数; + + :param folder: 存放着您需要加载的 model 的文件夹,默认会尝试读取该文件夹下的 fastnlp_model.pkl.tar 文件。在 model_load_fn 不为空时, + 直接将该 folder 传递到 model_load_fn 中; + :param only_state_dict: 要读取的文件中是否仅包含模型权重。在 ``model_load_fn 不为 None`` 时,该参数无意义; + :param model_load_fn: ``callable`` 的函数,接受一个 folder 作为参数,需要注意该函数不需要返回任何内容; + :param kwargs: 理论上您不需要使用到该参数; + + .. note:: + + 注意您需要在初始化 ``Trainer`` 后再通过 ``trainer`` 实例来调用该函数;这意味着您需要保证在保存和加载时使用的 ``driver`` 是属于同一个 + 训练框架的,例如都是 ``pytorch`` 或者 ``paddle``; + + 注意在大多数情况下您不需要使用该函数,如果您需要断点重训功能,您可以直接使用 ``trainer.load`` 函数; + + 该函数在通常情况下和 ``save_model`` 函数配套使用;其参数均与 ``save_model`` 函数成对应关系; """ self.on_load_model() self.driver.barrier() @@ -997,24 +1041,62 @@ class Trainer(TrainerEventTrigger): def save(self, folder: Union[str, Path], only_state_dict: bool = True, model_save_fn: Optional[Callable] = None, **kwargs): r""" - 用于断点重训 Trainer 的保存函数。 + 用于帮助您实现断点重训功能的保存函数; :param folder: 保存在哪个文件夹下,会在该文件下声称两个文件:fastnlp_checkpoint.pkl.tar 与 fastnlp_model.pkl.tar 。 - 如果 model_save_fn 不为空,则没有 fastnlp_model.pkl.tar 文件。 - :param only_state_dict: 当 model_save_fn 为空时有效,表明是否仅保存模型的权重。 - :param model_save_fn: 如果模型保存比较特殊,可以传入该函数自定义保存过程,输入应该接受一个文件夹(实际上就是接受上面的 folder - 参数),不必返回任何东西。 - :param kwargs: - :return: + 如果 model_save_fn 不为空,则没有 fastnlp_model.pkl.tar 文件; + :param only_state_dict: 当 model_save_fn 为空时有效,表明是否仅保存模型的权重; + :param model_save_fn: 如果模型保存比较特殊,可以传入该函数自定义模型的保存过程,输入应该接受一个文件夹(实际上就是接受上面的 folder + 参数),不需要返回值;这意味着您可以通过该函数来自己负责模型的保存过程,而我们则会将 ``trainer`` 的状态保存好; + :param kwargs: 理论上您不需要使用到该参数; + + .. note:: + + 注意如果您需要在训练的过程中使用断点重训功能,您可以直接使用 **``CheckpointCallback``**; + ``CheckpointCallback`` 的使用具体见 :class:`fastNLP.core.callbacks.checkpoint_callback.CheckpointCallback`; + + 这意味着在大多数时刻您并不需要自己主动地调用该函数来保存 ``Trainer`` 的状态;当然您可以在自己定制的 callback 类中通过直接调用 ``trainer.save`` 来保存 ``Trainer`` 的状态; + + 具体实际的保存状态的操作由具体的 driver 实现,这意味着对于不同的 ``Driver`` 来说,保存的操作可能是不尽相同的, + 您如果想要了解保存 ``Trainer`` 状态的更多细节,请直接查看各个 ``Driver`` 的 ``save`` 函数; + + ``save`` 函数和 ``load`` 函数是配套使用的; + + .. note:: + + 为了支持断点重训功能,我们会在调用该函数时保存以下内容: + + 1. 各个 ``callback`` 的状态,这主要涉及到一些带有运行状态的 ``callback``; + 2. 控制训练流程的变量 ``trainer_state``,具体详见 :class:`fastNLP.core.controllers.utils.states.TrainerState`; + 3. 一个特殊的变量 ``num_consumed_batches``,表示在这次训练过程中总共训练了多少个 batch 的数据;您不需要关心这个变量; + 4. sampler 的状态,为了支持断点重训功能,我们会在 trainer 初始化的时候,将您的 ``trainer_dataloader`` 的 ``sampler`` 替换为 + 我们专门用于断点重训功能的 ``ReproducibleSampler``,详见 :class:`fastNLP.core.samplers.reproducible_sampler.ReproducibleSampler`; + 5. model 的状态,即模型参数; + 6. optimizers 的状态,即优化器的状态; + 7. fp16 的状态; + + .. warning:: + + 一个值得注意的问题是 ``Driver`` 在新版 ``fastNLP`` 中的特殊作用,在断点重训时则体现为您应当尽量保证在前后两次训练中使用的 ``Driver`` + 是一致的,例如您不能在第一次训练时使用 ``pytorch``,而在第二次训练时使用 ``paddle``;或者尽量不要在第一次训练时使用分布式训练,但是 + 在第二次训练时使用非分布式训练(尽管这一行为的部分情况是支持的,请见下方的说明); + + 但是如果您一定需要在前后使用不同分布式情况的 ``Driver``,那么在简单的默认情况下,我们也还是支持您使用断点重训的,这意味您可以在第一次训练时 + 使用单卡,但是在第二次训练时使用多卡进行训练;或者反过来; + + 以 ``pytorch`` 为例,这里的简单的默认情况指的是您的 ``train_dataloader`` 所使用的 ``sampler`` 是 ``RandomSampler`` 或者 ``SequentialSampler``; + 如果您的 ``sampler`` 是其它类型的 ``sampler``,那么我们仅支持前后两次训练 ``driver`` 严格不变时的断点重训; """ + self.driver.barrier() # 1. callback states 和 每一个callback的具体 callback 函数的 filter 的状态; # 2. trainer_state; - states = {"callback_states": self.on_save_checkpoint(), - "trainer_state": self.trainer_state.state_dict(), - 'num_consumed_batches': self.batch_idx_in_epoch - getattr(self, 'start_batch_idx_in_epoch', 0) - } + states = { + "callback_states": self.on_save_checkpoint(), + "trainer_state": self.trainer_state.state_dict(), + 'num_consumed_batches': self.batch_idx_in_epoch - getattr(self, 'start_batch_idx_in_epoch', 0) + } if isinstance(folder, str): folder = Path(folder) @@ -1033,18 +1115,34 @@ class Trainer(TrainerEventTrigger): def load(self, folder: str, resume_training: bool = True, only_state_dict: bool = True, model_load_fn: Optional[Callable] = None, **kwargs): r""" - 用于断点重训的加载函数; - 注意在 fastNLP 中断点重训的保存和加载逻辑是分开的,因此可能存在一种情况:用户只希望加载一个断点重训的状态,而在之后不再进行断点重训的 - 保存;在这种情况下,dataloader 的 sampler 就不一定会被替换成我们的 ReproducibleSampler; + 用于帮助您实现断点重训功能的加载函数; + + :param folder: 保存断点重训时 ``trainer`` 的状态文件的文件夹; + :param resume_training: 是否精确到从上次训练时最终截断的那一个 batch 开始训练;如果 ``resume_training=True``,那么我们 + 只会加载 ``model`` 和 ``optimizers`` 的状态;而其余对象的值则根据用户的 ``Trainer`` 的初始化直接重置; + :param only_state_dict: 保存的 ``model`` 是否只保存了权重; + :param model_load_fn: 使用的模型加载函数,参数应为一个文件夹,注意该函数不需要返回任何内容;您可以传入该参数来定制自己的加载模型的操作, + 当该参数不为 None 时,我们默认加载模型由该函数完成,``trainer.load`` 函数则会把 ``trainer`` 的其余状态加载好; + + .. note:: - 注意我们目前不支持单卡到多卡的断点重训; + 在 fastNLP 中,断点重训的保存和加载的逻辑是完全分离的,这意味着您在第二次训练时可以将 ``CheckpointCallback`` 从 ``trainer`` 中 + 去除,而直接使用 ``trainer.load`` 函数加载 ``trainer`` 的状态来进行断点重训; + + 该函数在通常情况下和 ``save`` 函数配套使用;其参数与 ``save`` 函数成对应关系; + + 对于在前后两次训练 ``Driver`` 不同的情况时使用断点重训,请参考 :meth:`fastNLP.core.controllers.trainer.Trainer.load` 函数的 ``warning``; + + Example:: + + trainer = Trainer(...) + + trainer.load(folder='/path-to-your-saved_checkpoint_folder/', ...) + + trainer.run() - :param folder: 保存断点重训 states 的文件地址; - :param resume_training: 是否从上次的 batch 开始训练,或者只从最近的 epoch 开始训练;注意如果 resume_training=True,那么我们 - 只会加载 model 和 optimizers 的状态;而其余的对象的值则根据用户的 Trainer 的初始化直接重置; - :param only_state_dict: 保存的 model 是否只包含了权重。 - :param model_load_fn: 使用的模型加载函数,参数应为一个 文件夹,不返回任何内容。 """ + self.driver.barrier() if isinstance(folder, str): folder = Path(folder) @@ -1091,12 +1189,30 @@ class Trainer(TrainerEventTrigger): """ 这四个函数是用来方便用户定制自己的 batch_step_fn(用于替换 train_batch_loop 当中的 batch_step_fn 函数) 的 """ def train_step(self, batch): + r""" + 实现模型训练过程中的对一个 batch 的数据的前向传播过程; + + .. note:: + + 该函数的提供是为了您能够更方便地定制自己的 ``train_batch_step_fn`` 来替换原本的 ``train_batch_loop.batch_step_fn``;更具体的细节 + 请见 :meth:`fastNLP.core.controllers.loops.train_batch_loop.TrainBatchLoop.batch_step_fn`; + + ``trainer.backward / zero_grad / step`` 函数的作用类似; + + :param batch: 一个 batch 的数据; + :return: 返回模型的前向传播函数所返回的结果; + """ with self.driver.auto_cast(): outputs = self.driver.model_call(batch, self._train_step, self._train_step_signature_fn) outputs = match_and_substitute_params(self.output_mapping, outputs) return outputs def backward(self, outputs): + r""" + 实现模型训练过程中神经网络的反向传播过程; + + :param outputs: 模型的输出,应当为一个字典或者 dataclass,里面包含以 ``loss`` 为关键字的值; + """ self.on_before_backward(outputs) loss = self.extract_loss_from_outputs(outputs) loss = loss / self.accumulation_steps @@ -1104,27 +1220,40 @@ class Trainer(TrainerEventTrigger): self.on_after_backward() def zero_grad(self): + r""" + 实现模型训练过程中对优化器中的梯度的置零操作; + """ if (self.global_forward_batches + 1) % self.accumulation_steps == 0: self.on_before_zero_grad(self.optimizers) self.driver.zero_grad(self.set_grad_to_none) self.on_after_zero_grad(self.optimizers) def step(self): + r""" + 实现模型训练过程中的优化器的参数更新操作; + """ + if (self.global_forward_batches + 1) % self.accumulation_steps == 0: self.on_before_optimizers_step(self.optimizers) self.driver.step() self.on_after_optimizers_step(self.optimizers) def move_data_to_device(self, batch): + r""" + 将数据迁移到当前进程所使用的设备上; + + :param batch: 一个 batch 的数据; + :return: 位置已经被迁移后的数据; + """ return self.driver.move_data_to_device(batch) @staticmethod def extract_loss_from_outputs(outputs): r""" - 用来从用户模型的输出对象中抽取 `loss` 对象; - 目前支持 `outputs` 对象为 'Dict' 或者 'dataclass'; + 用来从用户模型的输出对象中抽取 ``loss`` 对象; + 目前支持 `outputs` 对象为 ``dict`` 或者 ``dataclass``; - :return: 返回被抽取出来的 `loss` 对象,如果当前运行的是 'pytorch' 的 `Driver`,那么返回的就是一个 tensor; + :return: 返回被抽取出来的 ``loss`` 对象,例如如果是 ``pytorch``,那么返回的就是一个 tensor; """ if isinstance(outputs, Dict): try: @@ -1147,10 +1276,10 @@ class Trainer(TrainerEventTrigger): @contextmanager def get_no_sync_context(self): r""" - 用于在梯度累积并且使用 DDP 时,由于在前 `accumulation_steps` - 1 的时间内不需要进行梯度的同步,因此通过使用该 context 上下文 - 环境来避免梯度的同步; + 用于在使用梯度累积并且进行分布式训练时,由于在前 ``accumulation_steps - 1`` 的时间内不需要进行梯度的同步,因此通过使用该 context 上下文 + 环境来避免梯度的同步; - :return: 一个 no_sync 的 context; + :return: 一个支持 ``no_sync`` 的 ``context``; """ if (self.global_forward_batches + 1) % self.accumulation_steps != 0: @@ -1165,6 +1294,9 @@ class Trainer(TrainerEventTrigger): @property def n_epochs(self) -> int: + r""" + :return: 返回当前训练的总体的 epoch 的数量; + """ return self.trainer_state.n_epochs @n_epochs.setter @@ -1173,6 +1305,9 @@ class Trainer(TrainerEventTrigger): @property def cur_epoch_idx(self) -> int: + r""" + :return: 返回当前正在第几个 epoch; + """ return self.trainer_state.cur_epoch_idx @cur_epoch_idx.setter @@ -1181,6 +1316,9 @@ class Trainer(TrainerEventTrigger): @property def global_forward_batches(self) -> int: + """ + :return: 返回从训练开始到当前总共训练了多少 batch 的数据; + """ return self.trainer_state.global_forward_batches @global_forward_batches.setter @@ -1189,6 +1327,9 @@ class Trainer(TrainerEventTrigger): @property def batch_idx_in_epoch(self) -> int: + r""" + :return: 返回在从当前的这个 epoch 开始,到现在共训练了多少 batch 的数据; + """ return self.trainer_state.batch_idx_in_epoch @batch_idx_in_epoch.setter @@ -1197,6 +1338,9 @@ class Trainer(TrainerEventTrigger): @property def num_batches_per_epoch(self) -> int: + r""" + :return: 返回每一个 epoch 实际会训练多少个 batch 的数据; + """ return self.trainer_state.num_batches_per_epoch @num_batches_per_epoch.setter @@ -1205,6 +1349,9 @@ class Trainer(TrainerEventTrigger): @property def total_batches(self) -> int: + r""" + :return: 返回整体的训练中实际会训练多少个 batch 的数据; + """ return self.trainer_state.total_batches @total_batches.setter @@ -1215,16 +1362,27 @@ class Trainer(TrainerEventTrigger): @property def model_device(self): + r""" + :return: 返回当前模型所在的设备;注意该值在当且仅当在少数情况下为 ``None``,例如当使用 ``pytorch`` 时,仅当用户自己初始化 ``init_progress_group`` 时 + ``model_device`` 才为 None; + """ return self.driver.model_device @property def data_device(self): + r""" + :return: 返回数据会被迁移到的目的设备; + """ return self.driver.data_device """ dataloader property """ @property def train_dataloader(self): + """ + :return: 返回用户传入的 ``train_dataloader``,注意该 ``dataloader`` 与用户传入给 ``Trainer`` 的 ``dataloader`` 对象是同一个对象,而我们在 + 实际训练过程中使用的 ``dataloader`` 的状态可能有所更改; + """ return self._train_dataloader @train_dataloader.setter @@ -1233,6 +1391,9 @@ class Trainer(TrainerEventTrigger): @property def evaluate_dataloaders(self): + """ + :return: 返回用户传入的 ``evaluate_dataloaders``; + """ return self._evaluate_dataloaders @evaluate_dataloaders.setter @@ -1242,6 +1403,10 @@ class Trainer(TrainerEventTrigger): def _get_input_output_mapping(input_mapping, output_mapping, train_input_mapping, train_output_mapping, evaluate_input_mapping, evaluate_output_mapping): + """ + 确定在训练过程中到底要使用哪个 input_mapping 和 output_mapping,之所以要设置该函数是因为在有些时候 evaluate 所需要的 input_mapping 和 + output_mapping 是与 train 的时候是不一样的,因此需要额外的定制; + """ if train_input_mapping is not None and input_mapping is not None: raise ValueError("Parameter `input_mapping` and `train_input_mapping` cannot be set simultaneously.") diff --git a/fastNLP/core/drivers/torch_driver/torch_driver.py b/fastNLP/core/drivers/torch_driver/torch_driver.py index 5aee15e9..a1b83d07 100644 --- a/fastNLP/core/drivers/torch_driver/torch_driver.py +++ b/fastNLP/core/drivers/torch_driver/torch_driver.py @@ -297,7 +297,7 @@ class TorchDriver(Driver): sampler = RandomSampler(dataloader_args.sampler.data_source) logger.debug("Replace torch RandomSampler into fastNLP RandomSampler.") elif self.is_distributed(): - raise RuntimeError("It is not allowed to use checkpoint retraining when you do not use our or " + raise RuntimeError("It is not allowed to use checkpoint retraining when you do not use our" "`ReproducibleSampler`.") else: sampler = ReproduceBatchSampler( From 6cbb5ceec41e17424f08b137dee9c8996455dfde Mon Sep 17 00:00:00 2001 From: x54-729 <17307130121@fudan.edu.cn> Date: Wed, 11 May 2022 15:43:45 +0000 Subject: [PATCH 06/14] =?UTF-8?q?PaddleFleetDriver=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/controllers/trainer.py | 2 +- fastNLP/core/drivers/paddle_driver/fleet.py | 281 ++++++++---------- .../drivers/paddle_driver/paddle_driver.py | 15 +- .../drivers/paddle_driver/single_device.py | 2 +- fastNLP/core/utils/jittor_utils.py | 4 +- fastNLP/core/utils/paddle_utils.py | 8 +- fastNLP/core/utils/rich_progress.py | 4 +- fastNLP/core/utils/utils.py | 16 +- fastNLP/modules/mix_modules/utils.py | 6 +- 9 files changed, 156 insertions(+), 182 deletions(-) diff --git a/fastNLP/core/controllers/trainer.py b/fastNLP/core/controllers/trainer.py index d64a39fe..dada3e68 100644 --- a/fastNLP/core/controllers/trainer.py +++ b/fastNLP/core/controllers/trainer.py @@ -264,7 +264,7 @@ class Trainer(TrainerEventTrigger): * fleet_kwargs -- 用于在使用 ``PaddleFleetDriver`` 时指定 ``DataParallel`` 和 ``fleet`` 初始化时的参数,包括: - * is_collective -- 是否使用 paddle 集群式的分布式训练方法,目前仅支持为 True 的情况; + * is_collective -- 是否使用 paddle 集群式的分布式训练方法,目前仅支持为 ``True`` 的情况; * role_maker -- 初始化 ``fleet`` 分布式训练 API 时使用的 ``RoleMaker`` * 其它用于初始化 ``DataParallel`` 的参数; * *data_device* -- 一个具体的 driver 实例中,有 ``model_device`` 和 ``data_device``,前者表示模型所在的设备,后者表示 diff --git a/fastNLP/core/drivers/paddle_driver/fleet.py b/fastNLP/core/drivers/paddle_driver/fleet.py index 03dc3375..36fb74fd 100644 --- a/fastNLP/core/drivers/paddle_driver/fleet.py +++ b/fastNLP/core/drivers/paddle_driver/fleet.py @@ -1,3 +1,69 @@ +r""" +用于实现 **PaddlePaddle** 框架下使用 ``fleet`` 分布式训练 API 进行集群式(*collective*)多卡训练的 Driver。 + +.. note:: + + 在 **PaddlePaddle** 框架中,使用分布式训练的方式可以参见 **PaddlePaddle** 的 + `官方文档 `_ 。 + 简言之,分布式训练的过程可以概括为:导入 ``fleet`` 包 -> 使用 :func:`fleet.init` 初始化分布式环境 -> 初始化模型,转换为并行模型开始训练。 + +**fastNLP** 支持三种启动分布式训练的方式(假设执行训练的文件名为 ``train.py``): + + A. 用户自己不进行分布式的任何操作,直接使用我们的 :class:`~fastNLP.core.Trainer` 进行训练,此时将参数 ``device`` + 设置为一个列表,然后使用 ``python train.py`` 的方式开始训练; + B. 用户自己不进行分布式的任何操作,但是使用 ``python -m paddle.distributed.launch train.py`` 开始训练; + C. 用户自己在外面初始化分布式环境,并且通过 ``python -m paddle.distributed.launch train.py`` 开始训练; + +.. note:: + + 在后两种启动方式中,您需要通过参数 ``--gpus`` 来指定训练使用的设备,在 ``trainer`` 中设置的参数是无效的。 + +不过在使用该 Driver 之前,我们需要向您说明 **fastNLP** 实现 ``PaddleFleetDriver`` 的思路,以便于您理解代码编写过程中可能出现的问题。 + +在 **fastNLP** 中,为了尽可能减少单卡向分布式训练转换过程中的代码变动,我们需要在 ``PaddleFleetDriver`` 中进行 **分布式环境初始化** +和 **将模型转换为并行模式** 等操作,同时实现多卡训练的方法是从主进程(``rank=0``)中创建其它的所有子进程(``rank=1,2,...``)。 +在这个过程中,我们发现由于 **PaddlePaddle** 框架的特性,会出现下面的问题: + + 1. **fastNLP** 中,初始化模型一定会在初始化 ``Driver`` 之前,因此调用 :func:`fleet.init` 的时机会在初始化模型之后; + 此时子进程中模型将无法正常地初始化,提示无法找到设备 ``gpu:0``; + 2. 在训练的过程中,会出现训练一个 ``batch`` 后程序卡住或程序会占用所有可见显卡的情况; + +考虑到这些问题,我们为 **PaddlePaddle** 的分布式训练制定了这样的约束:在导入 **fastNLP** 之前,必须设置环境变量 ``FASTNLP_BACKEND`` +为 ``paddle``。执行方法有两种:: + + >>> import os + >>> os.environ["FASTNLP_BACKEND"] = "paddle" # 设置环境变量 + >>> import fastNLP # 设置之后才可以导入 fastNLP + +或是在执行脚本(假设文件名为 ``train.py`` )时设置:: + + FASTNLP_BACKEND=paddle python train.py + FASTNLP_BACKEND=paddle python -m paddle.distributed.lauch train.py + +设置 ``FASTNLP_BACKEND=paddle`` 后,**fastNLP** 会在 ``import paddle`` 之前通过 ``CUDA_VISIBLE_DEVICES`` 将设备限制在所有可见设备的第 +**0** 张卡上,以此绕开通信和同步上的种种限制。我们会将用户希望可见的设备(如用户自己设置了 ``CUDA_VISIBLE_DEVICES`` 的情况)保存在另一个环境变量 +``USER_CUDA_VISIBLE_DEVICES`` 中来确保 **fastNLP** 能够知道用户的设置。假设用户希望在 ``[0,2,3]`` 三张显卡上进行分布式训练,那么在三个训练进程中, +``CUDA_VISIBLE_DEVICES`` 就分别为 0、2 和 3 。 + +.. note:: + + 我们会事先将设备限制在所有可见设备的第 **0** 张卡上,因此多卡训练的参数 ``device`` 一定要以 **0** 开始,否则会无法正常地启动。 + 如果您希望调整使用的第一张显卡,请使用 ``CUDA_VISIBLE_DEVICES`` 进行限制。 + +.. note:: + + 根据 **PaddlePaddle** 的说明,设置 ``CUDA_VISIBLE_DEVICES`` 之后启动分布式训练时,情况A与情况BC设置设备的方式会有所不同。 + 情况A应设置为实际设备相对可见设备的索引,而情况BC应设置为实际的设备号: + + 1. 情况A中, ``CUDA_VISIBLE_DEVICES=3,4,5,6`` 且参数 ``device=[0,2,3]`` 代表使用 **3号、5号和6号** 显卡; + 2. 情况BC中,``CUDA_VISIBLE_DEVICES=3,4,5,6`` 且参数 ``--gpu=3,5,6`` 代表使用 **3号、5号和6号** 显卡; + +.. note:: + + 多机的启动强制要求用户在每一台机器上使用 ``python -m paddle.distributed.launch`` 启动;因此我们不会在 ``PaddleFleetDriver`` + 中保存任何当前有多少台机器的信息; + +""" import os from typing import List, Union, Optional, Dict, Tuple, Callable @@ -53,6 +119,33 @@ __all__ = [ ] class PaddleFleetDriver(PaddleDriver): + """ + :param model: 训练使用的模型; + + * 如果不想自己初始化分布式环境,类型应为 :class:`paddle.nn.Layer`; + * 如果已经在外面初始化了分布式环境,类型应为 :class:`paddle.DataParallel`; + + :param parallel_device: 多卡训练时使用的设备,必须是一个列表。 + 当使用 ``python -m paddle.distributed.launch`` 启动时,该参数无效; + :param is_pull_by_paddle_run: 标记当前进程是否为通过 ``python -m paddle.distributed.launch`` 启动的。 + 这个参数仅在 :class:`~fastNLP.core.Trainer` 中初始化 driver 时使用 + :param fp16: 是否开启混合精度训练; + :kwargs: + * *paddle_kwargs* -- 用于在指定 ``driver`` 为 'paddle' 时设定具体 driver 实例的一些参数: + + * fleet_kwargs -- 用于在使用 ``PaddleFleetDriver`` 时指定 ``DataParallel`` 和 ``fleet`` 初始化时的参数,包括: + + * is_collective -- 是否使用 paddle 集群式的分布式训练方法,目前仅支持为 ``True`` 的情况; + * role_maker -- 初始化 ``fleet`` 分布式训练 API 时使用的 ``RoleMaker`` + * 其它用于初始化 ``DataParallel`` 的参数; + + * wo_auto_param_call (``bool``) -- 是否关闭在训练时调用我们的 ``auto_param_call`` 函数来自动匹配 batch 和前向函数的参数的行为; + + .. note:: + + 关于该参数的详细说明,请参见 :class:`~fastNLP.core.controllers.Trainer` 中的描述;函数 ``auto_param_call`` 详见 :func:`fastNLP.core.utils.auto_param_call`。 + + """ def __init__( self, model, @@ -61,143 +154,20 @@ class PaddleFleetDriver(PaddleDriver): fp16: bool = False, **kwargs ): - r""" - 通过使用 PaddlePaddle 的 Fleet 框架启动多卡进程的 Driver。 - 需要注意的一点是,由于 PaddlePaddle 框架的特性,如果直接使用在 rank0 拉起其它进程的方法的话,如果不加以任何限制,PaddlePaddle会出现 - 第一次前向传播后卡住或占用所有显卡的现象;为了解决这一问题,我们在引入 FastNLP 时,会使用 `CUDA_VISIBLE_DEVICES` 将设备限制在卡0上, - 而用户如果使用了这一环境变量,我们会将其储存在 `USER_CUDA_VISIBLE_DEVICES` 中,并且通过一定的手段实现了转换(详细的设置请参见: - `fastNLP/envs/set_backend.py`)。在拉起其它进程的时候,我们会如法炮制,将环境限制在对应的设备上。 - - `PaddleFleetDriver` 目前支持的三种启动方式: - 1. 用户自己不进行分布式的任何操作,直接使用我们的 Trainer,这时是由我们自己使用 `FleetLauncher` 拉起多个进程, - 然后 `PaddleFleetDriver` 自己通过调用 `fleet.init` 来初始化 ddp 的通信组;(情况 A) - 2. 用户同样不在 Trainer 之外初始化分布式训练,但是用户自己使用 python -m paddle.distributed.launch 拉起来创建多个进程,这时我们仍旧 - 会通过调用 `fleet.init` 来初始化 ddp 的通信组;(情况 B) - 3. 用户自己在外面初始化分布式,并且通过 python -m paddle.distributed.launch 拉起,这时无论是多个进程的拉起和通信组的建立 - 都由用户自己操作,我们只会在 driver.setup 的时候对 `PaddleFleetDriver` 设置一些必要的属性值;(情况 C) - - 注意多机的启动强制要求用户在每一台机器上使用 python -m paddle.distributed.launch 启动;因此我们不会在 `PaddleFleetDriver` 中保存 - 任何当前有多少台机器的信息; - - Part 1:三种启动方式的具体分析: - (1)对于用户运行的脚本中,如果 `driver.setup` 只会被调用一次(意味着用户的启动脚本中只初始化了一个 trainer/evaluator)时, - `PaddleFleetDriver` 在初始化以及 `setup` 函数中会做的事情分别如下所示: - -> 情况 A:这种情况下用户传入的 model 在一定是普通的 model(没有经 `DataParallel` 包裹的model), - 因为 `Parallel` 的使用一定要求 fleet.init 已经被调用用来建立当前的 ddp 通信组;但是这意味着如果 - 用户需要使用 2 张以上的显卡,那么其必然需要使用 paddle.distributed.launch 来启动,意味着就不是情况 A 了; - 这时我们首先会调用 `FleetLauncher.launch` 函数来拉起多个进程,其中进程的数量等于用户传入给 trainer 的使用的 gpu - 的数量(例如 `Trainer` 中的参数是 device=[0, 1, 6, 7],那么我们就会使用第 0、1、6、7 张 gpu 来拉起 4 个进程); - 接着我们会调用 `fleet.init` 来初始化各个进程之间的通信组; - 这里需要注意拉起的新的进程会从前到后完整地运行一遍用户的启动脚本(例如 main.py),因此也都会运行这两个函数,但是需要注意只有进程 0 - 才会去真正地运行 `FleetLauncher.launch`;进程 0 运行到 `fleet.init`,paddle 会阻塞进程 0 继续 - 向前运行,直到其它进程也运行到这里; - 最后我们会设置这个进程对应的 device,然后将模型迁移到对应的机器上,再使用 `DataParallel` 将模型包裹; - 至此,paddle 分布式的环境配置过程全部完成; - - -> 情况 B:注意这种情况我们直接限定了用户是通过 paddle.distributed.launch 拉起,并且没有自己建立分布式的通信组。这时在 - `PaddleFleetDriver` 的初始化和 setup 函数的调用过程中,与情况 A 首要的不同就在于用户在 trainer 中输入的参数 device 不再有效, - 这时每个进程所使用的 gpu 是我们直接通过 `CUDA_VISIBLE_DEVICE` 来配置的;因此,如果用户想要实现使用特定 gpu - 设备的目的,可以通过自己设置环境变量实现(例如 os.environ["CUDA_VISIBLE_DEVICE"] 来实现,我们会通过一定的手段将其保存起来); - 剩下的操作和情况 A 类似; - - -> 情况 C:注意这种情况我们限定了用户是通过 paddle.distributed.launch 拉起,并且 ddp 的通信组也是由自己建立。这时基本上所有的 - 与操作相关的操作都应当由用户自己完成,包括迁移模型到对应 gpu 上以及将模型用 `DataParallel` 包裹等。 - (2)如果 `driver.setup` 函数在脚本中会被调用两次及以上(意味着用户的启动脚本初始化了两个及以上的 trainer/evaluator)时: - 注意这种情况下我们是会保证前后两个 trainer/evaluator 使用的 `PaddleFleetDriver` 以及其初始化方式的一致性,换句话说,如果 trainer1 - 检测到的启动方式是 '情况 A',那么我们会保证 trainer2 检测到的启动方式同样是 '情况A'(即使这需要一些额外的处理);因此这里我们主要讨论 - 我们是通过怎样的操作来保证 trainer2/3/... 检测到的启动方式是和 trainer1 一致的;简单来说,我们是通过使用环境变量来标记每一种不同的 - 启动方式来实现这一点的: - 我们会使用 `FASTNLP_DISTRIBUTED_CHECK` 来标记 '情况 A',使用 `fastnlp_torch_launch_not_ddp` 来标记 '情况 B',意味着我们在 - 使用 '情况 A' 来启动 `PaddleFleetDriver` 时,我们会将 `FASTNLP_DISTRIBUTED_CHECK` 这一字符串注入到环境变量中,而 '情况 B' 时则 - 会将 `fastnlp_torch_launch_not_ddp` 这一字符串注入到环境变量中。因此在 trainer2 的 `PaddleFleetDriver` 的初始化和 setup 过程中, - 如果检测到这些特殊的环境变量,我们就会将启动方式变更为其对应的启动方式,即使其它的参数特征属于另外的启动方式。 - - Part 2:对应的代码细节: - 1. 如何判断当前的各进程之间的通信组已经被建立(fleet 已经被初始化); - parallel_helper._is_parallel_ctx_initialized(); - 2. 如何判断不同的进程是否是由 `python -m paddle.distributed.launch` 拉起还是由我们的 `FleetLauncher.launch()` - 函数拉起; - 我们会在用户脚本 `import fastNLP` 的时候检测当前的环境变量中是否有 'PADDLE_RANK_IN_NODE'、'PADDLE_TRAINER_ID' - 以及没有 `FASTNLP_DISTRIBUTED_CHECK`, - 如果满足条件,则我们会向环境变量中注入特殊的值 'FASTNLP_BACKEND_LAUNCH' 来标记用户是否使用了 `python -m paddle.distributed.launch` - 来拉起多个进程; - 3. 整体的处理判断流程: - ___________________________________ - |进入 PaddleFleetDriver 的 __init__ 函数| - ——————————————————————————————————— - ↓ - ___________________________________________________ - | 判断不同的进程是否是由 paddle.distributed.launch 拉起 | - |(或者我们自己的 FleetLauncher 函数拉起) | --------------> - ———————————————————————————————————————————————————  | - ↓ 是由 paddle.distributed.launch 拉起 | 我们自己的 FleetLauncher 函数拉起多个进程 -  _____________________________            |  - ←←←←← | 检测用户是否自己初始化了 fleet |              | - ↓ —————————————————————————————                  ↓ - ↓ ↓ 是 ________ - ↓ ______ | 情况 A | - ↓ 否 |情况 C| ————————— - ↓ ——————— - ↓ - ↓ ______ - ↓ -----------> |情况 B| -   ——————— - 4. 为了完成全部的建立分布式所需要的操作,三种情况都需要做的事情,以及每件事情的职责归属: - - 情况 A | 情况 B | 情况 C - ________________________________________________________________________________________________________ - 配置 fleet 所 | FleetLauncher.launch | paddle.distributed.launch| paddle.distributed.launch - 需要的环境变量 | | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 开启多个进程 | FleetLauncher.launch | paddle.distributed.launch| paddle.distributed.launch - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 调用 fleet.init函数 | PaddleFleetDriver.setup | PaddleFleetDriver.setup | 用户自己调用 - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 设置 PaddleFleetDriver | | | - 的 world_size 和 | PaddleFleetDriver.setup | PaddleFleetDriver.setup | PaddleFleetDriver.setup - global_rank 属性 | | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - - Part 3:其它的处理细节: - 1. 环境变量; - fastNLP 的 `PaddleFleetDriver` 运行时所需要的环境变量分为两种,一种是 paddle fleet 运行所需要的环境变量;另一种是 fastNLP 自己 - 的环境变量。前者的配置情况如上表所示;而后者中的大多数环境变量则是在用户 import fastNLP 时就设置好了; - 2. parallel_device, model_device 和 data_device 的关系; - parallel_device 为 `PaddleFleetDriver` 的参数,model_device 和 data_device 都为 driver 的属性; - 其中 data_device 仅当情况 C 时由用户自己指定;如果其不为 None,那么在模型 forward 的时候,我们就会将数据迁移到 data_device 上; - model_device 永远都为单独的一个 torch.device; - - 情况 A | 情况 B | 情况 C - ________________________________________________________________________________________________________ - parallel_device | 由用户传入trainer的参数 | | - | device 决定,必须是一个list, | 为 CUDA_VISIBLE_DEVICES | 为 CUDA_VISIBLE_DEVICES - | 其中每一个对象都是 int | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - model_device | parallel_device[local_rank] | parallel_device | None - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - data_device | model_device | model_device | 由用户传入 trainer 的参数 - | | | data_device 决定 - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - - 3. _DDPWrappingModel 的作用; - 因为我们即需要调用模型的 `train_step`、`evaluate_step`、`test_step` 方法,又需要通过 `DataParallel` 的forward 函数来帮助 - 我们同步各个设备上的梯度,因此我们需要先将模型单独包裹一层,然后在 forward 的时候,其先经过 `DataParallel` 的 forward 方法, - 然后再经过 `_DDPWrappingModel` 的 forward 方法,我们会在该 forward 函数中进行判断,确定调用的是模型自己的 forward 函数,还是 - `train_step`、`evaluate_step`、`test_step` 方法。 - - 4. 当某一个进程出现 exception 后,`PaddleFleetDriver` 的处理; - - 不管是什么情况,`PaddleFleetDriver` 在 `setup` 函数的最后,都会将所有进程的 pid 主动记录下来,这样当一个进程出现 exception 后, - driver 的 on_exception 函数就会被 trainer 调用,其会调用 os.kill 指令将其它进程 kill 掉; - """ if USER_CUDA_VISIBLE_DEVICES not in os.environ: raise RuntimeError("To run paddle distributed training, please set `FASTNLP_BACKEND` to 'paddle' before using FastNLP.") super(PaddleFleetDriver, self).__init__(model, fp16=fp16, **kwargs) # 如果不是通过 launch 启动,要求用户必须传入 parallel_device - if not is_pull_by_paddle_run and parallel_device is None: - raise ValueError("Parameter `parallel_device` can not be None when using `PaddleFleetDriver`. This error is caused " - "when your value of parameter `device` is `None` in your `Trainer` instance.") + if not is_pull_by_paddle_run: + if parallel_device is None: + raise ValueError("Parameter `parallel_device` can not be None when using `PaddleFleetDriver`. This error is caused " + "when your value of parameter `device` is `None` in your `Trainer` instance.") + if not isinstance(parallel_device, List): + raise ValueError("Parameter `parallel_device`'s type must be List when using `PaddleFleetDriver`, " + f"not {type(parallel_device)}.") + if get_paddle_device_id(parallel_device[0]) != 0: + raise ValueError("The first device of `parallel_device` must be 'gpu:0' in fastNLP.") # 如果用户自己初始化了 paddle 的分布式训练那么一定是通过 launch 拉起的 # 这个参数会在 initialize_paddle_drvier 中设置。 @@ -254,10 +224,10 @@ class PaddleFleetDriver(PaddleDriver): def setup(self): """ - 根据不同的情况进行不同的设置。 - 1、如果是通过 paddle.distributed.launch 方法启动时,则根据已经设置好的环境获取 - 分布式的属性。 - 2、否则,调用 FleetLauncher 类启动子进程 + 初始化分布式训练的环境。 + + 1. 如果是通过 ``paddle.distributed.launch`` 方法启动的,则根据已经设置好的环境获取分布式的属性。 + 2. 否则启动子进程。 """ if self._has_setup: return @@ -267,7 +237,7 @@ class PaddleFleetDriver(PaddleDriver): if self.outside_fleet: # 已经初始化了多机环境 - self.set_from_fleet_environment() + self._set_from_fleet_environment() else: # 用户没有初始化多机环境 # TODO 绕一下 @@ -287,7 +257,7 @@ class PaddleFleetDriver(PaddleDriver): # parallel_device 是 list, if not parallel_helper._is_parallel_ctx_initialized(): # 拉起子进程并设置相应的属性 - self.init_fleet_and_set() + self._init_fleet_and_set() # 用户在这个 trainer 前面又初始化了一个 trainer,并且使用的是 PaddleFleetDriver; else: # 已经设置过一次,保证参数必须是一样的 @@ -321,7 +291,7 @@ class PaddleFleetDriver(PaddleDriver): self._pids = self._pids[node_rank*local_world_size: (node_rank+1)*local_world_size] self._pids = self.tensor_to_numeric(self._pids) - def init_fleet_and_set(self): + def _init_fleet_and_set(self): """ 使用 FleetLauncher 拉起子进程 """ @@ -340,7 +310,7 @@ class PaddleFleetDriver(PaddleDriver): assert self.world_size is not None assert self.world_size == len(self.parallel_device) - def set_from_fleet_environment(self): + def _set_from_fleet_environment(self): """ 当用户使用了 `python -m paddle.distributed.launch xxx.py` 启动时,我们需要 根据 paddle 设置的环境变量来获得各种属性 @@ -349,19 +319,11 @@ class PaddleFleetDriver(PaddleDriver): self.global_rank = paddledist.get_rank() def barrier(self): - r""" - 用于在多进程工作时同步各进程的工作进度,运行快的进程运行到这里会等待运行慢的进程,只有所有进程都运行到此函数时,所有的进程才会继续运行; - 仅在多分布式训练场景中有使用。 - - 注意,该函数的行为会受到 FASTNLP_NO_SYNC 的影响。仅当 FASTNLP_NO_SYNC 在 os.environ 中不存在,或小于 1 时才真的执行 barrier 。 - """ if int(os.environ.get(FASTNLP_NO_SYNC, 0)) < 1: # 当 FASTNLP_NO_SYNC 小于 1 时实际执行 paddledist.barrier() def configure_fleet(self): - """ - 将模型用 DataParallel 和自定义的类型包裹起来 - """ + # 将模型用 DataParallel 和自定义的类型包裹起来 if not self._has_fleetwrapped and not isinstance(self.model, DataParallel): self.model = DataParallel( _FleetWrappingModel(self.model), @@ -395,10 +357,17 @@ class PaddleFleetDriver(PaddleDriver): @property def model_device(self): + """ + :return: 模型所在的设备; + """ return self._model_device @property def data_device(self): + """ + :return: 数据所在的设备;由于 **PaddlePaddle** 可以通过环境变量获取当前进程的设备,因此该属性 + 和 ``model_device`` 表现相同; + """ return self.model_device def model_call(self, batch, fn: Callable, signature_fn: Optional[Callable]) -> Dict: @@ -522,23 +491,29 @@ class PaddleFleetDriver(PaddleDriver): else: raise ValueError("Parameter `dist_sampler` can only be one of three values: ('dist', 'unrepeatdist', None).") - def is_global_zero(self): + def is_global_zero(self) -> bool: return self.global_rank == 0 def get_model_no_sync_context(self): return self.model.no_sync - def unwrap_model(self): + def unwrap_model(self) -> "paddle.nn.Layer": + """ + 获得 driver 最原始的模型。该函数可以取出被 :class:`paddle.DataParallel` 包裹起来的模型。 + """ _layers = self.model._layers if isinstance(_layers, _FleetWrappingModel): return _layers.model else: return _layers - def get_local_rank(self) ->int: + def get_local_rank(self) -> int: return self.local_rank - def is_distributed(self): + def is_distributed(self) -> bool: + """ + 判断是否为分布式的 **Driver** ,在 ``PaddleFleetDriver`` 中,返回 ``True``。 + """ return True @staticmethod diff --git a/fastNLP/core/drivers/paddle_driver/paddle_driver.py b/fastNLP/core/drivers/paddle_driver/paddle_driver.py index a228a90d..c10c98a1 100644 --- a/fastNLP/core/drivers/paddle_driver/paddle_driver.py +++ b/fastNLP/core/drivers/paddle_driver/paddle_driver.py @@ -55,7 +55,7 @@ class PaddleDriver(Driver): 这个类被以下子类继承: 1. :class:`~fastNLP.core.drivers.PaddleSingleDriver`:实现了使用单卡和 ``cpu`` 训练的具体功能; - 2. :class:`~fastNLP.core.drivers.PaddleFleetDriver`:实现了使用 ``fleet`` 分布式训练 API 进行分布式训练的具体功能; + 2. :class:`~fastNLP.core.drivers.PaddleFleetDriver`:实现了使用 ``fleet`` 分布式训练 API 进行集群式分布式训练的具体功能; :param model: 训练时使用的 **PaddlePaddle** 模型; :param fp16: 是否开启混合精度训练; @@ -174,8 +174,8 @@ class PaddleDriver(Driver): 将模型保存到 ``filepath`` 中。 :param filepath: 保存文件的文件位置(需要包括文件名); - :param only_state_dict: 是否只保存模型的 ``state_dict``;如果为 ``False``,则会调用 ``paddle.jit.save`` 函数 - 保存整个模型的参数,此时需要传入 ``input_spec`` 参数; + :param only_state_dict: 是否只保存模型的 ``state_dict``;如果为 ``False``,则会调用 ``paddle.jit.save`` + 函数保存整个模型的参数,此时需要传入 ``input_spec`` 参数; :kwargs: * input_spec -- 描述存储模型 ``forward`` 方法的输入; 当 ``only_state_dict`` 为 ``False`` 时必须传入,否则加载时会报错。您可以通过 ``InputSpec`` 或者示例 ``Tensor`` @@ -212,11 +212,10 @@ class PaddleDriver(Driver): 断点重训的保存函数,该函数会负责保存模型和 optimizers, fp16 的 state_dict;以及模型的保存(若 should_save_model 为 True) :param folder: 保存断点重训的状态的文件夹;save 函数应该在下面新增两(一)个文件 的 FASTNLP_CHECKPOINT_FILENAME 文件与 - FASTNLP_MODEL_FILENAME (如果 should_save_model 为 True )。把 model 相关的内容放入到 FASTNLP_MODEL_FILENAME 文件 - 中,将传入的 states 以及自身产生其它状态一并保存在 FASTNLP_CHECKPOINT_FILENAME 里面。 - :param states: 由 trainer 传入的一个字典,其中已经包含了为了实现断点重训所需要保存的其它对象的状态,Driver 应该只需要保存 - 该对象即可, Driver 应该不需要理解该对象,同时在 driver.load() 的时候,需要将 states 返回回去,load() 返回的值与这里的 - 传入的值保持一致。 + FASTNLP_MODEL_FILENAME (如果 should_save_model 为 True )。把 model 相关的内容放入到 FASTNLP_MODEL_FILENAME 文件中, + 将传入的 states 以及自身产生其它状态一并保存在 FASTNLP_CHECKPOINT_FILENAME 里面。 + :param states: 由 trainer 传入的一个字典,其中已经包含了为了实现断点重训所需要保存的其它对象的状态,Driver 应该只需要保存该对象即可, + Driver 应该不需要理解该对象,同时在 driver.load() 的时候,需要将 states 返回回去,load() 返回的值与这里的传入的值保持一致。 :param dataloader: 正在使用的 dataloader,需要保存里面的状态使得之后可以从当前迭代的位置恢复。 :param only_state_dict: 是否只保存模型的参数,当 should_save_model 为 False ,该参数无效。 :param should_save_model: 是否应该保存模型,如果为False,Driver 将不负责 model 的保存。 diff --git a/fastNLP/core/drivers/paddle_driver/single_device.py b/fastNLP/core/drivers/paddle_driver/single_device.py index a9e92fd3..10779fd6 100644 --- a/fastNLP/core/drivers/paddle_driver/single_device.py +++ b/fastNLP/core/drivers/paddle_driver/single_device.py @@ -152,6 +152,6 @@ class PaddleSingleDriver(PaddleDriver): def is_distributed(self) -> bool: """ - 判断是否为分布式的 **Driver** ,在 ``PaddleSingleDriver`` 中,返回 ``False`` + 判断是否为分布式的 **Driver** ,在 ``PaddleSingleDriver`` 中,返回 ``False``。 """ return False diff --git a/fastNLP/core/utils/jittor_utils.py b/fastNLP/core/utils/jittor_utils.py index 08b3b7a8..f29b1f46 100644 --- a/fastNLP/core/utils/jittor_utils.py +++ b/fastNLP/core/utils/jittor_utils.py @@ -32,8 +32,8 @@ def is_jittor_dataset(dataset) -> bool: def jittor_collate_wraps(func, auto_collator: Callable): """ - 对 ``jittor`` 的 ``collate_fn`` 进行 ``wrap`` 封装,。如果数据集为 ``mapping`` 类型,那么采用 ``auto_collator`` ,否则 - 还是采用 ``jittor`` 的 ``collate_batch``。 + 对 ``jittor`` 的 ``collate_fn`` 进行 ``wrap`` 封装,。如果数据集为 ``mapping`` 类型,那么采用 ``auto_collator`` , + 否则还是采用 ``jittor`` 的 ``collate_batch``。 :param func: :param auto_collator: diff --git a/fastNLP/core/utils/paddle_utils.py b/fastNLP/core/utils/paddle_utils.py index d3764d4e..2d7b65cc 100644 --- a/fastNLP/core/utils/paddle_utils.py +++ b/fastNLP/core/utils/paddle_utils.py @@ -61,8 +61,8 @@ def _convert_data_device(device: Union[str, int]) -> str: def paddle_to(data: "paddle.Tensor", device: Union[str, int]) -> "paddle.Tensor": """ - 将 ``data`` 迁移到指定的 ``device`` 上。``paddle.Tensor`` 没有类似 ``torch.Tensor`` 的 ``to`` 函数,该函数 - 只是集成了 :func:`paddle.Tensor.cpu` 和 :func:`paddle.Tensor.cuda` 两个函数。 + 将 ``data`` 迁移到指定的 ``device`` 上。``paddle.Tensor`` 没有类似 ``torch.Tensor`` 的 ``to`` 函数, + 该函数只是集成了 :func:`paddle.Tensor.cpu` 和 :func:`paddle.Tensor.cuda` 两个函数。 :param data: 要迁移的张量; :param device: 目标设备,可以是 ``str`` 或 ``int`` 类型; @@ -130,8 +130,8 @@ def paddle_move_data_to_device(batch: Any, device: Optional[Union[str, int]]) -> 将 **paddle** 的数据集合传输到给定设备。只有 :class:`paddle.Tensor` 对象会被传输到设备中,其余保持不变。 :param batch: 需要进行迁移的数据集合; - :param device: 目标设备。可以是显卡设备的编号,或是``cpu``, ``gpu`` 或 ``gpu:x`` 格式的字符串;当这个参数 - 为 `None`` 时,不会执行任何操作。 + :param device: 目标设备。可以是显卡设备的编号,或是``cpu``, ``gpu`` 或 ``gpu:x`` 格式的字符串; + 当这个参数为 `None`` 时,不会执行任何操作。 :return: 迁移到新设备上的数据集合; """ if device is None: diff --git a/fastNLP/core/utils/rich_progress.py b/fastNLP/core/utils/rich_progress.py index 53d4e281..fc056526 100644 --- a/fastNLP/core/utils/rich_progress.py +++ b/fastNLP/core/utils/rich_progress.py @@ -1,6 +1,6 @@ """ -该文件用于为 **fastNLP** 提供一个统一的 ``progress bar`` 管理,通过共用一个``Task`` 对象, :class:`~fastNLP.core.Trainer` 中 -的 ``progress bar`` 和 :class:`~fastNLP.core.Evaluator` 中的 ``progress bar`` 才能不冲突 +该文件用于为 **fastNLP** 提供一个统一的 ``progress bar`` 管理,通过共用一个``Task`` 对象, :class:`~fastNLP.core.Trainer` +中的 ``progress bar`` 和 :class:`~fastNLP.core.Evaluator` 中的 ``progress bar`` 才能不冲突 """ import sys from typing import Any, Union, Optional diff --git a/fastNLP/core/utils/utils.py b/fastNLP/core/utils/utils.py index 4d8bbb5e..5e02ef6d 100644 --- a/fastNLP/core/utils/utils.py +++ b/fastNLP/core/utils/utils.py @@ -60,7 +60,7 @@ def auto_param_call(fn: Callable, *args, signature_fn: Optional[Callable] = None ``value`` 的参数。 1. 该函数用来提供给用户根据字符串匹配从而实现自动调用; - 2. 注意 ``mapping`` 默认为 ``None``,如果你希望指定输入和运行函数的参数的对应方式,那么你应当让 ``mapping`` 为一个字典传入进来; + 2. 注意 ``mapping`` 默认为 ``None``,如果您希望指定输入和运行函数的参数的对应方式,那么您应当让 ``mapping`` 为一个字典传入进来; 如果 ``mapping`` 不为 ``None``,那么我们一定会先使用 ``mapping`` 将输入的字典的 ``keys`` 修改过来,因此请务必亲自检查 ``mapping`` 的正确性; 3. 如果输入的函数的参数有默认值,那么如果之后的输入中没有该参数对应的值,我们就会使用该参数对应的默认值,否则也会使用之后的输入的值; 4. 如果输入的函数是一个 ``partial`` 函数,情况同第三点,即和默认参数的情况相同; @@ -82,8 +82,8 @@ def auto_param_call(fn: Callable, *args, signature_fn: Optional[Callable] = None :param fn: 用来进行实际计算的函数,其参数可以包含有默认值; :param args: 一系列的位置参数,应当为一系列的字典,我们需要从这些输入中提取 ``fn`` 计算所需要的实际参数; - :param signature_fn: 函数,用来替换 ``fn`` 的函数签名,如果该参数不为 ``None``,那么我们首先会从该函数中提取函数签名,然后通过该函数签名提取 - 参数值后,再传给 ``fn`` 进行实际的运算; + :param signature_fn: 函数,用来替换 ``fn`` 的函数签名,如果该参数不为 ``None``,那么我们首先会从该函数中提取函数签名, + 然后通过该函数签名提取参数值后,再传给 ``fn`` 进行实际的运算; :param mapping: 一个字典,用来更改其前面的字典的键值; :return: 返回 ``fn`` 运行的结果; @@ -195,8 +195,8 @@ def _get_fun_msg(fn, with_fp=True)->str: def _check_valid_parameters_number(fn, expected_params:List[str], fn_name=None): """ - 检查一个函数是否需要 expected_params 参数(检测数量是否匹配)。除掉 self (如果是method),给定默认值的参数等。如果匹配不上,就会 - 进行报错。 + 检查一个函数是否需要 expected_params 参数(检测数量是否匹配)。除掉 self (如果是method),给定默认值的参数等。 + 如果匹配不上,就会进行报错。 :param fn: 需要检测的函数,可以是 method 或者 function 。 :param expected_params: 期待应该支持的参数。 @@ -345,8 +345,8 @@ def apply_to_collection( :param dtype: 数据的类型,函数 ``function`` 只会被应用于 ``data`` 中类型为 ``dtype`` 的数据; :param function: 对数据进行处理的函数; :param args: ``function`` 所需要的其它参数; - :param wrong_dtype: ``function`` 一定不会生效的数据类型。如果数据既是 ``wrong_dtype`` 类型又是 ``dtype`` 类型 - 那么也不会生效; + :param wrong_dtype: ``function`` 一定不会生效的数据类型。 + 如果数据既是 ``wrong_dtype`` 类型又是 ``dtype`` 类型那么也不会生效; :param include_none: 是否包含执行结果为 ``None`` 的数据,默认为 ``True``; :param kwargs: ``function`` 所需要的其它参数; :return: 经过 ``function`` 处理后的数据集合; @@ -587,7 +587,7 @@ def seq_len_to_mask(seq_len, max_len: Optional[int]): :param seq_len: 大小为 ``(B,)`` 的长度序列; :param int max_len: 将长度补齐或截断到 ``max_len``。默认情况(为 ``None``)使用的是 ``seq_len`` 中最长的长度; 但在 :class:`torch.nn.DataParallel` 等分布式的场景下可能不同卡的 ``seq_len`` 会有区别,所以需要传入 - 一个 ``max_len`` 使得 ``mask`` 的补齐或截断到该长度。 + ``max_len`` 使得 ``mask`` 的补齐或截断到该长度。 :return: 大小为 ``(B, max_len)`` 的 ``mask``, 元素类型为 ``bool`` 或 ``uint8`` """ if isinstance(seq_len, np.ndarray): diff --git a/fastNLP/modules/mix_modules/utils.py b/fastNLP/modules/mix_modules/utils.py index b19a5d53..142644f9 100644 --- a/fastNLP/modules/mix_modules/utils.py +++ b/fastNLP/modules/mix_modules/utils.py @@ -202,12 +202,12 @@ def jittor2torch(batch: Any, device: str = None, no_gradient: bool = None) -> An .. note:: - 注意,由于 **pytorch** 和 **jittor** 之间的差异,从 :class:`jittor.Var` 转换 - 至 :class:`torch.Tensor` 的过程中无法保留原张量的梯度。 + 注意,由于 **pytorch** 和 **jittor** 之间的差异,从 :class:`jittor.Var` 转换至 + :class:`torch.Tensor` 的过程中无法保留原张量的梯度。 :param batch: 包含 :class:`jittor.Var` 类型的数据集合; :param device: 是否将转换后的张量迁移到特定设备上。为 ``None``时,和输入保持一致; - :param no_gradient: 是否保留原张量的梯度,在这个函数中该参数无效。 + :param no_gradient: 是否保留原张量的梯度,在这个函数中该参数无效; :return: 转换后的数据; """ From c562982f1d98bc4af32930143a48b022b3cb0102 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 12 May 2022 01:02:10 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callbacks/__init__.py | 1 - fastNLP/core/callbacks/callback_event.py | 20 +-- fastNLP/core/callbacks/callback_manager.py | 4 - fastNLP/core/callbacks/checkpoint_callback.py | 90 +++++----- fastNLP/core/callbacks/early_stop_callback.py | 31 ++-- .../core/callbacks/has_monitor_callback.py | 93 +++++----- .../callbacks/load_best_model_callback.py | 48 ++--- .../core/callbacks/lr_scheduler_callback.py | 14 +- .../core/callbacks/more_evaluate_callback.py | 103 ++++++----- fastNLP/core/callbacks/progress_callback.py | 36 ++-- fastNLP/core/callbacks/topk_saver.py | 96 +++++----- .../torch_grad_clip_callback.py | 25 +-- .../torch_lr_sched_callback.py | 22 +-- fastNLP/core/collators/collator.py | 31 ++-- fastNLP/core/collators/packer_unpacker.py | 17 +- .../core/collators/padders/jittor_padder.py | 6 +- .../core/collators/padders/numpy_padder.py | 60 ++++--- fastNLP/core/collators/padders/padder.py | 20 ++- .../core/collators/padders/paddle_padder.py | 48 ++--- fastNLP/core/collators/padders/raw_padder.py | 48 ++--- .../core/collators/padders/torch_padder.py | 55 +++--- fastNLP/core/controllers/evaluator.py | 170 ++++++++++++------ fastNLP/core/controllers/trainer.py | 8 +- fastNLP/core/dataset/instance.py | 16 +- fastNLP/core/metrics/metric.py | 21 ++- .../core/metrics/span_f1_pre_rec_metric.py | 34 ++-- fastNLP/core/samplers/conversion_utils.py | 2 +- .../samplers/reproducible_batch_sampler.py | 78 ++++---- fastNLP/core/samplers/reproducible_sampler.py | 60 +++---- fastNLP/core/samplers/unrepeated_sampler.py | 52 +++--- fastNLP/core/utils/rich_progress.py | 5 - fastNLP/envs/distributed.py | 12 +- 32 files changed, 732 insertions(+), 594 deletions(-) diff --git a/fastNLP/core/callbacks/__init__.py b/fastNLP/core/callbacks/__init__.py index efd9280f..caf96af7 100644 --- a/fastNLP/core/callbacks/__init__.py +++ b/fastNLP/core/callbacks/__init__.py @@ -2,7 +2,6 @@ __all__ = [ 'Callback', 'Event', 'Filter', - 'CallbackManager', 'CheckpointCallback', 'choose_progress_callback', 'ProgressCallback', diff --git a/fastNLP/core/callbacks/callback_event.py b/fastNLP/core/callbacks/callback_event.py index 9bdbfd95..e7657a25 100644 --- a/fastNLP/core/callbacks/callback_event.py +++ b/fastNLP/core/callbacks/callback_event.py @@ -30,20 +30,20 @@ def check_legality(fn): class Event: + """ + 与 Trainer.on 函数配合使用,达到控制 callback 函数运行时机的目的。 + + :param value: Trainer 的 callback 时机。 + :param int every: 触发了多少次,才真正运行一次。 + :param bool once: 是否只在第一次运行后就不再执行了。 + :param Callable filter_fn: 输入参数的应该为 (filter, trainer),其中 filter 对象中包含了 filter.num_called 和 + filter.num_executed 两个变量分别获取当前被调用了多少次,真正执行了多少次。trainer 对象即为当前正在运行的 Trainer 。 + """ every: Optional[int] once: Optional[int] def __init__(self, value: str, every: Optional[int] = None, once: Optional[int] = None, filter_fn: Optional[Callable] = None): - """ - 请勿直接使用本对象,而是通过调用 Event.on_after_trainer_initialized() 等方式调用。 - - :param value: Trainer 的 callback 时机。 - :param int every: 触发了多少次,才真正运行一次。 - :param bool once: 是否只在第一次运行后就不再执行了。 - :param Callable filter_fn: 输入参数的应该为 (filter, trainer),其中 filter 对象中包含了 filter.num_called 和 - filter.num_executed 两个变量分别获取当前被调用了多少次,真正执行了多少次。trainer 对象即为当前正在运行的 Trainer 。 - """ self.every = every self.once = once self.filter_fn = filter_fn @@ -456,7 +456,7 @@ class Event: class Filter: def __init__(self, every: Optional[int] = None, once: Optional[bool] = None, filter_fn: Optional[Callable] = None): r""" - 通过该 `Filter` 作为函数修饰器来控制一个函数的实际的运行频率; + 通过该 `Filter` 作为函数修饰器来控制一个函数的实际的运行频率。 :param every: 表示一个函数隔多少次运行一次; :param once: 表示一个函数只运行一次; diff --git a/fastNLP/core/callbacks/callback_manager.py b/fastNLP/core/callbacks/callback_manager.py index 765a0346..60c9b17d 100644 --- a/fastNLP/core/callbacks/callback_manager.py +++ b/fastNLP/core/callbacks/callback_manager.py @@ -2,10 +2,6 @@ import inspect from typing import List, Optional, Dict, Sequence from collections import defaultdict -__all__ = [ - 'CallbackManager' -] - from .callback_event import Event from .callback import Callback from fastNLP.core.log import logger diff --git a/fastNLP/core/callbacks/checkpoint_callback.py b/fastNLP/core/callbacks/checkpoint_callback.py index 625aea09..04ce1fc9 100644 --- a/fastNLP/core/callbacks/checkpoint_callback.py +++ b/fastNLP/core/callbacks/checkpoint_callback.py @@ -13,55 +13,55 @@ from ..utils.exceptions import EarlyStopException class CheckpointCallback(Callback): + """ + 保存 checkpoint 的 callback ,其保存的文件目录以及文件名命名规则如下:: + + - folder/ + - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 + - {save_object}-epoch_{epoch_idx}/ # 满足 every_n_epochs 条件保存的模型 + - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}/ # 满足 every_n_batches 保存的模型 + - {save_object}-last/ # 最后一个 epoch 的保存 + - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-exception_{exception_type}/ # exception时保存。 + - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{monitor}_{monitor_value}/ # 满足topk条件存储文件名 + + model_save_fn 为 None ,则以上每个 folder 中,将生成 fastnlp_model.pkl.tar 文件。若 model_save_fn 不为 None, + 则 fastNLP 将 folder 绝对路径传递给该函数,fastNLP 在该 folder 下不进行模型保存。默认情况下,本 checkpoint 只保存了 model + 的状态;如还需保存 Trainer 的状态以断点重训的话,请使用 ``save_object='trainer'`` 。 + + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param folder: 保存的文件夹,fastNLP 将在该文件下以时间戳创建子文件夹,并在里面保存。因此不同次运行可以将被保存到不同的 + 时间戳文件夹中。如果为 None ,默认使用当前文件夹。 + :param every_n_epochs: 多少个 epoch 保存一次。 + :param every_n_batches: 多少个 batch 保存一次。 + :param last: 如果为 True ,将在每次 epoch 运行结束都保存一次,会覆盖之前的保存。 + :param topk: 保存 monitor 结果 topK 个。 + :param on_exceptions: 在出异常信息时,是否保存。传入需要捕获的异常的类。默认将捕获 EarlyStopException 。 + :param larger_better: monitor 的值是否时越大越好。 + :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 + :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 + 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 + :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 + :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 + fastnlp_evaluate_results.json 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 + :param kwargs: + """ def __init__(self, folder: Optional[Union[str, Path]] = None, every_n_epochs: Optional[int] = None, every_n_batches: Optional[int] = None, last: bool = False, topk: int = 0, on_exceptions: Optional[Union[BaseException, Sequence[BaseException]]] = [EarlyStopException], monitor: Optional[Union[str, Callable]] = None, larger_better: bool = True, only_state_dict: bool = True, model_save_fn: Optional[Callable] = None, save_object: str = 'model', save_evaluate_results=True, **kwargs): - """ - 保存 checkpoint 的 callback ,其保存的文件目录以及文件名命名规则如下:: - - - folder/ - - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 - - {save_object}-epoch_{epoch_idx}/ # 满足 every_n_epochs 条件保存的模型 - - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}/ # 满足 every_n_batches 保存的模型 - - {save_object}-last/ # 最后一个 epoch 的保存 - - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-exception_{exception_type}/ # exception时保存。 - - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{monitor}_{monitor_value}/ # 满足topk条件存储文件名 - - model_save_fn 为 None ,则以上每个 folder 中,将生成 fastnlp_model.pkl.tar 文件。若 model_save_fn 不为 None, - 则 fastNLP 将 folder 绝对路径传递给该函数,fastNLP 在该 folder 下不进行模型保存。默认情况下,本 checkpoint 只保存了 model - 的状态;如还需保存 Trainer 的状态以断点重训的话,请使用 ``save_object='trainer'`` 。 - - :param monitor: 监控的 metric 值。 - - * 为 ``None`` - 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 - * 为 ``str`` - 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 - 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 - * 为 ``Callable`` - 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 - 的 ``monitor`` 值请返回 ``None`` 。 - :param folder: 保存的文件夹,fastNLP 将在该文件下以时间戳创建子文件夹,并在里面保存。因此不同次运行可以将被保存到不同的 - 时间戳文件夹中。如果为 None ,默认使用当前文件夹。 - :param every_n_epochs: 多少个 epoch 保存一次。 - :param every_n_batches: 多少个 batch 保存一次。 - :param last: 如果为 True ,将在每次 epoch 运行结束都保存一次,会覆盖之前的保存。 - :param topk: 保存 monitor 结果 topK 个。 - :param on_exceptions: 在出异常信息时,是否保存。传入需要捕获的异常的类。默认将捕获 EarlyStopException 。 - :param larger_better: monitor 的值是否时越大越好。 - :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 - :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 - 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 - :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 - 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 - 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 - :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 - fastnlp_evaluate_results.json 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 - :param kwargs: - """ super().__init__() if every_n_epochs is not None: if not isinstance(every_n_epochs, int) or every_n_epochs < 1: @@ -133,10 +133,6 @@ class CheckpointCallback(Callback): self.topk_saver.save(trainer, folder_name=folder_name) def on_save_checkpoint(self, trainer) -> Dict: - """ - 保存状态,以便之后可以继续使用 - """ - states = {} states['topk_saver'] = self.topk_saver.state_dict() return states diff --git a/fastNLP/core/callbacks/early_stop_callback.py b/fastNLP/core/callbacks/early_stop_callback.py index c706bf12..db9b6493 100644 --- a/fastNLP/core/callbacks/early_stop_callback.py +++ b/fastNLP/core/callbacks/early_stop_callback.py @@ -9,22 +9,23 @@ from fastNLP.core.utils.exceptions import EarlyStopException class EarlyStopCallback(HasMonitorCallback): - def __init__(self, monitor:Union[str, Callable]=None, larger_better:bool=True, patience:int=10): - """ + """ + 用于 early stop 的 callback 。当监控的结果连续多少次没有变好边 raise 一个 EarlyStopException 。 - :param monitor: 监控的 metric 值。 + :param monitor: 监控的 metric 值。 - * 为 ``None`` - 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 - * 为 ``str`` - 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 - 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 - * 为 ``Callable`` - 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 - 的 ``monitor`` 值请返回 ``None`` 。 - :param larger_better: monitor 的值是否是越大越好。 - :param patience: 多少次 evaluate 不没有提升就停止。 - """ + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: monitor 的值是否是越大越好。 + :param patience: 多少次 evaluate 不没有提升就停止。 + """ + def __init__(self, monitor:Union[str, Callable]=None, larger_better:bool=True, patience:int=10): super(EarlyStopCallback, self).__init__(monitor=monitor, larger_better=larger_better, must_have_monitor=True) self.wait = 0 self.patience = patience @@ -42,7 +43,7 @@ class EarlyStopCallback(HasMonitorCallback): # 当是 step evaluate 的时候,下一步执行的就是这个, 所以在这里检查。 if self.wait >= self.patience: raise EarlyStopException(f"After {self.wait} validations, no improvement for " - f"metric `{self._real_monitor}`") + f"metric `{self._real_monitor}`(best value: {self.monitor_value})") def on_train_epoch_begin(self, trainer): # 当是 epoch evaluate 的时候,下一步执行的就是这个, 所以在这里检查。 diff --git a/fastNLP/core/callbacks/has_monitor_callback.py b/fastNLP/core/callbacks/has_monitor_callback.py index e5406d78..ac09e266 100644 --- a/fastNLP/core/callbacks/has_monitor_callback.py +++ b/fastNLP/core/callbacks/has_monitor_callback.py @@ -16,11 +16,6 @@ from fastNLP.core.utils.utils import _check_valid_parameters_number class CanItemDataType(ABC): - """ - 检测可以进行传输的对象。 - - """ - @classmethod def __subclasshook__(cls, subclass: Any) -> Union[bool, Any]: if cls is CanItemDataType: @@ -30,15 +25,22 @@ class CanItemDataType(ABC): class ResultsMonitor: + """ + 可用于监控某个数值,并通过 is_better_results() 等接口实现检测结果是否变得更好了。 + + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: monitor 是否时越大越好 + """ def __init__(self, monitor:Union[Callback, str], larger_better:bool=True): - """ - 可用于监控某个数值,并通过 is_better_results() 等接口实现检测结果是否变得更好了。 - - :param monitor: 监控的 metric 值。如果在 evaluation 结果中没有找到完全一致的名称,将使用 最长公共字符串算法 找到最匹配 - 的那个作为 monitor 。如果为 None,将尝试使用 Trainer 设置的 monitor 。也可以传入一个函数,接受参数为 evaluation 的结 - 果(字典类型),返回一个 float 值作为 monitor 的结果,如果当前结果中没有相关的 monitor 值请返回 None 。 - :param larger_better: monitor 是否时越大越好 - """ self.set_monitor(monitor, larger_better) def set_monitor(self, monitor, larger_better): @@ -66,9 +68,9 @@ class ResultsMonitor: def get_monitor_value(self, results:Dict)->Union[float, None]: """ - 获取 monitor 的值,如果 monitor 没有直接找到,会尝试使用匹配的方式寻找,并把匹配到的设置到 self._real_monitor 属性上。 + 获取 monitor 的值,如果 monitor 没有直接找到,会尝试使用 最长公共字符串算法 匹配的方式寻找。 - :param results: + :param results: 评测结果。 :return: 如果为 None ,表明此次没有找到合适的monitor """ if len(results) == 0 or self.monitor is None: @@ -113,7 +115,7 @@ class ResultsMonitor: """ 检测给定的 results 是否比上一次更好,如果本次 results 中没有找到相关的monitor 返回 False。 - :param results: on_valid_ends() 接口中传入的 evaluation 结果。 + :param results: evaluation 结果。 :param keep_if_better: 当返回为 True 时,是否保存到 self.monitor_value 中。 :return: """ @@ -166,24 +168,24 @@ class ResultsMonitor: class HasMonitorCallback(ResultsMonitor, Callback): + """ + 该 callback 不直接进行使用,作为其它相关 callback 的父类使用,如果 callback 有使用 monitor 可以继承该函数里面实现了 + (1)判断monitor合法性;(2)在需要时, 根据trainer的monitor设置自己的monitor名称。 + + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: monitor 是否时越大越好 + :param must_have_monitor: 这个 callback 是否必须有 monitor 设置。如果设置为 True ,且没检测到设置 monitor 会报错。 + """ def __init__(self, monitor, larger_better, must_have_monitor=False): - """ - 该 callback 不直接进行使用,作为其它相关 callback 的父类使用,如果 callback 有使用 monitor 可以继承该函数里面实现了 - (1)判断monitor合法性;(2)在需要时, 根据trainer的monitor设置自己的monitor名称。 - - :param monitor: 监控的 metric 值。 - - * 为 ``None`` - 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 - * 为 ``str`` - 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 - 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 - * 为 ``Callable`` - 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 - 的 ``monitor`` 值请返回 ``None`` 。 - :param larger_better: monitor 是否时越大越好 - :param must_have_monitor: 这个 callback 是否必须有 monitor 设置。如果设置为 True ,且没检测到设置 monitor 会报错。 - """ super().__init__(monitor, larger_better) self.must_have_monitor = must_have_monitor @@ -212,16 +214,23 @@ class HasMonitorCallback(ResultsMonitor, Callback): class ExecuteOnceBetterMonitor(HasMonitorCallback): + """ + 当监控的 monitor 结果更好的时候,调用 execute_fn 函数。 + + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: monitor 是否时越大越好 + :param execute_fn: 一个可执行的函数,不接受任何参数,不反回值。在 monitor 取得更好结果的时候会调用。 + """ def __init__(self, monitor, larger_better, execute_fn): - """ - 当监控的 monitor 结果更好的时候,调用 execute_fn 函数。 - - :param monitor: 监控的 metric 值。如果在 evaluation 结果中没有找到完全一致的名称,将使用 最长公共字符串算法 找到最匹配 - 的那个作为 monitor 。如果为 None,将尝试使用 Trainer 设置的 monitor 。也可以传入一个函数,接受参数为 evaluation 的结 - 果(字典类型),返回一个 float 值作为 monitor 的结果,如果当前结果中没有相关的 monitor 值请返回 None 。 - :param larger_better: monitor 是否时越大越好 - :param execute_fn: 一个可执行的函数,不接受任何参数,不反回值。在 monitor 取得更好结果的时候会调用。 - """ super().__init__(monitor, larger_better, must_have_monitor=True) _check_valid_parameters_number(execute_fn, expected_params=[], fn_name='execute_fn') self.execute_fn = execute_fn diff --git a/fastNLP/core/callbacks/load_best_model_callback.py b/fastNLP/core/callbacks/load_best_model_callback.py index 5083f5c3..94e6ed90 100644 --- a/fastNLP/core/callbacks/load_best_model_callback.py +++ b/fastNLP/core/callbacks/load_best_model_callback.py @@ -14,34 +14,34 @@ from fastNLP.envs import all_rank_call_context class LoadBestModelCallback(HasMonitorCallback): + """ + 保存最佳的 monitor 值最佳的模型,并在训练结束的时候重新加载模型,默认会在加载之后删除权重文件。仅在训练正常结束的时候才能加载 + 最好的模型。 + + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: 该 metric 值是否是越大越好。 + :param save_folder: 保存的文件夹,如果为空,则保存在内存中。不为空,则保存一份权重到文件中,当为多机训练,且本值不为空时,请确保 + 不同的机器均可访问当该路径。当 model_save_fn 不为 None 时该值一定不能为空。 + :param only_state_dict: 是否只保存模型的参数。当 model_save_fn 不为空时,该值无效。 + :param model_save_fn: 保存 model 的函数,与 model_load_fn 必须同时不为空。本函数的输入为一个已经创建好的文件夹,没有输出, + 请在函数内完成对模型的保存。 + :param model_load_fn: 加载 model 的函数,与 model_save_fn 必须同时不为空。本函数的输入为一个已经创建好的文件夹,没有输出, + 请在函数内完成对模型的加载。 + :param delete_after_train: 在训练结束后是否删掉模型。 + """ def __init__(self, monitor:Union[str, Callable]=None, larger_better:bool = True, only_state_dict:bool = True, save_folder:Optional[str] = None, model_save_fn:Optional[Callable] = None, model_load_fn:Optional[Callable] = None, delete_after_train:bool = True): - """ - 保存最佳的 monitor 值最佳的模型,并在训练结束的时候重新加载模型,默认会在加载之后删除权重文件。仅在训练正常结束的时候才能加载 - 最好的模型。 - - :param monitor: 监控的 metric 值。 - - * 为 ``None`` - 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 - * 为 ``str`` - 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 - 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 - * 为 ``Callable`` - 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 - 的 ``monitor`` 值请返回 ``None`` 。 - :param larger_better: 该 metric 值是否是越大越好。 - :param save_folder: 保存的文件夹,如果为空,则保存在内存中。不为空,则保存一份权重到文件中,当为多机训练,且本值不为空时,请确保 - 不同的机器均可访问当该路径。当 model_save_fn 不为 None 时该值一定不能为空。 - :param only_state_dict: 是否只保存模型的参数。当 model_save_fn 不为空时,该值无效。 - :param model_save_fn: 保存 model 的函数,与 model_load_fn 必须同时不为空。本函数的输入为一个已经创建好的文件夹,没有输出, - 请在函数内完成对模型的保存。 - :param model_load_fn: 加载 model 的函数,与 model_save_fn 必须同时不为空。本函数的输入为一个已经创建好的文件夹,没有输出, - 请在函数内完成对模型的加载。 - :param delete_after_train: 在训练结束后是否删掉模型。 - """ super().__init__(monitor=monitor, larger_better=larger_better, must_have_monitor=True) if model_load_fn is not None: assert callable(model_load_fn), "`model_load_fn` must be a callable object." diff --git a/fastNLP/core/callbacks/lr_scheduler_callback.py b/fastNLP/core/callbacks/lr_scheduler_callback.py index ba496b5e..37d089bd 100644 --- a/fastNLP/core/callbacks/lr_scheduler_callback.py +++ b/fastNLP/core/callbacks/lr_scheduler_callback.py @@ -6,14 +6,14 @@ __all__ = [ class LRSchedCallback(Callback): - def __init__(self, scheduler, step_on:str='batch'): - """ - 根据 step_on 参数在合适的时机调用 scheduler 的 step 函数。 + """ + 根据 step_on 参数在合适的时机调用 scheduler 的 step 函数。 - :param scheduler: 实现了 step() 函数的对象 - :param step_on: 可选 ['batch', 'epoch'] 表示在何时调用 scheduler 的 step 函数。如果为 batch 的话在每次更新参数 - 之前调用;如果为 epoch 则是在一个 epoch 运行结束后调用。 - """ + :param scheduler: 实现了 step() 函数的对象 + :param step_on: 可选 ['batch', 'epoch'] 表示在何时调用 scheduler 的 step 函数。如果为 batch 的话在每次更新参数 + 之前调用;如果为 epoch 则是在一个 epoch 运行结束后调用。 + """ + def __init__(self, scheduler, step_on:str='batch'): assert hasattr(scheduler, 'step') and callable(scheduler.step), "The scheduler object should have a " \ "step function." self.scheduler = scheduler diff --git a/fastNLP/core/callbacks/more_evaluate_callback.py b/fastNLP/core/callbacks/more_evaluate_callback.py index 896f8865..2b2aa16e 100644 --- a/fastNLP/core/callbacks/more_evaluate_callback.py +++ b/fastNLP/core/callbacks/more_evaluate_callback.py @@ -11,6 +11,67 @@ from .topk_saver import TopkSaver class MoreEvaluateCallback(HasMonitorCallback): + """ + 当评测时需要调用不同的 evaluate_fn (例如在大部分生成任务中,一般使用训练 loss 作为训练过程中的 evaluate ;但同时在训练到 + 一定 epoch 数量之后,会让 model 生成的完整的数据评测 bleu 等。此刻就可能需要两种不同的 evaluate_fn ),只使用 Trainer + 无法满足需求,可以通过调用本 callback 进行。如果需要根据本 callback 中的评测结果进行模型保存,请传入 topk 以及 + topk_monitor 等相关参数。可以通过 evaluate_every 或 watch_monitor 控制触发进行 evaluate 的条件。 + + 如果设置了 evaluate 结果更好就保存的话,将按如下文件结构进行保存:: + + - folder/ + - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 + - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{topk_monitor}_{monitor_value}/ # 满足topk条件存储文件名 + + :param dataloaders: 需要评估的数据 + :param metrics: 使用的 metrics 。 + :param evaluate_every: 用来控制 ``Trainer`` 内部的 ``Evaluator`` 验证的频率,其可以为负数、正数或者函数: + + 1. 为负数时表示每隔几个 ``epoch`` evaluate 一次; + 2. 为正数则表示每隔几个 ``batch`` evaluate 一次; + 3. 为函数时表示用户自己传入的用于控制 evaluate 的频率的函数,该函数的应该接受当前 trainer 对象作为参数,并 + 返回一个 bool 值,返回为 True 说明需要进行 evaluate ;将在每个 ``batch`` 结束后调用该函数判断是否需要 evaluate; + + .. note:: + + 如果参数 ``evaluate_every`` 为函数,其应当类似: + + >>> def my_evaluate_every(trainer) -> bool: + ... if (trainer.global_forward_batches+1) % 1000 == 0: + ... return True + ... else: + ... return False + + 该函数表示当每经过 1000 个 batch,``Trainer`` 中内置的 ``Evaluator`` 就会验证一次; + + 另一个需要注意的事情在于该函数会在每一次 batch 的结尾进行调用,当该函数返回 ``True`` 时,``Evaluator`` 才会进行验证; + :param watch_monitor: 这个值用来表示监控的 Trainer 中的 evaluate 结果的,当该值不为 None ,evaluate_every 失效。本参数的 + 意义是,当检测到 Trainer 中 evaluate results 的 {watch_monitor} 的结果更好时,则进行一次 evaluate 。该参数有两种 + 取值: (1) str 类型,监控的 metric 值。如果在 evaluation 结果中没有找到完全一致的名称,将使用 最长公共字符串算法 找到最 + 匹配的那个作为 monitor ; (2) 也可以传入一个函数,接受参数为 evaluation 的结果(字典类型),返回一个 float 值作为 monitor + 的结果,如果当前结果中没有相关的monitor 值请返回 None 。 + :param watch_monitor_larger_better: watch_monitor 是否越大越好。 + :param evaluate_fn: 用来控制 `Evaluator` 在评测的前向传播过程中是调用哪一个函数,例如是 `model.evaluate_step` 还是 + `model.forward`;(1) 如果该值是 None,那么我们会默认使用 `evaluate_step` 当做前向传播的函数,如果在模型中没有 + 找到该方法,则使用 `model.forward` 函数;(2) 如果为 str 类型,则尝试从 model 中寻找该方法,找不到则报错。 + :param num_eval_sanity_batch: 在初始化 Evaluator 后运行多少个 sanity check 的 batch ,检测一下。 + :param topk: 如果需要根据当前 callback 中的 evaluate 结果保存模型或 Trainer ,可以通过设置 tokp 实现。(1)为 -1 表示每次 + evaluate 后都保存;(2)为 0 (默认),表示不保存;(3)为整数,表示保存性能最 topk 个。 + :param topk_monitor: 如果需要根据当前 callback 中的 evaluate 结果保存。这个参数是指在当前 callback 中的 evaluate 结果寻找 + :param topk_larger_better: topk_monitor 的值是否时越大越好。 + :param folder: 保存的文件夹,fastNLP 将在该文件下以时间戳创建子文件夹,并在里面保存。因此不同次运行可以将被保存到不同的 + 时间戳文件夹中。如果为 None ,默认使用当前文件夹。 + :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 + :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 + :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 + 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 + :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 + ``fastnlp_evaluate_results.json`` 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 + :param save_kwargs: dict。更多的保存相关的参数。 + :param kwargs: 其它与 Evaluator 相关的初始化参数,如果不传入,将从 Trainer 中获取。 + """ def __init__(self, dataloaders, metrics:Dict, evaluate_every:Optional[Union[int, Callable]]=-1, watch_monitor:Union[str, Callable]=None, watch_monitor_larger_better:bool=True, evaluate_fn=None, num_eval_sanity_batch=2, @@ -18,48 +79,6 @@ class MoreEvaluateCallback(HasMonitorCallback): folder=None, only_state_dict=True, save_object='model', model_save_fn=None, save_evaluate_results=True, save_kwargs=None, **kwargs): - """ - 当评测时需要调用不同的 evaluate_fn (例如在大部分生成任务中,一般使用训练 loss 作为训练过程中的 evaluate ;但同时在训练到 - 一定 epoch 数量之后,会让 model 生成的完整的数据评测 bleu 等。此刻就可能需要两种不同的 evaluate_fn ),只使用 Trainer - 无法满足需求,可以通过调用本 callback 进行。如果需要根据本 callback 中的评测结果进行模型保存,请传入 topk 以及 - topk_monitor 等相关参数。可以通过 evaluate_every 或 watch_monitor 控制触发进行 evaluate 的条件。 - - 如果设置了 evaluate 结果更好就保存的话,将按如下文件结构进行保存:: - - - folder/ - - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 - - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{topk_monitor}_{monitor_value}/ # 满足topk条件存储文件名 - - :param dataloaders: 需要评估的数据 - :param metrics: 使用的 metrics 。 - :param evaluate_every: 可以为负数、正数和函数;(1) 为负整数时表示每隔几个 epoch evaluate 一次;(2) 为正整数则表示每隔几个 batch - evaluate 一次;(3) 为函数时表示用户自己传入的用于控制 evaluate 的频率的函数,该函数的应该接受 trainer 对象作为参数,并返回 - 一个 bool 值,返回为 True 说明需要进行 evaluate ;将在每个 batch 结束后调用该函数判断是否需要 evaluate 。 - :param watch_monitor: 这个值用来表示监控的 Trainer 中的 evaluate 结果的,当该值不为 None ,evaluate_every 失效。本参数的 - 意义是,当检测到 Trainer 中 evaluate results 的 {watch_monitor} 的结果更好时,则进行一次 evaluate 。该参数有两种 - 取值: (1) str 类型,监控的 metric 值。如果在 evaluation 结果中没有找到完全一致的名称,将使用 最长公共字符串算法 找到最 - 匹配的那个作为 monitor ; (2) 也可以传入一个函数,接受参数为 evaluation 的结果(字典类型),返回一个 float 值作为 monitor - 的结果,如果当前结果中没有相关的monitor 值请返回 None 。 - :param watch_monitor_larger_better: watch_monitor 是否越大越好。 - :param evaluate_fn: 用来控制 `Evaluator` 在评测的前向传播过程中是调用哪一个函数,例如是 `model.evaluate_step` 还是 - `model.forward`;(1) 如果该值是 None,那么我们会默认使用 `evaluate_step` 当做前向传播的函数,如果在模型中没有 - 找到该方法,则使用 `model.forward` 函数;(2) 如果为 str 类型,则尝试从 model 中寻找该方法,找不到则报错。 - :param num_eval_sanity_batch: 在初始化 Evaluator 后运行多少个 sanity check 的 batch ,检测一下。 - :param topk: 如果需要根据当前 callback 中的 evaluate 结果保存模型或 Trainer ,可以通过设置 tokp 实现。(1)为 -1 表示每次 - evaluate 后都保存;(2)为 0 (默认),表示不保存;(3)为整数,表示保存性能最 topk 个。 - :param topk_monitor: 如果需要根据当前 callback 中的 evaluate 结果保存。这个参数是指在当前 callback 中的 evaluate 结果寻找 - :param topk_larger_better: topk_monitor 的值是否时越大越好。 - :param folder: 保存的文件夹,fastNLP 将在该文件下以时间戳创建子文件夹,并在里面保存。因此不同次运行可以将被保存到不同的 - 时间戳文件夹中。如果为 None ,默认使用当前文件夹。 - :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 - :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 trainer+model 还是 只是model 。 - :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 - 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 - :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 - fastnlp_evaluate_results.json 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 - :param save_kwargs: dict。更多的保存相关的参数。 - :param kwargs: 其它与 Evaluator 相关的初始化参数,如果不传入,将从 Trainer 中获取。请特别留意 evaluate_fn 的设置。 - """ super(MoreEvaluateCallback, self).__init__(watch_monitor, watch_monitor_larger_better, must_have_monitor=False) diff --git a/fastNLP/core/callbacks/progress_callback.py b/fastNLP/core/callbacks/progress_callback.py index 24eda36e..2618431f 100644 --- a/fastNLP/core/callbacks/progress_callback.py +++ b/fastNLP/core/callbacks/progress_callback.py @@ -39,25 +39,27 @@ def choose_progress_callback(progress_bar: Union[str, ProgressCallback]) -> Prog class RichCallback(ProgressCallback): + """ + 在训练过程中打印 rich progress bar 的 callback 。在 Trainer 中,默认就会使用这个 callback 来显示进度。如果需要定制这个 Callback 的 + 参数,请通过实例化本 Callback 并传入到 Trainer 中实现。 + + :param print_every: 多少个 batch 更新一次显示。 + :param loss_round_ndigit: 显示的 loss 保留多少位有效数字 + :param monitor: 监控的 metric 值。当检测到这个key的结果更好时,会打印出不同的颜色进行提示。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: 是否是 monitor 的结果越大越好。 + :param format_json: 是否格式化 json 再打印 + """ def __init__(self, print_every:int = 1, loss_round_ndigit:int = 6, monitor:str=None, larger_better:bool=True, format_json=True): - """ - - :param print_every: 多少个 batch 更新一次显示。 - :param loss_round_ndigit: 显示的 loss 保留多少位有效数字 - :param monitor: 监控的 metric 值。当检测到这个key的结果更好时,会打印出不同的颜色进行提示。 - - * 为 ``None`` - 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 - * 为 ``str`` - 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 - 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 - * 为 ``Callable`` - 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 - 的 ``monitor`` 值请返回 ``None`` 。 - :param larger_better: 是否是 monitor 的结果越大越好。 - :param format_json: 是否格式化 json 再打印 - """ super().__init__(monitor=monitor, larger_better=larger_better, must_have_monitor=False) self.print_every = print_every self.progress_bar = f_rich_progress diff --git a/fastNLP/core/callbacks/topk_saver.py b/fastNLP/core/callbacks/topk_saver.py index c629e9de..9b02e1b2 100644 --- a/fastNLP/core/callbacks/topk_saver.py +++ b/fastNLP/core/callbacks/topk_saver.py @@ -16,22 +16,24 @@ from .has_monitor_callback import ResultsMonitor class Saver: + """ + 执行保存的对象。保存的文件组织结构为:: + + - folder # 当前初始化的参数 + - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 + - folder_name # 由 save() 调用时传入。 + + :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 + :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 + :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 + :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 + 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 + :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 + """ def __init__(self, folder:str=None, save_object:str='model', only_state_dict:bool=True, model_save_fn:Callable=None, **kwargs): - """ - 执行保存的对象。保存的文件组织结构为:: - - - folder # 当前初始化的参数 - - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 - - folder_name # 由 save() 调用时传入。 - - :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 - :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 trainer+model 还是 只是model 。 - :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 - :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 - 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 - :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 - """ if folder is None: folder = Path.cwd().absolute() logger.info(f"Parameter `folder` is None, and we will use {folder} to save and load your model.") @@ -79,8 +81,8 @@ class Saver: """ 以 json 格式保存 results 到 path 中 - :param results: - :param path: + :param results: 一般是评测后的结果。 + :param path: 保存的文件名 :return: """ with open(path, 'w', encoding='utf8') as f: @@ -117,12 +119,12 @@ class Saver: class TopkQueue: - def __init__(self, topk): - """ - 用于维护处于 topk 的 key, value 对。 + """ + 用于维护处于 topk 的 key, value 对。 - :param int topk: 整数,-1 表示所有数据都是 topk 的; 如果是 0, 表示没有任何数据是满足 topk 的。 - """ + :param int topk: 整数,-1 表示所有数据都是 topk 的; 如果是 0, 表示没有任何数据是满足 topk 的。 + """ + def __init__(self, topk): assert isinstance(topk, int) self.topk = topk self.topk_dict = {} # 其中 key 为保存的内容, value 是对应的性能。 @@ -170,31 +172,39 @@ class TopkQueue: class TopkSaver(ResultsMonitor, Saver): + """ + 用来识别 topk 模型并保存,也可以仅当一个保存 Saver 使用。保存路径为:: + + - folder/ + - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 + - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{topk_monitor}_{monitor_value}/ # 满足topk条件存储文件名 + + :param topk: 保存 topk 多少的模型,-1 为保存所有模型;0 为都不保存;大于 0 的数为保存 topk 个。 + :param monitor: 监控的 metric 值。 + + * 为 ``None`` + 将尝试使用 :class:`~fastNLP.Trainer` 中设置 `monitor` 值(如果有设置)。 + * 为 ``str`` + 尝试直接使用该名称从 ``evaluation`` 结果中寻找,如果在 ``evaluation`` 结果中没有找到完全一致的名称,将 + 使用 最长公共字符串算法 从 ``evaluation`` 结果中找到最匹配的那个作为 ``monitor`` 。 + * 为 ``Callable`` + 接受参数为 ``evaluation`` 的结果(字典类型),返回一个 ``float`` 值作为 ``monitor`` 的结果,如果当前结果中没有相关 + 的 ``monitor`` 值请返回 ``None`` 。 + :param larger_better: 该 monitor 是否越大越好。 + :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 + :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 + :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 + :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 + 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 + :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 + ``fastnlp_evaluate_results.json`` 文件,记录当前的 metric results 。仅在设置了 topk 的场景下有用,默认为 True 。 + :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 + """ def __init__(self, topk:int=0, monitor:str=None, larger_better:bool=True, folder:str=None, save_object:str='model', only_state_dict:bool=True, model_save_fn:Callable=None, save_evaluate_results:bool=True, **kwargs): - """ - 用来识别 topk 模型并保存,也可以仅当一个保存 Saver 使用。保存路径为:: - - - folder/ - - YYYY-mm-dd-HH_MM_SS_fffff/ # 自动根据当前脚本的启动时间创建的 - - {save_object}-epoch_{epoch_idx}-batch_{global_batch_idx}-{topk_monitor}_{monitor_value}/ # 满足topk条件存储文件名 - - :param topk: 保存 topk 多少的模型,-1 为保存所有模型;0 为都不保存;大于 0 的数为保存 topk 个。 - :param monitor: 监控哪个指标判断是否是 topk 的。监控的 metric 值。如果在 evaluation 结果中没有找到完全一致的名称,将使用 - 最长公共字符串算法 找到最匹配的那个作为 monitor 。如果为 None,将尝试使用 Trainer 设置的 monitor 。也可以传入一个函数, - 接受参数为 evaluation 的结果(字典类型),返回一个 float 值作为 monitor 的结果,如果当前结果中没有相关的 monitor 值请 - 返回 None 。 - :param larger_better: 该 monitor 是否越大越好。 - :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 - :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 trainer+model 还是 只是model 。 - :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 - :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 - 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 - :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 - fastnlp_evaluate_results.json 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 - :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 - """ ResultsMonitor.__init__(self, monitor, larger_better) Saver.__init__(self, folder, save_object, only_state_dict, model_save_fn, **kwargs) diff --git a/fastNLP/core/callbacks/torch_callbacks/torch_grad_clip_callback.py b/fastNLP/core/callbacks/torch_callbacks/torch_grad_clip_callback.py index cc0e1e98..40a03b89 100644 --- a/fastNLP/core/callbacks/torch_callbacks/torch_grad_clip_callback.py +++ b/fastNLP/core/callbacks/torch_callbacks/torch_grad_clip_callback.py @@ -1,25 +1,26 @@ __all__ = [ 'TorchGradClipCallback' ] +from typing import Union, List from ..callback import Callback class TorchGradClipCallback(Callback): - def __init__(self, clip_value=1, clip_type='norm', parameters=None): - r""" - 在每次 optimizer update 之前将 parameter 进行 clip + r""" + 在每次 optimizer update 之前将 parameter 进行 clip 。 - :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 - :param str clip_type: 支持'norm', 'value'两种: + :param clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 + :param clip_type: 支持'norm', 'value'两种: - 1. 'norm', 将gradient的norm rescale到[-clip_value, clip_value] - 2. 'value', 将gradient限制在[-clip_value, clip_value], - 小于-clip_value的gradient被赋值为-clip_value; - 大于clip_value的gradient被赋值为clip_value. + 1. 'norm', 将gradient的norm rescale到[-clip_value, clip_value] + 2. 'value', 将gradient限制在[-clip_value, clip_value], + 小于-clip_value的gradient被赋值为-clip_value;大于clip_value的gradient被赋值为clip_value. - :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。 - 如果为None则默认对 Trainer 的 optimizers 中所有参数进行梯度裁剪。 - """ + :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。 + 如果为None则默认对 Trainer 的 optimizers 中所有参数进行梯度裁剪。 + """ + def __init__(self, clip_value:int=1, clip_type:str='norm', + parameters:Union["torch.Tensor", List["torch.Tensor"]]=None): super().__init__() from torch import nn diff --git a/fastNLP/core/callbacks/torch_callbacks/torch_lr_sched_callback.py b/fastNLP/core/callbacks/torch_callbacks/torch_lr_sched_callback.py index 3d428d47..006a39c0 100644 --- a/fastNLP/core/callbacks/torch_callbacks/torch_lr_sched_callback.py +++ b/fastNLP/core/callbacks/torch_callbacks/torch_lr_sched_callback.py @@ -2,21 +2,23 @@ __all__ = [ 'TorchWarmupCallback' ] import math +from typing import Union from ..callback import Callback class TorchWarmupCallback(Callback): - def __init__(self, warmup=0.1, schedule='constant'): - r""" - 调整 learning rate 的 callback 。仅在实际发生参数更新的情况下 - - :param int,float warmup: 如果warmup为int,则在该step之前,learning rate根据schedule的策略变化; 如果warmup为float, - 如0.1, 则前10%的step是按照schedule策略调整learning rate。 - :param str schedule: 以哪种方式调整。 - linear: 前warmup的step上升到指定的learning rate(从Trainer中的optimizer处获取的), 后warmup的step下降到0; - constant前warmup的step上升到指定learning rate,后面的step保持learning rate. - """ + r""" + 调整 learning rate 的 callback 。 + + :param warmup: 如果warmup为int,则在该step之前,learning rate根据schedule的策略变化; 如果warmup为float, + 如0.1, 则前10%的step是按照schedule策略调整learning rate。 + :param schedule: 以哪种方式调整。 + + 1. linear: 前warmup的step上升到指定的learning rate(从Trainer中的optimizer处获取的), 后warmup的step下降到0; + 2. constant前warmup的step上升到指定learning rate,后面的step保持learning rate. + """ + def __init__(self, warmup:Union[int, float]=0.1, schedule:str='constant'): super().__init__() self.warmup = max(warmup, 0.) diff --git a/fastNLP/core/collators/collator.py b/fastNLP/core/collators/collator.py index aef9de4c..2978de89 100644 --- a/fastNLP/core/collators/collator.py +++ b/fastNLP/core/collators/collator.py @@ -82,16 +82,26 @@ def _get_backend() -> str: class Collator: - def __init__(self, backend='auto'): - """ - 用于 pad 数据的对象。会自动将所有能够 pad (由 fastNLP 根据数据判定能否 pad )的数据都进行 pad 操作,默认 pad 的值为 0。 - 可使用 set_pad() 函数调整。如果有些 field 不想输出,可以使用 set_ignore() 函数进行设置。Collator 在第一次进行 pad 的 - 时候自动根据设置以及数据情况,为每个 field 获取一个 padder ,在之后的每次调用中,都将使用对应的 Padder 给对应的 field 。 + """ + 用于 pad 数据的对象。会自动将所有能够 pad (由 fastNLP 根据数据判定能否 pad )的数据都进行 pad 操作,默认 pad 的值为 0。 + 哦安定一个 field 是否可以 pad 的方式为:(1)当前这个 field 是否所有对象都是一样的数据类型;(因此,如果某 field 的数据有些是float + 有些是 int 将知道该 field 被判定为不可 pad 类型。)(2)当前这个 field 是否每个 sample 都具有一样的深度;(因此,例如有个 field 的 + 数据转为 batch 类型后为 [1, [1,2]], 会被判定为不可 pad ,因为第一个 sample 与 第二个 sample 深度不同)(3)当前这个 field 的类 + 型是否是可以 pad (例如 str 类型的数据)。可以通过设置 logger.setLevel('debug') 来打印是判定不可 pad 的原因。 - :param backend: 对于可以 pad 的 field,使用哪种 tensor,支持 ['torch','jittor','paddle','numpy','raw', auto, None]。 - 若为 'auto' ,则在进行 pad 的时候会根据调用的环境决定其 backend 。该参数对不能进行 pad 的数据没用影响,不能 pad - 的数据返回一定是 list 。 - """ + todo 补充 code example 。 + + 如果需要将某个本可以 pad 的 field 设置为不可 pad ,则可以通过 :meth:`~fastNLP.Collator.set_pad` 的 pad_val 设置为 None 实现。 + 如果需要某些 field 不要包含在 pad 之后的结果中,可以使用 :meth:`~fastNLP.Collator.set_ignore` 进行设置。 + + Collator 在第一次进行 pad 的时候自动根据设置以及数据情况,为每个 field 获取一个 padder ,在之后的每次调用中,都将使用对应 + 的 Padder 给对应的 field 。 + + :param backend: 对于可以 pad 的 field,使用哪种 tensor,支持 ['torch','jittor','paddle','numpy','raw', auto, None]。 + 若为 'auto' ,则在进行 pad 的时候会根据调用的环境决定其 backend 。该参数对不能进行 pad 的数据没用影响,不能 pad + 的数据返回一定是 list 。 + """ + def __init__(self, backend='auto'): self.unpack_batch_func = None self.pack_batch_func = None self.ignore_fields = set() @@ -264,9 +274,8 @@ class Collator: def set_ignore(self, *field_names) -> "Collator": """ 如果有的内容不希望输出,可以在此处进行设置,被设置的 field 将在 batch 的输出中被忽略。 - Example:: - collator.set_ignore('field1', 'field2') + >>> collator = Collator().set_ignore('field1', 'field2') :param field_names: 需要忽略的 field 的名称。如果 Dataset 的 __getitem__ 方法返回的是 dict 类型的,则可以直接使用对应的 field 的 key 来表示,如果是 nested 的 dict,可以使用元组来表示,例如 {'a': {'b': 1}} 中的使用 ('a', 'b'); 如果 diff --git a/fastNLP/core/collators/packer_unpacker.py b/fastNLP/core/collators/packer_unpacker.py index 033cfca5..2b78ea0a 100644 --- a/fastNLP/core/collators/packer_unpacker.py +++ b/fastNLP/core/collators/packer_unpacker.py @@ -9,9 +9,9 @@ class MappingPackerUnpacker: """ 将 Sequence[Mapping] 转为 Dict 。例如 [{'a': [1, 2], 'b': 1}, {'a': [3], 'b': 2}] -> {'a': [[1, 2], [3]], 'b': [1, 2]} - :param batch: - :param ignore_fields: - :param input_fields: + :param batch: 需要 unpack 的 batch 数据。 + :param ignore_fields: 需要忽略的 field 。 + :param input_fields: 需要设置为 input 的 field 。 :return: """ dict_batch = defaultdict(list) @@ -29,13 +29,13 @@ class MappingPackerUnpacker: class NestedMappingPackerUnpacker: @staticmethod - def unpack_batch(batch:Sequence[Mapping], ignore_fields:set, input_fields:Dict): + def unpack_batch(batch:Sequence[Mapping], ignore_fields:set, input_fields:Dict)->Dict: """ 将 nested 的 dict 中的内容展开到一个 flat dict 中 - :param batch: + :param batch: 需要 unpack 的 batch 数据。 :param ignore_fields: 需要忽略的 field 。 - :param input_fields: 不需要继续往下衍射的 + :param input_fields: 需要设置为 input 的 field 。 :return: """ dict_batch = defaultdict(list) @@ -72,8 +72,9 @@ class SequencePackerUnpacker: """ 将 Sequence[Sequence] 转为 Mapping 。例如 [[[1, 2], 2], [[3], 2]] -> {'_0': [[1, 2], [3]], '_1': [1, 2]} - :param batch: - :param ignore_fields: 需要忽略的field + :param batch: 需要 unpack 的 batch 数据。 + :param ignore_fields: 需要忽略的 field 。 + :param input_fields: 需要设置为 input 的 field 。 :return: """ dict_batch = defaultdict(list) diff --git a/fastNLP/core/collators/padders/jittor_padder.py b/fastNLP/core/collators/padders/jittor_padder.py index 6c30d835..5fcc469b 100644 --- a/fastNLP/core/collators/padders/jittor_padder.py +++ b/fastNLP/core/collators/padders/jittor_padder.py @@ -90,7 +90,7 @@ class JittorNumberPadder(Padder): super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): return jittor.Var(np.array(batch_field, dtype=dtype)) @@ -107,7 +107,7 @@ class JittorSequencePadder(Padder): super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): tensor = get_padded_jittor_tensor(batch_field, dtype=dtype, pad_val=pad_val) return tensor @@ -125,7 +125,7 @@ class JittorTensorPadder(Padder): super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): try: if not isinstance(batch_field[0], jittor.Var): batch_field = [jittor.Var(np.array(field.tolist(), dtype=dtype)) for field in batch_field] diff --git a/fastNLP/core/collators/padders/numpy_padder.py b/fastNLP/core/collators/padders/numpy_padder.py index 1113c91a..5f2d70a9 100644 --- a/fastNLP/core/collators/padders/numpy_padder.py +++ b/fastNLP/core/collators/padders/numpy_padder.py @@ -30,53 +30,65 @@ def _get_dtype(ele_dtype, dtype, class_name): class NumpyNumberPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 可以将形如 [1, 2, 3] 这类的数据转为 np.array([1, 2, 3]) + """ + 可以将形如 [1, 2, 3] 这类的数据转为 np.array([1, 2, 3]) 。可以通过: + + >>> NumpyNumberPadder.pad([1, 2, 3]) + + 使用。 - :param pad_val: 该值无意义 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ + :param pad_val: 该值无意义 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): return np.array(batch_field, dtype=dtype) class NumpySequencePadder(Padder): + """ + 将类似于 [[1], [1, 2]] 的内容 pad 为 np.array([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 + 可以通过以下的方式直接使用: + + >>> NumpySequencePadder.pad([[1], [1, 2]], pad_val=-100, dtype=float) + [[ 1. -100.] + [ 1. 2.]] + + :param pad_val: pad 的值是多少。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 将类似于 [[1], [1, 2]] 的内容 pad 为 np.array([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 - - :param pad_val: pad 的值是多少。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): return get_padded_numpy_array(batch_field, dtype=dtype, pad_val=pad_val) class NumpyTensorPadder(Padder): + """ + pad 类似于 [np.array([3, 4]), np.array([1])] 的 field 。若内部元素不为 np.ndarray ,则必须含有 tolist() 方法。 + + >>> NumpyTensorPadder.pad([np.array([3, 4]), np.array([1])], pad_val=-100) + [[ 3. 4.] + [ 1. -100.]] + :param pad_val: pad 的值是多少。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - pad 类似于 [np.array([3, 4], np.array([1])] 的 field 。若内部元素不为 np.ndarray ,则必须含有 tolist() 方法。 - - :param pad_val: pad 的值是多少。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): try: if not isinstance(batch_field[0], np.ndarray): batch_field = [np.array(field.tolist(), dtype=dtype) for field in batch_field] diff --git a/fastNLP/core/collators/padders/padder.py b/fastNLP/core/collators/padders/padder.py index 065ce39f..6a75b634 100644 --- a/fastNLP/core/collators/padders/padder.py +++ b/fastNLP/core/collators/padders/padder.py @@ -1,5 +1,9 @@ class Padder: + """ + 所有 Padder 对象父类,所有的 Padder 对象都会实现 pad(batch_field, pad_val=0, dtype=None) 的静态函数。 + + """ def __init__(self, pad_val, dtype): self.pad_val = pad_val self.dtype = dtype @@ -8,19 +12,19 @@ class Padder: return self.pad(batch_field=batch_field, pad_val=self.pad_val, dtype=self.dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): raise NotImplementedError() class NullPadder(Padder): - def __init__(self, ele_dtype=None, pad_val=None, dtype=None): - """ - 不进行任何 检查 与 pad 的空 padder 。 + """ + 不进行任何 检查 与 pad 的空 padder 。 - :param ele_dtype: - :param pad_val: - :param dtype: - """ + :param ele_dtype: + :param pad_val: + :param dtype: + """ + def __init__(self, ele_dtype=None, pad_val=None, dtype=None): super().__init__(pad_val=pad_val, dtype=dtype) def __call__(self, batch_field): diff --git a/fastNLP/core/collators/padders/paddle_padder.py b/fastNLP/core/collators/padders/paddle_padder.py index 826d21c7..a891eb9f 100644 --- a/fastNLP/core/collators/padders/paddle_padder.py +++ b/fastNLP/core/collators/padders/paddle_padder.py @@ -80,55 +80,55 @@ def _get_dtype(ele_dtype, dtype, class_name): class PaddleNumberPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 可以将形如 [1, 2, 3] 这类的数据转为 paddle.Tensor([1, 2, 3]) + """ + 可以将形如 [1, 2, 3] 这类的数据转为 paddle.Tensor([1, 2, 3]) - :param pad_val: 该值无意义 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 - """ + :param pad_val: 该值无意义 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): # 仅当 ele_dtype 是 python number/ numpy number 或者 tensor dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): return paddle.to_tensor(batch_field, dtype=dtype) class PaddleSequencePadder(Padder): - def __init__(self, ele_dtype=None, pad_val=0, dtype=None): - """ - 将类似于 [[1], [1, 2]] 的内容 pad 为 paddle.Tensor([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 + """ + 将类似于 [[1], [1, 2]] 的内容 pad 为 paddle.Tensor([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 - :param pad_val: pad 的值。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 - """ + :param pad_val: pad 的值。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 + """ + def __init__(self, ele_dtype=None, pad_val=0, dtype=None): dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): tensor = get_padded_paddle_tensor(batch_field, dtype=dtype, pad_val=pad_val) return tensor class PaddleTensorPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 目前支持 [paddle.tensor([3, 2], paddle.tensor([2, 1])] 类似的,若内部元素不为 paddle.tensor ,则必须含有 tolist() 方法。 + """ + 目前支持 [paddle.tensor([3, 2], paddle.tensor([2, 1])] 类似的,若内部元素不为 paddle.tensor ,则必须含有 tolist() 方法。 - :param pad_val: pad 的值。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 - """ + :param pad_val: pad 的值。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 paddle.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 int, float, 'int32' 等 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): try: if not isinstance(batch_field[0], paddle.Tensor): batch_field = [np.array(field.tolist()) for field in batch_field] diff --git a/fastNLP/core/collators/padders/raw_padder.py b/fastNLP/core/collators/padders/raw_padder.py index bc3bbaee..91d97333 100644 --- a/fastNLP/core/collators/padders/raw_padder.py +++ b/fastNLP/core/collators/padders/raw_padder.py @@ -26,14 +26,14 @@ def _get_dtype(ele_dtype, dtype, class_name): class RawNumberPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 可以将形如 [1, 2, 3] 这类的数据转为 [1, 2, 3] 。实际上该 padder 无意义。 + """ + 可以将形如 [1, 2, 3] 这类的数据转为 [1, 2, 3] 。实际上该 padder 无意义。 - :param pad_val: 该值无意义 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ + :param pad_val: 该值无意义 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @@ -41,24 +41,24 @@ class RawNumberPadder(Padder): return batch_field @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): raise NotImplementedError() class RawSequencePadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 将类似于 [[1], [1, 2]] 的内容 pad 为 [[1, 0], [1, 2]] 。可以 pad 多重嵌套的数据。 + """ + 将类似于 [[1], [1, 2]] 的内容 pad 为 [[1, 0], [1, 2]] 。可以 pad 多重嵌套的数据。 - :param pad_val: pad 的值 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ + :param pad_val: pad 的值 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): """ :param batch_field: @@ -70,19 +70,19 @@ class RawSequencePadder(Padder): class RawTensorPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 将类似于 [[1], [1, 2]] 的内容 pad 为 [[1, 0], [1, 2]] 。可以 pad 多重嵌套的数据。 + """ + 将类似于 [[1], [1, 2]] 的内容 pad 为 [[1, 0], [1, 2]] 。可以 pad 多重嵌套的数据。 - :param pad_val: pad 的值 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 - :param dtype: 输出的数据的 dtype 是什么 - """ + :param pad_val: pad 的值 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 np.array 类型。 + :param dtype: 输出的数据的 dtype 是什么 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): """ :param batch_field: diff --git a/fastNLP/core/collators/padders/torch_padder.py b/fastNLP/core/collators/padders/torch_padder.py index aaa0d4e9..9ef2a12d 100644 --- a/fastNLP/core/collators/padders/torch_padder.py +++ b/fastNLP/core/collators/padders/torch_padder.py @@ -64,54 +64,61 @@ def _get_dtype(ele_dtype, dtype, class_name): class TorchNumberPadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 可以将形如 [1, 2, 3] 这类的数据转为 torch.Tensor([1, 2, 3]) + """ + 可以将形如 [1, 2, 3] 这类的数据转为 torch.Tensor([1, 2, 3]) - :param pad_val: 该值无意义 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 - """ + :param pad_val: 该值无意义 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): return torch.tensor(batch_field, dtype=dtype) class TorchSequencePadder(Padder): - def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 将类似于 [[1], [1, 2]] 的内容 pad 为 torch.Tensor([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 + """ + 将类似于 [[1], [1, 2]] 的内容 pad 为 torch.Tensor([[1, 0], [1, 2]]) 可以 pad 多重嵌套的数据。 - :param pad_val: 需要 pad 的值。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 - """ + :param pad_val: 需要 pad 的值。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 + """ + def __init__(self, pad_val=0, ele_dtype=None, dtype=None): dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): tensor = get_padded_torch_tensor(batch_field, dtype=dtype, pad_val=pad_val) return tensor class TorchTensorPadder(Padder): + """ + 目前支持 [torch.tensor([3, 2], torch.tensor([1])] 类似的。若内部元素不为 torch.tensor ,则必须含有 tolist() 方法。 + + >>> TorchTensorPadder.pad([np.array([3, 4]), np.array([1])], pad_val=-100) + [[ 3. 4.] + [ 1. -100.]] + >>> TorchTensorPadder.pad([torch.LongTensor([3, 4]), torch.LongTensor([1])], pad_val=-100) + tensor([[ 3, 4], + [ 1, -100]]) + + :param pad_val: 需要 pad 的值。 + :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 + :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 + """ def __init__(self, pad_val=0, ele_dtype=None, dtype=None): - """ - 目前支持 [torch.tensor([3, 2], torch.tensor([1])] 类似的。若内部元素不为 torch.tensor ,则必须含有 tolist() 方法。 - - :param pad_val: 需要 pad 的值。 - :param ele_dtype: 用于检测当前 field 的元素类型是否可以转换为 torch.tensor 类型。 - :param dtype: 输出的数据的 dtype 是什么。如 torch.long, torch.float32, int, float 等 - """ dtype = _get_dtype(ele_dtype, dtype, class_name=self.__class__.__name__) super().__init__(pad_val=pad_val, dtype=dtype) @staticmethod - def pad(batch_field, pad_val, dtype): + def pad(batch_field, pad_val=0, dtype=None): device = None try: if not isinstance(batch_field[0], torch.Tensor): diff --git a/fastNLP/core/controllers/evaluator.py b/fastNLP/core/controllers/evaluator.py index 8ac35ad2..7ef15392 100644 --- a/fastNLP/core/controllers/evaluator.py +++ b/fastNLP/core/controllers/evaluator.py @@ -20,6 +20,114 @@ from fastNLP.core.log import logger class Evaluator: + """ + 用于对数据进行评测。 + + :param model: 待测试的模型,如果传入的 driver 为 Driver 实例,该参数将被忽略。 + :param dataloaders: 待评测的数据集。如果为多个,请使用 dict 传入。 + :param metrics: 使用的 metric 。必须为 dict 类型,其中 key 为 metric 的名称,value 为一个 Metric 对象。支持 fastNLP 的 + metric ,torchmetrics,allennlpmetrics 等。 + :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:``["torch", "jittor", "paddle"]`` + 其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` + 的设置。 + :param device: 该参数用来指定具体训练时使用的机器;注意当该参数仅当您通过 `torch.distributed.launch/run` 启动时可以为 None, + 此时 fastNLP 不会对模型和数据进行设备之间的移动处理,但是你可以通过参数 `input_mapping` 和 `output_mapping` 来实现设备之间 + 数据迁移的工作(通过这两个参数传入两个处理数据的函数);同时你也可以通过在 kwargs 添加参数 "data_device" 来让我们帮助您将数据 + 迁移到指定的机器上(注意这种情况理应只出现在用户在 Trainer 实例化前自己构造 DDP 的场景); + + device 的可选输入如下所示: + + * *str*: 例如 'cpu', 'cuda', 'cuda:0', 'cuda:1' 等; + * *torch.device*: 例如 'torch.device("cuda:0")'; + * *int*: 将使用 ``device_id`` 为该值的 ``gpu`` 进行训练;如果值为 -1,那么默认使用全部的显卡,此时使用的 driver 实例是 `TorchDDPDriver`; + * *list(int)*: 如果多于 1 个device,应当通过该种方式进行设定;注意此时我们一定会使用 ``TorchDDPDriver``,不管您传入的列表的长度是 1 还是其它值; + * *None*: 仅当用户自己通过训练框架提供的并行训练启动脚本开启 ddp 进程时为 None; + + .. note:: + + 如果希望使用 ``TorchDDPDriver``,在初始化 ``Trainer`` 时您应当使用:: + + Trainer(driver="torch", device=[0, 1]) + + 注意如果这时 ``device=[0]``,我们仍旧会使用 ``TorchDDPDriver``。 + + 如果希望使用 ``TorchSingleDriver``,则在初始化 ``Trainer`` 时您应当使用:: + + Trainer(driver="torch", device=0) + + .. warning:: + + 注意参数 ``device`` 仅当您通过 pytorch 或者其它训练框架自身的并行训练启动脚本启动 ddp 训练时才允许为 ``None``! + + 例如,当您使用:: + + python -m torch.distributed.launch --nproc_per_node 2 train.py + + 来使用 ``TorchDDPDriver`` 时,此时参数 ``device`` 不再有效(不管您是否自己初始化 ``init_process_group``),我们将直接 + 通过 ``torch.device(f"cuda:{local_rank}")`` 来获取当前进程所使用的的具体的 gpu 设备。因此此时您需要使用 ``os.environ["CUDA_VISIBLE_DEVICES"]`` + 来指定要使用的具体的 gpu 设备。 + + 另一点需要注意的是,当您没有选择自己初始化 ``init_process_group`` 时,我们仍旧会帮助您把模型和数据迁移到当前进程所使用的 + 具体的 gpu 设备上。但是如果您选择自己在 ``Trainer`` 初始化前(意味着在 ``driver`` 的 ``setup`` 前)初始化 ``init_process_group``, + 那么对于模型的迁移应当完全由您自己来完成。此时对于数据的迁移,如果您在 ``Trainer`` 初始化时指定了参数 ``data_device``,那么 + 我们会将数据迁移到 ``data_device`` 上;如果其为 None,那么将数据迁移到正确的设备上应当由您自己来完成。 + + 对于使用 ``TorchDDPDriver`` 的更多细节,请见 :class:`fastNLP.core.drivers.torch_driver.TorchDDPDriver`。 + + :param evaluate_batch_step_fn: 定制每次 evaluate batch 执行的函数。该函数应接受的两个参数为 `evaluator` 和 `batch`, + 不需要有返回值;可以参考 :meth:`~fastNLP.core.controllers.loops.EvaluateBatchLoop.batch_step_fn` + 函数。 + :param evaluate_fn: 用来控制 `Evaluator` 在评测的前向传播过程中是调用哪一个函数,例如是 `model.evaluate_step` 还是 + `model.forward`;(1) 如果该值是 None,那么我们会默认使用 `evaluate_step` 函数,如果在模型中没有 + 找到该方法,则使用 `model.forward` 函数;(2) 如果为 str 类型,则尝试从 model 中寻找该方法,找不到则报错。 + :param input_mapping: 应当为一个字典或者一个函数,表示在当前 step 拿到一个 batch 的数据后,应当做怎样的映射处理: + + 1. 如果 ``input_mapping`` 是一个字典: + + 1. 如果此时 batch 也是一个 ``Dict``,那么我们会把 batch 中同样在 ``input_mapping`` 中的 key 修改为 ``input_mapping`` 的对应 ``key`` 的 ``value``; + 2. 如果此时 batch 是一个 ``dataclass``,那么我们会先将其转换为一个 ``Dict``,然后再进行上述转换; + 3. 如果此时 batch 此时是其它类型,那么我们将会直接报错; + 2. 如果 ``input_mapping`` 是一个函数,那么对于取出的 batch,我们将不会做任何处理,而是直接将其传入该函数里,并将其返回值 + 送入模型中; + + :param output_mapping: 应当为一个字典或者一个函数,表示在当前 step 拿到一个 model 的返回值后,应当做怎样的映射处理: + + 1. 如果 ``output_mapping`` 是一个字典: + + 1. 如果此时 batch 也是一个 ``Dict``,那么我们会把输出中同样在 ``output_mapping`` 中的 key 修改为 ``output_mapping`` 的对应 ``key`` 的 ``value``; + 例如输出结果为 {'a': 1},而 output_mapping={'a':'b'} ,那么结果就是 {'b': 1} + 2. 如果此时 batch 是一个 ``dataclass``,那么我们会先将其转换为一个 ``Dict``,然后再进行上述转换; + 3. 如果此时 batch 此时是其它类型,那么我们将会直接报错; + 2. 如果 ``output_mapping`` 是一个函数,我们将会把结果传入该函数里,并将其返回值送入到 metric 中。 + + :param model_wo_auto_param_call: 是否关闭在训练时调用我们的 auto_param_call 来自动匹配 batch 和 evaluate_fn 函数的参数的行为; + 如果该值为 True,并且当 batch 为字典时,我们会根据 evaluate_fn 所需要的参数从 batch 中提取对应的对象,传入到 evaluate_fn 函数中;如果该值 + 为 False,那么我们会将 batch 直接透传给 evaluate_fn 函数。 + :param fp16: 是否使用 fp16 。 + :param verbose: 是否打印 evaluate 的结果。 + :kwargs: + * *torch_kwargs* -- 用于在指定 ``driver`` 为 'torch' 时设定具体 driver 实例的一些参数: + * ddp_kwargs -- 用于在使用 ``TorchDDPDriver`` 时指定 ``DistributedDataParallel`` 初始化时的参数;例如传入 + {'find_unused_parameters': True} 来解决有参数不参与前向运算导致的报错等; + * torch_non_blocking -- 表示用于 pytorch 的 tensor 的 to 方法的参数 non_blocking; + * *data_device* -- 表示如果用户的模型 device (在 Driver 中对应为参数 model_device)为 None 时,我们会将数据迁移到 data_device 上; + 注意如果 model_device 为 None,那么 data_device 不会起作用; + * *model_use_eval_mode* (``bool``) -- + 是否在 evaluate 的时候将 model 的状态设置成 eval 状态。在 eval 状态下,model 的 + dropout 与 batch normalization 将会关闭。默认为True。如果为 False,fastNLP 不会对 model 的 evaluate 状态做任何设置。无论 + 该值是什么,fastNLP 都会在 evaluate 接受后将 model 的状态设置为 train 。 + * *use_dist_sampler* -- + 是否使用分布式evaluate的方式。仅当 driver 为分布式类型时,该参数才有效。默认为根据 driver 是否支持 + 分布式进行设置。如果为True,将使得每个进程上的 dataloader 自动使用不同数据,所有进程的数据并集是整个数据集。 + * *output_from_new_proc* -- + 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: + ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 + log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; + * *progress_bar* -- + evaluate 的时候显示的 progress bar 。目前支持三种 [None, 'raw', 'rich', 'auto'], auto 表示如果检测 + 到当前terminal为交互型则使用 rich,否则使用 raw。 + """ + driver: Driver _evaluate_batch_loop: Loop @@ -29,51 +137,6 @@ class Evaluator: input_mapping: Optional[Union[Callable, Dict]] = None, output_mapping: Optional[Union[Callable, Dict]] = None, model_wo_auto_param_call: bool = False, fp16: bool = False, verbose: int = 1, **kwargs): - """ - 用于对数据进行评测。 - - :param model: 待测试的模型,如果传入的 driver 为 Driver 实例,该参数将被忽略。 - :param dataloaders: 待评测的数据集。如果为多个,请使用 dict 传入。 - :param metrics: 使用的 metric 。必须为 dict 类型,其中 key 为 metric 的名称,value 为一个 Metric 对象。支持 fastNLP 的 - metric ,torchmetrics,allennlpmetrics 等。 - :param driver: 使用 driver 。 - :param device: 使用的设备。 - :param evaluate_batch_step_fn: 定制每次 evaluate batch 执行的函数。该函数应接受的两个参数为 `evaluator` 和 `batch`, - 不需要有返回值;可以参考 fastNLP.core.controllers.loops.evaluate_batch_loop.EvaluateBatchLoop中的batch_step_fn函数。 - :param evaluate_fn: 用来控制 `Evaluator` 在评测的前向传播过程中是调用哪一个函数,例如是 `model.evaluate_step` 还是 - `model.forward`;(1) 如果该值是 None,那么我们会默认使用 `evaluate_step` 当做前向传播的函数,如果在模型中没有 - 找到该方法,则使用 `model.forward` 函数;(2) 如果为 str 类型,则尝试从 model 中寻找该方法,找不到则报错。 - :param input_mapping: 对 dataloader 中输出的内容将通过 input_mapping 处理之后再输入到 model 以及 metric 中。如果针对 - model 和 metric 需要不同的 mapping,请考虑使用 evaluate_batch_step_fn 参数定制。 - :param output_mapping: 对 model 输出的内容,将通过 output_mapping 处理之后再输入到 metric 中。 - :param model_wo_auto_param_call: 是否关闭在训练时调用我们的 auto_param_call 来自动匹配 batch 和 forward 函数的参数的行为; - 如果该值为 True,并且当 batch 为字典时,我们会根据 forward 所需要的参数从 batch 中提取对应的对象,传入到 forward 函数中;如果该值 - 为 False,那么我们会将 batch 直接透传给 forward 函数。注意上述逻辑同样应用于 `train_step`, `evaluate_step` 和 `test_step`; - :param fp16: 是否使用 fp16 。 - :param verbose: 是否打印 evaluate 的结果。 - :kwargs: - * *torch_kwargs* -- 用于在指定 ``driver`` 为 'torch' 时设定具体 driver 实例的一些参数: - * ddp_kwargs -- 用于在使用 ``TorchDDPDriver`` 时指定 ``DistributedDataParallel`` 初始化时的参数;例如传入 - {'find_unused_parameters': True} 来解决有参数不参与前向运算导致的报错等; - * torch_non_blocking -- 表示用于 pytorch 的 tensor 的 to 方法的参数 non_blocking; - * *data_device* -- 表示如果用户的模型 device (在 Driver 中对应为参数 model_device)为 None 时,我们会将数据迁移到 data_device 上; - 注意如果 model_device 为 None,那么 data_device 不会起作用; - * *model_use_eval_mode* (``bool``) -- - 是否在 evaluate 的时候将 model 的状态设置成 eval 状态。在 eval 状态下,model 的 - dropout 与 batch normalization 将会关闭。默认为True。如果为 False,fastNLP 不会对 model 的 evaluate 状态做任何设置。无论 - 该值是什么,fastNLP 都会在 evaluate 接受后将 model 的状态设置为 train 。 - * *use_dist_sampler* -- - 是否使用分布式evaluate的方式。仅当 driver 为分布式类型时,该参数才有效。默认为根据 driver 是否支持 - 分布式进行设置。如果为True,将使得每个进程上的 dataloader 自动使用不同数据,所有进程的数据并集是整个数据集。 - * *output_from_new_proc* -- - 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: - ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 - log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; - * *progress_bar* -- - evaluate 的时候显示的 progress bar 。目前支持三种 [None, 'raw', 'rich', 'auto'], auto 表示如果检测 - 到当前terminal为交互型则使用 rich,否则使用 raw。 - """ - self.model = model self.metrics = metrics self.driver = choose_driver(model, driver, device, fp16=fp16, model_wo_auto_param_call=model_wo_auto_param_call, @@ -129,16 +192,13 @@ class Evaluator: def run(self, num_eval_batch_per_dl: int = -1, **kwargs) -> Dict: """ - 返回一个字典类型的数据,其中key为metric的名字,value为对应metric的结果。 - 如果存在多个metric,一个dataloader的情况,key的命名规则是 - metric_indicator_name#metric_name - 如果存在多个数据集,一个metric的情况,key的命名规则是 - metric_indicator_name#metric_name#dataloader_name (其中 # 是默认的 separator ,可以通过 Evaluator 初始化参数修改)。 - 如果存在多个metric,多个dataloader的情况,key的命名规则是 - metric_indicator_name#metric_name#dataloader_name - 其中 metric_indicator_name 可能不存在。 - - :param num_eval_batch_per_dl: 每个 dataloader 测试多少个 batch 的数据,-1 为测试所有数据。 + 返回一个字典类型的数据,其中 key 为 metric 的名字,value 为对应结果。返回的字典中,key 的命名规则如下 + ``metric_indicator_name#metric_name#dataloader_name`` ,其中 ``metric_indicator_name`` 是由使用的 metric 返回的结果 + 决定的,仅当 metric 的结果返回是 dict 类型是才有该值;``metric_name`` 则由 ``Evaluator`` 初始化时传入的 ``metrics`` 参数 + 决定;``dataloader_name``仅在传入的 ``dataloaders`` 为 dict 时会有。此外其中的 ``#`` 符号通过 ``Evaluator`` 初始化 + 参数 ``separator`` 进行设置。 + + :param num_eval_batch_per_dl: 每个 dataloader 测试前多少个 batch 的数据,-1 为测试所有数据。 :return: """ assert isinstance(num_eval_batch_per_dl, int), "num_eval_batch_per_dl must be of int type." diff --git a/fastNLP/core/controllers/trainer.py b/fastNLP/core/controllers/trainer.py index d0642e77..1b650a7d 100644 --- a/fastNLP/core/controllers/trainer.py +++ b/fastNLP/core/controllers/trainer.py @@ -80,9 +80,9 @@ class Trainer(TrainerEventTrigger): 您应当使用 ``TorchDDPDriver``,意味着您需要通过 ``python -m torch.distributed.launch`` 的方式来启动训练,此时参数 ``device`` 应当设置为 None(此时我们会忽略该参数),具体见下面对于参数 ``device`` 的更详细的解释。 - :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:["torch"],之后我们会加入 jittor、paddle 等 - 国产框架的训练模式;其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` - 的设置; + :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:``["torch", "jittor", "paddle"]`` + 其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` + 的设置。 :param train_dataloader: 训练数据集,注意其必须是单独的一个数据集,不能是 List 或者 Dict; :param optimizers: 训练所需要的优化器;可以是单独的一个优化器实例,也可以是多个优化器组成的 List; :param device: 该参数用来指定具体训练时使用的机器;注意当该参数仅当您通过 `torch.distributed.launch/run` 启动时可以为 None, @@ -135,7 +135,7 @@ class Trainer(TrainerEventTrigger): :param batch_step_fn: 定制每次训练时前向运行一个 batch 的数据所执行的函数。该函数应接受两个参数为 ``trainer`` 和 ``batch``, 不需要要返回值;更详细的使用位置和说明请见 :meth:`fastNLP.core.controllers.TrainBatchLoop.batch_step_fn`; :param evaluate_batch_step_fn: 定制每次验证时前向运行一个 batch 的数据所执行的函数。该函数应接受的两个参数为 ``evaluator`` 和 ``batch``, - 不需要有返回值;可以参考 :meth:`fastNLP.core.controllers.EvaluateBatchLoop.batch_step_fn`; + 不需要有返回值;可以参考 :meth:`fastNLP.core.controllers.TrainBatchLoop.batch_step_fn`; :param train_fn: 用来控制 ``Trainer`` 在训练的前向传播过程中是调用模型的哪一个函数,例如是 ``train_step`` 还是 ``forward``; 默认为 ``None``,如果该值是 ``None``,那么我们会默认使用 ``train_step`` 当做前向传播的函数,如果在模型的定义类中没有找到该方法, 则使用模型默认的前向传播函数,例如对于 pytorch 来说就是 ``forward``。 diff --git a/fastNLP/core/dataset/instance.py b/fastNLP/core/dataset/instance.py index f8f02621..88d4604b 100644 --- a/fastNLP/core/dataset/instance.py +++ b/fastNLP/core/dataset/instance.py @@ -1,6 +1,6 @@ r""" instance 模块实现了Instance 类在fastNLP中对应sample。一个sample可以认为是一个Instance类型的对象。 -便于理解的例子可以参考文档 :mod:`fastNLP.core.dataset` 中的表格 +便于理解的例子可以参考文档 :mod:`fastNLP.core.dataset` 。 """ @@ -14,10 +14,10 @@ from fastNLP.core.utils.utils import pretty_table_printer class Instance(Mapping): r""" - Instance是fastNLP中对应一个sample的类。每个sample在fastNLP中是一个Instance对象。 - Instance一般与 :class:`~fastNLP.DataSet` 一起使用, Instance的初始化如下面的Example所示:: + Instance 是 fastNLP 中对应一个 sample 的类。每个 sample 在 fastNLP 中是一个 Instance 对象。 + Instance 一般与 :class:`~fastNLP.DataSet` 一起使用, Instance 的初始化如下面的 Example 所示:: - instance = Instance() # 请补充完整 + >>> instance = Instance(input="this is a demo sentence", label='good') # 请补充完整 """ @@ -44,17 +44,17 @@ class Instance(Mapping): def keys(self): r""" - 返回一个迭代器,内容是field_name + 返回一个迭代器,内容是 field_name - :return: 一个迭代器 + :return: 一个迭代器 """ return self.fields.keys() def values(self): r""" - 返回一个迭代器,内容是field_value + 返回一个迭代器,内容是 field_value - :return: 一个迭代器 + :return: 一个迭代器 """ return self.fields.values() diff --git a/fastNLP/core/metrics/metric.py b/fastNLP/core/metrics/metric.py index 87505be1..b340beea 100644 --- a/fastNLP/core/metrics/metric.py +++ b/fastNLP/core/metrics/metric.py @@ -14,14 +14,16 @@ from fastNLP.core.metrics.element import Element class Metric: + """ + fastNLP 中 Metric 的基类,自定义 Metric 时,请继承该对象。使用该对象,将有助于减少在分布式状态下的 Metric 计算。 + + :param backend: 目前支持四种类型的 backend, ``[torch, paddle, jittor, auto]``。其中 ``auto`` 表示根据实际调用 + Metric.update() 函数时传入的参数决定具体的 ``backend`` ,大部分情况下直接使用 ``auto`` 即可。 + :param aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到metric, + 当 backend 不支持分布式时,该参数无意义。如果为 None ,将在 :class:`fastNLP.Evaluator` 中根据 sampler 是否使用分布式 + 进行自动设置。 + """ def __init__(self, backend: Union[str, Backend, None] = 'auto', aggregate_when_get_metric: bool = None): - """ - - :param str backend: 目前支持四种类型的backend, [torch, paddle, jittor, auto]。其中 auto 表示根据实际调用 Metric.update() - 函数时传入的参数决定具体的 backend ,大部分情况下直接使用 auto 即可。 - :param bool aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到metric, - 当 backend 不支持分布式时,该参数无意义。如果为 None ,将在 Evaluator 中根据 sampler 是否使用分布式进行自动设置。 - """ self.backend = AutoBackend(backend) self._updated = False self.get_metric = self._sync_get_metric(self.get_metric) @@ -39,7 +41,10 @@ class Metric: """ 注册一个 element 对象,注册之后便可以通过在 Metric 中直接通过 self.{name} 进行调用,可以认为该对象即为对应 backend 的 tensor 直接进行加减乘除计算即可。 - 注意:如果想使得该 metric 可自动扩展到多卡的情况,请一定申明 aggregate_method 。 + + ..warning:: + + 如果想使得该 metric 可自动扩展到多卡的情况,请一定申明 aggregate_method 。 :param name: 当前 element 的名字,注册后,在 Metric 中可以通过 self.{name} 访问该变量。 :param value: 初始化的值。在调用 Metric.reset() 方法时也将自动设置为该值 diff --git a/fastNLP/core/metrics/span_f1_pre_rec_metric.py b/fastNLP/core/metrics/span_f1_pre_rec_metric.py index 12d86a31..4879e724 100644 --- a/fastNLP/core/metrics/span_f1_pre_rec_metric.py +++ b/fastNLP/core/metrics/span_f1_pre_rec_metric.py @@ -200,27 +200,27 @@ def _bio_tag_to_spans(tags, ignore_labels=None): class SpanFPreRecMetric(Metric): + r""" + :param tag_vocab: 标签的 :class:`~fastNLP.Vocabulary` 。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), + 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. + :param pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用 `pred` 取数据 + :param target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用 `target` 取数据 + :param seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用 `seq_len` 取数据。 + :param encoding_type: 目前支持bio, bmes, bmeso, bioes。默认为None,通过tag_vocab自动判断. + :param ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'个label + :param only_gross: 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个label的f1, pre, rec + :param f_type: `micro` 或 `macro` . `micro` :通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; `macro` : 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) + :param beta: f_beta分数, :math:`f_{beta} = \frac{(1 + {beta}^{2})*(pre*rec)}{({beta}^{2}*pre + rec)}` . 常用为 `beta=0.5, 1, 2` 若为0.5则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 + :param backend: 目前支持四种类型的 backend, ``[torch, paddle, jittor, auto]``。其中 ``auto`` 表示根据实际调用 + Metric.update() 函数时传入的参数决定具体的 ``backend`` ,大部分情况下直接使用 ``auto`` 即可。 + :param aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到metric, + 当 backend 不支持分布式时,该参数无意义。如果为 None ,将在 :class:`fastNLP.Evaluator` 中根据 sampler 是否使用分布式 + 进行自动设置。 + """ def __init__(self, tag_vocab: Vocabulary, encoding_type: str = None, ignore_labels: List[str] = None, only_gross: bool = True, f_type='micro', beta=1, backend: Union[str, Backend, None] = 'auto', aggregate_when_get_metric: bool = None) -> None: - r""" - - :param tag_vocab: 标签的 :class:`~fastNLP.Vocabulary` 。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), - 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. - :param str pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用 `pred` 取数据 - :param str target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用 `target` 取数据 - :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用 `seq_len` 取数据。 - :param str encoding_type: 目前支持bio, bmes, bmeso, bioes。默认为None,通过tag_vocab自动判断. - :param list ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'个label - :param bool only_gross: 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个label的f1, pre, rec - :param str f_type: `micro` 或 `macro` . `micro` :通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; `macro` : 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) - :param float beta: f_beta分数, :math:`f_{beta} = \frac{(1 + {beta}^{2})*(pre*rec)}{({beta}^{2}*pre + rec)}` . 常用为 `beta=0.5, 1, 2` 若为0.5则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 - :param str backend: 目前支持四种类型的backend, ['auto', 'torch', 'paddle', 'jittor']。其中 auto 表示根据实际调用 Metric.update() - 函数时传入的参数决定具体的 backend ,一般情况下直接使用 'auto' 即可。 - :param bool aggregate_when_get_metric: 在计算 metric 的时候是否自动将各个进程上的相同的 element 的数字聚合后再得到metric, - 当 backend 不支持分布式时,该参数无意义。如果为 None ,将在 Evaluator 中根据 sampler 是否使用分布式进行自动设置。 - """ super(SpanFPreRecMetric, self).__init__(backend=backend, aggregate_when_get_metric=aggregate_when_get_metric) if f_type not in ('micro', 'macro'): raise ValueError("f_type only supports `micro` or `macro`', got {}.".format(f_type)) diff --git a/fastNLP/core/samplers/conversion_utils.py b/fastNLP/core/samplers/conversion_utils.py index c13e4884..85846bde 100644 --- a/fastNLP/core/samplers/conversion_utils.py +++ b/fastNLP/core/samplers/conversion_utils.py @@ -10,7 +10,7 @@ def conversion_between_reproducible_and_unrepeated_sampler(sampler): 将 sampler 替换成其对应的 reproducible 版本或 unrepeated 版本。如果输入是 UnrepeatedSampler 但是没找到对应的 ReproducibleSampler, - :param sampler: + :param sampler: 需要转换的 sampler 。 :return: """ assert isinstance(sampler, UnrepeatedSampler) or isinstance(sampler, ReproducibleSampler), \ diff --git a/fastNLP/core/samplers/reproducible_batch_sampler.py b/fastNLP/core/samplers/reproducible_batch_sampler.py index 143a5438..520ad9ba 100644 --- a/fastNLP/core/samplers/reproducible_batch_sampler.py +++ b/fastNLP/core/samplers/reproducible_batch_sampler.py @@ -55,16 +55,16 @@ class ReproducibleBatchSampler: class ReproduceBatchSampler(ReproducibleBatchSampler): + """ + 可以使得 batch_sampler 对象状态恢复的 wrapper 。 + + :param batch_sampler: 可迭代出 数字 或 数字列表 的可迭代对象。ReproduceBatchSampler 将首先遍历一边该对象,然后将迭代 + 出来的序号暂存起来,使用时按照 batch_size 的 batch 大小吐出序号列表。 + :param batch_size: 每个 batch 的大小是多少。 + :param drop_last: 如果最后一个 batch 无法构成 batch_size 那么多个 sample ,是否丢掉。 + :param kwargs: fastNLP 内部使用。 + """ def __init__(self, batch_sampler, batch_size: int, drop_last: bool, **kwargs): - """ - 可以使得 batch_sampler 对象状态恢复的 wrapper 。 - - :param batch_sampler: 可迭代出 数字 或 数字列表 的可迭代对象。ReproduceBatchSampler 将首先遍历一边该对象,然后将迭代 - 出来的序号暂存起来,使用时按照 batch_size 的 batch 大小吐出序号列表。 - :param batch_size: 每个 batch 的大小是多少。 - :param drop_last: 如果最后一个 batch 无法构成 batch_size 那么多个 sample ,是否丢掉。 - :param kwargs: fastNLP 内部使用。 - """ super().__init__() self.batch_sampler = batch_sampler @@ -158,18 +158,18 @@ class ReproduceBatchSampler(ReproducibleBatchSampler): class RandomBatchSampler(ReproducibleBatchSampler): + """ + 随机分 batch 的 batch_sampler 。 + + :param dataset: 实现了 __len__ 方法的数据容器。 + :param batch_size: 每个 batch 的大小 + :param shuffle: 如果为 True,将不进行 shuffle,实际上数据会以从长到短的方式输出。 + :param drop_last: 如果最后一个 batch 的 sample 数量无法凑齐 batch_size 这么多,是否需要丢掉。 + :param seed: 设置的随机数种子 + :param kwargs: fastNLP 保留使用 + """ def __init__(self, dataset, batch_size:int = 32, shuffle: bool = True, drop_last: bool = False, seed: int = 0, **kwargs): - """ - 随机分 batch 的 batch_sampler 。 - - :param dataset: 实现了 __len__ 方法的数据容器。 - :param batch_size: 每个 batch 的大小 - :param shuffle: 如果为 True,将不进行 shuffle,实际上数据会以从长到短的方式输出。 - :param drop_last: 如果最后一个 batch 的 sample 数量无法凑齐 batch_size 这么多,是否需要丢掉。 - :param seed: 设置的随机数种子 - :param kwargs: fastNLP 保留使用 - """ super().__init__() self.dataset = dataset @@ -363,28 +363,28 @@ class RandomBatchSampler(ReproducibleBatchSampler): class BucketedBatchSampler(ReproducibleBatchSampler): + """ + 首先按照 ``sample`` 的长度排序,然后按照 batch_size*num_batch_per_bucket 为一个桶的大小,``sample`` 只会在这个桶内进行组 + 合,这样每个 ``batch`` 中的 ``padding`` 数量会比较少 (因为桶内的数据的长度都接近)。 + + :param dataset: 实现了 __len__ 方法的数据容器。 + :param length: 每条数据的长度。 + + * 为 ``List[int]`` 时 + 应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量; + * 为 ``str`` 时 + 仅当传入的 ``dataset`` 是 :class:`fastNLP.DataSet` 时,允许传入 `str` ,该 `str` 将被认为是 ``dataset`` 中的 + ``field`` 。若 field 中的元素为 ``int``,则认为该值是 sample 的长度;若不为 ``int`` ,则尝试使用 ``len`` 方法 + 获取该 ``field`` 中每个元素的长度。 + :param batch_size: 每个 batch 的大小 + :param num_batch_per_bucket: 多少个 ``batch`` 组成一个桶,数据只会在一个桶内进行 ``shuffle`` 。 + :param shuffle: 如果为 True,将不进行 ``shuffle``,实际上数据会以从长到短的方式输出。 + :param drop_last: 如果最后一个 `batch` 的 ``sample`` 数量无法凑齐 ``batch_size`` 这么多,是否需要丢掉。 + :param seed: 设置的随机数种子 + :param kwargs: fastNLP 保留使用 + """ def __init__(self, dataset, length: Union[List[int], str], batch_size:int = 32, num_batch_per_bucket:int = 10, shuffle: bool = True, drop_last: bool = False, seed: int = 0, **kwargs): - """ - 首先按照 ``sample`` 的长度排序,然后按照 batch_size*num_batch_per_bucket 为一个桶的大小,``sample`` 只会在这个桶内进行组 - 合,这样每个 ``batch`` 中的 ``padding`` 数量会比较少 (因为桶内的数据的长度都接近)。 - - :param dataset: 实现了 __len__ 方法的数据容器。 - :param length: 每条数据的长度。 - - * 为 ``List[int]`` 时 - 应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量; - * 为 ``str`` 时 - 仅当传入的 ``dataset`` 是 :class:`fastNLP.DataSet` 时,允许传入 `str` ,该 `str` 将被认为是 ``dataset`` 中的 - ``field`` 。若 field 中的元素为 ``int``,则认为该值是 sample 的长度;若不为 ``int`` ,则尝试使用 ``len`` 方法 - 获取该 ``field`` 中每个元素的长度。 - :param batch_size: 每个 batch 的大小 - :param num_batch_per_bucket: 多少个 ``batch`` 组成一个桶,数据只会在一个桶内进行 ``shuffle`` 。 - :param shuffle: 如果为 True,将不进行 ``shuffle``,实际上数据会以从长到短的方式输出。 - :param drop_last: 如果最后一个 `batch` 的 ``sample`` 数量无法凑齐 ``batch_size`` 这么多,是否需要丢掉。 - :param seed: 设置的随机数种子 - :param kwargs: fastNLP 保留使用 - """ super().__init__() if isinstance(dataset, DataSet) and isinstance(length, str): length = dataset.get_field(length).content diff --git a/fastNLP/core/samplers/reproducible_sampler.py b/fastNLP/core/samplers/reproducible_sampler.py index 5972ce70..599f465f 100644 --- a/fastNLP/core/samplers/reproducible_sampler.py +++ b/fastNLP/core/samplers/reproducible_sampler.py @@ -53,15 +53,15 @@ class ReproducibleSampler: class RandomSampler(ReproducibleSampler): - def __init__(self, dataset, shuffle: bool = True, seed: int = 0, **kwargs): - """ - 随机顺序的 Sampler 。 + """ + 随机顺序的 Sampler 。 - :param dataset: 实现了 __len__ 方法的数据容器 - :param shuffle: 是否在每次 iterate 的时候打乱顺序。 - :param seed: 随机数种子。 - :param kwargs: 用户不需要使用,fastNLP 内部使用 - """ + :param dataset: 实现了 __len__ 方法的数据容器 + :param shuffle: 是否在每次 iterate 的时候打乱顺序。 + :param seed: 随机数种子。 + :param kwargs: 用户不需要使用,fastNLP 内部使用 + """ + def __init__(self, dataset, shuffle: bool = True, seed: int = 0, **kwargs): super(RandomSampler, self).__init__() self.dataset = dataset self.shuffle = shuffle @@ -213,13 +213,13 @@ class RandomSampler(ReproducibleSampler): class SequentialSampler(RandomSampler): - def __init__(self, dataset, **kwargs): - """ - 按照顺序读取 ``dataset`` 。在多卡情况下,间隔读取,例如,在两卡情况下,卡 0 取 ``[0,2,4,..]``, 卡1取 ``[1,3,5...]`` 。 + """ + 按照顺序读取 ``dataset`` 。在多卡情况下,间隔读取,例如,在两卡情况下,卡 0 取 ``[0,2,4,..]``, 卡1取 ``[1,3,5...]`` 。 - :param dataset: 实现了 __len__ 方法的数据容器。 - :param kwargs: - """ + :param dataset: 实现了 __len__ 方法的数据容器。 + :param kwargs: + """ + def __init__(self, dataset, **kwargs): super().__init__(dataset=dataset, **kwargs) def __iter__(self): @@ -283,23 +283,23 @@ class SequentialSampler(RandomSampler): class SortedSampler(SequentialSampler): + """ + 将 ``dataset`` 中的数据根据 ``length`` 从长到短进行迭代。在多卡情况下,由于 ``padding`` , 最后一个 ``sample`` 可能是最长 + 的那个 ``sample`` 。 + + :param dataset: 实现了 __len__ 方法的数据容器。 + :param length: 每条数据的长度。 + + * 为 ``List[int]`` 时 + 应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量; + * 为 ``str`` 时 + 仅当传入的 ``dataset`` 是 :class:`fastNLP.DataSet` 时,允许传入 `str` ,该 `str` 将被认为是 ``dataset`` 中的 + ``field`` 。若 field 中的元素为 ``int``,则认为该值是 sample 的长度;若不为 ``int`` ,则尝试使用 ``len`` 方法 + 获取该 ``field`` 中每个元素的长度。 + :param seed: 设置的随机数种子。 + :param kwargs: fastNLP 保留使用。 + """ def __init__(self, dataset, length:Union[str, List], **kwargs): - """ - 将 ``dataset`` 中的数据根据 ``length`` 从长到短进行迭代。在多卡情况下,由于 ``padding`` , 最后一个 ``sample`` 可能是最长 - 的那个 ``sample`` 。 - - :param dataset: 实现了 __len__ 方法的数据容器。 - :param length: 每条数据的长度。 - - * 为 ``List[int]`` 时 - 应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量; - * 为 ``str`` 时 - 仅当传入的 ``dataset`` 是 :class:`fastNLP.DataSet` 时,允许传入 `str` ,该 `str` 将被认为是 ``dataset`` 中的 - ``field`` 。若 field 中的元素为 ``int``,则认为该值是 sample 的长度;若不为 ``int`` ,则尝试使用 ``len`` 方法 - 获取该 ``field`` 中每个元素的长度。 - :param seed: 设置的随机数种子。 - :param kwargs: fastNLP 保留使用。 - """ super().__init__(dataset=dataset, **kwargs) if isinstance(dataset, DataSet) and isinstance(length, str): length = dataset.get_field(length).content diff --git a/fastNLP/core/samplers/unrepeated_sampler.py b/fastNLP/core/samplers/unrepeated_sampler.py index 3a7b813b..c5683771 100644 --- a/fastNLP/core/samplers/unrepeated_sampler.py +++ b/fastNLP/core/samplers/unrepeated_sampler.py @@ -19,15 +19,15 @@ class UnrepeatedSampler: class UnrepeatedRandomSampler(UnrepeatedSampler): - def __init__(self, dataset, shuffle: bool = False, seed: int = 0, **kwargs): - """ - 考虑在多卡evaluate的场景下,不能重复sample。 + """ + 考虑在多卡 evaluate 的场景下,不能重复 sample。 - :param dataset: 实现了 __len__ 方法的数据容器。 - :param shuffle: 如果为 True,将不进行 shuffle,实际上数据会以从长到短的方式输出。 - :param seed: 设置的随机数种子 - :param kwargs: fastNLP 保留使用 - """ + :param dataset: 实现了 __len__ 方法的数据容器。 + :param shuffle: 如果为 True,将不进行 shuffle,实际上数据会以从长到短的方式输出。 + :param seed: 设置的随机数种子 + :param kwargs: fastNLP 保留使用 + """ + def __init__(self, dataset, shuffle: bool = False, seed: int = 0, **kwargs): self.dataset = dataset self.shuffle = shuffle self.seed = seed @@ -96,16 +96,22 @@ class UnrepeatedRandomSampler(UnrepeatedSampler): class UnrepeatedSortedSampler(UnrepeatedRandomSampler): + """ + 将 dataset 中的数据根据 length 从长到短进行迭代,并且保证在多卡场景下数据不重复。本 sampler 可能导致各个机器上的 + batch 数量不完全一致。 + + :param dataset: 实现了 __len__ 方法的数据容器。 + :param length: 每条数据的长度。 + + * 为 ``List[int]`` 时 + 应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量; + * 为 ``str`` 时 + 仅当传入的 ``dataset`` 是 :class:`fastNLP.DataSet` 时,允许传入 `str` ,该 `str` 将被认为是 ``dataset`` 中的 + ``field`` 。若 field 中的元素为 ``int``,则认为该值是 sample 的长度;若不为 ``int`` ,则尝试使用 ``len`` 方法 + 获取该 ``field`` 中每个元素的长度。 + :param kwargs: fastNLP 保留使用 + """ def __init__(self, dataset, length:Union[str, List], **kwargs): - """ - 将 dataset 中的数据根据 length 从长到短进行迭代,并且保证在多卡场景下数据不重复。本 sampler 可能导致各个机器上的 - batch 数量不完全一致。 - - :param dataset: 实现了 __len__ 方法的数据容器。 - :param length: 如果为 List,应当与 dataset 有一样的长度,表示 dataset 中每个元素的数量;仅当传入的 dataset 为 fastNLP 的 - DataSet 时支持传入 str,会将该str理解为 dataset 的 field 名称,若 field 中的元素为 int,则认为该值是 sample 的长度。 - :param kwargs: fastNLP 保留使用 - """ super().__init__(dataset=dataset, shuffle=False, seed=0, **kwargs) if isinstance(dataset, DataSet) and isinstance(length, str): length = dataset.get_field(length).content @@ -125,13 +131,13 @@ class UnrepeatedSortedSampler(UnrepeatedRandomSampler): class UnrepeatedSequentialSampler(UnrepeatedRandomSampler): - def __init__(self, dataset, **kwargs): - """ - 按照顺序读取 dataset。在多卡情况下,间隔读取,例如,在两卡情况下,卡0取 [0,2,4,..], 卡1取 [1,3,5...]。 + """ + 按照顺序读取 dataset。在多卡情况下,间隔读取,例如,在两卡情况下,卡0取 [0,2,4,..], 卡1取 [1,3,5...]。 - :param dataset: 实现了 __len__ 方法的数据容器。 - :param kwargs: - """ + :param dataset: 实现了 __len__ 方法的数据容器。 + :param kwargs: + """ + def __init__(self, dataset, **kwargs): super(UnrepeatedSequentialSampler, self).__init__(dataset, shuffle=False, seed=0, **kwargs) def __iter__(self): diff --git a/fastNLP/core/utils/rich_progress.py b/fastNLP/core/utils/rich_progress.py index 53d4e281..04cb6383 100644 --- a/fastNLP/core/utils/rich_progress.py +++ b/fastNLP/core/utils/rich_progress.py @@ -44,11 +44,6 @@ class DummyFRichProgress: return True class FRichProgress(Progress, metaclass=Singleton): - """ - fastNLP 使用的 progress bar ,新增了 new_progress 函数,通过此函数即可定制 fastNLP 中所有 progress 的样式。 - - """ - def new_progess(self, *columns: Union[str, ProgressColumn], console: Optional[Console] = None, auto_refresh: bool = True, diff --git a/fastNLP/envs/distributed.py b/fastNLP/envs/distributed.py index adcfb085..706dfe00 100644 --- a/fastNLP/envs/distributed.py +++ b/fastNLP/envs/distributed.py @@ -20,13 +20,17 @@ def is_cur_env_distributed() -> bool: """ 单卡模式该函数一定返回 False; 注意进程 0 在多卡的训练模式下前后的值是不一样的,例如在开启多卡的 driver 之前,在进程 0 上的该函数返回 False;但是在开启后,在进程 0 上 - 的该函数返回的值是 True; - 多卡模式下除了进程 0 外的其它进程返回的值一定是 True; + 的该函数返回的值是 True;多卡模式下除了进程 0 外的其它进程返回的值一定是 True; """ return FASTNLP_GLOBAL_RANK in os.environ -def get_global_rank(): +def get_global_rank()->int: + """ + 获取当前进程的 global_rank 。 + + :return: + """ return int(os.environ.get(FASTNLP_GLOBAL_RANK, 0)) @@ -64,7 +68,7 @@ def rank_zero_call(fn: Callable): @contextmanager def fastnlp_no_sync_context(level=2): """ - 用于让 fastNLP 的 barrier 以及 gather/broadcast等操作等同于只有1卡的多卡程序。如果为 1 表示 fastNLP 里的barrier 操作失效; + 用于让 fastNLP 的 barrier 以及 gather/broadcast等操作等同于只有 1 卡的多卡程序。如果为 1 表示 fastNLP 里的barrier 操作失效; 如果为 2 表示 barrier 与 gather/broadcast 都失效。 :param int level: 可选 [0, 1, 2] From 93012a1004dbeece6cace8b59ef38d9cdf762817 Mon Sep 17 00:00:00 2001 From: YWMditto Date: Thu, 12 May 2022 13:49:38 +0800 Subject: [PATCH 08/14] =?UTF-8?q?evaluator=20=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/controllers/evaluator.py | 120 +++++++++++++++----------- fastNLP/core/controllers/trainer.py | 43 +++++---- fastNLP/core/utils/utils.py | 2 +- 3 files changed, 98 insertions(+), 67 deletions(-) diff --git a/fastNLP/core/controllers/evaluator.py b/fastNLP/core/controllers/evaluator.py index 8ac35ad2..18e81c8d 100644 --- a/fastNLP/core/controllers/evaluator.py +++ b/fastNLP/core/controllers/evaluator.py @@ -1,7 +1,15 @@ +r""" +``Evaluator`` 是新版 fastNLP 中用来进行评测模型的评测器,其与 ``Trainer`` 相对应,二者共同构建起了 fastNLP 中**训练**和**评测**的框架。 +``Evaluator`` 的整体架构与 ``Trainer`` 类似,也是利用 ``Driver`` 来负责底层的评测逻辑。通过使用 ``Evaluator``,您可以快速、方便、准确地 +对您的模型进行全方位地评测。 + +.. note:: + + ``Trainer`` 通过来自己内部内置一个 ``Evaluator`` 实例来支持在训练过程中进行验证的功能; +""" + from typing import Union, List, Optional, Dict, Callable -from functools import partial from dataclasses import is_dataclass -import sys __all__ = [ 'Evaluator' @@ -20,6 +28,65 @@ from fastNLP.core.log import logger class Evaluator: + """ + 用于评测模型性能好坏的评测器; + + .. note:: + + ``Evaluator`` 与 ``Trainer`` 类似,都是使用 ``Driver`` 作为底层来实现评测或者训练,因此大多数与 ``Trainer`` 同名的参数的意义和使用都与 + ``Trainer`` 中的参数相同,对于这些参数,您可以参考 ``Trainer`` 的文档来获取更详细的信息;详见 :class:`~fastNLP.core.controllers.trainer.Trainer`; + + :param model: 训练所需要的模型,例如 ``torch.nn.Module``,等价于 ``Trainer`` 中的 ``model`` 参数; + :param dataloaders: 用于评测的数据集。如果为多个,您需要使用 ``dict`` 传入,即对每一个数据集标上用于标识它们的标签; + :param metrics: 评测时使用的指标。注意该参数必须为 ``dict`` 类型,其中 ``key`` 为一个 ``metric`` 的名称,``value`` 为具体的 ``Metric`` 对象。目前支持以下 metrics: + + 1. fastNLP 自己的 ``metric``:详见 :class:`fastNLP.core.metrics.Metric`; + 2. torchmetrics; + 3. allennlp.training.metrics; + 4. paddle.metric; + + :param driver: 等价于 ``Trainer`` 中的 ``driver`` 参数; + :param device: 等价于 ``Trainer`` 中的 ``device`` 参数; + :param evaluate_batch_step_fn: 您可以传入该参数来定制每次评测一个 batch 的数据时所执行的函数。该函数应接受的两个参数为 ``evaluator`` 和 ``batch``, + 不需要有返回值;可以参考 :meth:`~fastNLP.core.controllers.loops.evaluate_batch_loop.EvaluateBatchLoop.batch_step_fn`; + :param evaluate_fn: 用来控制 ``Evaluator`` 在评测的前向传播过程中调用的是哪一个函数,例如对于 pytorch 而言,通过该参数确定使用的是 ``model.evaluate_step`` 还是 + ``model.forward``(不同训练框架所使用的的前向传播函数的方法名称不同); + + 1. 如果该值是 ``None``,那么我们会默认使用 ``evaluate_step`` 当做前向传播的函数,如果在模型中没有找到该方法,则使用训练框架默认的前向传播函数; + 2. 如果为 ``str`` 类型,例如为 ``my_evaluate_step_fn``,则尝试寻找 ``model.my_evaluate_step_fn``,如果找不到则直接报错; + + :param input_mapping: 等价于 ``Trainer`` 中的 ``input_mapping`` 参数;对具体的用于评测一个 batch 的数据使用 ``input_mapping`` 处理之后再输入到 ``model`` 以及 ``metric`` 中。如果针对 + ``model`` 和 ``metric`` 需要不同的 ``mapping``,请考虑使用 ``evaluate_batch_step_fn`` 参数定制; + + .. todo:: + + 之后链接上 参数匹配 的文档; + + :param output_mapping: 等价于 ``Trainer`` 中的 ``output_mapping`` 参数;对 ``model`` 输出的内容,将通过 ``output_mapping`` 处理之后再输入到 ``metric`` 中; + :param model_wo_auto_param_call: 等价于 ``Trainer`` 中的 ``model_wo_auto_param_call`` 参数; + + .. note:: + + 一个十分需要注意的问题在于 ``model_wo_auto_param_call`` 只会关闭部分的参数匹配,即指挥关闭前向传播时的参数匹配,但是由于 ``Evaluator`` 中 + ``metric`` 的计算都是自动化的,因此其一定需要参数匹配:根据 ``metric.update`` 的函数签名直接从字典数据中抽取其需要的参数传入进去; + + + :param fp16: 是否在评测时使用 fp16; + :param verbose: 是否打印 evaluate 的结果; + :kwargs: + * *torch_kwargs* -- 等价于 ``Trainer`` 中的 ``torch_kwargs`` 参数; + * *data_device* -- 等价于 ``Trainer`` 中的 ``data_device`` 参数; + * *model_use_eval_mode* (``bool``) -- + 是否在评测的时候将 ``model`` 的状态设置成 ``eval`` 状态。在 ``eval`` 状态下,``model`` 的 + ``dropout`` 与 ``batch normalization`` 将会关闭。默认为 ``True``。如果为 ``False``,``fastNLP`` 不会对 ``model`` 的 ``evaluate`` 状态做任何设置。无论 + 该值是什么,``fastNLP`` 都会在评测后将 ``model`` 的状态设置为 ``train``; + * *use_dist_sampler* -- + 是否使用分布式评测的方式。仅当 ``driver`` 为分布式类型时,该参数才有效。默认为根据 ``driver`` 是否支持 + 分布式进行设置。如果为 ``True``,将使得每个进程上的 ``dataloader`` 自动使用不同数据,所有进程的数据并集是整个数据集; + * *output_from_new_proc* -- 等价于 ``Trainer`` 中的 ``output_from_new_proc`` 参数; + * *progress_bar* -- 等价于 ``Trainer`` 中的 ``progress_bar`` 参数; + """ + driver: Driver _evaluate_batch_loop: Loop @@ -29,51 +96,6 @@ class Evaluator: input_mapping: Optional[Union[Callable, Dict]] = None, output_mapping: Optional[Union[Callable, Dict]] = None, model_wo_auto_param_call: bool = False, fp16: bool = False, verbose: int = 1, **kwargs): - """ - 用于对数据进行评测。 - - :param model: 待测试的模型,如果传入的 driver 为 Driver 实例,该参数将被忽略。 - :param dataloaders: 待评测的数据集。如果为多个,请使用 dict 传入。 - :param metrics: 使用的 metric 。必须为 dict 类型,其中 key 为 metric 的名称,value 为一个 Metric 对象。支持 fastNLP 的 - metric ,torchmetrics,allennlpmetrics 等。 - :param driver: 使用 driver 。 - :param device: 使用的设备。 - :param evaluate_batch_step_fn: 定制每次 evaluate batch 执行的函数。该函数应接受的两个参数为 `evaluator` 和 `batch`, - 不需要有返回值;可以参考 fastNLP.core.controllers.loops.evaluate_batch_loop.EvaluateBatchLoop中的batch_step_fn函数。 - :param evaluate_fn: 用来控制 `Evaluator` 在评测的前向传播过程中是调用哪一个函数,例如是 `model.evaluate_step` 还是 - `model.forward`;(1) 如果该值是 None,那么我们会默认使用 `evaluate_step` 当做前向传播的函数,如果在模型中没有 - 找到该方法,则使用 `model.forward` 函数;(2) 如果为 str 类型,则尝试从 model 中寻找该方法,找不到则报错。 - :param input_mapping: 对 dataloader 中输出的内容将通过 input_mapping 处理之后再输入到 model 以及 metric 中。如果针对 - model 和 metric 需要不同的 mapping,请考虑使用 evaluate_batch_step_fn 参数定制。 - :param output_mapping: 对 model 输出的内容,将通过 output_mapping 处理之后再输入到 metric 中。 - :param model_wo_auto_param_call: 是否关闭在训练时调用我们的 auto_param_call 来自动匹配 batch 和 forward 函数的参数的行为; - 如果该值为 True,并且当 batch 为字典时,我们会根据 forward 所需要的参数从 batch 中提取对应的对象,传入到 forward 函数中;如果该值 - 为 False,那么我们会将 batch 直接透传给 forward 函数。注意上述逻辑同样应用于 `train_step`, `evaluate_step` 和 `test_step`; - :param fp16: 是否使用 fp16 。 - :param verbose: 是否打印 evaluate 的结果。 - :kwargs: - * *torch_kwargs* -- 用于在指定 ``driver`` 为 'torch' 时设定具体 driver 实例的一些参数: - * ddp_kwargs -- 用于在使用 ``TorchDDPDriver`` 时指定 ``DistributedDataParallel`` 初始化时的参数;例如传入 - {'find_unused_parameters': True} 来解决有参数不参与前向运算导致的报错等; - * torch_non_blocking -- 表示用于 pytorch 的 tensor 的 to 方法的参数 non_blocking; - * *data_device* -- 表示如果用户的模型 device (在 Driver 中对应为参数 model_device)为 None 时,我们会将数据迁移到 data_device 上; - 注意如果 model_device 为 None,那么 data_device 不会起作用; - * *model_use_eval_mode* (``bool``) -- - 是否在 evaluate 的时候将 model 的状态设置成 eval 状态。在 eval 状态下,model 的 - dropout 与 batch normalization 将会关闭。默认为True。如果为 False,fastNLP 不会对 model 的 evaluate 状态做任何设置。无论 - 该值是什么,fastNLP 都会在 evaluate 接受后将 model 的状态设置为 train 。 - * *use_dist_sampler* -- - 是否使用分布式evaluate的方式。仅当 driver 为分布式类型时,该参数才有效。默认为根据 driver 是否支持 - 分布式进行设置。如果为True,将使得每个进程上的 dataloader 自动使用不同数据,所有进程的数据并集是整个数据集。 - * *output_from_new_proc* -- - 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: - ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 - log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; - * *progress_bar* -- - evaluate 的时候显示的 progress bar 。目前支持三种 [None, 'raw', 'rich', 'auto'], auto 表示如果检测 - 到当前terminal为交互型则使用 rich,否则使用 raw。 - """ - self.model = model self.metrics = metrics self.driver = choose_driver(model, driver, device, fp16=fp16, model_wo_auto_param_call=model_wo_auto_param_call, @@ -127,8 +149,10 @@ class Evaluator: self.driver.barrier() - def run(self, num_eval_batch_per_dl: int = -1, **kwargs) -> Dict: + def run(self, num_eval_batch_per_dl: int = -1) -> Dict: """ + 该函数是在 ``Evaluator`` 初始化后用于真正开始评测的函数; + 返回一个字典类型的数据,其中key为metric的名字,value为对应metric的结果。 如果存在多个metric,一个dataloader的情况,key的命名规则是 metric_indicator_name#metric_name diff --git a/fastNLP/core/controllers/trainer.py b/fastNLP/core/controllers/trainer.py index d64a39fe..36b066d6 100644 --- a/fastNLP/core/controllers/trainer.py +++ b/fastNLP/core/controllers/trainer.py @@ -43,20 +43,27 @@ class Trainer(TrainerEventTrigger): :param model: 训练所需要的模型,例如 ``torch.nn.Module``; - .. note:: + .. note:: - 当使用 pytorch 时,注意参数 ``model`` 在大多数情况下为 ``nn.Module``。但是您仍能够通过使用一些特定的组合来使用情况,如下所示: + 当使用 pytorch 时,注意参数 ``model`` 在大多数情况下为 ``nn.Module``。但是您仍能够通过使用一些特定的组合来使用情况,如下所示: - 1. 当希望使用 ``DataParallel`` 时,您应当使用 ``TorchSingleDriver``,意味着您在初始化 ``Trainer`` 时参数 ``device`` 不应当为 - 一个 ``List``; + 1. 当希望使用 ``DataParallel`` 时,您应当使用 ``TorchSingleDriver``,意味着您在初始化 ``Trainer`` 时参数 ``device`` 不应当为 + 一个 ``List``; - 2. 当您选择自己初始化 ``init_process_group`` 时(这种情况要求您传入的 ``model`` 参数一定为 ``DistributedDataParallel``), - 您应当使用 ``TorchDDPDriver``,意味着您需要通过 ``python -m torch.distributed.launch`` 的方式来启动训练,此时参数 ``device`` - 应当设置为 None(此时我们会忽略该参数),具体见下面对于参数 ``device`` 的更详细的解释。 + 2. 当您选择自己初始化 ``init_process_group`` 时(这种情况要求您传入的 ``model`` 参数一定为 ``DistributedDataParallel``), + 您应当使用 ``TorchDDPDriver``,意味着您需要通过 ``python -m torch.distributed.launch`` 的方式来启动训练,此时参数 ``device`` + 应当设置为 None(此时我们会忽略该参数),具体见下面对于参数 ``device`` 的更详细的解释。 :param driver: 训练模型所使用的具体的驱动模式,应当为以下选择中的一个:["torch"],之后我们会加入 jittor、paddle 等 国产框架的训练模式;其中 "torch" 表示使用 ``TorchSingleDriver`` 或者 ``TorchDDPDriver``,具体使用哪一种取决于参数 ``device`` 的设置; + + .. warning:: + + 因为设计上的原因,您可以直接传入一个初始化好的 ``driver`` 实例,但是需要注意的是一个 ``Driver`` 在初始化时需要 ``model`` 这一参数, + 这意味着当您传入一个 ``Driver`` 实例时,您传入给 ``Trainer`` 的 ``model`` 参数将会被忽略;也就是说模型在训练时使用的真正的模型是 + 您传入的 ``Driver`` 实例中的模型; + :param train_dataloader: 训练数据集,注意其必须是单独的一个数据集,不能是 List 或者 Dict; :param optimizers: 训练所需要的优化器;可以是单独的一个优化器实例,也可以是多个优化器组成的 List; :param device: 该参数用来指定具体训练时使用的机器;注意当该参数仅当您通过 `torch.distributed.launch/run` 启动时可以为 None, @@ -268,28 +275,28 @@ class Trainer(TrainerEventTrigger): * role_maker -- 初始化 ``fleet`` 分布式训练 API 时使用的 ``RoleMaker`` * 其它用于初始化 ``DataParallel`` 的参数; * *data_device* -- 一个具体的 driver 实例中,有 ``model_device`` 和 ``data_device``,前者表示模型所在的设备,后者表示 - 当 ``model_device`` 为 None 时应当将数据迁移到哪个设备; + 当 ``model_device`` 为 None 时应当将数据迁移到哪个设备; .. note:: - 注意您在绝大部分情况下不会用到该参数! + 注意您在绝大部分情况下不会用到该参数! - 1. 当 driver 实例的 ``model_device`` 不为 None 时,该参数无效; - 2. 对于 pytorch,仅当用户自己通过 ``python -m torch.distributed.launch`` 并且自己初始化 ``init_process_group`` 时, - driver 实例的 ``model_device`` 才会为 None; - 3. 对于 paddle,该参数无效; + 1. 当 driver 实例的 ``model_device`` 不为 None 时,该参数无效; + 2. 对于 pytorch,仅当用户自己通过 ``python -m torch.distributed.launch`` 并且自己初始化 ``init_process_group`` 时, + driver 实例的 ``model_device`` 才会为 None; + 3. 对于 paddle,该参数无效; * *use_dist_sampler* -- 表示是否使用分布式的 ``sampler``。在多卡时,分布式 ``sampler`` 将自动决定每张卡上读取的 sample ,使得一个 epoch - 内所有卡的 sample 加起来为一整个数据集的 sample。默认会根据 driver 是否为分布式进行设置。 + 内所有卡的 sample 加起来为一整个数据集的 sample。默认会根据 driver 是否为分布式进行设置。 * *evaluate_use_dist_sampler* -- 表示在 ``Evaluator`` 中在使用分布式的时候是否将 dataloader 的 ``sampler`` 替换为分布式的 ``sampler``;默认为 ``True``; * *output_from_new_proc* -- 应当为一个字符串,表示在多进程的 driver 中其它进程的输出流应当被做如何处理;其值应当为以下之一: - ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 - log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; + ["all", "ignore", "only_error"];当该参数的值不是以上值时,该值应当表示一个文件夹的名字,我们会将其他 rank 的输出流重定向到 + log 文件中,然后将 log 文件保存在通过该参数值设定的文件夹中;默认为 "only_error"; 注意该参数仅当使用分布式的 ``driver`` 时才有效,例如 ``TorchDDPDriver``; * *progress_bar* -- 以哪种方式显示 progress ,目前支持[None, 'raw', 'rich', 'auto'] 或者 RichCallback, RawTextCallback对象, - 默认为 auto , auto 表示如果检测到当前 terminal 为交互型则使用 RichCallback,否则使用 RawTextCallback对象。如果 - 需要定制 progress bar 的参数,例如打印频率等,可以传入 RichCallback, RawTextCallback 对象。 + 默认为 auto , auto 表示如果检测到当前 terminal 为交互型则使用 RichCallback,否则使用 RawTextCallback对象。如果 + 需要定制 progress bar 的参数,例如打印频率等,可以传入 RichCallback, RawTextCallback 对象。 * *train_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Trainer`` 中。与 input_mapping 互斥。 * *train_output_mapping* -- 与 output_mapping 一致,但是只用于 ``Trainer`` 中。与 output_mapping 互斥。 * *evaluate_input_mapping* -- 与 input_mapping 一致,但是只用于 ``Evaluator`` 中。与 input_mapping 互斥。 diff --git a/fastNLP/core/utils/utils.py b/fastNLP/core/utils/utils.py index 4d8bbb5e..fd5844a5 100644 --- a/fastNLP/core/utils/utils.py +++ b/fastNLP/core/utils/utils.py @@ -142,7 +142,7 @@ def auto_param_call(fn: Callable, *args, signature_fn: Optional[Callable] = None if _name not in _has_params: _has_params[_name] = _value - if len(_has_params) Date: Thu, 12 May 2022 15:52:44 +0800 Subject: [PATCH 09/14] =?UTF-8?q?Loop=20=E7=9A=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/loops/evaluate_batch_loop.py | 15 +++++++-- fastNLP/core/controllers/loops/loop.py | 33 +++++++++++++++---- .../controllers/loops/train_batch_loop.py | 22 +++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/fastNLP/core/controllers/loops/evaluate_batch_loop.py b/fastNLP/core/controllers/loops/evaluate_batch_loop.py index c81379a1..c6301772 100644 --- a/fastNLP/core/controllers/loops/evaluate_batch_loop.py +++ b/fastNLP/core/controllers/loops/evaluate_batch_loop.py @@ -10,16 +10,21 @@ from fastNLP.core.utils import match_and_substitute_params class EvaluateBatchLoop(Loop): + r""" + ``EvaluateBatchLoop`` 针对一个 dataloader 的数据完成一个 epoch 的评测迭代过程; + + :param batch_step_fn: 您可以传入该参数来替换默认的 bath_step_fn; + """ def __init__(self, batch_step_fn:Optional[Callable]=None): if batch_step_fn is not None: self.batch_step_fn = batch_step_fn def run(self, evaluator, dataloader) -> Dict: - """ + r""" 需要返回在传入的 dataloader 中的 evaluation 结果 :param evaluator: Evaluator 对象 - :param dataloader: 当前需要进行 evaluate 的dataloader + :param dataloader: 当前需要进行评测的dataloader :return: """ iterator = iter(dataloader) @@ -48,5 +53,11 @@ class EvaluateBatchLoop(Loop): @staticmethod def batch_step_fn(evaluator, batch): + r""" + 针对一个 batch 的数据的评测过程; + + :param evaluator: Evaluator 对象 + :param batch: 当前需要评测的一个 batch 的数据; + """ outputs = evaluator.evaluate_step(batch) # 将batch输入到model中得到结果 evaluator.update(batch, outputs) # evaluator将根据metric的形参名字从batch/outputs中取出对应的值进行赋值 diff --git a/fastNLP/core/controllers/loops/loop.py b/fastNLP/core/controllers/loops/loop.py index 19f5ccc6..b1952236 100644 --- a/fastNLP/core/controllers/loops/loop.py +++ b/fastNLP/core/controllers/loops/loop.py @@ -1,3 +1,12 @@ +r""" +``TrainBatchLoop`` 和 ``EvaluateBatchLoop`` 的父类,为了在实现 fastNLP 主要功能的同时保证 fastNLP 的易用性和代码的易读性,我们只对 +训练中的循环做了非常简单的抽象,``Loop`` 表示的是在训练或者评测的过程中针对单独一个 ``dataloader`` 的一个 ``epoch`` 的运算过程; + +更为具体的使用详见 :class:`~fastNLP.core.controllers.loops.train_batch_loop.TrainBatchLoop` 和 +:class:`~fastNLP.core.controllers.loops.evaluate_batch_loop.EvaluateBatchLoop` ; +""" + +from typing import Union __all__ = [ 'Loop' @@ -5,13 +14,25 @@ __all__ = [ class Loop: + r""" + ``TrainBatchLoop`` 和 ``EvaluateBatchLoop`` 的父类,您可以继承此类来定制自己的训练或者评测 ``loop``; + """ - def run(self, *args, **kwargs): - """ - 该循环的主要运行过程; - """ + def run(self, controller: Union["Trainer", "Evaluator"], dataloader): + r""" + 遍历参数 ``dataloader`` 的所有数据,使用 ``controller`` 进行训练或者评测; + + .. note:: + + ``Trainer`` 和 ``Evaluator`` 中都提供了方便您进行定制 ``Loop`` 的接口函数,例如 ``Trainer.train_step``,``Trainer.backward``等; + + 在定制您自己的 ``TrainBatchLoop`` 时,请务必记得在正确的时机调用对应的 callback 函数,详见 :class:`~fastNLP.core.controllers.loops.train_batch_loop.TrainBatchLoop` + 中对于 callback 函数的调用; - def step(self, *args, **kwargs): """ - 该循环运行过程中的一步; + + @staticmethod + def batch_step_fn(controller: Union["Trainer", "Evaluator"], batch): + r""" + 对于具体的一个 batch 的数据,实现训练或者评测过程中的一步; """ \ No newline at end of file diff --git a/fastNLP/core/controllers/loops/train_batch_loop.py b/fastNLP/core/controllers/loops/train_batch_loop.py index 7bb9b653..48485226 100644 --- a/fastNLP/core/controllers/loops/train_batch_loop.py +++ b/fastNLP/core/controllers/loops/train_batch_loop.py @@ -11,11 +11,27 @@ from fastNLP.core.utils.exceptions import EarlyStopException class TrainBatchLoop(Loop): + r""" + ``TrainBatchLoop`` 针对一个 dataloader 的数据完成一个 epoch 的训练迭代过程; + + :param batch_step_fn: 您可以传入该参数来替换默认的 bath_step_fn; + """ + def __init__(self, batch_step_fn: Optional[Callable] = None): if batch_step_fn is not None: self.batch_step_fn = batch_step_fn def run(self, trainer, dataloader): + r""" + 对传入的 dataloader 进行一个 epoch 的主要的训练的循环过程; + + .. note:: + + 您不需要自己主动地调用该方法,``Trainer`` 会负责调用该方法来完成训练过程; + + :param trainer: ``Trainer`` 实例; + :param dataloader: 当前训练所使用的 dataloader; + """ get_batch_indices = dataloader.get_batch_indices if callable(getattr(dataloader, 'get_batch_indices', None))\ else lambda *args, **kwargs: None dataloader = iter(dataloader) @@ -49,6 +65,12 @@ class TrainBatchLoop(Loop): @staticmethod def batch_step_fn(trainer, batch): + r""" + 针对一个 batch 的数据的训练过程; + + :param trainer: ``Trainer`` 实例; + :param batch: 一个 batch 的数据; + """ outputs = trainer.train_step(batch) trainer.backward(outputs) trainer.step() From 8de1477aa15fe85d0d3122c10b11ea12ffaf3554 Mon Sep 17 00:00:00 2001 From: YWMditto Date: Thu, 12 May 2022 16:10:17 +0800 Subject: [PATCH 10/14] =?UTF-8?q?controllers.utils=20=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/controllers/utils/state.py | 16 ++---------- fastNLP/core/controllers/utils/utils.py | 33 +++++++++++++++++-------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/fastNLP/core/controllers/utils/state.py b/fastNLP/core/controllers/utils/state.py index 528ab529..a8103c62 100644 --- a/fastNLP/core/controllers/utils/state.py +++ b/fastNLP/core/controllers/utils/state.py @@ -1,19 +1,6 @@ -""" - -该 Module 用来实现一个用于记载用户 callback 实时数据的 state,该 state 实际上是一个 字典,我们通过复用 __getattr__ 方法来实现类似 -类属性的字典调用方式; - -提供该类的主要目的在于与 Filter 中的特殊的 filter_fn 合作,方便用户能够使用到自己想要的一切特殊的定制方式; - -这一特殊的 Filter 实现需要用户记录一些特殊的状态值,例如 accuracy 等,但是我们不希望用户将这些状态值直接挂在 trainer 实例上,因为这样会 -污染 trainer 自己的类属性,从而可能导致一些莫名其妙的 bug; - -我们开放 state 用于用户这一特殊的定制选择; -""" from dataclasses import dataclass from typing import Optional, Dict - __all__ = [ 'State', 'TrainerState' @@ -22,7 +9,8 @@ __all__ = [ class State(dict): r""" - 提供给用户使用的 state; + 提供给用户使用的 ``state``,用来记载您的 ``callback`` 实时数据,该 ``state`` 实际上是一个字典,我们通过复用 ``__getattr__`` 方法来实现类似 + 类属性的字典调用方式; 为了实现断点重训,用户应当保证其保存的信息都是可序列化的; diff --git a/fastNLP/core/controllers/utils/utils.py b/fastNLP/core/controllers/utils/utils.py index a2b2d5ae..ef3cf98c 100644 --- a/fastNLP/core/controllers/utils/utils.py +++ b/fastNLP/core/controllers/utils/utils.py @@ -1,4 +1,3 @@ -import inspect from typing import Dict from fastNLP.core.callbacks import CallbackManager @@ -7,10 +6,10 @@ from fastNLP.core.utils.utils import _check_valid_parameters_number class TrainerEventTrigger: - """ + r""" 为了避免在训练流程中调用 callback 函数中写成类似 'trainer.callback_manager.on_train_begin' 的形式,我们选择单独抽象为 'Trainer' - 抽象一层,然后一些特殊的操作可以在这里进行,例如我们通过 `on_validate_end` 来通知所有的 'CheckpointCallback' 实例在当前的 step 后保存 - 模型。 + 抽象一层,然后一些特殊的操作可以在这里进行,例如我们通过 `on_validate_end` 来通知所有的 'CheckpointCallback' 实例在当前的 step 后保存 + 模型。 """ callback_manager: CallbackManager trainer_state: TrainerState @@ -90,13 +89,21 @@ class TrainerEventTrigger: class _TruncatedDataLoader: + r""" + ``_TruncatedDataLoader`` 用于实现 ``Trainer`` 和 ``Evaluator`` 中的 '预跑' 和 '假跑' 功能: + + 1. 预跑 是针对 trainer 的验证而言的,即我们在正式的训练前会先使用 trainer 内置的 evaluator(如果不为 None)评测数量非常少的数据, + 来检验用户的 metric 和 evaluate_dataloader 以及模型是否能够合作完成正确的评测过程; + 2. 假跑 的意思是改变每一个 epoch 中训练或者评测的实际的 batch 的数量,例如改成 10,来让模型快速地迭代整体的训练或者评测流程,来查看 + 整体的过程的正确性; + + ``_TruncatedDataLoader`` 的实现也非常简单,我们在该类中内置一个计数器,当迭代器的迭代数量达到指定数值后 ``raise StopIteration``; + + :param dataloader: 可迭代的 dataloader 。 + :param num_batches: 迭代多少个 batch 就停止。 + """ def __init__(self, dataloader, num_batches: int): - """ - 限制 - :param dataloader: 可迭代的 dataloader 。 - :param num_batches: 迭代多少个 batch 就停止。 - """ self.dataloader = dataloader self._num_batches = min(num_batches, len(dataloader)) self._count = 0 @@ -104,7 +111,6 @@ class _TruncatedDataLoader: def __len__(self): r""" 为了在外部调用 `len` 方法时正确地返回当前会迭代的长度; - """ return self._num_batches @@ -127,6 +133,13 @@ class _TruncatedDataLoader: def check_evaluate_every(evaluate_every): + r""" + 检验用户传入的 ``evaluate_every`` 参数是否合法; + + ``evaluate_every`` 的使用详见 ``Trainer`` 的 ``evaluate_every`` 参数; + + 主要在于当参数 ``evaluate_every`` 是一个 callable 的函数时,需要保证其参数的正确性; + """ if not callable(evaluate_every) and (not isinstance(evaluate_every, int) or evaluate_every == 0): raise ValueError("Parameter 'evaluate_every' should be set to 'int' type and either < 0 or > 0.") if callable(evaluate_every): From f4c284a6bb4005f00d801d9902940b7ce5d516bb Mon Sep 17 00:00:00 2001 From: YWMditto Date: Thu, 12 May 2022 20:10:52 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E4=BA=86=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callbacks/callback.py | 140 +++---- fastNLP/core/callbacks/callback_manager.py | 10 +- fastNLP/core/drivers/driver.py | 182 ++++---- fastNLP/core/drivers/torch_driver/ddp.py | 390 +++++++++++------- .../core/drivers/torch_driver/dist_utils.py | 21 + .../drivers/torch_driver/single_device.py | 23 +- .../core/drivers/torch_driver/torch_driver.py | 40 +- fastNLP/core/drivers/torch_driver/utils.py | 25 +- fastNLP/core/drivers/utils.py | 6 +- 9 files changed, 502 insertions(+), 335 deletions(-) diff --git a/fastNLP/core/callbacks/callback.py b/fastNLP/core/callbacks/callback.py index e895404b..f4c98c31 100644 --- a/fastNLP/core/callbacks/callback.py +++ b/fastNLP/core/callbacks/callback.py @@ -49,12 +49,17 @@ class Callback: def on_after_trainer_initialized(self, trainer, driver): r""" 在 `Trainer` 初始化后会被触发; + + :param trainer: ``Trainer`` 实例; + :param driver: ``Trainer`` 中的 ``driver`` 实例; """ pass def on_sanity_check_begin(self, trainer): r""" 在 '预跑'检测 开始前会被触发; + + :param trainer: ``Trainer`` 实例; """ pass @@ -62,9 +67,8 @@ class Callback: r""" 在 '预跑'检测 开始后会被触发; - :param trainer: - :param sanity_check_res: 预跑的 evaluate 结果 - :return: + :param trainer: ``Trainer`` 实例; + :param sanity_check_res: 预跑得到的评测结果,关于对于 **预跑** 的解释,请见 :meth:`~fastNLP.core.controllers.trainer.Trainer.run`; """ pass @@ -72,8 +76,7 @@ class Callback: r""" 在训练开始前会被触发; - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass @@ -81,8 +84,7 @@ class Callback: r""" 在训练完成后会被触发; - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass @@ -90,8 +92,7 @@ class Callback: r""" 在训练过程中的每一个 epoch 开始前会被触发; - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass @@ -99,8 +100,7 @@ class Callback: r""" 在训练过程中的每一个 epoch 完成后会被触发;此时 trainer.cur_epoch_idx 已经完成加 1 操作。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass @@ -108,8 +108,7 @@ class Callback: r""" 在训练过程中准备取出下一个 batch 的数据时触发 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass @@ -117,178 +116,161 @@ class Callback: r""" 在训练过程中拿到当前的 batch 数据后会被触发; - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_train_batch_begin(self, trainer, batch, indices): r""" - 在取得数据,执行完 input_mapping (如果 Trainer 传有该参数),并且移动 batch 中的 tensor 到了指定设备。 - 其中 batch 中的数据格式要么是 Dataloader 返回的每个 batch 的格式;要么是 input_mapping 之后的内容。 - 如果 batch 是 dict 类型,直接增删其中的 key 或 修改其中的 value 会影响到输入到 model 的中的 batch 数据。 + 在取得数据,执行完 ``input_mapping`` (如果 ``Trainer`` 传有该参数),并且移动 ``batch`` 中的 ``tensor`` 到了指定设备。 + 其中 ``batch`` 中的数据格式要么是 ``Dataloader`` 返回的每个 ``batch`` 的格式;要么是 ``input_mapping`` 之后的内容。 + 如果 ``batch`` 是 ``dict`` 类型,直接增删其中的 ``key`` 或 修改其中的 ``value`` 会影响到输入到 ``model`` 的中的 ``batch`` 数据。 - :param trainer: `fastNLP.Trainer` - :param batch: batch 的数据,已经经过 input_mapping (如果有) 以及 移动到指定设备 。 - :param list[int] indices: 当前的 batch 是 dataset 中的哪些数据。仅在 DataLoader 支持得到当前 batch index 的时候有值, + :param trainer: ``Trainer`` 实例; + :param batch: batch 的数据,已经经过 ``input_mapping`` (如果有) 以及移动到指定设备 。 + :param list[int] indices: 当前的 ``batch`` 是 ``dataset`` 中的哪些数据。仅在 ``DataLoader`` 支持得到当前 ``batch index`` 的时候有值, 其它时候为 None 。 """ pass def on_train_batch_end(self, trainer): - """ + r""" 完成一个 batch 的训练(forward)、梯度回传(backward)、梯度更新(step)、梯度置零、batch_idx_in_epoch与 global_forward_batches累计加1操作。其中梯度更新】梯度置零操作会考虑 accumulation_steps ,所以不一定在当前 batch 会 执行。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_exception(self, trainer, exception): - """ + r""" 在训练过程遇到异常时调用。 - :param trainer: - :param exception: 遭遇的异常。 - :return: + :param trainer: ``Trainer`` 实例; + :param exception: 遭遇的异常; """ pass def on_save_model(self, trainer): - """ + r""" 当将要保存模型时调用,此刻模型还未保存。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_load_model(self, trainer): - """ + r""" 当将要加载模型时调用,此刻模型还未加载。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_save_checkpoint(self, trainer) -> Dict: - """ - 当 Trainer 将要保存 checkpoint 的时候触发,该函数用于保存当前 callback 在恢复需要的相关数据。 + r""" + 当 ``Trainer`` 将要保存 ``checkpoint`` 的时候触发,该函数用于保存当前 ``callback`` 在恢复需要的相关数据。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_load_checkpoint(self, trainer, states: Optional[Dict]): r""" - 当 Trainer 要恢复 checkpoint 的时候触发( Trainer 与 Driver 已经加载好自身的状态),参数 states 为 on_save_checkpoint() + 当 ``Trainer`` 要恢复 ``checkpoint`` 的时候触发( ``Trainer`` 与 ``Driver`` 已经加载好自身的状态),参数 ``states`` 为 ``on_save_checkpoint()``; 的返回值。 - :param trainer: + :param trainer: ``Trainer`` 实例; :param states: - :return: """ pass def on_before_backward(self, trainer, outputs): - """ + r""" 在 backward 前执行。 - :param trainer: - :param outputs: model 的返回内容。如果有 output_mapping ,则 outputs 中的内容为已经执行了 output_mapping 后的结果。 - :return: + :param trainer: ``Trainer`` 实例; + :param outputs: ``model`` 的返回内容。如果有 ``output_mapping``,则 ``outputs`` 中的内容为已经执行了 ``output_mapping`` 后的结果。 """ pass def on_after_backward(self, trainer): - """ - 在 backward 后执行。在多卡场景下,由于 accumulation_steps 的影响,仅在需要真正 update 参数那次梯度回传才会触发梯度同步, - 因此在多卡且使用 accumulation_steps 时,可能存在某些 step 各卡上梯度不一致的问题。 + r""" + 在 ``backward`` 后执行。在多卡场景下,由于 ``accumulation_steps`` 的影响,仅在需要真正 ``update`` 参数那次梯度回传才会触发梯度同步, + 因此在多卡且使用 ``accumulation_steps`` 时,可能存在某些 ``step`` 各卡上梯度不一致的问题。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_before_optimizers_step(self, trainer, optimizers): - """ + r""" 在进行 optimizer 优化进行前调用。该接口不一定每次前向计算都会触发,实际调用会受到 accumulation_steps 的影响。 - :param trainer: - :param optimizers: 优化器,内容为在 Trainer 初始化时传入的值。 - :return: + :param trainer: ``Trainer`` 实例; + :param optimizers: 优化器,内容为在 ``Trainer`` 初始化时传入的值。 """ pass def on_after_optimizers_step(self, trainer, optimizers): - """ + r""" 在进行 optimizer 优化进行后调用。该接口不一定每次前向计算都会触发,实际调用会受到 accumulation_steps 的影响。 - :param trainer: - :param optimizers: 优化器,内容为在 Trainer 初始化时传入的值。 - :return: + :param trainer: ``Trainer`` 实例; + :param optimizers: 优化器,内容为在 ``Trainer`` 初始化时传入的值。 """ pass def on_before_zero_grad(self, trainer, optimizers): - """ + r""" 在进行模型梯度置零前调用。该接口不一定每次前向计算都会触发,实际调用会受到 accumulation_steps 的影响。 - :param trainer: - :param optimizers: 优化器,内容为在 Trainer 初始化时传入的值。 - :return: + :param trainer: ``Trainer`` 实例; + :param optimizers: 优化器,内容为在 ``Trainer`` 初始化时传入的值。 """ pass def on_after_zero_grad(self, trainer, optimizers): - """ + r""" 在进行模型梯度置零后调用。该接口不一定每次前向计算都会触发,实际调用会受到 accumulation_steps 的影响。 - :param trainer: - :param optimizers: 优化器,内容为在 Trainer 初始化时传入的值。 - :return: + :param trainer: ``Trainer`` 实例; + :param optimizers: 优化器,内容为在 ``Trainer`` 初始化时传入的值。 """ pass def on_evaluate_begin(self, trainer): - """ + r""" 在将要进行 evaluate 时调用。如果是设置的以 step 数量 或 自定义地 决定 evaluate 的频率,该接口是在 on_train_batch_end 之后 进行调用。如果是以 epoch 数量决定调用,该接口是在 on_train_epoch_end 之后调用。 - :param trainer: - :return: + :param trainer: ``Trainer`` 实例; """ pass def on_evaluate_end(self, trainer, results): - """ + r""" 结束 evaluate 时调用,并把 evaluate 的结果传入。 - :param trainer: - :param results: Evaluate 的结果,一般是个 dict 。 - :return: + :param trainer: ``Trainer`` 实例; + :param results: ``Trainer`` 内置的 ``Evaluator`` 评测的结果,通常是个 ``dict``; """ pass @property def callback_name(self): - """ - callback 的名称,我们会使用该名称从 checkpoint 中读取的相应的 state 并传递给 on_load_checkpoint() 函数。 + r""" + ``callback`` 的名称,我们会使用该名称从 ``checkpoint`` 中读取的相应的 ``state`` 并传递给 ``on_load_checkpoint()`` 函数。 - :return: + :return: 返回用于区分该 ``callback`` 实例的 ``name``; """ return self.__class__.__name__ @property def need_reproducible_sampler(self) -> bool: - """ + r""" 当前 callback 是否需要能够复现的 sampler 。一般用于 checkpoint 类的 callback 。 - - :return: """ return False diff --git a/fastNLP/core/callbacks/callback_manager.py b/fastNLP/core/callbacks/callback_manager.py index 60c9b17d..d3d8ae75 100644 --- a/fastNLP/core/callbacks/callback_manager.py +++ b/fastNLP/core/callbacks/callback_manager.py @@ -29,11 +29,10 @@ def _transfer(func): return wrapper -def prepare_callbacks(callbacks, progress_bar): +def prepare_callbacks(callbacks, progress_bar: str): """ - - :param callbacks: - :param progress_bar: + :param callbacks: 对用户传入的类 ``callback`` 进行检查,查看是否是否继承了我们的 ``Callback`` 类; + :param progress_bar: 选择怎样的 ``progress_bar`` 给 ``Trainer`` 使用; :return: """ _callbacks = [] @@ -81,7 +80,7 @@ class CallbackManager: 2. 通过 `Trainer` 的参数 `callbacks` 添加的 callback 类; 3. 通过 `Trainer.add_callback_fn` 添加的 callback 函数; - :param callbacks: 初始化时可以传入的一系列 callback 类,通常为用户在初始化 'Trainer' 时直接传入的 callback 类; + :param callbacks: 初始化时可以传入的一系列 callback 类,通常为用户在初始化 ``Trainer`` 时直接传入的 callback 类; """ self._need_reproducible_sampler = False @@ -158,7 +157,6 @@ class CallbackManager: "filter_states": {"on_train_begin": filter1.state_dict(), ...} } } - """ states = {} diff --git a/fastNLP/core/drivers/driver.py b/fastNLP/core/drivers/driver.py index 6ce168cb..28fd7e4a 100644 --- a/fastNLP/core/drivers/driver.py +++ b/fastNLP/core/drivers/driver.py @@ -1,7 +1,7 @@ import os import signal import sys -from typing import Any, Sequence, List, Optional, Callable, Dict, Union, Tuple +from typing import Sequence, List, Optional, Callable, Dict, Union, Tuple from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path @@ -19,13 +19,11 @@ class Driver(ABC): r""" 用来初始化 `Driver` 的基类,所有定制的 `driver` 都需要继承此类; fastNLP 提供的 driver 实例都会同时被 Trainer 和 Evaluator 调用; + :param model: 训练或者评测的模型,需要注意该模型可能为用户已经使用类似 `torch.nn.DataParallel` 或者 + `torch.nn.parallel.DistributedDataParallel` 包裹过的模型; """ def __init__(self, model): - r""" - :param model: 训练或者评测的模型,需要注意该模型可能为用户已经使用类似 `torch.nn.DataParallel` 或者 - `torch.nn.parallel.DistributedDataParallel` 包裹过的模型; - """ self.model = model # 这些属性用于 open_subprocess 和 on_exception 函数协同配合; @@ -36,24 +34,26 @@ class Driver(ABC): def setup(self): r""" 该函数用来初始化训练环境,例如将模型迁移到对应的设备上等; - 多卡的 driver 的该函数要更为复杂一些,例如其可能需要开启多进程之间的通信环境,以及设置一些环境变量和其余所需要的变量值; + 多卡的 ``driver`` 的该函数要更为复杂一些,例如其可能需要开启多进程之间的通信环境,以及设置一些环境变量和其余所需要的变量值; """ def set_dist_repro_dataloader(self, dataloader, dist=None, reproducible: bool = False): r""" - 根据输入的 dataloader 得到一个 支持分布式 (distributed) 与 可复现的 (reproducible) 的 dataloader。 + 根据输入的 ``dataloader`` 得到一个 支持分布式 (``distributed``) 与 可复现的 (``reproducible``) 的 dataloader。 + + :param dataloader: 根据 ``dataloade``r 设置其对应的分布式版本以及可复现版本; + :param dist: 应当为一个字符串,其值应当为以下之一:``[None, "dist", "unrepeatdist"]``;为 ``None`` 时,表示不需要考虑当前 dataloader + 切换为分布式状态;为 ``dist`` 时,表示该 dataloader 应该保证每个 gpu 上返回的 batch 的数量是一样多的,允许出现少量 sample ,在 + 不同 gpu 上出现重复;为 ``unrepeatdist`` 时,表示该 dataloader 应该保证所有 gpu 上迭代出来的数据合并起来应该刚好等于原始的 + 数据,允许不同 gpu 上 batch 的数量不一致。 - :param dataloader: 根据 dataloader 设置其对应的分布式版本以及可复现版本 - :param dist: 应当为一个字符串,其值应当为以下之一:[None, "dist", "unrepeatdist"];为 None 时,表示不需要考虑当前 dataloader - 切换为分布式状态;为 'dist' 时,表示该 dataloader 应该保证每个 gpu 上返回的 batch 的数量是一样多的,允许出现少量 sample ,在 - 不同 gpu 上出现重复;为 'unrepeatdist' 时,表示该 dataloader 应该保证所有 gpu 上迭代出来的数据合并起来应该刚好等于原始的 - 数据,允许不同 gpu 上 batch 的数量不一致。其中 trainer 中 kwargs 的参数 `use_dist_sampler` 为 True 时,该值为 "dist"; - 否则为 None ,evaluator 中的 kwargs 的参数 `use_dist_sampler` 为 True 时,该值为 "unrepeatdist",否则为 None; + 其中 trainer 中 kwargs 的参数 ``use_dist_sampler`` 为 ``True`` 时,该值为 ``dist``; + 否则为 ``None``,evaluator 中的 kwargs 的参数 ``use_dist_sampler`` 为 ``True`` 时,该值为 ``unrepeatdist``,否则为 ``None``; 注意当 dist 为 ReproducibleSampler, ReproducibleBatchSampler 时,是断点重训加载时 driver.load 函数在调用; 当 dist 为 str 或者 None 时,是 trainer 在初始化时调用该函数; - :param reproducible: 如果为 False ,不要做任何考虑;如果为 True ,需要保证返回的 dataloader 可以保存当前的迭代状态,使得 - 可以可以加载。 + :param reproducible: 如果为 ``False``,不要做任何考虑;如果为 ``True``,需要保证返回的 dataloader 可以保存当前的迭代状态,使得 + 该状态可以加载到一个全新的 dataloader 中然后恢复其状态; :return: 应当返回一个被替换 sampler 后的新的 dataloader 对象 (注意此处一定需要返回一个新的 dataloader 对象) ;此外, 如果传入的 dataloader 中是 ReproducibleSampler 或者 ReproducibleBatchSampler 需要重新初始化一个放入返回的 dataloader 中。如果 dist 为空,且 reproducible 为 False,可直接返回原对象。 @@ -65,50 +65,50 @@ class Driver(ABC): def set_deterministic_dataloader(self, dataloader): r""" - 为了确定性训练要对 dataloader 进行修改,保证在确定随机数种子后,每次重新训练得到的结果是一样的;例如对于 torch 的 dataloader,其 - 需要将 worker_init_fn 替换; + 为了确定性训练要对 ``dataloader`` 进行修改,保证在确定随机数种子后,每次重新训练得到的结果是一样的;例如对于 ``pytorch`` 的 ``dataloader``,其 + 需要将 ``worker_init_fn`` 替换; """ def set_sampler_epoch(self, dataloader, cur_epoch_idx): r""" - 对于分布式的 sampler,例如 torch 的 DistributedSampler,其需要在每一个 epoch 前设置随机数种子,来保证每一个进程上的 shuffle 是一样的; - dataloader 中可能真正发挥作用的是 batch_sampler 也可能是 sampler。 + 对于分布式的 ``sampler``,例如 ``pytorch`` 的 ``DistributedSampler``,其需要在每一个 ``epoch`` 前设置随机数种子,来保证每一个进程上的 ``shuffle`` 是一样的; + ``dataloader`` 中可能真正发挥作用的是 ``batch_sampler`` 也可能是 ``sampler``。 - :param dataloader: 需要设置 epoch 的 dataloader 。 - :param cur_epoch_idx: 当前是第几个 epoch; + :param dataloader: 需要设置 ``epoch`` 的 ``dataloader``; + :param cur_epoch_idx: 当前是第几个 ``epoch``; """ @abstractmethod def model_call(self, batch, fn: Callable, signature_fn: Optional[Callable]) -> Dict: - """ - 通过调用 `fn` 来实现训练时的前向传播过程; - 注意 Trainer 和 Evaluator 会调用该函数来实现网络的前向传播过程,其中传入该函数的参数 `fn` 是函数 `get_model_call_fn` 所返回的 + r""" + 通过调用 ``fn`` 来实现训练时的前向传播过程; + 注意 ``Trainer`` 和 ``Evaluator`` 会调用该函数来实现网络的前向传播过程,其中传入该函数的参数 ``fn`` 是函数 ``get_model_call_fn`` 所返回的 函数; :param batch: 当前的一个 batch 的数据;可以为字典或者其它类型; :param fn: 调用该函数进行一次计算。 - :param signature_fn: 由 Trainer 传入的用于网络前向传播一次的签名函数,因为当 batch 是一个 Dict 的时候,我们会自动调用 auto_param_call 函 - 数,而一些被包裹的模型需要暴露其真正的函数签名,例如 DistributedDataParallel 的调用函数是 forward,但是需要其函数签名为 model.module.forward; - :return: 返回由 `fn` 返回的结果(应当为一个 dict 或者 dataclass,但是不需要我们去检查); + :param signature_fn: 由 ``Trainer`` 传入的用于网络前向传播一次的签名函数,因为当 batch 是一个 ``Dict`` 的时候,我们会自动调用 ``auto_param_call`` 函 + 数,而一些被包裹的模型需要暴露其真正的函数签名,例如 ``DistributedDataParallel`` 的调用函数是 ``forward``,但是需要其函数签名为 ``model.module.forward``; + :return: 返回由 ``fn`` 返回的结果(应当为一个 ``dict`` 或者 ``dataclass``,但是不需要我们去检查); """ raise NotImplementedError("Each specific driver should implemented its own `model_call` function.") @abstractmethod def get_model_call_fn(self, fn: str) -> Tuple: - """ - 该函数会接受 Trainer 的 train_fn 或者 Evaluator 的 evaluate_fn,返回一个实际用于调用 driver.model_call 时传入的函数参数; - 该函数会在 Trainer 和 Evaluator 在 driver.setup 函数之后调用; + r""" + 该函数会接受 ``Trainer`` 的 ``train_fn`` 或者 ``Evaluator`` 的 ``evaluate_fn``,返回一个实际用于调用 ``driver.model_call`` 时传入的函数参数; + 该函数会在 ``Trainer`` 和 ``Evaluator`` 在 ``driver.setup`` 函数之后调用; 之所以设置该函数的目的在于希望将具体的 model_call function 从 driver 中抽离出来,然后将其附着在 Trainer 或者 Evaluator 身上; - 这样是因为在新版的设计中,使用 model 的哪种方法来进行 `train step` 或者 `evaluate step` 是通过额外的参数 `train_fn` 和 - `evaluate_fn` 来确定的,而二者又分别是通过 Trainer 和 Evaluator 来控制的;因此不能将确定具体的 `train step fn` 和 - `evaluate step fn` 的逻辑放在每一个 driver 的初始化的时候(因此在 Trainer 初始化第一个 driver 时,Evaluator 还没有初始化,但是 - `evaluate step fn` 的确定却需要 Evaluator 的初始化),因此我们将这一逻辑抽象到这一函数当中; + 这样是因为在新版的设计中,使用 model 的哪种方法来进行 ``train step`` 或者 ``evaluate step`` 是通过额外的参数 ``train_fn`` 和 + ``evaluate_fn`` 来确定的,而二者又分别是通过 Trainer 和 Evaluator 来控制的;因此不能将确定具体的 ``train step fn`` 和 + ``evaluate step fn`` 的逻辑放在每一个 driver 的初始化的时候(因此在 Trainer 初始化第一个 driver 时,Evaluator 还没有初始化,但是 + ``evaluate step fn`` 的确定却需要 Evaluator 的初始化),因此我们将这一逻辑抽象到这一函数当中; - 这一函数应当通过参数 `fn` 来判断应当返回的实际的调用的函数,具体逻辑如下所示: - 1. 如果 fn == "train_step" or "evaluate_step",那么对传入的模型进行检测,如果模型没有定义方法 `fn`,则默认调用模型的 `forward` + 这一函数应当通过参数 ``fn`` 来判断应当返回的实际的调用的函数,具体逻辑如下所示: + 1. 如果 fn == "train_step" or "evaluate_step",那么对传入的模型进行检测,如果模型没有定义方法 ``fn``,则默认调用模型的 ``forward`` 函数,然后给出 warning; - 2. 如果 fn 是其他字符串,那么如果模型没有定义方法 `fn` 则直接报错; + 2. 如果 fn 是其他字符串,那么如果模型没有定义方法 ``fn`` 则直接报错; 注意不同的 driver 需要做额外的检测处理,例如在 DDPDriver 中,当传入的模型本身就是 DistributedDataParallel 中,我们只能调用模型的 forward 函数,因此需要额外的 warning;这一点特别需要注意的问题在于 driver 自己在 setup 时也会对模型进行改变(DDPDriver),因此 @@ -121,6 +121,9 @@ class Driver(ABC): @property def model(self): + r""" + :return: 返回 driver 中在实际训练或者评测时所使用的模型; + """ return self._model @model.setter @@ -147,6 +150,9 @@ class Driver(ABC): @property def model_device(self): + r""" + :return: 返回 driver 中模型实际所在的设备; + """ return self._model_device @model_device.setter @@ -155,28 +161,30 @@ class Driver(ABC): @property def data_device(self): + """ + :return: 返回 driver 中数据默认会被迁移到的设备; + """ return self.model_device @staticmethod def _check_optimizer_legality(optimizers): - """ + r""" 对于用户传入 trainer 的每一个 optimizer,检测其是否合理,因为不同的深度学习框架所使用的的 optimizer 是不相同的; :param optimizers: 需要检测的 `optimizers`; """ - raise NotImplementedError("Each specific driver should implemented its own `_check_optimizer_legality` function.") + raise NotImplementedError( + "Each specific driver should implemented its own `_check_optimizer_legality` function.") def set_optimizers(self, optimizers=None): - """ + r""" trainer 会调用该函数将用户传入的 optimizers 挂载到 driver 实例上; - :param optimizers: - :return: """ self.optimizers = optimizers @abstractmethod def backward(self, loss): - """ + r""" 实现深度学习中的反向传播过程; :param loss: 用来实现反向传播的损失函数值; @@ -219,7 +227,7 @@ class Driver(ABC): @property def auto_cast(self): - """ + r""" fp16 的上下文环境; :return: 返回一个用于 fp16 计算的上下文环境; @@ -246,7 +254,7 @@ class Driver(ABC): r""" 加载模型的函数;将 filepath 中的模型加载并赋值给当前 model 。 - :param filepath: 需要被加载的对象的文件位置(需要包括文件名)或一个 BytesIO 对象; + :param filepath: 需要被加载的对象的文件位置(需要包括文件名)或一个 ``BytesIO`` 对象; :param load_state_dict: 保存的文件是否只是模型的权重,还是完整的模型。即便是保存的完整的模型,此处也只能使用尝试加载filepath 模型中的权重到自身模型,而不会直接替代当前 Driver 中的模型。 :return: 返回加载指定文件后的结果; @@ -254,7 +262,8 @@ class Driver(ABC): raise NotImplementedError("Each specific driver should implemented its own `load_model` function.") @abstractmethod - def save(self, folder, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): + def save(self, folder, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, + **kwargs): r""" 断点重训的保存函数,该函数会负责保存模型和 optimizers, fp16 的 state_dict;以及模型的保存(若 should_save_model 为 True) @@ -271,7 +280,8 @@ class Driver(ABC): raise NotImplementedError("Each specific driver should implemented its own `save` function.") @abstractmethod - def load(self, folder: Union[str, Path], dataloader, only_state_dict: bool =True, should_load_model: bool = True, **kwargs) -> Dict: + def load(self, folder: Union[str, Path], dataloader, only_state_dict: bool = True, should_load_model: bool = True, + **kwargs) -> Dict: r""" 断点重训的加载函数,注意该函数会负责读取数据,并且恢复 optimizers , fp16 的 state_dict 和 模型(根据 should_load_model )和; 其它在 Driver.save() 函数中执行的保存操作,然后将一个 state 字典返回给 trainer ( 内容为Driver.save() 接受到的 states )。 @@ -287,28 +297,30 @@ class Driver(ABC): :param should_load_model: 是否应该加载模型,如果为False,Driver 将不负责加载模型。若该参数为 True ,但在保存的状态中没有 找到对应的模型状态,则报错。 :return: 需要返回 save 函数输入的 states 内容 - 'dataloader',返回的是根据传入的 dataloader 与 保存的状态一起设置为合理的状态,可以返回的对象与传入的dataloader是同一个。 - 在保存与当前传入 data sample 数目不一致时报错。 - 'batch_idx_in_epoch': int 类型的数据,表明当前 epoch 进行到了进行到了第几个 batch 了。 请注意,该值不能是只能通过保存的 - 数据中读取的,因为前后两次运行 batch_size 可能由变化。该数字的原则应该符合以下等式 - '返回 dataloader 还会产生的batch数量' + 'batch_idx_in_epoch' = '原来不断点训练的batch的总数' 。 - 由于 '返回 dataloader 还会产生的batch数量' 这个数量在 batch_size 与 drop_last 参数给定的情况下,无法改变,因此 - 只能通过调整 batch_idx_in_epoch 这个值来使等式成立。一个简单的计算原则如下 - 当drop_last为True,等同于 floor(sample_in_this_rank/batch_size) - floor(num_left_samples/batch_size); - 当drop_last为False,等同于 ceil(sample_in_this_rank/batch_size) - ceil(num_left_samples/batch_size)。 + + * *dataloader* -- 返回的是根据传入的 dataloader 与 保存的状态一起设置为合理的状态,可以返回的对象与传入的dataloader是同一个。 + 在保存与当前传入 data sample 数目不一致时报错。 + + * *batch_idx_in_epoch* -- int 类型的数据,表明当前 epoch 进行到了进行到了第几个 batch 了。 请注意,该值不能是只能通过保存的 + 数据中读取的,因为前后两次运行 batch_size 可能由变化。该数字的原则应该符合以下等式 + '返回 dataloader 还会产生的batch数量' + 'batch_idx_in_epoch' = '原来不断点训练的batch的总数' 。 + 由于 '返回 dataloader 还会产生的batch数量' 这个数量在 batch_size 与 drop_last 参数给定的情况下,无法改变,因此 + 只能通过调整 batch_idx_in_epoch 这个值来使等式成立。一个简单的计算原则如下 + 当drop_last为True,等同于 floor(sample_in_this_rank/batch_size) - floor(num_left_samples/batch_size); + 当drop_last为False,等同于 ceil(sample_in_this_rank/batch_size) - ceil(num_left_samples/batch_size)。 """ raise NotImplementedError("Each specific driver should implemented its own `load` function.") @staticmethod - def tensor_to_numeric(tensor, reduce: Optional[str]=None): + def tensor_to_numeric(tensor, reduce: Optional[str] = None): r""" - 将一个 `tensor` 对象(仅处理当前 driver 使用的 tensor 即可)转换为 python 的 `numeric` 对象;如果 tensor 只包含一个 - 元素则返回 float 或 int 。 + 将一个 ``tensor`` 对象(仅处理当前 driver 使用的 tensor 即可)转换为 python 的 ``numeric`` 对象;如果 ``tensor`` 只包含一个 + 元素则返回 ``float`` 或 ``int``; - :param tensor: 需要被转换的 `tensor` 对象 - :param reduce: 可选 ['sum', 'max', 'mea', 'min'],如果不为 None 将使用该 reduce 方法来处理当前 tensor 再返回 - float 或 int 对象。 - :return: 转换后返回的结果 + :param tensor: 需要被转换的 `tensor` 对象; + :param reduce: 可选 ``['sum', 'max', 'mea', 'min']``,如果不为 ``None`` 将使用该 ``reduce`` 方法来处理当前 ``tensor`` 再返回 + ``float`` 或 ``int`` 对象; + :return: 转换后返回的结果; """ raise NotImplementedError("Each specific driver should implemented its own `tensor_to_numeric` function.") @@ -321,7 +333,7 @@ class Driver(ABC): """ def unwrap_model(self): - """ + r""" 保证用户拿到的模型一定是最原始的模型; 注意因为我们把保存模型的主要逻辑和代码移到了 `Driver` 中,因此在 `save_model` 函数中,一定要先调用此函数来保证我们保存的模型一定是 最为原始的模型; @@ -342,14 +354,14 @@ class Driver(ABC): @abstractmethod def move_data_to_device(self, batch): r""" - 将数据迁移到指定的机器上;batch 可能是 list 也可能 dict ,或其嵌套结构。 + 将数据迁移到指定的机器上;batch 可能是 list 也可能 dict ,或其嵌套结构; :return: 将移动到指定机器上的 batch 对象返回; """ def get_local_rank(self) -> int: r""" - 返回当前的local_rank,本函数的返回值只在运行分布式训练的时候有实际含义。 + 返回当前的local_rank,本函数的返回值只在运行分布式训练的时候有实际含义; :return: 一个整数值,表示当前进程在当前这台机器上的序号; """ @@ -358,13 +370,13 @@ class Driver(ABC): def barrier(self): r""" 用于在多进程工作时同步各进程的工作进度,运行快的进程运行到这里会等待运行慢的进程,只有所有进程都运行到此函数时,所有的进程才会继续运行; - 仅在多分布式训练场景中有使用。 + 仅在多分布式训练场景中有使用; - 注意,该函数的行为会受到 FASTNLP_NO_SYNC 的影响。仅当 FASTNLP_NO_SYNC 在 os.environ 中不存在,或小于 1 时才真的执行 barrier 。 + 注意,该函数的行为会受到 FASTNLP_NO_SYNC 的影响。仅当 FASTNLP_NO_SYNC 在 os.environ 中不存在,或小于 1 时才真的执行 barrier; """ def is_distributed(self) -> bool: - """ + r""" 当前的 driver 实例是否是分布式的; :return: 返回一个 bool 值,如果当前的 driver 实例是用于分布式的,那么返回 True; @@ -372,7 +384,7 @@ class Driver(ABC): return False def on_exception(self): - """ + r""" 该函数用于在训练或者预测过程中出现错误时正确地关掉其它的进程,这一点是通过在多进程 driver 调用 open_subprocess 的时候将每一个进程 的 pid 记录下来,然后在出现错误后,由出现错误的进程手动地将其它进程 kill 掉; @@ -390,40 +402,38 @@ class Driver(ABC): 'exc_local_rank': self.get_local_rank(), } sys.stderr.write("\nException info:\n") - sys.stderr.write(json.dumps(_write_exc_info, indent=2)+"\n") + sys.stderr.write(json.dumps(_write_exc_info, indent=2) + "\n") sys.stderr.write(f"Start to stop these pids:{self._pids}, please wait several seconds.\n") for pid in self._pids: if pid != os.getpid(): os.kill(pid, signal.SIGKILL) - def broadcast_object(self, obj, src:int=0, group=None, **kwargs): - """ - 从 src 端将 obj 对象(可能是 tensor ,可能是 object )broadcast 到其它所有进程。如果是非 tensor 的对象会尝试使用 pickle 进行打包进行 - 传输,然后再 dst 处再加载回来。仅在分布式的 driver 中有实际意义。 + def broadcast_object(self, obj, src: int = 0, group=None, **kwargs): + r""" + 从 ``src`` 端将 ``obj`` 对象(可能是 ``tensor``,可能是 ``object`` )broadcast 到其它所有进程。如果是非 ``tensor`` 的对象会尝试使用 ``pickle`` 进行打包进行 + 传输,然后再 ``dst`` 处再加载回来。仅在分布式的 ``driver`` 中有实际意义。 - :param obj: obj,可能是 Tensor 或 嵌套类型的数据 - :param int src: source 的 global rank 。 - :param group: 所属的 group - :param kwargs: - :return: 输入的 obj 。 + :param obj: obj,可能是 ``Tensor`` 或 嵌套类型的数据; + :param src: source 的 ``global rank``; + :param group: 所属的通信组; + :return: 输入的 ``obj``; """ if not self.is_distributed(): return obj raise NotImplementedError(f"Driver:{self.__class__.__name__} does not support `broadcast_object` method right " f"now.") - def all_gather(self, obj, group)->List: - """ + def all_gather(self, obj, group) -> List: + r""" 将 obj 互相传送到其它所有的 rank 上,其中 obj 可能是 Tensor,也可能是嵌套结构的 object 。如果不是基础类型的数据,尝试通过 pickle 进行序列化,接收到之后再反序列化。 - :param obj: 可以是 float/int/bool/np.ndarray/{}/[]/Tensor等。 - :param group: - :return: 返回值应该是 [obj0, obj1, ...], 其中obj1是rank0上的对象,obj1是rank1上的对象... + :param obj: 可以是 ``float/int/bool/np.ndarray/{}/[]/Tensor`` 等; + :param group: 用于不同进程之间互相通信的通信组; + :return: 返回值应该是 ``[obj0, obj1, ...]``,其中 ``obj1`` 是 ``rank0`` 上的对象,``obj1`` 是 ``rank1`` 上的对象; """ if not self.is_distributed(): return [obj] raise NotImplementedError(f"Driver:{self.__class__.__name__} does not support `all_gather` method right " f"now.") - diff --git a/fastNLP/core/drivers/torch_driver/ddp.py b/fastNLP/core/drivers/torch_driver/ddp.py index 85491b2e..0046551a 100644 --- a/fastNLP/core/drivers/torch_driver/ddp.py +++ b/fastNLP/core/drivers/torch_driver/ddp.py @@ -1,3 +1,130 @@ +r""" +""" + +r""" +`TorchDDPDriver` 目前支持的三种启动方式: +1. 用户自己不进行 ddp 的任何操作,直接使用我们的 Trainer,这时是由我们自己使用 `open_subprocesses` 拉起多个进程, + 然后 `TorchDDPDriver` 自己通过调用 `dist.init_process_group` 来初始化 ddp 的通信组;(情况 A) +2. 用户同样不在 Trainer 之外初始化 ddp,但是用户自己使用 python -m torch.distributed.launch 拉起来创建多个进程,这时我们仍旧 + 会通过调用 `dist.init_process_group` 来初始化 ddp 的通信组;(情况 B) +3. 用户自己在外面初始化 DDP,并且通过 python -m torch.distributed.launch 拉起,这时无论是多个进程的拉起和 ddp 的通信组的建立 + 都由用户自己操作,我们只会在 driver.setup 的时候对 `TorchDDPDriver` 设置一些必要的属性值;(情况 C) + +注意多机的启动强制要求用户在每一台机器上使用 python -m torch.distributed.launch 启动;因此我们不会在 `TorchDDPDriver` 中保存 + 任何当前有多少台机器的信息(num_nodes,不是 gpu 的数量); + +Part 1:三种启动方式的具体分析: +(1)对于用户运行的脚本中,如果 `driver.setup` 只会被调用一次(意味着用户的启动脚本中只初始化了一个 trainer/evaluator)时, + `TorchDDPDriver` 在初始化以及 `setup` 函数中会做的事情分别如下所示: + -> 情况 A:这种情况下用户传入的 model 在一定是普通的 model(没有经 `DistributedDataParallel` 包裹的model), + 因为 `DistributedDataParallel` 的使用一定要求 init_process_group 已经被调用用来建立当前的 ddp 通信组;但是这意味着如果 + 用户需要使用 2 张以上的显卡,那么其必然需要使用 torch.distributed.launch 来启动,意味着就不是情况 A 了; + 这时我们首先会调用 `TorchDDPDriver.open_subprocess` 函数来拉起多个进程,其中进程的数量等于用户传入给 trainer 的使用的 gpu + 的数量(例如 `Trainer` 中的参数是 device=[0, 1, 6, 7],那么我们就会使用第 0、1、6、7 张 gpu 来拉起 4 个进程); + 接着我们会调用 `dist.init_process_group` 来初始化各个进程之间的通信组; + 这里需要注意拉起的新的进程会从前到后完整地运行一遍用户的启动脚本(例如 main.py),因此也都会运行这两个函数,但是需要注意只有进程 0 + 才会去真正地运行 `TorchDDPDriver.open_subprocess`;进程 0 运行到 `dist.init_process_group`,pytorch 会阻塞进程 0 继续 + 向前运行,直到其它进程也运行到这里; + 最后我们会设置这个进程对应的 device,然后将模型迁移到对应的机器上,再使用 `DistributedDataParallel` 将模型包裹; + 至此,ddp 的环境配置过程全部完成; + + -> 情况 B:注意这种情况我们直接限定了用户是通过 torch.distributed.launch 拉起,并且没有自己建立 ddp 的通信组。这时在 + `TorchDDPDriver` 的初始化和 setup 函数的调用过程中,与情况 A 首要的不同就在于用户在 trainer 中输入的参数 device 不再有效, + 这时每个进程所使用的 gpu 是我们直接通过 `torch.device("cuda:{local_rank}")` 来配置的;因此,如果用户想要实现使用特定 gpu + 设备的目的,可以通过自己设置环境变量实现(例如 os.environ["CUDA_VISIBLE_DEVICE"] 来实现);剩下的操作和情况 A 类似; + + -> 情况 C:注意这种情况我们限定了用户是通过 torch.distributed.launch 拉起,并且 ddp 的通信组也是由自己建立。这时基本上所有的 + 与操作相关的操作都应当由用户自己完成,包括迁移模型到对应 gpu 上以及将模型用 `DistributedDataParallel` 包裹等。 +(2)如果 `driver.setup` 函数在脚本中会被调用两次及以上(意味着用户的启动脚本初始化了两个及以上的 trainer/evaluator)时: +注意这种情况下我们是会保证前后两个 trainer/evaluator 使用的 `TorchDDPDriver` 以及其初始化方式的一致性,换句话说,如果 trainer1 + 检测到的启动方式是 '情况 A',那么我们会保证 trainer2 检测到的启动方式同样是 '情况A'(即使这需要一些额外的处理);因此这里我们主要讨论 + 我们是通过怎样的操作来保证 trainer2/3/... 检测到的启动方式是和 trainer1 一致的;简单来说,我们是通过使用环境变量来标记每一种不同的 + 启动方式来实现这一点的: +我们会使用 `FASTNLP_DISTRIBUTED_CHECK` 来标记 '情况 A',使用 `fastnlp_torch_launch_not_ddp` 来标记 '情况 B',意味着我们在 + 使用 '情况 A' 来启动 `TorchDDPDriver` 时,我们会将 `FASTNLP_DISTRIBUTED_CHECK` 这一字符串注入到环境变量中,而 '情况 B' 时则 + 会将 `fastnlp_torch_launch_not_ddp` 这一字符串注入到环境变量中。因此在 trainer2 的 `TorchDDPDriver` 的初始化和 setup 过程中, + 如果检测到这些特殊的环境变量,我们就会将启动方式变更为其对应的启动方式,即使其它的参数特征属于另外的启动方式。 + +Part 2:对应的代码细节: + 1. 如何判断当前的各进程之间的通信组已经被建立(ddp 已经被初始化); + dist.is_initialized(); + 2. 如何判断不同的进程是否是由 `python -m torch.distributed.launch` 拉起还是由我们的 `TorchDDPDriver.open_subprocess` + 函数拉起; + 我们会在用户脚本 `import fastNLP` 的时候检测当前的环境变量中是否有 'LOCAL_RANK'、'WORLD_SIZE' 以及没有 `FASTNLP_DISTRIBUTED_CHECK`, + 如果满足条件,则我们会向环境变量中注入特殊的值 'FASTNLP_BACKEND_LAUNCH' 来标记用户是否使用了 `python -m torch.distributed.launch` + 来拉起多个进程; + 3. 整体的处理判断流程: + ___________________________________ + |进入 TorchDDPDriver 的 __init__ 函数| + ——————————————————————————————————— + ↓ + ___________________________________________________ + | 判断不同的进程是否是由 torch.distributed.launch 拉起 | + |(或者我们自己的 open_subprocess 函数拉起) | --------------> + ———————————————————————————————————————————————————  | + ↓ 是由 torch.distributed.launch 拉起 | 我们自己的 open_subprocess 函数拉起多个进程 +  ___________________________            |  + ←←←←← | 检测用户是否自己初始化了 ddp |              | + ↓ ———————————————————————————                    ↓ + ↓ ↓ 是 ________ + ↓ ______ | 情况 A | + ↓ 否 |情况 C| ————————— + ↓ ——————— + ↓ + ↓ ______ + ↓ -----------> |情况 B| +   ——————— + 4. 为了完成全部的建立 ddp 所需要的操作,三种情况都需要做的事情,以及每件事情的职责归属: + + 情况 A | 情况 B | 情况 C + ________________________________________________________________________________________________________ + 配置 ddp 所 | TorchDDPDriver.open_subprocess | torch.distributed.launch| torch.distributed.launch + 需要的环境变量 | | | + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + 开启多个进程 | TorchDDPDriver.open_subprocess | torch.distributed.launch| torch.distributed.launch + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + 调用 dist. | | | + init_process\ | TorchDDPDriver.setup | TorchDDPDriver.setup | 用户自己调用 + _group 函数 | | | + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + 设置 TorchDDPDriver | | | + 的 world_size 和 | TorchDDPDriver.setup | TorchDDPDriver.setup | TorchDDPDriver.setup + global_rank 属性 | | | + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + +Part 3:其它的处理细节: + 1. 环境变量; + fastNLP 的 `TorchDDPDriver` 运行时所需要的环境变量分为两种,一种是 torch 的 ddp 运行所需要的环境变量;另一种是 fastNLP 自己 + 的环境变量。前者的配置情况如上表所示;而后者中的大多数环境变量则是在用户 import fastNLP 时就设置好了; + 2. parallel_device, model_device 和 data_device 的关系; + parallel_device 为 `TorchDDPDriver` 的参数,model_device 和 data_device 都为 driver 的属性; + 其中 data_device 仅当情况 C 时由用户自己指定;如果其不为 None,那么在模型 forward 的时候,我们就会将数据迁移到 data_device 上; + model_device 永远都为单独的一个 torch.device; + + 情况 A | 情况 B | 情况 C + ________________________________________________________________________________________________________ + parallel_device | 由用户传入trainer的参数 | 为 torch.device( | 为 torch.device( + | device 决定,必须是一个list, | "cuda:{local_rank}") | "cuda:{local_rank}") + | 其中每一个对象都是 torch.device | | + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + model_device | parallel_device[local_rank] | parallel_device | None + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + data_device | model_device | model_device | 由用户传入 trainer 的参数 + | | | data_device 决定 + ———————————————————————————————————————————————————————————————————————————————————————————————————————— + + 3. _DDPWrappingModel 的作用; + 因为我们即需要调用模型的 `train_step`、`evaluate_step`、`test_step` 方法,又需要通过 `DistributedDataParallel` 的 + forward 函数来帮助我们同步各个设备上的梯度,因此我们需要先将模型单独包裹一层,然后在 forward 的时候,其先经过 `DistributedDataParallel` + 的 forward 方法,然后再经过 `_DDPWrappingModel` 的 forward 方法,我们会在该 forward 函数中进行判断,确定调用的是模型自己的 + forward 函数,还是 `train_step`、`evaluate_step`、`test_step` 方法。 + + 4. 当某一个进程出现 exception 后,`TorchDDPDriver` 的处理; + + 不管是什么情况,`TorchDDPDriver` 在 `setup` 函数的最后,都会将所有进程的 pid 主动记录下来,这样当一个进程出现 exception 后, + driver 的 on_exception 函数就会被 trainer 调用,其会调用 os.kill 指令将其它进程 kill 掉; +""" + import os import sys import __main__ @@ -7,6 +134,7 @@ from time import sleep from typing import List, Optional, Union, Dict, Tuple, Callable from fastNLP.envs.imports import _NEED_IMPORT_TORCH + if _NEED_IMPORT_TORCH: import torch import torch.distributed as dist @@ -26,7 +154,8 @@ from fastNLP.core.drivers.torch_driver.utils import ( ) from fastNLP.core.drivers.utils import distributed_open_proc from fastNLP.core.utils import auto_param_call, check_user_specific_params -from fastNLP.core.samplers import ReproducibleSampler, RandomSampler, UnrepeatedSequentialSampler, ReproducibleBatchSampler, \ +from fastNLP.core.samplers import ReproducibleSampler, RandomSampler, UnrepeatedSequentialSampler, \ + ReproducibleBatchSampler, \ re_instantiate_sampler, UnrepeatedSampler, conversion_between_reproducible_and_unrepeated_sampler from fastNLP.envs import FASTNLP_DISTRIBUTED_CHECK, FASTNLP_GLOBAL_RANK, FASTNLP_GLOBAL_SEED, FASTNLP_NO_SYNC from fastNLP.core.log import logger @@ -34,6 +163,81 @@ from fastNLP.core.drivers.torch_driver.dist_utils import fastnlp_torch_all_gathe class TorchDDPDriver(TorchDriver): + r""" + ``TorchDDPDriver`` 通过开启多个进程,让每个进程单独使用一个 gpu 设备来实现分布式训练; + + .. note:: + + 您在绝大多数情况下不需要自己使用到该类,通过向 ``Trainer`` 传入正确的参数,您可以方便快速地部署您的分布式训练; + + ``TorchDDPDriver`` 目前支持的三种启动方式: + + 1. 用户自己不进行 ``ddp`` 的任何操作,直接使用我们的 ``Trainer``,这时是由我们自己使用 ``open_subprocesses`` 拉起多个进程, + 然后 ``TorchDDPDriver`` 自己通过调用 ``dist.init_process_group`` 来初始化 ddp 的通信组;(情况 A) + + .. code-block:: + + trainer = Trainer( + ... + driver='torch', + device=[0, 1] + ) + trainer.run() + + 通过运行 ``python train.py`` 启动; + + 2. 用户同样不在 ``Trainer`` 之外初始化 ``ddp``,但是用户自己使用 ``python -m torch.distributed.launch`` 拉起来创建多个进程,这时我们仍旧 + 会通过调用 ``dist.init_process_group`` 来初始化 ``ddp`` 的通信组;(情况 B) + + .. code-block:: + + trainer = Trainer( + ... + driver='torch', + device=None + ) + trainer.run() + + 通过运行 ``python -m torch.distributed.launch --nproc_per_node 2 train.py`` 启动; + + 3. 用户自己在外面初始化 ``DDP``,并且通过 ``python -m torch.distributed.launch`` 拉起,这时无论是多个进程的拉起和 ddp 的通信组的建立 + 都由用户自己操作,我们只会在 ``driver.setup`` 的时候对 ``TorchDDPDriver`` 设置一些必要的属性值;(情况 C) + + .. code-block:: + + import torch.distributed as dist + from torch.nn.parallel import DistributedDataParallel + + # 获取当前的进程信息; + ... + + # 初始化 ddp 不同进程间的通信组; + dist.init_process_group(...) + + # 初始化模型使用 DistributedDataParallel 包裹; + model = Model() + model = DistributedDataParallel(model, ...) + + # 注意此时仍旧不需要您主动地将 datalaoder 的 sampler 替换为 DistributedSampler; + trainer = Trainer( + ... + driver='torch', + device=None + ) + trainer.run() + + 通过运行 ``python -m torch.distributed.launch --nproc_per_node 2 train.py`` 启动; + + 注意多机的启动强制要求用户在每一台机器上使用 ``python -m torch.distributed.launch`` 启动;因此我们不会在 ``TorchDDPDriver`` 中保存 + 任何当前有多少台机器的信息; + + :param model: 传入给 ``Trainer`` 的 ``model`` 参数; + :param parallel_device: 用于分布式训练的 ``gpu`` 设备; + :param is_pull_by_torch_run: 标志当前的脚本的启动是否由 ``python -m torch.distributed.launch`` 启动的; + :param fp16: 是否开启 fp16 训练; + :param kwargs: 其余的一些用于设定 ddp 训练的参数; + """ + def __init__( self, model, @@ -42,129 +246,7 @@ class TorchDDPDriver(TorchDriver): fp16: bool = False, **kwargs ): - r""" - `TorchDDPDriver` 目前支持的三种启动方式: - 1. 用户自己不进行 ddp 的任何操作,直接使用我们的 Trainer,这时是由我们自己使用 `open_subprocesses` 拉起多个进程, - 然后 `TorchDDPDriver` 自己通过调用 `dist.init_process_group` 来初始化 ddp 的通信组;(情况 A) - 2. 用户同样不在 Trainer 之外初始化 ddp,但是用户自己使用 python -m torch.distributed.launch 拉起来创建多个进程,这时我们仍旧 - 会通过调用 `dist.init_process_group` 来初始化 ddp 的通信组;(情况 B) - 3. 用户自己在外面初始化 DDP,并且通过 python -m torch.distributed.launch 拉起,这时无论是多个进程的拉起和 ddp 的通信组的建立 - 都由用户自己操作,我们只会在 driver.setup 的时候对 `TorchDDPDriver` 设置一些必要的属性值;(情况 C) - - 注意多机的启动强制要求用户在每一台机器上使用 python -m torch.distributed.launch 启动;因此我们不会在 `TorchDDPDriver` 中保存 - 任何当前有多少台机器的信息(num_nodes,不是 gpu 的数量); - - Part 1:三种启动方式的具体分析: - (1)对于用户运行的脚本中,如果 `driver.setup` 只会被调用一次(意味着用户的启动脚本中只初始化了一个 trainer/evaluator)时, - `TorchDDPDriver` 在初始化以及 `setup` 函数中会做的事情分别如下所示: - -> 情况 A:这种情况下用户传入的 model 在一定是普通的 model(没有经 `DistributedDataParallel` 包裹的model), - 因为 `DistributedDataParallel` 的使用一定要求 init_process_group 已经被调用用来建立当前的 ddp 通信组;但是这意味着如果 - 用户需要使用 2 张以上的显卡,那么其必然需要使用 torch.distributed.launch 来启动,意味着就不是情况 A 了; - 这时我们首先会调用 `TorchDDPDriver.open_subprocess` 函数来拉起多个进程,其中进程的数量等于用户传入给 trainer 的使用的 gpu - 的数量(例如 `Trainer` 中的参数是 device=[0, 1, 6, 7],那么我们就会使用第 0、1、6、7 张 gpu 来拉起 4 个进程); - 接着我们会调用 `dist.init_process_group` 来初始化各个进程之间的通信组; - 这里需要注意拉起的新的进程会从前到后完整地运行一遍用户的启动脚本(例如 main.py),因此也都会运行这两个函数,但是需要注意只有进程 0 - 才会去真正地运行 `TorchDDPDriver.open_subprocess`;进程 0 运行到 `dist.init_process_group`,pytorch 会阻塞进程 0 继续 - 向前运行,直到其它进程也运行到这里; - 最后我们会设置这个进程对应的 device,然后将模型迁移到对应的机器上,再使用 `DistributedDataParallel` 将模型包裹; - 至此,ddp 的环境配置过程全部完成; - - -> 情况 B:注意这种情况我们直接限定了用户是通过 torch.distributed.launch 拉起,并且没有自己建立 ddp 的通信组。这时在 - `TorchDDPDriver` 的初始化和 setup 函数的调用过程中,与情况 A 首要的不同就在于用户在 trainer 中输入的参数 device 不再有效, - 这时每个进程所使用的 gpu 是我们直接通过 `torch.device("cuda:{local_rank}")` 来配置的;因此,如果用户想要实现使用特定 gpu - 设备的目的,可以通过自己设置环境变量实现(例如 os.environ["CUDA_VISIBLE_DEVICE"] 来实现);剩下的操作和情况 A 类似; - - -> 情况 C:注意这种情况我们限定了用户是通过 torch.distributed.launch 拉起,并且 ddp 的通信组也是由自己建立。这时基本上所有的 - 与操作相关的操作都应当由用户自己完成,包括迁移模型到对应 gpu 上以及将模型用 `DistributedDataParallel` 包裹等。 - (2)如果 `driver.setup` 函数在脚本中会被调用两次及以上(意味着用户的启动脚本初始化了两个及以上的 trainer/evaluator)时: - 注意这种情况下我们是会保证前后两个 trainer/evaluator 使用的 `TorchDDPDriver` 以及其初始化方式的一致性,换句话说,如果 trainer1 - 检测到的启动方式是 '情况 A',那么我们会保证 trainer2 检测到的启动方式同样是 '情况A'(即使这需要一些额外的处理);因此这里我们主要讨论 - 我们是通过怎样的操作来保证 trainer2/3/... 检测到的启动方式是和 trainer1 一致的;简单来说,我们是通过使用环境变量来标记每一种不同的 - 启动方式来实现这一点的: - 我们会使用 `FASTNLP_DISTRIBUTED_CHECK` 来标记 '情况 A',使用 `fastnlp_torch_launch_not_ddp` 来标记 '情况 B',意味着我们在 - 使用 '情况 A' 来启动 `TorchDDPDriver` 时,我们会将 `FASTNLP_DISTRIBUTED_CHECK` 这一字符串注入到环境变量中,而 '情况 B' 时则 - 会将 `fastnlp_torch_launch_not_ddp` 这一字符串注入到环境变量中。因此在 trainer2 的 `TorchDDPDriver` 的初始化和 setup 过程中, - 如果检测到这些特殊的环境变量,我们就会将启动方式变更为其对应的启动方式,即使其它的参数特征属于另外的启动方式。 - - Part 2:对应的代码细节: - 1. 如何判断当前的各进程之间的通信组已经被建立(ddp 已经被初始化); - dist.is_initialized(); - 2. 如何判断不同的进程是否是由 `python -m torch.distributed.launch` 拉起还是由我们的 `TorchDDPDriver.open_subprocess` - 函数拉起; - 我们会在用户脚本 `import fastNLP` 的时候检测当前的环境变量中是否有 'LOCAL_RANK'、'WORLD_SIZE' 以及没有 `FASTNLP_DISTRIBUTED_CHECK`, - 如果满足条件,则我们会向环境变量中注入特殊的值 'FASTNLP_BACKEND_LAUNCH' 来标记用户是否使用了 `python -m torch.distributed.launch` - 来拉起多个进程; - 3. 整体的处理判断流程: - ___________________________________ - |进入 TorchDDPDriver 的 __init__ 函数| - ——————————————————————————————————— - ↓ - ___________________________________________________ - | 判断不同的进程是否是由 torch.distributed.launch 拉起 | - |(或者我们自己的 open_subprocess 函数拉起) | --------------> - ———————————————————————————————————————————————————  | - ↓ 是由 torch.distributed.launch 拉起 | 我们自己的 open_subprocess 函数拉起多个进程 -  ___________________________            |  - ←←←←← | 检测用户是否自己初始化了 ddp |              | - ↓ ———————————————————————————                    ↓ - ↓ ↓ 是 ________ - ↓ ______ | 情况 A | - ↓ 否 |情况 C| ————————— - ↓ ——————— - ↓ - ↓ ______ - ↓ -----------> |情况 B| -   ——————— - 4. 为了完成全部的建立 ddp 所需要的操作,三种情况都需要做的事情,以及每件事情的职责归属: - - 情况 A | 情况 B | 情况 C - ________________________________________________________________________________________________________ - 配置 ddp 所 | TorchDDPDriver.open_subprocess | torch.distributed.launch| torch.distributed.launch - 需要的环境变量 | | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 开启多个进程 | TorchDDPDriver.open_subprocess | torch.distributed.launch| torch.distributed.launch - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 调用 dist. | | | - init_process\ | TorchDDPDriver.setup | TorchDDPDriver.setup | 用户自己调用 - _group 函数 | | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - 设置 TorchDDPDriver | | | - 的 world_size 和 | TorchDDPDriver.setup | TorchDDPDriver.setup | TorchDDPDriver.setup - global_rank 属性 | | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - - Part 3:其它的处理细节: - 1. 环境变量; - fastNLP 的 `TorchDDPDriver` 运行时所需要的环境变量分为两种,一种是 torch 的 ddp 运行所需要的环境变量;另一种是 fastNLP 自己 - 的环境变量。前者的配置情况如上表所示;而后者中的大多数环境变量则是在用户 import fastNLP 时就设置好了; - 2. parallel_device, model_device 和 data_device 的关系; - parallel_device 为 `TorchDDPDriver` 的参数,model_device 和 data_device 都为 driver 的属性; - 其中 data_device 仅当情况 C 时由用户自己指定;如果其不为 None,那么在模型 forward 的时候,我们就会将数据迁移到 data_device 上; - model_device 永远都为单独的一个 torch.device; - - 情况 A | 情况 B | 情况 C - ________________________________________________________________________________________________________ - parallel_device | 由用户传入trainer的参数 | 为 torch.device( | 为 torch.device( - | device 决定,必须是一个list, | "cuda:{local_rank}") | "cuda:{local_rank}") - | 其中每一个对象都是 torch.device | | - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - model_device | parallel_device[local_rank] | parallel_device | None - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - data_device | model_device | model_device | 由用户传入 trainer 的参数 - | | | data_device 决定 - ———————————————————————————————————————————————————————————————————————————————————————————————————————— - - 3. _DDPWrappingModel 的作用; - 因为我们即需要调用模型的 `train_step`、`evaluate_step`、`test_step` 方法,又需要通过 `DistributedDataParallel` 的 - forward 函数来帮助我们同步各个设备上的梯度,因此我们需要先将模型单独包裹一层,然后在 forward 的时候,其先经过 `DistributedDataParallel` - 的 forward 方法,然后再经过 `_DDPWrappingModel` 的 forward 方法,我们会在该 forward 函数中进行判断,确定调用的是模型自己的 - forward 函数,还是 `train_step`、`evaluate_step`、`test_step` 方法。 - - 4. 当某一个进程出现 exception 后,`TorchDDPDriver` 的处理; - - 不管是什么情况,`TorchDDPDriver` 在 `setup` 函数的最后,都会将所有进程的 pid 主动记录下来,这样当一个进程出现 exception 后, - driver 的 on_exception 函数就会被 trainer 调用,其会调用 os.kill 指令将其它进程 kill 掉; - """ + # 在加入很多东西后,需要注意这里调用 super 函数的位置; super(TorchDDPDriver, self).__init__(model, fp16=fp16, **kwargs) @@ -176,8 +258,9 @@ class TorchDDPDriver(TorchDriver): self.is_pull_by_torch_run = is_pull_by_torch_run self.parallel_device = parallel_device if not is_pull_by_torch_run and parallel_device is None: - raise ValueError("Parameter `parallel_device` can not be None when using `TorchDDPDriver`. This error is caused " - "when your value of parameter `device` is `None` in your `Trainer` instance.") + raise ValueError( + "Parameter `parallel_device` can not be None when using `TorchDDPDriver`. This error is caused " + "when your value of parameter `device` is `None` in your `Trainer` instance.") # 注意我们在 initialize_torch_driver 中的逻辑就是如果是 is_pull_by_torch_run,那么我们就直接把 parallel_device 置为当前进程的gpu; if is_pull_by_torch_run: @@ -233,10 +316,16 @@ class TorchDDPDriver(TorchDriver): os.makedirs(name=self.output_from_new_proc, exist_ok=True) self.output_from_new_proc = os.path.abspath(self.output_from_new_proc) - self._has_setup = False # 设置这一参数是因为 evaluator 中也会进行 setup 操作,但是显然是不需要的也不应该的; + self._has_setup = False # 设置这一参数是因为 evaluator 中也会进行 setup 操作,但是显然是不需要的也不应该的; self._has_ddpwrapped = False # 判断传入的模型是否经过 _has_ddpwrapped 包裹; def setup(self): + r""" + 准备分布式环境,该函数主要做以下两件事情: + + 1. 开启多进程,每个 gpu 设备对应单独的一个进程; + 2. 每个进程将模型迁移到自己对应的 ``gpu`` 设备上;然后使用 ``DistributedDataParallel`` 包裹模型; + """ if self._has_setup: return self._has_setup = True @@ -280,9 +369,10 @@ class TorchDDPDriver(TorchDriver): # 使用的(即之后的)TorchDDPDriver 的设置和第一个 TorchDDPDriver 是完全一样的; pre_num_processes = int(os.environ[FASTNLP_DISTRIBUTED_CHECK]) if pre_num_processes != len(self.parallel_device): - raise RuntimeError("Notice you are using `TorchDDPDriver` after one instantiated `TorchDDPDriver`, it is not" - "allowed that your second `TorchDDPDriver` has a new setting of parameters " - "`num_nodes` and `num_processes`.") + raise RuntimeError( + "Notice you are using `TorchDDPDriver` after one instantiated `TorchDDPDriver`, it is not" + "allowed that your second `TorchDDPDriver` has a new setting of parameters " + "`num_nodes` and `num_processes`.") self.world_size = dist.get_world_size() self.global_rank = dist.get_rank() @@ -302,7 +392,7 @@ class TorchDDPDriver(TorchDriver): local_world_size = local_world_size.tolist() + 1 node_rank = self.global_rank // local_world_size - self._pids = self._pids[node_rank*local_world_size: (node_rank+1)*local_world_size] + self._pids = self._pids[node_rank * local_world_size: (node_rank + 1) * local_world_size] self._pids = self.tensor_to_numeric(self._pids) def configure_ddp(self): @@ -423,7 +513,8 @@ class TorchDDPDriver(TorchDriver): return self.model, model.forward - def set_dist_repro_dataloader(self, dataloader, dist: Optional[Union[str, ReproducibleSampler, ReproducibleBatchSampler]]=None, + def set_dist_repro_dataloader(self, dataloader, + dist: Optional[Union[str, ReproducibleSampler, ReproducibleBatchSampler]] = None, reproducible: bool = False): # 如果 dist 为 ReproducibleBatchSampler, ReproducibleSampler 说明是在断点重训时 driver.load 函数调用; # 注意这里不需要调用 dist_sampler.set_distributed;因为如果用户使用的是 TorchDDPDriver,那么其在 Trainer 初始化的时候就已经调用了该函数; @@ -505,16 +596,26 @@ class TorchDDPDriver(TorchDriver): batch_sampler = BatchSampler(sampler, args.batch_size, drop_last=False) return replace_batch_sampler(dataloader, batch_sampler) else: - raise ValueError("Parameter `dist_sampler` can only be one of three values: ('dist', 'unrepeatdist', None).") + raise ValueError( + "Parameter `dist_sampler` can only be one of three values: ('dist', 'unrepeatdist', None).") def is_global_zero(self): + r""" + :return: 返回当前的进程是否在全局上是进程 0 ; + """ return self.global_rank == 0 def get_model_no_sync_context(self): + r""" + :return: 返回一个 ``context`` 上下文环境,用于关闭各个进程之间的同步; + """ # 注意此时的 model 是 "DistributedDataParallel" 对象; return self.model.no_sync def unwrap_model(self): + r""" + :return: 返回没有经过 ``DistributedDataParallel`` 包裹的原始模型; + """ _module = self.model.module if isinstance(_module, _DDPWrappingModel): return _module.model @@ -522,17 +623,26 @@ class TorchDDPDriver(TorchDriver): return _module def get_local_rank(self) -> int: + r""" + :return: 返回当前进程局部的进程编号; + """ return self.local_rank def barrier(self): + r""" + 通过使用该函数来使得各个进程之间同步操作; + """ if int(os.environ.get(FASTNLP_NO_SYNC, 0)) < 1: # 当 FASTNLP_NO_SYNC 小于 1 时实际执行 torch.distributed.barrier(async_op=False) def is_distributed(self): + r""" + :return: 返回当前使用的 driver 是否是分布式的 driver,对于 ``TorchDDPDriver`` 来说,该函数一定返回 ``True``; + """ return True - def broadcast_object(self, obj, src:int=0, group=None, **kwargs): - """ + def broadcast_object(self, obj, src: int = 0, group=None, **kwargs): + r""" 从 src 端将 obj 对象(可能是 tensor ,可能是 object )发送到 dst 处。如果是非 tensor 的对象会尝试使用 pickle 进行打包进行 传输,然后再 dst 处再加载回来。仅在分布式的 driver 中有实际意义。 @@ -540,7 +650,6 @@ class TorchDDPDriver(TorchDriver): :param int src: source 的 global rank 。 :param int dst: target 的 global rank,可以是多个目标 rank :param group: 所属的 group - :param kwargs: :return: 如果当前不是分布式 driver 直接返回输入的 obj 。如果当前 rank 是接收端(其 global rank 包含在了 dst 中),则返回 接收到的参数;如果是 source 端则返回发射的内容;既不是发送端、又不是接收端,则返回 None 。 """ @@ -549,7 +658,7 @@ class TorchDDPDriver(TorchDriver): return fastnlp_torch_broadcast_object(obj, src, device=self.data_device, group=group) def all_gather(self, obj, group) -> List: - """ + r""" 将 obj 互相传送到其它所有的 rank 上,其中 obj 可能是 Tensor,也可能是嵌套结构的 object 。如果不是基础类型的数据,尝试通过 pickle 进行序列化,接收到之后再反序列化。 @@ -578,10 +687,9 @@ class TorchDDPDriver(TorchDriver): def find_free_network_port() -> str: - """Finds a free port on localhost. - - It is useful in single-node training when we don't want to connect to a real master node but have to set the - `MASTER_PORT` environment variable. + """ + 在 localhost 上找到一个空闲端口; + 当我们不想连接到真正的主节点但必须设置“MASTER_PORT”环境变量时在单节点训练中很有用; """ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 0)) diff --git a/fastNLP/core/drivers/torch_driver/dist_utils.py b/fastNLP/core/drivers/torch_driver/dist_utils.py index c5b90655..b31302ed 100644 --- a/fastNLP/core/drivers/torch_driver/dist_utils.py +++ b/fastNLP/core/drivers/torch_driver/dist_utils.py @@ -145,6 +145,27 @@ def _tensor_to_object(tensor, tensor_size): def send_recv_object(obj, src, cur_rank, device, group=None, tag=0): + r""" + pytorch 中的单点对多点的分发函数; + + 例如将进程 0 上的对象 object 分发到其它进程上; + + Example:: + + cur_rank = int(os.environ.get('LOCAL_RANK', 0)) + + # 拿到 local_device + + send_recv_object(object, 0, cur_rank, local_device) + + :param obj: 一个可以序列化的 python 对象; + :param src: 从哪一个 rank 上发送到其它 rank; + :param cur_rank: 当前的进程的 rank 序号; + :param device: 当前的进程所在的设备; + :param group: 通信组,默认为 None; + :param tag: 将发送与远程接收匹配的标记; + :return: + """ # src rank send to all other ranks size = torch.LongTensor([0]).to(device) diff --git a/fastNLP/core/drivers/torch_driver/single_device.py b/fastNLP/core/drivers/torch_driver/single_device.py index 8aa9a2d5..c67dfd89 100644 --- a/fastNLP/core/drivers/torch_driver/single_device.py +++ b/fastNLP/core/drivers/torch_driver/single_device.py @@ -25,7 +25,15 @@ from fastNLP.core.log import logger class TorchSingleDriver(TorchDriver): r""" - 用于 cpu 和 单卡 gpu 运算; + ``TorchSingleDriver`` 是用于 cpu 和 单卡 gpu 运算的 ``driver``; + + .. note:: + + 如果您希望使用 ``DataParallel`` 来训练您的模型,您应当自己在 ``Trainer`` 初始化之前初始化好 ``DataParallel``,然后将其传入 ``Trainer`` 中; + + :param model: 传入给 ``Trainer`` 的 ``model`` 参数; + :param device: torch.device,当前进程所使用的设备; + :param fp16: 是否开启 fp16; """ def __init__(self, model, device: "torch.device", fp16: bool = False, **kwargs): @@ -55,6 +63,9 @@ class TorchSingleDriver(TorchDriver): self.world_size = 1 def setup(self): + r""" + 将模型迁移到相应的设备上; + """ if self.model_device is not None: self.model.to(self.model_device) @@ -135,6 +146,9 @@ class TorchSingleDriver(TorchDriver): return dataloader def unwrap_model(self): + r""" + :return: 返回原本的模型,例如没有被 ``DataParallel`` 包裹; + """ if isinstance(self.model, torch.nn.DataParallel) or \ isinstance(self.model, torch.nn.parallel.DistributedDataParallel): return self.model.module @@ -143,10 +157,13 @@ class TorchSingleDriver(TorchDriver): @property def data_device(self): - """ - 单卡模式不支持 data_device; + r""" + 注意单卡模式下使用 ``driver.data_device`` 等价于使用 ``driver.model_device``; """ return self.model_device def is_distributed(self): + r""" + :return: 返回当前使用的 driver 是否是分布式的 driver,对于 ``TorchSingleDriver`` 来说直接返回 ``False``; + """ return False diff --git a/fastNLP/core/drivers/torch_driver/torch_driver.py b/fastNLP/core/drivers/torch_driver/torch_driver.py index a1b83d07..ffbf77d8 100644 --- a/fastNLP/core/drivers/torch_driver/torch_driver.py +++ b/fastNLP/core/drivers/torch_driver/torch_driver.py @@ -36,7 +36,17 @@ from fastNLP.core.samplers import ReproducibleBatchSampler, ReproducibleSampler, class TorchDriver(Driver): r""" - 专属于 pytorch 的 driver;因为我们会在同一个 Trainer 框架下提供 jittor、paddle 等训练框架的支持; + 专属于 ``pytorch`` 的 ``driver``,是 ``TorchSingleDriver`` 和 ``TorchDDPDriver`` 的父类; + + .. warning:: + + 您不应当直接初始化该类,然后传入给 ``Trainer``,换句话说,您应当使用该类的子类 ``TorchSingleDriver`` 和 ``TorchDDPDriver``,而不是 + 该类本身; + + .. note:: + + 您可以在使用 ``TorchSingleDriver`` 和 ``TorchDDPDriver`` 时使用 ``TorchDriver`` 提供的接口; + """ def __init__(self, model, fp16: Optional[bool] = False, **kwargs): super(TorchDriver, self).__init__(model) @@ -111,7 +121,15 @@ class TorchDriver(Driver): f"not {type(each_optimizer)}.") @staticmethod - def tensor_to_numeric(tensor, reduce=None): + def tensor_to_numeric(tensor, reduce: str = None): + r""" + 将 ``torch.Tensor`` 转换成 python 中的数值类型; + + :param tensor: ``torch.Tensor``; + :param reduce: 当 tensor 是一个多数值的张量时,应当使用何种归一化操作来转换成单一数值,应当为以下类型之一:``['max', 'min', 'sum', 'mean']``; + :return: 返回一个单一数值,其数值类型是 python 中的基本的数值类型,例如 ``int,float`` 等; + """ + if tensor is None: return None @@ -129,6 +147,10 @@ class TorchDriver(Driver): ) def set_model_mode(self, mode: str): + r""" + 设置模型的状态是 ``train`` 还是 ``eval``; + :param mode: ``train`` 或者 ``eval``; + """ assert mode in {"train", "eval"} getattr(self.model, mode)() @@ -326,14 +348,26 @@ class TorchDriver(Driver): return states def get_evaluate_context(self): + r""" + :return: 返回 ``torch.no_grad`` 这个 context; + """ return torch.no_grad @staticmethod def move_model_to_device(model: "torch.nn.Module", device: "torch.device"): + r""" + 将模型迁移到对应的设备上; + """ if device is not None: model.to(device) - def move_data_to_device(self, batch: "torch.Tensor"): + def move_data_to_device(self, batch): + """ + 将一个 batch 的数据迁移到对应的设备上; + + :param batch: 一个 batch 的数据,可以是 ``list、dict`` 等; + :return: + """ return torch_move_data_to_device(batch, self.data_device, self.non_blocking) @staticmethod diff --git a/fastNLP/core/drivers/torch_driver/utils.py b/fastNLP/core/drivers/torch_driver/utils.py index e4c84bf8..57e57061 100644 --- a/fastNLP/core/drivers/torch_driver/utils.py +++ b/fastNLP/core/drivers/torch_driver/utils.py @@ -174,7 +174,7 @@ def _build_fp16_env(dummy=False): def replace_sampler(dataloader: "DataLoader", sampler): - """ + r""" 替换 sampler (初始化一个新的 dataloader 的逻辑在于): 用户可能继承了 dataloader,定制了自己的 dataloader 类,这也是我们为什么先 `inspect.signature(dataloader)` 而不是直接 @@ -259,7 +259,7 @@ def replace_sampler(dataloader: "DataLoader", sampler): def _dataloader_init_kwargs_resolve_sampler( dataloader: "DataLoader", sampler: Optional["Sampler"] ) -> Dict[str, Any]: - """ + r""" 此函数用于处理与 DataLoader 关联的采样器、batch_sampler 参数重新实例化; """ batch_sampler = getattr(dataloader, "batch_sampler") @@ -279,15 +279,8 @@ def _dataloader_init_kwargs_resolve_sampler( def replace_batch_sampler(dataloader, new_batch_sampler): - """Helper function to replace current batch sampler of the dataloader by a new batch sampler. Function returns new - dataloader with new batch sampler. - - Args: - dataloader: input dataloader - new_batch_sampler: new batch sampler to use - - Returns: - DataLoader + r""" + 替换一个 dataloader 的 batch_sampler; """ params_keys = [k for k in dataloader.__dict__.keys() if not k.startswith("_")] for k in ["batch_size", "sampler", "drop_last", "batch_sampler", "dataset_kind"]: @@ -296,12 +289,16 @@ def replace_batch_sampler(dataloader, new_batch_sampler): params = {k: getattr(dataloader, k) for k in params_keys} params["batch_sampler"] = new_batch_sampler return type(dataloader)(**params) - # TODO 这里是否可以auto_param_call一下 - # return auto_param_call(type(dataloader), params, {'self': type(dataloader).__new__()}, - # signature_fn=type(dataloader).__init__) def optimizer_state_to_device(state, device): + r""" + 将一个 ``optimizer`` 的 ``state_dict`` 迁移到对应的设备; + + :param state: ``optimzier.state_dict()``; + :param device: 要迁移到的目的设备; + :return: 返回迁移后的新的 state_dict; + """ new_state = {} for name, param in state.items(): if isinstance(param, dict): diff --git a/fastNLP/core/drivers/utils.py b/fastNLP/core/drivers/utils.py index 09cac2b9..58a8abdf 100644 --- a/fastNLP/core/drivers/utils.py +++ b/fastNLP/core/drivers/utils.py @@ -3,7 +3,7 @@ import subprocess def distributed_open_proc(output_from_new_proc:str, command:List[str], env_copy:dict, rank:int=None): - """ + r""" 使用 command 通过 subprocess.Popen 开启新的进程。 :param output_from_new_proc: 可选 ["ignore", "all", "only_error"],以上三个为特殊关键字,分别表示完全忽略拉起进程的打印输出, @@ -11,8 +11,8 @@ def distributed_open_proc(output_from_new_proc:str, command:List[str], env_copy: 两个文件,名称分别为 {rank}_std.log, {rank}_err.log 。原有的文件会被直接覆盖。 :param command: List[str] 启动的命令 :param env_copy: 需要注入的环境变量。 - :param rank: - :return: + :param rank: global_rank; + :return: 返回使用 ``subprocess.Popen`` 打开的进程; """ if output_from_new_proc == "all": proc = subprocess.Popen(command, env=env_copy) From c2b396347a3dcf887d8951e4c425e4cabfbc1207 Mon Sep 17 00:00:00 2001 From: lxr-tech <1838593642@qq.com> Date: Fri, 13 May 2022 11:49:44 +0800 Subject: [PATCH 12/14] update tutorial-01 lxr 220513 --- tutorials/fastnlp_tutorial_0.ipynb | 42 +- tutorials/fastnlp_tutorial_1.ipynb | 732 ++---------------- .../figures/T0-fig-parameter-matching.png | Bin 0 -> 95576 bytes .../figures/T0-fig-trainer-and-evaluator.png | Bin 100764 -> 71418 bytes .../figures/T0-fig-training-structure.png | Bin 0 -> 80282 bytes 5 files changed, 80 insertions(+), 694 deletions(-) create mode 100644 tutorials/figures/T0-fig-parameter-matching.png create mode 100644 tutorials/figures/T0-fig-training-structure.png diff --git a/tutorials/fastnlp_tutorial_0.ipynb b/tutorials/fastnlp_tutorial_0.ipynb index 26675ecf..4368652a 100644 --- a/tutorials/fastnlp_tutorial_0.ipynb +++ b/tutorials/fastnlp_tutorial_0.ipynb @@ -86,9 +86,11 @@ "\n", "  具体`driver`与`Trainer`以及`Evaluator`之间的关系请参考`fastNLP 0.8`的框架设计\n", "\n", - "注:在同一脚本中,`Trainer`和`Evaluator`使用的`driver`应当保持一致\n", + "注:这里给出一条建议:**在同一脚本中**,**所有的`Trainer`和`Evaluator`使用的`driver`应当保持一致**\n", "\n", - "  一个不能违背的原则在于:**不要将多卡的`driver`前使用单卡的`driver`**(???),这样使用可能会带来很多意想不到的错误" + "  尽量不出现,之前使用单卡的`driver`,后面又使用多卡的`driver`,这是因为,当脚本执行至\n", + "\n", + "  多卡`driver`处时,会重启一个进程执行之前所有内容,如此一来可能会造成一些意想不到的麻烦" ] }, { @@ -167,7 +169,7 @@ "\n", "注:在`fastNLP 0.8`中,**`Trainer`要求模型通过`train_step`来返回一个字典**,**满足如`{\"loss\": loss}`的形式**\n", "\n", - "  此外,这里也可以通过传入`Trainer`的参数`output_mapping`来实现高度化的定制,具体请见这一note(???)\n", + "  此外,这里也可以通过传入`Trainer`的参数`output_mapping`来实现输出的转换,详见(trainer的详细讲解,待补充)\n", "\n", "同样,在`fastNLP 0.8`中,**函数`evaluate_step`是`Evaluator`中参数`evaluate_fn`的默认值**\n", "\n", @@ -177,7 +179,7 @@ "\n", "  从模块角度,该字典的键值和`metric`中的`update`函数的签名一致,这样的机制在传参时被称为“**参数匹配**”\n", "\n", - "" + "" ] }, { @@ -216,8 +218,14 @@ "\n", " def __getitem__(self, item):\n", " return {\"x\": self.x[item], \"y\": self.y[item]}\n", - "```\n", - "***\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "f5f1a6aa", + "metadata": {}, + "source": [ "对于后者,首先要明确,在`Trainer`和`Evaluator`中,`metrics`的计算分为`update`和`get_metric`两步\n", "\n", "    **`update`函数**,**针对一个`batch`的预测结果**,计算其累计的评价指标\n", @@ -230,7 +238,9 @@ "\n", "  在此基础上,**`fastNLP 0.8`要求`evaluate_dataloader`生成的每个`batch`传递给对应的`metric`**\n", "\n", - "    **以`{\"pred\": y_pred, \"target\": y_true}`的形式**,对应其`update`函数的函数签名" + "    **以`{\"pred\": y_pred, \"target\": y_true}`的形式**,对应其`update`函数的函数签名\n", + "\n", + "" ] }, { @@ -639,11 +649,11 @@ { "data": { "text/html": [ - "
{'acc#acc': 0.29}\n",
+       "
{'acc#acc': 0.39}\n",
        "
\n" ], "text/plain": [ - "\u001b[1m{\u001b[0m\u001b[32m'acc#acc'\u001b[0m: \u001b[1;36m0.29\u001b[0m\u001b[1m}\u001b[0m\n" + "\u001b[1m{\u001b[0m\u001b[32m'acc#acc'\u001b[0m: \u001b[1;36m0.39\u001b[0m\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, @@ -652,7 +662,7 @@ { "data": { "text/plain": [ - "{'acc#acc': 0.29}" + "{'acc#acc': 0.39}" ] }, "execution_count": 9, @@ -710,7 +720,9 @@ "source": [ "通过使用`Trainer`类的`run`函数,进行训练\n", "\n", - "  还可以通过参数`num_eval_sanity_batch`决定每次训练前运行多少个`evaluate_batch`进行评测,默认为2" + "  还可以通过参数`num_eval_sanity_batch`决定每次训练前运行多少个`evaluate_batch`进行评测,默认为2\n", + "\n", + "  之所以“先评测后训练”,是为了保证训练很长时间的数据,不会在评测阶段出问题,故作此试探性评测" ] }, { @@ -773,6 +785,14 @@ "source": [ "trainer.run()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4e9c619", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/tutorials/fastnlp_tutorial_1.ipynb b/tutorials/fastnlp_tutorial_1.ipynb index 93e7a664..c378b54a 100644 --- a/tutorials/fastnlp_tutorial_1.ipynb +++ b/tutorials/fastnlp_tutorial_1.ipynb @@ -153,7 +153,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2438703969992 2438374526920\n", + "1608199516936 1607874531400\n", "+-----+------------------------+------------------------+-----+\n", "| idx | sentence | words | num |\n", "+-----+------------------------+------------------------+-----+\n", @@ -183,7 +183,7 @@ "id": "aa277674", "metadata": {}, "source": [ - "  注二:在`fastNLP 0.8`中,**对`dataset`使用等号**,**其效果是传引用**,**而不是赋值**(???)\n", + "  注二:**对对象使用等号一般表示传引用**,所以对`dataset`使用等号,是传引用而不是赋值\n", "\n", "    如下所示,**`dropped`和`dataset`具有相同`id`**,**对`dropped`执行删除操作`dataset`同时会被修改**" ] @@ -198,7 +198,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2438374526920 2438374526920\n", + "1607874531400 1607874531400\n", "+-----+------------------------+------------------------+-----+\n", "| idx | sentence | words | num |\n", "+-----+------------------------+------------------------+-----+\n", @@ -296,9 +296,9 @@ "\n", "在`dataset`模块中,`apply`、`apply_field`、`apply_more`和`apply_field_more`函数可以进行简单的数据预处理\n", "\n", - "  **`apply`和`apply_more`针对整条实例**,**`apply_field`和`apply_field_more`仅针对实例的部分字段**\n", + "  **`apply`和`apply_more`输入整条实例**,**`apply_field`和`apply_field_more`仅输入实例的部分字段**\n", "\n", - "  **`apply`和`apply_field`仅针对单个字段**,**`apply_more`和`apply_field_more`则可以针对多个字段**\n", + "  **`apply`和`apply_field`仅输出单个字段**,**`apply_more`和`apply_field_more`则是输出多个字段**\n", "\n", "  **`apply`和`apply_field`返回的是个列表**,**`apply_more`和`apply_field_more`返回的是个字典**\n", "\n", @@ -311,14 +311,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "72a0b5f9", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "", + "model_id": "8532c5609a394c19b60315663a6f0f4a", "version_major": 2, "version_minor": 0 }, @@ -328,42 +328,6 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----+------------------------------+------------------------------+\n", - "| idx | sentence | words |\n", - "+-----+------------------------------+------------------------------+\n", - "| 0 | This is an apple . | ['This', 'is', 'an', 'app... |\n", - "| 1 | I like apples . | ['I', 'like', 'apples', '... |\n", - "| 2 | Apples are good for our h... | ['Apples', 'are', 'good',... |\n", - "+-----+------------------------------+------------------------------+\n" - ] } ], "source": [ @@ -384,57 +348,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "b1a8631f", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----+------------------------------+------------------------------+\n", - "| idx | sentence | words |\n", - "+-----+------------------------------+------------------------------+\n", - "| 0 | This is an apple . | ['This', 'is', 'an', 'app... |\n", - "| 1 | I like apples . | ['I', 'like', 'apples', '... |\n", - "| 2 | Apples are good for our h... | ['Apples', 'are', 'good',... |\n", - "+-----+------------------------------+------------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "dataset = DataSet(data)\n", "\n", @@ -459,57 +376,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "057c1d2c", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----+------------------------------+------------------------------+\n", - "| idx | sentence | words |\n", - "+-----+------------------------------+------------------------------+\n", - "| 0 | This is an apple . | ['This', 'is', 'an', 'app... |\n", - "| 1 | I like apples . | ['I', 'like', 'apples', '... |\n", - "| 2 | Apples are good for our h... | ['Apples', 'are', 'good',... |\n", - "+-----+------------------------------+------------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "dataset = DataSet(data)\n", "dataset.apply_field(lambda sent:sent.split(), field_name='sentence', new_field_name='words')\n", @@ -528,57 +398,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "51e2f02c", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----+------------------------+------------------------+-----+\n", - "| idx | sentence | words | num |\n", - "+-----+------------------------+------------------------+-----+\n", - "| 0 | This is an apple . | ['This', 'is', 'an'... | 5 |\n", - "| 1 | I like apples . | ['I', 'like', 'appl... | 4 |\n", - "| 2 | Apples are good for... | ['Apples', 'are', '... | 7 |\n", - "+-----+------------------------+------------------------+-----+\n" - ] - } - ], + "outputs": [], "source": [ "dataset = DataSet(data)\n", "dataset.apply_more(lambda ins:{'words': ins['sentence'].split(), 'num': len(ins['sentence'].split())})\n", @@ -597,57 +420,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "db4295d5", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----+------------------------+------------------------+-----+\n", - "| idx | sentence | words | num |\n", - "+-----+------------------------+------------------------+-----+\n", - "| 0 | This is an apple . | ['This', 'is', 'an'... | 5 |\n", - "| 1 | I like apples . | ['I', 'like', 'appl... | 4 |\n", - "| 2 | Apples are good for... | ['Apples', 'are', '... | 7 |\n", - "+-----+------------------------+------------------------+-----+\n" - ] - } - ], + "outputs": [], "source": [ "dataset = DataSet(data)\n", "dataset.apply_field_more(lambda sent:{'words': sent.split(), 'num': len(sent.split())}, \n", @@ -669,7 +445,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "012f537c", "metadata": {}, "outputs": [], @@ -700,20 +476,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "a4c1c10d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_items([('sentence', 'This is an apple .'), ('words', ['This', 'is', 'an', 'apple', '.']), ('num', 5)])\n", - "dict_keys(['sentence', 'words', 'num'])\n", - "dict_values(['This is an apple .', ['This', 'is', 'an', 'apple', '.'], 5])\n" - ] - } - ], + "outputs": [], "source": [ "ins = Instance(sentence=\"This is an apple .\", words=['This', 'is', 'an', 'apple', '.'], num=5)\n", "\n", @@ -732,22 +498,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "55376402", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------+------------------------+-----+-----+\n", - "| sentence | words | num | idx |\n", - "+--------------------+------------------------+-----+-----+\n", - "| This is an apple . | ['This', 'is', 'an'... | 5 | 0 |\n", - "+--------------------+------------------------+-----+-----+\n" - ] - } - ], + "outputs": [], "source": [ "ins.add_field(field_name='idx', field=0)\n", "print(ins)" @@ -767,44 +521,20 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "fe15f4c1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'sentence': ,\n", - " 'words': ,\n", - " 'num': }" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dataset.get_all_fields()" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "5433815c", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['num', 'sentence', 'words']" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dataset.get_field_names()" ] @@ -823,29 +553,10 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "25ce5488", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3 False\n", - "6 True\n", - "+------------------------------+------------------------------+--------+\n", - "| sentence | words | length |\n", - "+------------------------------+------------------------------+--------+\n", - "| This is an apple . | ['This', 'is', 'an', 'app... | 5 |\n", - "| I like apples . | ['I', 'like', 'apples', '... | 4 |\n", - "| Apples are good for our h... | ['Apples', 'are', 'good',... | 7 |\n", - "| This is an apple . | ['This', 'is', 'an', 'app... | 5 |\n", - "| I like apples . | ['I', 'like', 'apples', '... | 4 |\n", - "| Apples are good for our h... | ['Apples', 'are', 'good',... | 7 |\n", - "+------------------------------+------------------------------+--------+\n" - ] - } - ], + "outputs": [], "source": [ "print(len(dataset), dataset.has_field('length')) \n", "if 'num' in dataset:\n", @@ -877,21 +588,10 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "3515e096", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Vocabulary([]...)\n", - "{'': 0, '': 1}\n", - " 0\n", - " 1\n" - ] - } - ], + "outputs": [], "source": [ "from fastNLP.core.vocabulary import Vocabulary\n", "\n", @@ -914,20 +614,10 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "88c7472a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "5 Counter({'生活': 1, '就像': 1, '海洋': 1})\n", - "6 Counter({'生活': 1, '就像': 1, '海洋': 1, '只有': 1})\n", - "6 {'': 0, '': 1, '生活': 2, '就像': 3, '海洋': 4, '只有': 5}\n" - ] - } - ], + "outputs": [], "source": [ "vocab.add_word_lst(['生活', '就像', '海洋'])\n", "print(len(vocab), vocab.word_count)\n", @@ -950,21 +640,10 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "3447acde", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 0\n", - " 1\n", - "生活 2\n", - "彼岸 1 False\n" - ] - } - ], + "outputs": [], "source": [ "print(vocab.to_word(0), vocab.to_index(''))\n", "print(vocab.to_word(1), vocab.to_index(''))\n", @@ -986,21 +665,10 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "490b101c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "生活 2\n", - "彼岸 12 True\n", - "13 Counter({'人': 4, '生活': 2, '就像': 2, '海洋': 2, '只有': 2, '意志': 1, '坚强的': 1, '才': 1, '能': 1, '到达': 1, '彼岸': 1})\n", - "13 {'': 0, '': 1, '生活': 2, '就像': 3, '海洋': 4, '只有': 5, '人': 6, '意志': 7, '坚强的': 8, '才': 9, '能': 10, '到达': 11, '彼岸': 12}\n" - ] - } - ], + "outputs": [], "source": [ "vocab.add_word_lst(['生活', '就像', '海洋', '只有', '意志', '坚强的', '人', '人', '人', '人', '才', '能', '到达', '彼岸'])\n", "print(vocab.to_word(2), vocab.to_index('生活'))\n", @@ -1023,19 +691,10 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "a99ff909", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'positive': 0, 'negative': 1}\n", - "ValueError: word `neutral` not in vocabulary\n" - ] - } - ], + "outputs": [], "source": [ "vocab = Vocabulary(unknown=None, padding=None)\n", "\n", @@ -1058,19 +717,10 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "432f74c1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'': 0, 'positive': 1, 'negative': 2}\n", - "0 \n" - ] - } - ], + "outputs": [], "source": [ "vocab = Vocabulary(unknown='', padding=None)\n", "\n", @@ -1096,92 +746,10 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "3dbd985d", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
SentenceIdSentenceSentiment
01A series of escapades demonstrating the adage ...negative
12This quiet , introspective and entertaining in...positive
23Even fans of Ismail Merchant 's work , I suspe...negative
34A positively thrilling combination of ethnogra...neutral
45A comedy-drama of nearly epic proportions root...positive
56The Importance of Being Earnest , so thick wit...neutral
\n", - "
" - ], - "text/plain": [ - " SentenceId Sentence Sentiment\n", - "0 1 A series of escapades demonstrating the adage ... negative\n", - "1 2 This quiet , introspective and entertaining in... positive\n", - "2 3 Even fans of Ismail Merchant 's work , I suspe... negative\n", - "3 4 A positively thrilling combination of ethnogra... neutral\n", - "4 5 A comedy-drama of nearly epic proportions root... positive\n", - "5 6 The Importance of Being Earnest , so thick wit... neutral" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "import pandas as pd\n", "\n", @@ -1199,60 +767,10 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "4f634586", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------+------------------------------+-----------+\n", - "| SentenceId | Sentence | Sentiment |\n", - "+------------+------------------------------+-----------+\n", - "| 1 | ['a', 'series', 'of', 'es... | negative |\n", - "| 2 | ['this', 'quiet', ',', 'i... | positive |\n", - "| 3 | ['even', 'fans', 'of', 'i... | negative |\n", - "| 4 | ['a', 'positively', 'thri... | neutral |\n", - "| 5 | ['a', 'comedy-drama', 'of... | positive |\n", - "| 6 | ['the', 'importance', 'of... | neutral |\n", - "+------------+------------------------------+-----------+\n" - ] - } - ], + "outputs": [], "source": [ "from fastNLP.core.dataset import DataSet\n", "\n", @@ -1273,7 +791,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "46722efc", "metadata": {}, "outputs": [], @@ -1297,55 +815,10 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "a2de615b", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Counter({'a': 9, 'of': 9, ',': 7, 'the': 6, '.': 5, 'is': 3, 'and': 3, 'good': 2, 'for': 2, 'which': 2, 'this': 2, \"'s\": 2, 'series': 1, 'escapades': 1, 'demonstrating': 1, 'adage': 1, 'that': 1, 'what': 1, 'goose': 1, 'also': 1, 'gander': 1, 'some': 1, 'occasionally': 1, 'amuses': 1, 'but': 1, 'none': 1, 'amounts': 1, 'to': 1, 'much': 1, 'story': 1, 'quiet': 1, 'introspective': 1, 'entertaining': 1, 'independent': 1, 'worth': 1, 'seeking': 1, 'even': 1, 'fans': 1, 'ismail': 1, 'merchant': 1, 'work': 1, 'i': 1, 'suspect': 1, 'would': 1, 'have': 1, 'hard': 1, 'time': 1, 'sitting': 1, 'through': 1, 'one': 1, 'positively': 1, 'thrilling': 1, 'combination': 1, 'ethnography': 1, 'all': 1, 'intrigue': 1, 'betrayal': 1, 'deceit': 1, 'murder': 1, 'shakespearean': 1, 'tragedy': 1, 'or': 1, 'juicy': 1, 'soap': 1, 'opera': 1, 'comedy-drama': 1, 'nearly': 1, 'epic': 1, 'proportions': 1, 'rooted': 1, 'in': 1, 'sincere': 1, 'performance': 1, 'by': 1, 'title': 1, 'character': 1, 'undergoing': 1, 'midlife': 1, 'crisis': 1, 'importance': 1, 'being': 1, 'earnest': 1, 'so': 1, 'thick': 1, 'with': 1, 'wit': 1, 'it': 1, 'plays': 1, 'like': 1, 'reading': 1, 'from': 1, 'bartlett': 1, 'familiar': 1, 'quotations': 1}) \n", - "\n", - "{'': 0, '': 1, 'a': 2, 'of': 3, ',': 4, 'the': 5, '.': 6, 'is': 7, 'and': 8, 'good': 9, 'for': 10, 'which': 11, 'this': 12, \"'s\": 13, 'series': 14, 'escapades': 15, 'demonstrating': 16, 'adage': 17, 'that': 18, 'what': 19, 'goose': 20, 'also': 21, 'gander': 22, 'some': 23, 'occasionally': 24, 'amuses': 25, 'but': 26, 'none': 27, 'amounts': 28, 'to': 29, 'much': 30, 'story': 31, 'quiet': 32, 'introspective': 33, 'entertaining': 34, 'independent': 35, 'worth': 36, 'seeking': 37, 'even': 38, 'fans': 39, 'ismail': 40, 'merchant': 41, 'work': 42, 'i': 43, 'suspect': 44, 'would': 45, 'have': 46, 'hard': 47, 'time': 48, 'sitting': 49, 'through': 50, 'one': 51, 'positively': 52, 'thrilling': 53, 'combination': 54, 'ethnography': 55, 'all': 56, 'intrigue': 57, 'betrayal': 58, 'deceit': 59, 'murder': 60, 'shakespearean': 61, 'tragedy': 62, 'or': 63, 'juicy': 64, 'soap': 65, 'opera': 66, 'comedy-drama': 67, 'nearly': 68, 'epic': 69, 'proportions': 70, 'rooted': 71, 'in': 72, 'sincere': 73, 'performance': 74, 'by': 75, 'title': 76, 'character': 77, 'undergoing': 78, 'midlife': 79, 'crisis': 80, 'importance': 81, 'being': 82, 'earnest': 83, 'so': 84, 'thick': 85, 'with': 86, 'wit': 87, 'it': 88, 'plays': 89, 'like': 90, 'reading': 91, 'from': 92, 'bartlett': 93, 'familiar': 94, 'quotations': 95} \n", - "\n", - "Vocabulary(['a', 'series', 'of', 'escapades', 'demonstrating']...)\n" - ] - } - ], + "outputs": [], "source": [ "from fastNLP.core.vocabulary import Vocabulary\n", "\n", @@ -1368,60 +841,10 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "2f9a04b2", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------+------------------------------+-----------+\n", - "| SentenceId | Sentence | Sentiment |\n", - "+------------+------------------------------+-----------+\n", - "| 1 | [2, 14, 3, 15, 16, 5, 17,... | negative |\n", - "| 2 | [12, 32, 4, 33, 8, 34, 35... | positive |\n", - "| 3 | [38, 39, 3, 40, 41, 13, 4... | negative |\n", - "| 4 | [2, 52, 53, 54, 3, 55, 8,... | neutral |\n", - "| 5 | [2, 67, 3, 68, 69, 70, 71... | positive |\n", - "| 6 | [5, 81, 3, 82, 83, 4, 84,... | neutral |\n", - "+------------+------------------------------+-----------+\n" - ] - } - ], + "outputs": [], "source": [ "vocab.index_dataset(dataset, field_name='Sentence')\n", "print(dataset)" @@ -1437,67 +860,10 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "5f5eed18", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "{'negative': 0, 'positive': 1, 'neutral': 2}\n"
-     ]
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
\n",
-       "
\n" - ], - "text/plain": [ - "\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------+------------------------------+-----------+\n", - "| SentenceId | Sentence | Sentiment |\n", - "+------------+------------------------------+-----------+\n", - "| 1 | [2, 14, 3, 15, 16, 5, 17,... | 0 |\n", - "| 2 | [12, 32, 4, 33, 8, 34, 35... | 1 |\n", - "| 3 | [38, 39, 3, 40, 41, 13, 4... | 0 |\n", - "| 4 | [2, 52, 53, 54, 3, 55, 8,... | 2 |\n", - "| 5 | [2, 67, 3, 68, 69, 70, 71... | 1 |\n", - "| 6 | [5, 81, 3, 82, 83, 4, 84,... | 2 |\n", - "+------------+------------------------------+-----------+\n" - ] - } - ], + "outputs": [], "source": [ "target_vocab = Vocabulary(padding=None, unknown=None)\n", "\n", diff --git a/tutorials/figures/T0-fig-parameter-matching.png b/tutorials/figures/T0-fig-parameter-matching.png new file mode 100644 index 0000000000000000000000000000000000000000..410256ae444e97ef0a800f63edc8a041daa7a10e GIT binary patch literal 95576 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

b=mWB%B<9I({`8aQAL)B9mdpY=Y$Ho4 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%0ABv(@g0L;i~xLKgFv65I^L3VR@V; z#+&MF?pX zUbRnB<%Kbu-5mD$!4MY-l4DMoDD=qIB1z6; zp4fjf=i@JEF;_50#9_GakIC}FTFu16Fg_^<==&atrB0<<{tDje;J(cZ_;LVS zZA&DdMMw6dm`L0XWf%sKbaX7k&@Fg%$JS&9%kW z*RT}Y1P$!C;ja#OUrQX)HRN(QejuTbf20@V{S!<;L5m2jz#~$v&0nG-!&G2KMr1v= z@s3a>e!;Z?(^<{p2dnaDzeNWQ`ZNr=^fH~m3xLHoh}eboDQ8FiGq}INh9nFS9#XmY zuw(r9$8Ujfi1z1)T+M&{8?!acaHYjy`D`=`;PRL?psy|9>BT+&cIww}#Rx!RC5~2@ zA6?S@7dHk;RMbg549gApgF~TUMlpaP3}?$F^Y=dowuxtOUAspXe}T`ZSSX_iN@^*A zzyIUcZ=*QCf2-kM|97?i{=29&0AH1&*ZxKtzg9E44e(m#p~@tG{PXWME|dfN$>M)( z++bn#Kfd1C_$y$eDUbAO5|rMKfPz6J6A}b@exfUVV54rs%0>LI-U%H8?_CU|#dCao zEH0nS?l`vEd$Ma`@gA|KoBNxVsMDYM2F4igE!nJ*7UB~?0vVN@ylu}7*c2ZhA3PN$ zGz<$avq1G~S6F{fkNZjpEs5XlQ8$T~?48ZWa?Xw6k4S(0qo_Jrp^+9PGaj4i0C1Pj z!BXqPbSn@$2O^H1<+H%d@kOQX~4f{*dz6)KOSYPf~P9V)(CNuK>} zV?~|}-$?6fueG&Rzug~TilrnCV5$3!KoUrGv$th z#ZLa>_m5JK1Hlr2HaH{Dqi>E5} zub3g1Araq0Wb>h`oH*qYor;-9m&Aky!`Mf&$C>_yAdxdL#}Cek74u8o^Y5dNZ;}AYVd; zT}tlX#*FZbnw>UFI%a;>WE|rEg*RS6E5rggDcKA7=XbkGIx8pv7vPQ^**Xf~r82Y# zPfB!`01U-fQ853ECRsa7Q9@1Y(kBZqPtW`Ni`IudJHJqSeB)rD=Wt&loS!4-ynsdj zWCYkCx39Yj@o(2s>)#0_uD%D*COI)`6xtWsslP`zvJG%sh`06@1dqllG=_-x4kViw zN4Z6y@BvFw=w3WV2=tW>a5_!E_d0fD_W!Ybxi)^C@t;*;TJ_HZf8j8pXFzyq^hsg# zpD*b7qn7rhL{mOC>mLOCjq%pL;MYU`g>C*ql0YXTUpM_%1dBjs@DAEg3t5n;2uP3o zZtZtpf9nCz?Ea$5U!QAe_$N-aqXifJU%XS`b4ipdreEL2!0LIy6t1CX{JXEen)PQA zlI@3whK9|eI_!Vd@<~bR`CmU4$qob5Mkh%wkflmB`pXxOLZKqlwE^7Fq(8}Rv4N)aFWe$UAt_yVMQ{S#f$ zHK@dXTXVoW9CMIHMO9W|qfcTee(7$?ZcvImhcgQ6O{sDay(^Aw<-t))#XDuOA4oUp zdHceq5{|$wMph5(5ivU+<66HrHwwm)g?-`>o|6M!)rxk3pFc~tOs!DZV``z13=1f_|v)ioPzK%y`%pQ`qw93S#U@H2^A9XF!6F^`Soi7{D_ z&%;ox1>f0n%}q)_Bq@GF<_C_#t`g7C4(m`ig7rofVB0dg~`_p8s+wdqV_bc zPvtc`;%Ow)aOjccQ&YP>&F1S&gT&*O5HQ0yHw%@uJD6KS8)?VWj{Fc zhOfz{DTEAP1_mdGRJekdZr`t$Zd*dAy`0iE<1RGsCkQ}QLrh=67g74>$ro^XkLw-> zFhp?AhfIRsexoy2Xy{E!2TN_1qn)md&py&1(bPuCf_E0kFsZr9SKn&Al#KW@GQj!R zj5(@nXgIGl3}S!p{kg=bI;Ng(f% zol}l=w#vCeEqmqaI5%caH3PI0`P1!(v9ij+HctG7iBf6cNX^;Z=LWDLWU+zFHaTA1 ztGPzaq>#1>B=~ht*VOGzujFy+fLL6k4s}CJ(kuJNG`t4D(@se%d}#Q`Q%1lfd4MHq zLG!0uTr%JV`rTw_JJ0Yem_4m@*A~NZk7=+KWNdJeKWjTJH7L~TIHg!CIl(5mSi%G| zhX;Nso!RA~fi+}#c#6_BPJ2hn>${YCO}&%V^hey4WKRc^{T=*}Hk1F;+m-)A-L?M+ zl_jDSDf^HmWxeg{R@ucEjJ;$VAv9zeTZ=YJmYEn!h?>PP%-F_I>b4}Zjj=0h8bcAX z<@rqNeto~sAMnhNpPBib<$axVuIqih&pAJWV~}dn&Nj7E*MpGdRZ>yTr4fgT-RGDJ zmv`poPAJzzceI+72uFp##J2W|^+tM~xqq|-+Jy>=@f;Z_R_VwTx%F~cVhMB#FgpQP zU@7<<3I4r8JU9igw5U?!I&RnC+}cdl^8;HI(U@;o%9WiLmxf9RE;|3*(hrS5_V-|1 z4H{pVqA`BseV@1Ut1wPr|J%zy(c?Mw!KcEB{%$W_m}8Z5;17LW?bv5(Ee55&U@a1M z|MJkP<*M|TOdW3(G9>cDQ(AWC@pF?R5A>3ZVOH~3>2pSgYWeG}1{d0Ma-<8Mb%Pbc zmE4&clv|^H4-sHDwSA_KV zTw>b{5!1~M;!K@zsnsHAWn@|{$sT!V)zky+jLX^(RKZ!)CkroUTJ`1(X zv)CuFoWX6#@(_4{{IJvKE0>S#E?X03M$N~A7ToDr;rrOJLa)uhJ3@iZs$L>vk>g`d zJ7~z-{GLSO5ZFJFn={vR`309ap0V)U)kTVN1{3idX6|T-Ht1C|w@>;(gRNp8G`JOP zPQB9~R&WJdQ;*ibN*#k^rcXLA%)(wPQ(C#;ZqYF5*_#;7RGtu5vN{4#>aWj{#Rg?mo;w&iL zb!F|QE#JIg^nNz}S(x=kf(4nJrQpE>RKXTuGhJBk>1>N#S#a@$DC9FkRhOR|*4pK- zC=^8Dq8g#$MvNQ*-0H_ubFk9XWP3{-uFq=zEE0j7eil@d=rj9Qk=M#>Tvc&tHm@A= zpTvNb5c66Dkvkk>q}VbwSTTLseEA6o9cF78s==3NAG9gX7g-Tk5cCE_0afO1FZzMI zLuz?R_Gg)7riR<8kx3$zu*44ZOL^X&v!yF}2Y$)m0H>jo1B6nVAFFJn*k1fv6(PI` z*J#o!f<$s2^=Fg2yJ1lTAJw~`PoU*ZA$5kWI!Zp61z=+RcGM0oD#zO8S-x0Dgoa*wb$%M0F|BRhaLf*~Mc!^GQL)V|&fV&({Tf0L^>VG#!(odn7p5?SSVoPn zmytS6aPrK=I=*6dixN6r>J8j4Cn;I|Vh1`}9LCk(XT(S=zL2f-WLiymFiXvl{qiD5PXP+QyxVls?;zm?p zLl}NczHQ)=Pn9*1hZbUAv4X>5Pj=H{$FL{BmCm;>q`>6H7zCGV;e+plXQPLJl&mFA zVWe;@o%mMuOO1J%M`m%GR6nVA_Zwogs zX)wQ$u76jfP%krApSyiWyflBc zr{OkiWpYb(P*u+7)657cituyEn8pWfcL9 zRIt}O^SHT`)}XV02@0Z&OiB>dWWI+e8hT|8Ytg;)6qGWqd)Y2*RHSb_yj&~Tg*~6k z@D#c7Jb>CDTSk1DsnnVC;^@=0p?)O^s>*)QjQDy7(9p)_0_B7yBPfnb*G2MkwNy!6$5I(?>rI|id4;bT{p_iCca9GJ(XJuizD#LXi8Q0k4DCNM$n|PrlXqr>*qskYQ4uk zw#X&2qy&&J!pj4$o>ck#j64(!fp;aKdhSxnDb%^%CcjQ*bRUOPmb@ZP#5`?lD=RBP zma=37!92VSgT1?{2^i)yT)%SEB-IxRM})Z_0JFG z_Kl|9Lqy@OJ9BH17Y6N~sFq53fy3wWD%eFH`Qu*thRlE%+RC^wH09I))*9*+1sfJ? zZgqYTv+}X)h$m_u%QUP5j`b!wXyg)$dG{bkeHE|mJMp0#qm%!)c*W^HZp=E;1xW_7nSBw*0Or2msIRvO1 zE;{Ed?FMTmGHh9<$MNB3@!ny%LxHF}P%p-k_X0S)^{?Ta_j-wcD=OiG^iL`7^@sWA zV0*H+sE}3a-P&QXyhBxPvNcl8ZP-Uk+H-vx!UC&iLZ9z;D=Vej(3-4N`R%zB=$J0` z*wv+q?CESjckv^4u?Hb-gL(~4_AvwnY^AGWd1j0Zu~dqcjUqpq z*+Pc)c3AS1qf(wz@Kku;fK#)!4&8C&OpVN&`k6Uyf7UM-D-QiQ%k9s{ zX^e;Ujx{wM1!TleS|Ai>I`i#a0kS@Ajy&_Ec?15TIuKfbWiBrdr-<+z#K}A9f429J{~2JTFVfM#p;fFP+e9M%9%3WfX6^9h1Dq=! z?_hVxjT(_9Tk7xK9mdk z{@~_*w6_wjyR#FXa`d#p6-w3USVZ%-`f??=_Z+pWAau%=W!=K;r+^C)iSj7bKV;Wc zcHky4U`v3$zW%P_vH{NM2aY3&p_I#A<>crvk$Pj@2%Xg^)bJfL3L{HeWZtM@FtE%MPcpf_r6%dXAe&q zr|k6t=hQ>j6;TH1in!DJOpkpGN$1_p-5k;p@ehar!^1xioS5S+!wusxTeSsXbbA+6 z$MQ<$Pj3hR+YKS`2lVsyGn<9sW1DXqlf|@EWo2a}FW3sQ?|)kgxKkqrItsg*sW@|q zu(PE&t}SDULV#C0G%cgXA?OkZPZ8Q;>($$xrGL{g67fKAF7)G5kNW?A{F?RnfBkD) zp_%}7zFkId1m>S-45ZoFOn?d8|vG~~J$!2R4^s0(rSMss5332`7iNN$B zU;*SQp7OHB(C;FoPIb}8cbLqR>kIjF1HnSJCwGE8TZ&UeH6A|w{h#vy9DjfOD*S&7 zii|wT!Q;CbYpZ&8n@A$5OP3@$K4FoHra*ED&B31q`j?v&7N}XlxqIs3%C0OrQ{^9acko|#VYWA^?XGeaQF42uY z)b%cWa7svk1227Jr{yd{WQSJIuROf!I6HS)h-1PPfOjlkVg+az(2=jCe`Qj?$Z!AI z30B`C!ftaI$kiE_7F|j?X&4h78Mz^|{q)S0^G;6L(Z)c-e#pyn$G-&>qLy1e7CgTU zCGFg+&*)6wbA|Vbo-L~@E2bWb_YgZd-QPDjTVc!v_Koy>*I`(kw0Y0V|tc(fgJre!{Nl|lZ literal 0 HcmV?d00001 diff --git a/tutorials/figures/T0-fig-trainer-and-evaluator.png b/tutorials/figures/T0-fig-trainer-and-evaluator.png index 6e95650decb06472515c89f5ab7d51cda1f30499..38222ee8dccd69c402ab4aa343d0ad4e904a6935 100644 GIT binary patch literal 71418 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

*My@_n) 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 F{C_&s#3}#) diff --git a/tutorials/figures/T0-fig-training-structure.png b/tutorials/figures/T0-fig-training-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..6569f3d417946de9dfb15b9b790df669a7a22628 GIT binary patch literal 80282 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 EKZg3 Date: Fri, 13 May 2022 12:32:44 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E4=BF=AE=E6=94=B9Trainer.save/Driver.sav?= =?UTF-8?q?e=E4=B8=BATrainer.save=5Fcheckpoint/Driver.save=5Fcheckpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callbacks/callback.py | 9 +- fastNLP/core/callbacks/checkpoint_callback.py | 2 +- fastNLP/core/callbacks/fitlog_callback.py | 15 +- .../core/callbacks/more_evaluate_callback.py | 2 +- fastNLP/core/callbacks/topk_saver.py | 10 +- fastNLP/core/controllers/trainer.py | 35 +- fastNLP/core/drivers/driver.py | 20 +- .../drivers/jittor_driver/jittor_driver.py | 4 +- fastNLP/core/drivers/paddle_driver/fleet.py | 2 +- .../drivers/paddle_driver/paddle_driver.py | 6 +- .../drivers/paddle_driver/single_device.py | 2 +- fastNLP/core/drivers/torch_driver/ddp.py | 2 +- .../drivers/torch_driver/single_device.py | 2 +- .../core/drivers/torch_driver/torch_driver.py | 4 +- fastNLP/embeddings/__init__.py | 0 fastNLP/embeddings/torch/__init__.py | 15 + fastNLP/embeddings/torch/char_embedding.py | 287 ++++++++++++ fastNLP/embeddings/torch/embedding.py | 220 ++++++++++ fastNLP/embeddings/torch/stack_embedding.py | 101 +++++ fastNLP/embeddings/torch/static_embedding.py | 407 ++++++++++++++++++ fastNLP/embeddings/torch/utils.py | 106 +++++ fastNLP/io/__init__.py | 4 - fastNLP/io/model_io.py | 71 --- .../core/drivers/paddle_driver/test_fleet.py | 2 +- tests/core/drivers/torch_driver/test_ddp.py | 2 +- tests/embeddings/__init__.py | 0 tests/embeddings/torch/__init__.py | 0 tests/embeddings/torch/test_char_embedding.py | 29 ++ .../embeddings/torch/test_static_embedding.py | 195 +++++++++ .../glove.6B.50d_test.txt | 6 + .../small_static_embedding/word2vec_test.txt | 7 + 31 files changed, 1440 insertions(+), 127 deletions(-) create mode 100644 fastNLP/embeddings/__init__.py create mode 100644 fastNLP/embeddings/torch/__init__.py create mode 100644 fastNLP/embeddings/torch/char_embedding.py create mode 100644 fastNLP/embeddings/torch/embedding.py create mode 100644 fastNLP/embeddings/torch/stack_embedding.py create mode 100644 fastNLP/embeddings/torch/static_embedding.py create mode 100644 fastNLP/embeddings/torch/utils.py delete mode 100644 fastNLP/io/model_io.py create mode 100644 tests/embeddings/__init__.py create mode 100644 tests/embeddings/torch/__init__.py create mode 100644 tests/embeddings/torch/test_char_embedding.py create mode 100644 tests/embeddings/torch/test_static_embedding.py create mode 100644 tests/helpers/data/embedding/small_static_embedding/glove.6B.50d_test.txt create mode 100644 tests/helpers/data/embedding/small_static_embedding/word2vec_test.txt diff --git a/fastNLP/core/callbacks/callback.py b/fastNLP/core/callbacks/callback.py index e895404b..37dcaffc 100644 --- a/fastNLP/core/callbacks/callback.py +++ b/fastNLP/core/callbacks/callback.py @@ -158,7 +158,7 @@ class Callback: def on_save_model(self, trainer): """ - 当将要保存模型时调用,此刻模型还未保存。 + 当调用 Trainer.save_model() 时调用,此刻模型还未保存。 :param trainer: :return: @@ -167,7 +167,7 @@ class Callback: def on_load_model(self, trainer): """ - 当将要加载模型时调用,此刻模型还未加载。 + 当调用 Trainer.load_model() 加载模型时调用,此刻模型还未加载。 :param trainer: :return: @@ -176,7 +176,7 @@ class Callback: def on_save_checkpoint(self, trainer) -> Dict: """ - 当 Trainer 将要保存 checkpoint 的时候触发,该函数用于保存当前 callback 在恢复需要的相关数据。 + 当 Trainer 将要保存 checkpoint 的时候触发 (即调用 Trainer.save_checkpoint() 函数时),该函数用于保存当前 callback 在恢复需要的相关数据。 :param trainer: :return: @@ -185,7 +185,8 @@ class Callback: def on_load_checkpoint(self, trainer, states: Optional[Dict]): r""" - 当 Trainer 要恢复 checkpoint 的时候触发( Trainer 与 Driver 已经加载好自身的状态),参数 states 为 on_save_checkpoint() + 当 Trainer 要恢复 checkpoint 的时候触发(即调用 Trainer.load_checkpoint() 函数时 Trainer 与 Driver 已经加载好自身的状态), + 参数 states 为 on_save_checkpoint() 的返回值。 :param trainer: diff --git a/fastNLP/core/callbacks/checkpoint_callback.py b/fastNLP/core/callbacks/checkpoint_callback.py index 04ce1fc9..3265c90d 100644 --- a/fastNLP/core/callbacks/checkpoint_callback.py +++ b/fastNLP/core/callbacks/checkpoint_callback.py @@ -50,7 +50,7 @@ class CheckpointCallback(Callback): :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 - 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load_checkpoint` 加载该断 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 fastnlp_evaluate_results.json 文件,记录当前的 results。仅在设置了 topk 的场景下有用,默认为 True 。 diff --git a/fastNLP/core/callbacks/fitlog_callback.py b/fastNLP/core/callbacks/fitlog_callback.py index 867c9f68..586f1702 100644 --- a/fastNLP/core/callbacks/fitlog_callback.py +++ b/fastNLP/core/callbacks/fitlog_callback.py @@ -1,9 +1,12 @@ __all__ = [ 'FitlogCallback' ] +import os + from .has_monitor_callback import HasMonitorCallback from ...envs import _module_available from ...envs import get_global_rank +from ..log import logger if _module_available('fitlog'): import fitlog @@ -11,7 +14,9 @@ if _module_available('fitlog'): class FitlogCallback(HasMonitorCallback): """ 自动记录 ``evaluation`` 结果到 ``fitlog`` 中。会自动记录每一次 ``evaluate`` 后的结果;同时会根据 - ``monitor`` 记录最好的结果。另外,会自动将非 ``rank 0`` 上的 ``fitlog`` 设置为 ``debug`` 状态。 + ``monitor`` 记录最好的结果。另外,会自动将非 ``rank 0`` 上的 ``fitlog`` 设置为 ``debug`` 状态。同时还会在 ``fitlog`` 的 + ``other`` 列中记录一个 ``launch_time`` ,可以通过这个数值找到当前这个脚本的在 save_folder (如果有使用其它需要保存模型的 + ``Callback`` ,例如 :class:`~fastNLP.CheckpointCallback` )下的文件夹名称。 :param monitor: 监控的 metric 值。 @@ -38,6 +43,14 @@ class FitlogCallback(HasMonitorCallback): def on_after_trainer_initialized(self, trainer, driver): if get_global_rank() != 0: # 如果不是 global rank 为 0 ,需要关闭 fitlog fitlog.debug() + super().on_after_trainer_initialized(trainer, driver) + fitlog.add_other('launch_time', os.environ['FASTNLP_LAUNCH_TIME']) + + def on_sanity_check_end(self, trainer, sanity_check_res): + super(FitlogCallback, self).on_sanity_check_end(trainer, sanity_check_res) + if self.monitor is None: + logger.rank_zero_warning(f"No monitor set for {self.__class__.__name__}. Therefore, no best metric will " + f"be logged.") def on_evaluate_end(self, trainer, results): results = self.itemize_results(results) diff --git a/fastNLP/core/callbacks/more_evaluate_callback.py b/fastNLP/core/callbacks/more_evaluate_callback.py index 2b2aa16e..538ffc10 100644 --- a/fastNLP/core/callbacks/more_evaluate_callback.py +++ b/fastNLP/core/callbacks/more_evaluate_callback.py @@ -63,7 +63,7 @@ class MoreEvaluateCallback(HasMonitorCallback): 时间戳文件夹中。如果为 None ,默认使用当前文件夹。 :param only_state_dict: 保存模型时是否只保存 state_dict 。当 model_save_fn 不为 None 时,该参数无效。 :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 - 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load_checkpoint` 加载该断 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 diff --git a/fastNLP/core/callbacks/topk_saver.py b/fastNLP/core/callbacks/topk_saver.py index 9b02e1b2..e1dac878 100644 --- a/fastNLP/core/callbacks/topk_saver.py +++ b/fastNLP/core/callbacks/topk_saver.py @@ -25,12 +25,12 @@ class Saver: :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 - 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load_checkpoint` 加载该断 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 - :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 + :param kwargs: 更多需要传递给 Trainer.save_checkpoint() 或者 Trainer.save_model() 接口的参数。 """ def __init__(self, folder:str=None, save_object:str='model', only_state_dict:bool=True, model_save_fn:Callable=None, **kwargs): @@ -48,7 +48,7 @@ class Saver: self.model_save_fn = model_save_fn self.kwargs = kwargs self.save_object = save_object - self.save_fn_name = 'save' if save_object == 'trainer' else 'save_model' + self.save_fn_name = 'save_checkpoint' if save_object == 'trainer' else 'save_model' self.timestamp_path = self.folder.joinpath(os.environ[FASTNLP_LAUNCH_TIME]) @@ -193,14 +193,14 @@ class TopkSaver(ResultsMonitor, Saver): :param larger_better: 该 monitor 是否越大越好。 :param folder: 保存在哪个文件夹下,默认为当前 folder 下。 :param save_object: 可选 ['trainer', 'model'],表示在保存时的保存对象为 ``trainer+model`` 还是 只是 ``model`` 。如果 - 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load` 加载该断 + 保存 ``trainer`` 对象的话,将会保存 :class:~fastNLP.Trainer 的相关状态,可以通过 :meth:`Trainer.load_checkpoint` 加载该断 点继续训练。如果保存的是 ``Model`` 对象,则可以通过 :meth:`Trainer.load_model` 加载该模型权重。 :param only_state_dict: 保存时是否仅保存权重,在 model_save_fn 不为 None 时无意义。 :param model_save_fn: 个性化的保存函数,当触发保存操作时,就调用这个函数,这个函数应当接受一个文件夹作为参数,不返回任何东西。 如果传入了 model_save_fn 函数,fastNLP 将不再进行模型相关的保存。在多卡场景下,我们只在 rank 0 上会运行该函数。 :param save_evaluate_results: 是否保存 evaluate 的结果。如果为 True ,在保存 topk 模型的 folder 中还将额外保存一个 ``fastnlp_evaluate_results.json`` 文件,记录当前的 metric results 。仅在设置了 topk 的场景下有用,默认为 True 。 - :param kwargs: 更多需要传递给 Trainer.save() 或者 Trainer.save_model() 接口的参数。 + :param kwargs: 更多需要传递给 Trainer.save_checkpoint() 或者 Trainer.save_model() 接口的参数。 """ def __init__(self, topk:int=0, monitor:str=None, larger_better:bool=True, folder:str=None, save_object:str='model', only_state_dict:bool=True, model_save_fn:Callable=None, save_evaluate_results:bool=True, diff --git a/fastNLP/core/controllers/trainer.py b/fastNLP/core/controllers/trainer.py index b2388df1..44a74c69 100644 --- a/fastNLP/core/controllers/trainer.py +++ b/fastNLP/core/controllers/trainer.py @@ -595,7 +595,7 @@ class Trainer(TrainerEventTrigger): self.driver.barrier() self.driver.zero_grad(self.set_grad_to_none) while self.cur_epoch_idx < self.n_epochs: - # 这个是防止在 Trainer.load 之后还没结束当前 epoch 又继续 save + # 这个是防止在 Trainer.load_checkpoint 之后还没结束当前 epoch 又继续 save self.start_batch_idx_in_epoch = self.trainer_state.batch_idx_in_epoch self.driver.set_model_mode("train") self.on_train_epoch_begin() @@ -1018,7 +1018,7 @@ class Trainer(TrainerEventTrigger): 注意您需要在初始化 ``Trainer`` 后再通过 ``trainer`` 实例来调用该函数;这意味着您需要保证在保存和加载时使用的 ``driver`` 是属于同一个 训练框架的,例如都是 ``pytorch`` 或者 ``paddle``; - 注意在大多数情况下您不需要使用该函数,如果您需要断点重训功能,您可以直接使用 ``trainer.load`` 函数; + 注意在大多数情况下您不需要使用该函数,如果您需要断点重训功能,您可以直接使用 ``trainer.load_checkpoint`` 函数; 该函数在通常情况下和 ``save_model`` 函数配套使用;其参数均与 ``save_model`` 函数成对应关系; """ @@ -1045,9 +1045,10 @@ class Trainer(TrainerEventTrigger): self.driver.load_model(folder, only_state_dict, **kwargs) self.driver.barrier() - def save(self, folder: Union[str, Path], only_state_dict: bool = True, model_save_fn: Optional[Callable] = None, **kwargs): + def save_checkpoint(self, folder: Union[str, Path], only_state_dict: bool = True, model_save_fn: Optional[Callable] = None, **kwargs): r""" - 用于帮助您实现断点重训功能的保存函数; + 用于帮助您实现断点重训功能的保存函数;保存内容包括:callback 状态、Trainer 的状态、Sampler 的状态【在恢复的时候才能恢复到特定 batch 】、 + 模型参数、optimizer的状态、fp16 Scaler的状态【如果有】。 :param folder: 保存在哪个文件夹下,会在该文件下声称两个文件:fastnlp_checkpoint.pkl.tar 与 fastnlp_model.pkl.tar 。 如果 model_save_fn 不为空,则没有 fastnlp_model.pkl.tar 文件; @@ -1061,12 +1062,12 @@ class Trainer(TrainerEventTrigger): 注意如果您需要在训练的过程中使用断点重训功能,您可以直接使用 **``CheckpointCallback``**; ``CheckpointCallback`` 的使用具体见 :class:`fastNLP.core.callbacks.checkpoint_callback.CheckpointCallback`; - 这意味着在大多数时刻您并不需要自己主动地调用该函数来保存 ``Trainer`` 的状态;当然您可以在自己定制的 callback 类中通过直接调用 ``trainer.save`` 来保存 ``Trainer`` 的状态; + 这意味着在大多数时刻您并不需要自己主动地调用该函数来保存 ``Trainer`` 的状态;当然您可以在自己定制的 callback 类中通过直接调用 ``trainer.save_checkpoint`` 来保存 ``Trainer`` 的状态; 具体实际的保存状态的操作由具体的 driver 实现,这意味着对于不同的 ``Driver`` 来说,保存的操作可能是不尽相同的, 您如果想要了解保存 ``Trainer`` 状态的更多细节,请直接查看各个 ``Driver`` 的 ``save`` 函数; - ``save`` 函数和 ``load`` 函数是配套使用的; + ``save_checkpoint`` 函数和 ``load_checkpoint`` 函数是配套使用的; .. note:: @@ -1111,14 +1112,14 @@ class Trainer(TrainerEventTrigger): if not callable(model_save_fn): raise ValueError("Parameter `model_save_fn` should be `Callable` type when it is not None.") rank_zero_call(model_save_fn)(folder) - self.driver.save(folder=folder, dataloader=self.dataloader, states=states, should_save_model=False, **kwargs) + self.driver.save_checkpoint(folder=folder, dataloader=self.dataloader, states=states, should_save_model=False, **kwargs) else: - self.driver.save(folder=folder, dataloader=self.dataloader, states=states, + self.driver.save_checkpoint(folder=folder, dataloader=self.dataloader, states=states, only_state_dict=only_state_dict, should_save_model=True, **kwargs) self.driver.barrier() - def load(self, folder: str, resume_training: bool = True, only_state_dict: bool = True, + def load_checkpoint(self, folder: str, resume_training: bool = True, only_state_dict: bool = True, model_load_fn: Optional[Callable] = None, **kwargs): r""" 用于帮助您实现断点重训功能的加载函数; @@ -1128,22 +1129,22 @@ class Trainer(TrainerEventTrigger): 只会加载 ``model`` 和 ``optimizers`` 的状态;而其余对象的值则根据用户的 ``Trainer`` 的初始化直接重置; :param only_state_dict: 保存的 ``model`` 是否只保存了权重; :param model_load_fn: 使用的模型加载函数,参数应为一个文件夹,注意该函数不需要返回任何内容;您可以传入该参数来定制自己的加载模型的操作, - 当该参数不为 None 时,我们默认加载模型由该函数完成,``trainer.load`` 函数则会把 ``trainer`` 的其余状态加载好; + 当该参数不为 None 时,我们默认加载模型由该函数完成,``trainer.load_checkpoint`` 函数则会把 ``trainer`` 的其余状态加载好; .. note:: 在 fastNLP 中,断点重训的保存和加载的逻辑是完全分离的,这意味着您在第二次训练时可以将 ``CheckpointCallback`` 从 ``trainer`` 中 - 去除,而直接使用 ``trainer.load`` 函数加载 ``trainer`` 的状态来进行断点重训; + 去除,而直接使用 ``trainer.load_checkpoint`` 函数加载 ``trainer`` 的状态来进行断点重训; - 该函数在通常情况下和 ``save`` 函数配套使用;其参数与 ``save`` 函数成对应关系; + 该函数在通常情况下和 ``save_checkpoint`` 函数配套使用;其参数与 ``save_checkpoint`` 函数成对应关系; - 对于在前后两次训练 ``Driver`` 不同的情况时使用断点重训,请参考 :meth:`fastNLP.core.controllers.trainer.Trainer.load` 函数的 ``warning``; + 对于在前后两次训练 ``Driver`` 不同的情况时使用断点重训,请参考 :meth:`fastNLP.core.controllers.trainer.Trainer.load_checkpoint` 函数的 ``warning``; Example:: trainer = Trainer(...) - trainer.load(folder='/path-to-your-saved_checkpoint_folder/', ...) + trainer.load_checkpoint(folder='/path-to-your-saved_checkpoint_folder/', ...) trainer.run() @@ -1161,9 +1162,9 @@ class Trainer(TrainerEventTrigger): if not callable(model_load_fn): raise ValueError("Parameter `model_save_fn` should be `Callable`.") model_load_fn(folder) - states = self.driver.load(folder=folder, dataloader=dataloader, should_load_model=False, **kwargs) + states = self.driver.load_checkpoint(folder=folder, dataloader=dataloader, should_load_model=False, **kwargs) else: - states = self.driver.load(folder=folder, dataloader=dataloader, only_state_dict=only_state_dict, should_load_model=True, **kwargs) + states = self.driver.load_checkpoint(folder=folder, dataloader=dataloader, only_state_dict=only_state_dict, should_load_model=True, **kwargs) except FileNotFoundError as e: if FASTNLP_CHECKPOINT_FILENAME not in os.listdir(folder) and FASTNLP_MODEL_FILENAME in os.listdir(folder): logger.error("It seems that you are trying to load the trainer checkpoint from a model checkpoint folder.") @@ -1184,7 +1185,7 @@ class Trainer(TrainerEventTrigger): # 这里的原则就是应当使得 '还会产生的batch数量' + 'batch_idx_in_epoch' = '原来不断点训练的batch的总数'。其中由于 # '还会产生的batch数量' 是由还剩多少 sample 决定的,因此只能通过调整 'batch_idx_in_epoch' 使得等式成立 self.trainer_state.batch_idx_in_epoch = states.pop('batch_idx_in_epoch') - # 这个是防止用户在 Trainer.load 之后还没结束当前 epoch 又继续 save + # 这个是防止用户在 Trainer.load_checkpoint 之后还没结束当前 epoch 又继续 save_checkpoint self.start_batch_idx_in_epoch = self.trainer_state.batch_idx_in_epoch # 5. 恢复所有 callback 的状态; diff --git a/fastNLP/core/drivers/driver.py b/fastNLP/core/drivers/driver.py index 6ce168cb..ad0b980e 100644 --- a/fastNLP/core/drivers/driver.py +++ b/fastNLP/core/drivers/driver.py @@ -49,7 +49,7 @@ class Driver(ABC): 不同 gpu 上出现重复;为 'unrepeatdist' 时,表示该 dataloader 应该保证所有 gpu 上迭代出来的数据合并起来应该刚好等于原始的 数据,允许不同 gpu 上 batch 的数量不一致。其中 trainer 中 kwargs 的参数 `use_dist_sampler` 为 True 时,该值为 "dist"; 否则为 None ,evaluator 中的 kwargs 的参数 `use_dist_sampler` 为 True 时,该值为 "unrepeatdist",否则为 None; - 注意当 dist 为 ReproducibleSampler, ReproducibleBatchSampler 时,是断点重训加载时 driver.load 函数在调用; + 注意当 dist 为 ReproducibleSampler, ReproducibleBatchSampler 时,是断点重训加载时 driver.load_checkpoint 函数在调用; 当 dist 为 str 或者 None 时,是 trainer 在初始化时调用该函数; :param reproducible: 如果为 False ,不要做任何考虑;如果为 True ,需要保证返回的 dataloader 可以保存当前的迭代状态,使得 @@ -254,39 +254,39 @@ class Driver(ABC): raise NotImplementedError("Each specific driver should implemented its own `load_model` function.") @abstractmethod - def save(self, folder, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): + def save_checkpoint(self, folder, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): r""" 断点重训的保存函数,该函数会负责保存模型和 optimizers, fp16 的 state_dict;以及模型的保存(若 should_save_model 为 True) - :param folder: 保存断点重训的状态的文件夹;save 函数应该在下面新增两(一)个文件 的 FASTNLP_CHECKPOINT_FILENAME 文件与 + :param folder: 保存断点重训的状态的文件夹;save_checkpoint 函数应该在下面新增两(一)个文件 的 FASTNLP_CHECKPOINT_FILENAME 文件与 FASTNLP_MODEL_FILENAME (如果 should_save_model 为 True )。把 model 相关的内容放入到 FASTNLP_MODEL_FILENAME 文件 中,将传入的 states 以及自身产生其它状态一并保存在 FASTNLP_CHECKPOINT_FILENAME 里面。 :param states: 由 trainer 传入的一个字典,其中已经包含了为了实现断点重训所需要保存的其它对象的状态,Driver 应该只需要保存 - 该对象即可, Driver 应该不需要理解该对象,同时在 driver.load() 的时候,需要将 states 返回回去,load() 返回的值与这里的 + 该对象即可, Driver 应该不需要理解该对象,同时在 driver.load_checkpoint() 的时候,需要将 states 返回回去,load_checkpoint() 返回的值与这里的 传入的值保持一致。 :param dataloader: 正在使用的 dataloader,需要保存里面的状态使得之后可以从当前迭代的位置恢复。 :param only_state_dict: 是否只保存模型的参数,当 should_save_model 为 False ,该参数无效。 :param should_save_model: 是否应该保存模型,如果为False,Driver 将不负责 model 的保存。 """ - raise NotImplementedError("Each specific driver should implemented its own `save` function.") + raise NotImplementedError("Each specific driver should implemented its own `save_checkpoint` function.") @abstractmethod - def load(self, folder: Union[str, Path], dataloader, only_state_dict: bool =True, should_load_model: bool = True, **kwargs) -> Dict: + def load_checkpoint(self, folder: Union[str, Path], dataloader, only_state_dict: bool =True, should_load_model: bool = True, **kwargs) -> Dict: r""" 断点重训的加载函数,注意该函数会负责读取数据,并且恢复 optimizers , fp16 的 state_dict 和 模型(根据 should_load_model )和; - 其它在 Driver.save() 函数中执行的保存操作,然后将一个 state 字典返回给 trainer ( 内容为Driver.save() 接受到的 states )。 + 其它在 Driver.save_checkpoint() 函数中执行的保存操作,然后将一个 state 字典返回给 trainer ( 内容为Driver.save_checkpoint() 接受到的 states )。 该函数应该在所有 rank 上执行。 :param folder: 读取该 folder 下的 FASTNLP_CHECKPOINT_FILENAME 文件与 FASTNLP_MODEL_FILENAME (如果 should_load_model 为True)。 - :param dataloader: 当前给定 dataloader,需要根据 save 的 dataloader 状态合理设置。若该值为 None ,是不需要返回 'dataloader' + :param dataloader: 当前给定 dataloader,需要根据保存的 dataloader 状态合理设置。若该值为 None ,是不需要返回 'dataloader' 以及 'batch_idx_in_epoch' 这两个值。 :param only_state_dict: 读取的,当 should_save_model 为 False ,该参数无效。如果为 True ,说明保存的内容为权重;如果为 False 说明保存的是模型,但也是通过当前 Driver 的模型去加载保存的模型的权重,而不是使用保存的模型替换当前模型。 :param should_load_model: 是否应该加载模型,如果为False,Driver 将不负责加载模型。若该参数为 True ,但在保存的状态中没有 找到对应的模型状态,则报错。 - :return: 需要返回 save 函数输入的 states 内容 + :return: 需要返回 save_checkpoint 函数输入的 states 内容 'dataloader',返回的是根据传入的 dataloader 与 保存的状态一起设置为合理的状态,可以返回的对象与传入的dataloader是同一个。 在保存与当前传入 data sample 数目不一致时报错。 'batch_idx_in_epoch': int 类型的数据,表明当前 epoch 进行到了进行到了第几个 batch 了。 请注意,该值不能是只能通过保存的 @@ -297,7 +297,7 @@ class Driver(ABC): 当drop_last为True,等同于 floor(sample_in_this_rank/batch_size) - floor(num_left_samples/batch_size); 当drop_last为False,等同于 ceil(sample_in_this_rank/batch_size) - ceil(num_left_samples/batch_size)。 """ - raise NotImplementedError("Each specific driver should implemented its own `load` function.") + raise NotImplementedError("Each specific driver should implemented its own `load_checkpoint` function.") @staticmethod def tensor_to_numeric(tensor, reduce: Optional[str]=None): diff --git a/fastNLP/core/drivers/jittor_driver/jittor_driver.py b/fastNLP/core/drivers/jittor_driver/jittor_driver.py index 25fc4af6..14cb2b84 100644 --- a/fastNLP/core/drivers/jittor_driver/jittor_driver.py +++ b/fastNLP/core/drivers/jittor_driver/jittor_driver.py @@ -109,10 +109,10 @@ class JittorDriver(Driver): raise FileNotFoundError("Checkpoint at {} not found.".format(filepath)) return jt.load(filepath) - def save(self): + def save_checkpoint(self): ... - def load(self): + def load_checkpoint(self): ... def get_evaluate_context(self): diff --git a/fastNLP/core/drivers/paddle_driver/fleet.py b/fastNLP/core/drivers/paddle_driver/fleet.py index 36fb74fd..86a9c3f0 100644 --- a/fastNLP/core/drivers/paddle_driver/fleet.py +++ b/fastNLP/core/drivers/paddle_driver/fleet.py @@ -409,7 +409,7 @@ class PaddleFleetDriver(PaddleDriver): # 暂时不支持iterableDataset assert dataloader.dataset_kind != _DatasetKind.ITER, \ "FastNLP does not support `IteratorDataset` now." - # 如果 dist 为 ReproducibleBatchSampler, ReproducibleSampler 说明是在断点重训时 driver.load 函数调用; + # 如果 dist 为 ReproducibleBatchSampler, ReproducibleSampler 说明是在断点重训时 driver.load_checkpoint 函数调用; if isinstance(dist, ReproducibleBatchSampler): dist.set_distributed( num_replicas=self.world_size, diff --git a/fastNLP/core/drivers/paddle_driver/paddle_driver.py b/fastNLP/core/drivers/paddle_driver/paddle_driver.py index c10c98a1..606bec03 100644 --- a/fastNLP/core/drivers/paddle_driver/paddle_driver.py +++ b/fastNLP/core/drivers/paddle_driver/paddle_driver.py @@ -207,7 +207,7 @@ class PaddleDriver(Driver): model.load_dict(paddle.load(filepath)) @rank_zero_call - def save(self, folder: Path, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): + def save_checkpoint(self, folder: Path, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): r""" 断点重训的保存函数,该函数会负责保存模型和 optimizers, fp16 的 state_dict;以及模型的保存(若 should_save_model 为 True) @@ -215,7 +215,7 @@ class PaddleDriver(Driver): FASTNLP_MODEL_FILENAME (如果 should_save_model 为 True )。把 model 相关的内容放入到 FASTNLP_MODEL_FILENAME 文件中, 将传入的 states 以及自身产生其它状态一并保存在 FASTNLP_CHECKPOINT_FILENAME 里面。 :param states: 由 trainer 传入的一个字典,其中已经包含了为了实现断点重训所需要保存的其它对象的状态,Driver 应该只需要保存该对象即可, - Driver 应该不需要理解该对象,同时在 driver.load() 的时候,需要将 states 返回回去,load() 返回的值与这里的传入的值保持一致。 + Driver 应该不需要理解该对象,同时在 driver.load_checkpoint() 的时候,需要将 states 返回回去,load() 返回的值与这里的传入的值保持一致。 :param dataloader: 正在使用的 dataloader,需要保存里面的状态使得之后可以从当前迭代的位置恢复。 :param only_state_dict: 是否只保存模型的参数,当 should_save_model 为 False ,该参数无效。 :param should_save_model: 是否应该保存模型,如果为False,Driver 将不负责 model 的保存。 @@ -297,7 +297,7 @@ class PaddleDriver(Driver): paddle.save(states, str(folder.joinpath(FASTNLP_CHECKPOINT_FILENAME))) - def load(self, folder: Path, dataloader, only_state_dict: bool = True, should_load_model: bool = True, **kwargs) -> Dict: + def load_checkpoint(self, folder: Path, dataloader, only_state_dict: bool = True, should_load_model: bool = True, **kwargs) -> Dict: states = paddle.load(str(folder.joinpath(FASTNLP_CHECKPOINT_FILENAME))) diff --git a/fastNLP/core/drivers/paddle_driver/single_device.py b/fastNLP/core/drivers/paddle_driver/single_device.py index 10779fd6..9d362938 100644 --- a/fastNLP/core/drivers/paddle_driver/single_device.py +++ b/fastNLP/core/drivers/paddle_driver/single_device.py @@ -106,7 +106,7 @@ class PaddleSingleDriver(PaddleDriver): # 暂时不支持iterableDataset assert dataloader.dataset_kind != _DatasetKind.ITER, \ "FastNLP does not support `IteratorDataset` now." - # 如果 dist 为 ReproducibleBatchSampler, ReproducibleIterator 说明是在断点重训时 driver.load 函数调用; + # 如果 dist 为 ReproducibleBatchSampler, ReproducibleIterator 说明是在断点重训时 driver.load_checkpoint 函数调用; if isinstance(dist, ReproducibleBatchSampler): return replace_batch_sampler(dataloader, dist) elif isinstance(dist, ReproducibleSampler): diff --git a/fastNLP/core/drivers/torch_driver/ddp.py b/fastNLP/core/drivers/torch_driver/ddp.py index 85491b2e..225ad7f8 100644 --- a/fastNLP/core/drivers/torch_driver/ddp.py +++ b/fastNLP/core/drivers/torch_driver/ddp.py @@ -425,7 +425,7 @@ class TorchDDPDriver(TorchDriver): def set_dist_repro_dataloader(self, dataloader, dist: Optional[Union[str, ReproducibleSampler, ReproducibleBatchSampler]]=None, reproducible: bool = False): - # 如果 dist 为 ReproducibleBatchSampler, ReproducibleSampler 说明是在断点重训时 driver.load 函数调用; + # 如果 dist 为 ReproducibleBatchSampler, ReproducibleSampler 说明是在断点重训时 driver.load_checkpoint 函数调用; # 注意这里不需要调用 dist_sampler.set_distributed;因为如果用户使用的是 TorchDDPDriver,那么其在 Trainer 初始化的时候就已经调用了该函数; if isinstance(dist, ReproducibleBatchSampler): dist.set_distributed( diff --git a/fastNLP/core/drivers/torch_driver/single_device.py b/fastNLP/core/drivers/torch_driver/single_device.py index 8aa9a2d5..5ac7f2a9 100644 --- a/fastNLP/core/drivers/torch_driver/single_device.py +++ b/fastNLP/core/drivers/torch_driver/single_device.py @@ -96,7 +96,7 @@ class TorchSingleDriver(TorchDriver): dist: Union[str, ReproducibleBatchSampler, ReproducibleSampler] = None, reproducible: bool = False): - # 如果 dist 为 ReproducibleBatchSampler, ReproducibleIterator 说明是在断点重训时 driver.load 函数调用; + # 如果 dist 为 ReproducibleBatchSampler, ReproducibleIterator 说明是在断点重训时 driver.load_checkpoint 函数调用; if isinstance(dist, ReproducibleBatchSampler): return replace_batch_sampler(dataloader, dist) elif isinstance(dist, ReproducibleSampler): diff --git a/fastNLP/core/drivers/torch_driver/torch_driver.py b/fastNLP/core/drivers/torch_driver/torch_driver.py index a1b83d07..dd90e759 100644 --- a/fastNLP/core/drivers/torch_driver/torch_driver.py +++ b/fastNLP/core/drivers/torch_driver/torch_driver.py @@ -179,7 +179,7 @@ class TorchDriver(Driver): model.load_state_dict(res.state_dict()) @rank_zero_call - def save(self, folder: Path, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): + def save_checkpoint(self, folder: Path, states: Dict, dataloader, only_state_dict: bool = True, should_save_model: bool = True, **kwargs): # 传入的 dataloader 参数是 trainer 的 dataloader 属性,因为 driver 的所有 dataloader 我们是不会去改变它的,而是通过改变 # trainer.dataloader 来改变 dataloader 的状态,从而适配训练或者评测环境; @@ -253,7 +253,7 @@ class TorchDriver(Driver): states["optimizers_state_dict"] = optimizers_state_dict torch.save(states, Path(folder).joinpath(FASTNLP_CHECKPOINT_FILENAME)) - def load(self, folder: Path, dataloader, only_state_dict: bool = True, should_load_model: bool = True, **kwargs) -> Dict: + def load_checkpoint(self, folder: Path, dataloader, only_state_dict: bool = True, should_load_model: bool = True, **kwargs) -> Dict: states = torch.load(folder.joinpath(FASTNLP_CHECKPOINT_FILENAME)) # 1. 加载 optimizers 的状态; diff --git a/fastNLP/embeddings/__init__.py b/fastNLP/embeddings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastNLP/embeddings/torch/__init__.py b/fastNLP/embeddings/torch/__init__.py new file mode 100644 index 00000000..d105e329 --- /dev/null +++ b/fastNLP/embeddings/torch/__init__.py @@ -0,0 +1,15 @@ +""" +torch 可使用的几种 Embedding 。 +""" +__all__ = [ + "CNNCharEmbedding", + "LSTMCharEmbedding", + "Embedding", + "StackEmbedding", + "StaticEmbedding" +] + +from .char_embedding import * +from .embedding import * +from .stack_embedding import * +from .static_embedding import StaticEmbedding \ No newline at end of file diff --git a/fastNLP/embeddings/torch/char_embedding.py b/fastNLP/embeddings/torch/char_embedding.py new file mode 100644 index 00000000..6af0a7ff --- /dev/null +++ b/fastNLP/embeddings/torch/char_embedding.py @@ -0,0 +1,287 @@ +r""" +该文件中主要包含的是character的Embedding,包括基于CNN与LSTM的character Embedding。与其它Embedding一样,这里的Embedding输入也是 +词的index而不需要使用词语中的char的index来获取表达。 +""" + +__all__ = [ + "CNNCharEmbedding", + "LSTMCharEmbedding" +] + +from typing import List + +from ...envs.imports import _NEED_IMPORT_TORCH + +if _NEED_IMPORT_TORCH: + import torch + import torch.nn as nn + import torch.nn.functional as F + +from .embedding import TokenEmbedding +from .static_embedding import StaticEmbedding +from .utils import _construct_char_vocab_from_vocab +from .utils import get_embeddings +from ...core import logger +from ...core.vocabulary import Vocabulary +from ...modules.torch.encoder.lstm import LSTM + + +class CNNCharEmbedding(TokenEmbedding): + r""" + 使用 ``CNN`` 生成 ``character embedding``。``CNN`` 的结构为, char_embed(x) -> Dropout(x) -> CNN(x) -> activation(x) -> pool -> fc -> Dropout. + 不同的 ``kernel`` 大小的 ``fitler`` 结果是拼起来然后通过一层``fully connected layer,`` 然后输出``word``的表示。 + + Example:: + + >>> import torch + >>> from fastNLP import Vocabulary + >>> from fastNLP.embeddings.torch import CNNCharEmbedding + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = CNNCharEmbedding(vocab, embed_size=50) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + # torch.Size([1, 5,50]) + + """ + + def __init__(self, vocab: Vocabulary, embed_size: int = 50, char_emb_size: int = 50, word_dropout: float = 0, + dropout: float = 0, filter_nums: List[int] = (40, 30, 20), kernel_sizes: List[int] = (5, 3, 1), + pool_method: str = 'max', activation='relu', min_char_freq: int = 2, pre_train_char_embed: str = None, + requires_grad:bool=True, include_word_start_end:bool=True): + r""" + + :param vocab: 词表 + :param embed_size: 该CNNCharEmbedding的输出维度大小,默认值为50. + :param char_emb_size: character的embed的维度。character是从vocab中生成的。默认值为50. + :param word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 + :param dropout: 以多大的概率drop分布式表示与char embedding的输出。 + :param filter_nums: filter的数量. 长度需要和kernels一致。默认值为[40, 30, 20]. + :param kernel_sizes: kernel的大小. 默认值为[5, 3, 1]. + :param pool_method: character的表示在合成一个表示时所使用的pool方法,支持'avg', 'max'. + :param activation: CNN之后使用的激活方法,支持'relu', 'sigmoid', 'tanh' 或者自定义函数. + :param min_char_freq: character的最少出现次数。默认值为2. + :param pre_train_char_embed: 可以有两种方式调用预训练好的character embedding:第一种是传入embedding文件夹 + (文件夹下应该只有一个以.txt作为后缀的文件)或文件路径;第二种是传入embedding的名称,第二种情况将自动查看缓存中是否存在该模型, + 没有的话将自动下载。如果输入为None则使用embedding_dim的维度随机初始化一个embedding. + :param requires_grad: 是否更新权重 + :param include_word_start_end: 是否在每个word开始的character前和结束的character增加特殊标示符号; + """ + super(CNNCharEmbedding, self).__init__(vocab, word_dropout=word_dropout, dropout=dropout) + + for kernel in kernel_sizes: + assert kernel % 2 == 1, "Only odd kernel is allowed." + + assert pool_method in ('max', 'avg') + self.pool_method = pool_method + # activation function + if isinstance(activation, str): + if activation.lower() == 'relu': + self.activation = F.relu + elif activation.lower() == 'sigmoid': + self.activation = F.sigmoid + elif activation.lower() == 'tanh': + self.activation = F.tanh + elif activation is None: + self.activation = lambda x: x + elif callable(activation): + self.activation = activation + else: + raise Exception( + "Undefined activation function: choose from: [relu, tanh, sigmoid, or a callable function]") + + logger.info("Start constructing character vocabulary.") + # 建立char的词表 + self.char_vocab = _construct_char_vocab_from_vocab(vocab, min_freq=min_char_freq, + include_word_start_end=include_word_start_end) + self.char_pad_index = self.char_vocab.padding_idx + logger.info(f"In total, there are {len(self.char_vocab)} distinct characters.") + # 对vocab进行index + max_word_len = max(map(lambda x: len(x[0]), vocab)) + if include_word_start_end: + max_word_len += 2 + self.register_buffer('words_to_chars_embedding', torch.full((len(vocab), max_word_len), + fill_value=self.char_pad_index, dtype=torch.long)) + self.register_buffer('word_lengths', torch.zeros(len(vocab)).long()) + for word, index in vocab: + # if index!=vocab.padding_idx: # 如果是pad的话,直接就为pad_value了。修改为不区分pad, 这样所有的也是同一个embed + if include_word_start_end: + word = [''] + list(word) + [''] + self.words_to_chars_embedding[index, :len(word)] = \ + torch.LongTensor([self.char_vocab.to_index(c) for c in word]) + self.word_lengths[index] = len(word) + # self.char_embedding = nn.Embedding(len(self.char_vocab), char_emb_size) + if pre_train_char_embed: + self.char_embedding = StaticEmbedding(self.char_vocab, model_dir_or_name=pre_train_char_embed) + else: + self.char_embedding = get_embeddings((len(self.char_vocab), char_emb_size)) + + self.convs = nn.ModuleList([nn.Conv1d( + self.char_embedding.embedding_dim, filter_nums[i], kernel_size=kernel_sizes[i], bias=True, + padding=kernel_sizes[i] // 2) + for i in range(len(kernel_sizes))]) + self._embed_size = embed_size + self.fc = nn.Linear(sum(filter_nums), embed_size) + self.requires_grad = requires_grad + + def forward(self, words): + r""" + 输入words的index后,生成对应的words的表示。 + + :param words: [batch_size, max_len] + :return: [batch_size, max_len, embed_size] + """ + words = self.drop_word(words) + batch_size, max_len = words.size() + chars = self.words_to_chars_embedding[words] # batch_size x max_len x max_word_len + word_lengths = self.word_lengths[words] # batch_size x max_len + max_word_len = word_lengths.max() + chars = chars[:, :, :max_word_len] + # 为1的地方为mask + chars_masks = chars.eq(self.char_pad_index) # batch_size x max_len x max_word_len 如果为0, 说明是padding的位置了 + chars = self.char_embedding(chars) # batch_size x max_len x max_word_len x embed_size + chars = self.dropout(chars) + reshaped_chars = chars.reshape(batch_size * max_len, max_word_len, -1) + reshaped_chars = reshaped_chars.transpose(1, 2) # B' x E x M + conv_chars = [conv(reshaped_chars).transpose(1, 2).reshape(batch_size, max_len, max_word_len, -1) + for conv in self.convs] + conv_chars = torch.cat(conv_chars, dim=-1).contiguous() # B x max_len x max_word_len x sum(filters) + conv_chars = self.activation(conv_chars) + if self.pool_method == 'max': + conv_chars = conv_chars.masked_fill(chars_masks.unsqueeze(-1), float('-inf')) + chars, _ = torch.max(conv_chars, dim=-2) # batch_size x max_len x sum(filters) + else: + conv_chars = conv_chars.masked_fill(chars_masks.unsqueeze(-1), 0) + chars = torch.sum(conv_chars, dim=-2) / chars_masks.eq(False).sum(dim=-1, keepdim=True).float() + chars = self.fc(chars) + return self.dropout(chars) + + +class LSTMCharEmbedding(TokenEmbedding): + r""" + 使用 ``LSTM`` 的方式对 ``character`` 进行 ``encode``. embed(x) -> Dropout(x) -> LSTM(x) -> activation(x) -> pool -> Dropout + + Example:: + + >>> import torch + >>> from fastNLP import Vocabulary + >>> from fastNLP.embeddings.torch import LSTMCharEmbedding + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = LSTMCharEmbedding(vocab, embed_size=50) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + >>> # torch.Size([1, 5,50]) + + """ + + def __init__(self, vocab: Vocabulary, embed_size: int = 50, char_emb_size: int = 50, word_dropout: float = 0, + dropout: float = 0, hidden_size=50, pool_method: str = 'max', activation='relu', + min_char_freq: int = 2, bidirectional=True, pre_train_char_embed: str = None, + requires_grad:bool=True, include_word_start_end:bool=True): + r""" + + :param vocab: 词表 + :param embed_size: LSTMCharEmbedding的输出维度。默认值为50. + :param char_emb_size: character的embedding的维度。默认值为50. + :param word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 + :param dropout: 以多大概率drop character embedding的输出以及最终的word的输出。 + :param hidden_size: LSTM的中间hidden的大小,如果为bidirectional的,hidden会除二,默认为50. + :param pool_method: 支持'max', 'avg'。 + :param activation: 激活函数,支持'relu', 'sigmoid', 'tanh', 或者自定义函数. + :param min_char_freq: character的最小出现次数。默认值为2. + :param bidirectional: 是否使用双向的LSTM进行encode。默认值为True。 + :param pre_train_char_embed: 可以有两种方式调用预训练好的character embedding:第一种是传入embedding文件夹 + (文件夹下应该只有一个以.txt作为后缀的文件)或文件路径;第二种是传入embedding的名称,第二种情况将自动查看缓存中是否存在该模型, + 没有的话将自动下载。如果输入为None则使用embedding_dim的维度随机初始化一个embedding. + :param requires_grad: 是否更新权重 + :param include_word_start_end: 是否在每个word开始的character前和结束的character增加特殊标示符号; + """ + super(LSTMCharEmbedding, self).__init__(vocab, word_dropout=word_dropout, dropout=dropout) + + assert hidden_size % 2 == 0, "Only even kernel is allowed." + + assert pool_method in ('max', 'avg') + self.pool_method = pool_method + # activation function + if isinstance(activation, str): + if activation.lower() == 'relu': + self.activation = F.relu + elif activation.lower() == 'sigmoid': + self.activation = F.sigmoid + elif activation.lower() == 'tanh': + self.activation = F.tanh + elif activation is None: + self.activation = lambda x: x + elif callable(activation): + self.activation = activation + else: + raise Exception( + "Undefined activation function: choose from: [relu, tanh, sigmoid, or a callable function]") + + logger.info("Start constructing character vocabulary.") + # 建立char的词表 + self.char_vocab = _construct_char_vocab_from_vocab(vocab, min_freq=min_char_freq, + include_word_start_end=include_word_start_end) + self.char_pad_index = self.char_vocab.padding_idx + logger.info(f"In total, there are {len(self.char_vocab)} distinct characters.") + # 对vocab进行index + max_word_len = max(map(lambda x: len(x[0]), vocab)) + if include_word_start_end: + max_word_len += 2 + self.register_buffer('words_to_chars_embedding', torch.full((len(vocab), max_word_len), + fill_value=self.char_pad_index, dtype=torch.long)) + self.register_buffer('word_lengths', torch.zeros(len(vocab)).long()) + for word, index in vocab: + # if index!=vocab.padding_idx: # 如果是pad的话,直接就为pad_value了. 修改为不区分pad与否 + if include_word_start_end: + word = [''] + list(word) + [''] + self.words_to_chars_embedding[index, :len(word)] = \ + torch.LongTensor([self.char_vocab.to_index(c) for c in word]) + self.word_lengths[index] = len(word) + if pre_train_char_embed: + self.char_embedding = StaticEmbedding(self.char_vocab, pre_train_char_embed) + else: + self.char_embedding = get_embeddings((len(self.char_vocab), char_emb_size)) + + self.fc = nn.Linear(hidden_size, embed_size) + hidden_size = hidden_size // 2 if bidirectional else hidden_size + + self.lstm = LSTM(self.char_embedding.embedding_dim, hidden_size, bidirectional=bidirectional, batch_first=True) + self._embed_size = embed_size + self.bidirectional = bidirectional + self.requires_grad = requires_grad + + def forward(self, words): + r""" + 输入words的index后,生成对应的words的表示。 + + :param words: [batch_size, max_len] + :return: [batch_size, max_len, embed_size] + """ + words = self.drop_word(words) + batch_size, max_len = words.size() + chars = self.words_to_chars_embedding[words] # batch_size x max_len x max_word_len + word_lengths = self.word_lengths[words] # batch_size x max_len + max_word_len = word_lengths.max() + chars = chars[:, :, :max_word_len] + # 为mask的地方为1 + chars_masks = chars.eq(self.char_pad_index) # batch_size x max_len x max_word_len 如果为0, 说明是padding的位置了 + chars = self.char_embedding(chars) # batch_size x max_len x max_word_len x embed_size + chars = self.dropout(chars) + reshaped_chars = chars.reshape(batch_size * max_len, max_word_len, -1) + char_seq_len = chars_masks.eq(False).sum(dim=-1).reshape(batch_size * max_len) + lstm_chars = self.lstm(reshaped_chars, char_seq_len)[0].reshape(batch_size, max_len, max_word_len, -1) + # B x M x M x H + + lstm_chars = self.activation(lstm_chars) + if self.pool_method == 'max': + lstm_chars = lstm_chars.masked_fill(chars_masks.unsqueeze(-1), float('-inf')) + chars, _ = torch.max(lstm_chars, dim=-2) # batch_size x max_len x H + else: + lstm_chars = lstm_chars.masked_fill(chars_masks.unsqueeze(-1), 0) + chars = torch.sum(lstm_chars, dim=-2) / chars_masks.eq(False).sum(dim=-1, keepdim=True).float() + + chars = self.fc(chars) + + return self.dropout(chars) diff --git a/fastNLP/embeddings/torch/embedding.py b/fastNLP/embeddings/torch/embedding.py new file mode 100644 index 00000000..68acd2d3 --- /dev/null +++ b/fastNLP/embeddings/torch/embedding.py @@ -0,0 +1,220 @@ +r""" +该模块中的Embedding主要用于随机初始化的embedding(更推荐使用 :class:`fastNLP.embeddings.StaticEmbedding` ),或按照预训练权重初始化Embedding。 + +""" + +__all__ = [ + "Embedding", +] + +from abc import abstractmethod +from typing import Union, Tuple +from ...envs.imports import _NEED_IMPORT_TORCH + +if _NEED_IMPORT_TORCH: + import torch + from torch.nn import Module + from torch import nn +else: + from ...core.utils.dummy_class import DummyClass as Module + +import numpy as np + +from .utils import get_embeddings + + +class Embedding(Module): + r""" + 词向量嵌入,支持输入多种方式初始化. 可以通过 ``self.num_embeddings`` 获取词表大小; ``self.embedding_dim`` 获取 ``embedding`` 的维度. + + Example:: + + >>> import numpy as np + >>> from fastNLP.embeddings.torch import Embedding + >>> init_embed = (2000, 100) + >>> embed = Embedding(init_embed) # 随机初始化一个具有2000个词,每个词表示为100维的词向量 + >>> init_embed = np.zeros((2000, 100)) + >>> embed = Embedding(init_embed) # 使用numpy.ndarray的值作为初始化值初始化一个Embedding + + """ + + def __init__(self, init_embed:Union[Tuple[int,int],'torch.FloatTensor','nn.Embedding',np.ndarray], + word_dropout:float=0, dropout:float=0.0, unk_index:int=None): + r""" + + :param init_embed: 支持传入Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 或传入Tensor, Embedding, numpy.ndarray等则直接使用该值初始化Embedding; + :param word_dropout: 按照一定概率随机将word设置为unk_index,这样可以使得unk这个token得到足够的训练, 且会对网络有 + 一定的regularize的作用。设置该值时,必须同时设置unk_index + :param dropout: 对Embedding的输出的dropout。 + :param unk_index: drop word时替换为的index。fastNLP的Vocabulary的unk_index默认为1。 + """ + super(Embedding, self).__init__() + + self.embed = get_embeddings(init_embed) + + self.dropout = nn.Dropout(dropout) + if not isinstance(self.embed, TokenEmbedding): + if hasattr(self.embed, 'embed_size'): + self._embed_size = self.embed.embed_size + elif hasattr(self.embed, 'embedding_dim'): + self._embed_size = self.embed.embedding_dim + else: + self._embed_size = self.embed.weight.size(1) + if word_dropout > 0 and not isinstance(unk_index, int): + raise ValueError("When drop word is set, you need to pass in the unk_index.") + else: + self._embed_size = self.embed.embed_size + unk_index = self.embed.get_word_vocab().unknown_idx + self.unk_index = unk_index + self.word_dropout = word_dropout + + def forward(self, words): + r""" + :param torch.LongTensor words: [batch, seq_len] + :return: torch.Tensor : [batch, seq_len, embed_dim] + """ + if self.word_dropout > 0 and self.training: + mask = torch.ones_like(words).float() * self.word_dropout + mask = torch.bernoulli(mask).eq(1) # dropout_word越大,越多位置为1 + words = words.masked_fill(mask, self.unk_index) + words = self.embed(words) + return self.dropout(words) + + @property + def num_embedding(self) -> int: + if isinstance(self.embed, nn.Embedding): + return self.embed.weight.size(0) + else: + return self.embed.num_embeddings + + def __len__(self): + return len(self.embed) + + @property + def embed_size(self) -> int: + return self._embed_size + + @property + def embedding_dim(self) -> int: + return self._embed_size + + @property + def requires_grad(self): + r""" + Embedding的参数是否允许优化。True: 所有参数运行优化; False: 所有参数不允许优化; None: 部分允许优化、部分不允许 + :return: + """ + if not isinstance(self.embed, TokenEmbedding): + return self.embed.weight.requires_grad + else: + return self.embed.requires_grad + + @requires_grad.setter + def requires_grad(self, value): + if not isinstance(self.embed, TokenEmbedding): + self.embed.weight.requires_grad = value + else: + self.embed.requires_grad = value + + @property + def size(self): + if isinstance(self.embed, TokenEmbedding): + return self.embed.size + else: + return self.embed.weight.size() + + +class TokenEmbedding(Module): + r""" + fastNLP中各种Embedding的基类 + + """ + def __init__(self, vocab, word_dropout=0.0, dropout=0.0): + super(TokenEmbedding, self).__init__() + if vocab.rebuild: + vocab.build_vocab() + assert vocab.padding is not None, "Vocabulary must have a padding entry." + self._word_vocab = vocab + self._word_pad_index = vocab.padding_idx + if word_dropout > 0: + assert vocab.unknown is not None, "Vocabulary must have unknown entry when you want to drop a word." + self.word_dropout = word_dropout + self._word_unk_index = vocab.unknown_idx + self.dropout_layer = nn.Dropout(dropout) + + def drop_word(self, words): + r""" + 按照设定随机将words设置为unknown_index。 + + :param torch.LongTensor words: batch_size x max_len + :return: + """ + if self.word_dropout > 0 and self.training: + mask = torch.full_like(words, fill_value=self.word_dropout, dtype=torch.float, device=words.device) + mask = torch.bernoulli(mask).eq(1) # dropout_word越大,越多位置为1 + pad_mask = words.ne(self._word_pad_index) + mask = mask.__and__(pad_mask) + words = words.masked_fill(mask, self._word_unk_index) + return words + + def dropout(self, words): + r""" + 对embedding后的word表示进行drop。 + + :param torch.FloatTensor words: batch_size x max_len x embed_size + :return: + """ + return self.dropout_layer(words) + + @property + def requires_grad(self): + r""" + Embedding的参数是否允许优化。True: 所有参数运行优化; False: 所有参数不允许优化; None: 部分允许优化、部分不允许 + :return: + """ + requires_grads = set([param.requires_grad for param in self.parameters()]) + if len(requires_grads) == 1: + return requires_grads.pop() + else: + return None + + @requires_grad.setter + def requires_grad(self, value): + for param in self.parameters(): + param.requires_grad = value + + def __len__(self): + return len(self._word_vocab) + + @property + def embed_size(self) -> int: + return self._embed_size + + @property + def embedding_dim(self) -> int: + return self._embed_size + + @property + def num_embeddings(self) -> int: + r""" + 这个值可能会大于实际的embedding矩阵的大小。 + :return: + """ + return len(self._word_vocab) + + def get_word_vocab(self): + r""" + 返回embedding的词典。 + + :return: Vocabulary + """ + return self._word_vocab + + @property + def size(self): + return torch.Size(self.num_embeddings, self._embed_size) + + @abstractmethod + def forward(self, words): + raise NotImplementedError diff --git a/fastNLP/embeddings/torch/stack_embedding.py b/fastNLP/embeddings/torch/stack_embedding.py new file mode 100644 index 00000000..5ffb9dad --- /dev/null +++ b/fastNLP/embeddings/torch/stack_embedding.py @@ -0,0 +1,101 @@ +r""" +.. todo:: + doc +""" + +__all__ = [ + "StackEmbedding", +] + +from typing import List + +from ...envs.imports import _NEED_IMPORT_TORCH +if _NEED_IMPORT_TORCH: + import torch + from torch import nn + +from .embedding import TokenEmbedding +from .utils import _check_vocab_has_same_index + + +class StackEmbedding(TokenEmbedding): + r""" + 支持将多个embedding集合成一个embedding。 + + Example:: + + >>> from fastNLP import Vocabulary + >>> from fastNLP.embeddings.torch import StaticEmbedding, StackEmbedding + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed_1 = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d', requires_grad=True) + >>> embed_2 = StaticEmbedding(vocab, model_dir_or_name='en-word2vec-300', requires_grad=True) + >>> embed = StackEmbedding([embed_1, embed_2]) + + """ + + def __init__(self, embeds: List[TokenEmbedding], word_dropout=0, dropout=0): + r""" + + :param embeds: 一个由若干个TokenEmbedding组成的list,要求每一个TokenEmbedding的词表都保持一致 + :param word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。不同embedidng会在相同的位置 + 被设置为unknown。如果这里设置了dropout,则组成的embedding就不要再设置dropout了。 + :param dropout: 以多大的概率对embedding的表示进行Dropout。0.1即随机将10%的值置为0。 + """ + vocabs = [] + for embed in embeds: + if hasattr(embed, 'get_word_vocab'): + vocabs.append(embed.get_word_vocab()) + _vocab = vocabs[0] + for vocab in vocabs[1:]: + if _vocab!=vocab: + _check_vocab_has_same_index(_vocab, vocab) + + super(StackEmbedding, self).__init__(_vocab, word_dropout=word_dropout, dropout=dropout) + assert isinstance(embeds, list) + for embed in embeds: + assert isinstance(embed, TokenEmbedding), "Only TokenEmbedding type is supported." + self.embeds = nn.ModuleList(embeds) + self._embed_size = sum([embed.embed_size for embed in self.embeds]) + + def append(self, embed: TokenEmbedding): + r""" + 添加一个embedding到结尾。 + :param embed: + :return: + """ + assert isinstance(embed, TokenEmbedding) + _check_vocab_has_same_index(self.get_word_vocab(), embed.get_word_vocab()) + self._embed_size += embed.embed_size + self.embeds.append(embed) + return self + + def pop(self): + r""" + 弹出最后一个embed + :return: + """ + embed = self.embeds.pop() + self._embed_size -= embed.embed_size + return embed + + @property + def embed_size(self): + r""" + 该Embedding输出的vector的最后一维的维度。 + :return: + """ + return self._embed_size + + def forward(self, words): + r""" + 得到多个embedding的结果,并把结果按照顺序concat起来。 + + :param words: batch_size x max_len + :return: 返回的shape和当前这个stack embedding中embedding的组成有关 + """ + outputs = [] + words = self.drop_word(words) + for embed in self.embeds: + outputs.append(embed(words)) + outputs = self.dropout(torch.cat(outputs, dim=-1)) + return outputs diff --git a/fastNLP/embeddings/torch/static_embedding.py b/fastNLP/embeddings/torch/static_embedding.py new file mode 100644 index 00000000..8b555c6d --- /dev/null +++ b/fastNLP/embeddings/torch/static_embedding.py @@ -0,0 +1,407 @@ +r""" +.. todo:: + doc +""" + +__all__ = [ + "StaticEmbedding" +] +import os +import warnings +from collections import defaultdict +from copy import deepcopy +import json +from typing import Union + +import numpy as np +import torch +import torch.nn as nn + +from .embedding import TokenEmbedding +from ...core import logger +from ...core.vocabulary import Vocabulary +from ...io.file_utils import PRETRAIN_STATIC_FILES, _get_embedding_url, cached_path +from ...io.file_utils import _get_file_name_base_on_postfix + + +VOCAB_FILENAME = 'vocab.txt' +STATIC_HYPER_FILENAME = 'static_hyper.json' +STATIC_EMBED_FILENAME = 'static.txt' + + +class StaticEmbedding(TokenEmbedding): + r""" + StaticEmbedding组件. 给定预训练embedding的名称或路径,根据vocab从embedding中抽取相应的数据(只会将出现在vocab中的词抽取出来, + 如果没有找到,则会随机初始化一个值(但如果该word是被标记为no_create_entry的话,则不会单独创建一个值,而是会被指向unk的index))。 + 当前支持自动下载的预训练vector有: + + .. code:: + + en: 实际为en-glove-840b-300d(常用) + en-glove-6b-50d: glove官方的50d向量 + en-glove-6b-100d: glove官方的100d向量 + en-glove-6b-200d: glove官方的200d向量 + en-glove-6b-300d: glove官方的300d向量 + en-glove-42b-300d: glove官方使用42B数据训练版本 + en-glove-840b-300d: + en-glove-twitter-27b-25d: + en-glove-twitter-27b-50d: + en-glove-twitter-27b-100d: + en-glove-twitter-27b-200d: + en-word2vec-300d: word2vec官方发布的300d向量 + en-fasttext-crawl: fasttext官方发布的300d英文预训练 + cn-char-fastnlp-100d: fastNLP训练的100d的character embedding + cn-bi-fastnlp-100d: fastNLP训练的100d的bigram embedding + cn-tri-fastnlp-100d: fastNLP训练的100d的trigram embedding + cn-fasttext: fasttext官方发布的300d中文预训练embedding + + Example:: + + >>> from fastNLP import Vocabulary + >>> from fastNLP.embeddings.torch import StaticEmbedding + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-50d') + + >>> vocab = Vocabulary().add_word_lst(["The", 'the', "THE"]) + >>> embed = StaticEmbedding(vocab, model_dir_or_name="en-glove-50d", lower=True) + >>> # "the", "The", "THE"它们共用一个vector,且将使用"the"在预训练词表中寻找它们的初始化表示。 + + >>> vocab = Vocabulary().add_word_lst(["The", "the", "THE"]) + >>> embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=5, lower=True) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in ["The", "the", "THE"]]]) + >>> embed(words) + >>> tensor([[[ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849], + [ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849], + [ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849]]], + grad_fn=) # 每种word的输出是一致的。 + + """ + + def __init__(self, vocab: Vocabulary, model_dir_or_name: Union[str, None] = 'en', embedding_dim=-1, requires_grad: bool = True, + init_method=None, lower=False, dropout=0, word_dropout=0, normalize=False, min_freq=1, **kwargs): + r""" + + :param Vocabulary vocab: 词表. StaticEmbedding只会加载包含在词表中的词的词向量,在预训练向量中没找到的使用随机初始化 + :param model_dir_or_name: 可以有两种方式调用预训练好的static embedding:第一种是传入embedding文件夹(文件夹下应该只有一个 + 以.txt作为后缀的文件)或文件路径;第二种是传入embedding的名称,第二种情况将自动查看缓存中是否存在该模型,没有的话将自动下载。 + 如果输入为None则使用embedding_dim的维度随机初始化一个embedding。 + :param embedding_dim: 随机初始化的embedding的维度,当该值为大于0的值时,将忽略model_dir_or_name。 + :param requires_grad: 是否需要gradient. 默认为True + :param callable init_method: 如何初始化没有找到的值。可以使用torch.nn.init.*中各种方法, 传入的方法应该接受一个tensor,并 + inplace地修改其值。 + :param lower: 是否将vocab中的词语小写后再和预训练的词表进行匹配。如果你的词表中包含大写的词语,或者就是需要单独 + 为大写的词语开辟一个vector表示,则将lower设置为False。 + :param dropout: 以多大的概率对embedding的表示进行Dropout。0.1即随机将10%的值置为0。 + :param word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 + :param normalize: 是否对vector进行normalize,使得每个vector的norm为1。 + :param min_freq: Vocabulary词频数小于这个数量的word将被指向unk。 + :param kwargs: + * only_train_min_freq * (*bool*) -- 仅对 train 中的词语使用 ``min_freq`` 筛选; + * only_norm_found_vector * (*bool*) -- 默认为False, 是否仅对在预训练中找到的词语使用normalize; + * only_use_pretrain_word * (*bool*) -- 默认为False, 仅使用出现在pretrain词表中的词,如果该词没有在预训练的词表中出现 + 则为unk。如果embedding不需要更新建议设置为True。 + + """ + super(StaticEmbedding, self).__init__(vocab, word_dropout=word_dropout, dropout=dropout) + if embedding_dim > 0: + if model_dir_or_name: + logger.info(f"StaticEmbedding will ignore `model_dir_or_name`, and randomly initialize embedding with" + f" dimension {embedding_dim}. If you want to use pre-trained embedding, " + f"set `embedding_dim` to 0.") + model_dir_or_name = None + + # 得到cache_path + if model_dir_or_name is None: + assert embedding_dim >= 1, "The dimension of embedding should be larger than 1." + embedding_dim = int(embedding_dim) + model_path = None + elif model_dir_or_name.lower() in PRETRAIN_STATIC_FILES: + model_url = _get_embedding_url('static', model_dir_or_name.lower()) + model_path = cached_path(model_url, name='embedding') + # 检查是否存在 + elif os.path.isfile(os.path.abspath(os.path.expanduser(model_dir_or_name))): + model_path = os.path.abspath(os.path.expanduser(model_dir_or_name)) + elif os.path.isdir(os.path.abspath(os.path.expanduser(model_dir_or_name))): + model_path = _get_file_name_base_on_postfix(os.path.abspath(os.path.expanduser(model_dir_or_name)), '.txt') + else: + raise ValueError(f"Cannot recognize {model_dir_or_name}.") + + kwargs['min_freq'] = min_freq + kwargs['lower'] = lower + # 根据min_freq缩小vocab + truncate_vocab = (vocab.min_freq is None and min_freq > 1) or (vocab.min_freq and vocab.min_freq < min_freq) + if truncate_vocab: + truncated_vocab = deepcopy(vocab) + truncated_vocab.min_freq = min_freq + truncated_vocab.word2idx = None + if lower: # 如果有lower,将大小写的的freq需要同时考虑到 + lowered_word_count = defaultdict(int) + for word, count in truncated_vocab.word_count.items(): + lowered_word_count[word.lower()] += count + for word in truncated_vocab.word_count.keys(): + word_count = truncated_vocab.word_count[word] + if lowered_word_count[word.lower()] >= min_freq and word_count < min_freq: + truncated_vocab.add_word_lst([word] * (min_freq - word_count), + no_create_entry=truncated_vocab._is_word_no_create_entry(word)) + + # 只限制在train里面的词语使用min_freq筛选 + if kwargs.get('only_train_min_freq', False) and model_dir_or_name is not None: + for word in truncated_vocab.word_count.keys(): + if truncated_vocab._is_word_no_create_entry(word) and truncated_vocab.word_count[word] < min_freq: + truncated_vocab.add_word_lst([word] * (min_freq - truncated_vocab.word_count[word]), + no_create_entry=True) + truncated_vocab.build_vocab() + truncated_words_to_words = torch.arange(len(vocab)).long() + for word, index in vocab: + truncated_words_to_words[index] = truncated_vocab.to_index(word) + logger.info(f"{len(vocab) - len(truncated_vocab)} words have frequency less than {min_freq}.") + vocab = truncated_vocab + + self.only_use_pretrain_word = kwargs.get('only_use_pretrain_word', False) + self.only_norm_found_vector = kwargs.get('only_norm_found_vector', False) + # 读取embedding + if lower: + lowered_vocab = Vocabulary(padding=vocab.padding, unknown=vocab.unknown) + for word, index in vocab: + if vocab._is_word_no_create_entry(word): + lowered_vocab.add_word(word.lower(), no_create_entry=True) + else: + lowered_vocab.add_word(word.lower()) # 先加入需要创建entry的 + logger.info(f"All word in the vocab have been lowered. There are {len(vocab)} words, {len(lowered_vocab)} " + f"unique lowered words.") + if model_path: + embedding = self._load_with_vocab(model_path, vocab=lowered_vocab, init_method=init_method) + else: + embedding = self._randomly_init_embed(len(lowered_vocab), embedding_dim, init_method) + self.register_buffer('words_to_words', torch.arange(len(vocab)).long()) + if lowered_vocab.unknown: + unknown_idx = lowered_vocab.unknown_idx + else: + unknown_idx = embedding.size(0) - 1 # 否则是最后一个为unknow + self.register_buffer('words_to_words', torch.arange(len(vocab)).long()) + words_to_words = torch.full((len(vocab),), fill_value=unknown_idx, dtype=torch.long).long() + for word, index in vocab: + if word not in lowered_vocab: + word = word.lower() + if word not in lowered_vocab and lowered_vocab._is_word_no_create_entry(word): + continue # 如果不需要创建entry,已经默认unknown了 + words_to_words[index] = self.words_to_words[lowered_vocab.to_index(word)] + self.register_buffer('words_to_words', words_to_words) + self._word_unk_index = lowered_vocab.unknown_idx # 替换一下unknown的index + else: + if model_path: + embedding = self._load_with_vocab(model_path, vocab=vocab, init_method=init_method) + else: + embedding = self._randomly_init_embed(len(vocab), embedding_dim, init_method) + self.register_buffer('words_to_words', torch.arange(len(vocab)).long()) + if not self.only_norm_found_vector and normalize: + embedding /= (torch.norm(embedding, dim=1, keepdim=True) + 1e-12) + + if truncate_vocab: + for i in range(len(truncated_words_to_words)): + index_in_truncated_vocab = truncated_words_to_words[i] + truncated_words_to_words[i] = self.words_to_words[index_in_truncated_vocab] + del self.words_to_words + self.register_buffer('words_to_words', truncated_words_to_words) + self.embedding = nn.Embedding(num_embeddings=embedding.shape[0], embedding_dim=embedding.shape[1], + padding_idx=vocab.padding_idx, + max_norm=None, norm_type=2, scale_grad_by_freq=False, + sparse=False, _weight=embedding) + self._embed_size = self.embedding.weight.size(1) + self.requires_grad = requires_grad + self.kwargs = kwargs + + @property + def weight(self): + return self.embedding.weight + + def _randomly_init_embed(self, num_embedding, embedding_dim, init_embed=None): + r""" + + :param int num_embedding: embedding的entry的数量 + :param int embedding_dim: embedding的维度大小 + :param callable init_embed: 初始化方法 + :return: torch.FloatTensor + """ + embed = torch.zeros(num_embedding, embedding_dim) + + if init_embed is None: + nn.init.uniform_(embed, -np.sqrt(3 / embedding_dim), np.sqrt(3 / embedding_dim)) + else: + init_embed(embed) + + return embed + + def _load_with_vocab(self, embed_filepath, vocab, dtype=np.float32, padding='', unknown='', + error='ignore', init_method=None): + r""" + 从embed_filepath这个预训练的词向量中抽取出vocab这个词表的词的embedding。EmbedLoader将自动判断embed_filepath是 + word2vec(第一行只有两个元素)还是glove格式的数据。 + + :param str embed_filepath: 预训练的embedding的路径。 + :param vocab: 词表 :class:`~fastNLP.Vocabulary` 类型,读取出现在vocab中的词的embedding。 + 没有出现在vocab中的词的embedding将通过找到的词的embedding的正态分布采样出来,以使得整个Embedding是同分布的。 + :param dtype: 读出的embedding的类型 + :param str padding: 词表中padding的token + :param str unknown: 词表中unknown的token + :param str error: `ignore` , `strict` ; 如果 `ignore` ,错误将自动跳过; 如果 `strict` , 错误将抛出。 + 这里主要可能出错的地方在于词表有空行或者词表出现了维度不一致。 + :param init_method: 如何初始化没有找到的值。可以使用torch.nn.init.*中各种方法。默认使用torch.nn.init.zeros_ + :return torch.tensor: shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 + """ + assert isinstance(vocab, Vocabulary), "Only fastNLP.Vocabulary is supported." + if not os.path.exists(embed_filepath): + raise FileNotFoundError("`{}` does not exist.".format(embed_filepath)) + with open(embed_filepath, 'r', encoding='utf-8') as f: + line = f.readline().strip() + parts = line.split() + start_idx = 0 + if len(parts) == 2: + dim = int(parts[1]) + start_idx += 1 + else: + dim = len(parts) - 1 + f.seek(0) + matrix = {} # index是word在vocab中的index,value是vector或None(如果在pretrain中没有找到该word) + if vocab.padding: + matrix[vocab.padding_idx] = torch.zeros(dim) + if vocab.unknown: + matrix[vocab.unknown_idx] = torch.zeros(dim) + found_count = 0 + found_unknown = False + for idx, line in enumerate(f, start_idx): + try: + parts = line.strip().split() + word = ''.join(parts[:-dim]) + nums = parts[-dim:] + # 对齐unk与pad + if word == padding and vocab.padding is not None: + word = vocab.padding + elif word == unknown and vocab.unknown is not None: + word = vocab.unknown + found_unknown = True + if word in vocab: + index = vocab.to_index(word) + if index in matrix: + warnings.warn(f"Word has more than one vector in embedding file. Set logger level to " + f"DEBUG for detail.") + logger.debug(f"Word:{word} occurs again in line:{idx}(starts from 0)") + matrix[index] = torch.from_numpy(np.fromstring(' '.join(nums), sep=' ', dtype=dtype, count=dim)) + if self.only_norm_found_vector: + matrix[index] = matrix[index] / np.linalg.norm(matrix[index]) + found_count += 1 + except Exception as e: + if error == 'ignore': + warnings.warn("Error occurred at the {} line.".format(idx)) + else: + logger.error("Error occurred at the {} line.".format(idx)) + raise e + logger.info("Found {} out of {} words in the pre-training embedding.".format(found_count, len(vocab))) + if not self.only_use_pretrain_word: # 如果只用pretrain中的值就不要为未找到的词创建entry了 + for word, index in vocab: + if index not in matrix and not vocab._is_word_no_create_entry(word): + if found_unknown: # 如果有unkonwn,用unknown初始化 + matrix[index] = matrix[vocab.unknown_idx] + else: + matrix[index] = None + # matrix中代表是需要建立entry的词 + vectors = self._randomly_init_embed(len(matrix), dim, init_method) + + if vocab.unknown is None: # 创建一个专门的unknown + unknown_idx = len(matrix) + vectors = torch.cat((vectors, torch.zeros(1, dim)), dim=0).contiguous() + else: + unknown_idx = vocab.unknown_idx + self.register_buffer('words_to_words', torch.full((len(vocab), ), fill_value=unknown_idx, dtype=torch.long).long()) + index = 0 + for word, index_in_vocab in vocab: + if index_in_vocab in matrix: + vec = matrix.get(index_in_vocab) + if vec is not None: # 使用找到的vector, 如果为None说明需要训练 + vectors[index] = vec + self.words_to_words[index_in_vocab] = index + index += 1 + + return vectors + + def forward(self, words): + r""" + 传入words的index + + :param words: torch.LongTensor, [batch_size, max_len] + :return: torch.FloatTensor, [batch_size, max_len, embed_size] + """ + if hasattr(self, 'words_to_words'): + words = self.words_to_words[words] + words = self.drop_word(words) + words = self.embedding(words) + words = self.dropout(words) + return words + + def save(self, folder): + """ + 将embedding存储到folder下,之后可以通过使用load方法读取 + + :param str folder: 会在该folder下生成三个文件, vocab.txt, static_embed_hyper.txt, static_embed_hyper.json. + 其中vocab.txt可以用Vocabulary通过load读取; embedding.txt按照word2vec的方式存储,以空格的方式隔开元素, + 第一行只有两个元素,剩下的行首先是word然后是各个维度的值; static_embed_hyper.json是StaticEmbedding的超参数 + :return: + """ + os.makedirs(folder, exist_ok=True) + + vocab = self.get_word_vocab() + vocab_fp = os.path.join(folder, VOCAB_FILENAME) + vocab.save(vocab_fp) + kwargs = self.kwargs.copy() + kwargs['dropout'] = self.dropout_layer.p + kwargs['word_dropout'] = self.word_dropout + kwargs['requires_grad'] = self.requires_grad + kwargs['only_norm_found_vector'] = False + kwargs['only_use_pretrain_word'] = True + + with open(os.path.join(folder, STATIC_HYPER_FILENAME), 'w', encoding='utf-8') as f: + json.dump(kwargs, f, indent=2) + + with open(os.path.join(folder, STATIC_EMBED_FILENAME), 'w', encoding='utf-8') as f: + f.write('{}\n'.format(' '*30)) # 留白之后再来填写 + word_count = 0 + saved_word = {} + valid_word_count = 0 + for i in range(len(self.words_to_words)): + word = vocab.to_word(i) + if not vocab._is_word_no_create_entry(word): + word_count += 1 + if kwargs['lower']: + word = word.lower() + if word in saved_word: + continue + saved_word[word] = 1 + vec_i = self.words_to_words[i] + if vec_i==vocab.unknown_idx and i!=vocab.unknown_idx: + continue + vec = self.embedding.weight.data[vec_i].tolist() + vec_str = ' '.join(map(str, vec)) + f.write(f'{word} {vec_str}\n') + valid_word_count += 1 + f.seek(0) + f.write('{} {}'.format(valid_word_count, self.embedding_dim)) + logger.debug(f"StaticEmbedding has been saved to {folder}.") + + @classmethod + def load(cls, folder): + """ + + :param str folder: 该folder下应该有以下三个文件vocab.txt, static_embed.txt, static_hyper.json + :return: + """ + for name in [VOCAB_FILENAME, STATIC_EMBED_FILENAME, STATIC_HYPER_FILENAME]: + assert os.path.exists(os.path.join(folder, name)), f"{name} not found in {folder}." + + vocab = Vocabulary.load(os.path.join(folder, VOCAB_FILENAME)) + with open(os.path.join(folder, STATIC_HYPER_FILENAME), 'r', encoding='utf-8') as f: + hyper = json.load(f) + + logger.info(f"Load StaticEmbedding from {folder}.") + embed = cls(vocab=vocab, model_dir_or_name=os.path.join(folder, STATIC_EMBED_FILENAME), **hyper) + return embed + diff --git a/fastNLP/embeddings/torch/utils.py b/fastNLP/embeddings/torch/utils.py new file mode 100644 index 00000000..31695048 --- /dev/null +++ b/fastNLP/embeddings/torch/utils.py @@ -0,0 +1,106 @@ +r""" +.. todo:: + doc +""" +import numpy as np +from ...envs.imports import _NEED_IMPORT_TORCH +if _NEED_IMPORT_TORCH: + import torch + from torch import nn as nn + +from ...core.vocabulary import Vocabulary + +__all__ = [ + 'get_embeddings', + 'get_sinusoid_encoding_table' +] + + +def _construct_char_vocab_from_vocab(vocab: Vocabulary, min_freq: int = 1, include_word_start_end=True): + r""" + 给定一个word的vocabulary生成character的vocabulary. + + :param vocab: 从vocab + :param min_freq: + :param include_word_start_end: 是否需要包含特殊的 + :return: + """ + char_vocab = Vocabulary(min_freq=min_freq) + for word, index in vocab: + if not vocab._is_word_no_create_entry(word): + char_vocab.add_word_lst(list(word)) + if include_word_start_end: + char_vocab.add_word_lst(['', '']) + return char_vocab + + +def get_embeddings(init_embed, padding_idx=None): + r""" + 根据输入的init_embed返回Embedding对象。如果输入是tuple, 则随机初始化一个nn.Embedding; 如果输入是numpy.ndarray, 则按照ndarray + 的值将nn.Embedding初始化; 如果输入是torch.Tensor, 则按该值初始化nn.Embedding; 如果输入是fastNLP中的embedding将不做处理 + 返回原对象。 + + :param init_embed: 可以是 tuple:(num_embedings, embedding_dim), 即embedding的大小和每个词的维度;也可以传入 + nn.Embedding 对象, 此时就以传入的对象作为embedding; 传入np.ndarray也行,将使用传入的ndarray作为作为Embedding初始化; + 传入torch.Tensor, 将使用传入的值作为Embedding初始化。 + :param padding_idx: 当传入tuple时,padding_idx有效 + :return nn.Embedding: embeddings + """ + if isinstance(init_embed, tuple): + res = nn.Embedding( + num_embeddings=init_embed[0], embedding_dim=init_embed[1], padding_idx=padding_idx) + nn.init.uniform_(res.weight.data, a=-np.sqrt(3 / res.weight.data.size(1)), + b=np.sqrt(3 / res.weight.data.size(1))) + elif isinstance(init_embed, nn.Module): + res = init_embed + elif isinstance(init_embed, torch.Tensor): + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + elif isinstance(init_embed, np.ndarray): + init_embed = torch.tensor(init_embed, dtype=torch.float32) + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + else: + raise TypeError( + 'invalid init_embed type: {}'.format((type(init_embed)))) + return res + + +def get_sinusoid_encoding_table(n_position, d_hid, padding_idx=None): + """ + sinusoid的embedding,其中position的表示中,偶数维(0,2,4,...)是sin, 奇数(1,3,5...)是cos + + :param int n_position: 一共多少个position + :param int d_hid: 多少维度,需要为偶数 + :param padding_idx: + :return: torch.FloatTensor, shape为n_position x d_hid + """ + + def cal_angle(position, hid_idx): + return position / np.power(10000, 2 * (hid_idx // 2) / d_hid) + + def get_posi_angle_vec(position): + return [cal_angle(position, hid_j) for hid_j in range(d_hid)] + + sinusoid_table = np.array([get_posi_angle_vec(pos_i) for pos_i in range(n_position)]) + + sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i + sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 + + if padding_idx is not None: + # zero vector for padding dimension + sinusoid_table[padding_idx] = 0. + + return torch.FloatTensor(sinusoid_table) + + +def _check_vocab_has_same_index(vocab, other_vocab): + """ + 检查两个vocabulary是否含有相同的word idx + + :param Vocabulary vocab: + :param Vocabulary other_vocab: + :return: + """ + if other_vocab != vocab: + for word, word_ix in vocab: + other_word_idx = other_vocab.to_index(word) + assert other_word_idx == word_ix, f"Word {word} has different index in vocabs, {word_ix} Vs. {other_word_idx}." \ No newline at end of file diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index 75edf1c5..290d8ffe 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -109,13 +109,9 @@ __all__ = [ "CMRC2018BertPipe", - 'ModelLoader', - 'ModelSaver', - ] from .data_bundle import DataBundle from .embed_loader import EmbedLoader from .loader import * -from .model_io import ModelLoader, ModelSaver from .pipe import * diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py deleted file mode 100644 index 30a8ef33..00000000 --- a/fastNLP/io/model_io.py +++ /dev/null @@ -1,71 +0,0 @@ -r""" -用于载入和保存模型 -""" -__all__ = [ - "ModelLoader", - "ModelSaver" -] - -from fastNLP.envs.imports import _NEED_IMPORT_TORCH -if _NEED_IMPORT_TORCH: - import torch - - -class ModelLoader: - r""" - 用于读取模型 - """ - - def __init__(self): - super(ModelLoader, self).__init__() - - @staticmethod - def load_pytorch(empty_model, model_path): - r""" - 从 ".pkl" 文件读取 PyTorch 模型 - - :param empty_model: 初始化参数的 PyTorch 模型 - :param str model_path: 模型保存的路径 - """ - empty_model.load_state_dict(torch.load(model_path)) - - @staticmethod - def load_pytorch_model(model_path): - r""" - 读取整个模型 - - :param str model_path: 模型保存的路径 - """ - return torch.load(model_path) - - -class ModelSaver(object): - r""" - 用于保存模型 - - Example:: - - saver = ModelSaver("./save/model_ckpt_100.pkl") - saver.save_pytorch(model) - - """ - - def __init__(self, save_path): - r""" - - :param save_path: 模型保存的路径 - """ - self.save_path = save_path - - def save_pytorch(self, model, param_only=True): - r""" - 把 PyTorch 模型存入 ".pkl" 文件 - - :param model: PyTorch 模型 - :param bool param_only: 是否只保存模型的参数(否则保存整个模型) - - """ - if param_only is True: - torch.save(model.state_dict(), self.save_path) - else: - torch.save(model, self.save_path) diff --git a/tests/core/drivers/paddle_driver/test_fleet.py b/tests/core/drivers/paddle_driver/test_fleet.py index 453af92a..ef22ba80 100644 --- a/tests/core/drivers/paddle_driver/test_fleet.py +++ b/tests/core/drivers/paddle_driver/test_fleet.py @@ -160,7 +160,7 @@ class TestSetDistReproDataloader: """ 传入的 `dist` 参数为具体的 ReproducibleSampler 或 ReproducibleBatchSampler 的情况 - 此时对应 driver.load 中的情况 + 此时对应 driver.load_checkpoint 中的情况 """ @magic_argv_env_context diff --git a/tests/core/drivers/torch_driver/test_ddp.py b/tests/core/drivers/torch_driver/test_ddp.py index 0e3f99ad..89e0a7ae 100644 --- a/tests/core/drivers/torch_driver/test_ddp.py +++ b/tests/core/drivers/torch_driver/test_ddp.py @@ -186,7 +186,7 @@ class TestSetDistReproDataloader: """ 传入的 `dist` 参数为具体的 ReproducibleSampler 或 ReproducibleBatchSampler 的情况 - 此时对应 driver.load 中的情况 + 此时对应 driver.load_checkpoint 中的情况 """ @magic_argv_env_context diff --git a/tests/embeddings/__init__.py b/tests/embeddings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/embeddings/torch/__init__.py b/tests/embeddings/torch/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/embeddings/torch/test_char_embedding.py b/tests/embeddings/torch/test_char_embedding.py new file mode 100644 index 00000000..81ed757a --- /dev/null +++ b/tests/embeddings/torch/test_char_embedding.py @@ -0,0 +1,29 @@ +import pytest +from fastNLP.envs.imports import _NEED_IMPORT_TORCH +if _NEED_IMPORT_TORCH: + import torch + +from fastNLP import Vocabulary, DataSet, Instance +from fastNLP.embeddings.torch.char_embedding import LSTMCharEmbedding, CNNCharEmbedding + + +class TestCharEmbed: + @pytest.mark.test + def test_case_1(self): + ds = DataSet([Instance(words=['hello', 'world']), Instance(words=['Jack'])]) + vocab = Vocabulary().from_dataset(ds, field_name='words') + assert len(vocab)==5 + embed = LSTMCharEmbedding(vocab, embed_size=3) + x = torch.LongTensor([[2, 1, 0], [4, 3, 4]]) + y = embed(x) + assert tuple(y.size()) == (2, 3, 3) + + @pytest.mark.test + def test_case_2(self): + ds = DataSet([Instance(words=['hello', 'world']), Instance(words=['Jack'])]) + vocab = Vocabulary().from_dataset(ds, field_name='words') + assert len(vocab)==5 + embed = CNNCharEmbedding(vocab, embed_size=3) + x = torch.LongTensor([[2, 1, 0], [4, 3, 4]]) + y = embed(x) + assert tuple(y.size()) == (2, 3, 3) diff --git a/tests/embeddings/torch/test_static_embedding.py b/tests/embeddings/torch/test_static_embedding.py new file mode 100644 index 00000000..f2b53607 --- /dev/null +++ b/tests/embeddings/torch/test_static_embedding.py @@ -0,0 +1,195 @@ +import pytest +import os + +from fastNLP.embeddings.torch import StaticEmbedding +from fastNLP import Vocabulary +from fastNLP.envs.imports import _NEED_IMPORT_TORCH +if _NEED_IMPORT_TORCH: + import torch +import numpy as np + +tests_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')) + +@pytest.mark.torch +class TestLoad: + def test_norm1(self): + # 测试只对可以找到的norm + vocab = Vocabulary().add_word_lst(['the', 'a', 'notinfile']) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + only_norm_found_vector=True) + assert round(torch.norm(embed(torch.LongTensor([[2]]))).item(), 4) == 1 + assert torch.norm(embed(torch.LongTensor([[4]]))).item() != 1 + + def test_norm2(self): + # 测试对所有都norm + vocab = Vocabulary().add_word_lst(['the', 'a', 'notinfile']) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + normalize=True) + assert round(torch.norm(embed(torch.LongTensor([[2]]))).item(), 4) == 1 + assert round(torch.norm(embed(torch.LongTensor([[4]]))).item(), 4) == 1 + + def test_dropword(self): + # 测试是否可以通过drop word + vocab = Vocabulary().add_word_lst([chr(i) for i in range(1, 200)]) + embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=10, dropout=0.1, word_dropout=0.4) + for i in range(10): + length = torch.randint(1, 50, (1,)).item() + batch = torch.randint(1, 4, (1,)).item() + words = torch.randint(1, 200, (batch, length)).long() + embed(words) + + def test_only_use_pretrain_word(self): + def check_word_unk(words, vocab, embed): + for word in words: + assert embed(torch.LongTensor([vocab.to_index(word)])).tolist()[0] == embed(torch.LongTensor([1])).tolist()[0] + + def check_vector_equal(words, vocab, embed, embed_dict, lower=False): + for word in words: + index = vocab.to_index(word) + v1 = embed(torch.LongTensor([index])).tolist()[0] + if lower: + word = word.lower() + v2 = embed_dict[word] + for v1i, v2i in zip(v1, v2): + assert np.allclose(v1i, v2i) + embed_dict = read_static_embed(tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt') + + # 测试是否只使用pretrain的word + vocab = Vocabulary().add_word_lst(['the', 'a', 'notinfile']) + vocab.add_word('of', no_create_entry=True) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + only_use_pretrain_word=True) + # notinfile应该被置为unk + check_vector_equal(['the', 'a', 'of'], vocab, embed, embed_dict) + check_word_unk(['notinfile'], vocab, embed) + + # 测试在大小写情况下的使用 + vocab = Vocabulary().add_word_lst(['The', 'a', 'notinfile']) + vocab.add_word('Of', no_create_entry=True) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + only_use_pretrain_word=True) + check_word_unk(['The', 'Of', 'notinfile'], vocab, embed) # 这些词应该找不到 + check_vector_equal(['a'], vocab, embed, embed_dict) + + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + only_use_pretrain_word=True, lower=True) + check_vector_equal(['The', 'Of', 'a'], vocab, embed, embed_dict, lower=True) + check_word_unk(['notinfile'], vocab, embed) + + # 测试min_freq + vocab = Vocabulary().add_word_lst(['The', 'a', 'notinfile1', 'A', 'notinfile2', 'notinfile2']) + vocab.add_word('Of', no_create_entry=True) + + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', + only_use_pretrain_word=True, lower=True, min_freq=2, only_train_min_freq=True) + + check_vector_equal(['Of', 'a'], vocab, embed, embed_dict, lower=True) + check_word_unk(['notinfile1', 'The', 'notinfile2'], vocab, embed) + + def test_sequential_index(self): + # 当不存在no_create_entry时,words_to_words应该是顺序的 + vocab = Vocabulary().add_word_lst(['The', 'a', 'notinfile1', 'A', 'notinfile2', 'notinfile2']) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt') + for index,i in enumerate(embed.words_to_words): + assert index==i + + embed_dict = read_static_embed(tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt') + + for word, index in vocab: + if word in embed_dict: + index = vocab.to_index(word) + v1 = embed(torch.LongTensor([index])).tolist()[0] + v2 = embed_dict[word] + for v1i, v2i in zip(v1, v2): + assert np.allclose(v1i, v2i) + + def test_save_load_static_embed(self): + static_test_folder = 'static_save_test' + try: + # 测试包含no_create_entry + os.makedirs(static_test_folder, exist_ok=True) + + vocab = Vocabulary().add_word_lst(['The', 'a', 'notinfile1', 'A']) + vocab.add_word_lst(['notinfile2', 'notinfile2'], no_create_entry=True) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt') + embed.save(static_test_folder) + load_embed = StaticEmbedding.load(static_test_folder) + words = torch.randint(len(vocab), size=(2, 20)) + assert (embed(words) - load_embed(words)).sum() == 0 + + # 测试不包含no_create_entry + vocab = Vocabulary().add_word_lst(['The', 'a', 'notinfile1', 'A']) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt') + embed.save(static_test_folder) + load_embed = StaticEmbedding.load(static_test_folder) + words = torch.randint(len(vocab), size=(2, 20)) + assert (embed(words) - load_embed(words)).sum() == 0 + + # 测试lower, min_freq + vocab = Vocabulary().add_word_lst(['The', 'the', 'the', 'A', 'a', 'B']) + embed = StaticEmbedding(vocab, model_dir_or_name=tests_folder+'/helpers/data/embedding/small_static_embedding/' + 'glove.6B.50d_test.txt', min_freq=2, lower=True) + embed.save(static_test_folder) + load_embed = StaticEmbedding.load(static_test_folder) + words = torch.randint(len(vocab), size=(2, 20)) + assert (embed(words) - load_embed(words)).sum() == 0 + + # 测试random的embedding + vocab = Vocabulary().add_word_lst(['The', 'the', 'the', 'A', 'a', 'B']) + vocab = vocab.add_word_lst(['b'], no_create_entry=True) + embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=4, min_freq=2, lower=True, + normalize=True) + embed.weight.data += 0.2 # 使得它不是normalize + embed.save(static_test_folder) + load_embed = StaticEmbedding.load(static_test_folder) + words = torch.randint(len(vocab), size=(2, 20)) + assert (embed(words) - load_embed(words)).sum()==0 + + finally: + if os.path.isdir(static_test_folder): + import shutil + shutil.rmtree(static_test_folder) + + +def read_static_embed(fp): + """ + + :param str fp: embedding的路径 + :return: {}, key是word, value是vector + """ + embed = {} + with open(fp, 'r') as f: + for line in f: + line = line.strip() + if line: + parts = line.split() + vector = list(map(float, parts[1:])) + word = parts[0] + embed[word] = vector + return embed + + +@pytest.mark.torch +class TestRandomSameEntry: + def test_same_vector(self): + vocab = Vocabulary().add_word_lst(["The", "the", "THE", 'a', "A"]) + embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=5, lower=True) + words = torch.LongTensor([[vocab.to_index(word) for word in ["The", "the", "THE", 'a', 'A']]]) + words = embed(words) + embed_0 = words[0, 0] + for i in range(1, 3): + assert torch.sum(embed_0==words[0, i]).eq(len(embed_0)) + embed_0 = words[0, 3] + for i in range(3, 5): + assert torch.sum(embed_0 == words[0, i]).eq(len(embed_0)) diff --git a/tests/helpers/data/embedding/small_static_embedding/glove.6B.50d_test.txt b/tests/helpers/data/embedding/small_static_embedding/glove.6B.50d_test.txt new file mode 100644 index 00000000..707e48e8 --- /dev/null +++ b/tests/helpers/data/embedding/small_static_embedding/glove.6B.50d_test.txt @@ -0,0 +1,6 @@ +the 0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 -0.6566 0.27843 -0.14767 -0.55677 0.14658 -0.0095095 0.011658 0.10204 -0.12792 -0.8443 -0.12181 -0.016801 -0.33279 -0.1552 -0.23131 -0.19181 -1.8823 -0.76746 0.099051 -0.42125 -0.19526 4.0071 -0.18594 -0.52287 -0.31681 0.00059213 0.0074449 0.17778 -0.15897 0.012041 -0.054223 -0.29871 -0.15749 -0.34758 -0.045637 -0.44251 0.18785 0.0027849 -0.18411 -0.11514 -0.78581 +of 0.70853 0.57088 -0.4716 0.18048 0.54449 0.72603 0.18157 -0.52393 0.10381 -0.17566 0.078852 -0.36216 -0.11829 -0.83336 0.11917 -0.16605 0.061555 -0.012719 -0.56623 0.013616 0.22851 -0.14396 -0.067549 -0.38157 -0.23698 -1.7037 -0.86692 -0.26704 -0.2589 0.1767 3.8676 -0.1613 -0.13273 -0.68881 0.18444 0.0052464 -0.33874 -0.078956 0.24185 0.36576 -0.34727 0.28483 0.075693 -0.062178 -0.38988 0.22902 -0.21617 -0.22562 -0.093918 -0.80375 +to 0.68047 -0.039263 0.30186 -0.17792 0.42962 0.032246 -0.41376 0.13228 -0.29847 -0.085253 0.17118 0.22419 -0.10046 -0.43653 0.33418 0.67846 0.057204 -0.34448 -0.42785 -0.43275 0.55963 0.10032 0.18677 -0.26854 0.037334 -2.0932 0.22171 -0.39868 0.20912 -0.55725 3.8826 0.47466 -0.95658 -0.37788 0.20869 -0.32752 0.12751 0.088359 0.16351 -0.21634 -0.094375 0.018324 0.21048 -0.03088 -0.19722 0.082279 -0.09434 -0.073297 -0.064699 -0.26044 +and 0.26818 0.14346 -0.27877 0.016257 0.11384 0.69923 -0.51332 -0.47368 -0.33075 -0.13834 0.2702 0.30938 -0.45012 -0.4127 -0.09932 0.038085 0.029749 0.10076 -0.25058 -0.51818 0.34558 0.44922 0.48791 -0.080866 -0.10121 -1.3777 -0.10866 -0.23201 0.012839 -0.46508 3.8463 0.31362 0.13643 -0.52244 0.3302 0.33707 -0.35601 0.32431 0.12041 0.3512 -0.069043 0.36885 0.25168 -0.24517 0.25381 0.1367 -0.31178 -0.6321 -0.25028 -0.38097 +in 0.33042 0.24995 -0.60874 0.10923 0.036372 0.151 -0.55083 -0.074239 -0.092307 -0.32821 0.09598 -0.82269 -0.36717 -0.67009 0.42909 0.016496 -0.23573 0.12864 -1.0953 0.43334 0.57067 -0.1036 0.20422 0.078308 -0.42795 -1.7984 -0.27865 0.11954 -0.12689 0.031744 3.8631 -0.17786 -0.082434 -0.62698 0.26497 -0.057185 -0.073521 0.46103 0.30862 0.12498 -0.48609 -0.0080272 0.031184 -0.36576 -0.42699 0.42164 -0.11666 -0.50703 -0.027273 -0.53285 +a 0.21705 0.46515 -0.46757 0.10082 1.0135 0.74845 -0.53104 -0.26256 0.16812 0.13182 -0.24909 -0.44185 -0.21739 0.51004 0.13448 -0.43141 -0.03123 0.20674 -0.78138 -0.20148 -0.097401 0.16088 -0.61836 -0.18504 -0.12461 -2.2526 -0.22321 0.5043 0.32257 0.15313 3.9636 -0.71365 -0.67012 0.28388 0.21738 0.14433 0.25926 0.23434 0.4274 -0.44451 0.13813 0.36973 -0.64289 0.024142 -0.039315 -0.26037 0.12017 -0.043782 0.41013 0.1796 \ No newline at end of file diff --git a/tests/helpers/data/embedding/small_static_embedding/word2vec_test.txt b/tests/helpers/data/embedding/small_static_embedding/word2vec_test.txt new file mode 100644 index 00000000..c16170f2 --- /dev/null +++ b/tests/helpers/data/embedding/small_static_embedding/word2vec_test.txt @@ -0,0 +1,7 @@ +5 50 +the 0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 -0.6566 0.27843 -0.14767 -0.55677 0.14658 -0.0095095 0.011658 0.10204 -0.12792 -0.8443 -0.12181 -0.016801 -0.33279 -0.1552 -0.23131 -0.19181 -1.8823 -0.76746 0.099051 -0.42125 -0.19526 4.0071 -0.18594 -0.52287 -0.31681 0.00059213 0.0074449 0.17778 -0.15897 0.012041 -0.054223 -0.29871 -0.15749 -0.34758 -0.045637 -0.44251 0.18785 0.0027849 -0.18411 -0.11514 -0.78581 +of 0.70853 0.57088 -0.4716 0.18048 0.54449 0.72603 0.18157 -0.52393 0.10381 -0.17566 0.078852 -0.36216 -0.11829 -0.83336 0.11917 -0.16605 0.061555 -0.012719 -0.56623 0.013616 0.22851 -0.14396 -0.067549 -0.38157 -0.23698 -1.7037 -0.86692 -0.26704 -0.2589 0.1767 3.8676 -0.1613 -0.13273 -0.68881 0.18444 0.0052464 -0.33874 -0.078956 0.24185 0.36576 -0.34727 0.28483 0.075693 -0.062178 -0.38988 0.22902 -0.21617 -0.22562 -0.093918 -0.80375 +to 0.68047 -0.039263 0.30186 -0.17792 0.42962 0.032246 -0.41376 0.13228 -0.29847 -0.085253 0.17118 0.22419 -0.10046 -0.43653 0.33418 0.67846 0.057204 -0.34448 -0.42785 -0.43275 0.55963 0.10032 0.18677 -0.26854 0.037334 -2.0932 0.22171 -0.39868 0.20912 -0.55725 3.8826 0.47466 -0.95658 -0.37788 0.20869 -0.32752 0.12751 0.088359 0.16351 -0.21634 -0.094375 0.018324 0.21048 -0.03088 -0.19722 0.082279 -0.09434 -0.073297 -0.064699 -0.26044 +and 0.26818 0.14346 -0.27877 0.016257 0.11384 0.69923 -0.51332 -0.47368 -0.33075 -0.13834 0.2702 0.30938 -0.45012 -0.4127 -0.09932 0.038085 0.029749 0.10076 -0.25058 -0.51818 0.34558 0.44922 0.48791 -0.080866 -0.10121 -1.3777 -0.10866 -0.23201 0.012839 -0.46508 3.8463 0.31362 0.13643 -0.52244 0.3302 0.33707 -0.35601 0.32431 0.12041 0.3512 -0.069043 0.36885 0.25168 -0.24517 0.25381 0.1367 -0.31178 -0.6321 -0.25028 -0.38097 +in 0.33042 0.24995 -0.60874 0.10923 0.036372 0.151 -0.55083 -0.074239 -0.092307 -0.32821 0.09598 -0.82269 -0.36717 -0.67009 0.42909 0.016496 -0.23573 0.12864 -1.0953 0.43334 0.57067 -0.1036 0.20422 0.078308 -0.42795 -1.7984 -0.27865 0.11954 -0.12689 0.031744 3.8631 -0.17786 -0.082434 -0.62698 0.26497 -0.057185 -0.073521 0.46103 0.30862 0.12498 -0.48609 -0.0080272 0.031184 -0.36576 -0.42699 0.42164 -0.11666 -0.50703 -0.027273 -0.53285 +a 0.21705 0.46515 -0.46757 0.10082 1.0135 0.74845 -0.53104 -0.26256 0.16812 0.13182 -0.24909 -0.44185 -0.21739 0.51004 0.13448 -0.43141 -0.03123 0.20674 -0.78138 -0.20148 -0.097401 0.16088 -0.61836 -0.18504 -0.12461 -2.2526 -0.22321 0.5043 0.32257 0.15313 3.9636 -0.71365 -0.67012 0.28388 0.21738 0.14433 0.25926 0.23434 0.4274 -0.44451 0.13813 0.36973 -0.64289 0.024142 -0.039315 -0.26037 0.12017 -0.043782 0.41013 0.1796 \ No newline at end of file From 655e48de99d7ab71d68c76a5c6659d2264d0bb0c Mon Sep 17 00:00:00 2001 From: x54-729 <17307130121@fudan.edu.cn> Date: Fri, 13 May 2022 11:19:31 +0000 Subject: [PATCH 14/14] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9torch=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BE=8B=E4=B8=AD=E5=A4=9A=E5=8D=A1=E7=9A=84driver?= =?UTF-8?q?=E5=8F=82=E6=95=B0=202.=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=BE=8B=E4=B8=AD=E7=9A=84driver.save=20driver.load=E4=B8=BAdr?= =?UTF-8?q?iver.save=5Fcheckpoint=20driver.load=5Fcheckpoint=203.=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0lstm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../torch_driver/initialize_torch_driver.py | 3 +- fastNLP/modules/torch/encoder/__init__.py | 5 ++ fastNLP/modules/torch/encoder/lstm.py | 82 +++++++++++++++++++ .../test_checkpoint_callback_torch.py | 28 +++---- .../test_load_best_model_callback_torch.py | 2 +- .../callbacks/test_more_evaluate_callback.py | 6 +- .../_test_distributed_launch_torch_1.py | 2 +- .../_test_distributed_launch_torch_2.py | 2 +- .../test_trainer_wo_evaluator_torch.py | 2 +- .../core/drivers/paddle_driver/test_fleet.py | 12 +-- .../paddle_driver/test_single_device.py | 12 +-- tests/core/drivers/torch_driver/test_ddp.py | 10 +-- .../test_initialize_torch_driver.py | 41 +--------- .../torch_driver/test_single_device.py | 8 +- tests/embeddings/torch/test_char_embedding.py | 5 +- 15 files changed, 136 insertions(+), 84 deletions(-) create mode 100644 fastNLP/modules/torch/encoder/__init__.py create mode 100644 fastNLP/modules/torch/encoder/lstm.py diff --git a/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py b/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py index 723765d2..f8fe63d8 100644 --- a/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py +++ b/fastNLP/core/drivers/torch_driver/initialize_torch_driver.py @@ -32,7 +32,7 @@ def initialize_torch_driver(driver: str, device: Optional[Union[str, "torch.devi return TorchDDPDriver(model, torch.device(f"cuda:{os.environ['LOCAL_RANK']}"), True, **kwargs) if driver not in {"torch", "fairscale"}: - raise ValueError("Parameter `driver` can only be one of these values: ['torch', 'torch_ddp', 'fairscale'].") + raise ValueError("Parameter `driver` can only be one of these values: ['torch', 'fairscale'].") _could_use_device_num = torch.cuda.device_count() if isinstance(device, str): @@ -43,6 +43,7 @@ def initialize_torch_driver(driver: str, device: Optional[Union[str, "torch.devi raise ValueError("Parameter `device` can only be '-1' when it is smaller than 0.") device = [torch.device(f"cuda:{w}") for w in range(_could_use_device_num)] elif device >= _could_use_device_num: + print(device, _could_use_device_num) raise ValueError("The gpu device that parameter `device` specifies is not existed.") else: device = torch.device(f"cuda:{device}") diff --git a/fastNLP/modules/torch/encoder/__init__.py b/fastNLP/modules/torch/encoder/__init__.py new file mode 100644 index 00000000..d893305f --- /dev/null +++ b/fastNLP/modules/torch/encoder/__init__.py @@ -0,0 +1,5 @@ +__all__ = [ + "LSTM", +] + +from .lstm import LSTM \ No newline at end of file diff --git a/fastNLP/modules/torch/encoder/lstm.py b/fastNLP/modules/torch/encoder/lstm.py new file mode 100644 index 00000000..bd0d844d --- /dev/null +++ b/fastNLP/modules/torch/encoder/lstm.py @@ -0,0 +1,82 @@ +r"""undocumented +轻量封装的 Pytorch LSTM 模块. +可在 forward 时传入序列的长度, 自动对padding做合适的处理. +""" + +__all__ = [ + "LSTM" +] + +import torch +import torch.nn as nn +import torch.nn.utils.rnn as rnn + + +class LSTM(nn.Module): + r""" + LSTM 模块, 轻量封装的Pytorch LSTM. 在提供seq_len的情况下,将自动使用pack_padded_sequence; 同时默认将forget gate的bias初始化 + 为1; 且可以应对DataParallel中LSTM的使用问题。 + """ + + def __init__(self, input_size, hidden_size=100, num_layers=1, dropout=0.0, batch_first=True, + bidirectional=False, bias=True): + r""" + + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度. 如果bidirectional为True,则输出的维度会是hidde_size*2 + :param num_layers: rnn的层数. Default: 1 + :param dropout: 层间dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + """ + super(LSTM, self).__init__() + self.batch_first = batch_first + self.lstm = nn.LSTM(input_size, hidden_size, num_layers, bias=bias, batch_first=batch_first, + dropout=dropout, bidirectional=bidirectional) + self.init_param() + + def init_param(self): + for name, param in self.named_parameters(): + if 'bias' in name: + # based on https://github.com/pytorch/pytorch/issues/750#issuecomment-280671871 + param.data.fill_(0) + n = param.size(0) + start, end = n // 4, n // 2 + param.data[start:end].fill_(1) + else: + nn.init.xavier_uniform_(param) + + def forward(self, x, seq_len=None, h0=None, c0=None): + r""" + :param x: [batch, seq_len, input_size] 输入序列 + :param seq_len: [batch, ] 序列长度, 若为 ``None``, 所有输入看做一样长. Default: ``None`` + :param h0: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全0向量. Default: ``None`` + :param c0: [batch, hidden_size] 初始Cell状态, 若为 ``None`` , 设为全0向量. Default: ``None`` + :return (output, (ht, ct)): output: [batch, seq_len, hidden_size*num_direction] 输出序列 + 和 ht,ct: [num_layers*num_direction, batch, hidden_size] 最后时刻隐状态. + """ + batch_size, max_len, _ = x.size() + if h0 is not None and c0 is not None: + hx = (h0, c0) + else: + hx = None + if seq_len is not None and not isinstance(x, rnn.PackedSequence): + sort_lens, sort_idx = torch.sort(seq_len, dim=0, descending=True) + if self.batch_first: + x = x[sort_idx] + else: + x = x[:, sort_idx] + x = rnn.pack_padded_sequence(x, sort_lens.cpu(), batch_first=self.batch_first) + output, hx = self.lstm(x, hx) # -> [N,L,C] + output, _ = rnn.pad_packed_sequence(output, batch_first=self.batch_first, total_length=max_len) + _, unsort_idx = torch.sort(sort_idx, dim=0, descending=False) + if self.batch_first: + output = output[unsort_idx] + else: + output = output[:, unsort_idx] + hx = hx[0][:, unsort_idx], hx[1][:, unsort_idx] + else: + output, hx = self.lstm(x, hx) + return output, hx \ No newline at end of file diff --git a/tests/core/callbacks/test_checkpoint_callback_torch.py b/tests/core/callbacks/test_checkpoint_callback_torch.py index 3105acba..5f7d553f 100644 --- a/tests/core/callbacks/test_checkpoint_callback_torch.py +++ b/tests/core/callbacks/test_checkpoint_callback_torch.py @@ -74,7 +74,7 @@ def model_and_optimizers(request): @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", [0, 1])]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("version", [0, 1]) @pytest.mark.parametrize("only_state_dict", [True, False]) @magic_argv_env_context(timeout=100) @@ -121,7 +121,7 @@ def test_model_checkpoint_callback_1( # 检查生成保存模型文件的数量是不是正确的; if version == 0: - if driver == "torch": + if not isinstance(device, list): assert "model-epoch_10" in all_saved_model_paths assert "model-epoch_4-batch_123" in all_saved_model_paths @@ -144,7 +144,7 @@ def test_model_checkpoint_callback_1( pattern = re.compile("model-epoch_[0-9]+-batch_[0-9]+-[a-zA-Z#]+_[0-9]*.?[0-9]*") - if driver == "torch": + if not isinstance(device, list): assert "model-epoch_9" in all_saved_model_paths assert "model-last" in all_saved_model_paths aLL_topk_folders = [] @@ -206,7 +206,7 @@ def test_model_checkpoint_callback_1( @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch", [0, 1]), ("torch", 1)]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("only_state_dict", [True]) @magic_argv_env_context(timeout=100) def test_model_checkpoint_callback_2( @@ -259,7 +259,7 @@ def test_model_checkpoint_callback_2( # 检查生成保存模型文件的数量是不是正确的; all_saved_model_paths = {w.name: w for w in path.joinpath(os.environ[FASTNLP_LAUNCH_TIME]).iterdir()} - if driver == "torch": + if not isinstance(device, list): assert "model-epoch_4-batch_100-exception_NotImplementedError" in all_saved_model_paths exception_model_path = all_saved_model_paths["model-epoch_4-batch_100-exception_NotImplementedError"] # ddp 下的文件名不同,因为同样的数据,ddp 用了更少的步数跑完; @@ -299,7 +299,7 @@ def test_model_checkpoint_callback_2( @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 0)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch", [0, 1]), ("torch", 0)]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("version", [0, 1]) @pytest.mark.parametrize("only_state_dict", [True, False]) @magic_argv_env_context(timeout=100) @@ -347,7 +347,7 @@ def test_trainer_checkpoint_callback_1( # 检查生成保存模型文件的数量是不是正确的; if version == 0: - if driver == "torch": + if not isinstance(device, list): assert "trainer-epoch_7" in all_saved_model_paths assert "trainer-epoch_4-batch_123" in all_saved_model_paths @@ -371,7 +371,7 @@ def test_trainer_checkpoint_callback_1( pattern = re.compile("trainer-epoch_[0-9]+-batch_[0-9]+-[a-zA-Z#]+_[0-9]*.?[0-9]*") # all_saved_model_paths = {w.name: w for w in path.joinpath(os.environ[FASTNLP_LAUNCH_TIME]).iterdir()} - if driver == "torch": + if not isinstance(device, list): assert "trainer-last" in all_saved_model_paths aLL_topk_folders = [] for each_folder_name in all_saved_model_paths: @@ -417,7 +417,7 @@ def test_trainer_checkpoint_callback_1( n_epochs=13, output_from_new_proc="all" ) - trainer.load(folder, only_state_dict=only_state_dict) + trainer.load_checkpoint(folder, only_state_dict=only_state_dict) trainer.run() trainer.driver.barrier() @@ -489,7 +489,7 @@ def test_load_state(model_and_optimizers): callbacks=callbacks, output_from_new_proc="all" ) - trainer.load(folder=epoch_2_path) + trainer.load_checkpoint(folder=epoch_2_path) with Capturing() as output: trainer.run(num_eval_sanity_batch=0, num_train_batch_per_epoch=2) @@ -503,7 +503,7 @@ def test_load_state(model_and_optimizers): @pytest.mark.torch # 通过自己编写 model_save_fn 和 model_load_fn 来测试 huggingface 的 transformers 的模型的保存和加载; -@pytest.mark.parametrize("driver,device", [("torch_ddp", [6, 7]), ("torch", 7)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", [6, 7]), ("torch", 7)]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("version", [0, 1]) @magic_argv_env_context @pytest.mark.skip("Skip transformers test for now.") @@ -675,7 +675,7 @@ def test_trainer_checkpoint_callback_2( # 检查生成保存模型文件的数量是不是正确的; if version == 0: - if driver == "torch": + if not isinstance(device, list): assert "trainer-epoch_1-batch_200" in all_saved_model_paths epoch_save_path = all_saved_model_paths["trainer-epoch_1-batch_200"] @@ -695,7 +695,7 @@ def test_trainer_checkpoint_callback_2( pattern = re.compile("trainer-epoch_[0-9]+-batch_[0-9]+-[a-zA-Z#]+_[0-9]*.?[0-9]*") # all_saved_model_paths = {w.name: w for w in path.joinpath(os.environ[FASTNLP_LAUNCH_TIME]).iterdir()} - if driver == "torch": + if not isinstance(device, list): assert "trainer-last" in all_saved_model_paths aLL_topk_folders = [] for each_folder_name in all_saved_model_paths: @@ -740,7 +740,7 @@ def test_trainer_checkpoint_callback_2( output_mapping=bert_output_mapping, metrics={"acc": acc}, ) - trainer.load(folder, model_load_fn=model_load_fn) + trainer.load_checkpoint(folder, model_load_fn=model_load_fn) trainer.run() trainer.driver.barrier() diff --git a/tests/core/callbacks/test_load_best_model_callback_torch.py b/tests/core/callbacks/test_load_best_model_callback_torch.py index 7501aabf..c607bb87 100644 --- a/tests/core/callbacks/test_load_best_model_callback_torch.py +++ b/tests/core/callbacks/test_load_best_model_callback_torch.py @@ -72,7 +72,7 @@ def model_and_optimizers(request): @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch_ddp", [4, 5]), ("torch", 1), ("torch", "cpu")]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", [4, 5]), ("torch", 1), ("torch", "cpu")]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("save_folder", ['save_models', None]) @pytest.mark.parametrize("only_state_dict", [True, False]) @magic_argv_env_context diff --git a/tests/core/callbacks/test_more_evaluate_callback.py b/tests/core/callbacks/test_more_evaluate_callback.py index 9c32c20b..925be172 100644 --- a/tests/core/callbacks/test_more_evaluate_callback.py +++ b/tests/core/callbacks/test_more_evaluate_callback.py @@ -98,7 +98,7 @@ def model_and_optimizers(request): @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch", [0, 1]), ("torch", 1)]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("version", [0, 1]) @pytest.mark.parametrize("only_state_dict", [True, False]) @magic_argv_env_context @@ -183,7 +183,7 @@ def test_model_more_evaluate_callback_1( @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 0)]) # ("torch", "cpu"), ("torch_ddp", [0, 1]), ("torch", 1) +@pytest.mark.parametrize("driver,device", [("torch", "cpu"), ("torch", [0, 1]), ("torch", 0)]) # ("torch", "cpu"), ("torch", [0, 1]), ("torch", 1) @pytest.mark.parametrize("version", [0, 1]) @pytest.mark.parametrize("only_state_dict", [True, False]) @magic_argv_env_context @@ -256,7 +256,7 @@ def test_trainer_checkpoint_callback_1( evaluate_fn='train_step' ) folder = path.joinpath(os.environ[FASTNLP_LAUNCH_TIME]).joinpath(folder) - trainer.load(folder, only_state_dict=only_state_dict) + trainer.load_checkpoint(folder, only_state_dict=only_state_dict) trainer.run() trainer.driver.barrier() diff --git a/tests/core/controllers/_test_distributed_launch_torch_1.py b/tests/core/controllers/_test_distributed_launch_torch_1.py index 60f5e36f..0f607423 100644 --- a/tests/core/controllers/_test_distributed_launch_torch_1.py +++ b/tests/core/controllers/_test_distributed_launch_torch_1.py @@ -85,7 +85,7 @@ def _test_trainer_torch_with_evaluator_fp16_accumulation_steps( ): trainer = Trainer( model=model, - driver="torch_ddp", + driver="torch", device=None, optimizers=optimizers, train_dataloader=train_dataloader, diff --git a/tests/core/controllers/_test_distributed_launch_torch_2.py b/tests/core/controllers/_test_distributed_launch_torch_2.py index 37b22590..650f2782 100644 --- a/tests/core/controllers/_test_distributed_launch_torch_2.py +++ b/tests/core/controllers/_test_distributed_launch_torch_2.py @@ -73,7 +73,7 @@ def _test_trainer_torch_with_evaluator_fp16_accumulation_steps( ): trainer = Trainer( model=model, - driver="torch_ddp", + driver="torch", device=None, optimizers=optimizers, train_dataloader=train_dataloader, diff --git a/tests/core/controllers/test_trainer_wo_evaluator_torch.py b/tests/core/controllers/test_trainer_wo_evaluator_torch.py index e3d90e9b..5b794459 100644 --- a/tests/core/controllers/test_trainer_wo_evaluator_torch.py +++ b/tests/core/controllers/test_trainer_wo_evaluator_torch.py @@ -318,7 +318,7 @@ def test_torch_distributed_launch_2(version): @pytest.mark.torch -@pytest.mark.parametrize("driver,device", [("torch", 0), ("torch_ddp", [0, 1])]) +@pytest.mark.parametrize("driver,device", [("torch", 0), ("torch", [0, 1])]) @magic_argv_env_context def test_torch_wo_auto_param_call( driver, diff --git a/tests/core/drivers/paddle_driver/test_fleet.py b/tests/core/drivers/paddle_driver/test_fleet.py index ef22ba80..d3bffb9f 100644 --- a/tests/core/drivers/paddle_driver/test_fleet.py +++ b/tests/core/drivers/paddle_driver/test_fleet.py @@ -626,9 +626,9 @@ class TestSaveLoad: sampler_states = dataloader.batch_sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} if only_state_dict: - self.driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + self.driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) else: - self.driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) + self.driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) # 加载 # 更改 batch_size dataloader = DataLoader( @@ -644,7 +644,7 @@ class TestSaveLoad: rank=self.driver2.global_rank, pad=True ) - load_states = self.driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = self.driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 # TODO optimizer 的 state_dict 总是为空 @@ -736,9 +736,9 @@ class TestSaveLoad: sampler_states = dataloader.batch_sampler.sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} if only_state_dict: - self.driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + self.driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) else: - self.driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) + self.driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) # 加载 # 更改 batch_size batch_sampler = BatchSampler(dataset=self.dataset, batch_size=2) @@ -752,7 +752,7 @@ class TestSaveLoad: self.dataset, batch_sampler=batch_sampler ) - load_states = self.driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = self.driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 diff --git a/tests/core/drivers/paddle_driver/test_single_device.py b/tests/core/drivers/paddle_driver/test_single_device.py index ffcb35e7..e7d6707a 100644 --- a/tests/core/drivers/paddle_driver/test_single_device.py +++ b/tests/core/drivers/paddle_driver/test_single_device.py @@ -615,16 +615,16 @@ def test_save_and_load_with_randombatchsampler(only_state_dict, fp16): sampler_states = dataloader.batch_sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} if only_state_dict: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) else: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) # 加载 # 更改 batch_size dataloader = DataLoader( dataset=dataset, batch_sampler=ReproduceBatchSampler(BatchSampler(dataset, batch_size=2, shuffle=True), 2, False) ) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 # TODO optimizer 的 state_dict 总是为空 @@ -697,9 +697,9 @@ def test_save_and_load_with_randomsampler(only_state_dict, fp16): sampler_states = dataloader.batch_sampler.sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} if only_state_dict: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) else: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[paddle.ones((16, 10))]) # 加载 # 更改 batch_size @@ -709,7 +709,7 @@ def test_save_and_load_with_randomsampler(only_state_dict, fp16): dataset, batch_sampler=batch_sampler ) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 diff --git a/tests/core/drivers/torch_driver/test_ddp.py b/tests/core/drivers/torch_driver/test_ddp.py index 89e0a7ae..cb7ed68c 100644 --- a/tests/core/drivers/torch_driver/test_ddp.py +++ b/tests/core/drivers/torch_driver/test_ddp.py @@ -648,7 +648,7 @@ class TestSaveLoad: # 保存状态 sampler_states = dataloader.batch_sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) # 加载 # 更改 batch_size dataloader = dataloader_with_bucketedbatchsampler( @@ -663,7 +663,7 @@ class TestSaveLoad: rank=driver2.global_rank, pad=True ) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 # TODO optimizer 的 state_dict 总是为空 @@ -754,9 +754,9 @@ class TestSaveLoad: sampler_states = dataloader.batch_sampler.sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} if only_state_dict: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) else: - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[torch.ones((16, 10))]) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True, input_spec=[torch.ones((16, 10))]) # 加载 # 更改 batch_size dataloader = dataloader_with_randomsampler(self.dataset, 2, True, False, unrepeated=False) @@ -765,7 +765,7 @@ class TestSaveLoad: rank=driver2.global_rank, pad=True ) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 diff --git a/tests/core/drivers/torch_driver/test_initialize_torch_driver.py b/tests/core/drivers/torch_driver/test_initialize_torch_driver.py index 8ec70de1..dc89ad0d 100644 --- a/tests/core/drivers/torch_driver/test_initialize_torch_driver.py +++ b/tests/core/drivers/torch_driver/test_initialize_torch_driver.py @@ -37,28 +37,6 @@ def test_get_single_device(driver, device): driver = initialize_torch_driver(driver, device, model) assert isinstance(driver, TorchSingleDriver) - -@pytest.mark.torch -@pytest.mark.parametrize( - "device", - [0, 1] -) -@pytest.mark.parametrize( - "driver", - ["torch_ddp"] -) -@magic_argv_env_context -def test_get_ddp_2(driver, device): - """ - 测试 ddp 多卡的初始化情况,但传入了单个 gpu - """ - - model = TorchNormalModel_Classification_1(64, 10) - driver = initialize_torch_driver(driver, device, model) - - assert isinstance(driver, TorchDDPDriver) - - @pytest.mark.torch @pytest.mark.parametrize( "device", @@ -66,7 +44,7 @@ def test_get_ddp_2(driver, device): ) @pytest.mark.parametrize( "driver", - ["torch", "torch_ddp"] + ["torch"] ) @magic_argv_env_context def test_get_ddp(driver, device): @@ -79,21 +57,6 @@ def test_get_ddp(driver, device): assert isinstance(driver, TorchDDPDriver) - -@pytest.mark.torch -@pytest.mark.parametrize( - ("driver", "device"), - [("torch_ddp", "cpu")] -) -def test_get_ddp_cpu(driver, device): - """ - 测试试图在 cpu 上初始化分布式训练的情况 - """ - model = TorchNormalModel_Classification_1(64, 10) - with pytest.raises(ValueError): - driver = initialize_torch_driver(driver, device, model) - - @pytest.mark.torch @pytest.mark.parametrize( "device", @@ -101,7 +64,7 @@ def test_get_ddp_cpu(driver, device): ) @pytest.mark.parametrize( "driver", - ["torch", "torch_ddp"] + ["torch"] ) def test_device_out_of_range(driver, device): """ diff --git a/tests/core/drivers/torch_driver/test_single_device.py b/tests/core/drivers/torch_driver/test_single_device.py index 086f4251..1fbc9d82 100644 --- a/tests/core/drivers/torch_driver/test_single_device.py +++ b/tests/core/drivers/torch_driver/test_single_device.py @@ -595,12 +595,12 @@ def test_save_and_load_with_randombatchsampler(only_state_dict, fp16): sampler_states = dataloader.batch_sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) # 加载 # 更改 batch_size dataloader = dataloader_with_randombatchsampler(dataset, 2, True, False) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 # TODO optimizer 的 state_dict 总是为空 @@ -664,12 +664,12 @@ def test_save_and_load_with_randomsampler(only_state_dict, fp16): sampler_states = dataloader.batch_sampler.sampler.state_dict() save_states = {"num_consumed_batches": num_consumed_batches} - driver1.save(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) + driver1.save_checkpoint(Path(path), save_states, dataloader, only_state_dict, should_save_model=True) # 加载 # 更改 batch_size dataloader = dataloader_with_randomsampler(dataset, 2, True, False) - load_states = driver2.load(Path(path), dataloader, only_state_dict, should_load_model=True) + load_states = driver2.load_checkpoint(Path(path), dataloader, only_state_dict, should_load_model=True) replaced_loader = load_states.pop("dataloader") # 1. 检查 optimizer 的状态 diff --git a/tests/embeddings/torch/test_char_embedding.py b/tests/embeddings/torch/test_char_embedding.py index 81ed757a..8decce3f 100644 --- a/tests/embeddings/torch/test_char_embedding.py +++ b/tests/embeddings/torch/test_char_embedding.py @@ -7,8 +7,9 @@ from fastNLP import Vocabulary, DataSet, Instance from fastNLP.embeddings.torch.char_embedding import LSTMCharEmbedding, CNNCharEmbedding +@pytest.mark.torch class TestCharEmbed: - @pytest.mark.test + # @pytest.mark.test def test_case_1(self): ds = DataSet([Instance(words=['hello', 'world']), Instance(words=['Jack'])]) vocab = Vocabulary().from_dataset(ds, field_name='words') @@ -18,7 +19,7 @@ class TestCharEmbed: y = embed(x) assert tuple(y.size()) == (2, 3, 3) - @pytest.mark.test + # @pytest.mark.test def test_case_2(self): ds = DataSet([Instance(words=['hello', 'world']), Instance(words=['Jack'])]) vocab = Vocabulary().from_dataset(ds, field_name='words')