python实战(九)——如何微调预训练模型?

部署运行你感兴趣的模型镜像

一、任务目标

        在之前的实战项目中,我们都是直接使用预训练模型,如Bert进行文本表示,进而再训练下游模型用于预测。尽管预训练模型具备强大的zero-shot能力和领域知识迁移能力,但在一些特定领域中,预训练模型的表现依旧是不足的。那么,为了让预训练模型能够更好地适应我们当前的任务,微调就成为了一个必要途径。这里,我们将展示如何进行预训练模型的微调,并给出详细的python建模框架解读

二、微调流程

        为了更加方便地对预训练模型进行微调,我们通过pytorch来搭建一个数据集类和模型类:

1、数据集类构建

        为了提高torch训练模型的效率,我们往往会自定义一个dataset类,用于对原始数据的批量处理。在数据量小的时候可以直接写循环,但是为了规范我们的代码,且为了让程序具备处理大批量数据的能力,使用Dataset和DataLoader是必要的

from torch.utils.data import Dataset

# 自定义数据集类,并继承Dataset
class taskDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len):
        # 数据和标签
        self.texts = texts
        self.labels = labels
        # 分词器和文本最大长度
        self.tokenizer = tokenizer
        self.max_len = max_len

    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self,idx):
        
        text = self.texts[idx]
        label = self.labels[idx]
        
        encoding = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=self.max_len,
            return_token_type_ids=False,
            padding='max_length',
            truncation=True,
            return_attention_mask=True,
            return_tensors='pt',
        )
        
        return {
            'text': text,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

        taskDataset类用于后续的DataLoader数据加载,DataLoader会根据我们在taskDataset中定义的方法批量处理数据并返回相应格式的结果。其中,taskDataset必须包含__init__,__len__和__getitem__方法。除了__len__方法直接返回数据长度之外,其余两个方法的输入和输出由我们自定义,只需要确保输出的结果能够输入到预训练模型中即可(即符合预训练模型输入要求)。

2、模型类构建

        定义好数据集类之后,模型类也是必不可少的。模型类必须包括__init__和forward两个类方法,而类方法的参数输入和输出则由我们定义。一般而言,__init__方法中会定义好预训练模型层以及其后的附加层(对于不同的任务和模型结构,更改此处模型层定义即可),而forward方法则获取预训练模型必要的输入以及其他可选参数,并定义好输入数据的处理步骤,例如先经过bert层,再经过fc1,最后fc2等,输出一般是logits,即最后一层的神经元输出值(对应任务输出要求,例如这里是三分类,那么forward输出的结果应当是三个神值,代表属于对应类的概率)。

import torch.nn as nn

class BertMLPClassifier(nn.Module):
    def __init__(self):
        super(BertMLPClassifier, self).__init__()
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        self.dropout = nn.Dropout(p=0.3)
        self.fc1 = nn.Linear(self.bert.config.hidden_size, 256)
        self.fc2 = nn.Linear(256, 3)
    
    def forward(self, input_ids, attention_mask):
        outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
        pooled_output = outputs[1]  # BERT的池化输出
        x = self.dropout(pooled_output)
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

3、数据准备

        完成了数据集类和模型类的定义之后,我们就可以开始准备数据了。首先读取数据集,并划分训练集和测试集,然后taskDataset对数据进行处理,最后调用DataLoader实现批量数据的输出。这里,我们使用的是kaggle的情感分类数据集

from datasets import load_dataset
from torch.utils.data import DataLoader
from transformers import BertModel, BertTokenizer
import json
from sklearn.model_selection import train_test_split

# 数据集读取
df = pd.read_csv('./data/Emotion_classify_Data.csv')
label_idx = {'anger':0, 'joy':1, 'fear':2}
df['Emotion'] = df['Emotion'].apply(lambda x: label_idx[x])

# 数据划分
train_texts, val_texts, train_labels, val_labels = train_test_split(df['Comment'].tolist(), df['Emotion'].tolist(), stratify=df['Emotion'].tolist(), test_size=0.3, random_state=2024)
print(len(train_texts))
print(len(val_texts))

# 第一步:定义BERT tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# 第二步:创建自定义数据集
train_dataset = taskDataset(train_texts, train_labels, tokenizer, max_len=64)
val_dataset = taskDataset(val_texts, val_labels, tokenizer, max_len=64)

# 第三步:创建数据加载器
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)

