news 2026/10/20 6:58:21

BERT中文文本分类实战:从数据流到模型微调的完整工程链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BERT中文文本分类实战:从数据流到模型微调的完整工程链路

简介:面向 NLP 初学者和算法工程师,一套基于 pytorch-transformers 的 BERT 中文文本分类代码覆盖了数据准备、模型微调到效果评估的完整流程,可用于快速搭建分类基线或业务预研。数据源自 THUCNews 随机抽取的20万条新闻标题,分为财经、房产、股票等10个类别,按18万、1万、1万划分训练、验证与测试集,标注清晰且类别均衡,可直接用于监督学习实验。代码包共29个文件,以 Python 脚本、预训练 BERT 权重、词汇表、JSON 配置、标注文本和说明文档为主,整体约732.57MB;其中预训练权重与配套配置、词汇表可加载为可用的中文 BERT 模型,预处理脚本完成文本清洗,训练脚本实现微调与保存,并附结果图表辅助评估,目录划分清晰,便于按模块查阅。已有3700人学习下载。借助这套代码,使用者可完整复现中文文本分类结果,也可基于模块化设计扩展类别、调整超参数或接入自身业务数据,适合作为 NLP 分类任务的实践参考。

1. BERT 中文文本分类:先读懂数据流,再动手跑代码

做中文 NLP 分类任务,很多同学第一反应就是把文本丢给 BERT,跑一个 finetune 就完事。但真正上手你会发现,翻车率最高的环节反而不在模型本身——tokenizer 的编码方式、label 与 id 的映射、数据集的封装顺序,任何一个地方对不上,训练出来的模型就是一团黑匣子。这份基于 pytorch-transformers(也就是现在的 HuggingFace transformers 库)实现的 BERT 中文文本分类代码,恰好把整条数据流从原始文本到预测结果串得清清楚楚。它适合两类人:一类是刚接触预训练模型微调、想跑通一个完整中文分类项目的入门者;另一类是自己写过分类脚本、但想梳理清楚数据封装和训练循环的细节、顺便看看有没有更规范的写法的开发者。下面我会从目录结构、数据侧实现、训练闭环、踩坑记录到改造思路,把这份代码逐层拆开讲。

2. 整体架构与目录设计:先把一份能跑的工程布局讲清楚

2.1 目录结构:每个文件放在哪、负责什么事

打开这份资源,第一件事不是看代码,而是先把目录结构过一遍。一份好的分类工程,目录就代表了数据流动的方向。下面是我拿到的源码包展开后的典型结构(和多数 BERT 分类项目的组织方式一致):

BERT_Chinese_Classifier/ ├── config.py # 全局配置:数据路径、超参数、模型路径 ├── requirements.txt # 依赖列表 ├── data/ │ ├── train.txt # 训练集:每行 text\tlabel │ ├── dev.txt # 验证集 │ └── test.txt # 测试集 ├── src/ │ ├── dataset.py # 数据读取、tokenizer 编码、Dataset 封装 │ ├── model.py # BERT 模型 + 分类头 │ ├── train.py # 训练主流程 │ ├── evaluate.py # 验证/测试脚本 │ └── predict.py # 单条文本推理入口 └── output/ ├── checkpoints/ # 训练产生的模型权重 └── predictions/ # 预测结果输出

这个布局的思路很直接:config.py 是全局的“总闸”,所有路径和超参都集中在这里,训练、评估、预测三个入口共享同一份配置,避免每个脚本各自维护一套参数。data/ 目录放原始文本,src/ 目录放核心代码,output/ 目录放产物。

看这份代码你会发现,作者把数据读取和模型定义拆成了 dataset.py 和 model.py 两个独立文件。这样做的好处是:改数据格式时不用动模型文件,换模型结构时也不用碰数据封装,职责边界清晰,二次开发时能少踩很多坑。

2.2 依赖与版本:pytorch-transformers 的前世今生

这里先解释一个关键点:项目标题里写的 pytorch-transformers 其实是 transformers 库的曾用名。在 2019 年前后,这个库还叫 pytorch-transformers,后来和 tensorflow 版本合并,统一改名为 transformers。所以这份代码实际加载的依赖是当下的 HuggingFace transformers 库,安装时直接用新名字即可。

