微调bert大模型

1. setup enviroment

2. prepare dataset

ruby 复制代码
class IMDbDataset(torch.utils.data.Dataset): def __init__(self, encodings, labels): self.encodings = encodings self.labels = labels def __getitem__(self, idx): item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()} item["labels"] = torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels) train_dataset = IMDbDataset(train_encodings, train_labels)

3.Tokenize the data

ini 复制代码
encodings = tokenizer(
    list_of_questions,  # 要处理的文本列表
    truncation=True,    # 超长截断
    padding=True,       # 不足补零
    max_length=128,     # 最大长度
    return_tensors="pt" # 返回PyTorch张量
)

1. 核心作用

把原始文本(如["How does BERT work?", "什么是机器学习?"])转换成BERT能理解的数字格式,包含:

  • input_ids :文本→数字编号(如[101, 1731, 1452, 102])
  • attention_mask:标记哪些是真实词(1)、哪些是填充的0(0)
  • token_type_ids(可选):区分句子A/B(对问答任务有用)

2. 参数详解(附比喻)

参数 作用 比喻
list_of_questions 要处理的文本列表(如["问题1", "问题2"] 就像待翻译的外语句子列表
truncation=True 超过max_length时自动截断 像剪刀裁掉过长的句子(保留前128个词)
padding=True 短于max_length时补零([PAD]) 像用空白填空使所有句子长度相同
max_length=128 每条文本最大词数(BERT通常≤512) 规定所有句子翻译后最多128个单词
return_tensors="pt" 返回PyTorch张量(不是Python列表) 要求返回"PyTorch专用格式"的数据

3. 输出示例

假设输入:["How old are you?", "你好"]

yaml 复制代码
python
{
    'input_ids':        # 文本的数字编码
        tensor([
            [101, 1731, 1385, 1132, 1128, 102, 0, ..., 0],  # "How old are you?"
            [101, 6821, 102, 0, 0, ..., 0]                   # "你好"
        ]),
    'attention_mask':   # 标识哪些是有效词
        tensor([
            [1, 1, 1, 1, 1, 1, 0, ..., 0],  # 前6个是真实词,后面是填充的0
            [1, 1, 1, 0, 0, ..., 0]
        ])
}

4. 为什么需要这些处理?

  • BERT的固定输入长度:模型需要统一尺寸的输入(像电梯限载10人,不足补空,超载要裁)
  • 注意力机制依赖结构attention_mask告诉模型哪些位置需要计算注意力
  • GPU计算优化 :张量格式(return_tensors="pt")能加速GPU并行计算

5. 常见问题

Q1:max_length设多大合适?

  • 一般设为你数据中大多数文本的长度(可通过统计得出)
  • BERT最大支持512,但越长消耗显存越多

Q2:padding补零会影响模型吗?

  • 不会!因为有attention_mask告诉模型忽略这些填充位置

Q3:截断会丢失信息吗?

  • 可能丢失长文本尾部信息,可通过以下方法缓解:

    ini 复制代码
    python
    tokenizer(..., truncation="longest_first")  # 优先截断长句子
    # 或
    tokenizer(..., stride=64, return_overflowing_tokens=True)  # 滑动窗口处理超长文本

6. 完整流程图示

css 复制代码
原始文本 → Tokenizer分词 → 添加[CLS][SEP] → 截断/填充 → 转数字ID → 输出张量
           │              │               │             │
           ↓              ↓               ↓             ↓
        "你好" → ["[CLS]", "你", "好", "[SEP]"] → [101, 6821, 102] → 补零到128长度

只要记住:Tokenizer就像翻译官+标准化工厂,把乱七八糟的文本变成整齐划一的数字矩阵!

4.Create a Custom Dataset

在PyTorch中创建自定义Dataset类来处理BERT问答(QA)或分类任务数据是一个关键步骤。下面我将详细解释如何创建一个QADataset类,并解释每一部分的作用。

完整代码实现

python 复制代码
python
from torch.utils.data import Dataset

class QADataset(Dataset):
    def __init__(self, encodings, labels):
        """
        初始化数据集
        :param encodings: 包含input_ids, attention_mask等的字典
        :param labels: 样本对应的标签
        """
        self.encodings = encodings
        self.labels = labels
    
    def __len__(self):
        """
        返回数据集中的样本数量
        """
        return len(self.labels)
    
    def __getitem__(self, idx):
        """
        获取单个样本
        :param idx: 样本索引
        :return: 包含编码和标签的字典
        """
        item = {key: val[idx] for key, val in self.encodings.items()}
        item['labels'] = self.labels[idx]
        return item

代码逐步解释

1. 继承Dataset基类

kotlin 复制代码
python
from torch.utils.data import Dataset
class QADataset(Dataset):
  • Dataset是PyTorch提供的抽象基类
  • 继承它后必须实现__len____getitem__方法

2. __init__方法

ruby 复制代码
python
def __init__(self, encodings, labels):
    self.encodings = encodings
    self.labels = labels
  • 接收两个参数:

    • encodings: 包含BERT输入特征的字典,通常有:

      • input_ids: 分词后的token IDs
      • attention_mask: 注意力掩码
      • token_type_ids: 区分句子A/B的标记(对于QA任务)
    • labels: 样本标签(分类任务)或答案位置(QA任务)

3. __len__方法

python 复制代码
python
def __len__(self):
    return len(self.labels)
  • 返回数据集中的样本总数
  • PyTorch的DataLoader会调用这个方法确定batch数量

4. __getitem__方法

ruby 复制代码
python
def __getitem__(self, idx):
    item = {key: val[idx] for key, val in self.encodings.items()}
    item['labels'] = self.labels[idx]
    return item
  • 根据索引idx返回单个样本

  • 将encodings中的每个特征在idx位置的值取出

  • 添加对应的标签

  • 返回格式示例:

    css 复制代码
    python
    {
        'input_ids': tensor([101, 1731, 1452, 102]),
        'attention_mask': tensor([1, 1, 1, 1]),
        'token_type_ids': tensor([0, 0, 0, 0]),
        'labels': 1  # 或对于QA任务可能是[start_pos, end_pos]
    }

如何使用这个Dataset类

1. 准备数据

ini 复制代码
python
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# 示例数据
questions = ["What is BERT?", "How does attention work?"]
answers = ["A language model", "It weights input tokens"]

# 对问题和答案进行编码
encodings = tokenizer(questions, answers, truncation=True, 
                     padding='max_length', max_length=128, 
                     return_tensors='pt')

# 假设是分类任务,创建虚拟标签
labels = torch.tensor([1, 0])  # 1表示正例,0表示负例

2. 创建Dataset实例

ini 复制代码
python
dataset = QADataset(encodings, labels)

3. 创建DataLoader

ini 复制代码
python
from torch.utils.data import DataLoader

dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

4. 在训练循环中使用

ini 复制代码
python
for batch in dataloader:
    input_ids = batch['input_ids']
    attention_mask = batch['attention_mask']
    labels = batch['labels']
    
    # 将这些输入传递给BERT模型
    outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
    loss = outputs.loss
    loss.backward()
    # ...训练步骤...

针对QA任务的调整

对于问答任务,通常需要修改__init____getitem__来处理答案位置:

python 复制代码
python
class QADataset(Dataset):
    def __init__(self, encodings):
        """QA任务可能不需要显式labels参数"""
        self.encodings = encodings
    
    def __getitem__(self, idx):
        return {key: val[idx] for key, val in self.encodings.items()}

然后编码时包含答案位置信息:

ini 复制代码
python
encodings = tokenizer(questions, answers, truncation=True, padding=True, 
                     return_tensors='pt', return_offsets_mapping=True)

# 添加答案的start/end位置
encodings['start_positions'] = ...
encodings['end_positions'] = ...

总结

  1. 自定义Dataset类封装了数据加载逻辑
  2. 必须实现__len____getitem__方法
  3. 与BERT Tokenizer的输出格式配合使用
  4. 可以通过调整来适应分类或QA等不同任务

这种模式使数据加载与模型训练代码分离,提高了代码的可维护性和可复用性。

这个代码的作用是 加载一个预训练的BERT模型,并为其添加一个分类层,使其能够执行特定类别数量的文本分类任务。下面我会用通俗易懂的方式解释它的具体作用和实际应用场景:


🎯 核心作用

ini 复制代码
python
model = BertForSequenceClassification.from_pretrained(
    "bert-base-uncased", 
    num_labels=5
)
  1. 加载预训练知识

    • 从Hugging Face模型库下载bert-base-uncased(一个通用的英语BERT模型),这个模型已经通过海量文本学习了语言理解能力。
    • 相当于你拿到一个"读过千万本书的AI大脑"。
  2. 适配分类任务

    • 通过num_labels=5,在BERT的原始输出层上新增一个分类器,将BERT输出的768维向量转换为5个类别的概率(比如五星评价分类、情感五分类等)。
    • 相当于给AI大脑接上一个"答题卡扫描仪",让它能回答你的具体问题。

🛠️ 实际应用场景

场景示例 num_labels 用途
情感分析 2 (正面/负面) 判断评论是好评还是差评
五星评分 5 预测用户会打1-5星中的哪一星
新闻分类 10 将新闻归类到体育/科技/政治等10个类别
意图识别 8 判断用户提问是咨询/投诉/售后等8种意图

🔧 代码细节拆解

1. 模型选择 "bert-base-uncased"
  • bert-base: 基础版模型(12层Transformer,768隐藏层维度)

  • uncased: 不区分大小写(会把"Apple"和"apple"视为相同)

  • 其他常用选项:

    bash 复制代码
    python
    "bert-large-uncased"  # 更大模型(24层,1024隐藏层)
    "bert-base-chinese"   # 中文BERT
2. 分类头改造 num_labels=5
  • 自动添加的结构

    scss 复制代码
    BERT原始输出 → Dropout层 → 线性层(768输入 → 5输出)
  • 如果是二分类,输出形状就是(batch_size, 2),每行两个数表示两类概率(如[0.2, 0.8])

3. 与普通BERT的区别
ini 复制代码
python
# 普通BERT(仅特征提取)
from transformers import BertModel
base_model = BertModel.from_pretrained("bert-base-uncased")  # 输出768维向量

# 分类专用BERT(带任务接口)
from transformers import BertForSequenceClassification
cls_model = BertForSequenceClassification.from_pretrained(...)  # 直接输出类别概率

📌 关键注意事项

  1. 必须微调(fine-tune)

    预训练BERT只是"知识库",需要用你的标注数据训练分类层(甚至全部参数),例如:

    ini 复制代码
    python
    from transformers import Trainer, TrainingArguments
    
    trainer = Trainer(
        model=model,
        args=TrainingArguments(output_dir="./results"),
        train_dataset=train_data
    )
    trainer.train()
  2. 输入数据需要匹配

    输入文本必须用相同的tokenizer处理:

    ini 复制代码
    python
    from transformers import BertTokenizer
    tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
    inputs = tokenizer("Your text here", return_tensors="pt")
  3. GPU加速建议

    如果可用GPU,运行:

    bash 复制代码
    python
    model.to("cuda")  # 将模型转移到GPU

🌰 举个栗子:餐饮评论分类

假设你想建立一个模型,自动判断食评属于以下哪类:

css 复制代码
python
类别映射 = {0: "差评", 1: "一般", 2: "好评", 3: "强烈推荐", 4: "完美"}

只需:

  1. 准备标注数据(如5000条带标签的食评)
  2. 用上述代码加载num_labels=5的模型
  3. 在数据上微调后,模型就能预测新评论的类别!

总结来说,这行代码是将通用的BERT模型改造成适合你具体任务的分类工具的起点。就像给瑞士军刀安装一个专用刀头,让它能完美解决你的特定问题。

6。handle class imbalance

处理类别不平衡:在损失函数中使用类别权重

在文本分类任务中,经常会遇到类别不平衡的问题(某些类别的样本数量远多于其他类别)。这会导致模型偏向多数类,影响对少数类的识别能力。使用类别权重(class weights)是解决这个问题的有效方法之一。

代码解释

ini 复制代码
python
from sklearn.utils.class_weight import compute_class_weight

# 1. 计算类别权重
class_weights = compute_class_weight(
    "balanced", 
    classes=range(num_classes), 
    y=train_labels
)

# 2. 转换为PyTorch张量并移至设备(GPU/CPU)
weights_tensor = torch.tensor(class_weights, dtype=torch.float).to(device)

参数说明:

  • "balanced":自动计算权重,使各类别的权重与其频率成反比
  • classes=range(num_classes):所有可能的类别标签(如0,1,2,3,4)
  • y=train_labels:训练集中的真实标签列表

计算原理:

对于每个类别c,其权重计算为:

ini 复制代码
weight_c = 总样本数 / (类别数 * 类别c的样本数)

使用方法

1. 在损失函数中应用权重

ini 复制代码
python
import torch.nn as nn

# 创建带权重的交叉熵损失函数
criterion = nn.CrossEntropyLoss(weight=weights_tensor)

# 在训练循环中使用
loss = criterion(outputs, labels)

2. 完整示例

ini 复制代码
python
from transformers import BertForSequenceClassification, Trainer, TrainingArguments
from sklearn.utils.class_weight import compute_class_weight
import numpy as np
import torch

# 假设我们有5个类别和训练标签
num_classes = 5
train_labels = [0, 1, 1, 2, 2, 2, 3, 4]  # 示例标签(明显不平衡)

# 计算类别权重
class_weights = compute_class_weight(
    "balanced",
    classes=np.unique(train_labels),
    y=train_labels
)
weights_tensor = torch.tensor(class_weights, dtype=torch.float)

# 自定义带权重的Trainer
class WeightedTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False):
        labels = inputs.pop("labels")
        outputs = model(**inputs)
        loss = torch.nn.functional.cross_entropy(
            outputs.logits, 
            labels, 
            weight=weights_tensor.to(outputs.logits.device)
        )
        return (loss, outputs) if return_outputs else loss

