这篇论文的方法,简单说就是用预训练语言模型对文本做分类。我直接在自己机器上把流程复现了一遍,从环境配置、数据处理到模型微调和结果评估,把每一步的细节和踩过的坑都记录下来。如果你也正准备拿BERT做文本分类,或者想复现这篇论文但不知道从哪里下手,这篇应该能帮你省不少时间。
1. 复现前的核心问题:这篇论文到底在做什么
先说清楚技术路线,后面才好展开。
金星晔等(2024)在《经济研究》上讨论的是大语言模型在经济学研究中的应用问题。经济学研究里有大量文本数据——政策文件、上市公司年报、财经新闻、官员讲话等,传统做法是用词典法或者人工标注来做文本分类和情感判断,但这两类方法各有各的麻烦:词典法依赖人工构造的词表,覆盖面有限,换个领域就得重新搭;人工标注虽然准,但成本高、一致性难保证,样本稍微多一点就扛不住。
大语言模型这条路线的本质,是把文本分类的任务转成预训练模型的下游微调任务。模型在海量语料上已经学到了一般性的语言规律,我们只需要在它上面加一个分类头,再用有标签的数据微调一小段时间,就能拿到一个针对特定任务表现还不错的分类器。整个过程的核心就是:加载预训练模型、构造数据集、微调、评估。
1.1 论文方法和BERT微调在技术层面的对应关系
论文讨论的大语言模型方法,落到工程实现上,并不是说非得调用GPT这样的大模型API,也不一定需要本地部署几十上百B参数的模型。对于文本分类这类任务,用BERT这样的预训练模型做微调,其实是性价比最高的选择。
原因很简单:
- BERT属于预训练语言模型,它的参数量在1亿到3亿级别,单张消费级GPU就能跑起来,不像大几十B的模型动辄需要多卡并行甚至量化推理。
- BERT基于Transformer的双向编码结构,对文本语义理解的综合能力很强,中文文本分类任务上表现稳定且可复现。
- HuggingFace的transformers库把BERT的加载、分词、微调流程封装得很好,代码量可以压缩到很短。
论文中维度更抽象、面向方法论的讨论,在实验操作中对应的就是这些工程环节。
1.2 构建可复现实验的三个关键要素
做经管论文复现,和单纯跑一个深度学习Demo有本质区别。论文的结论要立得住,实验必须满足三个条件:
- 数据可复现:使用的数据源要明确,分类标签体系要可理解,训练集和测试集的划分要固定。
- 模型选择明确:用哪个预训练模型、什么版本、什么参数规模,要能说清楚。
- 训练参数固定:学习率、batch size、训练轮数、随机种子,这些直接决定结果能不能被复现出来。
我在实操中遇到过很多同学做类似实验,往往跑通了一个Demo就觉得自己复现完成了,但实际上随机种子没固定,每次跑出来的指标都不一样;数据划分方式一变,结果浮动能达到几个百分点。这些都是论文复现中容易被忽略,却又影响结论可靠性的细节。
2. 环境搭建:Anaconda和PyTorch版本怎么选最省心
复现的第一步是跑通环境。这一步虽然基础,但版本选错了后面全崩。我在配置过程中吃过不少亏,这里直接给出验证过的组合。
2.1 一套经过验证的环境组合方案
我先列一下我最终稳定运行的环境版本,方便直接照抄:
| 组件 | 版本/说明 | 备注 |
|---|---|---|
| 操作系统 | Windows 10 / Ubuntu 20.04+ | 两者均可用,本文以Windows为主 |
| Anaconda | 4.12+ | 用于创建隔离的Python环境 |
| Python | 3.9 | 对PyTorch和transformers兼容性最好 |
| PyTorch | 2.0.1+cpu / 2.0.1+cu118 | 有NVIDIA显卡用CUDA版本,显卡不支持就选CPU版本 |
| Transformers | 4.30.2 | 注意和PyTorch版本匹配 |
| Tokenizers | 0.13.3 | 随transformers自动安装 |
| Datasets | 2.12.0 | HuggingFace数据集库,方便做数据集划分 |
| Pandas | 2.0.x | 读取和处理原始表格数据 |
| Scikit-learn | 1.2.x | 用于做数据切分和评估指标计算 |
这里每个版本号都是我实际跑过没出问题的,不敢说绝对最优,但绝对能跑通。如果你之前装了其他版本,大概率也能跑,只是遇到莫名其妙的报错时,先检查版本兼容性。
2.2 安装过程中的三个高频坑
坑一:CUDA版本和PyTorch版本不匹配
NVIDIA官方的CUDA版本和PyTorch的CUDA运行库不是同一个概念。PyTorch会自带一部分CUDA运行库,所以即使显卡驱动版本稍微旧一点,只要PyTorch是支持对应CUDA版本的,一般也能跑起来。
我的经验是:
- 先看显卡驱动支持的CUDA版本,在命令行输入
nvidia-smi,右上角能看到CUDA Version。 - 然后去PyTorch官网选择匹配的安装命令。比如驱动支持CUDA 11.8,就选
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118。 - 如果没有独显或显卡太老,直接装CPU版本,跑BERT微调在小数据集上没有问题,只是速度慢一些。
坑二:conda和pip混用导致包冲突
用conda install安装PyTorch,再同时用pip install安装transformers,有概率会把环境搞得一塌糊涂。我的建议是:从conda只建环境,所有Python包统一走pip安装。这样依赖管理更一致,排查问题也简单。
创建一个干净环境然后装包:
conda create -n bert_finetune python=3.9 -y conda activate bert_finetune # 有NVIDIA显卡用户,选择对应CUDA版本的安装命令 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 无显卡或者Mac用户 pip install torch torchvision torchaudio坑三:HuggingFace模型下载速度慢或失败
国内网络环境下,直接从HuggingFace下载BERT模型经常超时。替代方案是用镜像站点:
import os os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"也可以在命令行设置:
export HF_ENDPOINT=https://hf-mirror.com设置完之后,transformers库会自动从镜像下载模型权重和词表文件。第一次下载bert-base-chinese大概400MB左右,下载完成后会自动缓存,后续加载直接读本地。
3. 数据准备:从原始文本到BERT能读懂的Tensor
论文复现里最花时间的往往不是模型训练,而是数据准备。BERT本身不对中文文本做任何处理,它只认识经过分词后的Token序列和对应的ID。原始文本要经过一套完整的转换流程才能变成模型的输入。
3.1 数据格式与标签体系设计
假设你要复现的是针对经济文本(比如财经新闻、上市公司公告或政策文件)的分类任务,一般需要准备一个表格文件,至少包含两列:
text:原始文本内容,可以是新闻标题、正文段落等。label:分类标签,既可以是整数(0、1、2……),也可以是文本标签(如"正面"、"负面"、"中性")。
用Pandas读取数据后,第一步是检查标签分布是否均衡。这一步我建议用代码直接跑一眼:
import pandas as pd df = pd.read_csv("data/news_classification.csv") print(df["label"].value_counts())如果发现某个类别的样本数特别少(比如还不到总样本的5%),后续训练时模型基本学不好这个类别。这时候可以考虑数据增强,或者采用分层抽样来保证训练集和验证集中各类别比例一致。
标签处理上,我习惯把原始文本标签转成整数ID,用字典映射完成:
label_mapping = {"负面财经新闻": 0, "中性财经新闻": 1, "正面财经新闻": 2} df["label_id"] = df["label"].map(label_mapping)这里有一个容易踩的坑:标签映射字典必须固定,并且训练集和测试集用同一个映射。千万不要每读完一份数据就重新生成一个映射,否则训练和预测时标签对不上,最终结果会完全乱掉。
3.2 分词与编码:让文本变成数字
BRET本身是子词级别的模型,中文的编码方式采用的是字级别的切分。bert-base-chinese这个预训练模型的词表大小是21128,覆盖了常用汉字、英文字母、中文标点和特殊符号。
使用transformers库加载分词器,然后用__call__方法对文本进行编码:
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") # 单条文本编码后的效果 text = "央行今日开展逆回购操作,维护银行体系流动性合理充裕" encoded = tokenizer( text, max_length=128, truncation=True, padding="max_length", return_tensors="pt" ) print(encoded["input_ids"]) print(encoded["attention_mask"])输出结果中:
input_ids:每个Token在词表中的ID号。attention_mask:标识哪些位置是真实Token,哪些位置是补零填充的。token_type_ids:区分第一个句子和第二个句子,单句分类任务中用不到,但BERT输出中会带出来。
编码参数需要特别注意:
max_length:设置每条文本的最大长度。超过会被截断,不足会被填充。truncation=True:开启截断。padding="max_length":填充到统一长度。
max_length的选择直接影响模型效果和显存占用。我发现很多初学者喜欢设置512甚至更长,但实际上对于新闻标题、短评这类短文本,128完全够用;对于长财报文本,设置256或384更合适,但训练成本也会相应上升。
3.3 封装成Dataset类:PyTorch训练的第一步
PyTorch要求所有训练数据封装成Dataset类,然后通过DataLoader按批次加载。自定义Dataset类的写法比较固定,我习惯这样写:
import torch from torch.utils.data import Dataset class CustomDataset(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)需要注意的是,encodings必须是批量文本一次性编码之后的结果。也就是说,不要逐条调用tokenizer,而是把整个训练集的文本列表一次性传入:
# 将整个训练集的文本一次性编码 train_encodings = tokenizer( list(train_df["text"]), max_length=128, truncation=True, padding="max_length" ) train_dataset = CustomDataset( train_encodings, list(train_df["label_id"]) )一次编码整个训练集,速度比逐条编码快很多。另外,编码完成后最好检查一下input_ids里面是否有0之外的填充值异常,如果所有值都是0,说明分词过程出了问题,检查一下text列是否存在空值或缺失值。
3.4 数据划分:分层抽样确保验证集可靠性
论文复现中,训练集和验证集的划分方式会影响最终评估结果。我推荐使用train_test_split的分层抽样参数,确保每个类别在训练集和验证集中的比例一致:
from sklearn.model_selection import train_test_split train_texts, val_texts, train_labels, val_labels = train_test_split( df["text"].tolist(), df["label_id"].tolist(), test_size=0.2, random_state=42, stratify=df["label_id"].tolist() )random_state=42这个参数很关键。不设置随机种子的话,每次划分结果都不同,复现就无从谈起。这也是论文复现中一定要做的操作。
4. 模型构建与微调:核心代码逐段拆解
数据准备好之后,进入模型的加载和微调环节。这块是整个复现过程的核心,也是论文方法落到工程上的直接体现。
4.1 加载BERT模型并配置分类头
BERT本身是一个编码器结构,输出的是每个Token的语义表示。要做文本分类,需要在它之上加一个分类头,通常是取[CLS]位置对应的输出,经过一个全连接层映射到类别数量上。
使用HuggingFace的BertForSequenceClassification可以直接实现这一逻辑:
from transformers import BertForSequenceClassification num_labels = len(label_mapping) model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=num_labels )加载模型这一行代码背后发生的事情值得展开说一下。bert-base-chinese包含12层Transformer编码器、768维的隐层表示、12个注意力头,总参数量约1.02亿。当我们用BertForSequenceClassification加载时,transformers库会自动在BERT模型顶部加一个ClassifierHead,这个分类头默认是一个线性层,把768维的向量映射到num_labels维。
有一个参数设置容易忽略,就是model.config中的id2label和label2id。设置好这两个映射,在训练和预测时可以直接通过模型输出拿到标签名称:
model.config.id2label = {v: k for k, v in label_mapping.items()} model.config.label2id = label_mapping4.2 参数冻结策略:全量微调还是部分微调
这是微调BERT时一个常见的选择题。我自己在实际操作中试过两种方式,在这里把区别和适用场景说清楚。
全量微调:模型所有层的参数都参与训练,包括BERT主体和新增的分类头。优点是模型能更好地适应目标数据分布,训练充分后效果通常更好;缺点是显存占用高、训练时间长,在数据量比较小的时候可能有轻微过拟合风险。
部分微调:将BERT主体的参数冻结(requires_grad=False),只训练分类头。这种做法可以大幅降低计算量,在小数据集上也能得到不错的结果,适合快速跑通基线模型。
冻结BERT参数的代码实现:
for param in model.bert.parameters(): param.requires_grad = False对于论文复现,我的建议是优先做全量微调。论文的结论基于完整模型的效果,部分微调只是对完整效果的近似,两者可能相差一到几个百分点的准确率。如果机器性能不够,先跑部分微调作为快速的可行性验证,等确认流程没问题了再放开全量微调。
4.3 优化器、学习率与训练参数的选择逻辑
对BERT做微调,学习率的选择是个关键点。BERT在大规模语料上预训练时使用过很高的学习率,但微调阶段如果用同样的学习率,会迅速破坏预训练学到的通用特征。行业内通用的经验是:微调阶段学习率通常设置在2e-5到5e-5之间。
我自己惯用的配置如下:
| 超参数 | 数值 | 设置理由 |
|---|---|---|
| 学习率 | 2e-5 | BERT微调的标准选择,兼顾收敛速度和稳定性 |
| Batch Size | 16或32 | 根据显存调整,GPU显存不够就降到8 |
| 训练轮数 | 3-5 | 数据量小用3轮,数据量大可以跑到5轮,更多轮数容易过拟合 |
| Warmup比例 | 10% | 前10%的步数内学习率从0线性升到设定值 |
| 优化器 | AdamW | BERT官方推荐,带权重衰减 |
| Weight Decay | 0.01 | 防止过拟合,标准配置 |
配置TrainingArguments并启用Trainer:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=64, warmup_ratio=0.1, weight_decay=0.01, logging_dir="./logs", logging_steps=10, evaluation_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="accuracy", save_total_limit=2, seed=42, report_to=[] )这里有个细节:seed=42必须设置。Trainer在每轮训练开始前会重置各个随机数生成器,固定种子能保证实验的可复现性。report_to=[]是为了关掉WandB等外部日志记录工具,避免在无网络环境下的连接等待。
4.4 自定义评估函数
Trainer默认只输出Loss,但论文复现中我们需要的是准确率、F1这类更直观的分类指标。在HuggingFace的Trainer中,需要通过compute_metrics函数来自定义评估指标计算逻辑:
from sklearn.metrics import accuracy_score, f1_score, classification_report def compute_metrics(eval_pred): predictions, labels = eval_pred predictions = predictions.argmax(axis=-1) accuracy = accuracy_score(labels, predictions) f1_macro = f1_score(labels, predictions, average="macro") f1_weighted = f1_score(labels, predictions, average="weighted") return { "accuracy": accuracy, "f1_macro": f1_macro, "f1_weighted": f1_weighted }这里有两个容易犯的错误想提醒一下:
- 模型的原始输出是logits,维度是
[batch_size, num_labels],必须经过argmax取最大值对应的下标才是预测的类别标签。 - F1分数的
average参数有两种常用取值,macro是所有类别F1的算术平均,不关心样本量;weighted会按照各类别样本量加权求和。对于类别不均衡的数据,建议同时关注两者。
然后创建Trainer并开始训练:
trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=val_dataset, compute_metrics=compute_metrics ) trainer.train()4.5 模型保存与加载
训练结束后,需要把模型保存到本地,方便后续做预测推理。注意要同时保存模型权重和分词器,这样加载时才不会出问题:
model_path = "./models/bert_finetuned" trainer.save_model(model_path) tokenizer.save_pretrained(model_path)加载训练好的模型做预测时,权重和分词器要一起加载:
from transformers import AutoModelForSequenceClassification, AutoTokenizer loaded_model = AutoModelForSequenceClassification.from_pretrained(model_path) loaded_tokenizer = AutoTokenizer.from_pretrained(model_path)5. 训练过程中的实测问题与排查思路
我在复现过程中遇到过不少问题,挑几个典型的、对后面自己动手跑实验的朋友最有帮助的问题,列一下完整的排查链路。
5.1 损失不降怎么办
第一个明显的异常是loss在训练过程中几乎不下降,运行了几个epoch还是原地打转。
排查链路如下:
第一步:确认数据标签是否匹配。
我在第一次运行时就踩过这个坑。原始数据中的标签是"正面"、"负面"、"中性"这样的中文文本,但我直接把中文文本塞给了模型,而分类头输出维度是3。模型看到的是三个不同的字符串,完全不知道哪个是哪个,loss自然不下降。
解决方法是严格执行标签映射,把文本标签转成0、1、2的整数形式,并且检查label的取值区间是否在[0, num_labels-1]之内。
第二步:检查学习率是否过小或过大。
BERT微调学习率设置太低,比如1e-6,模型几乎不会更新参数;设置太高,比如1e-3,loss可能直接变成NaN。我的排查方式是观察前200个step的loss走势。
- loss在缓慢下降,说明学习率设置合理。
- loss几乎不变,可以适当提高到5e-5。
- loss出现震荡或变成NaN,必须降低学习率。
第三步:检查数据中是否存在大量缺失值。
原始表格中如果text列有空白或NaN值,tokenizer在编码时会把空文本编码成全部是填充标记的Tensor。模型读到一堆0,啥也学不到。一个简单的检查方法:
df["text"].isna().sum() df["text"].str.len().describe()5.2 显存溢出(CUDA out of memory)怎么办
BERT模型参数量大,输入序列长,batch size稍微设置大一点就可能显存溢出。
排查链路如下:
第一步:降低Batch Size。
这是最直接的解决方式。如果per_device_train_batch_size=16溢出,就改成8。注意在降低Batch Size的同时,可以考虑适当降低学习率,因为更小的batch意味着更多次的参数更新,学习率不变有时会不稳定。
第二步:启用梯度累积。
在不想降低batch size的情况下,可以使用梯度累积来模拟更大的batch size:
training_args = TrainingArguments( gradient_accumulation_steps=2, ... )这种方式的效果是,每2个batch更新一次参数,相当于整体batch size扩大了一倍,但显存占用不变。
第三步:检查是否有其他进程占用显存。
运行nvidia-smi查看GPU显存使用情况。如果有残留的Python进程占用显存,可以用如下命令清理:
nvidia-smi --query-compute-apps=pid --format=csv找到PID之后终止对应进程。
5.3 每次跑出来的结果不一样
这个问题直接关系到论文结果能否复现。
排查链路如下:
第一步:确认是否设置了random seed。
在TrainingArguments中除了设置seed,还可以在训练前手动固定所有随机数生成器:
import torch import numpy as np import random def set_seed(seed): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) random.seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)这里通常容易被忽略的是添加torch.backends.cudnn.deterministic = True。cuDNN在训练时默认会使用启发式算法来寻找最快的卷积实现,这样会带来一定随机性。关闭benchmark模式后训练速度会略有下降,但结果可以确定性地复现。
第二步:确认数据划分是否固定。
每跑一次实验都用同一个random_state切分数据,并且固定切分顺序,确保训练集和验证集完全一致。
第三步:确认模型初始权重一致。
加载同一个预训练模型时,权重理论上是一致的。但如果你之前训练过同一个模型并修改了模型参数再保存,后续实验用的可能不是你期望的初始权重,结果自然对不上。
6. 评估结果分析与对比验证
模型训练完之后,如何分析和使用结果是复现论文的最后一个关键步骤。我在这里补充一些实际分析和操作思路。
6.1 从训练日志中观察模型状态
训练结束后,先不要急着看准确率,可以先从训练日志里看几个指标的走势。
Trainer运行后会输出类似下面的日志:
{'loss': 0.4123, 'learning_rate': 2e-5, 'epoch': 0.32} {'eval_loss': 0.3129, 'eval_accuracy': 0.8832, 'eval_runtime': 3.4, 'epoch': 1.0}需要关注两个点:
- 训练loss在每轮之内应该逐步下降,如果某一轮结束后eval_loss反而上升了,说明模型开始过拟合。
- learning_rate从初始值开始线性增长(warmup阶段),达到预设值后保持稳定,随后缓慢降低。
6.2 分类报告的深度解读
用sklearn生成完整的分类报告:
from sklearn.metrics import classification_report predictions, labels, _ = trainer.predict(val_dataset) pred_classes = predictions.argmax(axis=-1) print(classification_report(labels, pred_classes, target_names=list(label_mapping.keys())))输出会包含每个类别的precision、recall和F1分数。
论文复现中,要特别注意以下三个层面的对比:
- 整体准确率:模型在全部测试样本上预测正确的比例。如果论文报告准确率在百分之八十多,而你的复现结果在百分之七八十,需要检查数据处理和训练参数是否一致。
- 每个类别的precision/recall差距:某个特定类别recall特别低,说明模型对这个类别的区分能力不足,通常是训练样本量不够造成的,这种情况整体准确率再高也不足以支撑论文中的细分结论。
- 难分类的样本:可以单独提取出预测错误最多的样本,查看是标签标注错误还是文本本身有歧义。我自己在排查时发现,一些分类错误的财经新闻,实际上是人工标注时本身就不够准确,模型预测结果反而比人工标注更合理。
6.3 和论文结论的差距分析
复现实验的必要步骤之一,是和论文报告的结果进行对比。如果完全一致,当然是最好的状态。但更常见的情况是:结果略低于论文报告值,或者某些类别的指标和论文存在系统性差异。这时候要通过以下步骤排查:
- 数据集规模差异:论文可能使用了几万甚至几十万条数据,而你的复现只用了其中一小部分样本,效果差别会很明显。
- 数据清洗方式:原始文本是否做了去重、去除标点、去除网址和HTML标签,这些细节论文可能不会逐一列出,但每一步都会影响最后的结果。
- 预训练模型版本:
bert-base-chinese这一模型有多平台的不同版本,不同导出渠道的权重可能存在细微差异。 - 训练轮数和早停策略:论文可能采用了验证集上早停的策略来选择最优epoch,而不是简单固定训练轮数。
以上这些差异点逐个排查,基本就能定位结果不一致的原因。
7. 从复现到扩展:方法迁移的实操思路
跑通一篇文章只是第一步。更有价值的事情,是把这套方法迁移到自己的研究和工作场景里。
7.1 把二分类扩展到多分类
论文中的文本分类可能是二分类(比如判断政策文本属于宽松还是紧缩),也可能是多分类。多分类任务在代码层面几乎不需要改动:
- 模型定义时的
num_labels设置为类别数量。 - 数据标注时,标签映射为0到
num_labels-1的整数。 - 评估时,
accuracy_score和f1_score原样可用。
真正需要调整的是训练策略。类别不平衡问题在多分类里更常见,解决思路有两个:
- 计算各类别权重,在损失函数中加入
class_weight来处理。PyTorch自带交叉熵损失支持权重设置。 - 对少数类样本做数据增强或过采样。
更复杂的做法是对多标签分类任务,每一条文本可以同时属于多个类别。这种情况下需要把分类头从sigmoid改为多标签形式,num_labels改成全部标签的数量,评估指标也要换成子集准确率或Hamming Loss。
7.2 替换掉bert-base-chinese,使用其他预训练模型
如果你处理的文本是英文财经新闻,可以把bert-base-chinese替换成bert-base-uncased。如果是金融领域的中文文本,可以试试领域预训练模型。替换模型时只需要改一行代码:
model_name = "bert-base-chinese" # 可以替换成其他模型名称 model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels) tokenizer = AutoTokenizer.from_pretrained(model_name)不同模型的词表和分词器不同,数据预处理部分无需变动,transformers库会自动适配。
7.3 代码组织建议
复现类的项目,代码结构建议按照以下方式组织,便于后续复用:
project/ ├── data/ # 原始数据,通常不进Git仓库 ├── models/ # 保存微调后的模型权重 ├── scripts/ │ ├── data_preprocess.py # 数据加载与标签处理 │ ├── train.py # 模型训练 │ └── evaluate.py # 评估和预测 ├── results/ # 训练日志和评估结果 ├── requirements.txt # 环境依赖 └── README.md # 复现说明文档这个结构的核心逻辑是数据、模型、代码、结果四者分离,任何一部分更新了,其他部分不需要跟着改动。下次拿新数据做实验时,只要改数据预处理脚本里的文件路径和标签映射就够了。
8. 最后分享一个省时间的技巧
跑BERT微调的时候,如果只是验证代码能否跑通,不建议一上来就用几万条数据训练好几个epoch。我习惯先做一个小规模跑通测试:数据只用几百条,训练轮数设成1到2轮,模型还是用完整的BERT结构不变。这个做法不是为了拿到好结果,而是快速确认数据管道、模型定义、训练流程和保存加载这些环节都没有问题。
小规模跑通之后,再把数据换成全量数据,训练轮数加到合适的数值。这样排查问题的时间成本会低很多,也不容易在长训练过程中反复怀疑是代码问题还是参数问题。
另外,关于评价标准多说一句:除了准确率,建议把F1分数也作为核心指标。学术场景里分类数据往往有类别不平衡的问题,准确率容易被多数类拉高,F1能更均衡地反映模型在各类别上的表现。论文复现报告里同时列出这两个指标,说服力会强很多。
这套流程我已经在多个文本分类场景下验证过多次,稳定性是可以放心的。希望你也能以此为起点,跑通属于自己的第一个BERT微调模型。