requirements.txt 里的核心依赖一般长这样:

transformers>=4.18.0 torch>=1.10.0 numpy pandas scikit-learn tqdm

装的时候有个血泪经验:不要一上来就pip install transformers装最新版。transformers 库迭代很快,4.x 各个小版本之间 API 有细微差异,尤其是AutoModelForSequenceClassification的返回类型和Trainer的行为,不同版本会有些不同。我一般会先建一个干净的虚拟环境,装好 torch 之后再装 transformers,然后跑一段最小样例验证能加载 BERT 模型再往下走。

安装命令:

# Python 3.8+ 环境,建议先装 torch 再装 transformers pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers numpy pandas scikit-learn tqdm

CUDA 版本要和你本机的显卡驱动匹配,装错的话训练时会出现 CUDA error,这类问题后面第 5 章单独讲。

2.3 配置文件:超参数集中在 config.py 里

config.py 把所有可调整的参数都集中在一起,这份代码的超参设计比较常规,拿过来改几行就能适配自己的数据。核心参数拆解大概是这样:

# config.py 核心内容 import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 数据路径 TRAIN_PATH = os.path.join(BASE_DIR, "data", "train.txt") DEV_PATH = os.path.join(BASE_DIR, "data", "dev.txt") TEST_PATH = os.path.join(BASE_DIR, "data", "test.txt") # 模型配置 MODEL_NAME = "bert-base-chinese" # 中文 BERT 预训练模型 NUM_LABELS = 10 # 分类类别数,按自己的数据改 MAX_LEN = 128 # 文本最大长度,超过则截断 # 训练超参数 BATCH_SIZE = 32 LEARNING_RATE = 2e-5 EPOCHS = 5 WARMUP_RATIO = 0.1 # warmup 比例 # 输出配置 OUTPUT_DIR = os.path.join(BASE_DIR, "output") CHECKPOINT_DIR = os.path.join(OUTPUT_DIR, "checkpoints") DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

参数说明:MAX_LEN = 128是中文 BERT 的标准输入长度上限,BERT 本身最长支持 512 token,但实际分类任务里 128 通常够用——长文本截断到 128 之后信息损失不大,但训练速度快接近 4 倍。如果你的数据是长文本(比如新闻全文、法律文书),这个值要调大,后面第 6 章会讲长文本的处理策略。

WARMUP_RATIO表示训练过程中前 10% 的 step 使用线性 warmup,学习率从 0 逐渐升到设定的LEARNING_RATE,这样能避免模型在初始阶段因为学习率过大而震荡。

3. 数据侧核心代码:从原始文本到模型输入的完整编码链路

3.1 原始数据格式:一行文本一个 tab 一个标签

这份代码要求的数据格式非常简单:data/train.txt里每一行是一条样本,文本和标签之间用\t分隔,标签是字符串形式的类别名。示例:

这家餐厅的菜非常好吃,环境也很优雅 正面 完全是一次糟糕的体验,再也不来了 负面 性价比很高,服务态度也不错 正面

文本里不能出现 tab 符,否则会切错位置。如果你的原始数据是 CSV 或者 Excel,建议提前转成这种格式,或者改 dataset.py 里的读取逻辑。作者在 README 里提到“文本中的特殊符号建议提前清洗”,我在实际使用时,一般会在前面加一步去重和清洗:去掉换行符、规范全角半角、过滤掉过短的文本。

3.2 tokenizer 编码:padding、truncation、return_tensors 缺一不可

接下来看 dataset.py 的核心部分。BERT 不能直接吃原始字符串,需要先把文本转成 input_ids、attention_mask、token_type_ids 三个张量,这个转换由 tokenizer 完成。

# src/dataset.py from torch.utils.data import Dataset from transformers import AutoTokenizer class BertClassifierDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len): self.texts = texts # labels 是字符串形式的类别名,转成 id 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] # 核心编码逻辑:padding + truncation 一步完成 encoded = self.tokenizer.encode_plus( text, max_length=self.max_len, add_special_tokens=True, # 自动加 [CLS] 和 [SEP] padding="max_length", # 统一 pad 到 max_len truncation=True, # 超过 max_len 截断 return_attention_mask=True, return_token_type_ids=True, return_tensors="pt" # 返回 PyTorch 张量,不需要手动转换 ) return { "input_ids": encoded["input_ids"].squeeze(0), "attention_mask": encoded["attention_mask"].squeeze(0), "token_type_ids": encoded["token_type_ids"].squeeze(0), "label": label }