# 初始化模型和训练参数
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=num_classes)
training_args = TrainingArguments(output_dir="./results")

# 创建并运行训练器
trainer = WeightedTrainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset
)
trainer.train()

注意事项

  1. 权重计算时机:应在划分训练/验证集后,仅基于训练集标签计算权重

  2. 极端不平衡情况:对于极度不平衡的数据(如1:100),可能需要结合其他技术:

    • 过采样少数类(如SMOTE)
    • 欠采样多数类
    • 分层抽样确保每批次的类别平衡
  3. 验证集评估:即使训练时使用权重,验证指标仍应关注原始分布或使用宏观平均

  4. 多标签任务 :对于多标签分类,需要使用BCEWithLogitsLoss并设置pos_weight

  5. 权重调整:可以手动调整权重,给特别重要的类别更高权重

通过这种方法,模型在训练时会更加关注样本较少的类别,从而改善整体分类性能,特别是在需要平衡召回率和精确度的场景中。

通俗解释:用"班级投票"理解类别权重

想象你是一个老师,班上要评选"最受欢迎班干部",有5个职位候选(班长、学习委员、文体委员、生活委员、纪律委员),但学生们投票时出现了问题:

问题情景

  • 班长有40个支持者(全班80人里占一半)
  • 学习委员有20个支持者
  • 其他三个委员各自只有5-6个支持者

