训练模型
这段代码的主要功能是 构建一个用于序列标注任务的模型,尤其是针对 命名实体识别 (NER, Named Entity Recognition) 的任务。通过利用 BERT 模型 和 Transformers 库 提供的工具,快速构建一个可用于标注每个 token 的实体标签的分类器。
构建模型
具体功能
-
AutoModelForTokenClassification 使用:
- 通过
AutoModelForTokenClassification.from_pretrained()
方法直接加载预训练的 BERT 模型,并传入标签映射(id2label
和label2id
)来创建模型。这样可以快速实现一个基于预训练 BERT 的 token 分类模型。
- 通过
-
手工构建模型:
- 继承
BertPreTrainedModel
类并手动定义模型架构,从而更灵活地调整模型的结构。 - 使用
BertModel
来提取 token 的语义表示。 - 添加
Dropout
层来防止过拟合。 - 使用一个 线性分类器(
Linear
层)将 BERT 模型的输出映射到实体标签空间,输出每个 token 对应的标签概率。
- 继承
-
模型前向传播 (forward):
- 输入数据传入模型时,首先通过 BERT 模型获取每个 token 的表示(向量),然后通过 Dropout 层进行处理,再将处理后的输出通过一个线性分类器(全连接层)进行标注分类。
-
模型输出验证:
- 对于一个 batch 的输入数据,模型输出的尺寸为
[batch_size, sequence_length, num_labels]
,其中num_labels
是标签类别的数量(例如 7 种实体标签),此处模型输出的维度符合预期。
- 对于一个 batch 的输入数据,模型输出的尺寸为
具体代码
快速模型构建(AutoModelForTokenClassification)
from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
model_checkpoint,
id2label=id2label,
label2id=label2id,
)
- 使用
AutoModelForTokenClassification
直接构建一个基于 BERT 的序列标注模型。 - 传入预训练模型检查点
model_checkpoint
和标签映射id2label
、label2id
,快速创建模型。 - 这种方法简单、快速,但灵活性不足,不能自定义模型细节。
手工构建自定义模型(BertForNER)
from torch import nn
from transformers import AutoConfig, BertPreTrainedModel, BertModel
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using {device} device')
class BertForNER(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.bert = BertModel(config, add_pooling_layer=False)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(768, len(id2label)) # 线性分类器,768 为 BERT 输出维度
self.post_init()
def forward(self, x):
bert_output = self.bert(**x)
sequence_output = bert_output.last_hidden_state
sequence_output = self.dropout(sequence_output)
logits = self.classifier(sequence_output) # 输出每个 token 的分类结果
return logits
config = AutoConfig.from_pretrained(checkpoint)
model = BertForNER.from_pretrained(checkpoint, config=config).to(device)
print(model)
导入的模块和类
AutoConfig (from transformers import AutoConfig
):
AutoConfig
是 Hugging Face 的一个工具,专门用来加载预训练模型的配置。配置文件包含了模型的参数、结构等信息,通常在初始化模型时使用。
BertPreTrainedModel (from transformers import BertPreTrainedModel
):
BertPreTrainedModel
是 Hugging Face 库中的一个基类,它为所有基于 BERT 的模型提供了预训练模型的功能(如加载预训练权重、保存模型等)。这个基类通常用于继承,以便创建一个自定义的 BERT 模型。
BertModel (from transformers import BertModel
):
BertModel
是 Hugging Face 库中的 BERT 模型实现。它是预训练的 BERT 模型的基础,提供了对输入文本进行编码的能力,生成每个 token 的表示(即隐藏层输出)。BertModel
本身没有做下游任务(如分类),而是用于提取文本特征(如在这个例子中用于序列标注任务)。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
判断当前环境中是否有可用的 GPU(通过 CUDA)来加速计算,如果有,则将计算设备设为 GPU (cuda
),如果没有 GPU,则使用 CPU 进行计算。
具体来说:
-
torch.cuda.is_available()
:这是 PyTorch 提供的一个方法,用来检查当前是否有可用的 GPU。如果有可用的 GPU,它返回True
,否则返回False
。 -
device = 'cuda' if torch.cuda.is_available() else 'cpu'
:这行代码使用 Python 的条件表达式(或三元表达式),根据torch.cuda.is_available()
的返回值来决定使用'cuda'
(GPU)还是'cpu'
(CPU)。如果系统上有 GPU 可用,则device
变量将被赋值为'cuda'
,否则为'cpu'
。
作用
- 动态选择设备:根据硬件配置(是否有 GPU)自动选择计算设备,确保在有 GPU 时利用 GPU 加速运算,在没有 GPU 时仍然可以使用 CPU 运行模型。
- 代码兼容性:使得模型代码在不同的计算环境中(有无 GPU)都能正常运行,无需修改代码,只需选择合适的设备。
定义类 BertForNER
BertForNER
是一个自定义的 命名实体识别 (NER) 模型类,继承了 BertPreTrainedModel
。它基于预训练的 BERT 模型构建,并在其顶部添加了用于分类的线性层,能够对每个 token 进行分类。
__init__(self, config)
:初始化模型结构,定义模型的组成部分。forward(self, x)
:定义模型的前向传播逻辑,指定输入如何通过模型的各部分计算输出。
初始化方法:__init__(self, config)
功能
初始化模型的各个部分,主要包括加载 BERT 模型、添加 Dropout 层和线性分类器。
def __init__(self, config):
super().__init__(config)
super().__init__(config)
:- 调用父类
BertPreTrainedModel
的初始化方法。 - 加载预训练的 BERT 模型权重和配置文件。
config
包含模型的所有超参数(例如隐藏层大小、分类标签数等)。
- 调用父类
self.bert = BertModel(config, add_pooling_layer=False)
self.bert
:- 加载 BERT 模型部分,用于生成每个 token 的语义表示。
add_pooling_layer=False
:- 关闭池化层(默认 BERT 会在最后一层添加池化层用于句子级别任务)。
- 在序列标注任务中(如 NER),需要对每个 token 进行分类,因此不需要句子级别的池化操作。
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.dropout
:- 添加 Dropout 层,防止模型过拟合。
- Dropout 的概率由
config.hidden_dropout_prob
控制,通常为 0.1 或其他小值。
self.classifier = nn.Linear(768, len(id2label))
self.classifier
:- 定义一个全连接层(线性分类器)。
- 输入维度:
768
(BERT 的隐藏层维度)。 - 输出维度:
len(id2label)
(标签类别数,例如 7 个标签:O
,B-LOC
,I-LOC
, 等)。 - 作用:将 BERT 输出的每个 token 的向量映射到分类标签空间。
self.post_init()
self.post_init()
:- 继承自
BertPreTrainedModel
的方法,用于进一步初始化(如加载预训练权重)。 - 在 Hugging Face 的实现中,这是一个可选的扩展。
- 继承自
前向传播方法:forward(self, x)
功能
定义模型的前向传播过程,具体实现输入如何经过模型处理后输出。
bert_output = self.bert(**x)
bert_output
:
- 将输入
x
传递给self.bert
(即 BERT 模型部分)。 - 输入
x
是一个字典,包含 BERT 所需的输入(如input_ids
,attention_mask
,token_type_ids
)。 - 输出:
bert_output.last_hidden_state
:每个 token 的语义表示(shape:[batch_size, seq_len, hidden_dim]
,其中hidden_dim=768
)。bert_output.pooler_output
(可选):句子级别表示(这里被禁用)。
sequence_output = bert_output.last_hidden_state
sequence_output
:
- 提取
last_hidden_state
,表示 BERT 对每个 token 的上下文语义表示(shape:[batch_size, seq_len, 768]
)。
sequence_output = self.dropout(sequence_output)
self.dropout(sequence_output)
:
- 对 BERT 的输出添加 Dropout 层,随机置零部分神经元,防止过拟合。
logits = self.classifier(sequence_output)
logits
:
- 将 Dropout 后的语义表示传入全连接分类器,输出每个 token 的分类 logits(shape:
[batch_size, seq_len, num_labels]
)。 num_labels
是标签类别数(例如 7)。- logits 是未归一化的分数,可以通过 Softmax 转换为概率。
return logits
- 返回 logits,表示每个 token 对应每个标签的分类结果。
- 输出的 shape 是
[batch_size, seq_len, num_labels]
。
总结
__init__(self, config)
的功能
- 加载预训练的 BERT 模型。
- 添加 Dropout 层,防止过拟合。
- 定义线性分类器,将 BERT 的输出映射到标签类别。
forward(self, x)
的功能
- 输入经过 BERT 提取每个 token 的语义表示。
- 通过 Dropout 层处理,防止过拟合。
- 使用线性分类器将语义表示映射到实体标签类别。
- 输出 logits,用于表示每个 token 的分类结果。
类的整体功能
BertForNER
是一个基于 BERT 的命名实体识别模型,用于对输入序列中的每个 token 进行分类,输出每个 token 的实体标签类别概率。
加载预训练的 BERT 模型配置,构建自定义的 NER 模型,并将其移动到指定的计算设备(如 GPU 或 CPU)
加载预训练模型配置
config = AutoConfig.from_pretrained(checkpoint)
- 功能:
- 使用 Hugging Face 的
AutoConfig
类从指定的预训练模型检查点 (checkpoint
) 中加载模型的配置文件。 checkpoint
是预训练模型的名称或路径,例如"bert-base-chinese"
或本地的模型文件夹路径。- 配置文件包含模型结构和参数的信息(如隐藏层大小、标签类别数、Dropout 概率等),但不包括模型的权重。
- 使用 Hugging Face 的
- 输出:
config
是一个包含模型配置的对象。- 例如,
config
中可能包含: - {
"hidden_size": 768, # BERT 隐藏层维度
"num_hidden_layers": 12, # BERT 的 Transformer 层数
"num_attention_heads": 12, # 自注意力头的数量
"hidden_dropout_prob": 0.1, # Dropout 概率
...
}
加载预训练权重,并构建自定义模型
model = BertForNER.from_pretrained(checkpoint, config=config).to(device)
-
BertForNER.from_pretrained()
:- 使用自定义的
BertForNER
类加载预训练模型的权重(from_pretrained
方法)。 - 通过
checkpoint
提供的路径加载权重,并将权重与模型的结构(由config
定义)匹配。 - 此方法会加载预训练模型的参数(如 BERT 的词嵌入和 Transformer 层的权重),然后应用到自定义的 NER 模型中。
- 使用自定义的
-
config=config
:- 明确指定模型使用的配置对象
config
。 - 这一步确保加载的权重与模型结构兼容。
- 明确指定模型使用的配置对象
-
.to(device)
:- 将模型移动到指定的设备(如 GPU 或 CPU)。
device
的值取决于前面的代码: - device = 'cuda' if torch.cuda.is_available() else 'cpu'
- 如果有可用的 GPU,模型会加载到 GPU 上;否则,加载到 CPU 上。
- 将模型移动到指定的设备(如 GPU 或 CPU)。
-
输出:
model
是一个完整的 BERT 模型,经过自定义的BertForNER
构造,适用于命名实体识别任务。
打印模型结构
print(model)
- 打印模型的详细结构,包括各层的组成和参数。
- 例如,输出可能如下:
- BertForNER(
(bert): BertModel(...)
(dropout): Dropout(p=0.1, inplace=False)
(classifier): Linear(in_features=768, out_features=7, bias=True)
)(bert): BertModel(...)
:- BERT 的主模型部分,负责生成每个 token 的语义表示。
- 包括嵌入层、12 层 Transformer 层等。
(dropout): Dropout(p=0.1, inplace=False)
:- Dropout 层,用于防止过拟合。
- Dropout 概率为 0.1(由
config.hidden_dropout_prob
指定)。
(classifier): Linear(in_features=768, out_features=7, bias=True)
:- 线性分类器,用于将每个 token 的 768 维表示映射到 7 个标签类别。
in_features=768
:输入维度,来自 BERT 的隐藏层。out_features=7
:输出维度,对应实体标签数。
总结
这段代码的作用是:
-
加载配置:
- 使用
AutoConfig
从预训练检查点加载模型的配置参数。 - 确保模型结构(如隐藏层大小、Dropout 概率)与预训练权重匹配。
- 使用
-
加载模型:
- 使用自定义的
BertForNER
类,通过from_pretrained
方法加载预训练的 BERT 权重,并结合配置构造一个自定义的命名实体识别模型。 - 将模型移动到指定的计算设备(GPU 或 CPU)。
- 使用自定义的
-
打印模型结构:
- 输出模型的各层结构,方便检查模型是否加载正确。
优化模型参数
训练流程中的关键概念和损失计算
我们首先阐述序列标注任务(例如命名实体识别 NER)中 损失函数的特殊性 和计算方式。注意:与文本分类任务不同,序列标注任务的每个样本会输出一个预测向量序列(每个 token 都对应一个预测向量)。
关键点:
CrossEntropyLoss
要求输入的 logits 和目标标签的形状特定。- 模型输出
[batch_size, sequence_length, num_labels]
必须通过permute
转换为[batch_size, num_labels, sequence_length]
。
我们就在这里解释了这个调整的必要性和计算方式:
我们将每一轮 Epoch 分为“训练循环”和“验证/测试循环”,在训练循环中计算损失,优化模型参数,在验证/测试循环中评估模型性能。下面我们首先实现训练循环。
但是,与文本分类任务对于每个样本只输出一个预测向量不同,token 分类任务会输出一个预测向量序列(因为对每个 token 都进行了单独分类),因此在 CrossEntropyLoss 中计算损失时,不能像之前直接将模型的预测结果与标签送入到 CrossEntropyLoss 中进行计算。
对于高维输出(例如 2D 图像需要按像素计算交叉熵),CrossEntropyLoss 需要输入维度调整为:(batch, C, d1, d2, ..., dk)。其中 C 是类别个数,d1, d2, ..., dk
是输入的维度。对于 token 分类任务,就是在 token 序列维度上计算交叉熵(Keras 称为时间步),因此下面我们先通过 pred.permute(0, 2, 1)
交换后两维,将模型输出维度从:(batch, seq, 7) 调整为:(batch, 7, seq),然后计算损失。
训练循环的实现
from tqdm.auto import tqdm
def train_loop(dataloader, model, loss_fn, optimizer, lr_scheduler, epoch, total_loss):
progress_bar = tqdm(range(len(dataloader)))
progress_bar.set_description(f'loss: {0:7f}')
finish_batch_num = (epoch - 1) * len(dataloader)
model.train()
for batch, (X, y) in enumerate(dataloader, start=1):
X, y = X.to(device), y.to(device)
pred = model(X)
loss = loss_fn(pred.permute(0, 2, 1), y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr_scheduler.step()
total_loss += loss.item()
progress_bar.set_description(f'loss: {total_loss / (finish_batch_num + batch):7f}')
progress_bar.update(1)
return total_loss
代码逐行功能解释
导入库
from tqdm.auto import tqdm
- 功能:导入
tqdm
模块,用于在训练过程中显示进度条,直观地跟踪训练进度。
定义训练循环
def train_loop(dataloader, model, loss_fn, optimizer, lr_scheduler, epoch, total_loss):
- 定义
train_loop
函数,用于执行一轮训练循环。 - 参数:
dataloader
:数据加载器,提供训练数据。model
:待训练的模型。loss_fn
:损失函数,用于计算模型输出与目标标签的误差。optimizer
:优化器,用于更新模型参数。lr_scheduler
:学习率调度器,动态调整学习率。epoch
:当前训练的轮次。total_loss
:累计的损失,用于统计和显示。
创建进度条
progress_bar = tqdm(range(len(dataloader)))
progress_bar.set_description(f'loss: {0:7f}')
finish_batch_num = (epoch - 1) * len(dataloader)
- progress_bar = tqdm(range(len(dataloader))):创建训练进度条,显示数据加载器的迭代进度。
-
使用
tqdm
的方法: tqdm(iterable)
:tqdm
是一个 Python 库,用于快速显示迭代任务的进度。- 参数:
iterable
:一个可迭代对象(例如列表、生成器等)。在这里,range(len(dataloader))
表示迭代dataloader
中的批次数。len(dataloader)
:返回数据加载器(dataloader
)中的批次数。range(len(dataloader))
:生成一个从0
到len(dataloader) - 1
的序列。
-
作用:
- 这个进度条会随着每次迭代更新,帮助用户了解训练的实时状态和完成进度。
-
- progress_bar.set_description(f'loss: {0:7f}'):为进度条添加描述信息,这里是当前损失值。
-
功能:
- 为进度条添加描述信息(
description
),通常用于显示额外的上下文信息,例如当前的损失值。
- 为进度条添加描述信息(
-
方法:
set_description()
:tqdm
提供的方法,用于设置进度条的描述。- 参数:
f'loss: {0:7f}'
:f''
:格式化字符串(f-string),允许在字符串中插入变量或表达式。loss: {0:7f}
:格式化输出,表示初始的损失值为0
,使用浮点数的格式显示,总宽度为 7。
- 参数:
-
作用:
- 在训练的最开始阶段,将进度条描述设置为 "损失(
loss
)的初始值为 0"。
- 在训练的最开始阶段,将进度条描述设置为 "损失(
-
- finish_batch_num = (epoch - 1) * len(dataloader):计算前几轮训练完成的批次数,用于计算累计损失。
-
功能:
- 计算当前轮次(
epoch
)开始时,已经完成的总批次数。
- 计算当前轮次(
-
变量含义:
epoch
:当前训练的轮次,从 1 开始计数。len(dataloader)
:每一轮训练中,数据加载器所需的批次数。
-
公式解释:
finish_batch_num = (epoch - 1) * len(dataloader)
:epoch - 1
:已经完成的轮次数。len(dataloader)
:每一轮所需的批次数。- 这两者相乘,得出当前轮次开始之前已完成的批次数。
-
作用:
- 这个变量用于计算训练的平均损失值时,将当前轮次的批次数与之前轮次完成的批次数进行累计。
-
模型进入训练模式
model.train()
- 将模型切换到训练模式(
train mode
),启用 Dropout 等训练专用机制。
遍历数据加载器
for batch, (X, y) in enumerate(dataloader, start=1):
X, y = X.to(device), y.to(device)
enumerate(dataloader, start=1)
:从数据加载器中逐批读取数据,X
是输入(通常是一个张量),y
是目标标签(label)。.to(device)
:是 PyTorch 中的方法,用于将张量移动到指定的设备上(如 GPU)。-
参数
device
:device
是一个变量,通常在代码中通过以下方式定义:- device = 'cuda' if torch.cuda.is_available() else 'cpu'
- 如果有 GPU 可用(
torch.cuda.is_available()
返回True
),device
的值为'cuda'
,表示使用 GPU。 - 如果没有 GPU,可用
device
为'cpu'
,表示使用 CPU。
- 如果有 GPU 可用(
-
移动的作用:
- PyTorch 的计算设备需要一致,模型参数和输入数据必须在同一设备上(例如,不能一个在 GPU 上,另一个在 CPU 上)。
- 这行代码确保
X
和y
都移动到了指定的设备上(GPU 或 CPU),以便后续的模型计算。
-
-
作用总结
这行代码:
- 确保模型输入(
X
)和目标标签(y
)被移动到指定设备(device
,例如 GPU 或 CPU)。 - 避免设备不一致导致的运行错误。
- 保证计算效率(如果有 GPU,加速计算;如果没有 GPU,默认使用 CPU)。
- 确保模型输入(
前向传播和损失计算
pred = model(X)
loss = loss_fn(pred.permute(0, 2, 1), y)
model(X)
:将输入X
传入模型,得到预测结果pred
,形状为[batch_size, sequence_length, num_labels]
。permute(0, 2, 1)
:调整预测结果的形状为[batch_size, num_labels, sequence_length]
,以适配损失函数loss_fn
。permute
是 PyTorch 张量(Tensor)的一个函数,不是参数。它的功能是用于重新排列维度顺序。
- pred是模型的输出,通常是一个多维张量。
permute
是 PyTorch 提供的张量方法,作用是根据参数重新排列维度。- 参数
(0, 2, 1)
指定了新维度的顺序:0
:保持第一个维度(batch_size
)。2
:将原来的第三个维度(num_labels
)调整到第二个位置。1
:将原来的第二个维度(sequence_length
)调整到第三个位置。
- 经过调用后,
pred
的形状从[batch_size, sequence_length, num_labels]
转换为[batch_size, num_labels, sequence_length]
。
loss_fn(pred, y)
:计算预测值pred
与目标值y
之间的损失。
反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
:清零优化器中所有参数的梯度,以防止梯度累加。loss.backward()
:对损失值loss
进行反向传播,计算每个参数的梯度。optimizer.step()
:根据梯度更新模型的参数。lr_scheduler.step()
:更新学习率。
累计损失并更新进度条
total_loss += loss.item()
progress_bar.set_description(f'loss: {total_loss / (finish_batch_num + batch):7f}')
progress_bar.update(1)
total_loss += loss.item()
:将当前批次的损失累加到总损失中。set_description
:更新进度条的描述信息为当前的平均损失。progress_bar.update(1)
:进度条前进一格。
返回总损失
return total_loss
- 将累计的总损失返回,以便用于日志记录或进一步处理。
总结
这段代码的功能是实现一个完整的训练循环,核心包括以下步骤:
- 创建进度条:用于显示训练进度和实时损失信息。
- 数据加载与设备切换:将数据加载到 GPU 或 CPU。
- 前向传播:通过模型得到预测结果。
- 损失计算:调整预测结果的形状并计算损失。
- 反向传播与参数优化:更新模型参数。
- 累积损失与进度更新:实时记录损失值并更新进度条。
模型训练和验证
验证环节的评价标准
如何使用 seqeval
库对命名实体识别(NER)的模型进行评估,对模型的预测结果进行量化评估,明确其分类性能。
from seqeval.metrics import classification_report
from seqeval.scheme import IOB2
y_true = [['O', 'O', 'B-LOC', 'I-LOC', 'O', 'B-PER', 'I-PER', 'O']]
y_pred = [['O', 'O', 'B-LOC', 'I-LOC', 'O', 'B-PER', 'I-PER', 'O']]
print(classification_report(y_true, y_pred, mode='strict', scheme=IOB2))
导入
seqeval
是一个专门用于序列标注任务(如 NER)的 Python 库,支持多种标注格式(如 IOB, IOB2)。- 通过
classification_report
函数,计算模型的预测结果与真实标签的性能指标(如 Precision、Recall、F1-score)。
内容
- 列表的内容采用了 IOB2 标注格式:
O
:非实体(Outside)。B-LOC
:实体类型为LOC
的开始位置(Begin)。I-LOC
:实体类型为LOC
的后续部分(Inside)。B-PER
和I-PER
:实体类型为PER
的开始和后续部分。
数据结构
[['O', 'O', 'B-LOC', 'I-LOC', 'O', 'B-PER', 'I-PER', 'O']]
- 外层列表表示一个样本(句子)。
- 内层列表表示该样本中的每个 token 的标签。
生成分类报告
print(classification_report(y_true, y_pred, mode='strict', scheme=IOB2))
作用
- 使用
seqeval
的classification_report
函数,生成一份分类报告,评估预测结果(y_pred
)与真实标签(y_true
)的差异。 - 输出的报告包括以下指标:
- Precision(精确率):模型预测的实体中有多少是真正正确的。
- Recall(召回率):真实存在的实体中有多少被模型正确预测。
- F1-score:精确率和召回率的调和平均值,综合衡量模型性能。
参数
y_true
:真实标签。y_pred
:模型预测的标签。mode='strict'
:- 指定评估模式为严格模式,只有在实体的类型和位置都完全匹配时,才认为预测正确。
scheme=IOB2
:- 指定标签格式为 IOB2(常见的序列标注格式)。
- 例如,
B-LOC
表示一个位置实体的开始,I-LOC
表示其后续部分。
核心任务
评估模型在命名实体识别任务中的性能:
- 定义真实标签
y_true
。 - 定义预测标签
y_pred
。 - 使用
seqeval
的classification_report
生成分类性能报告。
输出指标含义
- Precision:预测为某实体类型的标签中,有多少是正确的。
- Recall:真实存在的某实体类型的标签中,有多少被正确预测。
- F1-score:精确率和召回率的调和平均值。
- Support:每个标签类别的真实样本数量。