几个参数值得特别注意:

padding="max_length"会把所有样本统一 pad 到max_len,这样同一个 batch 里的 tensor 形状一致,能直接拼成一个矩阵喂给模型。如果用padding=True只 pad 到当前 batch 的最长长度,输出形状会不一致,DataLoader 里的collate_fn处理起来很麻烦。

truncation=True表示超过max_len就截断。中文 BERT 的词表里是字级别的 token,128 个 token 大约对应 128 个汉字,短文本完全够用。

return_tensors="pt"让 tokenizer 直接返回 PyTorch 张量。这里有个小坑:返回的张量是 shape(1, max_len),多了个 batch 维度,所以后面要加一个.squeeze(0)。

add_special_tokens=True会自动在文本开头加[CLS],结尾加[SEP]。对分类任务来说,[CLS]位置的向量会作为整个句子的语义表示输入到分类头,所以这个 token 必须加。

3.3 label 与 id 的映射:字符串到数字的跨接

文本转成 token 之后,标签也不能直接用字符串。在train.py里有一段代码负责构建 label 映射关系:

# src/train.py 中处理 label 映射的片段 labels = list(set(all_labels)) # 从训练集里取所有不重复的标签 labels.sort() # 排序保证映射稳定 label2id = {label: i for i, label in enumerate(labels)} id2label = {i: label for label, i in label2id.items()} # 把文本和 label id 组装成 Dataset train_dataset = BertClassifierDataset( texts=train_texts, labels=[label2id[label] for label in train_labels], # 转成 id tokenizer=tokenizer, max_len=config.MAX_LEN )

这段代码对初学者来说很有学习价值。label2id和id2label两个字典构建出来之后,训练时用 label id 计算 loss,预测时用 id2label 把输出转回可读的字符串类别名。很多新手在这块踩坑,最常见的问题是:训练集和验证集的 label 集合不一致,导致验证时label2id[label]直接 KeyError。我一般会在构建映射前先打印一下验证集的标签分布,确认两边一致再跑训练。

3.4 DataLoader 的 collate_fn:一个容易忽略的细节

Dataset 定义好之后,DataLoader 默认的 collate 行为是按 batch 把多个样本堆叠。但这里有个问题:__getitem__返回的是一个 dict,每个字段都是单个样本的张量,直接堆叠会报错。所以还得配一个collate_fn:

from torch.utils.data import DataLoader def collate_fn(batch): # batch 是一个 list,里面是多个样本的 dict input_ids = torch.stack([item["input_ids"] for item in batch]) attention_mask = torch.stack([item["attention_mask"] for item in batch]) token_type_ids = torch.stack([item["token_type_ids"] for item in batch]) labels = torch.tensor([item["label"] for item in batch], dtype=torch.long) return { "input_ids": input_ids, "attention_mask": attention_mask, "token_type_ids": token_type_ids, "labels": labels } train_loader = DataLoader( train_dataset, batch_size=config.BATCH_SIZE, shuffle=True, collate_fn=collate_fn )

这个collate_fn的核心动作是:把 batch 里 N 条样本的每个字段分别用torch.stack堆叠成形状(N, max_len)的张量。labels 单独转成torch.long类型,因为后面的CrossEntropyLoss要求目标必须是 long 类型。

注意这里用torch.stack而不是torch.cat。stack 是在新维度上堆叠,cat 是沿着已有维度拼接。对单条样本来说,必须是 stack 才能把多个样本合成一个 batch。

4. 训练闭环拆解:模型初始化、训练循环与评估

4.1 模型初始化:AutoModelForSequenceClassification 一行搞定

模型侧的核心代码在 model.py 和 train.py 里。用 transformers 做分类微调,最常见的方式是直接用AutoModelForSequenceClassification,它在 BERT 顶部自动添加了一个线性分类层,隐藏层输出经过池化后映射到 NUM_LABELS 维。

# src/train.py 中模型初始化部分 from transformers import AutoModelForSequenceClassification, AdamW model = AutoModelForSequenceClassification.from_pretrained( config.MODEL_NAME, # "bert-base-chinese" num_labels=config.NUM_LABELS, return_dict=True # 返回 dict 形式的输出,便于取 logits ) model.to(config.DEVICE)

