代码及注释
class BertForSequenceClassification(BertPreTrainedModel):
def __init__(self, config): # config:BERT预训练模型的配置文件
"""调用父类的__init__方法,初始化BERT的配置"""
super().__init__(config)
"""几分类"""
self.num_labels = config.num_labels
self.config = config
"""
实例化一个BERT模型,并将其作为自己的一个组件。
在后续的前向传播 (forward 方法) 中,self.bert 实例将用于处理输入的文本数据(如 input_ids、token_type_ids 和 attention_mask),产生用于分类任务的特征表示。
这些特征(特别是经过池化后的输出)随后会传递给分类器头部以进行最终的多标签分类。
"""
self.bert = BertModel(config)
"""
添加一个Dropout层,用于防止过拟合
其丢弃比例默认由 config.hidden_dropout_prob 定义。
"""
classifier_dropout = (
config.classifier_dropout if config.classifier_dropout is not None else config.hidden_dropout_prob
)
self.dropout = nn.Dropout(classifier_dropout)
"""
分类器层(线性变换层、全连接层)
将BERT的池化输出(大小为config.hidden_size)转换为类别预测的logits(大小为config.num_labels,未经过softmax的原始输出)
y = wx + b,w是权重矩阵,x是输入向量,b是偏置向量,y是输出向量
"""
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
"""初始化权重并应用最终处理"""
self.post_init()
"""模型的核心推理逻辑部分"""
def forward(
self,
"""整数张量,表示输入序列的token IDs"""
input_ids=None,
"""指示哪些token应当被模型注意的二进制张量,0表示忽略,1表示关注"""
attention_mask=None,
"""区分句子对中的两个句子的张量"""
token_type_ids=None,
"""序列中每个token位置的张量"""
position_ids=None,
"""是否掩蔽Transformer层中的注意力头,用于精细调整模型"""
head_mask=None,
"""是否直接提供嵌入向量而非通过input_ids,可提供灵活性"""
inputs_embeds=None,
"""
监督学习的标签,定义了损失函数的计算方式(回归或分类)
当config.num_labels == 1时,模型计算回归损失(均方误差损失),
反之则计算分类损失(交叉熵损失)。
"""
labels=None,
"""是否返回注意力权重、隐藏状态以及是否以字典形式返回结果的布尔标志。"""
output_attentions=None,
output_hidden_states=None,
return_dict=None,
):
"""是否使用字典来返回模型的输出"""
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
"""
当调用self.bert(...)时,BERT模型会执行以下操作:
1. Embedding Layer: 将input_ids(或inputs_embeds)转换为词嵌入。
2. Positional Encoding: 添加位置信息到词嵌入中(如果未直接提供inputs_embeds)。
3. Transformer Layers: 输入通过一系列的Transformer编码器层,每一层包含自注意力(Self-Attention)和前馈神经网络(Feed-Forward Network)组件。
4. Output Processing: 根据output_attentions和output_hidden_states参数决定是否返回每层的注意力权重和隐藏状态。
5. Final Output: 最终输出通常包括last_hidden_state(序列中每个token的最终隐藏状态),也可能包括其他根据参数设定的附加输出。
outputs的结果:
1. Last Hidden State: `outputs[0]` 是序列中每个token经过BERT编码后的最终隐藏状态,形状为 `[batch_size, sequence_length, hidden_size]`。这里,`hidden_size` 通常是768(对于BERT-base)或1024(对于BERT-large)。
2. Pooled Output: `outputs[1]` 是整个序列的池化表示(Pooling Output),它是通过对最后一个隐藏层的`[CLS]`标记(分类标记)的输出进行进一步处理得到的,形状为 `[batch_size, hidden_size]`。这个`pooled_output`常用于下游的分类任务,因为它被认为捕获了整个输入序列的语义信息。
"""
outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
"""
先获取BERT的池化输出,
再通过Dropout层,一个正则化技术,随机“丢弃”(设置为0)一部分神经元的输出以减少模型过拟合的风险。
然后将pooled_output映射到类别空间中。
如果分类任务有n个类别,那么logits将会是一个形状为(batch_size, n)的张量,
每一行对应一个样本,每一列对应一个类别的logit得分。
"""
pooled_output = outputs[1]
pooled_output = self.dropout(pooled_output)
logits = self.classifier(pooled_output)
loss = None
"""检查是否有实际的标签数据用于计算损失。
如果没有标签(例如,在预测阶段),则不需要计算损失。"""
if labels is not None:
if self.config.problem_type is None:
if self.num_labels == 1:
self.config.problem_type = "regression"
elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int):
self.config.problem_type = "single_label_classification"
else:
self.config.problem_type = "multi_label_classification"
if self.config.problem_type == "regression":
loss_fct = MSELoss()
if self.num_labels == 1:
loss = loss_fct(logits.squeeze(), labels.squeeze())
else:
loss = loss_fct(logits, labels)
elif self.config.problem_type == "single_label_classification":
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
elif self.config.problem_type == "multi_label_classification":
loss_fct = BCEWithLogitsLoss()
loss = loss_fct(logits, labels)
if not return_dict:
output = (logits,) + outputs[2:]
return ((loss,) + output) if loss is not None else output
return SequenceClassifierOutput(
loss=loss,
logits=logits,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
解释:
last_hidden_state
中的[CLS]位置输出:前向传播结束后,最后一层的隐藏状态输出矩阵中的第一行(或第一个元素,具体取决于维度排列)。它对应于输入序列开始处的特殊标记[CLS]
(分类标记)的隐状态,用于代表整个句子的语义信息。- BERT的池化输出
pooler_output:
一个高维向量,具体数值依赖于输入BERT的具体文本内容。
假设我们有一个经过训练的BERT模型,并且我们输入一句话:“I really enjoyed the movie.”,BERT会对这句话进行编码处理,最终在所有处理步骤后,会得到一个与`[CLS]`标记相关的隐藏状态,该隐藏状态可能会被用作`pooler_output`。这个输出通常是一个浮点数列表,表示了输入文本在BERT的高维语义空间中的嵌入。
一个简化的示例 `pooler_output` 可能看起来像这样(实际数值仅为示意):
[0.123, -0.456, 0.987, ..., -0.321]
这里的数组长度与BERT的隐藏层维度相对应,对于BERT-Base模型,这个维度是768。每个值代表了输入文本在该维度上的特征强度,这些特征综合起来捕获了句子的整体意义。
真实的`pooler_output`数值会根据模型权重和输入文本的具体内容有所不同。此向量可以用于后续的分类任务,比如判断这句话的情感倾向等。
- Logits:未经归一化的分数向量,表示输入样本属于各个类别的概率分布的原始预测值。在多分类任务中,logits通常通过softmax函数转换为概率值。(归一化)
pooler_output是
通过对last_hidden_state
中的[CLS]
位置的输出向量进行进一步处理得到的。具体来说,BERT模型会将这个[CLS]
向量传递给一个线性层(通常后面跟着一个激活函数,如Tanh),从而产生一个固定维度的向量,相对于[CLS]
更抽象和浓缩。- 分类头利用
pooler_output
(BERT是如此,因为顶部是池化输出,其他或直接使用last_hidden_state
的[CLS]
位置输出)进一步计算出类别预测所需的logits。这个过程可以视为从高层次语义表示到具体类别预测的映射。
单标签分类 vs 多标签分类
交叉熵损失 vs 带Logits的二元交叉熵损失
交叉熵损失(Cross-Entropy Loss)
- 适用场景:主要用于多分类问题,其中目标类别不止两个,而是存在多个类别。例如,图像分类任务中,可能需要从1000个不同的类别中选择正确的类别。
- 公式表示:对于多分类任务,假设模型输出为经过softmax函数处理后的概率分布yi,真实标签为one-hot编码形式pi,则交叉熵损失定义为
。
- 特点:鼓励模型的预测概率分布尽可能接近真实的标签分布,从而最小化两者之间的信息熵差异。
带Logits的二元交叉熵损失(Binary Cross-Entropy Loss with Logits)
- 适用场景:专为二分类问题设计,即目标只有两个类别(例如,是/否,正类/负类)。也适合改为多标签分类问题的损失函数。
- 公式表示:在这种情况下,模型直接输出未经softmax处理的logits——z,而真实标签 t 通常为0或1。损失函数定义为
,其中是sigmoid函数,用于将logits转化为概率。
- 特点:二元交叉熵损失直接在logits层面计算,避免了额外的softmax操作,这样可以提高数值稳定性,并且在某些情况下计算更高效。它直接衡量了模型输出的logits与实际标签的匹配程度,非常适合处理二分类任务。
区别总结
- 适用范围:交叉熵损失适用于多分类问题,二元交叉熵损失专门用于二分类问题且适合改为用于多标签分类。
- 输出处理:交叉熵损失通常用在softmax之后的概率分布上,而二元交叉熵损失直接作用于模型的logits输出。
- 计算效率与稳定性:二元交叉熵损失由于直接在logits层面计算,有时能提供更好的数值稳定性和计算效率,尤其是在二分类问题中。