ARTICLE DETAIL

资讯详情

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

BERT图书多分类实战:数据清洗到评估全流程解析

BERT图书多分类实战:数据清洗到评估全流程解析 简介基于BERT的Python图书多分类课程设计项目集成了完整源码与全部数据集面向NLP初学者及需要完成课程设计、期末大作业的学生。项目采用预训练语言模型BERT进行图书主题多分类覆盖数据预处理、模型构建、训练与评估等模块下载即可直接运行无需修改。压缩包共16个文件包含9个Python脚本如数据处理、训练、测试等、2个编译缓存文件pyc、1个说明文档README及若干git相关文件包体仅14KB轻量易用。目前已有46人学习下载。通过本项目可掌握BERT微调流程、文本分类任务实现细节及数据组织方式适合作为深度学习与自然语言处理方向的实践参考。1. 这个课程设计到底值不值得做先看清 BERT 图书多分类的底细“基于BERT的Python图书多分类项目源码及全数据集课程设计”这类标题在毕业设计和课程作业里出现频率极高。它指向的任务很明确给图书数据做文本多分类类别通常覆盖文学、历史、科技、经济等用 BERT 做特征抽取器跑一个典型的序列分类模型。你要做的不是发明算法而是把一条成熟的 NLP 流水线跑通并拿到一张能放进报告里的指标表。对初学者来说这个项目最大的价值在于“数据、代码、报告三者齐全”。常见做法是拿到一份 CSV 格式的图书数据集每一行是书名或图书简介附带一个类别标签然后通过 Python 脚本转换成 BERT 能吃的 input_ids 和 attention_mask再交给 transformers 库里的 BertForSequenceClassification 做微调。整个过程不需要你从头写模型结构但需要你对数据清洗、分词、batch 拼接、损失函数、类别不平衡这些基础概念有实操认识。这个项目适合的人群很具体Python 基础已过关、会装库、想第一次完整跑通一个深度学习训练流程的人。花一个周末把环境搭好、把代码跑通比抱着论文啃两周更有效。2. 从原始数据集到 BERT 输入图书数据的清洗、切分与编码课程设计里的数据集通常不会太干净。拿到手的第一件事不是写模型而是先打开数据文件确认它的真实形态。常见做法是数据以 CSV 或 Excel 形式存放至少有两列一列是图书标题或简介一列是分类标签。真实课程设计里这可能是老师直接发的一份全数据集也可能是从某图书网站爬下来后再人工标注的产物所以字段名可能五花八门比如 book_title、category、label 或者干脆是中文列名。第一步永远是打印前五行看清楚再说。2.1 图书数据集的常见形态与标签体系我一般会先用 pandas 快速摸底确认数据量、类别数量、是否有空值和重复值。一个比较典型的图书分类数据集大概有几千条样本类别在 5 到 10 个之间。标签体系常见的有两种一种是中文字符串比如“文学”“历史”“科技”另一种是数字编码 0、1、2、3。建议在预处理阶段就把中文字符串转成数字编码因为 BERT 的输出层是一个线性分类头它只认整数标签。import pandas as pd from sklearn.preprocessing import LabelEncoder df pd.read_csv(books_dataset.csv) df.dropna(subset[title, label], inplaceTrue) df.drop_duplicates(subset[title], inplaceTrue) print(df[label].value_counts()) encoder LabelEncoder() df[label_id] encoder.fit_transform(df[label]) # 保存标签映射后面预测和画混淆矩阵都要用 label_mapping dict(zip(encoder.classes_, encoder.transform(encoder.classes_))) print(label_mapping)这段代码做了三件事读入数据、把标题为空或标签为空的样本丢掉、按书名去重。之后用 LabelEncoder 把文本标签转成数字。这里有一个容易被忽略的点fit_transform必须在全量数据上做不能在训练集上 fit 之后再拿同一套编码作用到验证集上否则标签顺序不一致会直接导致模型输出混乱。保存label_mapping的目的是在最后做预测时把数字还原成中文否则报告里的混淆矩阵没法看。2.2 用 BertTokenizer 把图书标题变成 input_idsBERT 不是直接吃原始文本的它需要一个分词器Tokenizer先把文本切成 token再把每个 token 映射成一个整数 ID。中文场景下最常用的预训练权重是 bert-base-chinese它的分词方式是字级别也就是说“机器学习”会被切分成“机”“器”“学”“习”四个 token。这对图书标题这种短文本来说其实很合适因为字级分词不会出现英文那种 OOV词表外单词问题。from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def encode_texts(texts, max_length128): return tokenizer( texts, max_lengthmax_length, paddingmax_length, truncationTrue, return_tensorspt, ) titles df[title].tolist() encoded encode_texts(titles, max_length64) print(encoded[input_ids].shape) print(encoded[attention_mask].shape)这里最关键的是三个参数max_length、padding、truncation。图书标题一般很短十几个字到几十个字不等max_length 设成 64 通常就够了。如果数据里包含图书简介那就设成 128 或 256但要意识到这会让训练和推理变慢。padding 策略我建议直接用 max_length它会把每条样本都补齐到 64 个 token好处是能拼成一个规整的 tensor 一次性丢进 GPU坏处是浪费算力。attention_mask 是 BERT 用来区分真实 token 与 padding token 的长度与 input_ids 一致值为 1 的位置是真实 token0 是 padding。2.3 切分数据与类别平衡验证集不能拍脑袋切分数据不是随便train_test_split一下就完事。图书多分类里最常见的隐患是类别严重不平衡比如“文学”类有 3000 条“艺术”类只有 150 条。如果按默认方式随机切分验证集里很可能没有“艺术”类样本导致你看到验证准确率挺高但模型其实根本没学会这类。常见做法是分层切分这能保证每个类别在训练集和验证集中的比例与全量数据一致。from sklearn.model_selection import train_test_split train_texts, val_texts, train_labels, val_labels train_test_split( titles, df[label_id].tolist(), test_size0.2, stratifydf[label_id].tolist(), random_state42, ) train_encodings encode_texts(train_texts, max_length64) val_encodings encode_texts(val_texts, max_length64) print(len(train_texts), len(val_texts))random_state42是为了让结果可复现不管谁跑这段代码训练集和验证集的划分方式都一样。这一步在实际课程设计里很重要因为老师复现你的实验时如果每次运行结果都不一样他会认为你的项目不稳定。stratify 参数就是分层切分的关键它要求测试集占 20%并且每个类别在切分后保持原比例。切分完成后训练集和验证集的文本和标签要分别编码注意验证集不能用训练集的 Tokenizer 重新编码后再混进来。3. 用 transformers 在本地跑通 BERT 图书分类核心代码与关键参数数据准备好了下一步是加载模型并训练。这一步是整个项目的重心也是最容易卡住的地方。环境依赖方面需要安装 torch 和 transformers建议用 Python 3.8 或 3.10显存不够可以考虑在 Google Colab 上跑免费 T4但这不属于“本地”方案了。先说结论这个项目的模型部分其实很短真正决定成败的是超参数和训练循环的写法。3.1 用 BertForSequenceClassification 搭出分类器transformers 库提供了一个现成的模型类 BertForSequenceClassification它在 BERT 输出的 CLS 向量上面加了一个全连接层类别数是num_labels。你要做的不是从零实现 Transformer 编码器而是决定用哪个预训练权重、微调哪些层、学习率怎么设。from transformers import BertForSequenceClassification num_labels len(label_mapping) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labelsnum_labels, ) print(model.config.num_labels)这段代码就从 Hugging Face 拉取 bert-base-chinese 的预训练权重并在顶部初始化一个分类头。第一次运行会下载大约 400MB 的模型文件网络状况不好时容易中断后续章节会专门讲这个问题。num_labels 必须和你的类别数一致否则最后一层维度对不上前向传播直接报错。这里有人会问要不要冻结 BERT 参数只微调分类头在课程设计的数据量几千条下我的建议是全量微调准确率更高只微调分类头适合数据量极少且想快速验证管线的情况。3.2 训练超参batch_size、学习率、epoch 的实操取值图书多分类属于典型的短文本分类BERT 微调的超参数在社区里已经形成一套比较稳定的经验值。学习率通常取 2e-5 到 5e-5这个区间太小会导致收敛极慢太大会让预训练权重被破坏。epoch 在 3 到 5 之间因为 BERT 原本就在通用语料上训练过它不需要像训练随机初始化的模型那样跑几十个 epoch。batch_size 取决于显存本课程设计的数据量不大batch_size 设 16 或 32 即可。from torch.utils.data import DataLoader, TensorDataset, RandomSampler, SequentialSampler import torch train_dataset TensorDataset( train_encodings[input_ids], train_encodings[attention_mask], torch.tensor(train_labels), ) train_loader DataLoader( train_dataset, batch_size32, samplerRandomSampler(train_dataset), )TensorDataset 把 input_ids、attention_mask、label 三个 tensor 打包成一个可迭代的数据集对象。DataLoader 的 sampler 参数控制取样本的顺序RandomSampler 用来在训练时打乱顺序验证阶段则应该用 SequentialSampler 保持顺序。这里有个细节你不需要手动构造 dataset 类因为本次任务用的默认 tokenizer 已经完成了文本转 ID 的工作。batch_size32 在 12GB 显存下跑 bert-base-chinese 是安全的如果显存只有 6GB降到 16。输入序列长度是 64显存占用其实很小。3.3 训练循环的完整写法手动步进比 Trainer 更可控课程设计里用 Trainer API 当然可以它封装了训练、评估、保存的全部逻辑几行代码就能跑起来。但我更推荐手写训练循环理由是课程设计评审老师会更看重你能否解释清楚 loss 是怎么算的、梯度是怎么更新的、训练和验证之间有什么区别。手写循环并没有多复杂核心是 optimizer、loss 计算、梯度清零和 backward。from transformers import AdamW, get_linear_schedule_with_warmup optimizer AdamW(model.parameters(), lr2e-5) total_steps len(train_loader) * 4 # 4个epoch scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps0, num_training_stepstotal_steps, ) model.train() for epoch in range(4): total_loss 0 for batch in train_loader: input_ids, attention_mask, labels batch outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() print(fepoch {epoch 1}, 平均loss: {total_loss / len(train_loader)})AdamW 是 BERT 微调最常用的优化器它的解耦权重衰减设计在 Transformer 类模型上表现稳定。warmup 步数设置为 0 在本项目里完全够用因为数据量小一步一个脚印稳定下降。loss 取自模型输出的短路返回值它就是 CrossEntropyLoss 对 logits 与 labels 的计算结果。训练时每个 batch 都要经历前向传播、反向传播、参数更新、梯度清零四个步骤。值得注意的地方是 scheduler 必须在 optimizer.step() 之后调用顺序反了会导致学习率更新错位而 optimizer.zero_grad() 放在最后而不是最前是为了避免上一个 batch 的梯度累积到当前 batch。4. 把模型输出变成课程设计报告混淆矩阵、分类指标与可视化模型训练完任务只完成一半。课程设计评分看重的是两种东西一是模型实际效果二是你如何证明它有效。后者靠的是评估环节——你需要把验证集样本交给模型预测然后把预测结果与真实标签逐条对比最终得到一组能让老师看懂的指标。这一章的核心是用 sklearn 的三行代码生成混淆矩阵再用 matplotlib 把矩阵画成图片插进报告。4.1 验证集的准确率与损失计算训练循环里用model.train()切到训练模式评估时则要切到model.eval()。eval 模式会影响模型内部的 dropout 层和 LayerNorm如果忘记切换验证集上的指标会轻微偏高或偏低。另外评估阶段用torch.no_grad()包裹前向传播因为不需要计算梯度这能减少显存占用并提速。from sklearn.metrics import accuracy_score, classification_report import numpy as np model.eval() val_predictions [] val_true_labels [] with torch.no_grad(): for batch in val_loader: input_ids, attention_mask, labels batch outputs model(input_ids, attention_maskattention_mask) logits outputs.logits preds torch.argmax(logits, dim1) val_predictions.extend(preds.cpu().numpy()) val_true_labels.extend(labels.numpy()) acc accuracy_score(val_true_labels, val_predictions) report classification_report( val_true_labels, val_predictions, target_nameslabel_mapping.keys(), digits3, ) print(f验证集准确率: {acc:.4f}) print(report)torch.argmax(logits, dim1)是在每个样本输出的类别概率中挑概率最大的索引这就是预测标签。.cpu().numpy()是因为当前计算结果可能在 GPU 上必须先搬到 CPU 再去转 numpy。classification_report 会按类别输出 precision、recall、F1-score 和每个类的样本数这比只汇报一个准确率有说服力得多。这一步的输出直接复制进课程设计报告即可但要替换掉类别的数字标签这里用了 target_names 参数把数字还原成中文。4.2 python 多分类混淆矩阵代码一张图看懂分错到哪准确率只能说明总体正确率看不出“模型把哪些类别搞混了”。比如历史类图书经常被预测成文学类这在准确率上只表现为一个数字减少但混淆矩阵能让你直观看到错在哪。sklearn 的 confusion_matrix 接收真实标签和预测标签两个一维数组返回一个二维矩阵行是真实类别列是预测类别。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix import seaborn as sns cm confusion_matrix(val_true_labels, val_predictions) plt.figure(figsize(10, 8)) sns.heatmap( cm, annotTrue, fmtd, xticklabelslabel_mapping.keys(), yticklabelslabel_mapping.keys(), cmapBlues, ) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight)seaborn 的 heatmap 会把二维矩阵渲染成色块图颜色越深表示数量越多。annotTrue 让每个格子直接显示数字fmtd 表示按整数格式输出。这一步是报告的可视化核心建议 dpi 设置成 150 以上否则插进 Word 里会模糊。保存图片时用 bbox_inchestight 去掉周围多余的空白边。如果不想引入 seaborn直接plt.imshow(cm, cmapBlues)加注释也能出效果但 seaborn 的美观度更高。4.3 F1 与类别样本数别只盯着准确率准确率在类别不平衡时会骗人。假设“文学”类占 80%模型把所有样本都预测成“文学”准确率能到 80%但这个模型没有任何实用价值。所以课程设计报告里一定要同时给出三个数字宏平均 F1、加权平均 F1、每个类别的样本数。宏平均 F1 是每个类别的 F1 直接取平均它对小类别更敏感加权平均 F1 按样本数加权更贴近实际整体表现。classification_report 里已经列出了这三项建议在报告正文中直接用表格复制过去不要只贴一张图。from sklearn.metrics import f1_score macro_f1 f1_score(val_true_labels, val_predictions, averagemacro) weighted_f1 f1_score(val_true_labels, val_predictions, averageweighted) print(f宏平均F1: {macro_f1:.4f}) print(f加权平均F1: {weighted_f1:.4f})如果宏平均 F1 比加权平均 F1 低很多说明小类别几乎没被正确识别。这时候可以考虑两种应对一是收集更多小类别的样本二是给损失函数加类别权重。在报告中写明“宏平均 F1 与加权平均 F1 的差距反映了类别不平衡问题”这句话能直接体现你对模型的理解深度比堆一堆输出截图更让老师信服。把这三组数据和一张混淆矩阵图放在一起评估环节就已经相当完整了。5. 从数据到训练到评估的 5 个常见坑现象、原因与解决这一章要写的是跑这个项目最容易踩的坑每一类我都遇到过并且能给出具体的排查路径。很多人把 BERT 跑起来的姿势都差不多但真正让项目卡住的往往是环境问题、数据问题或显存问题而不是模型结构本身。按“现象 → 原因 → 解决”的结构逐个说。5.1 显存不足batch_size 一调大就 OOM 怎么办现象是训练刚开始就抛CUDA out of memory停在第一个 batch。这通常有两个原因一是 batch_size 设大比如 64二是 max_length 设太长比如 512导致每个样本的 attention 矩阵占用大量显存。处理办法是把 batch_size 降到 16 或 8同时把 max_length 降到 64。此外可以开启梯度累积用多个小 batch 模拟一个大 batch 的更新效果。accumulation_steps 2 optimizer.zero_grad() for step, batch in enumerate(train_loader): outputs model(**batch) loss outputs.loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()梯度累积的原理是把 loss 按累积步数缩小然后累加梯度每隔 accumulation_steps 步才做一次参数更新。这样显存占用等同于小 batch更新频率等同于大 batch。需要注意 loss 必须除以累积步数否则梯度会被放大导致更新步长过大甚至不收敛。5.2 验证集准确率纹丝不动学习率与 warmup 的坑现象是训练几轮后 loss 在下降但验证集准确率一直停留在初始水平。最可能的原因是学习率偏大或偏小。BERT 微调对学习率极其敏感5e-5 以上容易让预训练权重被破坏1e-5 以下收敛极慢在 3 个 epoch 内难以见效。我固定用 2e-5 起步如果验证指标仍然不动就把 batch_size 调小或把 warmup 步数加上。另一种常见原因是模型进入了错误的模式——train 时忘记切回 eval 才会这样但 eval 时还开着 dropout 也会导致验证不稳定。排查方法是打印验证集前几个样本的 logits看看模型输出是否高度集中在某一个类别。5.3 预测结果全是“文学”类数据不平衡的应对现象是训练完成后分类报告里小类别的 F1 是 0预测全部落在大类上。根本原因是数据分布不平衡模型学到的是“全部预测成大类也能得到不错的准确率”而不是学到类别之间的边界。常见做法有三个第一个是给 CrossEntropyLoss 设置 class_weight让小类别的错分惩罚更大第二个是过采样小类别样本比如对小类别做简单复制第三个是改用宏平均 F1 作为评估标准让评分机制本身倒逼模型关注小类别。from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight(balanced, classesnp.unique(train_labels), ytrain_labels) class_weights_tensor torch.tensor(class_weights, dtypetorch.float)compute_class_weight 会自动计算每个类别的权重样本数越少的类别权重越高。把它传给模型的 loss 计算需要在训练循环里手动传入而不是依赖模型自带的 labels 参数。注意这里用 labels 参数时模型用的是普通 CrossEntropyLoss要自定义权重就得手动构造损失函数把模型输出的 logits 和真实标签分开传。对课程设计要求来说先尝试过采样是最简单的但报告里解释起来不如 class_weight 有深度。5.4 BERT 预训练权重下载失败离线权重的处理方法现象是在 from_pretrained 时卡在下载进度条或者直接报连接错误。本质原因是网络环境导致无法稳定访问 Hugging Face 官方模型库。常见处理办法是先在一台网络正常的机器上下载 bert-base-chinese 权重得到一个包含 config.json、pytorch_model.bin 等文件的本地目录然后把整个目录复制到目标机器的项目目录里用本地路径加载。model BertForSequenceClassification.from_pretrained( ./bert-base-chinese-local, # 本地目录 num_labelsnum_labels, )from_pretrained 既可以接收模型名称也可以接收本地目录路径它会自动识别目录下的 bin 文件和 json 文件。需要注意的是本地目录必须包含完整的三个核心文件config.json模型超参、pytorch_model.bin权重、vocab.txt词表。缺少 vocab.txt 时 Tokenizer 无法运行。课程设计的报告里可以写一句“由于网络原因改为本地加载预训练权重”这不会被扣分反而能体现你处理环境问题的能力。5.5 同一份代码两次运行结果不一样随机种子没固定现象是训练两遍验证集准确率差 1 到 2 个百分点。这里面有随机性来自 PyTorch 的随机初始化、DataLoader 的随机采样顺序、CUDA 算子本身的不确定性。课程设计为了保证可复现需要在代码入口处把三处随机种子全部固定。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)如果固定种子后两次运行结果仍然不一致还有可能是 DataLoader 设置了 num_workers0多进程采样会引入不确定。解决方式是 num_workers 设为 0或者每次训练前重新设置不同的种子并记录在报告中。严格意义上CUDA 算子如某些卷积和注意力计算在特定 GPU 上仍存在微小非确定性但在这个数据规模下不影响课程设计的结论。6. 让图书多分类再进一步对抗训练与半监督伪标签的尝试如果基础版本已经跑通想让成绩更好看或为报告增加亮点有两个主要方向值得尝试。第一个是在训练循环里加入对抗训练具体做法是把模型输入梯度作为扰动叠加到 embedding 上再重新前向传播这能让模型对输入噪声更鲁棒。常见做法是采用 FGM 或 PGD 方法只需要在原有训练循环里增加一小段代码。第二个是半监督伪标签用训练好的模型预测一批无标注数据把置信度高于阈值的预测结果当作标签加入训练集。对图书分类这样的小规模数据这种方式提升的效果在分类边界清晰时比较明显。实际操作上最常见的一处教训是加对抗训练后训练 loss 会上升但验证集的宏平均 F1 往往会提高因为模型不再只依赖个别关键词做决策。如果你决定做对抗训练学习率要额外下调一点比如从 2e-5 降到 1.5e-5否则模型容易震荡。半监督伪标签的置信度阈值我会设成 0.9太低会把噪声当信号。这两种进阶方法写进课程设计报告作为“改进方案与实验”比单纯增加 epoch 更有说服力。一个我持续强调的习惯是每次实验只改一个变量并且把配置参数写进报告里。BERT 出错了先看数据再看代码最后再看参数不要上来就怀疑模型结构。做课程设计最重要的是先跑通基线再调优而不是一开始就贪多。希望帮到你。本文还有配套的精品资源点击获取
返回列表