这里的num_labels必须和 config.py 里的NUM_LABELS一致,同时要和你数据集的类别总数一致。假如你的数据有 10 个类别,但NUM_LABELS误设成 2,模型最后一层输出维度就是 2,训练时 loss 会异常高,模型永远收敛不了。

我见过有人在这里“手写” BERT 分类模型:加载BertModel,手动取[CLS]向量,再接一个nn.Linear(768, num_labels)。这种做法当然可行,但没必要。AutoModelForSequenceClassification内部做的就是这件事,而且它对return_dict的处理、对标签缺省的容错都更规范。除非你需要自定义分类头(比如多层全连接加 dropout),否则用 Auto 系列就够了。

关于参数冻结的问题:很多做迁移学习的同学习惯把 bert 底层的参数 freeze 住,只训练分类头。但 BERT 微调的主流做法恰恰相反——全量参数都参与训练,只是学习率设得低一些(2e-5 到 5e-5 之间)。BERT 预训练时见过大量语料,文本语义特征已经很强,但下游任务的特定模式仍需微调底层的注意力权重。强行冻结底层参数,在数据量较小时效果还行,数据量一大,效果就不如全量微调。

4.2 训练循环:optimizer、scheduler、梯度累积

训练循环写得比较“基础但规范”,没有用 transformers 的 Trainer 封装,而是手写了标准的训练 loop,这样每一步发生的计算都清晰可见,适合学习,也方便改造成自己的逻辑。

# src/train.py 训练主循环 from transformers import AdamW, get_linear_schedule_with_warmup import torch optimizer = AdamW(model.parameters(), lr=config.LEARNING_RATE) # 总训练步数:len(train_loader) 是每个 epoch 的 batch 数 total_steps = len(train_loader) * config.EPOCHS warmup_steps = int(total_steps * config.WARMUP_RATIO) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps ) for epoch in range(config.EPOCHS): model.train() total_loss = 0 for step, batch in enumerate(train_loader): # 把数据搬到 GPU input_ids = batch["input_ids"].to(config.DEVICE) attention_mask = batch["attention_mask"].to(config.DEVICE) token_type_ids = batch["token_type_ids"].to(config.DEVICE) labels = batch["labels"].to(config.DEVICE) # 前向传播 outputs = model( input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, labels=labels ) loss = outputs.loss total_loss += loss.item() # 反向传播与参数更新 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad() if step % 50 == 0: print(f"Epoch {epoch+1}/{config.EPOCHS}, Step {step}, Loss: {loss.item():.4f}")

这段代码里的几个细节值得展开:

outputs.loss是模型内部计算好的交叉熵损失。当传入labels参数时,AutoModelForSequenceClassification会自动计算CrossEntropyLoss,不需要在外部手动写损失函数。如果你想用别的方式计算 loss,可以不给 model 传 labels,改为在拿到 logits 后自己算。

clip_grad_norm_(max_norm=1.0)是梯度裁剪。BERT 在训练初期容易出现梯度爆炸,表现为 loss 突然变成 nan 或 inf,加上这个裁剪能防止梯度超过 1.0 的范数,是保险丝式的设置。

optimizer.zero_grad()放在 update 之后、下一轮反向传播之前。顺序错了会导致梯度累积到上一次的参数上,训练出现诡异波动。很多人在这块出过问题,现象是 loss 忽高忽低不下降。

如果你的显存不够,常见做法是加一个梯度累积:每 N 个 batch 才执行一次optimizer.step(),用更小的有效 batch size 拼出大 batch 的效果。改法很简单,把loss.backward()之后的内容包在if (step + 1) % gradient_accumulation_steps == 0:里。源代码没写这个,但实际跑大批量数据时很有用。

4.3 评估函数与模型保存:别只盯 accuracy

训练完之后,evaluate.py 负责在验证集上评估模型。这份代码的评估写得比较完整,除了准确率,还输出了精确率、召回率和 F1:

# src/evaluate.py from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score def evaluate(model, dev_loader): model.eval() all_preds = [] all_labels = [] with torch.inference_mode(): # 比 no_grad 更快,不追踪梯度 for batch in dev_loader: input_ids = batch["input_ids"].to(config.DEVICE) attention_mask = batch["attention_mask"].to(config.DEVICE) token_type_ids = batch["token_type_ids"].to(config.DEVICE) labels = batch["labels"].to(config.DEVICE) # 前向传播,拿到 logits outputs = model( input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids ) logits = outputs.logits # 取最大概率对应的索引作为预测类别 preds = torch.argmax(logits, dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) accuracy = accuracy_score(all_labels, all_preds) precision = precision_score(all_labels, all_preds, average="weighted") recall = recall_score(all_labels, all_preds, average="weighted") f1 = f1_score(all_labels, all_preds, average="weighted") print(f"Accuracy: {accuracy:.4f}, Precision: {precision:.4f}, Recall: {recall:.4f}, F1: {f1:.4f}") return f1

torch.inference_mode()是 PyTorch 1.9 之后引入的替代torch.no_grad()的上下文管理器,它会关闭梯度跟踪和自动求导,推理时速度更快。

torch.argmax(logits, dim=1)取每个样本预测概率最大的类别索引。这里要注意:logits 的形状是(batch_size, num_labels),所以dim=1是对类别维度求最大值,取出来之后是每个样本的预测类别 id。

评估时模型一定要加model.eval()。这行代码会把 dropout 层关掉、BN 层切换为推理模式,如果漏了,评估结果会带有随机性,同样的数据跑两次结果不一样。

模型保存部分一般会执行两个动作:保存完整的模型权重,同时把 tokenizer 也一并保存,这样后续加载时不用再指定模型名称:

model.save_pretrained(config.CHECKPOINT_DIR) tokenizer.save_pretrained(config.CHECKPOINT_DIR)

保存 tokenizer 是一个很容易被忽略的动作。换个环境、换个机器,如果还要重新下载 bert-base-chinese 的 tokenizer 文件,慢且容易出网络问题。把 tokenizer 放到 checkpoint 目录里,推理时直接AutoTokenizer.from_pretrained(CHECKPOINT_DIR)就能恢复出和训练时完全一致的配置。

5. 常见问题排查:跑通之后还得防的坑

5.1 现象:训练时报错CUDA out of memory

这个报错在 BERT 微调项目里出现频率极高。显存不够的表现是:程序跑到第一个 batch 前向传播时,直接抛出RuntimeError: CUDA out of memory,有时候甚至是在加载模型的时候就炸了。

原因基本是两种:一是 batch size 太大,一个 batch 的文本经过 BERT 的 12 层 Transformer 之后,中间激活值占用显存非常高;二是机器上其他程序占用了显存,nvidia-smi 看下来可用显存只有几百兆。

解决路径从简单到复杂:先调小config.BATCH_SIZE,从 32 降到 8 或者 4,显存占用是线性下降的;再不行开梯度累积,用小 batch 模拟大 batch;最后还有一招是开torch.cuda.empty_cache()释放缓存碎片。如果用的是 6G 显存的卡,BERT-base 加 batch size 8 和 max_len 128 通常能跑动,再大就得考虑换显卡或者用混合精度。混合精度在代码里开启很简单:

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for step, batch in enumerate(train_loader): # ... with autocast(): outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels) loss = outputs.loss scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

这段代码把前向传播和 loss 计算放在autocast()上下文里,FP16 计算能在不损失太多精度的情况下把显存占用降低一半左右。

5.2 现象:loss 不下降或直接变成 nan

训练了十几个 step,loss 纹丝不动,甚至直接输出 nan 或者 inf。排查顺序一般是下面这样:

先看学习率。BERT 微调的学习率范围极其敏感,1e-4就很容易导致 loss 震荡甚至发散,2e-5到5e-5是安全区间。这份代码里LEARNING_RATE = 2e-5是一个经过验证的起点值。

再看数据和 label 映射。如果NUM_LABELS设置成 2,但实际数据有 10 类,label2id映射就会出现越界的 label id,CrossEntropyLoss收到超出范围的 target 会计算出 nan 或者报错。我遇到过一次很隐蔽的情况:训练集 label 正常,验证集里混入了一个训练集没出现过的新标签,label2id[label]直接 KeyError。这个问题在真实项目里很常见,原因是数据划分时没有保证训练/验证集的标签空间一致。解决方法是构造映射时,从两个集合的并集里取 label,或者更稳妥的做法是直接检查验证集中每个标签是否都出现在训练集的标签集合里,发现不在就报错提示。

