Bert实战-酒店评价情感分类(八)

一、序

该项目预训练模型源自huggingface官网google-bert/bert-base-chinese,在本地计算机上我们只进行下游任务,通过微调预训练模型实现基于 BERT 的酒店评价情感分类。

google-bert/bert-base-chinese at main (hf-mirror.com)

一段话概述:

李哥深度学习六--Bert实战-优快云博客

这篇博客概括得很好

二、Bert的具体训练框架

2.1 目录架构

从上往下说,

  • .idea目录,含检查配置、模块配置等 XML 文件,不用管
  • bert-base-chinese目录,预训练模型文件,包含以下内容

这里的config,json代码如下:
 

{
  "architectures": [
    "BertForMaskedLM"  // 模型架构类型,适用于掩码语言模型预训练任务
  ],
  "attention_probs_dropout_prob": 0.1,  // 注意力权重的dropout概率,防止过拟合
  "directionality": "bidi",  // 双向Transformer(BERT的核心特性)
  "hidden_act": "gelu",  // 隐藏层激活函数为GELU(Gaussian Error Linear Unit)
  "hidden_dropout_prob": 0.1,  // 隐藏层输出的dropout概率
  "hidden_size": 768,  // 隐藏层维度大小,即Transformer编码器的输出维度
  "initializer_range": 0.02,  // 权重初始化的标准差范围
  "intermediate_size": 3072,  // 前馈网络层的中间维度大小(通常为hidden_size的4倍)
  "layer_norm_eps": 1e-12,  // LayerNorm层的epsilon值,防止数值不稳定
  "max_position_embeddings": 512,  // 最大位置编码,支持的输入序列最大长度
  "model_type": "bert",  // 模型类型标识为BERT
  "num_attention_heads": 12,  // 多头注意力机制中的注意力头数量
  "num_hidden_layers": 12,  // Transformer编码器的层数(对应BERT-base)
  "pad_token_id": 0,  // 填充标记的ID,用于批次处理时对齐序列长度
  "pooler_fc_size": 768,  // 池化层全连接层的维度大小
  "pooler_num_attention_heads": 12,  // 池化层注意力头数量
  "pooler_num_fc_layers": 3,  // 池化层中全连接层的数量
  "pooler_size_per_head": 128,  // 每个注意力头的维度大小
  "pooler_type": "first_token_transform",  // 池化类型,使用[CLS]标记作为句子表示
  "type_vocab_size": 2,  // 句子类型词汇表大小(用于区分句子对A和B)
  "vocab_size": 21128  // 词汇表大小,决定了模型能够处理的不同token数量
}

tokenizer.json部分代码示例如下:

就是分词器,原理和上一小节介绍的原理一样。

  • model_save 目录,存储训练过程中保存的模型 checkpoint(如 best_model.pth5ckpt),用于模型持久化和恢复训练。
  • model_utils 目录(模型工具模块)

  • 执行与测试文件

  • 数据集文件

2.2 数据预处理

data.py

# data负责产生两个dataloader,即训练集和验证集
from torch.utils.data import DataLoader, Dataset
from sklearn.model_selection import train_test_split   # 给X,Y和分割比例,分割出训练集和验证集的X, Y
import torch


def read_file(path):
    """读取文本文件,返回数据和标签列表"""
    data = []  # 存储文本内容
    label = []  # 存储对应标签

    with open(path, "r", encoding="utf-8") as f:
        for i, line in enumerate(f):
            if i == 0:  # 跳过标题行
                continue
            if i > 200 and i < 7500:  # 跳过中间部分数据(可能是为了减少样本量)
                continue
            line = line.strip("\n")  # 去除换行符
            line = line.split(",", 1)  # 按逗号分割,只分割一次(处理文本中可能包含的逗号)
            data.append(line[1])  # 文本内容
            label.append(line[0])  # 标签
    print(f"读取了{len(data)}条数据")
    return data, label


# file = "../jiudian.txt"
# read_file(file)

class jdDataset(Dataset):
    """自定义数据集类,用于包装文本数据和标签"""
    def __init__(self, data, label):
        self.X = data  # 文本数据
        self.Y = torch.LongTensor([int(i) for i in label])  # 标签转为LongTensor类型

    def __getitem__(self, item):
        return self.X[item], self.Y[item]  # 返回索引对应的文本和标签

    def __len__(self):
        return len(self.Y)  # 返回数据集大小


def get_data_loader(path, batchsize, val_size=0.2):
    """读取数据,分割为训练集和验证集,并返回对应的DataLoader"""
    data, label = read_file(path)  # 读取文件
    # 按指定比例分割训练集和验证集,保持类别分布相同(stratify=label)
    train_x, val_x, train_y, val_y = train_test_split(data, label, test_size=val_size, shuffle=True, stratify=label)
    train_set = jdDataset(train_x, train_y)  # 创建训练集
    val_set = jdDataset(val_x, val_y)  # 创建验证集
    train_loader = DataLoader(train_set, batchsize, shuffle=True)  # 训练集数据加载器
    val_loader = DataLoader(val_set, batchsize, shuffle=True)  # 验证集数据加载器
    return train_loader, val_loader