如果直接按票数决定,班长肯定每次都赢,其他委员永远没机会------这就是类别不平衡问题。

解决办法:给少数派"加分"

你作为老师,决定这样调整规则:

  1. 计算权重:给票数少的职位额外加分

    • 班长票数多 → 每票算0.5分
    • 学习委员 → 每票算1分
    • 其他委员 → 每票算2分(因为支持者最少)
  2. 重新计票

    • 班长:40票 × 0.5 = 20分
    • 学习委员:20票 × 1 = 20分
    • 文体委员:6票 × 2 = 12分
    • (其他委员同理)

这样虽然班长原始票数最多,但经过加权后和学习委员持平,小职位也有机会被看到。

对应到AI模型

  1. BERT就像全班同学:默认情况下会倾向于选择数据量大的类别(就像同学都投熟人)

  2. 类别权重就是加分规则

    • 让模型在训练时更关注样本少的类别
    • 防止模型"偷懒"只学多数类
  3. 效果:就像公平的选举结果,模型会对所有类别都保持敏感

就像老师调节投票规则保证公平性,我们调节损失函数的权重来保证模型对所有类别都"一视同仁"。

7.Defined Training Parameters

通俗解释:定义训练参数(Define Training Parameters)的作用

简单来说,这一步就是告诉计算机:
"你要用什么样的方式和节奏来训练这个AI模型?"