4、定义损失函数和优化器

        这里,我们根据任务的需求定义好损失函数和优化器,以及各自的参数。

import torch

# 实例化模型
model = BertMLPClassifier()

# 将模型移动到GPU(如果可用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

# 定义优化器和损失函数,这里损失函数根据任务而定,多分类使用CrossEntropyLoss,二分类使用BCELoss,聚类使用MSELoss,在此处修改即可
optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)
criterion = nn.CrossEntropyLoss()

5、设置微调策略

        现在到了最关键的步骤,就是设置我们的微调策略。如果不进行这一步的设置,那么默认预训练模型的所有参数都会参与到微调中,这就意味着模型可能会丢失在预训练阶段学习到的大量知识(这是由于微调数据相比起预训练数据的量级差别巨大,如果全参数进行微调很容易会改变已经学习好的参数,从而降低模型表现)。

        首先,我们可以把当前我们所构建的模型中,所有可训练的参数名称及shape打印出来,看看我们要让哪一些层参与微调训练(除非我们很熟悉这个模型了)。

# 打印模型中所有参数的名称、形状和是否需要梯度
for name, param in model.named_parameters():
    print(f"Name: {name}")
    print(f"Shape: {param.shape}")
    print(f"Requires Grad: {param.requires_grad}\n")

        选定层之后,通过设置更改requires_grad参数实现层的冻结与解冻,冻结之后的参数不参与梯度更新,也就是无法训练并保持原始值。这里我们选择解冻bert的最后一层的output子层组件,以及下游的全连接层。然后,我们打印出需要梯度更新的参数看看是否符合我们的设计。

# 冻结BERT模型的前几层参数
for name, param in model.bert.named_parameters():
    if 'encoder.layer.11.out' not in name and 'fc' not in name:  # 只训练Bert最后一层和全连接层
        param.requires_grad = False

# 再次打印模型中需要梯度的参数
print("Model parameters after freezing:")
for name, param in model.named_parameters():
    if param.requires_grad==True:
        print(f"Name: {name}")
        print(f"Shape: {param.shape}")

        打印出来可以看到我们要训练的层以及对应的参数数量:

405413b6a3da43b28cef1c0345cff248.png

6、编写训练和评估函数

        完成参数设置之后,我们编写一个训练的函数和测试的函数。下面的训练代码几乎是通用的,大体的流程就是:

(1)设置模型为可训练模式

(2)data_loader按设定的batch_size批量输出数据

        (a)模型预测当前batch的数据得到结果

        (b)计算当前batch的损失

        (c)损失反向传播

        (d)优化器状态更新

def train_epoch(model, data_loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    
    # 批量遍历数据
    for batch in data_loader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)
        # print(labels.shape)
        
        # 获取模型输出并计算损失
        outputs = model(input_ids=input_ids, attention_mask=attention_mask)
        loss = criterion(outputs, labels)
        
        total_loss += loss.item()
        
        # 优化器梯度清零
        optimizer.zero_grad()
        
        # 反向传播损失 
        loss.backward()

        # 优化器更新
        optimizer.step()
    
    return total_loss / len(data_loader)

        测试模型的代码同样是通用的,与训练代码基本相同,只不过这时候的模型是eval()模式,同时我们要使用torch.no_grad()禁用梯度计算,以避免多余的计算资源消耗:

def eval_model(model, data_loader, criterion, device):
    model.eval()
    total_loss = 0
    correct_predictions = 0
    
    with torch.no_grad():
        for batch in data_loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            
            outputs = model(input_ids=input_ids, attention_mask=attention_mask)
            loss = criterion(outputs, labels)
            total_loss += loss.item()
            
            _, preds = torch.max(outputs, dim=1)
            correct_predictions += torch.sum(preds == labels)
    
    return total_loss / len(data_loader), correct_predictions.double() / len(data_loader.dataset)