最后看数据里面有没有空文本。空文本经过 tokenizer 后只剩[CLS]和[SEP]两个 token,语义信息为零,模型在这种样本上容易输出极端 logits。我在预处理阶段会过滤掉len(text.strip()) < 1的样本。

5.3 现象:预测结果全部都是同一个类别

训练完模型,评估 F1 看起来还行,但实际拿来预测单条文本时,所有输出都集中在某一个类别上,尤其是多数类。这是典型的类别不均衡问题。

原因很简单:数据里某个类别的样本占比过高,比如 90% 都是“不好”这个类别,模型学到的策略是“永远预测不好”,整体准确率也有 90%,但另一个类别完全没学会。

解决思路有几个层面。数据层面:做下采样或上采样,让类别分布均衡一些。代码层面:给CrossEntropyLoss传class_weight。在 transformers 的模型里,可以在初始化时传入classifier_dropout之类的参数,但计算 class_weight 更直接的方式是手动改训练循环:

from torch.nn import CrossEntropyLoss # 按样本数量的倒数计算权重,数据里样本越少的类别权重越大 class_counts = torch.bincount(torch.tensor(all_labels), minlength=config.NUM_LABELS) class_weights = 1.0 / class_counts.float() loss_fn = CrossEntropyLoss(weight=class_weights.to(config.DEVICE)) # 训练时不用 outputs.loss,改为手动计算 logits = outputs.logits loss = loss_fn(logits, labels)

模型层面:对少数类样本做数据增强,比如 EDA(同义词替换、随机插入、随机删除、随机交换),这在文本分类里能起到一定缓解作用。这份源代码没有实现这些,但作为二次开发的改进方向很适合。

5.4 现象:不同机器上跑出来的效果不一致

这里有两种情况。第一种是随机性问题。BERT 微调本身有一定随机性:数据加载顺序、dropout 的随机掩码、权重初始化的微小差异都会导致最终模型效果有波动。解决方式是固定随机种子。我在开始训练前一般会加这样几行:

import numpy as np import torch import random def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)

第二种是环境一致性。transformers 版本不同、torch 版本不同,同一个模型的输出会有细微差异,严重的甚至导致模型结构对不上、无法加载权重。所以跨机器复现时,首先保证pip freeze > requirements-lock.txt,把精确版本号锁定,再用这个文件到新环境安装依赖。

6. 二次开发思路:把这份代码改成你自己的业务分类器

跑通这份代码只是开始,把它改造成能支撑真实业务的分类器才是关键。这里分享三个我在实际项目中用得最多的改造方向。

第一,多标签分类。这份代码默认是单标签分类,一个样本只属于一个类别。但现实业务里,一篇文章可能同时属于科技和生活两个类别,这就是多标签分类任务。改造点有两个:模型侧把num_labels改成年标签总数;损失函数从 CrossEntropyLoss 换成 BCEWithLogitsLoss。模型侧直接用AutoModelForSequenceClassification也能做多标签,只要把损失函数换掉即可。

# 多标签分类损失改造 from torch.nn import BCEWithLogitsLoss loss_fn = BCEWithLogitsLoss() # 标签格式从 single label id 改成 multi-hot 向量 # 比如 [0, 1, 0, 1, 0] 表示该样本属于第 1 和第 3 个类别 loss = loss_fn(logits, multi_hot_labels.float())

推理时不能用argmax,要改成按阈值判断每个类别是否生效,比如sigmoid(logits) > 0.5记为 1,否则为 0。

第二,长文本处理。MAX_LEN=128对短文本(如评论、微博)够用,但遇到行业报告、法律文书这类长文本,截断到 128 后信息丢失严重。一个常规做法是分段 + 池化:把长文本切成多个 128 token 的段,每段单独过模型取[CLS]向量,然后用最大池化或加权平均合并成整个文本的表示,再过分类头。这个改动会涉及模型结构的调整,手写一个自定义分类器会更灵活。