🔍 为什么需要定义训练参数?

想象你在教一个学生做数学题:

  • 学多久? → 训练多少轮(epochs)
  • 一次做几道题? → 每次喂多少数据(batch size)
  • 学多快? → 学习率(learning rate)
  • 要不要记笔记? → 是否保存检查点(save_steps)
  • 在哪里考试? → 验证集评估(evaluation_strategy)

这些设置直接影响模型的学习效果训练速度


📝 常见的训练参数(以Hugging Face的TrainingArguments为例)

ini 复制代码
python
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./results",          # 训练结果保存路径(像作业本放哪)
    num_train_epochs=3,             # 训练3轮(把数据集学3遍)
    per_device_train_batch_size=8,  # 每次用8条数据训练(一次做8道题)
    per_device_eval_batch_size=16,  # 验证时每次用16条数据
    learning_rate=2e-5,            # 学习速率(小步慢走 vs 大步快跑)
    evaluation_strategy="epoch",    # 每轮结束后考一次试(验证)
    save_strategy="epoch",         # 每轮结束后保存一次模型
    logging_steps=100,             # 每100步打印一次日志
    load_best_model_at_end=True,   # 训练完后自动加载效果最好的模型
)

💡 关键参数的作用

参数名 作用类比 典型值
num_train_epochs 把教材反复学几遍 3-5(太多会死记硬背)
batch_size 一次做多少题(太大显存爆炸,太小速度慢) 8/16/32
learning_rate 调整参数时的步幅(太大错过答案,太小学得慢) 2e-5 到 5e-5
evaluation_strategy 什么时候检查学习效果(随时考 or 学完一章考) "steps"或"epoch"
save_steps 隔多久保存一次进度(防崩溃丢失) 500或"epoch"