if __name__ == "__main__":
    get_data_loader("../waimai.txt", 2)  # 测试数据加载功能,批次大小为2

2.3 模型搭建

model.py

import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer, BertConfig


class myBertModel(nn.Module):
    """基于BERT的文本分类模型"""
    def __init__(self, bert_path, num_class, device):
        """
        初始化模型
        :param bert_path: BERT预训练模型路径
        :param num_class: 分类类别数量
        :param device: 运行设备(CPU/GPU)
        """
        super(myBertModel, self).__init__()
        
        # 加载预训练的BERT模型
        self.bert = BertModel.from_pretrained(bert_path)
        # 也可以通过配置文件加载模型(当前被注释)
        # config = BertConfig.from_pretrained(bert_path)
        # self.bert = BertModel(config)
        
        self.device = device
        # 分类头:将BERT的768维输出映射到指定类别数
        self.cls_head = nn.Linear(768, num_class)
        # 加载BERT分词器
        self.tokenizer = BertTokenizer.from_pretrained(bert_path)

    def forward(self, text):
        """
        前向传播过程
        :param text: 输入文本列表
        :return: 分类预测结果
        """
        # 对输入文本进行分词处理
        input = self.tokenizer(text, 
                              return_tensors="pt",  # 返回PyTorch张量
                              truncation=True,      # 截断过长文本
                              padding="max_length", # 填充至最大长度
                              max_length=128)       # 最大序列长度
        
        # 将输入张量移至指定设备
        input_ids = input["input_ids"].to(self.device)
        token_type_ids = input['token_type_ids'].to(self.device)
        attention_mask = input['attention_mask'].to(self.device)
        
        # 通过BERT模型获取输出
        # sequence_out: 每个token的隐藏状态 [batch_size, seq_len, 768]
        # pooler_out: [CLS]标记的聚合表示 [batch_size, 768]
        sequence_out, pooler_out = self.bert(input_ids=input_ids,
                        token_type_ids=token_type_ids,
                        attention_mask=attention_mask,
                        return_dict=False)      # 使用元组形式返回结果
        
        # 通过分类头进行预测
        pred = self.cls_head(pooler_out)
        return pred

if __name__ == "__main__":
    # 设置运行设备
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    
    # 实例化模型并移至指定设备
    model = myBertModel("../bert-base-chinese", 2, device)  # 二分类任务
    model.to(device)  # 将模型参数移至GPU/CPU
    
    # 示例预测
    text = "今天天气真好"
    model.eval()  # 设置为评估模式(关闭dropout等训练特有的层)
    with torch.no_grad():  # 关闭梯度计算,节省内存和计算资源
        pred = model(text)
    print(pred)  # 输出预测结果(未经过softmax,为logits值)

2.4 训练与验证

train.py

import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm

def train_val(para):
    """
    模型训练和验证的主函数
    :param para: 参数字典,包含模型、数据加载器、优化器等训练所需组件
    """
    ########################################################
    # 从参数字典中解包训练所需组件
    model = para['model']
    train_loader = para['train_loader']
    val_loader = para['val_loader']
    scheduler = para['scheduler']
    optimizer = para['optimizer']
    loss = para['loss']
    epoch = para['epoch']
    device = para['device']
    save_path = para['save_path']
    max_acc = para['max_acc']
    val_epoch = para['val_epoch']

    #################################################
    # 初始化训练和验证过程的记录列表
    plt_train_loss = []  # 训练损失记录
    plt_train_acc = []   # 训练准确率记录
    plt_val_loss = []    # 验证损失记录
    plt_val_acc = []     # 验证准确率记录
    val_rel = []         # 验证预测结果记录

    for i in range(epoch):
        start_time = time.time()  # 记录每个epoch开始时间
        model.train()             # 设置模型为训练模式
        train_loss = 0.0          # 累积训练损失
        train_acc = 0.0           # 累积训练准确率
        val_acc = 0.0             # 累积验证准确率
        val_loss = 0.0            # 累积验证损失

        # 训练循环,使用tqdm显示进度条
        for batch in tqdm(train_loader):
            model.zero_grad()  # 清除上一步的梯度
            text, labels = batch[0], batch[1].to(device)  # 获取文本和标签并移至设备
            pred = model(text)  # 前向传播
            bat_loss = loss(pred, labels)  # 计算损失
            bat_loss.backward()  # 反向传播
            optimizer.step()  # 更新参数
            scheduler.step()  # 学习率调度器更新学习率
            optimizer.zero_grad()  # 梯度清零
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # 梯度裁剪,防止梯度爆炸
            train_loss += bat_loss.item()  # 累积损失
            # 累积正确预测数
            train_acc += np.sum(np.argmax(pred.cpu().data.numpy(), axis=1) == labels.cpu().numpy())

        # 计算并记录训练损失和准确率
        plt_train_loss.append(train_loss / train_loader.dataset.__len__())
        plt_train_acc.append(train_acc / train_loader.dataset.__len__())

        # 每val_epoch次执行一次验证
        if i % val_epoch == 0:
            model.eval()  # 设置模型为评估模式
            with torch.no_grad():  # 关闭梯度计算,节省内存和计算资源
                for batch in tqdm(val_loader):
                    val_text, val_labels = batch[0], batch[1].to(device)  # 获取验证数据
                    val_pred = model(val_text)  # 前向传播
                    val_bat_loss = loss(val_pred, val_labels)  # 计算损失
                    val_loss += val_bat_loss.cpu().item()  # 累积损失
                    # 累积正确预测数
                    val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_labels.cpu().numpy())
                    val_rel.append(val_pred)  # 记录预测结果

            # 如果当前验证准确率高于之前的最高准确率,保存模型
            if val_acc > max_acc:
                torch.save(model, save_path + str(epoch) + "ckpt")
                max_acc = val_acc

            # 记录验证损失和准确率
            plt_val_loss.append(val_loss / val_loader.dataset.__len__())
            plt_val_acc.append(val_acc / val_loader.dataset.__len__())

            # 打印训练和验证指标
            print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f  ' % \
                  (i, epoch, time.time() - start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1]))

            # 每50个epoch保存一次模型
            if i % 50 == 0:
                torch.save(model, save_path + '-epoch:' + str(i) + '-%.2f' % plt_val_acc[-1])
        else:
            # 如果不执行验证,沿用上次的验证结果
            plt_val_loss.append(plt_val_loss[-1])
            plt_val_acc.append(plt_val_acc[-1])
            print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f   ' % \
                  (i, epoch, time.time() - start_time, plt_train_acc[-1], plt_train_loss[-1]))

