目录
1.1、在trainer训练时,怎么控制模型保存的数量的同时,还可以保存最优的模型参数呢?
1.2、使用trainer训练ds ZeRO3或fsdp时,怎么保存模型为huggingface格式呢?
二、PreTrained Model中的from_pretrained常见的参数
五、Trainer 中的 _inner_training_loop 函数
六、Trainer 中的 _load_best_model函数
七、Trainer 中的_save_checkpoint函数
7.2、保存优化器状态和学习率调度_save_optimizer_and_scheduler
十一、模型加载函数deepspeed_load_checkpoint与_load_from_checkpoint
11.1、deepspeed_load_checkpoint 加载deepspeed模型
11.2、_load_from_checkpoint 加载除了deepspeed情况的模型
12.1 梯度检查点(PEFTmodel,PreTrainedModel)
12.2 transformer logger、set_seed()
12.4.2 tokenizer 参数及使用方法(encode,encode_plus,batch_encode_plus,decode...)
十三、TrainingArguments 与Seq2SeqTrainingArguments 常见的参数
13.1 Seq2SeqTrainingArguments 参数
13.2 Seq2SeqTrainingArguments 参数中GenerationConfig的常见参数(用于 generate 推理函数)
本文主要简单介绍 huggingface 中常见函数的使用
torch版本:2.1
Transformers 版本:4.39-4.40.1、4.30
huggingface transformers 中文deepspeed、trainer等文档:
一、trainer保存模型的问题
1.1、在trainer训练时,怎么控制模型保存的数量的同时,还可以保存最优的模型参数呢?
transformers:4.39
在使用Hugging Face的
Trainer
进行模型训练时,通过设置TrainingArguments
中的一些参数,你可以控制保存模型的数量,并确保最优模型不会被删除。主要涉及以下几个参数:
save_strategy
:定义了模型保存的策略,可以是"no"(不保存模型),"epoch"(每个epoch结束时保存),或"steps"(每指定步数后保存)。save_total_limit
:指定同时保存的模型检查点的最大数量。如果设置了这个参数,当保存新的检查点时,超出这个数量限制的最旧的检查点将被删除。evaluation_strategy
:定义了评估的策略,与save_strategy
类似,可以是"no","epoch"或"steps"。load_best_model_at_end
:如果设置为True
,在训练结束时将加载评价指标最好的模型(需要同时设置evaluation_strategy
)。metric_for_best_model
:"eval_loss", 指定哪个评价指标用于评估最佳模型,一般用的是自定义的compute_metrics函数返回的字典的某个key。这个设置要求evaluation_strategy
不为"no"。
- 需要注意的是,"eval_loss" 系统会自己算,无需自定义compute_metrics
- include_inputs_for_metrics=True True 表示调用compute_metrics会将输入input_ids 也进行导入,方便计算
- compute_metrics:自定义用于计算评估指标的函数
- 可以参考chatglm3的代码:https://github.com/THUDM/ChatGLM3/blob/main/finetune_demo/finetune_hf.py
这里有一个简单的参考示例:
import os from transformers import TrainingArguments, Trainer # 设置保存目录 output_dir = "path/to/output/directory" best_model_dir = os.path.join(output_dir, "best_model") # 设置 TrainingArguments training_args = TrainingArguments( output_dir=output_dir, evaluation_strategy="epoch", # 每个 epoch 进行一次评估 save_strategy="epoch", # 每个 epoch 保存一次模型 save_total_limit=3, load_best_model_at_end=True, # 在训练结束时加载最优模型 metric_for_best_model="eval_loss", # 根据验证集的损失值选择最优模型,"eval_loss" 默认会计算,无需指定自定义compute_metrics,但如果是使用其他的指标,需要与compute_metrics 字典对应 greater_is_better=False, # 损失值越小越好 include_inputs_for_metrics=True # True 表示调用compute_metrics会将输入input_ids 也进行导入,方便计算 ) # 创建 Trainer 实例 trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, tokenizer=tokenizer, compute_metrics=compute_metrics, # 请确保compute_metrics函数正确定义并返回一个包含metric_for_best_model指定指标的字典。 ) metrics = train_result.metrics trainer.log_metrics("train", metrics) trainer.save_metrics("train", metrics) trainer.save_state() # 在训练完成后,最优模型将被自动保存在 best_model_dir 中 # 但是,如果需要手动保存模型,可以使用以下代码: trainer.save_model(best_model_dir) # 加载保存的最优模型 best_model = AutoModelForSequenceClassification.from_pretrained(best_model_dir)
1.2、使用trainer训练ds ZeRO3或fsdp时,怎么保存模型为huggingface格式呢?
transformers:4.39
新版trainer中存在函数
self.accelerator.get_state_dict,这个函数可以将ZeRO3切片在其他设备上的参数加载过来,然后使用self._save()保存,具体见下文_save_checkpoint、save_model、_save 函数
二、PreTrained Model中的from_pretrained常见的参数
transformers:4.39
# from_pretrained 是一个类方法,用于从预训练模型中加载模型实例
@classmethod
def from_pretrained(
cls,
# pretrained_model_name_or_path: 预训练模型的名称或路径,可以是本地路径或在线路径
pretrained_model_name_or_path: Optional[Union[str, os.PathLike]],
# model_args: 模型的初始化参数,将传递给模型的构造函数
*model_args,
# config: 模型配置对象或其路径,如果未提供,将尝试从 pretrained_model_name_or_path 加载
config: Optional[Union[PretrainedConfig, str, os.PathLike]] = None,
# cache_dir: 用于缓存下载的模型文件的目录
cache_dir: Optional[Union[str, os.PathLike]] = None,
# ignore_mismatched_sizes: 是否忽略权重大小不匹配的情况
ignore_mismatched_sizes: bool = False,
# force_download: 是否强制重新下载模型权重
force_download: bool = False,
# local_files_only: 是否只使用本地文件,不尝试从远程下载
local_files_only: bool = False,
# token: Hugging Face Hub 的访问令牌,用于下载模型权重
token: Optional[Union[str, bool]] = None,
# revision: 要使用的模型修订版本
revision: str = "main",
# use_safetensors: 是否使用 safetensors 格式加载模型权重
use_safetensors: bool = None,
**kwargs,
):
# 从 kwargs 中提取一些常用参数
state_dict = kwargs.pop("state_dict", None)
from_tf = kwargs.pop("from_tf", False)
from_flax = kwargs.pop("from_flax", False)
resume_download = kwargs.pop("resume_download", False)
proxies = kwargs.pop("proxies", None)
output_loading_info = kwargs.pop("output_loading_info", False)
use_auth_token = kwargs.pop("use_auth_token", None)
trust_remote_code = kwargs.pop("trust_remote_code", None) # 使用远端加载模型文件
_ = kwargs.pop("mirror", None)
from_pipeline = kwargs.pop("_from_pipeline", None)
from_auto_class = kwargs.pop("_from_auto", False)
_fast_init = kwargs.pop("_fast_init", True)
torch_dtype = kwargs.pop("torch_dtype", None) # 模型加载的数据类型,type.bfloat16 等
low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", None) # 低占用
device_map = kwargs.pop("device_map", None) # auto 的话 accelerate 会自动分配设备
max_memory = kwargs.pop("max_memory", None)
offload_folder = kwargs.pop("offload_folder", None)
offload_state_dict = kwargs.pop("offload_state_dict", False)
offload_buffers = kwargs.pop("offload_buffers", False)
load_in_8bit = kwargs.pop("load_in_8bit", False)
load_in_4bit = kwargs.pop("load_in_4bit", False) # 4bit加载(未来删除),现在使用 quantization_config 参数
quantization_config = kwargs.pop("quantization_config", None) # 量化的参数
subfolder = kwargs.pop("subfolder", "")
commit_hash = kwargs.pop("_commit_hash", None)
variant = kwargs.pop("variant", None)
adapter_kwargs = kwargs.pop("adapter_kwargs", {})
adapter_name = kwargs.pop("adapter_name", "default") # peft adapter_name, 默认即可
use_flash_attention_2 = kwargs.pop("use_flash_attention_2", False) # 未来弃用,使用参数 attn_implementation 来控制,使用方法见下文
# 如果启用了 FSDP,则强制启用 low_cpu_mem_usage
if is_fsdp_enabled():
low_cpu_mem_usage = True
# 对于 use_auth_token 参数的处理,已弃用,建议使用 token 参数代替
if use_auth_token is not None:
warnings.warn(
"The `use_auth_token` argument is deprecated and will be removed in v5 of Transformers. Please use `token` instead.",
FutureWarning,
)
if token is not None:
raise ValueError(
"`token` and `use_auth_token` are both specified. Please set only the argument `token`."
)
token = use_auth_token
# 如果提供了 token 和 adapter_kwargs,则将 token 添加到 adapter_kwargs 中
if token is not None and adapter_kwargs is not None and "token" not in adapter_kwargs:
adapter_kwargs["token"] = token
# 处理 use_safetensors 参数
if use_safetensors is None and not is_safetensors_available():
use_safetensors = False
if trust_remote_code is True:
logger.warning(
"The argument `trust_remote_code` is to be used with Auto classes. It has no effect here and is"
" ignored."
)
# 尝试从预训练模型路径获取 commit_hash
if commit_hash is None:
if not isinstance(config, PretrainedConfig):
resolved_config_file = cached_file(
pretrained_model_name_or_path,
CONFIG_NAME,
cache_dir=cache_dir,
force_download=force_download,
resume_download=resume_download,
proxies=proxies,
local_files_only=local_files_only,
token=token,
revision=revision,
subfolder=subfolder,
_raise_exceptions_for_gated_repo=False,
_raise_exceptions_for_missing_entries=False,
_raise_exceptions_for_connection_errors=False,
)
commit_hash = extract_commit_hash(resolved_config_file, commit_hash)
else:
commit_hash = getattr(config, "_commit_hash", None)
# 如果使用了 PEFT 并且可用,则尝试加载适配器配置文件
if is_peft_available():
_adapter_model_path = adapter_kwargs.pop("_adapter_model_path", None)
if _adapter_model_path is None:
_adapter_model_path = find_adapter_config_file(
pretrained_model_name_or_path,
cache_dir=cache_dir,
force_download=force_download,
resume_download=resume_download,
proxies=proxies,
local_files_only=local_files_only,
_commit_hash=commit_hash,
**adapter_kwargs,
)
if _adapter_model_path is not None and os.path.isfile(_adapter_model_path):
with open(_adapter_model_path, "r", encoding="utf-8") as f:
_adapter_model_path = pretrained_model_name_or_path
pretrained_model_name_or_path = json.load(f)["base_model_name_or_path"]
else:
_adapter_model_path = None
# 处理 device_map 参数,将其转换为适当的格式
if isinstance(device_map, torch.device):
device_map = {"": device_map}
elif isinstance(device_map, str) and device_map not in ["auto", "balanced", "balanced_low_0", "sequential"]:
try:
device_map = {"": torch.device(device_map)}
except RuntimeError:
raise ValueError(
"When passing device_map as a string, the value needs to be a device name (e.g. cpu, cuda:0) or "
f"'auto', 'balanced', 'balanced_low_0', 'sequential' but found {device_map}."
)
elif isinstance(device_map, int):
if device_map < 0:
raise ValueError(
"You can't pass device_map as a negative int. If you want to put the model on the cpu, pass device_map = 'cpu' "
)
else:
device_map = {"": device_map}
# 如果提供了 device_map,则强制启用 low_cpu_mem_usage
if device_map is not None:
if low_cpu_mem_usage is None:
low_cpu_mem_usage = True
elif not low_cpu_mem_usage:
raise ValueError("Passing along a `device_map` requires `low_cpu_mem_usage=True`")
# 如果启用了 low_cpu_mem_usage,但未安装 Accelerate,则引发异常
if low_cpu_mem_usage:
if is_deepspeed_zero3_enabled():
raise ValueError(
"DeepSpeed Zero-3 is not compatible with `low_cpu_mem_usage=True` or with passing a `device_map`."
)
elif not is_accelerate_available():
raise ImportError(
"Using `low_cpu_mem_usage=True` or a `device_map` requires Accelerate: `pip install accelerate`"
)
# 处理 quantization_config 参数
if load_in_4bit or load_in_8bit:
if quantization_config is not None:
raise ValueError(
"You can't pass `load_in_4bit`or `load_in_8bit` as a kwarg when passing "
"`quantization_config` argument at the same time."
)
config_dict = {k: v for k, v in kwargs.items() if k in inspect.signature(BitsAndBytesConfig).parameters}
config_dict = {**config_dict, "load_in_4bit": load_in_4bit, "load_in_8bit": load_in_8bit}
quantization_config, kwargs = BitsAndBytesConfig.from_dict(
config_dict=config_dict, return_unused_kwargs=True, **kwargs
)
logger.warning(
"The `load_in_4bit` and `load_in_8bit` arguments are deprecated and will be removed in the future versions. "
"Please, pass a `BitsAndBytesConfig` object in `quantization_config` argument instead."
)
from_pt = not (from_tf | from_flax)
# 设置用户代理字符串,用于下载模型权重
user_agent = {"file_type": "model", "framework": "pytorch", "from_auto_class": from_auto_class}
if from_pipeline is not None:
user_agent["using_pipeline"] = from_pipeline
# 如果处于离线模式,则强制启用 local_files_only
if is_offline_mode() and not local_files_only:
logger.info("Offline mode: forcing local_files_only=True")
local_files_only = True
# 加载模型配置
if not isinstance(config, PretrainedConfig):
config_path = config if config is not None else pretrained_model_name_or_path
config, model_kwargs = cls.config_class.from_pretrained(
config_path,
cache_dir=cache_dir,
return_unused_kwargs=True,
force_download=force_download,
resume_download=resume_download,
proxies=proxies,
local_files_only=local_files_only,
token=token,
revision=revision,
subfolder=subfolder,
_from_auto=from_auto_class,
_from_pipeline=from_pipeline,
**kwargs,
)
else:
# 如果直接提供了配置对象,则复制一份,以免修改原始配置
config = copy.deepcopy(config)
# 处理 attn_implementation 参数
kwarg_attn_imp = kwargs.pop("attn_implementation", None)
if kwarg_attn_imp is not None and config._attn_implementation != kwarg_attn_imp:
config._attn_implementation = kwarg_attn_imp
model_kwargs = kwargs
# 处理量化相关配置
pre_quantized = getattr(config, "quantization_config", None) is not None
if pre_quantized or quantization_config is not None:
if pre_quantized:
config.quantization_config = AutoHfQuantizer.merge_quantization_configs(
config.quantization_config, quantization_config
)
else:
config.quantization_config = quantization_config
hf_quantizer = AutoHfQuantizer.from_config(config.quantization_config, pre_quantized=pre_quantized)
else:
hf_quantizer = None
# 如果启用了量化,则需要验证环境和调整一些参数
if hf_quantizer is not None:
hf_quantizer.validate_environment(
torch_dtype=torch_dtype, from_tf=from_tf, from_flax=from_flax, device_map=device_map
)
torch_dtype = hf_quantizer.update_torch_dtype(torch_dtype)
device_map = hf_quantizer.update_device_map(device_map)
if low_cpu_mem_usage is None:
low_cpu_mem_usage = True
logger.warning("`low_cpu_mem_usage` was None, now set to True since model is quantized.")
is_quantized = hf_quantizer is not None
# 检查是否为分片加载检查点
is_sharded = False
sharded_metadata = None
# 加载模型权重
loading_info = None
keep_in_fp32_modules = None
use_keep_in_fp32_modules = False
(......)
一些简单的加载案例
- attn_implementation 为新的参数, 使用类型有“spda”,“eager”(原生的attention),“flash attention”,需要注意的,attn_implementation = "eager",需要config 额外指定config._attn_implementation_internal = "eager" 才会生效,具体用法见下面例子:
- 为什么需要额外指定config._attn_implementation_internal = "eager",可以看源码PreTrainedModel中 ._autoset_attn_implementation 函数的实现过程:
"""
例子1 :使用 flash_attention_2 或 spda 加载模型,如果都不支持则使用eager
"""
config = transformers.AutoConfig.from_pretrained(model_args.model_name_or_path)
config.use_cache=False # 对预训练的config进行修改,这里取消保存 k v 缓存
# 使用AutoModelForCausalLM.from_pretrained方法加载语言模型,传入模型路径、量化配置、是否信任远程代码、注意力实现方式和数据类型等参数
model = AutoModelForCausalLM.from_pretrained(
args.model_name_or_path, # 模型路径
quantization_config=bnb_config, # 量化配置
trust_remote_code=True, # 是否信任远程代码(如从Hub加载模型)
# 注意力实现方式,flash_attention_2或spda
attn_implementation="flash_attention_2" if args.use_flash_attn else "spda"
torch_dtype=torch_dtype, # 模型权重的数据类型
config = config
)
"""
例子2: 使用 eager 原生注意力,但相对于上面的例子,
需要额外指定: config._attn_implementation_internal = "eager"
"""
config = transformers.AutoConfig.from_pretrained(model_args.model_name_or_path)
config.use_cache=False
# 需要config 中额外指定:
config._attn_implementation_internal = "eager"
model = AutoModelForCausalLM.from_pretrained(
args.model_name_or_path,
quantization_config=bnb_config,
trust_remote_code=True,
# 指定模式为 ”eager“
attn_implementation="eager"
torch_dtype=torch_dtype,
config = config
)
model tokenizer 更多初始化设置详情见(章节2.2):
三、Trainer初始化
transformers:4.39
这部分代码主要涉及以下几个方面:
- 处理 SageMaker 模型并行时的混合精度设置
- 根据设备自动选择合适的半精度后端(FP16 或 BF16)
- 设置标签平滑
- 初始化训练器状态和控制器
- 统计浮点运算数的内部变量
- 设置标签名和判断模型是否可返回损失
- 执行初始化结束回调
- 内部变量用于自动批大小调整
- 停止并更新内存跟踪器的指标
- 检查是否支持 torch.compile
- 设置 FSDP XLA V2 相关配置
class Trainer:
"""
Trainer是一个简单但功能完备的PyTorch训练和评估循环,专为Transformers库中的模型进行了优化和定制。
它封装了训练过程中的各个环节,如数据加载、模型实例化、优化器和学习率调度器配置、训练和评估循环等,
并提供了灵活的回调机制来定制训练行为。
Args:
model ([`PreTrainedModel`] 或 `torch.nn.Module`, *可选*):
要训练、评估或用于预测的模型实例。如果未提供,则必须传递`model_init`函数。
[`PreTrainedModel`]是Transformers库中的基类模型,已经实现了一些基础功能,如权重初始化、前向传播等。
你也可以传入自定义的`torch.nn.Module`,只要它的使用方式与Transformers模型相同即可。
args ([`TrainingArguments`], *可选*):
用于微调训练的参数集合,包括了诸如学习率、批次大小、训练轮数等超参数配置。
如果未提供,将使用默认的[`TrainingArguments`]实例,其`output_dir`设置为当前目录下名为*tmp_trainer*的目录。
data_collator (`DataCollator`, *可选*):
用于从`train_dataset`或`eval_dataset`的样本列表中构建批次的函数对象。
它定义了如何从一个列表的单个样本中构建一个批次的张量。
如果未提供`tokenizer`,将默认使用[`default_data_collator`];否则为[`DataCollatorWithPadding`]的实例,
它会自动对批次数据执行填充,使每个张量维度保持一致。
train_dataset (`torch.utils.data.Dataset` 或 `torch.utils.data.IterableDataset`, *可选*):
用于训练的数据集对象。如果它是一个[`~datasets.Dataset`]实例,那些不被`model.forward()`方法接受的列将自动被移除。
`Dataset`表示可索引的数据集,而`IterableDataset`表示可迭代的数据集,一般用于数据量过大无法直接加载到内存中的场景。
注意,如果`train_dataset`是一个带有随机性的`IterableDataset`(如随机采样、随机扩增等),
并且你正在分布式方式进行训练,则该数据集应该具有以下两种方式之一:
1) 包含一个内部属性`generator`,它是一个用于随机化的`torch.Generator`,该生成器必须在所有进程中保持一致。
Trainer将在每个epoch手动设置该`generator`的种子。
2) 具有一个内部的`set_epoch()`方法,用于在每个epoch内部设置RNG种子。
eval_dataset (Union[`torch.utils.data.Dataset`, Dict[str, `torch.utils.data.Dataset`]], *可选*):
用于评估的数据集对象。如果它是一个[`~datasets.Dataset`]实例,那些不被`model.forward()`方法接受的列将自动被移除。
如果它是一个字典,那么它将对每个数据集进行评估,并在指标名称前加上字典键作为前缀。
tokenizer ([`PreTrainedTokenizerBase`], *可选*):
用于对文本数据进行预处理(分词、转id等)的tokenizer对象。
如果提供,它将用于在批处理输入时自动将输入填充到最大长度,并将与模型一起保存,
以便更轻松地重新运行中断的训练或重新使用微调后的模型。
model_init (`Callable[[], PreTrainedModel]`, *可选*):
一个可调用对象(函数或类),用于实例化将被使用的模型。
如果提供,每次调用[`~Trainer.train`]时都将从该函数返回的新模型实例开始训练。
该可调用对象可以没有参数,也可以只接受一个参数,包含来自超参数优化框架(如optuna、Ray Tune、SigOpt等)的试验对象,
以便根据超参数配置(如层数、内层大小、dropout概率等)选择不同的模型架构。
compute_metrics (`Callable[[EvalPrediction], Dict]`, *可选*):
用于在评估时计算指标的函数对象。它必须接受一个[`EvalPrediction`]对象作为输入,并返回一个将指标名映射到对应值的字典。
该函数对评估期间模型的预测输出和真实标签进行了自定义的指标计算。
callbacks (List of [`TrainerCallback`], *可选*):
一系列回调对象的列表,用于定制和扩展训练循环的行为。
这些回调会被添加到Trainer默认的回调列表中,可用于插入自定义的训练行为。
如果你想移除正在使用的默认回调之一,可以使用[`Trainer.remove_callback`]方法。
optimizers (`Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`, *可选*, 默认为`(None, None)`):
一个包含优化器和学习率调度器对象的元组。
如果未提供,将默认为针对模型创建一个[`AdamW`]优化器实例,
以及由`args`控制的基于学习率预热的线性学习率调度器[`get_linear_schedule_with_warmup`]。
preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`, *可选*):
一个在每个评估步骤缓存logits(模型输出)之前对它们进行预处理的函数对象。
它必须接受两个张量作为输入:logits和标签,并返回经过所需处理的logits张量。
该函数对logits的修改将反映在传递给`compute_metrics`的预测结果中。
请注意,如果数据集没有标签,第二个参数(标签张量)将为`None`。
Trainer的重要属性:
- **model** -- 始终指向核心模型实例。如果使用Transformers模型,它将是[`PreTrainedModel`]的子类实例。
- **model_wrapped** -- 始终指向最外层包装后的模型实例。
例如在使用DeepSpeed时,内部的`model`会被DeepSpeed和`torch.nn.DistributedDataParallel`包装。
如果`model`没有被包装,那么`self.model_wrapped`就等于`self.model`。
- **is_model_parallel** -- 指示模型是否已切换到模型并行模式。
模型并行是指将模型的不同层分布在不同的GPU上,以支持超大型模型的训练。
- **place_model_on_device** -- 指示是否自动将模型放置到目标设备(如GPU)上。
在使用模型并行或DeepSpeed时,该值将被设置为`False`,因为它们有自己的设备管理机制。
- **is_in_train** -- 指示模型当前是否处于训练模式。当在训练过程中调用`evaluate`时,该值会被设为`True`。
"""
# 这些方法在示例中被用作Trainer的一部分
from .trainer_pt_utils import _get_learning_rate, log_metrics, metrics_format, save_metrics, save_state
# __init__方法是Trainer类的构造函数,用于初始化训练器实例
def __init__(
self,
model: Union[PreTrainedModel, nn.Module] = None, # 待训练的模型,可以是Huggingface的预训练模型或普通的PyTorch模型
args: TrainingArguments = None, # 训练参数,包括批大小、优化器设置、评估指标、混合精度配置等
data_collator: Optional[DataCollator] = None, # 可选的数据collator,用于对批数据进行处理,如padding、批构造等
train_dataset: Optional[Dataset] = None, # 可选的训练数据集
eval_dataset: Optional[Union[Dataset, Dict[str, Dataset]]] = None, # 可选的评估数据集,可以是单个数据集或字典形式的多个数据集
tokenizer: Optional[PreTrainedTokenizerBase] = None, # 可选的tokenizer,用于文本数据的预处理,如分词、序列化等
model_init: Optional[Callable[[], PreTrainedModel]] = None, # 可选的模型初始化函数,用于惰性地初始化模型
compute_metrics: Optional[Callable[[EvalPrediction], Dict]] = None, # 可选的指标计算函数,用于计算评估指标
callbacks: Optional[List[TrainerCallback]] = None, # 可选的回调函数列表,用于在训练过程中执行特定操作,如记录日志、模型保存等
optimizers: Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), # 可选的优化器和学习率调度器实例,默认为None
preprocess_logits_for_metrics: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = None, # 可选的预处理logits函数,用于在计算评估指标前对logits进行处理
):
# 如果没有指定TrainingArguments,则使用默认的输出目录
if args is None:
output_dir = "tmp_trainer" # 设置默认输出目录为"tmp_trainer"
logger.info(f"No `TrainingArguments` passed, using `output_dir={output_dir}`.") # 打印信息
args = TrainingArguments(output_dir=output_dir) # 创建默认的TrainingArguments实例
self.args = args # 保存训练参数
# 设置随机种子,以确保实验可复现
# enable_full_determinism是一个辅助函数,用于设置CUDA、Pytorch等的种子,以获得完全确定性的运行结果
# set_seed是另一个辅助函数,用于设置Python、NumPy和PyTorch的种子,以获得部分确定性的运行结果
enable_full_determinism(self.args.seed) if self.args.full_determinism else set_seed(self.args.seed)
self.hp_name = None # 超参搜索的名称,默认为None
self.deepspeed = None # DeepSpeed实例,默认为None,DeepSpeed是一种模型并行化技术
self.is_in_train = False # 是否处于训练状态,默认为False
# 创建加速器(Accelerator)并进行后处理
# Accelerator是Huggingface提供的一个工具,用于自动管理分布式训练、混合精度、梯度累积等
self.create_accelerator_and_postprocess()
# 初始化内存跟踪器,用于跟踪内存使用情况
# TrainerMemoryTracker是一个内存跟踪工具,可以监控和记录模型、数据集、优化器等的内存占用
self._memory_tracker = TrainerMemoryTracker(self.args.skip_memory_metrics)
self._memory_tracker.start() # 开始跟踪内存
# 设置日志级别
# get_process_log_level是一个获取当前进程日志级别的函数,日志级别由TrainingArguments指定
log_level = args.get_process_log_level()
logging.set_verbosity(log_level) # 设置日志输出的详细程度
# 强制设置设备和分布式设置
# _setup_devices是TrainingArguments的一个内部方法,用于设置训练的硬件设备(CPU/GPU)和分布式配置
args._setup_devices
# 如果未提供模型实例,则尝试使用model_init函数初始化模型
if model is None:
if model_init is not None:
self.model_init = model_init
model = self.call_model_init() # 调用模型初始化函数
else:
raise RuntimeError("`Trainer` requires either a `model` or `model_init` argument")
else:
if model_init is not None:
warnings.warn(
"`Trainer` requires either a `model` or `model_init` argument, but not both. `model_init` will"
" overwrite your model when calling the `train` method. This will become a fatal error in the next"
" release.",
FutureWarning,
)
self.model_init = model_init
# 检查模型类型,确保它可以用于训练(需要有头部用于预测输出)
if model.__class__.__name__ in MODEL_MAPPING_NAMES:
raise ValueError(
f"The model you have picked ({model.__class__.__name__}) cannot be used as is for training: it only "
"computes hidden states and does not accept any labels. You should choose a model with a head "
"suitable for your task like any of the `AutoModelForXxx` listed at "
"https://huggingface.co/docs/transformers/model_doc/auto"
)
# 检测模型是否已启用并行模式
if hasattr(model, "is_parallelizable") and model.is_parallelizable and model.model_parallel:
self.is_model_parallel = True
else:
self.is_model_parallel = False
# 进一步检测模型并行,考虑了模型的设备映射情况
if getattr(model, "hf_device_map", None) is not None:
devices = [device for device in set(model.hf_device_map.values()) if device not in ["cpu", "disk"]]
if len(devices) > 1:
self.is_model_parallel = True
elif len(devices) == 1:
self.is_model_parallel = self.args.device != torch.device(devices[0])
else:
self.is_model_parallel = False
# 如果启用了模型并行,输出警告信息
if self.is_model_parallel:
logger.info(
"You have loaded a model on multiple GPUs. `is_model_parallel` attribute will be force-set"
" to `True` to avoid any unexpected behavior such as device placement mismatching."
)
# 检查模型是否为量化模型,并且不支持训练
_is_quantized_and_base_model = getattr(model, "is_quantized", False) and not getattr(
model, "_hf_peft_config_loaded", False
)
_quantization_method_supports_training = (
getattr(model, "hf_quantizer", None) is not None and model.hf_quantizer.is_trainable
)
# 如果模型为纯量化模型,且不支持训练或微调,则抛出异常
if _is_quantized_and_base_model and hasattr(model, "_orig_mod"):
raise ValueError(
"You cannot fine-tune quantized model with `torch.compile()` make sure to pass a non-compiled model when fine-tuning a quantized model with PEFT"
)
if _is_quantized_and_base_model and not _is_peft_model(model):
raise ValueError(
"You cannot perform fine-tuning on purely quantized models. Please attach trainable adapters on top of"
" the quantized model to correctly perform fine-tuning. Please see: https://huggingface.co/docs/transformers/peft"
" for more details"
)
elif _is_quantized_and_base_model and not _quantization_method_supports_training:
raise ValueError(
f"The model you are trying to fine-tune is quantized with {model.hf_quantizer.quantization_config.quant_method}"
" but that quantization method do not support training. Please open an issue on GitHub: https://github.com/huggingface/transformers"
f" to request the support for training support for {model.hf_quantizer.quantization_config.quant_method}"
)
# 检测是否启用了 FSDP (Fully Sharded Data Parallelism)
self.is_fsdp_xla_enabled = args.fsdp_config["xla"]
if len(args.fsdp) > 0:
if self.is_deepspeed_enabled:
raise ValueError(
"Using --fsdp xxx together with --deepspeed is not possible, deactivate one of those flags."
)
if not args.fsdp_config["xla"] and args.parallel_mode != ParallelMode.DISTRIBUTED:
raise ValueError("Using fsdp only works in distributed training.")
# 确定是否需要将模型放置到设备上
# 在以下几种情况下,将暂时不移动模型到设备:
# 1. 模型并行 - 因为我们试图将比单GPU大得多的模型装载到GPU上
# 2. 启用了fp16的DeepSpeed - 它会将模型加载到一半的大小,不需要调用.to()
# 3. 全精度(bf16或fp16)评估 - 因为模型需要先被cast为正确的数据类型
# 4. FSDP - 与模型并行情况类似
self.place_model_on_device = args.place_model_on_device
if (
self.is_model_parallel
or self.is_deepspeed_enabled
or ((args.fp16_full_eval or args.bf16_full_eval) and not args.do_train)
or self.is_fsdp_xla_enabled
or self.is_fsdp_enabled
):
self.place_model_on_device = False
# 实例化DataCollator,用于构建批次数据
default_collator = default_data_collator if tokenizer is None else DataCollatorWithPadding(tokenizer)
self.data_collator = data_collator if data_collator is not None else default_collator
# 保存训练和评估数据集
self.train_dataset = train_dataset
self.eval_dataset = eval_dataset
self.tokenizer = tokenizer
# 如果启用了place_model_on_device,则将模型移动到目标设备
# 但Bnb量化模型不支持.to()操作,需要排除
if (
self.place_model_on_device
and not getattr(model, "quantization_method", None) == QuantizationMethod.BITS_AND_BYTES
):
self._move_model_to_device(model, args.device)
# 如果启用了模型并行,将n_gpu强制设为1,避免使用DataParallel
if self.is_model_parallel:
self.args._n_gpu = 1
# 保存原始模型和外部包装模型的引用
self.model_wrapped = model
self.model = model
# 用于NeFtune噪声训练的alpha参数
self.neftune_noise_alpha = args.neftune_noise_alpha
# 保存计算指标函数和logits预处理函数
self.compute_metrics = compute_metrics
self.preprocess_logits_for_metrics = preprocess_logits_for_metrics
# 保存优化器和学习率调度器
self.optimizer, self.lr_scheduler = optimizers
if model_init is not None and (self.optimizer is not None or self.lr_scheduler is not None):
raise RuntimeError(
"Passing a `model_init` is incompatible with providing the `optimizers` argument. "
"You should subclass `Trainer` and override the `create_optimizer_and_scheduler` method."
)
# 如果使用TPU训练,检查模型与优化器参数是否在同一设备上
if is_torch_tpu_available() and self.optimizer is not None:
for param in self.model.parameters():
model_device = param.device
break
for param_group in self.optimizer.param_groups:
if len(param_group["params"]) > 0:
optimizer_device = param_group["params"][0].device
break
if model_device != optimizer_device:
raise ValueError(
"The model and the optimizer parameters are not on the same device, which probably means you"
" created an optimizer around your model **before** putting on the device and passing it to the"
" `Trainer`. Make sure the lines `import torch_xla.core.xla_model as xm` and"
" `model.to(xm.xla_device())` is performed before the optimizer creation in your script."
)
# 如果使用DeepSpeed或PyTorch FSDP,检查是否传入了优化器和调度器
if (self.is_deepspeed_enabled or self.is_fsdp_xla_enabled or self.is_fsdp_enabled) and (
self.optimizer is not None or self.lr_scheduler is not None
):
raise RuntimeError(
"Passing `optimizers` is not allowed if Deepspeed or PyTorch FSDP is enabled. "
"You should subclass `Trainer` and override the `create_optimizer_and_scheduler` method."
)
# 设置默认回调和指定的自定义回调
default_callbacks = DEFAULT_CALLBACKS + get_reporting_integration_callbacks(self.args.report_to)
callbacks = default_callbacks if callbacks is None else default_callbacks + callbacks
self.callback_handler = CallbackHandler(
callbacks, self.model, self.tokenizer, self.optimizer, self.lr_scheduler
)
# 添加进度条回调
self.add_callback(PrinterCallback if self.args.disable_tqdm else DEFAULT_PROGRESS_CALLBACK)
# 初始化日志记录器,在第一次调用self.log()时完成设置
self._loggers_initialized = False
# 如果需要,创建远程repo和输出目录
self.hub_model_id = None
if self.args.push_to_hub:
self.init_hf_repo()
if self.args.should_save:
os.makedirs(self.args.output_dir, exist_ok=True)
# 检查data_collator是否可调用
if not callable(self.data_collator) and callable(getattr(self.data_collator, "collate_batch", None)):
raise ValueError("The `data_collator` should be a simple callable (function, class with `__call__`).")
# 如果设置了max_steps,它将覆盖num_train_epochs
if args.max_steps > 0:
logger.info("max_steps is given, it will override any value given in num_train_epochs")
# 如果训练数据集没有__len__方法且未设置max_steps,则抛出异常
# 因为需要预先知道训练步数,以用于学习率调度
if train_dataset is not None and not has_length(train_dataset) and args.max_steps <= 0:
raise ValueError(
"The train_dataset does not implement __len__, max_steps has to be specified. "
"The number of steps needs to be known in advance for the learning rate scheduler."
)
# 检查是否将IterableDataset与group_by_length标志一起使用,两者不兼容
if (
train_dataset is not None
and isinstance(train_dataset, torch.utils.data.IterableDataset)
and args.group_by_length
):
raise ValueError("the `--group_by_length` option is only available for `Dataset`, not `IterableDataset")
self._signature_columns = None
# 混合精度设置
self.use_apex = False
self.use_cpu_amp = False
# 针对SageMaker模型并行的混合精度设置
if is_sagemaker_mp_enabled():
# SageMaker模型并行目前不支持BF16,如果启用则抛出异常
if args.bf16:
raise ValueError("SageMaker Model Parallelism does not support BF16 yet. Please use FP16 instead ")
if IS_SAGEMAKER_MP_POST_1_10:
# 当SMP配置与训练参数不匹配时,以SMP配置为准
if args.fp16 != smp.state.cfg.fp16:
logger.warning(
f"FP16 provided in SM_HP_MP_PARAMETERS is {smp.state.cfg.fp16}, "
f"but FP16 provided in trainer argument is {args.fp16}, "
f"setting to {smp.state.cfg.fp16}"
)
args.fp16 = smp.state.cfg.fp16
else:
# smp < 1.10不支持在训练器中启用fp16
if hasattr(smp.state.cfg, "fp16"):
logger.warning(
f"FP16 provided in SM_HP_MP_PARAMETERS is {smp.state.cfg.fp16}, "
"but SageMaker Model Parallelism < 1.10 does not support FP16 in trainer."
)
# 根据设备和后端配置混合精度训练
if (args.fp16 or args.bf16) and args.half_precision_backend == "auto":
if args.device == torch.device("cpu"):
if args.fp16:
raise ValueError("Tried to use `fp16` but it is not supported on cpu")
else:
args.half_precision_backend = "cpu_amp"
logger.info(f"Using {args.half_precision_backend} half precision backend")
if (args.fp16 or args.bf16) and not (self.is_deepspeed_enabled or is_sagemaker_mp_enabled()):
# deepspeed和SageMaker模型并行会自行管理半精度训练
if args.half_precision_backend == "cpu_amp":
self.use_cpu_amp = True
self.amp_dtype = torch.bfloat16 # 使用bfloat16进行混合精度训练
elif args.half_precision_backend == "apex":
if not is_apex_available():
raise ImportError(
"Using FP16 with APEX but APEX is not installed, please refer to"
" https://www.github.com/nvidia/apex."
)
self.use_apex = True
# 标签平滑
if self.args.label_smoothing_factor != 0:
self.label_smoother = LabelSmoother(epsilon=self.args.label_smoothing_factor)
else:
self.label_smoother = None
# 设置进程状态
self.state = TrainerState(
is_local_process_zero=self.is_local_process_zero(),
is_world_process_zero=self.is_world_process_zero(),
)
self.control = TrainerControl()
# 内部变量,用于跟踪每个进程的浮点运算量
self.current_flos = 0
self.hp_search_backend = None
# 获取标签名称,如未指定则使用默认值
default_label_names = find_labels(self.model.__class__)
self.label_names = default_label_names if self.args.label_names is None else self.args.label_names
# 检查模型是否可以返回损失
self.can_return_loss = can_return_loss(self.model.__class__)
# 允许回调函数修改训练控制流程
self.control = self.callback_handler.on_init_end(self.args, self.state, self.control)
# 内部变量,用于自动调整批次大小
self._train_batch_size = args.train_batch_size
self._created_lr_scheduler = False
# 停止并更新内存跟踪器的指标
self._memory_tracker.stop_and_update_metrics()
# torch.compile配置
if args.torch_compile and not is_torch_compile_available():
raise RuntimeError("Using torch.compile requires PyTorch 2.0 or higher.")
# 检测是否启用了FSDP XLA v2
self.is_fsdp_xla_v2_enabled = args.fsdp_config["xla_fsdp_v2"]
if self.is_fsdp_xla_v2_enabled:
# 为FSDP v2数据加载器和包装器准备SPMD网格
num_devices = xr.global_runtime_device_count()
xs.set_global_mesh(xs.Mesh(np.array(range(num_devices)), (num_devices, 1), axis_names=("fsdp", "tensor")))
四、Trainer 中的train()
transformers:4.39
好的,我将罗列代码的执行步骤,并对重要部分进行解析。
- 检查
resume_from_checkpoint
参数,如果为 False,则将其设置为 None。- 启动内存跟踪器,用于跟踪训练过程中的内存使用情况。
- 获取训练参数
args
。- 设置
is_in_train
标志为 True,表示当前处于训练模式。- 如果
neftune_noise_alpha
参数不为 None,则调用_activate_neftune
方法,为模型添加 NeFTune 噪声。NeFTune 是一种用于训练鲁棒神经网络的技术,可以提高模型对噪声的鲁棒性。- 如果指定了全精度(fp16或bf16)评估,且不进行训练,则将模型移动到指定的设备(如GPU)上。这种情况通常用于评估或推理。
- 检查
kwargs
中是否包含"model_path"
键,如果包含,则将其值赋给resume_from_checkpoint
,并发出警告,提示"model_path"
参数已被弃用。- 如果
kwargs
不为空,则抛出TypeError
异常,提示train()
函数收到了意外的关键字参数。- 重要步骤: 调用
_hp_search_setup
方法,传入trial
参数,用于超参数搜索的设置。超参数搜索是机器学习中一种常用的技术,用于自动搜索模型的最佳超参数组合。- 设置训练批次大小为
args.train_batch_size
。- 检查是否需要重新初始化模型。如果
self.model_init
不为 None,则执行以下操作:
a. 根据args.full_determinism
的值,启用完全确定性模式或设置随机种子。
b. 调用call_model_init
方法,传入trial
参数,获取新的模型实例。
c. 将优化器和学习率调度器重置为 None。- 检查是否需要从检查点恢复训练。如果
resume_from_checkpoint
是布尔值且为 True,则从args.output_dir
中获取最新的检查点路径。如果没有找到有效的检查点,则抛出ValueError
异常。- 如果
resume_from_checkpoint
不为 None,则执行以下操作:
a. 如果没有启用 SageMaker 模型并行、DeepSpeed 或 PyTorch FSDP,则调用_load_from_checkpoint
方法,从检查点加载模型、优化器和学习率调度器的状态。也就是说train函数默认的断点续传不支持
SageMaker 模型或 DeepSpeed模型。但是train函数中_inner_training_loop()函数提供了deepspeed断点续传的加载方法(详情见下文)。
b. 从检查点中加载TrainerState
对象,如果其中包含train_batch_size
信息,则将训练批次大小设置为该值。- 如果模型已重新加载,则执行以下操作:
a. 如果需要将模型放置到设备上,则调用_move_model_to_device
方法,将模型移动到指定的设备上。
b. 将model_wrapped
设置为新的模型实例。- 重要步骤: 调用
find_executable_batch_size
函数,获取可执行的内部训练循环函数。该函数根据训练批次大小和是否自动查找批次大小的设置,返回合适的内部训练循环函数。- 检查是否需要将模型推送到 Hugging Face Hub。
a. 如果需要推送,则在try
块中禁用进度条,调用内部训练循环函数,并在finally
块中重新启用进度条。
b. 如果不需要推送,则直接调用内部训练循环函数。解析重要步骤:
_hp_search_setup
方法用于超参数搜索的设置。在进行超参数搜索时,需要根据给定的trial
对象或超参数字典,对模型或训练过程进行相应的配置和初始化。这个步骤是机器学习中自动化超参数调优的关键。find_executable_batch_size
函数用于查找可执行的训练批次大小。在训练过程中,批次大小的选择很重要,它需要综合考虑GPU内存大小、模型复杂度等因素。该函数会根据指定的训练批次大小和是否自动查找批次大小的设置,返回一个合适的内部训练循环函数,以确保训练过程可以顺利执行。这两个重要步骤体现了 Hugging Face Transformers 库对于超参数调优和资源利用的优化,有助于提高模型训练的效率和性能。
def train(
self,
resume_from_checkpoint: Optional[Union[str, bool]] = None, # 1. resume_from_checkpoint参数是一个可选的Union类型,可以是字符串或布尔值
# 如果是字符串,表示要从指定的检查点路径恢复训练
# 如果是True,则从args.output_dir中加载最新的检查点恢复训练
# 如果为None或False,则从头开始训练
trial: Union["optuna.Trial", Dict[str, Any]] = None, # 2. trial参数是一个可选的Union类型,可以是Optuna库中的Trial对象或字典
# 它用于在进行超参数搜索时传递试验信息或超参数字典
ignore_keys_for_eval: Optional[List[str]] = None, # 3. ignore_keys_for_eval参数是一个可选的列表,包含在评估时要忽略的模型输出键
# 如果模型输出是一个字典,可以指定在评估时要忽略的键
**kwargs, # 4. **kwargs用于接收其他任何关键字参数,以隐藏已弃用的参数
):
"""
Main training entry point. # 5. 这是训练的主要入口点
Args:
resume_from_checkpoint (`str` or `bool`, *optional*):
If a `str`, local path to a saved checkpoint as saved by a previous instance of [`Trainer`]. If a
`bool` and equals `True`, load the last checkpoint in *args.output_dir* as saved by a previous instance
of [`Trainer`]. If present, training will resume from the model/optimizer/scheduler states loaded here.
trial (`optuna.Trial` or `Dict[str, Any]`, *optional*):
The trial run or the hyperparameter dictionary for hyperparameter search.
ignore_keys_for_eval (`List[str]`, *optional*)
A list of keys in the output of your model (if it is a dictionary) that should be ignored when
gathering predictions for evaluation during the training.
kwargs (`Dict[str, Any]`, *optional*):
Additional keyword arguments used to hide deprecated arguments
"""
if resume_from_checkpoint is False: # 6. 如果resume_from_checkpoint参数为False,则将其设置为None
resume_from_checkpoint = None
# memory metrics - must set up as early as possible
self._memory_tracker.start() # 7. 启动内存跟踪器,尽早设置内存指标
args = self.args # 8. 获取训练参数args
self.is_in_train = True # 9. 设置is_in_train标志为True,表示当前处于训练模式
# Attach NEFTune hooks if necessary
if self.neftune_noise_alpha is not None: # 10. 如果neftune_noise_alpha参数不为None,则激活NeFTune
self.model = self._activate_neftune(self.model) # 11. 调用_activate_neftune方法,为模型添加NeFTune噪声
# do_train is not a reliable argument, as it might not be set and .train() still called, so
# the following is a workaround:
if (args.fp16_full_eval or args.bf16_full_eval) and not args.do_train: # 12. 如果指定了全精度(fp16或bf16)评估,且不进行训练,则执行以下操作
self._move_model_to_device(self.model, args.device) # 13. 将模型移动到指定的设备(如GPU)上
if "model_path" in kwargs: # 14. 如果kwargs中包含"model_path"键
resume_from_checkpoint = kwargs.pop("model_path") # 15. 将"model_path"的值赋给resume_from_checkpoint
warnings.warn(
"`model_path` is deprecated and will be removed in a future version. Use `resume_from_checkpoint` "
"instead.",
FutureWarning,
) # 16. 发出警告,提示"model_path"参数已被弃用,将在未来版本中删除
if len(kwargs) > 0: # 17. 如果kwargs不为空
raise TypeError(f"train() received got unexpected keyword arguments: {', '.join(list(kwargs.keys()))}.")
# 18. 抛出TypeError异常,提示train()函数收到了意外的关键字参数
# This might change the seed so needs to run first.
self._hp_search_setup(trial) # 19. 调用_hp_search_setup方法,传入trial参数,用于超参数搜索的设置
self._train_batch_size = self.args.train_batch_size # 20. 设置训练批次大小为args.train_batch_size
# Model re-init
model_reloaded = False # 21. 初始化model_reloaded标志为False,表示模型尚未重新加载
if self.model_init is not None: # 22. 如果model_init不为None,表示需要重新初始化模型
# Seed must be set before instantiating the model when using model_init.
enable_full_determinism(self.args.seed) if self.args.full_determinism else set_seed(self.args.seed)
# 23. 如果args.full_determinism为True,则启用完全确定性模式;否则设置随机种子
self.model = self.call_model_init(trial) # 24. 调用call_model_init方法,传入trial参数,获取新的模型实例
model_reloaded = True # 25. 设置model_reloaded标志为True,表示模型已重新加载
# Reinitializes optimizer and scheduler
self.optimizer, self.lr_scheduler = None, None # 26. 将优化器和学习率调度器重置为None
# Load potential model checkpoint
if isinstance(resume_from_checkpoint, bool) and resume_from_checkpoint: # 27. 如果resume_from_checkpoint是布尔值且为True
resume_from_checkpoint = get_last_checkpoint(args.output_dir) # 28. 从args.output_dir中获取最新的检查点路径
if resume_from_checkpoint is None: # 29. 如果没有找到有效的检查点
raise ValueError(f"No valid checkpoint found in output directory ({args.output_dir})")
# 30. 抛出ValueError异常,提示在输出目录中没有找到有效的检查点
if resume_from_checkpoint is not None: # 31. 如果resume_from_checkpoint不为None,表示需要从检查点恢复训练
if not is_sagemaker_mp_enabled() and not self.is_deepspeed_enabled and not self.is_fsdp_enabled:
# 32. 如果没有启用SageMaker模型并行、DeepSpeed或PyTorch FSDP,则执行以下操作
self._load_from_checkpoint(resume_from_checkpoint) # 33. 调用_load_from_checkpoint方法,从检查点加载模型、优化器和学习率调度器的状态
# In case of repeating the find_executable_batch_size, set `self._train_batch_size` properly
state = TrainerState.load_from_json(os.path.join(resume_from_checkpoint, TRAINER_STATE_NAME))
# 34. 从检查点中加载TrainerState对象
if state.train_batch_size is not None: # 35. 如果TrainerState中包含train_batch_size信息
self._train_batch_size = state.train_batch_size # 36. 将训练批次大小设置为TrainerState中的值
# If model was re-initialized, put it on the right device and update self.model_wrapped
if model_reloaded: # 37. 如果模型已重新加载
if self.place_model_on_device: # 38. 如果需要将模型放置到设备上
self._move_model_to_device(self.model, args.device) # 39. 调用_move_model_to_device方法,将模型移动到指定的设备上
self.model_wrapped = self.model # 40. 将model_wrapped设置为新的模型实例
inner_training_loop = find_executable_batch_size(
self._inner_training_loop, self._train_batch_size, args.auto_find_batch_size
)
# 41. 调用find_executable_batch_size函数,获取可执行的内部训练循环函数
# 该函数根据训练批次大小和是否自动查找批次大小的设置,返回合适的内部训练循环函数
if args.push_to_hub: # 42. 如果需要将模型推送到Hugging Face Hub
try:
# Disable progress bars when uploading models during checkpoints to avoid polluting stdout
hf_hub_utils.disable_progress_bars() # 43. 在检查点上传模型时,禁用进度条,避免污染标准输出
return inner_training_loop(
args=args,
resume_from_checkpoint=resume_from_checkpoint,
trial=trial,
ignore_keys_for_eval=ignore_keys_for_eval,
)
# 44. 调用内部训练循环函数,传入相关参数,并返回结果
finally:
hf_hub_utils.enable_progress_bars() # 45. 在finally块中,重新启用进度条
else: # 46. 如果不需要将模型推送到Hugging Face Hub
return inner_training_loop(
args=args,
resume_from_checkpoint=resume_from_checkpoint,
trial=trial,
ignore_keys_for_eval=ignore_keys_for_eval,
)
# 47. 直接调用内部训练循环函数,传入相关参数,并返回结果
五、Trainer 中的 _inner_training_loop 函数
transformers:4.39
def _inner_training_loop(
self, batch_size=None, args=None, resume_from_checkpoint=None, trial=None, ignore_keys_for_eval=None
):
# 释放内存,防止内存泄漏
self.accelerator.free_memory()
# 设置当前批量大小
self._train_batch_size = batch_size
# 如果设置了自动查找批量大小,则根据当前批量大小与之前设置的不同,重新包装模型
if self.args.auto_find_batch_size:
if self.state.train_batch_size != self._train_batch_size:
from accelerate.utils import release_memory
# 释放模型占用的内存
(self.model_wrapped,) = release_memory(self.model_wrapped)
self.model_wrapped = self.model
# 对于DeepSpeed模型,需要根据新的批量大小修改配置
if self.is_deepspeed_enabled:
# 暂时取消设置的训练批量大小
original_bs = self.args.per_device_train_batch_size
self.args.per_device_train_batch_size = self._train_batch_size // max(1, self.args.n_gpu)
# 将新的配置传播到DeepSpeed
self.propagate_args_to_deepspeed(True)
self.args.per_device_train_batch_size = original_bs
self.state.train_batch_size = self._train_batch_size
# 记录当前训练使用的批量大小
logger.debug(f"Currently training with a batch size of: {self._train_batch_size}")
# 获取训练数据加载器
train_dataloader = self.get_train_dataloader()
# 对于FSDP-XLA-V2模型,需要使用tpu_spmd_dataloader包装数据加载器
if self.is_fsdp_xla_v2_enabled:
train_dataloader = tpu_spmd_dataloader(train_dataloader)
# 设置训练控制变量
total_train_batch_size = self._train_batch_size * args.gradient_accumulation_steps * args.world_size
# 计算每个epoch的更新步数、总训练步数等
len_dataloader = None
num_train_tokens = None
if has_length(train_dataloader):
len_dataloader = len(train_dataloader)
num_update_steps_per_epoch = len_dataloader // args.gradient_accumulation_steps
num_update_steps_per_epoch = max(num_update_steps_per_epoch, 1)
num_examples = self.num_examples(train_dataloader)
if args.max_steps > 0:
max_steps = args.max_steps
num_train_epochs = args.max_steps // num_update_steps_per_epoch + int(
args.max_steps % num_update_steps_per_epoch > 0
)
num_train_samples = args.max_steps * total_train_batch_size
if args.include_tokens_per_second:
num_train_tokens = (
self.num_tokens(train_dataloader, args.max_steps) * args.gradient_accumulation_steps
)
else:
max_steps = math.ceil(args.num_train_epochs * num_update_steps_per_epoch)
num_train_epochs = math.ceil(args.num_train_epochs)
num_train_samples = self.num_examples(train_dataloader) * args.num_train_epochs
if args.include_tokens_per_second:
num_train_tokens = self.num_tokens(train_dataloader) * args.num_train_epochs
# 如果数据加载器没有长度信息,则依赖max_steps参数
elif args.max_steps > 0:
max_steps = args.max_steps
num_train_epochs = sys.maxsize # 设置为一个很大的数字,以便遍历整个迭代器
num_update_steps_per_epoch = max_steps
num_examples = total_train_batch_size * args.max_steps
num_train_samples = args.max_steps * total_train_batch_size
if args.include_tokens_per_second:
num_train_tokens = self.num_tokens(train_dataloader, args.max_steps) * args.gradient_accumulation_steps
else:
raise ValueError(
"args.max_steps must be set to a positive value if dataloader does not have a length, was"
f" {args.max_steps}"
)
# 检查是否启用了下溢/上溢调试选项,如果启用则进行相应处理
if DebugOption.UNDERFLOW_OVERFLOW in self.args.debug:
if self.args.n_gpu > 1:
raise ValueError(
"Currently --debug underflow_overflow is not supported under DP. Please use DDP"
" (torchrun or torch.distributed.launch (deprecated))."
)
else:
debug_overflow = DebugUnderflowOverflow(self.model) # noqa
# 判断是否需要延迟创建优化器
delay_optimizer_creation = is_sagemaker_mp_enabled() or self.is_fsdp_xla_enabled or self.is_fsdp_enabled
# 重置学习率调度器
if self._created_lr_scheduler:
self.lr_scheduler = None
self._created_lr_scheduler = False
# 对于DeepSpeed模型,使用deepspeed_init初始化优化器和学习率调度器
if self.is_deepspeed_enabled:
self.optimizer, self.lr_scheduler = deepspeed_init(self, num_training_steps=max_steps)
# 如果不需要延迟创建优化器,则立即创建
if not delay_optimizer_creation:
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
# 初始化训练状态
self.state = TrainerState()
self.state.is_hyper_param_search = trial is not None
self.state.train_batch_size = self._train_batch_size
# 计算日志、评估和保存的绝对步数,如果给定的是比例则转换为步数
if args.logging_steps is not None:
if args.logging_steps < 1:
self.state.logging_steps = math.ceil(max_steps * args.logging_steps)
else:
self.state.logging_steps = args.logging_steps
if args.eval_steps is not None:
if args.eval_steps < 1:
self.state.eval_steps = math.ceil(max_steps * args.eval_steps)
else:
self.state.eval_steps = args.eval_steps
if args.save_steps is not None:
if args.save_steps < 1:
self.state.save_steps = math.ceil(max_steps * args.save_steps)
else:
self.state.save_steps = args.save_steps
# 如果启用了梯度检查点,则对模型进行相应设置
if args.gradient_checkpointing:
if args.gradient_checkpointing_kwargs is None:
gradient_checkpointing_kwargs = {}
else:
gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs
self.model.gradient_checkpointing_enable(gradient_checkpointing_kwargs=gradient_checkpointing_kwargs)
# 对模型进行包装,以支持不同的并行化方式
model = self._wrap_model(self.model_wrapped)
# 判断是否需要使用accelerator.prepare方法准备模型
use_accelerator_prepare = True if model is self.model else False
# 如果需要延迟创建优化器,则在此处准备模型
if delay_optimizer_creation:
if use_accelerator_prepare:
self.model = self.accelerator.prepare(self.model)
self.create_optimizer_and_scheduler(num_training_steps=max_steps)
# 使用accelerator.prepare方法准备模型、优化器和学习率调度器
if use_accelerator_prepare:
self.model.train()
if hasattr(self.lr_scheduler, "step"):
if self.use_apex:
model = self.accelerator.prepare(self.model)
else:
model, self.optimizer = self.accelerator.prepare(self.model, self.optimizer)
else:
model, self.optimizer, self.lr_scheduler = self.accelerator.prepare(
self.model, self.optimizer, self.lr_scheduler
)
# 对于FSDP模型,更新模型包装
if self.is_fsdp_enabled:
self.model = self.model_wrapped = model
# 更新模型包装
if model is not self.model:
self.model_wrapped = model
# 对于DeepSpeed模型,设置self.deepspeed
if self.is_deepspeed_enabled:
self.deepspeed = self.model_wrapped
# 如果指定了从检查点恢复训练,则加载检查点
if resume_from_checkpoint is not None:
if self.is_deepspeed_enabled:
deepspeed_load_checkpoint(
self.model_wrapped, resume_from_checkpoint, load_module_strict=not _is_peft_model(self.model)
)
elif is_sagemaker_mp_enabled() or self.is_fsdp_enabled:
self._load_from_checkpoint(resume_from_checkpoint, self.model_wrapped)
# 加载优化器和学习率调度器的状态
self._load_optimizer_and_scheduler(resume_from_checkpoint)
# 在此时,self.model是Transformers模型,self.model_wrapped是包装后的模型
logger.info("***** Running training *****")
logger.info(f" Num examples = {num_examples:,}")
logger.info(f" Num Epochs = {num_train_epochs:,}")
logger.info(f" Instantaneous batch size per device = {self.args.per_device_train_batch_size:,}")
if self.args.per_device_train_batch_size != self._train_batch_size:
logger.info(f" Training with DataParallel so batch size has been adjusted to: {self._train_batch_size:,}")
logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_train_batch_size:,}")
logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
logger.info(f" Total optimization steps = {max_steps:,}")
logger.info(f" Number of trainable parameters = {get_model_param_count(model, trainable_only=True):,}")
# 初始化训练相关变量
self.state.epoch = 0
start_time = time.time()
epochs_trained = 0
steps_trained_in_current_epoch = 0
steps_trained_progress_bar = None
# 如果从检查点恢复训练,则加载训练状态
if resume_from_checkpoint is not None and os.path.isfile(
os.path.join(resume_from_checkpoint, TRAINER_STATE_NAME)
):
self.state = TrainerState.load_from_json(os.path.join(resume_from_checkpoint, TRAINER_STATE_NAME))
epochs_trained = self.state.global_step // num_update_steps_per_epoch
if not args.ignore_data_skip:
steps_trained_in_current_epoch = self.state.global_step % (num_update_steps_per_epoch)
steps_trained_in_current_epoch *= args.gradient_accumulation_steps
else:
steps_trained_in_current_epoch = 0
logger.info(" Continuing training from checkpoint, will skip to saved global_step")
logger.info(f" Continuing training from epoch {epochs_trained}")
logger.info(f" Continuing training from global step {self.state.global_step}")
if not args.ignore_data_skip:
logger.info(
f" Will skip the first {epochs_trained} epochs then the first"
f" {steps_trained_in_current_epoch} batches in the first epoch."
)
# 更新回调处理器中的引用
self.callback_handler.model = self.model
self.callback_handler.optimizer = self.optimizer
self.callback_handler.lr_scheduler = self.lr_scheduler
self.callback_handler.train_dataloader = train_dataloader
if self.hp_name is not None and self._trial is not None:
self.state.trial_name = self.hp_name(self._trial)
if trial is not None:
assignments = trial.assignments if self.hp_search_backend == HPSearchBackend.SIGOPT else trial
self.state.trial_params = hp_params(assignments)
else:
self.state.trial_params = None
self.state.max_steps = max_steps
self.state.num_train_epochs = num_train_epochs
self.state.is_local_process_zero = self.is_local_process_zero()
self.state.is_world_process_zero = self.is_world_process_zero()
# 初始化损失和梯度范数
tr_loss = torch.tensor(0.0).to(args.device)
self._total_loss_scalar = 0.0
self._globalstep_last_logged = self.state.global_step
model.zero_grad()
grad_norm: Optional[float] = None
# 触发训练开始回调
self.control = self.callback_handler.on_train_begin(args, self.state, self.control)
# 如果从检查点恢复训练,则跳过已经训练的epoch
if not args.ignore_data_skip:
for epoch in range(epochs_trained):
sampler = get_dataloader_sampler(train_dataloader)
sampler_kinds = [RandomSampler]
if version.parse(accelerate_version) > version.parse("0.23.0"):
sampler_kinds.append(SeedableRandomSampler)
is_random_sampler = isinstance(sampler, tuple(sampler_kinds))
if not is_random_sampler:
for _ in train_dataloader:
break
else:
sampler = sampler if sampler is not None else []
_ = list(sampler)
total_batched_samples = 0
for epoch in range(epochs_trained, num_train_epochs):
epoch_iterator = train_dataloader
if hasattr(epoch_iterator, "set_epoch"):
epoch_iterator.set_epoch(epoch)
# 在每个epoch开始时重置past mems状态
if args.past_index >= 0:
self._past = None
steps_in_epoch = (
len(epoch_iterator)
if len_dataloader is not None else args.max_steps * args.gradient_accumulation_steps
)
# 触发 epoch 开始的回调
self.control = self.callback_handler.on_epoch_begin(args, self.state, self.control)
# 如果是从检查点恢复的第一个 epoch,且之前已经训练过一些 batch,则加载之前的随机数生成器状态
if epoch == epochs_trained and resume_from_checkpoint is not None and steps_trained_in_current_epoch == 0:
self._load_rng_state(resume_from_checkpoint)
rng_to_sync = False
steps_skipped = 0
# 如果当前 epoch 已经训练过一些 batch,则跳过这些 batch
if steps_trained_in_current_epoch > 0:
epoch_iterator = skip_first_batches(epoch_iterator, steps_trained_in_current_epoch)
steps_skipped = steps_trained_in_current_epoch
steps_trained_in_current_epoch = 0
rng_to_sync = True
step = -1
for step, inputs in enumerate(epoch_iterator):
total_batched_samples += 1
# 如果启用了跟踪输入 token 数量,则计算当前 batch 的输入 token 数
if self.args.include_num_input_tokens_seen:
main_input_name = getattr(self.model, "main_input_name", "input_ids")
if main_input_name not in inputs:
logger.warning(
"Tried to track the number of tokens seen, however the current model is "
"not configured properly to know what item is the input. To fix this, add "
"a `main_input_name` attribute to the model class you are using."
)
else:
self.state.num_input_tokens_seen += self.accelerator.gather(inputs[main_input_name]).numel()
# 如果需要同步随机数生成器状态,则从检查点加载
if rng_to_sync:
self._load_rng_state(resume_from_checkpoint)
rng_to_sync = False
# 如果是从检查点恢复训练,则跳过已经训练过的 batch
if steps_trained_in_current_epoch > 0:
steps_trained_in_current_epoch -= 1
if steps_trained_progress_bar is not None:
steps_trained_progress_bar.update(1)
if steps_trained_in_current_epoch == 0:
self._load_rng_state(resume_from_checkpoint)
continue
elif steps_trained_progress_bar is not None:
steps_trained_progress_bar.close()
steps_trained_progress_bar = None
# 触发 step 开始的回调
if step % args.gradient_accumulation_steps == 0:
self.control = self.callback_handler.on_step_begin(args, self.state, self.control)
# 使用 accelerator.accumulate 上下文管理器累积梯度
with self.accelerator.accumulate(model):
tr_loss_step = self.training_step(model, inputs)
# 如果损失是 NaN 或无穷大,并且启用了 NaN/Inf 过滤,则使用之前记录的损失的平均值
if (
args.logging_nan_inf_filter
and not is_torch_tpu_available()
and (torch.isnan(tr_loss_step) or torch.isinf(tr_loss_step))
):
tr_loss += tr_loss / (1 + self.state.global_step - self._globalstep_last_logged)
else:
tr_loss += tr_loss_step
# 计算当前 batch 的浮点运算数量
self.current_flos += float(self.floating_point_ops(inputs))
# 判断是否是当前 epoch 的最后一个 step,并且 step 数小于梯度累积步数
is_last_step_and_steps_less_than_grad_acc = (
steps_in_epoch <= args.gradient_accumulation_steps and (step + 1) == steps_in_epoch
)
# 如果是梯度累积的边界步,或者是最后一个 step 且 step 数小于梯度累积步数,则进行以下操作
if (
total_batched_samples % args.gradient_accumulation_steps == 0
or is_last_step_and_steps_less_than_grad_acc
):
# 如果是最后一个 step 且 step 数小于梯度累积步数,则显式设置 sync_gradients 为 True
# 这是因为在 accelerate 中,该情况下的 sync_gradients 默认为 False
if is_last_step_and_steps_less_than_grad_acc:
self.accelerator.gradient_state._set_sync_gradients(True)
# 梯度裁剪
if args.max_grad_norm is not None and args.max_grad_norm > 0:
if is_sagemaker_mp_enabled() and args.fp16:
# 对于 SageMaker 混合精度训练,使用 clip_master_grads 进行梯度裁剪
_grad_norm = self.optimizer.clip_master_grads(args.max_grad_norm)
elif self.use_apex:
# 对于 Apex 混合精度训练,使用 nn.utils.clip_grad_norm_ 进行梯度裁剪
_grad_norm = nn.utils.clip_grad_norm_(
amp.master_params(self.optimizer),
args.max_grad_norm,
)
else:
# 对于其他情况,使用 accelerator.clip_grad_norm_ 进行梯度裁剪
_grad_norm = self.accelerator.clip_grad_norm_(
model.parameters(),
args.max_grad_norm,
)
# DeepSpeed 模型使用自己的梯度裁剪方式
# 如果是 DeepSpeed 分布式训练,则获取全局梯度范数
if (
is_accelerate_available()
and self.accelerator.distributed_type == DistributedType.DEEPSPEED
):
grad_norm = model.get_global_grad_norm()
else:
# 否则,使用裁剪后的梯度范数
grad_norm = _grad_norm.item() if _grad_norm is not None else None
# 优化器步进
self.optimizer.step()
optimizer_was_run = not self.accelerator.optimizer_step_was_skipped
# 如果优化器确实运行,则更新学习率调度器
if optimizer_was_run:
if not isinstance(self.lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
self.lr_scheduler.step()
# 梯度清零
model.zero_grad()
# 更新全局步数和 epoch 进度
self.state.global_step += 1
self.state.epoch = epoch + (step + 1 + steps_skipped) / steps_in_epoch
# 触发 step 结束的回调
self.control = self.callback_handler.on_step_end(args, self.state, self.control)
# 根据设置,进行日志记录、模型评估和保存
self._maybe_log_save_evaluate(tr_loss, grad_norm, model, trial, epoch, ignore_keys_for_eval)
else:
# 触发子步结束的回调
self.control = self.callback_handler.on_substep_end(args, self.state, self.control)
# 检查是否需要提前停止当前 epoch 或整个训练
if self.control.should_epoch_stop or self.control.should_training_stop:
# 对于 PyTorch/XLA,需要手动插入 mark_step
if is_torch_tpu_available():
xm.mark_step()
break
# 如果当前 epoch 没有遍历任何 batch,则打印警告并停止训练
if step < 0:
logger.warning(
"There seems to be not a single sample in your epoch_iterator, stopping training at step"
f" {self.state.global_step}! This is expected if you're using an IterableDataset and set"
f" num_steps ({max_steps}) higher than the number of available samples."
)
self.control.should_training_stop = True
# 触发 epoch 结束的回调
self.control = self.callback_handler.on_epoch_end(args, self.state, self.control)
# 根据设置,进行日志记录、模型评估和保存
self._maybe_log_save_evaluate(tr_loss, grad_norm, model, trial, epoch, ignore_keys_for_eval)
# 如果启用了 TPU 指标调试,则打印相关指标报告
if DebugOption.TPU_METRICS_DEBUG in self.args.debug:
if is_torch_tpu_available():
xm.master_print(met.metrics_report())
else:
logger.warning(
"You enabled PyTorch/XLA debug metrics but you don't have a TPU "
"configured. Check your training configuration if this is unexpected."
)
# 如果控制器指示需要停止训练,则退出训练循环
if self.control.should_training_stop:
break
# 清理 past 状态(如果存在)
if args.past_index and hasattr(self, "_past"):
delattr(self, "_past")
logger.info("\n\nTraining completed. Do not forget to share your model on huggingface.co/models =)\n\n")
# 如果设置了在结束时加载最佳模型,并且最佳模型检查点存在,则加载最佳模型
if args.load_best_model_at_end and self.state.best_model_checkpoint is not None:
if is_torch_tpu_available():
xm.rendezvous("load_best_model_at_end")
elif args.parallel_mode == ParallelMode.DISTRIBUTED:
dist.barrier()
elif is_sagemaker_mp_enabled():
smp.barrier()
self._load_best_model()
# 计算总体训练损失
self._total_loss_scalar += tr_loss.item()
train_loss = self._total_loss_scalar / self.state.global_step
# 计算训练速度相关指标
metrics = speed_metrics(
"train",
start_time,
num_samples=num_train_samples,
num_steps=self.state.max_steps,
num_tokens=num_train_tokens,
)
self.store_flos()
metrics["total_flos"] = self.state.total_flos
metrics["train_loss"] = train_loss
self.is_in_train = False
# 更新内存跟踪器的指标
self._memory_tracker.stop_and_update_metrics(metrics)
# 记录指标
self.log(metrics)
run_dir = self._get_output_dir(trial)
checkpoints_sorted = self._sorted_checkpoints(use_mtime=False, output_dir=run_dir)
# 如果设置了保存总数限制为 1,并且最佳模型检查点存在,则删除非最佳模型检查点
if self.args.should_save and self.state.best_model_checkpoint is not None and self.args.save_total_limit == 1:
for checkpoint in checkpoints_sorted:
if not os.path.samefile(checkpoint, self.state.best_model_checkpoint):
logger.info(f"Deleting older checkpoint [{checkpoint}] due to args.save_total_limit")
shutil.rmtree(checkpoint)
# 触发训练结束的回调
self.control = self.callback_handler.on_train_end(args, self.state, self.control)
# 等待检查点上传完成
self._finish_current_push()
# 如果启用了 Neftune,则在训练结束时停用
if self.neftune_noise_alpha is not None:
self._deactivate_neftune(self.model)
return TrainOutput(self.state.global_step, train_loss, metrics)
六、Trainer 中的 _load_best_model函数
transformers:4.39
- 这个函数的主要作用是加载模型的最佳检查点。它首先构建最佳检查点文件的路径,然后根据不同的配置(如是否启用DeepSpeed、FSDP等)选择不同的加载方式。
- 如果启用了DeepSpeed,则使用
deepspeed_load_checkpoint
函数加载检查点;如果启用了FSDP,则使用load_fsdp_model
函数加载检查点。如果存在模型权重文件或安全模型权重文件,则直接加载对应的权重。- 在加载过程中,代码还考虑了一些特殊情况,如是否启用了SageMaker多进程、是否使用了PEFT和LoRA等。如果模型使用了PEFT和LoRA,则需要单独加载适配器权重。
- 此外,代码还支持加载分片检查点(sharded checkpoint),用于分布式训练场景。
- 最后,如果找不到最佳检查点文件,则会打印相应的警告信息。
总的来说,这个函数涉及了一些高级的深度学习训练技术,如DeepSpeed、FSDP、PEFT等,用于加速训练或减小模型大小。同时,它也考虑了不同的模型权重格式(PyTorch、SafeTensors等)和分布式训练场景,提供了相应的加载方式。
def _load_best_model(self):
# 加载模型的最佳检查点
# 打印日志,显示最佳检查点的路径和相应的评分
logger.info(f"Loading best model from {self.state.best_model_checkpoint} (score: {self.state.best_metric}).")
# 构建最佳检查点文件的路径
best_model_path = os.path.join(self.state.best_model_checkpoint, WEIGHTS_NAME)
best_safe_model_path = os.path.join(self.state.best_model_checkpoint, SAFE_WEIGHTS_NAME)
best_adapter_model_path = os.path.join(self.state.best_model_checkpoint, ADAPTER_WEIGHTS_NAME)
best_safe_adapter_model_path = os.path.join(self.state.best_model_checkpoint, ADAPTER_SAFE_WEIGHTS_NAME)
# 获取模型对象
# 如果启用了SageMaker多进程(sagemaker_mp_enabled),则使用model_wrapped
# 否则使用model
model = self.model_wrapped if is_sagemaker_mp_enabled() else self.model
# 根据不同的配置,选择不同的加载方式
if self.is_deepspeed_enabled:
# 如果启用了DeepSpeed,则使用deepspeed_load_checkpoint函数加载检查点
# load_module_strict参数指定是否严格加载模型权重
# 如果模型使用了PEFT(参数高效微调),则不启用严格模式
deepspeed_load_checkpoint(
self.model_wrapped,
self.state.best_model_checkpoint,
load_module_strict=not _is_peft_model(self.model),
)
elif self.is_fsdp_enabled:
# 如果启用了FSDP(完全分片数据并行),则使用load_fsdp_model函数加载检查点
# 传入FSDP插件、加速器、模型对象和检查点路径
load_result = load_fsdp_model(
self.accelerator.state.fsdp_plugin,
self.accelerator,
model,
self.state.best_model_checkpoint,
**_get_fsdp_ckpt_kwargs(),
)
elif (
os.path.exists(best_model_path)
or os.path.exists(best_safe_model_path)
or os.path.exists(best_adapter_model_path)
or os.path.exists(best_safe_adapter_model_path)
):
# 如果存在模型权重文件或安全模型权重文件,则加载对应的权重
has_been_loaded = True
# weights_only参数用于指定是否只加载模型权重
# 在PyTorch 1.13及更高版本中,需要使用该参数
weights_only_kwarg = {"weights_only": True} if is_torch_greater_or_equal_than_1_13 else {}
if is_sagemaker_mp_enabled():
# 如果启用了SageMaker多进程
if os.path.isfile(os.path.join(self.state.best_model_checkpoint, "user_content.pt")):
# 如果存在user_content.pt文件,则使用新的SMP API加载检查点
smp.resume_from_checkpoint(
path=self.state.best_model_checkpoint,
tag=WEIGHTS_NAME,
partial=False,
load_optimizer=False,
)
else:
# 如果不存在user_content.pt文件,则使用旧的SMP API加载检查点
if self.args.save_safetensors and os.path.isfile(best_safe_model_path):
# 如果启用了SafeTensors并存在安全模型权重文件,则使用SafeTensors加载
state_dict = safetensors.torch.load_file(best_safe_model_path, device="cpu")
else:
# 否则使用PyTorch加载模型权重文件
state_dict = torch.load(
best_model_path,
map_location="cpu",
**weights_only_kwarg,
)
# 设置_smp_is_partial标志为False
state_dict["_smp_is_partial"] = False
# 加载模型状态字典
load_result = model.load_state_dict(state_dict, strict=True)
else:
if _is_peft_model(model):
# 如果模型使用了PEFT和LoRA(低秩适配)
# 假设适配器已正确保存
if hasattr(model, "active_adapter") and hasattr(model, "load_adapter"):
if os.path.exists(best_adapter_model_path) or os.path.exists(best_safe_adapter_model_path):
# 加载适配器权重
model.load_adapter(self.state.best_model_checkpoint, model.active_adapter)
# load_adapter没有返回值,因此手动设置一个占位的返回值
from torch.nn.modules.module import _IncompatibleKeys
load_result = _IncompatibleKeys([], [])
else:
# 如果适配器权重文件不存在,则打印警告
logger.warning(
"The intermediate checkpoints of PEFT may not be saved correctly, "
f"consider using a custom callback to save {ADAPTER_WEIGHTS_NAME} in corresponding saving folders. "
"Check some examples here: https://github.com/huggingface/peft/issues/96"
)
has_been_loaded = False
else:
# 如果模型没有active_adapter或load_adapter方法,则打印警告
logger.warning("Could not load adapter model, make sure to have `peft>=0.3.0` installed")
has_been_loaded = False
else:
# 如果不是PEFT模型
# 在CPU上加载模型权重,以避免内存不足错误
if self.args.save_safetensors and os.path.isfile(best_safe_model_path):
state_dict = safetensors.torch.load_file(best_safe_model_path, device="cpu")
else:
state_dict = torch.load(
best_model_path,
map_location="cpu",
**weights_only_kwarg,
)
# 如果模型在GPU上,也可以正常加载
# 这是为了解决FSDP的一个bug(https://github.com/pytorch/pytorch/issues/82963)
load_result = model.load_state_dict(state_dict, False)
# 如果成功加载了模型权重,并且未启用SageMaker多进程,则发出加载警告
if not is_sagemaker_mp_enabled() and has_been_loaded:
self._issue_warnings_after_load(load_result)
elif os.path.exists(os.path.join(self.state.best_model_checkpoint, WEIGHTS_INDEX_NAME)):
# 如果存在分片检查点索引文件,则加载分片检查点
load_result = load_sharded_checkpoint(
model, self.state.best_model_checkpoint, strict=is_sagemaker_mp_enabled()
)
# 如果未启用SageMaker多进程,则发出加载警告
if not is_sagemaker_mp_enabled():
self._issue_warnings_after_load(load_result)
else:
# 如果找不到最佳检查点文件,则打印警告
logger.warning(
f"Could not locate the best model at {best_model_path}, if you are running a distributed training "
"on multiple nodes, you should activate `--save_on_each_node`."
)
七、Trainer 中的_save_checkpoint函数
transformers:4.39
7.1、_save_checkpoint函数
- 这个方法的主要作用是保存模型的检查点,包括模型权重、优化器状态、调度器状态和RNG状态等。它首先构建检查点文件夹的路径,并根据指定的条件(如是否进行超参数搜索、是否保存浮点运算数等)执行相应的操作。
- 然后,该方法会创建一个暂存输出目录,用于临时保存检查点文件。它调用
save_model
方法将模型权重保存到暂存目录中(save_model函数保存模型包括deepspeed zero3,FSDP等分布式保存,具体见下文
save_model函数
)。如果需要,还会保存优化器、调度器和RNG状态。- 接下来,该方法会根据指定的指标和
greater_is_better
参数,确定是否需要更新最佳指标和最佳模型检查点。如果需要更新,则会更新相应的状态变量。- 之后,该方法会保存Trainer的状态,并在需要时将检查点推送到Hugging Face Hub。
- 在所有进程完成写入操作后,主进程会将暂存输出目录重命名为最终输出目录。如果指定了
save_on_each_node
参数,则每个节点都会执行此操作;否则,只有世界进程0会执行。同时,主进程还会根据需要删除一些较旧的检查点,以节省磁盘空间。- 最后,该方法会等待所有进程完成相关操作。
总的来说,这个方法涉及了分布式训练、检查点保存和管理等多个方面的功能,确保了模型的检查点可以在各种环境和配置下正常保存和加载。
def _save_checkpoint(self, model, trial, metrics=None):
# 在所有情况下,包括ddp/dp/deepspeed,self.model始终是我们要保存的模型的引用,除了FullyShardedDDP
# assert unwrap_model(model) is self.model, "internal model should be a reference to self.model"
# 保存模型检查点
checkpoint_folder = f"{PREFIX_CHECKPOINT_DIR}-{self.state.global_step}"
# 如果没有使用超参数搜索后端(hp_search_backend)且trial为None,则存储浮点运算数(flos)
if self.hp_search_backend is None and trial is None:
self.store_flos()
# 获取输出目录的路径
run_dir = self._get_output_dir(trial=trial)
output_dir = os.path.join(run_dir, checkpoint_folder)
# 如果输出目录已存在且不为空,则发出警告
if os.path.exists(output_dir) and len(os.listdir(output_dir)) > 0:
logger.warning(
f"Checkpoint destination directory {output_dir} already exists and is non-empty. "
"Saving will proceed but saved results may be invalid."
)
staging_output_dir = output_dir
else:
# 否则,创建一个临时的暂存输出目录
staging_output_dir = os.path.join(run_dir, f"tmp-{checkpoint_folder}")
# 调用save_model方法,将模型保存到暂存输出目录
self.save_model(staging_output_dir, _internal_call=True)
# 如果不仅保存模型,还需要保存优化器、调度器和RNG状态
if not self.args.save_only_model:
# 保存优化器和调度器
self._save_optimizer_and_scheduler(staging_output_dir)
# 保存RNG状态
self._save_rng_state(staging_output_dir)
# 确定新的最佳指标/最佳模型检查点
if metrics is not None and self.args.metric_for_best_model is not None:
metric_to_check = self.args.metric_for_best_model
if not metric_to_check.startswith("eval_"):
metric_to_check = f"eval_{metric_to_check}"
metric_value = metrics[metric_to_check]
# 根据指定的greater_is_better参数,确定是否需要更新最佳指标和最佳模型检查点
operator = np.greater if self.args.greater_is_better else np.less
if (
self.state.best_metric is None
or self.state.best_model_checkpoint is None
or operator(metric_value, self.state.best_metric)
):
self.state.best_metric = metric_value
self.state.best_model_checkpoint = output_dir
# 保存Trainer状态
if self.args.should_save:
self.state.save_to_json(os.path.join(staging_output_dir, TRAINER_STATE_NAME))
# 如果指定了push_to_hub,则将检查点推送到Hugging Face Hub
if self.args.push_to_hub:
self._push_from_checkpoint(staging_output_dir)
# 等待所有进程完成写入操作
self.args.distributed_state.wait_for_everyone()
# 然后通过主进程(es)进行重命名和轮换过程
if self.is_local_process_zero() if self.args.save_on_each_node else self.is_world_process_zero():
# 如果暂存输出目录与最终输出目录不同,则重命名暂存输出目录
if staging_output_dir != output_dir:
if os.path.exists(staging_output_dir):
os.rename(staging_output_dir, output_dir)
# 确保重命名操作完成,特别是在非Windows系统上,因为os.rename可能不是原子操作
if os.name != "nt":
fd = os.open(output_dir, os.O_RDONLY)
os.fsync(fd)
os.close(fd)
# 可能删除一些较旧的检查点
if self.args.should_save:
# 仅依赖数字检查点ID进行轮换,而不使用修改时间(mtime),因为mtime在某些云环境下的文件系统中可能不可靠
self._rotate_checkpoints(use_mtime=False, output_dir=run_dir)
elif self.is_local_process_zero():
# 在其他节点上清理剩余的暂存检查点文件夹
if staging_output_dir != output_dir and os.path.exists(staging_output_dir):
shutil.rmtree(staging_output_dir)
# 等待所有进程完成
self.args.distributed_state.wait_for_everyone()
7.2、保存优化器状态和学习率调度_save_optimizer_and_scheduler
该函数的主要作用是保存模型训练过程中使用的优化器和学习率调度器的状态,以便在后续的训练或推理过程中继续使用。它支持多种不同的训练环境和加速技术,包括:
- PyTorch XLA: 用于加速模型训练和推理的技术,提供了分布式同步功能。
- SageMaker Model Parallelism (SMP): Amazon SageMaker提供的模型并行库,用于分布式训练。
- DeepSpeed: 用于加速大型模型训练的库,提供了优化器状态保存和检查点保存功能。
- FSDP (Fully Sharded Data Parallelism): PyTorch中用于数据并行的技术,可以减少内存占用。
- PEFT (Parameter-Efficient Transfer Learning): 一种高效fine-tuning技术,可以选择排除冻结的参数。
根据当前的训练环境和设置,该函数会使用相应的方式来保存优化器和学习率调度器的状态。例如,如果启用了DeepSpeed,它会使用DeepSpeed提供的
save_checkpoint
函数来保存检查点;如果启用了FSDP,它会使用FSDP提供的save_fsdp_model
和save_fsdp_optimizer
函数来保存模型和优化器。在保存过程中,该函数还会处理一些特殊情况,如捕获PyTorch警告、排除冻结参数等。
该函数的主要目的是确保在训练过程中,优化器和学习率调度器的状态能够被正确地保存下来,以便在后续的训练或推理过程中继续使用,从而提高模型训练的可靠性和灵活性。
# _save_optimizer_and_scheduler函数用于保存优化器和学习率调度器的状态
def _save_optimizer_and_scheduler(self, output_dir):
# 如果使用了PyTorch XLA (用于加速模型训练和推理的技术),则使用XLA提供的分布式同步功能
if is_torch_xla_available():
xm.rendezvous("saving_optimizer_states")
xm.save(self.optimizer.state_dict(), os.path.join(output_dir, OPTIMIZER_NAME))
# 捕获PyTorch警告,并使用reissue_pt_warnings函数重新发出这些警告
with warnings.catch_warnings(record=True) as caught_warnings:
xm.save(self.lr_scheduler.state_dict(), os.path.join(output_dir, SCHEDULER_NAME))
reissue_pt_warnings(caught_warnings)
# 如果使用了SageMaker Model Parallelism (SMP),则使用SMP提供的分布式保存功能
elif is_sagemaker_mp_enabled():
opt_state_dict = self.optimizer.local_state_dict(gather_if_shard=False)
smp.barrier()
if smp.rdp_rank() == 0 or smp.state.cfg.shard_optimizer_state:
smp.save(
opt_state_dict,
os.path.join(output_dir, OPTIMIZER_NAME),
partial=True,
v3=smp.state.cfg.shard_optimizer_state,
)
# 如果启用了DeepSpeed (用于加速大型模型训练的库),则使用DeepSpeed提供的保存检查点功能
elif self.is_deepspeed_enabled:
# 在DeepSpeed的ZeRO-3模式下,模型文件本身不会被保存,除非deepspeed配置中的stage3_gather_16bit_weights_on_model_save设置为True
accept_exclude_frozen_parameters = "exclude_frozen_parameters" in set(
inspect.signature(self.model_wrapped.save_checkpoint).parameters.keys()
)
# 如果是使用PEFT (Parameter-Efficient Transfer Learning)进行微调的模型,并且支持exclude_frozen_parameters参数
# 则在保存检查点时排除冻结的参数
if accept_exclude_frozen_parameters and _is_peft_model(self.model):
self.model_wrapped.save_checkpoint(output_dir, exclude_frozen_parameters=True)
# 否则,直接保存检查点
else:
self.model_wrapped.save_checkpoint(output_dir)
# 如果启用了FSDP (Fully Sharded Data Parallelism),则使用FSDP提供的保存模型和优化器的函数
elif self.is_fsdp_enabled:
save_fsdp_model(
self.accelerator.state.fsdp_plugin, self.accelerator, self.model, output_dir, **_get_fsdp_ckpt_kwargs()
)
save_fsdp_optimizer(
self.accelerator.state.fsdp_plugin, self.accelerator, self.optimizer, self.model, output_dir
)
# 如果启用了普通的保存功能,则使用PyTorch的torch.save函数保存优化器状态
elif self.args.should_save:
torch.save(self.optimizer.state_dict(), os.path.join(output_dir, OPTIMIZER_NAME))
# 保存学习率调度器和梯度缩放器的状态
is_deepspeed_custom_scheduler = self.is_deepspeed_enabled and not isinstance(
self.lr_scheduler, DeepSpeedSchedulerWrapper
)
if (
self.args.should_save
and (not self.is_deepspeed_enabled or is_deepspeed_custom_scheduler)
and not is_torch_xla_available()
):
with warnings.catch_warnings(record=True) as caught_warnings:
torch.save(self.lr_scheduler.state_dict(), os.path.join(output_dir, SCHEDULER_NAME))
reissue_pt_warnings(caught_warnings)
八、Trainer 中的save_model函数
transformers:4.39
该代码定义了一个
save_model
方法,用于将训练好的模型保存到磁盘。具体来说:
- 该方法首先检查输出目录,如果没有指定则使用
self.args.output_dir
作为输出目录。- 接下来,根据使用的硬件和框架,分别采取不同的保存策略:
- 如果使用TPU设备,调用
self._save_tpu
方法保存模型。- 如果启用了SageMaker多进程训练,需要在所有进程上获取模型的
state_dict
,然后调用self._save
保存。- 如果启用了FSDP,需要使用
self.accelerator.get_state_dict
获取模型状态字典,然后调用self._save
保存。- 如果启用了DeepSpeed,同样需要使用
self.accelerator.get_state_dict(.get_state_dict 这个函数可以将ZeRO3切片在其他设备上的参数加载过来)
获取模型状态字典,然后调用self._save
保存(新版的trainer ZeRO3的参数也会通过self._save函数保存了
)。如果出现异常,则保存完整的检查点,并提示使用zero_to_fp32.py
脚本恢复权重。- 如果没有启用上述特殊功能,直接调用
self._save
保存模型。- 最后,如果
self.args.push_to_hub
为True,且该方法不是由内部调用,则将模型推送到Hugging Face Hub。新版本的transformers
def save_model(self, output_dir: Optional[str] = None, _internal_call: bool = False):
"""
将模型保存到磁盘,以便后续可以使用`from_pretrained()`重新加载该模型。
该方法只会在主进程中执行保存操作。
"""
if output_dir is None:
# 如果没有指定输出目录,则使用self.args.output_dir作为输出目录
output_dir = self.args.output_dir
if is_torch_tpu_available():
# 如果使用的是TPU设备,调用self._save_tpu方法保存模型
self._save_tpu(output_dir)
elif is_sagemaker_mp_enabled():
# 如果启用了SageMaker多进程训练,则需要在所有进程上调用state_dict方法
os.makedirs(output_dir, exist_ok=True) # 创建输出目录(如果不存在)
state_dict = self.model_wrapped.state_dict() # 获取模型的state_dict
if self.args.should_save:
self._save(output_dir, state_dict=state_dict) # 保存模型
if IS_SAGEMAKER_MP_POST_1_10:
# SageMaker多进程版本>=1.10时,在输出目录创建一个"user_content.pt"文件
Path(os.path.join(output_dir, "user_content.pt")).touch()
elif self.is_fsdp_enabled:
# 如果启用了FSDP,则需要使用self.accelerator.get_state_dict获取模型状态字典(.get_state_dict 这个函数可以将切片在其他设备上的参数加载过来)
if ("FULL_STATE_DICT" in str(self.accelerator.state.fsdp_plugin.state_dict_type)) and (
version.parse(accelerate_version) > version.parse("0.24.1")
):
state_dict = self.accelerator.get_state_dict(self.model)
if self.args.should_save:
self._save(output_dir, state_dict=state_dict)
elif self.is_deepspeed_enabled:
# 如果启用了DeepSpeed,则需要使用self.accelerator.get_state_dict获取模型状态字典
# .get_state_dict 这个函数可以将ZeRO3切片在其他设备上的参数加载过来
try:
state_dict = self.accelerator.get_state_dict(self.deepspeed)
if self.args.should_save:
self._save(output_dir, state_dict=state_dict)
except ValueError:
# 如果出现Value警告,可能是由于stage3_gather_16bit_weights_on_model_save=false导致的
# 这种情况下,将保存完整的检查点,并提示使用zero_to_fp32.py脚本恢复权重
logger.warning(
" stage3_gather_16bit_weights_on_model_save=false. Saving the full checkpoint instead, use"
" zero_to_fp32.py to recover weights"
)
if self.args.should_save:
self._save(output_dir, state_dict={})
# 移除之前的state_dict文件
remove_dummy_checkpoint(self.args.should_save, output_dir, [WEIGHTS_NAME, SAFE_WEIGHTS_NAME])
self.model_wrapped.save_checkpoint(output_dir) # 保存完整的检查点
elif self.args.should_save:
# 如果没有启用特殊功能(如TPU、SageMaker多进程、FSDP或DeepSpeed),直接调用self._save保存模型
self._save(output_dir)
# 如果self.args.push_to_hub为True,且该方法不是由内部调用,则将模型推送到Hugging Face Hub
if self.args.push_to_hub and not _internal_call:
self.push_to_hub(commit_message="Model save")
九、Trainer 中的_save函数
transformers:4.39
def _save(self, output_dir: Optional[str] = None, state_dict=None):
# 如果执行该函数,则意味着当前是进程0,因此无需检查进程编号
output_dir = output_dir if output_dir is not None else self.args.output_dir
# 创建输出目录(如果不存在)
os.makedirs(output_dir, exist_ok=True)
logger.info(f"Saving model checkpoint to {output_dir}")
# 支持的模型类型,如果安装了PEFT,则还支持PeftModel
supported_classes = (PreTrainedModel,) if not is_peft_available() else (PreTrainedModel, PeftModel)
# 使用 save_pretrained 方法保存训练好的模型和配置,以便后续使用 from_pretrained 重新加载
if not isinstance(self.model, supported_classes):
# 如果模型不属于支持的类型,则需要获取模型的state_dict
if state_dict is None:
state_dict = self.model.state_dict()
# 如果模型的内部模型属于支持的类型,则调用内部模型的save_pretrained方法保存
if isinstance(unwrap_model(self.model), supported_classes):
unwrap_model(self.model).save_pretrained(output_dir,state_dict=state_dict, safe_serialization=self.args.save_safetensors
)
else:
# 如果模型及其内部模型都不属于支持的类型,则只保存模型的state_dict
logger.info("Trainer.model is not a `PreTrainedModel`, only saving its state dict.")
if self.args.save_safetensors:
# 如果设置了save_safetensors,则使用safetensors库保存state_dict
safetensors.torch.save_file(
state_dict, os.path.join(output_dir, SAFE_WEIGHTS_NAME), metadata={"format": "pt"}
)
else:
# 否则使用torch.save保存state_dict
torch.save(state_dict, os.path.join(output_dir, WEIGHTS_NAME))
else:
# 如果模型属于支持的类型,则直接调用模型的save_pretrained方法保存
self.model.save_pretrained(
output_dir, state_dict=state_dict, safe_serialization=self.args.save_safetensors
)
# 如果存在tokenizer,则保存tokenizer
if self.tokenizer is not None:
self.tokenizer.save_pretrained(output_dir)
# 将训练参数保存到输出目录,这是一个好的实践,方便后续分析和复现
torch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))
十、Trainer 中的compute_loss函数
transformers:4.39
该代码定义了一个
compute_loss
方法,用于计算模型训练过程中的损失值。具体来说:
- 首先检查是否设置了标签平滑(
label_smoother
)以及输入中是否包含"labels"。如果同时满足,则从输入中弹出"labels"。- 使用输入调用模型,获取模型输出(
outputs
)。- 如果
self.args.past_index >= 0
,则保存模型输出中对应索引处的值,这可能与模型的过去状态(past state
)有关,但需要后续优化该部分代码。- 如果输入中包含"labels":
- 获取模型的名称,判断是否为PEFT模型。
- 如果模型属于因果语言模型类型,则在计算损失时应用标签平滑并进行标签移位(
shift_labels=True
)。- 否则直接应用标签平滑。
- 如果输入中不包含"labels":
- 检查模型输出是否为字典且包含"loss"键。如果不包含,则抛出
ValueError
异常。- 从模型输出中获取损失值,因为有些模型可能返回元组而不是
ModelOutput
对象。- 如果设置了
return_outputs
为True
,则同时返回损失值和模型输出,否则只返回损失值。
def compute_loss(self, model, inputs, return_outputs=False):
"""
计算训练损失的方法,默认情况下,大多数模型会在输出的第一个元素中返回损失值。
可以通过继承并覆盖该方法来自定义损失计算的行为。
"""
if self.label_smoother is not None and "labels" in inputs:
# 如果设置了标签平滑(label smoother)且输入中包含"labels",则将labels从输入中弹出
labels = inputs.pop("labels")
else:
labels = None
outputs = model(**inputs)
# 保存过去的状态(past state),如果存在的话
# TODO: 这部分代码需要在后续进行修复和优化,使其更加清晰
if self.args.past_index >= 0:
self._past = outputs[self.args.past_index]
if labels is not None:
unwrapped_model = unwrap_model(model)
if _is_peft_model(unwrapped_model):
# 如果是PEFT模型,获取基础模型的名称
model_name = unwrapped_model.base_model.model._get_name()
else:
# 否则直接获取模型名称
model_name = unwrapped_model._get_name()
if model_name in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.values():
# 如果模型属于因果语言模型类型,则在计算损失时应用标签平滑并进行标签移位
loss = self.label_smoother(outputs, labels, shift_labels=True)
else:
# 否则直接应用标签平滑
loss = self.label_smoother(outputs, labels)
else:
if isinstance(outputs, dict) and "loss" not in outputs:
# 如果模型输出是一个字典且不包含"loss"键,则抛出ValueError异常
raise ValueError(
"The model did not return a loss from the inputs, only the following keys: "
f"{','.join(outputs.keys())}. For reference, the inputs it received are {','.join(inputs.keys())}."
)
# 从模型输出中获取损失值,因为有些模型可能返回元组而不是ModelOutput
loss = outputs["loss"] if isinstance(outputs, dict) else outputs[0]
# 如果设置了return_outputs为True,则同时返回损失值和模型输出,否则只返回损失值
return (loss, outputs) if return_outputs else loss
下面是一个自定义compute_loss函数的例子做参考, 可以继承trainer后重写compute_loss函数里使用
class CausalLMLoss(Loss):
def __init__(self, ignore_index=-100):
super().__init__()
self.ignore_index = ignore_index
self.loss_fn = nn.CrossEntropyLoss(ignore_index=ignore_index)
# TODO 改,修改官方代码逻辑
def __call__(self, model, inputs, return_outputs=False):
"""
自定义的loss逻辑, 修改 trainer loss 获得
"""
if not ('input_ids' in inputs and 'labels' in inputs):
raise ValueError("input_ids or labels is not in inputs")
outputs = model(**inputs, return_dict=True)
# We don't use .loss here since the model may return tuples instead of ModelOutput.
# 取 logits 与 labels
logits = outputs["logits"] if isinstance(outputs, dict) else outputs[1]
labels = inputs['labels']
# Shift so that tokens < n predict n
# 其中 .float 表示使用32位来计算loss
shift_logits = logits[..., :-1, :].float().contiguous()
shift_labels = labels[..., 1:].contiguous()
# Flatten the tokens
shift_logits = shift_logits.view(-1, shift_logits.size(-1)) # [batch_size, seq_len, vocab_size] ->[batch_size * seq_len, vocab_size]
shift_labels = shift_labels.view(-1) # [batch_size, seq_len] -> [batch_size * seq_len]
# Enable model parallelism
# to. 将label移动到与logits相同的设备上,以支持模型并行
shift_labels = shift_labels.to(shift_logits.device)
loss = self.loss_fn(shift_logits, shift_labels)
return (loss, outputs) if return_outputs else loss
# 上述loss 用法参考
class Trainer(_Trainer):
"""
主要修改逻辑:通过传入compute_loss,支持自定义loss计算方式
"""
def __init__(
self,
model: Union[PreTrainedModel, nn.Module] = None,
args: TrainingArguments = None,
data_collator: Optional[DataCollator] = None,
train_dataset: Optional[Dataset] = None,
eval_dataset: Optional[Dataset] = None,
tokenizer: Optional[PreTrainedTokenizerBase] = None,
model_init: Callable[[], PreTrainedModel] = None,
compute_metrics: Optional[Callable[[EvalPrediction], Dict]] = None,
callbacks: Optional[List[TrainerCallback]] = None,
optimizers: Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None),
preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = None,
compute_loss=None, # 新增 loss 计算逻辑
):
super(Trainer, self).__init__(
model=model,
args=args,
data_collator=data_collator,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
model_init=model_init,
compute_metrics=compute_metrics,
callbacks=callbacks,
optimizers=optimizers,
preprocess_logits_for_metrics=preprocess_logits_for_metrics,
)
self.loss_func = compute_loss # 新增 loss 计算逻辑
def compute_loss(self, model, inputs, return_outputs=False):
"""
重写loss的计算方式
How the loss is computed by Trainer. By default, all models return the loss in the first element.
Subclass and override for custom behavior.
"""
if self.loss_func is None:
# 使用父类 compute_loss
return super().compute_loss(model, inputs, return_outputs)
else:
# 调用自定义
return self.loss_func(model, inputs, return_outputs)
十一、模型加载函数deepspeed_load_checkpoint与_load_from_checkpoint
transformers:4.39
11.1、deepspeed_load_checkpoint 加载deepspeed模型
该函数的主要作用是从指定的检查点路径加载DeepSpeed引擎的模型、优化器和学习率调度器的状态。它首先检查给定的检查点路径是否包含DeepSpeed检查点的目录结构(以"global_step"开头的目录)。
# deepspeed_load_checkpoint函数用于从指定的检查点路径加载DeepSpeed引擎的模型、优化器和学习率调度器的状态
def deepspeed_load_checkpoint(deepspeed_engine, checkpoint_path, load_module_strict=True):
import glob
# 使用glob模块查找checkpoint_path下所有以"global_step"开头的目录
# 并按字母数字顺序对这些目录进行排序
# DeepSpeed在保存检查点时会创建这样的目录结构
deepspeed_checkpoint_dirs = sorted(glob.glob(f"{checkpoint_path}/global_step*"))
# 如果找到了DeepSpeed检查点目录
if len(deepspeed_checkpoint_dirs) > 0:
logger.info(f"Attempting to resume from {checkpoint_path}")
# 调用deepspeed_engine.load_checkpoint方法从指定路径加载检查点
# 这将自动更新deepspeed_engine中的优化器(self.optimizer)和学习率调度器(self.lr_scheduler)的状态
# load_module_strict控制是否严格加载模型权重
# load_optimizer_states和load_lr_scheduler_states分别指定是否加载优化器和学习率调度器的状态
load_path, _ = deepspeed_engine.load_checkpoint(
checkpoint_path,
load_module_strict=load_module_strict,
load_optimizer_states=True,
load_lr_scheduler_states=True,
)
# 如果load_path为None,说明从指定路径加载检查点失败,抛出ValueError
if load_path is None:
raise ValueError(f"[deepspeed] failed to resume from checkpoint {checkpoint_path}")
# 如果没有找到DeepSpeed检查点目录,抛出ValueError
else:
raise ValueError(f"Can't find a valid checkpoint at {checkpoint_path}")
11.2、_load_from_checkpoint 加载除了deepspeed情况的模型
该函数的主要作用是从指定的检查点路径加载模型的权重和配置,支持多种不同的检查点格式,包括:
- 普通格式: 将模型权重、配置等信息存储在多个文件中。
- FSDP (Fully Sharded Data Parallelism) 格式: 用于数据并行训练,将模型权重分散到多个文件夹或文件中,以节省内存。
- PEFT (Parameter-Efficient Transfer Learning) 格式: 用于高效fine-tuning,只需加载适配器权重。
- 分片 (Sharded) 格式: 将模型权重分割成多个分片,分别存储在不同文件中。
# _load_from_checkpoint函数用于从指定的检查点路径加载模型的权重和配置
def _load_from_checkpoint(self, resume_from_checkpoint, model=None):
# 如果没有传入模型实例,则使用self.model作为模型实例
if model is None:
model = self.model
# 构建各种文件路径,包括配置文件、权重文件、适配器权重文件等
# 这些文件存储了模型的配置、权重、适配器权重等信息
config_file = os.path.join(resume_from_checkpoint, CONFIG_NAME)
adapter_weights_file = os.path.join(resume_from_checkpoint, ADAPTER_WEIGHTS_NAME)
adapter_safe_weights_file = os.path.join(resume_from_checkpoint, ADAPTER_SAFE_WEIGHTS_NAME)
weights_file = os.path.join(resume_from_checkpoint, WEIGHTS_NAME)
weights_index_file = os.path.join(resume_from_checkpoint, WEIGHTS_INDEX_NAME)
safe_weights_file = os.path.join(resume_from_checkpoint, SAFE_WEIGHTS_NAME)
safe_weights_index_file = os.path.join(resume_from_checkpoint, SAFE_WEIGHTS_INDEX_NAME)
# 检查检查点是否为FSDP (Fully Sharded Data Parallelism)格式
# FSDP是PyTorch中用于数据并行的一种技术,可以减少内存占用
# 它将模型权重分散到多个GPU上,因此需要特殊的加载方式
is_fsdp_ckpt = os.path.isdir(resume_from_checkpoint) and (
# 检查是否使用了SHARDED_STATE_DICT,即将模型权重分散到多个文件夹中
any(
FSDP_MODEL_NAME in folder_name
for folder_name in os.listdir(resume_from_checkpoint)
if os.path.isdir(os.path.join(resume_from_checkpoint, folder_name))
)
# 检查是否使用了FULL_STATE_DICT,即将整个模型权重存储在一个文件中
or os.path.isfile(os.path.join(resume_from_checkpoint, f"{FSDP_MODEL_NAME}.bin"))
)
# 如果检查点是FSDP格式,但当前环境没有启用FSDP,则抛出ValueError
if is_fsdp_ckpt and not self.is_fsdp_enabled:
raise ValueError(f"Checkpoint found at {resume_from_checkpoint} is only supported when using PyTorch FSDP")
# 检查是否存在有效的检查点文件
# 如果没有找到任何权重文件或适配器权重文件,且也不是FSDP格式,则抛出ValueError
if not (
any(
os.path.isfile(f)
for f in [
weights_file,
safe_weights_file,
weights_index_file,
safe_weights_index_file,
adapter_weights_file,
adapter_safe_weights_file,
]
)
or is_fsdp_ckpt
):
raise ValueError(f"Can't find a valid checkpoint at {resume_from_checkpoint}")
logger.info(f"Loading model from {resume_from_checkpoint}.")
# 如果存在配置文件,则加载配置
# 并检查配置中的Transformers版本是否与当前版本一致,如果不一致则打印警告
if os.path.isfile(config_file):
config = PretrainedConfig.from_json_file(config_file)
checkpoint_version = config.transformers_version
if checkpoint_version is not None and checkpoint_version != __version__:
logger.warning(
f"You are resuming training from a checkpoint trained with {checkpoint_version} of "
f"Transformers but your current version is {__version__}. This is not recommended and could "
"yield to errors or unwanted behaviors."
)
# 如果存在权重文件或者是FSDP格式,则加载模型权重
if os.path.isfile(weights_file) or os.path.isfile(safe_weights_file) or is_fsdp_ckpt:
weights_only_kwarg = {"weights_only": True} if is_torch_greater_or_equal_than_1_13 else {}
# 如果模型在GPU上,它仍然可以工作!
if is_sagemaker_mp_enabled():
# 如果存在"user_content.pt"文件,使用新的SMP API加载检查点
if os.path.isfile(os.path.join(resume_from_checkpoint, "user_content.pt")):
smp.resume_from_checkpoint(
path=resume_from_checkpoint, tag=WEIGHTS_NAME, partial=False, load_optimizer=False
)
# 如果不存在"user_content.pt"文件,使用旧的SMP API加载检查点
else:
state_dict = torch.load(
weights_file,
map_location="cpu",
**weights_only_kwarg,
)
state_dict["_smp_is_partial"] = False
load_result = model.load_state_dict(state_dict, strict=True)
del state_dict
# 如果启用了FSDP,则使用FSDP加载模型
elif self.is_fsdp_enabled:
load_fsdp_model(
self.accelerator.state.fsdp_plugin,
self.accelerator,
model,
resume_from_checkpoint,
**_get_fsdp_ckpt_kwargs(),
)
# 否则,在CPU上加载模型权重,以避免OOM错误
else:
# 如果使用SafeTensors格式,则从safe_weights_file中加载权重
if self.args.save_safetensors and os.path.isfile(safe_weights_file):
state_dict = safetensors.torch.load_file(safe_weights_file, device="cpu")
# 否则从weights_file中加载权重
else:
state_dict = torch.load(
weights_file,
map_location="cpu",
**weights_only_kwarg,
)
# 加载模型权重
load_result = model.load_state_dict(state_dict, False)
del state_dict
self._issue_warnings_after_load(load_result)
# 如果是使用PEFT (Parameter-Efficient Transfer Learning)训练的模型,则加载适配器权重
elif _is_peft_model(model):
if hasattr(model, "active_adapter") and hasattr(model, "load_adapter"):
if os.path.exists(resume_from_checkpoint):
model.load_adapter(resume_from_checkpoint, model.active_adapter, is_trainable=True)
else:
logger.warning(
"The intermediate checkpoints of PEFT may not be saved correctly, "
f"consider using a custom callback to save {ADAPTER_WEIGHTS_NAME} in corresponding saving folders. "
"Check some examples here: https://github.com/huggingface/peft/issues/96"
)
else:
logger.warning("Could not load adapter model, make sure to have `peft>=0.3.0` installed")
# 否则,加载分片检查点 (sharded checkpoint)
else:
load_result = load_sharded_checkpoint(
model, resume_from_checkpoint, strict=is_sagemaker_mp_enabled(), prefer_safe=self.args.save_safetensors
)
if not is_sagemaker_mp_enabled():
self._issue_warnings_after_load(load_result)
十二、huggingface 常用组件
12.1 梯度检查点(PEFTmodel,PreTrainedModel)
查看是否开启了梯度检查点
model.is_gradient_checkpointing -----> bool
开启梯度检查点
# 启用梯度检查点 # 如果启用了梯度检查点,则对模型进行相应设置 # args 是 TrainingArguments的对象 args.gradient_checkpointing_kwargs={'use_reentrant': True} if args.gradient_checkpointing: if args.gradient_checkpointing_kwargs is None: gradient_checkpointing_kwargs = {} else: gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs # 模型也可以自定义重写 gradient_checkpointing_enable()函数用于梯度检查点的层 model.gradient_checkpointing_enable(gradient_checkpointing_kwargs=gradient_checkpointing_kwargs)
12.2 transformer logger、set_seed()
transformer logger with seed
第一种:
# 创建一个HfArgumentParser对象,用于解析命令行参数。它接受一个元组,包含三个自定义的参数类:ModelArguments、DataTrainingArguments和Seq2SeqTrainingArguments。
parser = HfArgumentParser((ModelArguments, DataTrainingArguments, Seq2SeqTrainingArguments))
# 检查命令行参数的数量和格式。如果只传递了一个参数,并且该参数是一个JSON文件,则使用parse_json_file方法从JSON文件中解析出模型参数、数据参数和训练参数。
if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):
model_args, data_args, training_args = parser.parse_json_file(json_file=os.path.abspath(sys.argv[1]))
else:
# 否则,使用parse_args_into_dataclasses方法从命令行参数中解析出这些参数。
model_args, data_args, training_args = parser.parse_args_into_dataclasses()
# 设置 Transformers 日记对象
def set_logger_with_seed(training_args: Seq2SeqTrainingArguments):
import logging,sys
import transformers
from transformers import set_seed
logger = logging.getLogger(__name__) # 7. 创建一个日志记录器,用于记录脚本运行时的日志信息。
# Setup logging
# 10. 设置日志记录的基本配置,包括日志格式、日期格式和输出处理器(在这里是将日志输出到标准输出流)。
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%m/%d/%Y %H:%M:%S",
handlers=[logging.StreamHandler(sys.stdout)],
)
if training_args.should_log:
# The default of training_args.log_level is passive, so we set log level at info here to have that default.
# 11. 如果训练参数指定应该记录日志,则将Transformers库的日志级别设置为info(信息级别)。默认情况下,训练参数的日志级别是被动的,所以这里将其设置为信息级别。
transformers.utils.logging.set_verbosity_info()
# 12. 获取训练参数指定的进程日志级别,并将该级别设置为当前日志记录器的级别。
log_level = training_args.get_process_log_level()
logger.setLevel(log_level)
# datasets.utils.logging.set_verbosity(log_level)
# 13. 设置Transformers库的日志级别为训练参数指定的进程日志级别,启用默认的日志处理器和显式的日志格式。
transformers.utils.logging.set_verbosity(log_level)
transformers.utils.logging.enable_default_handler()
transformers.utils.logging.enable_explicit_format()
# 设置一切相关的随机种子,保证训练结果的可重现性
set_seed(training_args.seed)
return logger
第二种
import sys
from loguru import logger
# 重定义终端显示logger颜色
logger.configure(handlers=[
{
"sink": sys.stderr,
"format": "{time:YYYY-MM-DD HH:mm:ss.SSS} |<cyan><lvl>{level:8}</></>| {name} : {module}:{line:4} | <cyan>mymodule</> | - <lvl>{message}</>",
"colorize": True
},
])
logger.debug('this is debug')
logger.info('this is info')
logger.success('this is success')
logger.warning('this is warning')
logger.error('this is error')
logger.critical('this is critical')
12.3 模型结构、参数、设备等打印
from loguru import logger
# 重定义终端显示logger颜色
logger.configure(handlers=[
{
"sink": sys.stderr,
"format": "{time:YYYY-MM-DD HH:mm:ss.SSS} |<cyan><lvl>{level:8}</></>| {name} : {module}:{line:4} | <cyan>mymodule</> | - <lvl>{message}</>",
"colorize": True
},
])
def verify_model_dtype(model):
"""
功能: 查看模型中各种类型的参数的情况
使用技术: Python 的 defaultdict、model.named_parameters()、参数遍历等
解决问题: 帮助开发者了解模型中参数的数据类型分布,以及可训练参数的情况,从而更好地优化模型
"""
logger.info(f"--> model structure: \n{model}")
ignore_layers = [f"layers.{i}" for i in range(2,21)] # 减少打印的层数
logger.info(f"ignore print layers: \n{ignore_layers}")
for n,v in model.named_parameters():
# 少打印一些层
if not any([i in n for i in ignore_layers]):
if v.requires_grad:
logger.info(f"trainable model arguments: {n} - {v.dtype} - {v.shape} - {v.device}")
else:
logger.info(f"not trainable model arguments: {n} - {v.dtype} - {v.shape} - {v.device}")
dtype2param_num = defaultdict(int) # 每种数据类型的参数量
dtype2param_name = defaultdict(list) # 每种数据类型的参数名称
dtype2trainable_param_num = defaultdict(int) # 每种数据类型参与训练的参数量
dtype2trainable_param_name = defaultdict(list) # 每种数据类型参与训练的参数名称
for name, p in model.named_parameters():
dtype = p.dtype # 获取参数的数据类型
# 统计参数数量和参数名称
dtype2param_num[dtype] += p.numel()
dtype2param_name[dtype].append(name)
# 如果参数参与训练(requires_grad=True),则统计可训练参数的数量和名称
if p.requires_grad:
dtype2trainable_param_num[dtype] += p.numel()
dtype2trainable_param_name[dtype].append(name)
# 统计全部参数中,各种类型参数分布
total = 0
logger.info('verify all params of the model')
for k, v in dtype2param_num.items():
total += v
for k, v in dtype2param_num.items():
print("all params info: {} num: {} {:.3f}%".format(k, v, 100.0 * v / total)) # 打印每种数据类型的参数量和占比
print()
# 统计可训练参数中,各种类型参数分布
logger.info('verify trainable params the model')
total_trainable = 0
for k, v in dtype2trainable_param_num.items():
total_trainable += v
for k, v in dtype2trainable_param_num.items():
print("trainable params info: {} num: {} {:.3f}%".format(k, v, 100.0 * v / total_trainable))
print()
for k, v in dtype2trainable_param_name.items():
print("all params info: {} trainable layers: {}".format(k, v)) # 打印每种数据类型的可训练参数名称
print()
# 查看参与训练的参数情况
total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
logger.info("Total model params: %.2fM" % (total / 1e6))
logger.info(
f'trainable params: {trainable} || all params: {total} || trainable%: {round(trainable / total, 4)}')
logger = ...
if training_args.local_rank == 0: # local 为0时打印
# 打印 TrainingArguments 或 model的config 或 peft model 的 peft_config
logger.info(f"Training/evaluation parameters {training_args}")
logger.info(f"model parameters {model.config}")
logger.info(f"PEFT parameters {peft_config}")
# 用于方便调试, 判断是否使用分布式
if (torch.distributed.is_available() and torch.distributed.is_initialized()):
torch.distributed.barrier() # 进程阻塞同步
12.4 tokenizer 初始化与使用
12.4.1 tokenizer 初始化的常见参数
# 使用AutoTokenizer.from_pretrained方法加载分词器
# 设置填充标记、句首标记、句尾标记和其他特殊标记
tokenizer = AutoTokenizer.from_pretrained(
args.model_name_or_path,
pad_token=special_tokens.pad_token.value, # 填充标记
bos_token=special_tokens.bos_token.value, # 句首标记
eos_token=special_tokens.eos_token.value, # 句尾标记
additional_special_tokens=special_tokens.list(), # 其他特殊标记
trust_remote_code=True,
model_max_length=args.model_max_length,
padding_side="right",
# use_fast=True,
use_fast=False if config.model_type == 'llama' else True
)
tokenizer.chat_template = chat_template # 设置聊天模板
# make embedding resizing configurable?
# 调整tokenizer的嵌入大小,使其能够容纳新增的特殊标记
# 模型重设置词汇表大小,pad_to_multiple_of=8用于对齐,提高GPU计算效率,详情见 https://blog.csdn.net/qq_16555103/article/details/137677561
model.resize_token_embeddings(len(tokenizer), pad_to_multiple_of=8)
更多详情见: 下文中章节2.2
12.4.2 tokenizer 参数及使用方法(encode,encode_plus,batch_encode_plus,decode...)
a) __call__() 方法
# 将 List str ---> {input_ids attention_mask ...}
inputs = self.tokenizer(
batch, # batch str
return_tensors='pt', # 如果为none表示返回为python对象
max_length=self.max_seq_length, # 与truncation共同使用,开启截取时最大长度
truncation=True,
padding='max_length', # (bool 或 str, 可选),是否开启pad或开启时填充的方法,“longest” 表示填充至批次最大长度
add_special_tokens=False # tokenizer 时不使用特殊标记
)
# 除了上述参数,该函数其他的参数如下
return_offsets_mapping (bool, optional, 默认为False) - 是否返回一个将字符映射到其相应的单词中位置的字典。这对于获取每个单词在原始字符串中的确切位置很有用。
return_position_ids (bool, optional, 默认为False) - 是否返回 position ids。通常用于像 BERT这样的模型,这些模型利用 position ids 来区分序列中不同位置的标记。
return_token_type_ids (bool, 可选): 是否返回token类型 ID。在一些任务中,如问答任务,需要区分问题和答案的 token。
return_special_tokens_mask (bool, 可选): 是否返回特殊标记掩码,用于识别序列中的特殊标记。
b) tokenizer. 常用函数与案例
选择搭配 add_special_tokens=False/True 或 skip_special_tokens=False/True 使用
__call__(text, ...)
- 这是tokenizer的主要功能,用于对输入文本进行标记化,生成token id序列。上面提到的参数如add_special_tokens
、padding
、truncation
等都可以在这里设置encode(text, ...)
- 与__call__
类似,也用于对输入文本进行标记化,但返回值略有不同。
- 功能: 对输入文本进行编码,返回token id序列
- 返回值:
List[int]
, 仅返回的token id序列, 不包含attention_mask等
encode_plus(text, ...)
- 在encode
的基础上增加了一些功能,如生成注意力掩码和标记类型id。
- 功能: 在
encode
的基础上,增加了一些用于构建模型输入的特性- 返回值:
BatchEncoding
, 包含以下字段:
input_ids
: token id序列attention_mask
: 注意力掩码,用于区分实际token和padding tokentoken_type_ids
: 标记类型id,用于区分句子边界(仅BERT等模型需要)batch_encode_plus(text_batch, ...)
- 对一批文本进行标记化,返回已padding和截断的编码序列。
- 功能: 对一批文本进行编码,并自动执行padding和截断操作
- 返回值:
BatchEncoding
, 与encode_plus
类似,但对应一批输入decode(ids, ...)
- 将token id序列解码为原始文本。
- 功能: 将token id序列解码为原始文本
- 返回值:
str
, 解码后的文本字符串- tokenize(text, ...)
- 功能: 对输入文本进行tokenize,将其分割成token序列
- 返回值:
List[str]
, 表示token序列,每个元素是一个字符串token总的来说:
- encode只返回token id序列,适合简单的标记化场景
- encode_plus和batch_encode_plus返回模型输入所需的各种编码,用于序列分类、序列到序列等任务
- decode用于将模型输出(token id)解码为可读文本
- tokenizer是最通用和灵活的编码函数,可满足大多数需求
1. encode
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "This is an example sentence."
# 添加特殊tokens
ids = tokenizer.encode(text, add_special_tokens=True)
print(ids)
# 输出: [101, 1188, 1109, 1153, 1719, 1103, 4962, 102]
# 不添加特殊tokens
ids = tokenizer.encode(text, add_special_tokens=False)
print(ids)
# 输出: [1188, 1109, 1153, 1719, 1103, 4962]
2. encode plus
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "This is another example sentence."
encoded = tokenizer.encode_plus(text, max_length=16, padding='max_length', truncation=True, return_tensors='pt')
print(encoded)
# 输出: {'input_ids': tensor([101, 1188, 1109, 1153, 1720, 1119, 4962, 1105,102, 0, 0, 0, 0, 0, 0, 0]), 'token_type_ids': tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 'attention_mask': tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0])}
# 也可以通过 add_special_tokens 控制是否使用 特殊标志,下面的函数同理
3. batch_encode_plus(text_batch, ...)
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text_batch = ["This is a short sentence.", "This is another longer sentence for batch encoding."]
encoded = tokenizer.batch_encode_plus(text_batch, max_length=16, padding=True, truncation=True, return_tensors='pt')
print(encoded)
# 输出: {'input_ids': tensor([[ 101, 1188, 1109, 1153, 1155, 1109, 4962, 102, 0, 0, 0, 0, 0, 0, 0, 0],
# [ 101, 1188, 1109, 1153, 1720, 1119, 4962, 1164, 1112, 1111, 4962, 1111, 102, 0, 0, 0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
# [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]])}
4. decode
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
ids = [101, 1188, 1109, 1153, 1719, 1103, 4962, 102]
text = tokenizer.decode(ids)
print(text)
# 输出: "[CLS] This is an example sentence. [SEP]"
# 我们希望获得原始的文本内容,因此skip_special_tokens=True是比较常用的设置
# 跳过特殊tokens
text = tokenizer.decode(ids, skip_special_tokens=True)
print(text)
# 输出: "This is an example sentence."
5. tokenize(text, ...)
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "This is an example sentence."
tokens = tokenizer.tokenize(text)
print(tokens)
# 输出: ['This', 'is', 'an', 'example', 'sentence', '.']
c) _pad 方法(适合重写)
Hugging Face tokenizer中的
_pad
函数用于对一批编码后的序列进行padding(填充)操作,使它们具有相同的长度,便于批量输入模型进行处理。
def _pad(encoded_inputs, max_length, padding_strategy, pad_to_multiple_of=None, return_tensors=None):
...
参数说明:
encoded_inputs: 一批已编码的序列,是一个nest的list或dict,通常来自batch_encode_plus()的输出。
max_length: 期望的最大长度,所有序列将被padding到这个长度。如果设为None,则使用批次中最长序列的长度。
padding_strategy: 指定padding的策略,可选值包括'longest'、'max_length'和'do_not_pad'。
pad_to_multiple_of: 如果设置了这个值,则所有序列的长度将被padding到该值的最小整数倍数。
return_tensors: 指定返回值的类型,可以是None、'pt'(PyTorch)或'tf'(TensorFlow)。
该函数的主要作用是对输入的编码序列进行padding,使它们具有相同的长度。具体的padding策略由padding_strategy参数控制:
'longest': 使用批次中最长序列的长度进行padding。
'max_length': 使用max_length指定的长度进行padding。
'do_not_pad': 不进行padding操作。
如果设置了pad_to_multiple_of,那么padding后的序列长度将被调整为该值的最小整数倍数。
最后,根据return_tensors参数的设置,函数可以返回PyTorch或TensorFlow张量,也可以返回普通的Python列表或字典。
经过_pad函数处理后,批量输入就可以直接传递给下游的模型进行前向计算了。这个函数极大地方便了批量数据的预处理工作。
十三、TrainingArguments 与Seq2SeqTrainingArguments 常见的参数
具体用法可以看:Chatglm3 代码 中 class FinetuningConfig(object) 类
https://github.com/THUDM/ChatGLM3/blob/main/finetune_demo/finetune_hf.py
13.1 Seq2SeqTrainingArguments 参数
Transformers 版本:4.39
04/07/2022 07:30:32 - INFO - __main__ - Training/evaluation parameters Seq2SeqTrainingArguments(
_n_gpu=1,
accelerator_config={'split_batches': False, 'dispatch_batches': None, 'even_batches': True, 'use_seedable_sampler': True},
adafactor=False,
adam_beta1=0.9,
adam_beta2=0.999,
adam_epsilon=1e-08,
auto_find_batch_size=False,
bf16=False,
bf16_full_eval=False,
data_seed=None,
dataloader_drop_last=False,
dataloader_num_workers=0, # dataloader 进程数
dataloader_persistent_workers=False,
dataloader_pin_memory=True,
dataloader_prefetch_factor=None,
ddp_backend=None,
ddp_broadcast_buffers=None,
ddp_bucket_cap_mb=None,
ddp_find_unused_parameters=None,
ddp_timeout=1800,
debug=[],
deepspeed=None, # deepspeed json config
disable_tqdm=False,
dispatch_batches=None,
do_eval=True, # eval, evaluation_strategy 与 eval_steps 给定时,training_args.do_eval 将会被设置为true
do_predict=False, # predict
do_train=False, # train
eval_accumulation_steps=None,
eval_delay=0,
eval_steps=100,
evaluation_strategy=steps,
fp16=False,
fp16_backend=auto,
fp16_full_eval=False,
fp16_opt_level=O1,
fsdp=[],
fsdp_config={'min_num_params': 0, 'xla': False, 'xla_fsdp_v2': False, 'xla_fsdp_grad_ckpt': False},
fsdp_min_num_params=0,
fsdp_transformer_layer_cls_to_wrap=None,
full_determinism=False,
generation_config=GenerationConfig { # 详细见下文
"max_new_tokens": 512
}
,
generation_max_length=None,
generation_num_beams=None,
gradient_accumulation_steps=2,
gradient_checkpointing=False,
gradient_checkpointing_kwargs=None,
greater_is_better=None,
group_by_length=False,
half_precision_backend=auto,
hub_always_push=False,
hub_model_id=None,
hub_private_repo=False,
hub_strategy=every_save,
hub_token=<HUB_TOKEN>,
ignore_data_skip=False,
include_inputs_for_metrics=False,
include_num_input_tokens_seen=False,
include_tokens_per_second=False,
jit_mode_eval=False,
label_names=None,
label_smoothing_factor=0.0,
learning_rate=5e-05,
length_column_name=length,
load_best_model_at_end=False,
local_rank=0,
log_level=info,
log_level_replica=warning,
log_on_each_node=True,
logging_dir=/workspace/output/runs/Apr07_07-30-11_a22064c1308d,
logging_first_step=False,
logging_nan_inf_filter=True,
logging_steps=10,
logging_strategy=steps,
lr_scheduler_kwargs={},
lr_scheduler_type=linear,
max_grad_norm=1.0,
max_steps=200,
metric_for_best_model=None,
mp_parameters=,
neftune_noise_alpha=None,
no_cuda=False,
num_train_epochs=3.0,
optim=adamw_torch,
optim_args=None,
optim_target_modules=None,
output_dir=/workspace/output,
overwrite_output_dir=False,
past_index=-1,
per_device_eval_batch_size=16,
per_device_train_batch_size=4,
predict_with_generate=True, # 设置为 True,则调用evaluate或predict时使用模型的 generate 方法进行生成式预测;否则直接调用模型的 forward 方法进行前向计算。
prediction_loss_only=False, # 设置为 False,则在调用evaluate时只进行计算loss,计算loss的过程与训练时相同,不需要generate推理
push_to_hub=False,
push_to_hub_model_id=None,
push_to_hub_organization=None,
push_to_hub_token=<PUSH_TO_HUB_TOKEN>,
ray_scope=last,
remove_unused_columns=False,
report_to=[],
resume_from_checkpoint=None,
run_name=/workspace/output,
save_on_each_node=False,
save_only_model=False,
save_safetensors=False,
save_steps=50,
save_strategy=steps,
save_total_limit=10,
seed=42,
skip_memory_metrics=True,
sortish_sampler=False,
split_batches=None,
tf32=None,
torch_compile=False,
torch_compile_backend=None,
torch_compile_mode=None,
torchdynamo=None,
tpu_metrics_debug=False,
tpu_num_cores=None,
use_cpu=False,
use_ipex=False,
use_legacy_prediction_loop=False,
use_mps_device=False,
warmup_ratio=0.0,
warmup_steps=0,
weight_decay=0.0,
)
13.2 Seq2SeqTrainingArguments 参数中GenerationConfig的常见参数(用于 generate 推理函数)
覆盖权限:.generation_config > model.generation_config > default GenerationConfig.
GenerationConfig {
bad_words_ids:None
begin_suppress_tokens:None
bos_token_id:None
cache_implementation:None
constraints:None
decoder_start_token_id:None
diversity_penalty:0.0
do_sample:False # 是否做采样
early_stopping:False
encoder_no_repeat_ngram_size:0
encoder_repetition_penalty:1.0
eos_token_id:[2, 64795, 64797] # 结束符列表,可以傳入多个id的list
epsilon_cutoff:0.0
eta_cutoff:0.0
exponential_decay_length_penalty:None
force_words_ids:None
forced_bos_token_id:None
forced_decoder_ids:None
forced_eos_token_id:None
generation_kwargs:{}
guidance_scale:None
length_penalty:1.0 # 长度惩罚
low_memory:None
max_length:20
max_matching_ngram_size:None
max_new_tokens:512 # 生成文本的最大新 token 数
max_time:None
min_length:0
min_new_tokens:None # 同上
no_repeat_ngram_size:0
num_assistant_tokens:5
num_assistant_tokens_schedule:'heuristic'
num_beam_groups:1
num_beams:1 # beam search
num_return_sequences:1
output_attentions:False
output_hidden_states:False
output_logits:None
output_scores:False
pad_token_id:0 # pad id
penalty_alpha:None
prompt_lookup_num_tokens:None
remove_invalid_values:False
renormalize_logits:False
repetition_penalty:1.0 # 重复度惩罚
return_dict_in_generate:False
sequence_bias:None
suppress_tokens:None
temperature:1.0 # 温度系数,越高模型生成越具有想象力,越低生成内容越固定
top_k:50 # top k
top_p:1.0 # top p
transformers_version:'4.39.3'
typical_p:1.0
use_cache:True # True 表示 generate推理时保存 k v 历史缓存
}
十四、Seq2SeqTrainer 类
这段代码主要定义了
Seq2SeqTrainer
类的以下几个核心方法:
prediction_step
方法是执行预测的核心。如果self.args.predict_with_generate
被设置为 True,则使用模型的generate
方法进行生成式预测;否则直接调用模型的forward
方法进行前向计算。该方法返回预测损失、生成的 token 和标签(如果存在)。evaluate
方法用于在评估数据集上进行评估。它会根据self.args.generation_max_length
和self.args.generation_num_beams
等参数设置生成配置,然后调用prediction_step
进行预测并计算相关指标。predict
方法与evaluate
方法类似,但是在测试数据集上进行预测,并返回预测结果和可能的指标。_pad_tensors_to_max_len
是一个辅助方法,用于将张量填充到指定的最大长度。它首先尝试从标记器(tokenizer)或模型配置中获取填充 token ID,如果两者都没有指定,则抛出 ValueError。
# Seq2SeqTrainer 类继承自 Hugging Face Transformers 库中的 Trainer 类
# 它专门用于训练序列到序列(Sequence-to-Sequence, Seq2Seq)任务的模型,如机器翻译、文本摘要等
class Seq2SeqTrainer(Trainer):
def __init__(
self,
model: Union["PreTrainedModel", nn.Module] = None,
# model 参数可以传入一个预训练的 Transformers 模型实例或一个 PyTorch 模型模块
# 如果为 None,则在后续需要初始化模型
args: "TrainingArguments" = None,
# args 参数接受一个 TrainingArguments 实例,其中包含了训练相关的参数配置,如批次大小、学习率等
data_collator: Optional["DataCollator"] = None,
# data_collator 参数可以传入一个数据收集器(DataCollator)实例,用于在训练时合并样本数据
train_dataset: Optional[Dataset] = None,
# train_dataset 参数可以传入一个 Dataset 实例作为训练数据集
eval_dataset: Optional[Union[Dataset, Dict[str, Dataset]]] = None,
# eval_dataset 参数可以传入一个 Dataset 实例或包含数据集的字典作为评估数据集
tokenizer: Optional["PreTrainedTokenizerBase"] = None,
# tokenizer 参数可以传入一个预训练的标记器(tokenizer)实例,用于将文本转换为模型可接受的输入
model_init: Optional[Callable[[], "PreTrainedModel"]] = None,
# model_init 参数可以传入一个可调用对象,用于初始化预训练模型
compute_metrics: Optional[Callable[["EvalPrediction"], Dict]] = None,
# compute_metrics 参数可以传入一个可调用对象,用于计算评估指标
callbacks: Optional[List["TrainerCallback"]] = None,
# callbacks 参数可以传入一个训练回调函数列表,用于在训练过程中执行自定义操作
optimizers: Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None),
# optimizers 参数是一个元组,包含优化器(Optimizer)和学习率调度器(LambdaLR)实例
preprocess_logits_for_metrics: Optional[Callable[[torch.Tensor, torch.Tensor], torch.Tensor]] = None,
# preprocess_logits_for_metrics 参数可以传入一个可调用对象,用于预处理 logits 以计算指标
):
# 调用父类 Trainer 的构造函数,传入相应的参数
super().__init__(
model=model,
args=args,
data_collator=data_collator,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
model_init=model_init,
compute_metrics=compute_metrics,
callbacks=callbacks,
optimizers=optimizers,
preprocess_logits_for_metrics=preprocess_logits_for_metrics,
)
# 如果在 args.generation_config 中指定了 GenerationConfig,则使用它覆盖模型的默认 generation_config
# GenerationConfig 包含了一些生成(generation)相关的参数配置,如 max_length、num_beams 等
# 优先级是: args.generation_config > model.generation_config > 默认 GenerationConfig
if self.args.generation_config is not None:
gen_config = self.load_generation_config(self.args.generation_config)
self.model.generation_config = gen_config
@staticmethod
def load_generation_config(gen_config_arg: Union[str, GenerationConfig]) -> GenerationConfig:
"""
根据 `Seq2SeqTrainingArguments.generation_config` 参数加载 `~generation.GenerationConfig`。
Args:
gen_config_arg (`str` 或 [`~generation.GenerationConfig`]):
`Seq2SeqTrainingArguments.generation_config` 参数的值,可以是一个字符串(表示文件路径或预训练模型名称)或一个 GenerationConfig 对象。
Returns:
`~generation.GenerationConfig` 对象。
"""
# 如果 gen_config_arg 是一个 GenerationConfig 对象,则直接返回它的深拷贝
if isinstance(gen_config_arg, GenerationConfig):
gen_config = deepcopy(gen_config_arg)
else:
# 如果 gen_config_arg 是一个字符串
pretrained_model_name = Path(gen_config_arg) if isinstance(gen_config_arg, str) else gen_config_arg
config_file_name = None
# 根据 pretrained_model_name 的类型确定 config_file_name
# 如果 pretrained_model_name 是一个文件路径,则 config_file_name 是文件名
if pretrained_model_name.is_file():
config_file_name = pretrained_model_name.name
pretrained_model_name = pretrained_model_name.parent
# 如果 pretrained_model_name 是一个目录路径,则不需要设置 config_file_name
elif pretrained_model_name.is_dir():
pass
# 否则,认为 pretrained_model_name 是一个预训练模型名称或 URL
else:
pretrained_model_name = gen_config_arg
# 从指定的预训练模型或配置文件加载 GenerationConfig
gen_config = GenerationConfig.from_pretrained(pretrained_model_name, config_file_name)
# 对加载的 GenerationConfig 进行严格验证,如果有警告或异常则抛出 ValueError
# 这是为了在训练开始前尽早发现问题
try:
with warnings.catch_warnings(record=True) as caught_warnings:
gen_config.validate()
if len(caught_warnings) > 0:
raise ValueError(str([w.message for w in caught_warnings]))
except ValueError as exc:
raise ValueError(
"加载的 GenerationConfig 无效 -- `GenerationConfig.validate()` 抛出警告和/或异常。"
"修复这些问题以训练您的模型。\n\n验证过程中抛出的异常:\n" + str(exc)
)
return gen_config
def evaluate(
self,
eval_dataset: Optional[Dataset] = None,
# eval_dataset 参数可以传入一个 Dataset 实例作为评估数据集,如果为 None 则使用初始化时传入的 eval_dataset
ignore_keys: Optional[List[str]] = None,
# ignore_keys 参数是一个字符串列表,指定在计算指标时要忽略的键(通常是模型输出中不需要的部分)
metric_key_prefix: str = "eval",
# metric_key_prefix 参数指定计算得到的指标键的前缀,默认为 "eval"
**gen_kwargs,
# gen_kwargs 收集其他与生成(generation)相关的参数,如 max_length、num_beams 等
) -> Dict[str, float]:
"""
运行评估并返回指标。
需要在初始化时通过 `compute_metrics` 参数提供一个用于计算指标的函数,因为指标计算是任务特定的。
您也可以继承并覆盖此方法以注入自定义行为。
Args:
eval_dataset (`Dataset`, *可选*):
如果希望覆盖 `self.eval_dataset`,可以传递一个数据集。如果是 `~datasets.Dataset`,
那些不被 `model.forward()` 方法接受的列将自动被移除。它必须实现 `__len__` 方法。
ignore_keys (`List[str]`, *可选*):
在收集预测结果时要忽略的模型输出中键的列表。
metric_key_prefix (`str`, *可选*, 默认为 `"eval"`):
指标键的可选前缀。例如,如果前缀是 `"eval"`(默认值),则指标 "bleu" 将命名为 "eval_bleu"。
max_length (`int`, *可选*):
在使用 generate 方法进行预测时要使用的最大目标长度。
num_beams (`int`, *可选*):
在使用 generate 方法进行预测时要使用的 beam search 的 beam 数量。1 表示不使用 beam search。
gen_kwargs:
其他 `generate` 相关的参数。
Returns:
一个包含评估损失和可能从预测中计算出的指标的字典。
该字典还包含来自训练状态的 epoch 数。
"""
# 创建 gen_kwargs 副本,避免修改原始字典
gen_kwargs = gen_kwargs.copy()
# 如果没有显式传递 max_length 和 max_new_tokens 选项,并且这些参数在训练参数中设置了,则使用训练参数中的设置
if (
gen_kwargs.get("max_length") is None
and gen_kwargs.get("max_new_tokens") is None
and self.args.generation_max_length is not None
):
gen_kwargs["max_length"] = self.args.generation_max_length
if gen_kwargs.get("num_beams") is None and self.args.generation_num_beams is not None:
gen_kwargs["num_beams"] = self.args.generation_num_beams
# 设置 gather_function 为 accelerator.gather,用于在分布式训练时收集预测结果
self.gather_function = self.accelerator.gather
# 缓存 gen_kwargs,以便在 prediction_step 中使用
self._gen_kwargs = gen_kwargs
# 调用父类的 evaluate 方法进行评估
return super().evaluate(eval_dataset, ignore_keys=ignore_keys, metric_key_prefix=metric_key_prefix)
def predict(
self,
test_dataset: Dataset,
# test_dataset 参数是一个 Dataset 实例,表示要进行预测的测试数据集
ignore_keys: Optional[List[str]] = None,
# ignore_keys 参数是一个字符串列表,指定在收集预测结果时要忽略的键
metric_key_prefix: str = "test",
# metric_key_prefix 参数指定计算得到的指标键的前缀,默认为 "test"
**gen_kwargs,
# gen_kwargs 收集其他与生成(generation)相关的参数
) -> "PredictionOutput":
"""
运行预测并返回预测结果和可能的指标。
根据数据集和用例,您的测试数据集可能包含标签。在这种情况下,此方法也将返回指标,就像在 `evaluate()` 中一样。
Args:
test_dataset (`Dataset`):
要进行预测的数据集。如果是 `~datasets.Dataset`,那些不被 `model.forward()` 方法接受的列将自动被移除。
必须实现 `__len__` 方法。
ignore_keys (`List[str]`, *可选*):
在收集预测结果时要忽略的模型输出中键的列表。
metric_key_prefix (`str`, *可选*, 默认为 `"eval"`):
指标键的可选前缀。例如,如果前缀是 `"eval"`(默认值),则指标 "bleu" 将命名为 "eval_bleu"。
max_length (`int`, *可选*):
在使用 generate 方法进行预测时要使用的最大目标长度。
num_beams (`int`, *可选*):
在使用 generate 方法进行预测时要使用的 beam search 的 beam 数量。1 表示不使用 beam search。
gen_kwargs:
其他 `generate` 相关的参数。
<Tip>
如果您的预测或标签具有不同的序列长度(例如,因为您在标记分类任务中执行动态填充),则预测将被填充(在右侧)以允许连接成一个数组。填充索引为 -100。
</Tip>
Returns: *NamedTuple* 具有以下键的命名元组:
- predictions (`np.ndarray`): 对 `test_dataset` 的预测。
- label_ids (`np.ndarray`, *可选*): 标签(如果数据集包含标签)。
- metrics (`Dict[str, float]`, *可选*): 可能的指标字典(如果数据集包含标签)。
"""
# 创建 gen_kwargs 副本,避免修改原始字典
gen_kwargs = gen_kwargs.copy()
# 如果没有显式传递 max_length 和 max_new_tokens 选项,并且这些参数在训练参数中设置了,则使用训练参数中的设置
if (
gen_kwargs.get("max_length") is None
and gen_kwargs.get("max_new_tokens") is None
and self.args.generation_max_length is not None
):
gen_kwargs["max_length"] = self.args.generation_max_length
if gen_kwargs.get("num_beams") is None and self.args.generation_num_beams is not None:
gen_kwargs["num_beams"] = self.args.generation_num_beams
# 设置 gather_function 为 accelerator.gather,用于在分布式训练时收集预测结果
self.gather_function = self.accelerator.gather
# 缓存 gen_kwargs,以便在 prediction_step 中使用
self._gen_kwargs = gen_kwargs
# 调用父类的 predict 方法进行预测
return super().predict(test_dataset, ignore_keys=ignore_keys, metric_key_prefix=metric_key_prefix)
def prediction_step(
self,
model: nn.Module,
# model 参数是一个 PyTorch 模型模块,用于进行预测
inputs: Dict[str, Union[np.ndarray, torch.Tensor, torch.LongTensor]],
# inputs 参数是一个字典,包含了模型输入的各种数据,如输入 id、掩码等,可以是 NumPy 数组或 PyTorch 张量
**gen_kwargs,
# gen_kwargs 收集其他与生成(generation)相关的参数
) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]]:
"""
执行一个预测前向步骤,可能会生成辅助预测。
Args:
model (`nn.Module`):
用于进行预测的模型模块。
inputs (`Dict`):
字典,包含模型输入。
- **inputs (`torch.Tensor`)** -- 输入 ids
- **attention_mask (`torch.Tensor` 或 `None`)** -- 注意力掩码 ids,如果输入长度是静态的则可以为 None
- **token_type_ids (`torch.Tensor` 或 `None`)** -- 标记类型 ids,如果未使用则可以为 None
gen_kwargs:
其他 `generate` 相关的参数。
返回:
预测损失作为可选项返回,生成的token 和创建的标签作为可选项返回,
这取决于 `self.args.prediction_loss_only` 标志。
"""
# 如果 self.args.predict_with_generate 被设置为 True
# 则使用 self.model.generate 方法进行预测
if self.args.predict_with_generate:
# 获取输入数据中的一些键
input_ids = inputs.get("input_ids")
attention_mask = inputs.get("attention_mask", None)
# 如果模型需要 token_type_ids
token_type_ids = inputs.get("token_type_ids", None)
# 设置编码器输出,以便可能在解码器输入中使用
encoder = self.model.get_encoder()
encoder_outputs = encoder(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
# 准备解码器输入 ids
if self.tokenizer is not None:
# 获取解码器起始标记 id
decoder_start_token_id = self.tokenizer.pad_token_id
# 如果使用 past_key_values,则直接在 encoder_outputs 上调用 generate
has_past = "past_key_values" in gen_kwargs and gen_kwargs["past_key_values"] is not None
# 初始化 decoder_inputs 为解码器起始令牌,重复填充到批次大小
decoder_inputs = torch.tensor(
[decoder_start_token_id] * inputs.shape[0],
dtype=torch.long,
device=inputs.device,
)
if has_past:
# 如果使用 past_key_values,则不需要上下文输入
decoder_inputs = None
else:
raise ValueError(
"您必须在初始化 Seq2SeqTrainer 时传递一个 tokenizer,以使用 `predict_with_generate`。"
)
# 获取用于生成的参数
gen_kwargs = self._gen_kwargs(gen_kwargs)
# 更新 `gen_kwargs` 以包含最新的 encoder_outputs
gen_kwargs.update(
{
"encoder_outputs": encoder_outputs,
"decoder_attention_mask": None, # 由模型处理
}
)
# 如果使用 past_key_values,则构建所需的 past_key_values
if "past_key_values" in gen_kwargs and gen_kwargs["past_key_values"] is not None:
gen_kwargs = self._prepare_encoder_decoder_kwargs_for_generation(
inputs, gen_kwargs=gen_kwargs
)
# 如果使用监督模式,则准备监督解码器输入参数
if gen_kwargs.get("use_cached_inputs", False):
gen_kwargs.update(
{
"decoder_input_ids": inputs["labels"],
"use_cached_inputs": True,
}
)
# 更新 `gen_kwargs` 以包含延迟解码器输入 ids
if decoder_inputs is not None:
gen_kwargs["decoder_input_ids"] = decoder_inputs
# 使用 generate 方法生成标记
generated_tokens = self.model.generate(**gen_kwargs)
# 临时解决方案,确保每次评估循环迭代时不会初始化 generation_config
# TODO: 在删除初始化 generation_config 的旧代码后,删除此临时解决方案
# https://github.com/huggingface/transformers/blob/98d88b23f54e5a23e741833f1e973fdf600cc2c5/src/transformers/generation/utils.py#L1183
if self.model.generation_config._from_model_config:
self.model.generation_config._from_model_config = False
# 从模型获取 GenerationConfig
gen_config = self.model.generation_config
# 如果批次长度小于最大长度,则输出应该被填充
if generated_tokens.shape[-1] < gen_config.max_length:
generated_tokens = self._pad_tensors_to_max_len(
generated_tokens, gen_config.max_length
)
elif (
gen_config.max_new_tokens is not None
and generated_tokens.shape[-1] < gen_config.max_new_tokens + 1
):
generated_tokens = self._pad_tensors_to_max_len(
generated_tokens, gen_config.max_new_tokens + 1
)
with torch.no_grad():
if inputs.get("labels", None) is not None:
has_labels = True
with self.compute_loss_context_manager():
outputs = model(**inputs)
if self.label_smoother is not None:
loss = self.label_smoother(
outputs, inputs["labels"]
).mean().detach()
else:
loss = (
outputs["loss"]
if isinstance(outputs, dict)
else outputs[0]
).mean().detach()
else:
loss = None
has_labels = False
if self.args.prediction_loss_only:
return loss, None, None
if has_labels:
labels = inputs["labels"]
if labels.shape[-1] < gen_config.max_length:
labels = self._pad_tensors_to_max_len(
labels, gen_config.max_length
)
elif (
gen_config.max_new_tokens is not None
and labels.shape[-1] < gen_config.max_new_tokens + 1
):
labels = self._pad_tensors_to_max_len(
labels, gen_config.max_new_tokens + 1
)
else:
labels = None
return loss, generated_tokens, labels
else:
# 否则,直接调用 `model.forward` 进行前向计算
with torch.no_grad():
with self.compute_loss_context_manager():
outputs = model(**inputs)
if self.label_smoother is not None:
loss = self.label_smoother(
outputs, inputs["labels"]
).mean().detach()
else:
loss = (
outputs["loss"] if isinstance(outputs, dict) else outputs[0]
).mean().detach()
return loss, None, None
def _pad_tensors_to_max_len(self, tensor, max_length):
"""
填充张量到最大长度。如果 `tokenizer` 存在并带有 `pad_token_id`属性,
则使用该令牌进行填充。否则,从模型配置中获取 `pad_token_id`。
如果两者都没有指定 `pad_token_id`,则抛出 ValueError。
Args:
tensor (`torch.Tensor`): 要填充的张量。
max_length (`int`): 填充张量的目标长度。
Returns:
`torch.Tensor`: 填充到 max_length 长度的张量。
"""
# 获取填充 token ID
if self.tokenizer is not None and hasattr(self.tokenizer, "pad_token_id"):
pad_token_id = (
self.tokenizer.pad_token_id
if self.tokenizer.pad_token_id is not None
else self.tokenizer.eos_token_id
)
else:
if self.model.config.pad_token_id is not None:
pad_token_id = self.model.config.pad_token_id
else:
raise ValueError(
"模型配置中必须设置 pad_token_id,以便对张量进行填充"
)
# 创建填充张量并将输入张量复制到其中
padded_tensor = pad_token_id * torch.ones(
(tensor.shape[0], max_length), dtype=tensor.dtype, device=tensor.device
)
padded_tensor[:, : tensor.shape[-1]] = tensor
return padded_tensor
待更新..............