用 Fairseq 微调 RoBERTa 完成自定义文本分类任务:IMDB 情感分类全流程实战
2026/9/14 19:02:22 网站建设 项目流程

用 Fairseq 微调 RoBERTa 完成自定义文本分类任务:IMDB 情感分类全流程实战

【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm

本文基于 unilm 仓库中 kosmos-2 附带的 fairseq 示例文档(kosmos-2/fairseq/examples/roberta/README.custom_classification.md),完整讲解如何把 RoBERTa 预训练模型微调到一个自定义文本分类任务上,以 IMDB 电影评论情感二分类为具体实例。读完后你可以独立走通“原始文本数据 → 格式化 → BPE 编码 → fairseq 二进制预处理 → 训练命令 → 用 hub 接口加载 checkpoint 推理”的完整链路,并理解每一步背后的 fairseq 源码实现(sentence_prediction任务、分类头注册与 hub 推理接口)。

一、这条自定义分类流水线要解决什么问题

fairseq 中 RoBERTa 的微调示例默认面向 GLUE、WSC 等标准基准,而实际业务中更多是“自己手里有一批文本 + 标签”。这份文档给出的方案是:把任意分类数据组织成 fairseq 的sentence_prediction任务所需的目录结构,然后复用fairseq-train的通用训练框架,用--restore-file加载 RoBERTa 预训练权重,只训练一个随机初始化的分类头(classification head)。

整条链路由 6 个阶段构成:

阶段命令 / 工具产物
1. 获取数据wget/taraclImdb/原始目录
2. 格式化文档中的 Python 脚本train.input0/train.label/dev.input0/dev.label
3. BPE 编码multiprocessing_bpe_encoder*.input0.bpe(token id 文本)
4. 二进制预处理fairseq-preprocess(两次调用)IMDB-bin/input0/IMDB-bin/label/.bin/.idx
5. 训练fairseq-traincheckpoints/checkpoint_best.pt
6. 推理RobertaModel.from_pretrained+predict分类预测

下面逐步展开。

二、阶段 1:获取并解压 IMDB 数据

IMDB 数据集来自 Stanford 的 aclImdb 发布包,下载后是一个“一条样本一个文件”的目录树(train/postrain/negtest/postest/neg):

wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz tar zxvf aclImdb_v1.tar.gz