main.py

import random
import torch
import torch.nn as nn
import numpy as np
import os
# 导入工具库
# random:设置随机种子
# torch:PyTorch深度学习框架
# torch.nn:神经网络模块
# numpy:数值计算库
# os:操作系统相关功能

from model_utils.data import get_data_loader
from model_utils.model import myBertModel
from model_utils.train import train_val
# 从自定义模块导入功能:
# data模块获取数据加载器
# model模块获取自定义BERT模型
# train模块获取训练验证主函数


def seed_everything(seed):
    """设置全局随机种子,确保实验可复现"""
    torch.manual_seed(seed)            # 设置CPU随机种子
    torch.cuda.manual_seed(seed)        # 设置当前GPU随机种子
    torch.cuda.manual_seed_all(seed)    # 设置所有GPU随机种子
    torch.backends.cudnn.benchmark = False  # 关闭CuDNN自动优化(保证确定性)
    torch.backends.cudnn.deterministic = True  # 强制CuDNN使用确定性算法
    random.seed(seed)                   # 设置Python内置随机种子
    np.random.seed(seed)                # 设置NumPy随机种子
    os.environ['PYTHONHASHSEED'] = str(seed)  # 固定Python哈希种子
#################################################################
seed_everything(0)  # 设置随机种子为0,确保实验可复现
###############################################


# 超参数配置
lr = 0.0001                     # 学习率(AdamW优化器初始学习率)
batchsize = 16                  # 批量大小(每次输入模型的样本数)
loss = nn.CrossEntropyLoss()    # 损失函数(适用于多分类任务)
bert_path = "bert-base-chinese" # 预训练模型路径(中文BERT-base)
num_class = 2                   # 分类类别数(二分类任务)
data_path = "waimai.txt"        # 数据集路径(外卖文本数据)
max_acc = 0.6                   # 初始最高验证准确率(用于模型保存对比)
device = "cuda" if torch.cuda.is_available() else "cpu"  # 自动检测设备(优先GPU)
model = myBertModel(bert_path, num_class, device).to(device)  # 实例化模型并移至目标设备

# 优化器配置
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.00001)  
# AdamW优化器:
# - model.parameters():优化模型所有参数
# - weight_decay:权重衰减(L2正则化),防止过拟合

# 数据加载
train_loader, val_loader = get_data_loader(data_path, batchsize)  
# 调用数据模块函数:
# - 读取数据并分割为训练集/验证集
# - 返回DataLoader对象,支持批量加载数据

# 训练配置
epochs = 5                       # 训练总轮数(epoch)
save_path = "model_save/best_model.pth"  # 最佳模型保存路径
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=20, eta_min=1e-9)  
# 学习率调度器:
# - CosineAnnealingWarmRestarts:余弦退火+重启机制
# - T_0=20:第一次重启前的epoch数
# - eta_min=1e-9:学习率最小值
val_epoch = 1                    # 验证频率(每1个epoch执行一次验证)


# 参数字典(整合训练所需组件)
para = {
    "model": model,                # 模型对象
    "train_loader": train_loader,  # 训练数据加载器
    "val_loader": val_loader,      # 验证数据加载器
    "scheduler": scheduler,        # 学习率调度器
    "optimizer": optimizer,        # 优化器
    "loss": loss,                  # 损失函数
    "epoch": epochs,               # 总训练轮数
    "device": device,              # 运行设备
    "save_path": save_path,        # 模型保存路径
    "max_acc": max_acc,            # 初始最高准确率
    "val_epoch": val_epoch         # 验证间隔轮数
}

train_val(para)  # 启动训练和验证流程(调用train模块主函数)

2.5 运行

本小节结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值