ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BERT图书多分类实战:从微调到混淆矩阵的课设方案

BERT图书多分类实战:从微调到混淆矩阵的课设方案 简介这份资源是面向高校学生与Python学习者的课程设计级项目围绕BERT模型完成图书多分类任务适合作为期末大作业、课设提交或NLP入门实战参考。压缩包共15个文件以9个Python源码文件为主辅以少量缓存与版本控制相关文件整体约15KB体量轻便下载后可直接运行调试。项目结构涵盖数据处理、模型定义、训练与预测等模块包含数据集与配置脚本便于理解BERT在多分类场景下的完整流程。已有38人学习下载说明其具备一定参考价值。读者可从中获得一套可复用的文本分类代码框架、数据组织方式与训练思路既能快速完成课设任务也能在此基础上替换数据集开展自己的分类实验适合需要高分课设或希望掌握BERT实战的读者。1. 图书多分类项目从 BERT 微调到混淆矩阵一套能跑通的课设方案做图书分类的课设最怕的不是模型跑不起来而是跑起来之后不知道哪里出了问题。我见过太多同学拿着 BERT 的预训练权重把图书标题和简介一股脑塞进模型训练完准确率看着还行一画混淆矩阵发现全堆在某一类上——这就是典型的类别不均衡加标签噪声。这个项目要解决的核心问题很具体给定图书的文本信息书名、作者、简介、分类标签用 BERT 做多分类输出每本书属于哪个类别并且用混淆矩阵把每一类的误判情况看清楚。适合正在做 NLP 课设、需要一份能复现的 BERT 多分类源码和数据集的同学也适合想搞明白 BERT 微调到底怎么调参、怎么评估的从业者。下面从数据准备到模型部署把每一步拆开讲。2. 图书数据集的获取与清洗别急着喂给 BERT2.1 图书多分类数据集长什么样图书多分类的数据集通常包含这几个字段书名、作者、出版社、出版年份、简介、类别标签。类别数量从十几类到几十类不等常见的有文学、历史、计算机、经济管理、艺术、教育等。数据来源一般是图书电商平台的公开信息、图书馆编目数据或者公开的图书元数据集。我一般会先看三个指标总样本量、类别分布、文本长度分布。总样本量低于 5000 条的话BERT 微调容易过拟合类别分布如果最大类和最小类差 10 倍以上必须做处理文本长度超过 512 个 token 的简介需要截断但截断策略会影响分类效果。提示不要一上来就用全部字段拼接。书名和简介的信息量最大作者和出版社可以作为辅助特征但直接拼进去可能引入噪声。2.2 用 pandas 做数据清洗的四个步骤拿到原始数据后先做一轮清洗。下面这段代码是我常用的清洗流程每一步都有明确目的。import pandas as pd import re # 读取原始数据假设是 CSV 格式 df pd.read_csv(books_raw.csv) # 步骤1去掉完全重复的行 df df.drop_duplicates(subset[title, description], keepfirst) # 步骤2处理缺失值简介缺失的用书名填充类别缺失的直接丢弃 df[description] df[description].fillna(df[title]) df df.dropna(subset[category]) # 步骤3文本清洗去掉 HTML 标签、多余空白、特殊符号 def clean_text(text): text re.sub(r[^], , str(text)) # 去 HTML 标签 text re.sub(r\s, , text) # 合并空白 text re.sub(r[^\w\s\u4e00-\u9fff], , text) # 保留中文、英文、数字 return text.strip() df[title] df[title].apply(clean_text) df[description] df[description].apply(clean_text) # 步骤4过滤掉文本长度过短的样本 df[text_len] df[title].str.len() df[description].str.len() df df[df[text_len] 10] print(f清洗后样本数{len(df)}) print(df[category].value_counts())这段代码的逻辑很直接先去重再补缺失然后清洗文本最后过滤掉太短的样本。参数方面text_len 10这个阈值可以根据实际数据调整如果书名普遍很短可以降到 5。value_counts()的输出一定要看如果发现某个类别只有几十条样本要么合并类别要么在训练时用类别权重。2.3 类别不均衡的处理策略图书数据里文学类往往占大头小众类别可能只有几百条。直接训练的话模型会倾向于预测大类。我一般用两种方式处理一是对少数类做数据增强比如同义词替换、回译二是用加权损失函数让模型对少数类的错误更敏感。加权损失在 PyTorch 里实现很简单from sklearn.utils.class_weight import compute_class_weight import numpy as np import torch # 计算类别权重 class_weights compute_class_weight( balanced, classesnp.unique(df[label]), ydf[label] ) class_weights torch.tensor(class_weights, dtypetorch.float) # 在损失函数里传入权重 criterion torch.nn.CrossEntropyLoss(weightclass_weights)compute_class_weight的balanced模式会根据类别频率自动计算权重频率越低权重越高。这样模型在训练时少数类的损失会被放大梯度更新更关注这些样本。注意权重不要设得过于极端否则模型会过度偏向少数类导致大类准确率暴跌。3. BERT 多分类模型搭建从预训练权重到分类头3.1 为什么选 BERT 而不是 TextCNN 或 LSTM图书分类这个任务文本长度中等语义信息丰富类别之间边界模糊。TextCNN 和 LSTM 在短文本分类上够用但对长距离依赖和上下文语义的捕捉不如 BERT。BERT 的预训练权重已经学到了通用的语言表示微调时只需要少量数据就能达到不错的效果。我实测过同样的图书数据集BERT 比 TextCNN 的 F1 高 8 到 12 个百分点。当然BERT 的推理速度慢一些如果对延迟要求高可以考虑蒸馏后的模型。3.2 用 HuggingFace 加载 BERT 并改造分类头下面是用transformers库搭建分类模型的代码。我一般用bert-base-chinese作为基座因为图书数据以中文为主。from transformers import BertTokenizer, BertForSequenceClassification import torch # 加载分词器和模型 model_name bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_name) model BertForSequenceClassification.from_pretrained( model_name, num_labelslen(df[label].unique()) # 类别数 ) # 文本编码 def encode_text(texts, max_len256): return tokenizer( texts, paddingmax_length, truncationTrue, max_lengthmax_len, return_tensorspt ) # 假设已经划分好训练集和验证集 train_encodings encode_text(train_texts.tolist()) val_encodings encode_text(val_texts.tolist())num_labels必须和实际类别数一致否则分类头维度对不上。max_length设为 256 是折中方案图书简介一般不会超过这个长度设太大显存吃不消。paddingmax_length会让所有样本补齐到同一长度方便批量训练但会浪费一些计算资源如果显存紧张可以改成longest。3.3 训练循环与关键参数设置训练循环里学习率、批次大小、训练轮数是三个最关键的参数。我一般用2e-5的学习率批次大小 16 或 32训练 3 到 5 轮。from torch.utils.data import DataLoader, TensorDataset from transformers import AdamW, get_linear_schedule_with_warmup # 构建 Dataset train_dataset TensorDataset( train_encodings[input_ids], train_encodings[attention_mask], torch.tensor(train_labels) ) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) # 优化器和调度器 optimizer AdamW(model.parameters(), lr2e-5) total_steps len(train_loader) * 3 # 训练3轮 scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps ) # 训练 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) model.train() for epoch in range(3): for batch in train_loader: input_ids, attention_mask, labels [b.to(device) for b in batch] optimizer.zero_grad() outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss loss.backward() optimizer.step() scheduler.step() print(fEpoch {epoch1}, Loss: {loss.item():.4f})AdamW是 BERT 微调的标配优化器get_linear_schedule_with_warmup让学习率在前 10% 的步数里线性上升然后线性下降。这个策略能防止训练初期梯度震荡。批次大小 16 在 8GB 显存的卡上能跑如果显存更大可以调到 32。训练轮数不要超过 5否则容易过拟合验证集损失会回升。4. 模型评估与混淆矩阵看懂每一类的误判4.1 用 sklearn 生成混淆矩阵和分类报告训练完之后光看准确率不够必须看混淆矩阵。下面这段代码输出混淆矩阵和分类报告。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds [] all_labels [] with torch.no_grad(): for batch in val_loader: input_ids, attention_mask, labels [b.to(device) for b in batch] outputs model(input_ids, attention_maskattention_mask) preds torch.argmax(outputs.logits, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 混淆矩阵 cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix) plt.savefig(confusion_matrix.png) # 分类报告 print(classification_report(all_labels, all_preds, target_nameslabel_names))confusion_matrix的输出是一个二维数组行是真实标签列是预测标签。对角线上的数字越大越好非对角线上的数字就是误判。classification_report会输出每一类的精确率、召回率和 F1 值。如果某一类的召回率特别低说明模型漏判了很多如果精确率低说明误判成了其他类。4.2 混淆矩阵里常见的三种问题模式第一种所有样本都预测成大类。这说明类别不均衡没处理好或者学习率太大导致模型没学到东西。解决办法是加类别权重或者先冻结 BERT 底层只训练分类头。第二种两个类别互相误判严重。比如“计算机”和“科技”互相混淆说明这两个类别的文本特征太接近。解决办法是检查数据标注是否准确或者增加更多区分性特征。第三种某些类别完全预测不出来。这通常是因为这些类别的样本太少模型没见过足够的例子。解决办法是数据增强或者合并类别。注意混淆矩阵的标签顺序要和label_names对应否则图看懂了但名字对不上白忙一场。5. 避坑与排查图书分类项目里最容易翻车的五个地方5.1 标签编码不一致导致训练全错现象训练时 loss 正常下降但验证集准确率始终在随机水平附近。原因训练集和验证集的标签编码方式不一致比如训练集用LabelEncoder从 0 开始编码验证集却用了原始字符串标签。解决在划分数据集之前就统一做标签编码保存编码映射表训练和验证都用同一套。5.2 分词器截断把关键信息切掉了现象某些类别的分类效果特别差检查发现这些类别的图书简介很长关键信息在末尾被截断了。原因max_length设得太小或者truncationTrue直接切掉了后半部分。解决把max_length调到 512或者对长文本做分段处理取每段预测结果的平均。5.3 学习率太大导致模型震荡现象训练 loss 忽高忽低验证集准确率波动超过 5 个百分点。原因学习率设成了1e-3甚至更大BERT 微调一般用2e-5到5e-5。解决降低学习率加 warmup观察前 100 步的 loss 曲线是否平滑。5.4 验证集划分不合理导致评估失真现象验证集准确率很高但换一批数据测试就崩了。原因验证集是从训练集里随机划分的和训练集分布太接近没有代表性。解决按类别分层抽样划分验证集确保每个类别在验证集里的比例和整体一致。5.5 显存不足导致训练中断现象训练到一半报CUDA out of memory。原因批次太大或者max_length太长或者没有用torch.no_grad()做验证。解决减小批次大小开启梯度累积验证时用with torch.no_grad():包起来。6. 进阶技巧用混淆矩阵反推数据问题混淆矩阵不只是评估工具还能反过来指导数据清洗。我一般会做一件事把混淆矩阵里误判最多的那几对类别拎出来人工看几十条样本判断是标注错了还是文本本身太像。如果是标注错了修正标签后重新训练准确率往往能涨 3 到 5 个百分点。如果是文本太像就考虑合并类别或者增加外部特征。另一个技巧是用classification_report里的support列对照混淆矩阵。如果某一类的support很小但误判很多说明这类样本太少模型没学好。这时候可以针对性地对这类样本做数据增强比如用同义词替换生成新样本。# 找出误判最多的类别对 import numpy as np cm confusion_matrix(all_labels, all_preds) # 把对角线置零只看误判 cm_offdiag cm.copy() np.fill_diagonal(cm_offdiag, 0) # 找出误判最多的前5对 top_pairs np.dstack(np.unravel_index(np.argsort(cm_offdiag.ravel())[::-1], cm.shape))[0][:5] for true_idx, pred_idx in top_pairs: print(f真实类别 {label_names[true_idx]} 被误判为 {label_names[pred_idx]}{cm_offdiag[true_idx][pred_idx]} 次)这段代码先把混淆矩阵对角线清零然后按误判次数排序输出前 5 对最容易混淆的类别。拿到这个列表后就可以有针对性地去检查这些类别的数据质量。我自己的习惯是每次训练完必看混淆矩阵不看准确率就下结论。这个习惯帮我省了很多后悔药也让我对数据的理解越来越深。希望帮到你。本文还有配套的精品资源点击获取
返回列表