解压后的结构为aclImdb/{train,test}/{pos,neg}/*.txt,每个.txt文件包含一条影评,所在目录名即类别标签。这种“文件即样本”的组织方式正是下一步格式化脚本要处理的输入形态。

三、阶段 2:把“文件即样本”转成两个对齐的文本文件

fairseq 的sentence_prediction任务要求输入是行对齐的文本文件(或后续的二进制索引文件):*.input0存放样本文本,*.label存放对应的标签整数,且两文件行数一一对应。文档给出的转换脚本如下(完整保留):

import argparse import os import random from glob import glob random.seed(0) def main(args): for split in ['train', 'test']: samples = [] for class_label in ['pos', 'neg']: fnames = glob(os.path.join(args.datadir, split, class_label) + '/*.txt') for fname in fnames: with open(fname) as fin: line = fin.readline() samples.append((line, 1 if class_label == 'pos' else 0)) random.shuffle(samples) out_fname = 'train' if split == 'train' else 'dev' f1 = open(os.path.join(args.datadir, out_fname + '.input0'), 'w') f2 = open(os.path.join(args.datadir, out_fname + '.label'), 'w') for sample in samples: f1.write(sample[0] + '\n') f2.write(str(sample[1]) + '\n') f1.close() f2.close() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--datadir', default='aclImdb') args = parser.parse_args() main(args)

几个细节值得注意:

  • 标签映射pos → 1neg → 0,写入.label文件的是纯整数。后续 fairseq 会把这些行当作“词典 token”处理(见第五节label词典的加载方式)。
  • 随机打乱random.seed(0)固定随机种子后用random.shuffle(samples)打乱样本,保证每次运行结果可复现。
  • split 重命名:原始数据里的test在这里被重命名为dev(验证集)。这是 fine-tuning 阶段的惯例——dev集用于训练中挑选最佳 checkpoint(--best-checkpoint-metric accuracy),真正的test评测留到最后做。
  • 产出文件aclImdb/train.input0aclImdb/train.labelaclImdb/dev.input0aclImdb/dev.label。注意每条样本只读取文件第一行(fin.readline()),因为 IMDB 的每条影评实际只占一行。

换自己的数据集时,只需把这里的读取与标签映射逻辑替换掉,输出保持“一行一样本、input0 与 label 行对齐”即可。

四、阶段 3:用 GPT-2 BPE 对文本做分词编码

RoBERTa 的分词遵循 GPT-2 BPE,必须使用官方发布的encoder.jsonvocab.bpe两份文件。fairseq 提供了多线程编码工具examples/roberta/multiprocessing_bpe_encoder.py,它把--inputs指定的原始文本逐行转成以空格分隔的 token id 序列(对应实现见 multiprocessing_bpe_encoder.py 中的MultiprocessingEncoder.encode_lines)。

# Download encoder.json and vocab.bpe wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json' wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe' for SPLIT in train dev; do python -m examples.roberta.multiprocessing_bpe_encoder \ --encoder-json encoder.json \ --vocab-bpe vocab.bpe \ --inputs "aclImdb/$SPLIT.input0" \ --outputs "aclImdb/$SPLIT.input0.bpe" \ --workers 60 \ --keep-empty done

命令行参数说明(均可在 multiprocessing_bpe_encoder.py 的argparse定义中确认):

  • --encoder-json/--vocab-bpe:GPT-2 BPE 词表文件路径;
  • --inputs/--outputs:支持多个输入输出对,要求数量一致,-表示标准输入输出;
  • --workers:多进程 worker 数,默认 20,文档示例用 60;
  • --keep-empty:保留空行。不加该选项时,遇到空行该样本会被标记为EMPTY并过滤掉(见 源码 L108-L119)——分类任务中一旦过滤了某一行,input 与 label 就会行错位,因此这里必须加--keep-empty

文档也指出,BPE 编码完全可以合并进上一步的格式化脚本中对每条样本直接执行,只是那样更慢,所以实践中把编码独立成一步、用多进程批量跑更快。注意:本步只编码input0label文件里的整数不需要 BPE 编码,直接进入下一步的预处理。

五、阶段 4:fairseq-preprocess 生成二进制数据

这一步把文本文件转成 fairseq 的二进制索引格式(.bin/.idx),训练时才能被高效加载。需要两次调用,分别处理输入文本和标签,并各自建立词典:

# Download fairseq dictionary. wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt' fairseq-preprocess \ --only-source \ --trainpref "aclImdb/train.input0.bpe" \ --validpref "aclImdb/dev.input0.bpe" \ --destdir "IMDB-bin/input0" \ --workers 60 \ --srcdict dict.txt fairseq-preprocess \ --only-source \ --trainpref "aclImdb/train.label" \ --validpref "aclImdb/dev.label" \ --destdir "IMDB-bin/label" \ --workers 60

两次调用的差异:

  • 第一次(IMDB-bin/input0--srcdict dict.txt指定使用随 RoBERTa 发布的官方 GPT-2 词典(49152 词元)。这样预处理后的 token id 与预训练模型 embedding 的 id 空间完全一致,--restore-file加载权重时才能对得上,无需重建词表。
  • 第二次(IMDB-bin/label:不指定词典,由 fairseq 从train.label/dev.label中自动建一个小词典(对 IMDB 而言就是01两个符号)。

从源码可以确认这个目录布局的要求:sentence_prediction.py 的setup_task会分别从data/input0/dict.txtdata/label/dict.txt加载输入词典与标签词典,并要求--num-classes > 0load_dataset则依次加载input0(必需)、input1(可选,用于句子对)和label三个数据集(L132-L256)。因此IMDB-bin/下必须有input0/label/两个子目录,且label的标签值必须是可被其词典表示的 token。

六、阶段 5:运行训练

这是整条流水线的核心命令,文档完整给出如下(含全部环境变量):

TOTAL_NUM_UPDATES=7812 # 10 epochs through IMDB for bsz 32 WARMUP_UPDATES=469 # 6 percent of the number of updates LR=1e-05 # Peak LR for polynomial LR scheduler. HEAD_NAME=imdb_head # Custom name for the classification head. NUM_CLASSES=2 # Number of classes for the classification task. MAX_SENTENCES=8 # Batch size. ROBERTA_PATH=/path/to/roberta.large/model.pt CUDA_VISIBLE_DEVICES=0 fairseq-train IMDB-bin/ \ --restore-file $ROBERTA_PATH \ --max-positions 512 \ --batch-size $MAX_SENTENCES \ --max-tokens 4400 \ --task sentence_prediction \ --reset-optimizer --reset-dataloader --reset-meters \ --required-batch-size-multiple 1 \ --init-token 0 --separator-token 2 \ --arch roberta_large \ --criterion sentence_prediction \ --classification-head-name $HEAD_NAME \ --num-classes $NUM_CLASSES \ --dropout 0.1 --attention-dropout 0.1 \ --weight-decay 0.1 --optimizer adam --adam-betas "(0.9, 0.98)" --adam-eps 1e-06 \ --clip-norm 0.0 \ --lr-scheduler polynomial_decay --lr $LR --total-num-update $TOTAL_NUM_UPDATES --warmup-updates $WARMUP_UPDATES \ --fp16 --fp16-init-scale 4 --threshold-loss-scale 1 --fp16-scale-window 128 \ --max-epoch 10 \ --best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \ --shorten-method "truncate" \ --find-unused-parameters \ --update-freq 4

关键参数逐项解读

调度与批量

参数取值作用
TOTAL_NUM_UPDATES7812多项式衰减的总优化步数,等于“10 个 epoch × IMDB 训练集 25000 条 ÷ 有效 batch 32”取整
WARMUP_UPDATES469warmup 步数,约为总步数的 6%
--lr1e-05多项式调度的峰值学习率
--batch-size/--update-freq8 / 4有效 batch 为 8×4 = 32 句
--max-tokens4400单步最大 token 数,防止长句把显存打爆
--max-epoch10最多训练 10 个 epoch

任务与模型

参数取值作用
--tasksentence_prediction使用上文分析的分类任务,数据目录为第一个位置参数IMDB-bin/
--restore-fileRoBERTa 预训练权重加载 encoder 权重,重置优化器/数据加载器/计步器(--reset-optimizer --reset-dataloader --reset-meters)以支持从头开始微调
--archroberta_large355M 参数的大模型架构
--num-classes2分类类别数(IMDB 二分类);SentencePredictionTask.setup_task强制要求num_classes > 0
--classification-head-nameimdb_head分类头注册名,推理时roberta.predict('imdb_head', ...)用它寻址
--init-token/--separator-token0 / 2句首加<s>(id 0),句子对之间以</s>(id 2)分隔;IMDB 是单句任务,separator 实际不参与
--max-positions512序列上限;配合--shorten-method truncate对超长样本做截断

优化与数值

  • --criterion sentence_prediction:与 task 配套的分类损失;
  • --dropout 0.1 --attention-dropout 0.1--weight-decay 0.1;Adam 参数为(0.9, 0.98)、eps1e-06(沿用 RoBERTa 官方配置风格);
  • --clip-norm 0.0:不做梯度裁剪;
  • --fp16系列:混合精度训练及损失缩放窗口参数(--fp16-init-scale 4 --threshold-loss-scale 1 --fp16-scale-window 128);
  • --find-unused-parameters:分类头之外的部分参数在某些 step 可能不参与梯度,DistributedDataParallel 需要该标志避免报错;
  • --best-checkpoint-metric accuracy --maximize-best-checkpoint-metric:以验证集 accuracy 为指标保留最优 checkpoint。

显存不足时的调整方法

文档给出的建议是:调小--batch-size、同时调大--update-freq以保持有效 batch size 不变(例如--batch-size 4 --update-freq 8)。

预期结果

文档报告:RoBERTa-large 在有效 batch 32 下训练 10 个 epoch 后,best-validation-accuracy96.5%(IMDB dev 集)。这是文档原文明确给出的参考值,可作为你环境是否配置正确的对照基线。

训练时分类头是如何被创建并训练的

从源码链路看:fairseq-train启动时,sentence_prediction.py 的 build_model 在构建完 RoBERTa encoder 后,调用model.register_classification_head(head_name, num_classes=...)注册一个随机初始化的RobertaClassificationHead;该头以nn.ModuleDict形式挂载在模型上(model.py L51),注册逻辑见 register_classification_head。训练时每个 batch 的目标是 OffsetTokensDataset 提供的标签 id(label - nspecial,即0/1映射回0/1),损失由sentence_predictioncriterion 在分类头输出上计算——encoder 主体与分类头一起被端到端微调。

七、阶段 6:用 hub 接口加载 checkpoint 并推理

训练结束后,checkpoint 位于checkpoints/目录。此时可以用 RoBERTa 的 hub 接口把“task + 模型 + 分词器”打包加载,接口实现见 hub_interface.py:

from fairseq.models.roberta import RobertaModel roberta = RobertaModel.from_pretrained( 'checkpoints', checkpoint_file='checkpoint_best.pt', data_name_or_path='IMDB-bin' ) roberta.eval() # disable dropout
  • checkpoint_file='checkpoint_best.pt':即第五步按 accuracy 指标挑选出的最优 checkpoint;
  • data_name_or_path='IMDB-bin':指向预处理好的数据目录,接口用它重建sentence_predictiontask(从而拿到source_dictionarylabel_dictionary和 BPE 编码器)。

然后用--classification-head-name设置的名字imdb_head做预测,并把预测 id 通过 label 词典翻译回字符串(hub_interface.py 的 predict 会对特征提取结果过分类头并返回log_softmax值,argmax即预测类别):

label_fn = lambda label: roberta.task.label_dictionary.string( [label + roberta.task.label_dictionary.nspecial] ) tokens = roberta.encode('Best movie this year') pred = label_fn(roberta.predict('imdb_head', tokens).argmax().item()) assert pred == '1' # positive tokens = roberta.encode('Worst movie ever') pred = label_fn(roberta.predict('imdb_head', tokens).argmax().item()) assert pred == '0' # negative

两处容易踩坑的细节:

  • roberta.encode(...)会自动补<s> ... </s>并在 BPE 前加空格处理(encode 实现),与预处理阶段用dict.txt编码的 token 空间一致,无需手动分词;
  • label_fn中的+ nspecial是逆操作:训练时目标做了id - nspecial(见 OffsetTokensDataset),推理翻译回字符串时就要把特殊符号的偏移加回来,label_dictionary.string([label + nspecial])才能得到'0'/'1'这样的字符串。

八、复用到你自己的分类数据集:改造清单

把这条流水线迁移到其他分类任务(多分类、短文本/长文本均可)时,需要改动的点非常集中:

  1. 阶段 2 脚本:按你的数据源改读取逻辑,保证输出train.input0/train.label/dev.input0/dev.label且行对齐;标签可以是任意整数集合(0..K-1)。
  2. 阶段 4 第二次预处理label词典仍自动生成,无需干预;但如果你的任务需要句子对(如 NLI),则需准备input0+input1两份文本,任务会按--separator-token拼接(ConcatSentencesDataset 逻辑)。
  3. 阶段 5 命令:只改NUM_CLASSESTOTAL_NUM_UPDATES(按你的训练集大小与有效 batch 重算)、HEAD_NAMEROBERTA_PATH;其余超参可作为起点。
  4. 阶段 6 推理predict的第一个参数换成你的HEAD_NAME

九、涉及的关键文件索引

内容路径
本文对应的原始示例文档kosmos-2/fairseq/examples/roberta/README.custom_classification.md
多进程 BPE 编码工具kosmos-2/fairseq/examples/roberta/multiprocessing_bpe_encoder.py
sentence_prediction 任务实现kosmos-2/fairseq/fairseq/tasks/sentence_prediction.py
RoBERTa 模型与分类头注册kosmos-2/fairseq/fairseq/models/roberta/model.py
hub 加载/编码/预测接口kosmos-2/fairseq/fairseq/models/roberta/hub_interface.py
RoBERTa 主 README(模型下载与用法总览)kosmos-2/fairseq/examples/roberta/README.md

适用前提与限制:以上流程基于仓库中 kosmos-2 目录下捆绑的这一版 fairseq(hydra 化之前的fairseq-train命令行风格),要求能拿到roberta.largemodel.pt与 GPT-2 BPE 词表文件;--restore-file指向的路径需替换为你本地的实际 checkpoint 位置。IMDB 的 96.5% 验证准确率是文档给出的参考基线,复现时应以同配置对照。

【免费下载链接】unilmLarge-scale Self-supervised Pre-training Across Tasks, Languages, and Modalities项目地址: https://gitcode.com/GitHub_Trending/un/unilm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询