大模型学习:从零到一实现一个BERT微调
目录
一、准备阶段
1.导入模块
2.指定使用的是GPU还是CPU
3.加载数据集
二、对数据添加词元和分词
1.根据BERT的预训练,我们要将一个句子的句头添加[CLS]句尾添加[SEP]
2.激活BERT词元分析器
3.填充句子为固定长度
代码解释:
三、数据处理
1.创建masks掩码矩阵
代码解释:
2.拆分数据集
3.将所有的数据转换为torch张量
4.选择批量大小并创建迭代器
代码解释:
四、BERT模型配置
1.初始化一个不区分大小写的 BERT 配置:
代码解释:
2.这些配置参数的作用:
3.加载模型
4.优化器分组参数
代码解释:
5.训练循环的超参数
代码解释:
五、训练循环
代码解释:
训练图解:
六、使用测试数据集进行预测和评估
七、使用马修斯相关系数(MCC)评估
2. 代码实现:
1. 测试数据预处理与预测
2.模型预测与结果收集
3.计算MCC
到这里就完美收官咯!!!!! 大家点个赞吧!!!
本章将微调一个BERT模型来预测下游的可接受性判断任务,如果你的电脑还没有配置相关环境的可以去使用 Colaboratory - Colab,里面已经全部帮你配置好啦!而且还可以免费使用GPU。
一、准备阶段
1.导入模块
导入所需的预训练相关模块,包括用于词元化的 BertTokenizer、用于配置 BERT 模型的 BertConfig,还有 Adam 优化器(AdamW),以及序列分类模块(BertFo SequenceClassification):
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset,DataLoader,RandomSampler,SequentialSampler
from sklearn.model_selection import train_test_split
from transformers import BertTokenizer,BertConfig
from transformers import BertForSequenceClassification,get_linear_schedule_with_warmup
# from transformers import AdamW
from torch.optim import AdamW
from tqdm import tqdm,trange
import pandas as pd
import io
import numpy as np
import matplotlib.pyplot as plt
# 导入进度条
from tqdm import tqdm,trange
# 导入常用的标注python模块
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
2.指定使用的是GPU还是CPU
使用GPU加速对我们的训练非常又有帮助
device = torch.device("cuda" if torch.cuda.is_avsilable() else "cpu")
3.加载数据集
这里的代码和数据是参考https://github.com/Denis2054/Transformers-for-NLP-2nd-Edition/tree/main/Chapter03
使用git仓库拉取一下就可以得到数据了
df=pd.read_csv("你拉取的数据in_domain_train.tsv的路径",
delimiter='\t',header=None,names=['sentence_source','label','label_notes','sentence'])
# 展示数据维度
df.shape # (8551,4)
随机抽取十个样本数据的看看:
df.sample(10)
以看到数据集中的数据包含了以下四列(即.tsv文件中四个用制表符分隔的列)。
● 第1列:句子来源(用编号表示)
● 第2列:标注(0=不可接受,1= 可接受)
● 第3列:作者的标注
● 第4列;要分类的句子
二、对数据添加词元和分词
1.根据BERT的预训练,我们要将一个句子的句头添加[CLS]句尾添加[SEP]
代码解释:将数据中的需要分类的句子提取为sentences,循环出每个句子,在每个句子的句头添加[CLS]句尾添加[SEP],将数据中的标签提取为labels
sentences=df.sentence.values
sentences=["[CLS]"+ sentence+"[SEP]" for sentence in sentences]
labels=df.label.values
2.激活BERT词元分析器
这里是初始化一个预训练BERT词元分析器。相比与从头开始训练一个词元分析器相对,节省很多时间和资源。们选择了一个不区分大小写的词元分析器,激活它,并展示对第一个句子词元 化之后的结果:
代码讲解:tokenizer是我们初始化的词元分析器,BertTokenizer.from_pretrained('bert-base-uncased')是使用BERT中自带的预训练好的参数,关于BertTokenizer.from_pretrained可以去看我的另外一章博客BertTokenizer.from_pretreined。
tokenizer_texts是已经每个句子词元分析好的一个迭代器,因为sentences中保存的句子的type为array类型,而tokenize中要传入的是字符串类型,所以这里要强转一下
词元分析后的一条句子为:Tokenize the first sentence: ['[CLS]', 'our', 'friends', 'wo', 'n', "'", 't', 'buy', 'this', 'analysis', ',', 'let', 'alone', 'the', 'next', 'one', 'we', 'propose', '.', '[SEP]']
tokenizer=BertTokenizer.from_pretrained('bert-base-uncased')
tokenizer_texts=[tokenizer.tokenize(str(sent)) for sent in sentences]
print("Tokenize the first sentence: ")
print(tokenizer_texts[0])
# Tokenize the first sentence:
['[CLS]', 'our', 'friends', 'wo', 'n', "'", 't', 'buy', 'this', 'analysis', ',', 'let', 'alone', 'the', 'next', 'one', 'we', 'propose', '.', '[SEP]']
3.填充句子为固定长度
上面的处理中我们不难想到,每个句子分析后的长度会随句子的大小而改变,而在BERT微调的时候需要保证句子的长度应用,所以我们要将长度不够的句子进行填充,我们将这个最大长度设置为128,对于长度超过128的数据我们将它截断,保证每个句子序列的大小都为128
代码解释:
input_ids:里面保存的是将上面的句子分词后的词元列表转化成对应数字的列表,其中将词元一个个的循环出来后经过tokenizer.convert_tokens_to_ids,它能把分词后的词元转化为整数 ID,从而让深度学习模型能够处理文本数据。在不同的库中,其使用方式可能会有所不同,但核心功能是一致的。
第二个input_ids:保存的是将每个词元序列转化成相同大小后的迭代器,pad_sequences函数的主要作用是将多个序列填充或截断至相同的长度,这在处理序列数据(像文本序列)时十分关键,因为神经网络通常要求输入数据具有统一的形状
from tensorflow.keras.preprocessing.sequence import pad_sequences
MAX_LEN=128
input_ids=[tokenizer.convert_tokens_to_ids(x) for x in tokenizer_texts]
print(input_ids[0])
input_ids=pad_sequences(input_ids,maxlen=MAX_LEN,dtype='long',truncating='post',padding='post')
print(input_ids[0])
三、数据处理
1.创建masks掩码矩阵
如果不知道为什么需要掩码的可以去看看transformers架构
为了防止模型对填充词元进行注意力计算,我们在前面的步骤中对序列进行了填充补齐。但是我们希望防止模型对这些填充 的词元进行注意力计算!首先创建一个空的 attention_masks 列表,用于存储每个序列的注意力掩码。然后, 对于输入序列(input_ids)中的每个序列(seq),我们遍历其中的每个词元。
针对每个词元,我们判断其索引是否大于 0。如果大于 0,则将对应位置的掩码 值设置为1,表示该词元是有效词元。如果等于0,则将对应位置的掩码值设置为0, 表示该词元是填充词元。最终得到的 attention_masks 列表中的每个元素都是一个与对应输入序列长度相同的 列表,其中每个位置的掩码值表示该位置的词元是否有效(1表示有效,0表示填充)。
通过使用注意力掩码,可确保在模型的注意力计算中,只有真实的词元会被考虑, 而填充词元则被忽略。这样可提高计算效率,并减少模型学习无用信息的概率。
代码解释:
attention_masks是保存所有词元掩码的列表,上面我们说到input_ids保存的是将每个词元序列转化成相同大小后的迭代器,我们将里面的每个词元序列遍历为seq,判断seq中的值是否大于0,如果大于0,那么它是有效词元,将他对应的掩码设置为1,反正为0
attention_masks=[]
for seq in input_ids:
seq_mask=[float(i>0) for i in seq]
attention_masks.append(seq_mask)
print(attention_masks[0])
"""[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.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.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.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.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.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.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.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]"""
2.拆分数据集
将数据拆分成训练集和验证集,训练集和测试集的比例为9:1
# 拆分训练集和验证集 (90%训练,10%验证)
train_inputs, val_inputs, train_labels, val_labels = train_test_split(
input_ids, labels, test_size=0.1, random_state=2025)
train_masks, val_masks, _, _ = train_test_split(
attention_masks, labels, test_size=0.1, random_state=2025)
3.将所有的数据转换为torch张量
微调模型需要使用 torch 张量,所以我们需要将数据转换为 torch 张量:
train_inputs = torch.tensor(train_inputs)
validation_inputs = torch.tensor(validation_inputs)
txain_labels=torch.tensor(train_labels)
validation_labels = torch.tensor(validation_labels)
train_masks = torch.tensor(train_masks)
validation_masks = torch.tensor(validation_masks)
4.选择批量大小并创建迭代器
如果一股脑地将所有数据都喂进机器,会导致机器因为内存不足而崩溃。所以需 要将数据一批一批地喂给机器。这里将把批量大小(batch size)设置为 32 并创建迭代 器。然后将迭代器与 torch的 DataLoader 相结合,以批量训练大量数据集,以免导致 机器因为内存不足而崩溃:
代码解释:
这里我们选的批量大小(batch_size)为32,使用TensorDataset和DataLoader创建训练数据迭代器,如果对TensorDataset和DataLoader不清楚的可以去看看我的另外一篇博客:TensorData和DataLoader
RandomSampler 是一种随机采样器,从给定的数据集中随机抽取样本,可选择有放回或无放回采样。无放回采样时,从打乱的数据集里抽取样本;有放回采样时,可指定抽取的样本数量 num_samples
。
batch_size=32
# 训练数据迭代器
train_data=TensorDataset(train_inputs,train_masks,train_labels)
train_sampler=RandomSampler(train_data)
train_dataloader=DataLoader(train_data,sampler=train_sampler,batch_size=batch_size)
# 测试数据迭代器
validation_data=TensorDataset(validation_inputs,validation_masks,validation_label)
validation_sampler=RandomSampler(train_data)
validation_dataloader=DataLoader(validation_data,sampler=validation_sampler,batch_size=batch_size)
四、BERT模型配置
1.初始化一个不区分大小写的 BERT 配置:
代码解释:
后面的代码寻妖用到transformers这个包,如果没有的pip安装一下。
configuration是初始化了一个包含BERT预训练模型中所有超参数的配置实例,如果BertConfig中不加任何的参数,那么会生成一个标准的BERT-base配置:
{
"hidden_size": 768, # 每个Transformer层的维度
"num_hidden_layers": 12, # Transformer层数(深度)
"num_attention_heads": 12, # 注意力头的数量
"intermediate_size": 3072, # FeedForward层的中间维度
"vocab_size": 30522, # 词表大小(需与预训练模型一致)
"max_position_embeddings": 512, # 最大序列长度
...
}
model:BertModel是根据配置生成一个随机初始化权重的BERT模型,根据传入的配置信息生成。
configuration是一个保存模型内部存储的配置信息副本
try:
import transformers
except:
print("installing transformers")
from transformers import BertModel,BertConfig
configuration=BertConfig()
model=BertModel(config=configuration)
configuration=model.config
print(configuration)
"""
输出为:
BertConfig {
"_attn_implementation_autoset": true,
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 0,
"position_embedding_type": "absolute",
"transformers_version": "4.50.0",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}
"""
2.这些配置参数的作用:
"""
attention probs_dropout_prob:对注意力概率应用的 dropout 率,这里设置为
0.1。
● hidden_act;编码器中的非线性激活函数,这里使用 gelu。gelu 是高斯误差线
性单位(Gaussian Eror Linear Units)激活函数的简称,它对输入按幅度加权,
使其成为非线性。
● hidden_dropout_prob:应用于全连接层的 dropout 概率。嵌入、编码器和汇聚
器层中都有全连接。输出不总是对序列内容的良好反映。汇聚隐藏状态的序
第3章 微调BERT 模型
列可改善输出序列。这里设置为0.1。
● hidden_size:编码器层的维度,也是汇聚层的维度,这里设置为768。
● initializer_range:初始化权重矩阵时的标准偏差值,这里设置为0.02。
· intermediate_size:编码器前馈层的维度,这里设置为3072。
● layer_norm_eps:是层规范化层的 epsilon 值,这里设置为le-12。
● max_position_embeddings:模型使用的最大长度,这里设置为512。
● model_type:模型的名称,这里设置为 bert。
● numattention_heads:注意力头数,这里设置为12。
· num_hidden_layers:层数,这里设置为12。
● pad_tokenid:使用0作为填充词元的HD,以避免对填充词元进行训练。
57
· type_vocab_size:token_type_ids的大小用于标识序列。例如,“the dog[SEP]
The cat.[SEP]”可用词元 ID [0,0,0,1,1,1]表示。
· vocab_size:模型用于表示 input_ids 的不同词元数量。换句话说,这是模型
可以识别和处理的不同词元或单词的总数。在训练过程中,模型会根据给定
的词表将文本输入转换为对应的词元序列,其中包含的词元数量是
vocab_size。通过使用这个词表,模型能够理解和表示更广泛的语言特征。这
里设置为 30522。
讲解完这些参数后,接下来将加载预训练模型。
"""
3.加载模型
现在开始加载预训练BERT模型
BertForSequenceClassification.from_pretrained
能够让你加载预训练的 BERT 模型权重,并且可以根据需求调整模型以适应特定的序列分类任务。这个方法非常实用,因为借助预训练的权重,模型通常能更快收敛,并且在特定任务上表现更优。第一个参数bert-base-uncased意思是加载BERT的默认权重,如果你有别的模型权重可以填写它的名字或者路径;nums_labels:表示你这个任务中的类别数,我们这个任务的label只有两种,所以这里是2。
DataParallel:DataParallel 是一种数据并行的实现方式,其核心思想是将大规模的数据集分割成若干个较小的数据子集,然后将这些子集分配到不同的计算节点(如 GPU)上,每个节点运行相同的模型副本,但处理不同的数据子集。在每一轮训练结束后,各节点会将计算得到的梯度进行汇总,并更新模型参数。如果不知道分布式计算的可以去看看我的另外一篇博客如何在多个GPU上训练
model=BertForSequenceClassification.from_pretrained("bert-base-uncased",num_labels=2)
model=nn.DataParallel(model)
model.to(device)
4.优化器分组参数
在将为模型的参数初始化优化器。在进行模型微调的过程中,首先需要初始化 预训练模型已学到的参数值。 微调一个预训练模型时,通常会使用之前在大规模数据上训练好的模型作为初始 模型。这些预训练模型已通过大量数据和计算资源进行了训练,学到了很多有用的特 征表示和参数权重。因此,我们希望在微调过程中保留这些已经学到的参数值,而不 是重新随机初始化它们。 所以,程序会使用预训练模型的参数值来初始化优化器,以便在微调过程中更好 地利用这些已经学到的参数。这样可以加快模型收敛速度并提高微调效果;
代码解释:
这段代码是用与为BERT模型的参数设置差异化的权重衰减策略,是训练Transformer模型时的常用技巧
param_optimizer是以字典的方式保存模型中所有可训练参数的名称和值,named_parameters()函数是获取模型中所有的参数名称和值。
no_decay是定义无需权重衰减的参数类型,权重衰减对偏置项bias和归一化层的weight无益,bias可能破坏模型对称性,LayerNorm的weight需保持灵活性,正则化会抑制其适应性。
optimizer_grouped_parametes: 组1:分组设置优化策略,筛选出参数名称中不包含bias和LayerNorm.weight的参数然后将权重衰减率设为0.1。组2:禁止权重衰减的参数,筛选出参数名称中包含bias和LayerNorm.weight的参数将权重衰减率设为0.0
param_optimizer=list(model.named_parameters())
no_decay=['bias','LayerNorm.weight']
optimizer_grouped_parametes=[
{'params':[p for n,p in param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay_rate':0.1},
{"params":[p for n,p in param_optimizer if any(nd in n for nd in no_decay)],
'weight_decay_rate':0.0}
]
5.训练循环的超参数
训练循环中的超参数非常重要,尽管它们看起来可能无害。例如,Adam 优化器 会激活权重衰减并经历一个预热阶段。学习率(lr)和预热率(warnup)应该在优化阶段的早期设置为一个非常小的值,在一 定迭代次数后逐渐增加。这样可以避免出现过大的梯度和超调问题,以更好地优化模 型目标。
代码解释:
使用AdamW优化器,将模型的参数传入,并设置初始学习率为2e-5
定义一个函数calculate_accuracy来度量准确率,用于测试结果与标注进行比较,向函数中传入预测结果的概率分布和真实标签,pred_flat是查找这个概率分布中的最大概率的索引,flatten是将数组一维化,labels_flat是真实结果的一维化。最后,计算预测结果展平后的数组中与展平后的标签数组相等的元素数量占标签数组长度的比例,并将这个比例作为结果返回。
optimizer=AdamW(optimizer_grouped_parametes,lr=2e-5)
def calculate_accuracy(preds, labels):
"""计算准确率的优化版本"""
preds = np.argmax(preds, axis=1).flatten()
labels = labels.flatten()
return np.sum(preds == labels) / len(labels)
五、训练循环
我们的训练循环将遵循标准的学习过程。轮数(epochs)设置为 4,并将绘制损失和 准确率的度量值。训练循环使用 dataloader 来加载和训练批量。我们将对训练过程进 行度量和评估。 首先初始化 train_loss_set(用于存储损失和准确率的数值,以便后续绘图)。然后 开始训练每一轮,并运行标准的训练循环,
代码解释:
这段代码实现了BERT模型的完整训练和验证流程,包含以下核心步骤:
- 初始化训练记录容器
- 循环训练多个epoch
- 每个epoch包含训练阶段和验证阶段
- 记录并输出训练指标
train_loss_history: 储存每个epoch的平均训练损失
val_accuracy_history:存储每个epoch的验证集准确率
代码太多了,大家在代码中看注释吧,这里主要说一下训练步骤
1.初始化记录容器
2.设置外层epoch(循环次数)循环
3.设置model为训练模式
4.将数据挨个前向传播和反向传播更新参数
5.计算平均训练损失
6.将model设置为评估阶段并进行评估
7.计算平均验证准确率
8.打印训练信息
train_loss_history = [] # 存储每个epoch的平均训练损失
val_accuracy_history = [] # 存储每个epoch的验证集准确率
for epoch_i in trange(epochs, desc="Epoch"):
# ========== 训练阶段 ==========
model.train() # 设置模型为训练模式
total_train_loss = 0 # 初始化累计损失
for batch in train_dataloader:
# 数据转移到GPU
b_input_ids, b_input_mask, b_labels = tuple(t.to(device) for t in batch)
# 梯度清零
model.zero_grad()
# 前向传播
outputs = model(b_input_ids,
attention_mask=b_input_mask,
labels=b_labels)
# 多GPU处理:取平均损失
loss = outputs.loss.mean()
# 反向传播
loss.backward()
"""在深度学习训练时,梯度可能会变得非常大,这会导致训练不稳定,甚至引发梯度爆炸的问题。torch.nn.utils.clip_grad_norm_ 函数通过对梯度的范数进行裁剪,避免梯度变得过大,从而让训练过程更加稳定。"""
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪
# 参数更新
optimizer.step()
scheduler.step()
total_train_loss += loss.item()
# 记录平均训练损失
avg_train_loss = total_train_loss / len(train_dataloader)
train_loss_history.append(avg_train_loss) # 记录历史损失
# ========== 验证阶段 ==========
model.eval()
total_eval_accuracy = 0
model.eval() # 设置模型为评估模式
total_eval_accuracy = 0 # 初始化累计准确率
with torch.no_grad(): # 禁用梯度计算
for batch in val_dataloader:
b_input_ids, b_input_mask, b_labels = tuple(t.to(device) for t in batch)
# 前向传播
outputs = model(b_input_ids,
attention_mask=b_input_mask)
# .logits是将model中还没有经过激活函数的值提取出来,因为验证不需要激活
# 这样节省了显存,.detach将张量从计算图中分离,断开梯度追踪。因为验证阶段不需要计算梯度,这一步可以节省内存并避免不必要的计算。
# .cpu():如果张量在GPU上(例如通过.to('cuda')加载),这一步会将其移动到CPU内存中。NumPy无法直接处理GPU上的张量,必须转移到CPU。
logits = outputs.logits.detach().cpu().numpy()
label_ids = b_labels.to('cpu').numpy()
total_eval_accuracy += calculate_accuracy(logits, label_ids)
avg_val_accuracy = total_eval_accuracy / len(val_dataloader)
val_accuracy_history.append(avg_val_accuracy)
# 打印训练信息
print(f"\nEpoch {epoch_i + 1}/{epochs}")
print(f"Train loss: {avg_train_loss:.4f}")
print(f"Validation Accuracy: {avg_val_accuracy:.4f}")
训练图解:
graph TD
A[开始训练] --> B[设置训练模式]
B --> C[遍历训练数据]
C --> D[数据转GPU]
D --> E[梯度清零]
E --> F[前向传播]
F --> G[计算损失]
G --> H[反向传播]
H --> I[梯度裁剪]
I --> J[参数更新]
J --> K[学习率更新]
K --> C
C --> L[计算平均损失]
L --> M[验证模式]
M --> N[遍历验证数据]
N --> O[前向传播]
O --> P[计算准确率]
P --> Q[计算平均准确率]
Q --> R[记录结果]
R --> S[打印信息]
S --> T[完成epoch?]
T --是--> U[结束训练]
T --否--> B
六、使用测试数据集进行预测和评估
们使用了in_domain_traintsv 数据集训练 BERT下游模型。现在我们将使用基 于留出法!分出的测试数据集 outof_domain_dev.v 文件进行预测。我们的目标是预 测句子在语法上是否正确。 以下代码展示了测试数据准备过程:
# 加载测试数据
test_df = pd.read_csv("out_of_domain_dev.tsv", delimiter='\t', header=None,
names=['sentence_source', 'label', 'label_notes', 'sentence'])
# 预处理测试数据
test_input_ids, test_attention_masks, test_labels = preprocess_data(test_df, tokenizer, max_len=128)
# 创建预测DataLoader
prediction_dataset = TensorDataset(test_input_ids, test_attention_masks, test_labels)
prediction_dataloader = DataLoader(prediction_dataset, sampler=SequentialSampler(prediction_dataset),
batch_size=batch_size)
# 初始化存储
predictions = []
true_labels = []
model.eval()
for batch in prediction_dataloader:
batch = tuple(t.to(device) for t in batch)
b_input_ids, b_input_mask, b_labels = batch
with torch.no_grad():
outputs = model(b_input_ids, attention_mask=b_input_mask)
logits = outputs.logits.detach().cpu().numpy()
label_ids = b_labels.cpu().numpy()
predictions.append(logits)
true_labels.append(label_ids)
# 计算准确率
flat_predictions = np.concatenate(predictions, axis=0)
flat_predictions = np.argmax(flat_predictions, axis=1)
flat_true_labels = np.concatenate(true_labels, axis=0)
accuracy = np.sum(flat_predictions == flat_true_labels) / len(flat_true_labels)
print(f"Test Accuracy: {accuracy:.4f}")
七、使用马修斯相关系数(MCC)评估
1、MCC的核心原理与优势
马修斯相关系数(Matthews Correlation Coefficient, MCC)是一种综合评估二分类模型性能的指标,尤其适用于类别不平衡数据集。其优势包括:
- 全面性:同时考虑真阳性(TP)、真阴性(TN)、假阳性(FP)、假阴性(FN)。、
- 鲁棒性:在类别分布不均衡时仍能准确反映模型性能(例如医学诊断中的罕见病检测)。
- 可解释性:取值范围为[-1, 1],1表示完美预测,0表示随机猜测,-1表示完全错误。
计算公式:
2. 代码实现:
1. 测试数据预处理与预测
# 加载测试数据(示例路径需替换为实际路径)
test_df = pd.read_csv("out_of_domain_dev.tsv", delimiter='\t', header=None,
names=['sentence_source', 'label', 'label_notes', 'sentence'])
# 预处理(复用preprocess_data函数)
test_input_ids, test_attention_masks, test_labels = preprocess_data(test_df, tokenizer, max_len=128)
# 创建DataLoader
prediction_dataset = TensorDataset(test_input_ids, test_attention_masks, test_labels)
prediction_dataloader = DataLoader(prediction_dataset, sampler=SequentialSampler(prediction_dataset), batch_size=batch_size)
2.模型预测与结果收集
# 初始化存储
predictions = []
true_labels = []
model.eval()
for batch in prediction_dataloader:
batch = tuple(t.to(device) for t in batch)
b_input_ids, b_input_mask, b_labels = batch
with torch.no_grad():
outputs = model(b_input_ids, attention_mask=b_input_mask)
logits = outputs.logits.detach().cpu().numpy()
label_ids = b_labels.cpu().numpy()
predictions.append(logits)
true_labels.append(label_ids)
# 合并结果
flat_predictions = np.concatenate(predictions, axis=0)
flat_predictions = np.argmax(flat_predictions, axis=1) # 将logits转为类别(0/1)
flat_true_labels = np.concatenate(true_labels, axis=0)
3.计算MCC
from sklearn.metrics import matthews_corrcoef
mcc = matthews_corrcoef(flat_true_labels, flat_predictions)
print(f"Test MCC: {mcc:.4f}")