⚠️ 注意事项

  1. 学习率(LR)

    • BERT通常用很小的LR(如2e-5),因为预训练模型已经很接近目标,只需微调。
    • 太大容易"学歪",太小训练慢。
  2. Batch Size

    • 取决于你的GPU显存(8GB显存建议≤16)。
  3. Epochs

    • 太多会导致过拟合(像学生死记硬背,但不会举一反三)。
  4. 验证集监控

    • load_best_model_at_end可以自动保存效果最好的版本。

8 Evaluation and Tune

简单来说:
"考试 + 改错题 + 调整学习方法"

就像老师教学生做题:

  1. 考试(Evaluate) → 用测试集看看模型学得怎么样
  2. 改错题(Analyze) → 分析哪些题错得多(比如某个类别总错)
  3. 调整学习方法(Tune) → 改变学习策略(比如学慢点、多练错题)

🔍 详细拆解

1️⃣ 评估(Evaluate)------ "考完试批卷子"

目的 :检查模型在没见过的数据(测试集)上表现如何。
关键指标

  • 准确率(Accuracy) :整体对了多少题
  • F1分数(类别不平衡时更重要)
  • 混淆矩阵:哪些类别容易混淆(比如猫狗分不清)

代码示例

ini 复制代码
python
from sklearn.metrics import accuracy_score, f1_score

# 模型预测测试集
predictions = model.predict(test_encodings)
pred_labels = predictions.logits.argmax(dim=1)

# 计算准确率和F1
accuracy = accuracy_score(test_labels, pred_labels)
f1 = f1_score(test_labels, pred_labels, average="weighted")
print(f"准确率: {accuracy:.2f}, F1分数: {f1:.2f}")

2️⃣ 调优(Tune)------ "改学习方法"

如果考得不好,可能是以下问题:

