
简介基于BERT的Python图书多分类项目源码及完整数据集是为自然语言处理课程设计、期末大作业量身定制的实战资源面向掌握基础Python、希望深入文本分类应用的学习者可无缝用于图书主题、体裁等多分类任务。压缩包共16个文件包含9个Python脚本覆盖数据预处理、BERT模型构建、训练、测试及预测等模块、配置与说明文档整体仅14KB轻量且模块划分清晰便于针对性阅读和调试。目前已有46人参与学习项目来自高分课程设计95分以上下载即可运行无需任何改动并附带可直接使用的全部数据。借助BERT预训练语言模型项目完整演示了从原始数据清洗、特征编码到模型微调、效果评估的NLP分类流水线同时配套全量数据集便于读者边调试边理解attention机制与双向Transformer的语义捕捉能力是快速上手深度学习文本分类的优质参考。1. 用 BERT 做图书多分类为什么是课程设计里性价比最高的选题如果课程设计的题目挂在「图书分类」上大部分人的第一反应是 TF-IDF 加朴素贝叶斯跑完发现准确率卡在 85% 上下答辩老师追问一句“你试过 BERT 吗”就接不住。基于BERT的Python图书多分类项目源码及全数据集这条路线把「预训练模型微调」变成一门可以短期交付的课设数据是文本、标签是图书类别、输出是概率分布链路清晰效果比传统机器学习高一截答辩也有内容可讲。它适合有 Python 基础、想在一个月内跑通完整流程的人也适合作为 transformer 入门的第一个完整项目。课程设计最怕的不是技术难而是“跑完了讲不清”。BERT 这条路线的好处恰恰在于每一步都对应一个能展开讲的问题——数据怎么切、模型怎么选、参数为什么这么设、哪些样本分错了。这篇文章就按这个顺序从数据集结构讲到训练脚本最后落到答辩能直接用的评估材料。2. 图书多分类的数据集结构先确认标签体系再写代码2.1 单标签多分类与多标签分类课程设计先二选一图书按类别划分文学、历史、计算机、经济……是典型的单标签多分类问题每本书只属于一个主分类模型输出维度等于类别数损失函数用 CrossEntropyLoss。如果一本书可以同时属于多个类别比如“计算机史”这种交叉属性就变成多标签分类损失函数要换 BCEWithLogitsLoss评估指标也要从 accuracy 换成 hamming loss 或基于 F1 的变体。课程设计里绝大多数情况选单标签原因很简单模型只有一个分类头训练和答辩都好讲。拿到题目先问自己一个问题——标签之间能不能共存不能共存就是单标签别一上来就设计成多标签结构。标签体系本身也要提前定好。类别编号必须从 0 开始连续编号中间不能有空洞。比如你有 10 个类别编号就应该是 0 到 9不能出现 0 到 7 加 9 再加 12 这种结构。原因是模型最后一层的输出维度 num_labels 必须和类别数严格相等而训练数据里的 label 是整数如果你的编号中间有空洞数据里 label 最大值和 num_labels 对不上训练就会报维度错误。这类问题早早在预处理阶段解决不要在训练时报了错再回头查。类别命名也建议用英文或者拼音首字母缩写存一份映射表比如{文学: 0, 历史: 1, 计算机: 2}训练时用的是 int展示时再映射回中文。直接用中文类别名训练不是不行但 transformers 在序列化保存模型时对中文 label 的处理偶尔会出问题课程设计没必要在这个地方冒险。2.2 一份能直接喂给 BERT 的数据集长什么样一份可用的数据集至少包含两列text 和 label。text 可以是“书名加简介摘要”label 是整数形式的类别编号。我一般会再保留一列 book_id用来做按书分组的切分为什么这么做看 2.4 节。数据格式用 CSV 最省事训练、验证、测试三份文件直接读入不用做复杂的数据管线。字段示例说明book_idB0001同一本书的所有行共用同一个 id用于分组切分text红楼梦中国古代章回体长篇小说以贾史王薛四大家族兴衰为背景……书名简介摘要喂给分词器的原始文本label3类别整数编号从 0 开始连续编号拿到数据第一件事是看类别分布不是直接开训。用 pandas 几行代码就能看到每个类别的样本量这决定了后面要不要做类别均衡处理。我见过不少人跳过这一步直接写模型代码跑完发现 92% 的准确率全是假象——因为某个类别占了 70% 的样本模型把所有样本猜成这个类就有 70% 准确率。import pandas as pd df pd.read_csv(books_train.csv) print(df.head()) print(df[label].value_counts().sort_index())这段代码的作用是查看前几行数据长什么样以及每个类别的样本数量分布。value_counts输出里如果出现某个类别只有几十条、另一个类别有几千条后面训练就要考虑加权采样或者调整评估指标。这个检查应该在写训练脚本之前完成因为它直接决定你要不要为类别不平衡设计额外的处理逻辑。text 字段的构造也值得花十分钟想清楚。理想情况下每一行是“书名 冒号 内容简介”因为书名本身就是最强的分类信号。如果数据集里只有简介没有书名不是不能做模型需要从摘要措辞里推断类别准确率会低几个点。构造 text 时把书名放最前面还有一个好处BERT 的注意力机制对每个 token 一视同仁放前面的内容在截断时更容易被保留。2.3 数据量下限每个类别 200 条是课程设计的安全线BERT 微调和从零训练不一样它不需要海量数据就能生效因为模型的通用语言能力已经在预训练阶段学好了。我的经验是单标签分类、类别数在 5 到 20 个之间时每个类别有 200 到 500 条样本就能把准确率稳定在 90% 上下。低于 100 条也不是不能跑但验证集上的波动会非常大同一份代码换一次随机种子可能就差三四个百分点答辩时不好解释因为你自己也说不清这个结果到底稳不稳定。如果全数据集包含的类别超过 20 个我建议做一次类别合并把相近的小类合并成一个大类比如「中国文学」和「外国文学」合并成「文学」。类别一多每个类别分到的训练样本就变少混淆矩阵变得稀疏答辩时也很难逐个类别讲清楚。合并之后如果还有类别样本量特别少可以考虑对这类文本做简单复制增强把每条样本重复 2 到 3 次。这个操作治标不治本但对课设来说能让训练过程平稳很多属于性价比很高的“后悔药”。数据量充足时还有一个容易忽略的点训练集和测试集的划分比例。课程设计我习惯用 8:1:1即 80% 训练、10% 验证、10% 测试。验证集用来调参测试集只跑一次作为最终成绩展示。如果总数据量只有一两千条可以把比例改成 6:2:2保证测试集也有足够的样本否则测试集上几十条样本的准确率波动很大结果呈现不漂亮。2.4 切分数据时最容易被忽视的坑按文本行随机切会导致数据泄露这是我在做文本分类时踩过的最深的坑之一。很多课程设计的数据集里同一本书的简介会被拆成多行或者一本书有作者简介、内容提要、目录三行文本。如果直接用train_test_split按行切分同一本书的内容会同时出现在训练集和验证集里模型在训练时已经“见过”验证集里同一本书的表述风格验证分数虚高。答辩时老师只要把验证集样本拿出来反查这个漏洞就藏不住。正确的做法是按 book_id 分组切分保证同一本书的所有文本都落在同一侧。这里用GroupShuffleSplit而不是train_test_split它专门处理“同一组不能拆散”的场景。from sklearn.model_selection import GroupShuffleSplit split GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(split.split(df, groupsdf[book_id])) train_df df.iloc[train_idx] val_df df.iloc[val_idx] print(train_df[book_id].nunique(), val_df[book_id].nunique())split方法里的groups参数是关键它告诉切分逻辑“哪些行属于同一组”切分时以组为单位而不是以行为单位。test_size0.2表示拿出 20% 的书作为验证集。最后打印两个集合里的 book_id 数量确认没有一本书被切到两侧。这一步做完后面训练的验证指标才有参考意义否则一切调参都是在自欺欺人。2.5 切分后先做分布检查验证集和训练集的类别比例不能差太多分组切分完成后还有一个容易忽略的检查验证集的类别分布是否和训练集接近。GroupShuffleSplit 只保证组不散不保证类别比例一致。如果某类图书只有 10 本切分时可能全落到训练集验证集里这个类别的样本数是 0那么验证准确率对这个小类完全没有监控。标准做法是切分后各自打印一次类别分布确认每个类别在两侧都存在。train_counts train_df[label].value_counts() val_counts val_df[label].value_counts() for label in sorted(df[label].unique()): print(label, train_counts.get(label, 0), val_counts.get(label, 0))输出里如果某个类别在验证集为 0有两种处理方式一是把这个类别的几本书从训练集手动挪到验证集代价是训练数据少一点二是调整test_size让验证集占比增大。挪数据的方式更直接我一般手工指定一个最小的验证样本数比如每个类别至少保留 10 条在验证集。这个检查只需要一分钟但它决定了后面所有评估结果是否可信。3. 模型与运行环境把 BERT 参数下载变成离线可复用的落地步骤3.1 选中文 BERT-base 还是 tiny取决于显卡显存和训练时间标题写的是 BERT落到代码里第一个选择是模型文件。中文文本分类我默认用bert-base-chinese这是最常用的中文预训练权重模型结构是 12 层 Transformer、隐藏维度 768、参数量大约 1.1 亿。如果你的机器显存只有 4G 甚至没有独立显卡bert-base-chinese在 batch_size16 的情况下也会把显存吃满。这时候有两个选择一是把 batch_size 降到 8 并开启梯度累积见 4.4 节二是换成参数量更小的中文模型。tiny 版参数量小一个数量级训练速度快很多准确率会低 2 到 4 个百分点但对课程设计来说完全够用。我的建议是能跑 base 就跑 base显存不够就换 tiny不要为了省显存把文本长度从 512 砍到 128。文本截断长度对分类准确率的影响比模型大小更直接把长摘要硬压到 128 个 token关键信息丢失之后再大的模型也救不回来。选 base 还有一个答辩上的好处你可以讲“BERT-base 的 12 层 Transformer 如何通过自注意力捕捉文本语义”这是课程设计报告里最出彩的一段。tiny 版也可以讲但效果上缺少说服力。如果实在没有 GPUCPU 训练 base 模型跑 3 个 epoch 可能要十几个小时不现实。常见做法是缩成 tiny 模型加短文本或者找一台有 GPU 的云服务器跑完下载权重本地只做推理。3.2 手动下载模型参数三步把预训练权重变成本地目录课程设计最常见的翻车现场是代码跑到from_pretrained那行开始从网上下载模型然后卡住不动过十分钟报连接错误。transformers库默认从模型中心下载权重但课设现场的网络时好时坏我不指望在一篇博客里解决所有网络问题但有一个通用做法可以绕开运行时下载提前把模型参数下载到本地目录。一个完整的模型目录至少包含三个文件文件作用说明config.json模型结构配置包括层数、隐藏维度、num_labelspytorch_model.bin预训练权重几百 MB 的二进制参数文件vocab.txt词表中文模型用的是字符级别的词表下载好的文件放到项目的books_bert文件夹下代码里不再写“去网上下载”这个动作直接指定本地路径。这样能保证每次运行加载的都是同一份参数不会因为网络状态不同而改变结果。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_path ./books_bert tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained( model_path, num_labels10 )from_pretrained的第一个参数传本地路径而不是模型名transformers会优先读取本地文件如果目录里文件不完整它会明确报错说缺少哪个文件而不是反复尝试下载这比运行时静默失败好排查得多。num_labels10要和数据集里的类别数严格一致这个参数决定最后一层分类头的输出维度差一个数字模型输出和损失函数就对不上一训练就崩。3.3 版本搭配与设备声明torch 与 transformers 的兼容组合我训练文本分类模型用的组合是 Python 3.10、PyTorch 2.x、transformers 4.x。这三个版本配套使用最稳transformers4.x 对 AutoModel 系列的支持已经很成熟不需要追最新版本。依赖写入 requirements.txt两行就能复现环境torch2.0 transformers4.30装完之后先验证模型能不能加载再写训练循环。有几个细节值得养成习惯。第一transformers 在加载AutoModelForSequenceClassification时会读取 config.json 里的 num_labels 字段如果你在代码里传了num_labels10但本地 config.json 里写的是 2from_pretrained以代码里传的值覆盖。这个行为看似方便但也意味着改错 num_labels 时不会报错模型输出维度却已经变了。训练前打印一次model.config.num_labels确认它等于类别数这个习惯能省半小时排错时间。第二设备声明要显式写不要依赖 transformers 自动判断。代码里常用的写法是import torch device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) print(fUsing device: {device})torch.cuda.is_available()返回 False 时自动落到 CPU这套代码在有无显卡的机器上都能跑只是速度差很多。打印设备信息能让你第一时间知道当前跑在什么硬件上避免出现“我以为在 GPU 上跑实际在 CPU 上磨了半下午”的尴尬。提示模型权重文件几百 MB不要塞进 git 仓库。课程设计交源码时单独把模型目录打包或者写一个下载脚本让老师按脚本准备模型文件。4. 最小可复现的训练脚本数据加载、模型输出与训练主循环4.1 封装 Dataset 与分词策略max_len 不是越大越好BERT 的输入长度上限是 512 个 token超过的部分会被截断。图书简介这种文本经常超过 500 字直接全量截断会丢掉结尾的信息。我在课程设计里通常用 max_len256原因是大部分图书简介的前半段已经包含分类所需的核心信息“本书是 XX 领域的入门教材”这种话几乎都出现在前两句。长度设成 512 会让训练时间翻倍但分类准确率并不会等比提升。这个截断行为要在代码里显式写出来不让 tokenizer 用默认值。import torch from torch.utils.data import Dataset class BookDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len256): 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( text, truncationTrue, paddingmax_length, max_lengthself.max_len, return_tensorspt, ) return { input_ids: encoding[input_ids].squeeze(0), attention_mask: encoding[attention_mask].squeeze(0), label: torch.tensor(label, dtypetorch.long), }这个 Dataset 类的作用是把原始文本转成 BERT 能读的输入格式。tokenizer 内部会自动加上 [CLS] 和 [SEP] 标记不需要手工处理。paddingmax_length会让所有样本都变成同样的长度这样 DataLoader 在拼 batch 时不需要额外做动态 padding省事但会浪费一点显存。return_tensorspt表示返回 PyTorch 张量squeeze(0)是把 tokenizer 返回的 batch 维度去掉因为这里一次只处理一条样本。4.2 DataLoader 组装与形状自检训练前先跑一个 batchDataset 封装好之后用 DataLoader 包一层指定 batch_size 和 shuffle。这里有一个常见错误shuffle 只对训练集开验证集和测试集要保持顺序因为后面画混淆矩阵时要按顺序把预测结果和真实标签对齐。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse) for batch in train_loader: print(batch[input_ids].shape) print(batch[label].shape) break先跑一个 batch 看一眼形状是训练前最廉价的自检。input_ids的形状应该是[batch_size, max_len]label 的形状是[batch_size]。如果 label 形状变成[batch_size, 1]说明数据里 label 是多维的在构造 Dataset 时要先压成一维。这个自检步骤能提前拦截大部分维度不匹配的问题避免训练跑到一半才崩溃白白浪费几十分钟。4.3 手写训练循环三个参数决定最终效果课程设计我不推荐直接用 transformers 的 Trainer因为它把训练逻辑封装得太黑匣子答辩时老师问“学习率是多少、warmup 怎么设、损失是谁算的”你答不上来。手写训练循环十几行代码每一个参数都能对着讲。关键参数有三个学习率、batch_size、训练轮数。from transformers import AdamW from tqdm import tqdm optimizer AdamW(model.parameters(), lr2e-5) total_steps len(train_loader) * 3 scheduler torch.optim.lr_scheduler.LinearLR( optimizer, start_factor1.0, end_factor0.0, total_iterstotal_steps ) model.to(device) model.train() for epoch in range(3): loop tqdm(train_loader) for batch in loop: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) outputs model(input_ids, attention_maskattention_mask, labelslabels) loss outputs.loss optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() loop.set_description(fEpoch {epoch}) loop.set_postfix(lossloss.item())这段代码是全流程的核心。model 的 forward 里传了 labelstransformers 会自动计算交叉熵损失不需要自己写 loss 函数。学习率 2e-5 是 BERT 微调最常用的起点比普通分类模型小一个数量级因为预训练权重已经很好了学习率太大会破坏学到的语义。epochs 设 3对文本分类足够超过 5 轮通常会在验证集上过拟合。LinearLR 做的是线性衰减让学习率在训练结束时归零这是 BERT 微调的标准做法比固定学习率效果更稳。4.4 显存不够时的兜底方案梯度累积训练到中途显存溢出是最打击人的因为前面的时间都白费了。如果 batch_size16 跑不动不要急着换小模型可以先试gradient_accumulation_steps。它的原理是把一个大 batch 拆成几个小 batch各自算完梯度后累加再统一更新一次参数数学上近似等于用大 batch 训练。accumulation_steps 4 optimizer.zero_grad() for step, batch in enumerate(loop): outputs model( batch[input_ids].to(device), attention_maskbatch[attention_mask].to(device), labelsbatch[label].to(device), ) loss outputs.loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()注意 loss 要除以 accumulation_steps否则梯度会是正常值的 4 倍导致训练发散。这个技巧不需要改模型、不需要改数据就是把显存压力分摊到时间上。8G 显存的 GPU 用这个方案跑 bert-base-chinese 做图书分类一点问题没有。答辩时被问到“显存不够怎么办”能讲清楚梯度累积的原理反而是一个加分项。5. 避坑BERT 微调最容易翻车的 5 个问题5.1 损失下降但验证准确率纹丝不动现象训练集 loss 一路从 1.2 降到 0.2训练集准确率接近 99%但验证集准确率一直在 60% 上下波动。原因这种状态基本可以断定是过拟合但过拟合背后通常是两类问题。一是切分方式不对验证集和训练集之间存在数据泄露模型在训练时已经见过验证集的文本表达。二是训练样本类别分布极度不均衡模型把大类猜得很准小类全部猜错整体准确率被大类拉高但验证集上表现不出来。解决排查顺序是先改切分方式把 2.4 节的GroupShuffleSplit用上重新训练。如果改了还是这样打印验证集的分类报告看每个类别的 F1。小类 F1 低的给训练数据做类别加权采样让每个 batch 里类别的比例接近均匀。用 sklearn 的class_weight思路也行本质都是让模型对小类更敏感。5.2 模型下载到一半中断之后每次运行都卡在加载现象第一次运行from_pretrained时下载到 80% 断网重新运行后总是卡很久才报错有时看起来像是死循环。原因transformers 的下载是缓存式的它发现本地缓存目录里有文件残留会尝试复用或重新比对哈希网络不稳定时这个过程会反复超时。这不是代码问题是缓存被污染了。解决不要试图在代码里修直接把缓存目录里对应模型的文件删干净手动下载完整权重放到项目本地目录然后指定本地路径加载。找到缓存目录的方式是打印transformers的缓存位置或直接搜索huggingface文件夹删除后按 3.2 节的方式准备一份完整的模型目录。一劳永逸之后离线也能跑。5.3 类别不平衡让准确率虚高答辩时经不住追问现象训练 3 轮后 accuracy 显示 92%看起来很不错。老师问“每个类别的准确率分别是多少”现场打印才发现其中一个只有 30% 的类别把整体分数拉低了另一类样本量占 70% 的类别准确率接近 100%。原因accuracy 是“猜对的样本数除以总样本数”大类样本多对总分贡献大小类被牺牲掉也不影响总分。课程设计如果选的数据集本身不平衡accuracy 不是可信的指标。解决评估时至少同时打印 macro-F1 和每个类别的 recall。我习惯在训练脚本里加评估函数每次 epoch 结束输出一次classification_report它会列出每个类别的 precision、recall、F1答辩时直接把这个表贴进报告比单薄的 accuracy 有说服力得多。如果某个小类 F1 特别低答辩前想一下原因是样本太少还是这个类别的文本和另一个类别在表达上高度相似。5.4 标签映射错位模型输出的类别 5 和数据集的类别 5 不是一回事现象训练正常推理时发现模型把“计算机”类全部预测成“经济”类但训练时的准确率又很高。原因label 在数据加载时被当成字符串传给模型transformers 内部做了某种排序或词表映射运行时数据里没见过的字符串被映射到了未知位置。这种错位在 num_labels 较大的时候尤其隐蔽因为只有部分类别错位整体准确率看起来还正常。解决在数据预处理阶段把 label 一律转成整数并保持从 0 开始连续编号。训练前打印一次 label 列表和model.config.num_labels确认两者一致。我踩过一次这个坑之后在代码里加了一行断言num_classes len(label_names) assert model.config.num_labels num_classes这行断言会在类别数不一致时直接报错而不是让模型带着错误的输出维度训练三轮之后才在推理时暴露问题。5.5 长文本被粗暴截断丢失了分类关键信息现象验证集准确率在 85% 左右怎么调都上不去试了更大的模型、更小的学习率都没用。原因图书简介超过 512 词时tokenizer 默认从开头截断。如果一本书的简介先讲作者生平最后才说这本书属于什么领域关键信息恰好被截掉了。解决一个实用的做法是把文本拼接后再截断——把书名重复两次放到开头再接摘要的前 200 个字。书名本身就是最强的分类信号重复放相当于给模型一个提醒。另一个做法是分段截断如果文本超过 512 token取前 256 和后 256拼接成新文本。这个操作看起来有点玄学本质上是提高有效信息的密度。效果通常能提高 3 到 5 个百分点而且代码改动只有几行。6. 答辩准备的最后一公里混淆矩阵、分类报告与一次完整推理6.1 测试集完整推理预测、标签与置信度一次收集齐训练结束后的第一件事是在测试集上跑一次完整推理把预测结果、真实标签和置信度全部保存下来。推理阶段要关闭梯度计算否则显存被占满容易在答辩演示时出问题。model.eval() preds, true_labels, all_logits [], [], [] with torch.no_grad(): for batch in test_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) outputs model(input_ids, attention_maskattention_mask) preds.extend(torch.argmax(outputs.logits, dim1).cpu().numpy()) true_labels.extend(batch[label].cpu().numpy()) all_logits.extend(outputs.logits.cpu().numpy())outputs.logits的形状是[batch_size, num_labels]torch.argmax沿最后一维取最大值的下标就是预测类别。torch.no_grad()告诉 PyTorch 不需要记录梯度推理显存占用减半。all_logits是原始输出后面算置信度要用。6.2 混淆矩阵与分类报告把“哪两类容易混”变成图表文本分类答辩里老师必看的两样东西是分类报告和混淆矩阵。分类报告展示每个类别的 precision、recall、F1混淆矩阵则暴露类别之间的混淆关系。比如“计算机”和“经济”频繁混淆很可能是因为数据集里有一批“互联网金融”类图书在标签上归属模糊。from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt cm confusion_matrix(true_labels, preds) report classification_report(true_labels, preds, target_nameslabel_names) print(report) plt.figure(figsize(8, 6)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xlabel(Predicted) plt.ylabel(True) plt.xticks(range(num_classes), label_names, rotation45) plt.yticks(range(num_classes), label_names) for i in range(num_classes): for j in range(num_classes): plt.text(j, i, cm[i, j], hacenter, vacenter) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)混淆矩阵图保存成 PNG课程设计报告里直接插入比截图训练日志专业得多。看矩阵时先看对角线以外的亮点对角线数值大是好事非对角线的亮点就是“哪两类容易混”的答案。6.3 错误样本分析表让答辩从“跑通脚本”升级到“讲得清楚”混淆矩阵只能告诉你哪两类容易混老师接下来一定会问“能不能举几个分类错误的例子”。提前准备一张错误样本表把预测错的文本、真实类别、预测类别和置信度列出来现场随时翻开讲。import numpy as np import pandas as pd probs np.array(all_logits) probs np.exp(probs) / np.exp(probs).sum(axis1, keepdimsTrue) error_rows [] for i in range(len(true_labels)): if preds[i] ! true_labels[i]: error_rows.append({ text: test_texts[i], true_label: label_names[true_labels[i]], pred_label: label_names[preds[i]], confidence: probs[i][preds[i]], }) error_df pd.DataFrame(error_rows) error_df.to_csv(error_analysis.csv, indexFalse)confidence 是模型对预测结果的置信度。置信度很高的错误样本最有分析价值说明模型“很自信地犯错”这类样本往往指向标签噪声或文本本身的歧义。把案例整理出来讲一个具体错误案例比复述原理更能说明你真的做过这个项目。我自己做这个课程设计时最后两小时全用在这里把错误样本按置信度排序挑出三条典型错误分别对应“标签标注错误”“文本信息不足”“类别边界模糊”三种情况。这个动作让我在答辩现场从容很多也让整个项目从“跑通脚本”变成“讲得清楚”。训练参数和模型结构都能查到但“错误样本为什么错”只能从你自己的数据集里找答案。希望这个方向能帮你的课设少走弯路把时间省下来真正理解 BERT 在做什么。本文还有配套的精品资源点击获取