7、训练模型

        迭代训练模型num_epochs次,完成模型的微调,后续可存储微调后的模型以便复用。

num_epochs = 5

for epoch in range(num_epochs):
    train_loss = train_epoch(model, train_loader, criterion, optimizer, device)
    val_loss, val_acc = eval_model(model, val_loader, criterion, device)
    
    print(f"Epoch {epoch+1}/{num_epochs}")
    print(f"Train loss: {train_loss:.4f}")
    print(f"Val loss: {val_loss:.4f}, Val accuracy: {val_acc:.4f}")

8、预测并获取输出

        最后,我们写一个函数获取模型预测结果:

def get_predictions(model, texts, tokenizer, max_length, device):
    model.eval()

    inputs = tokenizer(texts, return_tensors="pt", max_length=max_length, truncation=True, padding="max_length")

    # 将输入数据移动到GPU(如果可用)
    input_ids = inputs["input_ids"].to(device)
    attention_mask = inputs["attention_mask"].to(device)
    
    # 禁用梯度计算(在评估模式下不需要计算梯度)
    with torch.no_grad():
        # 前向传播,获取输出
        outputs = model(input_ids, attention_mask)
    
    # 获取预测标签
    predictions = torch.argmax(outputs, dim=1)
    
    return predictions.cpu().numpy()

三、完整代码

from torch.utils.data import Dataset, DataLoader
from transformers import BertModel, BertTokenizer
import torch.nn as nn
import torch
import json
from sklearn.model_selection import train_test_split

# 自定义数据集类,并继承Dataset
# 自定义数据集类,并继承Dataset
class taskDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len):
        # 数据和标签
        self.texts = texts
        self.labels = labels
        # 分词器和文本最大长度
        self.tokenizer = tokenizer
        self.max_len = max_len

    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self,idx):
        
        text = self.texts[idx]
        label = self.labels[idx]
        
        encoding = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=self.max_len,
            return_token_type_ids=False,
            padding='max_length',
            truncation=True,
            return_attention_mask=True,
            return_tensors='pt',
        )
        
        return {
            'text': text,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

class BertMLPClassifier(nn.Module):
    def __init__(self):
        super(BertMLPClassifier, self).__init__()
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        self.dropout = nn.Dropout(p=0.3)
        self.fc1 = nn.Linear(self.bert.config.hidden_size, 256)
        self.fc2 = nn.Linear(256, 3)
    
    def forward(self, input_ids, attention_mask):
        outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
        pooled_output = outputs[1]  # BERT的池化输出
        x = self.dropout(pooled_output)
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

# 数据集读取
df = pd.read_csv('./data/Emotion_classify_Data.csv')
label_idx = {'anger':0, 'joy':1, 'fear':2}
df['Emotion'] = df['Emotion'].apply(lambda x: label_idx[x])

# 数据划分
train_texts, val_texts, train_labels, val_labels = train_test_split(df['Comment'].tolist(), df['Emotion'].tolist(), stratify=df['Emotion'].tolist(), test_size=0.3, random_state=2024)
print(len(train_texts))
print(len(val_texts))

# 定义BERT tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# 创建自定义数据集
train_dataset = taskDataset(train_texts, train_labels, tokenizer, max_len=64)
val_dataset = taskDataset(val_texts, val_labels, tokenizer, max_len=64)

# 创建数据加载器
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)

model = BertMLPClassifier(num_classes=2)

# 将模型移动到GPU(如果可用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)
criterion = nn.CrossEntropyLoss()

# 打印模型中所有参数的名称、形状和是否需要梯度
for name, param in model.named_parameters():
    print(f"Name: {name}")
    print(f"Shape: {param.shape}")
    print(f"Requires Grad: {param.requires_grad}\n")