问题 可能原因 调优方法
准确率低 学习不够/学过头 增加epochs / 减小学习率
某些类别总错 数据不平衡 加类别权重 / 过采样少数类
训练集好但测试集差 过拟合(死记硬背) 加Dropout / 早停 / 数据增强
训练速度太慢 Batch Size太小 / 模型太大 增大Batch Size / 换轻量模型

常见调优操作

A. 调整超参数
ini 复制代码
python
training_args = TrainingArguments(
    learning_rate=5e-5,  # 原先是2e-5,试试加大
    per_device_train_batch_size=32,  # 原先16,试试翻倍
    num_train_epochs=5,   # 原先3轮,加2轮
)
B. 处理数据不平衡
ini 复制代码
python
# 方法1:加类别权重(少数类错误惩罚更大)
loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights)

# 方法2:过采样少数类(复制数据)
from imblearn.over_sampling import RandomOverSampler
ros = RandomOverSampler()
X_resampled, y_resampled = ros.fit_resample(train_texts, train_labels)
C. 防过拟合
ini 复制代码
python
# 加Dropout(让模型别太依赖某些特征)
model = BertForSequenceClassification.from_pretrained(
    "bert-base-uncased",
    num_labels=5,
    hidden_dropout_prob=0.2,  # 默认0.1,调高
)

3️⃣ 自动化调优工具

如果手动调太麻烦,可以用工具自动搜索最佳参数:

Optuna(超参数搜索)
ini 复制代码
python
import optuna

def objective(trial):
    lr = trial.suggest_float("learning_rate", 1e-5, 5e-5)
    batch_size = trial.suggest_categorical("batch_size", [16, 32, 64])
    
    training_args = TrainingArguments(
        learning_rate=lr,
        per_device_train_batch_size=batch_size,
        ...
    )
    trainer = Trainer(model, args=training_args, ...)
    trainer.train()
    return trainer.evaluate()["eval_f1"]  # 以F1分数为目标优化

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=10)  # 尝试10组参数
print("最佳参数:", study.best_params)

🌰 举个实际例子

任务 :电商评论分类(好评/中评/差评)
问题 :模型总是把"中评"预测成"好评"
调优步骤

  1. 评估发现:中评的F1只有0.5(好评0.9,差评0.8)

  2. 分析原因:中评数据只有好评的1/3

  3. 调优操作

    • 给中评加3倍权重
    • 增加中评的过采样数据
    • 降低学习率(从2e-5 → 1e-5)
  4. 结果:中评F1提升到0.7,整体更平衡


✅ 总结

  • 评估(Evaluate) → 定期考试,避免闭门造车

  • 调优(Tune) → 针对性改进,像老师因材施教

  • 核心原则

    • 数据问题 → 加数据/改权重
    • 模型问题 → 调参数/改结构
    • 效率问题 → 改Batch Size/换硬件

最终目标:让模型像学霸一样,不偏科、不死记、高效学习! 🚀

相关推荐
程序员阿鹏5 分钟前
实现SpringBoot底层机制【Tomcat启动分析+Spring容器初始化+Tomcat 如何关联 Spring容器】
java·spring boot·后端·spring·docker·tomcat·intellij-idea
Asthenia041225 分钟前
HTTPS 握手过程与加密算法详解
后端
刘大猫2642 分钟前
Arthas sc(查看JVM已加载的类信息 )
人工智能·后端·算法
Asthenia04121 小时前
操作系统/进程线程/僵尸进程/IPC与PPC/进程大小/进程的内存组成/协程相关/Netty相关拷打
后端
Asthenia04122 小时前
深入解析 MySQL 执行更新语句、查询语句及 Redo Log 与 Binlog 一致性
后端
杨充2 小时前
10.接口而非实现编程
后端
等什么君!2 小时前
springmvc入门案例
后端·spring
苏三说技术3 小时前
基于SpringBoot的课程管理系统
java·spring boot·后端
桦说编程3 小时前
警惕AI幻觉!Deepseek对Java线程池中断机制的理解有误
java·后端·deepseek
用户276174834213 小时前
GitLab-CE 及 GitLab Runner 安装部署
后端