第三,推理性能优化。模型训练好只是第一步,如果要做在线推理(比如 API 接口),速度和显存就是大问题。这份代码的 predict.py 是单条预测的示例,实际部署时我会做两个优化:一是用torch.inference_mode()包住推理函数;二是把模型转成 half 精度,显存直接减半,速度提升明显。如果对延迟要求更高,还可以用 ONNX Runtime 导出模型做 CPU 推理,能压到几十毫秒级别。

最后说一个我个人的习惯:每次拿到一个新的文本分类任务,我不会直接上全量数据训练,而是先抽 200 条样本、跑 3 个 epoch,快速验证数据读取、label 映射、训练循环整条链路没有问题,再上全量数据。这个“冒烟测试”的习惯帮我挡掉了无数次“辛辛苦苦训练了 5 个小时,评估损失却比随机猜测还差”的尴尬局面,其中大部分问题都出在数据预处理和 label 映射上,而不是模型本身。

这份资源的价值在于它把 BERT 中文分类的整条工程链路揉成了一坨完整的代码,从数据格式、tokenizer 编码到训练评估、模型保存,每一环都能对照着看。希望你能把它跑起来,改造成符合自己业务场景的分类器。如果过程中遇到我上面提到的问题,回过来看看第 5 章的排查清单,多半能找到答案。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 6:57:37

无人驾驶MPC轨迹跟踪代码实战:从仿真到实车避坑指南

简介&#xff1a;这份资源面向无人驾驶与自动控制方向的学习者和开发者&#xff0c;提供模型预测控制&#xff08;MPC&#xff09;的完整程序代码实现&#xff0c;帮助理解MPC在车辆路径规划、速度控制与转向操作中的落地方式。压缩包共16个文件&#xff0c;以13个m脚本为主&am…

作者头像 李华
网站建设 2026/10/20 6:57:16

msinttypes-r26.rar:解决老MSVC缺失stdint.h的C语言补丁包

简介&#xff1a;msinttypes-r26.rar 面向在 Visual C 环境下编译 C99 代码时遭遇 fatal error C1083 无法打开 stdint.h 的开发者&#xff0c;尤其适合仍在使用 VS2008 等旧版编译器的同学。该错误源于 VC 未内置 C99 标准的 stdint.h 头文件&#xff0c;资源提供的正是缺失的…

作者头像 李华
网站建设 2026/10/20 6:57:00

cy163聚合客服系统:开源多渠道统一接入与私有化部署指南

简介&#xff1a;聚合客服&#xff08;万能客服&#xff09;cy163_customerservice 22.0.0 全开源版安装更新一体包&#xff0c;是一款面向中小企业、开发者及技术团队的轻量级多渠道客服系统解决方案&#xff0c;聚焦于降低部署门槛、支持深度定制与快速集成。资源共269个文件…

作者头像 李华
网站建设 2026/10/20 6:56:27

微信小程序从零开发到上线:原生框架+云开发完整实践

先说结论&#xff1a;用微信原生框架从零开始写一个能正常上线的小程序&#xff0c;没有想象中那么玄乎&#xff0c;但也不会像看几分钟视频就能马上搞定那么容易。这篇文章我想用一个名字叫 WorkBuddy 的“工作辅助小程序”当例子&#xff0c;把从注册账号、安装开发者工具、创…

作者头像 李华
网站建设 2026/10/20 6:55:34

价值投资与量化投资:两种世界观、决策链与本土实战对比

1. 先从一句话说起&#xff1a;价投和量化争论的其实不是方法&#xff0c;是世界观国内资管行业里有一个很常见、也很容易吵起来的场面&#xff1a;饭桌上有人聊起价值投资&#xff0c;强调要长期持有好公司&#xff0c;旁边马上就有人接话&#xff0c;说量化才是更科学、更可复…

作者头像 李华
网站建设 2026/10/20 6:55:30

gcc编译流程与静态库动态库完全指南:嵌入式开发必啃的硬骨头

1. 从零开始&#xff1a;今天为什么要死磕gcc和库嵌入式学习进入第十天&#xff0c;我给自己定的主题是Linux系统编程里的gcc工作流程和库的使用。很多初学者会在这一步草草带过&#xff0c;觉得不就是gcc hello.c -o hello嘛&#xff0c;有什么好学的&#xff1f;但我在前几天…

作者头像 李华