# 冻结BERT模型的前几层参数
for name, param in model.bert.named_parameters():
    if 'encoder.layer.11.out' not in name and 'fc' not in name:  # 只训练Bert最后一层和全连接层
        param.requires_grad = False

# 再次打印模型中需要梯度的参数
print("Model parameters after freezing:")
for name, param in model.named_parameters():
    if param.requires_grad==True:
        print(f"Name: {name}")
        print(f"Shape: {param.shape}")

def train_epoch(model, data_loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    
    for batch in data_loader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)
        # print(labels.shape)
        
        outputs = model(input_ids=input_ids, attention_mask=attention_mask)
        loss = criterion(outputs, labels)
        
        total_loss += loss.item()
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    return total_loss / len(data_loader)

def eval_model(model, data_loader, criterion, device):
    model.eval()
    total_loss = 0
    correct_predictions = 0
    
    with torch.no_grad():
        for batch in data_loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            
            outputs = model(input_ids=input_ids, attention_mask=attention_mask)
            loss = criterion(outputs, labels)
            total_loss += loss.item()
            
            _, preds = torch.max(outputs, dim=1)
            correct_predictions += torch.sum(preds == labels)
    
    return total_loss / len(data_loader), correct_predictions.double() / len(data_loader.dataset)

num_epochs = 5

for epoch in range(num_epochs):
    train_loss = train_epoch(model, train_loader, criterion, optimizer, device)
    val_loss, val_acc = eval_model(model, val_loader, criterion, device)
    
    print(f"Epoch {epoch+1}/{num_epochs}")
    print(f"Train loss: {train_loss:.4f}")
    print(f"Val loss: {val_loss:.4f}, Val accuracy: {val_acc:.4f}")

def get_predictions(model, texts, tokenizer, max_length, device):
    model.eval()

    inputs = tokenizer(texts, return_tensors="pt", max_length=max_length, truncation=True, padding="max_length")

    # 将输入数据移动到GPU(如果可用)
    input_ids = inputs["input_ids"].to(device)
    attention_mask = inputs["attention_mask"].to(device)
    
    # 禁用梯度计算(在评估模式下不需要计算梯度)
    with torch.no_grad():
        # 前向传播,获取输出
        outputs = model(input_ids, attention_mask)
    
    # 获取预测标签
    predictions = torch.argmax(outputs, dim=1)
    
    return predictions.cpu().numpy()

preds = get_predictions(model, val_texts, tokenizer, 64, device)

四、总结

        从流程上来看,几乎所有预训练模型的微调流程都离不开上面所述的各个步骤,这是我们所必须要掌握的内容。理想的情况是我们能够轻松地手撕数据集类、模型类的构造代码,以及模型的训练和测试代码。只要记住了这套代码框架,不论遇到什么任务,对模型结构和损失函数稍加修改即可快速建模了。

        当然,实际业务应用过程中,还有许多细节需要去雕刻,比如为了使得微调后的预训练模型取得更好的效果,我们可以先训练下游层的参数,然后再返回来解冻预训练模型的后面一到两层,与下游层共同进行新一轮的训练。这么做的理由是下游层的参数一般是随机初始化的,直接解冻预训练模型的后面几层进行梯度更新,可能在一开始会造成预训练模型后面可训练参数失效。如果先训练好下游层的参数,在下游层具备一定的性能基础之后对预训练模型进行微调,得到的模型将更加稳定可靠。此外,解冻最后的一层还是两层,还是某一层中的一部分参数,更多地需要凭我们的经验以及训练的效果来确定,所以我们往往需要分别使用不同的参数训练策略进行模型训练,最后选出最优方案。

您可能感兴趣的与本文相关的镜像

Llama Factory

Llama Factory

模型微调
LLama-Factory

LLaMA Factory 是一个简单易用且高效的大型语言模型(Large Language Model)训练与微调平台。通过 LLaMA Factory,可以在无需编写任何代码的前提下,在本地完成上百种预训练模型的微调

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值