torchtune 数据集完全实战:把你的数据从零接入 4 类微调任务
【免费下载链接】torchtunePyTorch native post-training library项目地址: https://gitcode.com/GitHub_Trending/to/torchtune
你手里有一份自己的 CSV:两列文本,一列问题一列答案,想拿它微调模型,却不知道该改成什么格式、接到哪个配置里。这正是torchtune(PyTorch 原生的后训练库)的数据集体系要解决的问题:不管来源是 Hugging Face Hub、本地文件还是远程 URL,你的数据进模型前都要过同一条四步流水线,而入口 builder 就是这套流水线的预填组合。这篇按“先选型 → 走一遍 → 拆原理”的顺序讲清楚整个 torchtune 数据集与微调数据格式。
30 秒定位 torchtune 数据集入口:训练目标决定 builder
先别碰配置,先用数据形态选入口——选对 builder,后面全是填空。
| 你的训练目标 | 数据形态 | 入口 builder |
|---|---|---|
| 单轮指令 SFT | 两列:prompt + 回复 | instruct_dataset |
| 多轮对话 SFT | 一列:消息列表(sharegpt / openai 风格) | chat_dataset |
| DPO 偏好对齐 | chosen / rejected 两列(prompt 完全一致) | preference_dataset |
| 持续预训练 | 一列无标注文本 | text_completion_dataset |
| 视觉语言模型 SFT | 图文两列(sharegpt 风格 + 图像路径) | multimodal_chat_dataset |
| 以上都覆盖不了 | 任意 schema | 自定义 message transform + SFTDataset |
这些 builder 本质上是同一套模板的不同预填组合:SFTDataset(source + message_transform + model_transform)。区别只在 message_transform 一层,看懂这一层,你就能自己拼出任意新组合。
torchtune.datasets还导出一批面向公开数据集的内置 builder(torchtune/datasets/ 的__init__.py有完整清单):alpaca_dataset、alpaca_cleaned_dataset、grammar_dataset、samsum_dataset、slimorca_dataset、hh_rlhf_helpful_dataset、stack_exchange_paired_dataset、cnn_dailymail_articles_dataset、wikitext_dataset,以及多模态的the_cauldron_dataset、llava_instruct_dataset;PackedDataset和ConcatDataset用于样本打包与多数据集拼接。
torchtune SFT 数据接入:两列 instruct 数据从零走一遍
最高频场景就是两列 CSV,从本地文件一路接到lora_finetune_single_devicerecipe(recipe 是 torchtune 里"训练脚本 + YAML 配置"的完整组合)。下面按顺序演示。
1. 准备文件:只要两列,列名随意
CSV 每行一个样本,一列是用户 prompt,一列是模型回复:
incorrect,correct This are a cat,This is a cat.JSON 同理,列名随便起——不叫input/output也没关系,下一步对齐即可。
2. 用 column_map 一映射就能改列名
先用 Python 直接调 builder,方便调试:
from torchtune.datasets import instruct_dataset from torchtune.models.gemma import gemma_tokenizer g_tokenizer = gemma_tokenizer( path="/tmp/gemma-7b/tokenizer.model", prompt_template="torchtune.data.GrammarErrorCorrectionTemplate", max_seq_len=8192, ) ds = instruct_dataset( tokenizer=g_tokenizer, source="csv", data_files="data/my_data.csv", split="train", column_map={"input": "incorrect", "output": "correct"}, )大白话:column_map的键固定是 builder 期望的input/output,值才是你文件里的真实列名——方向别写反,反了就是 KeyError。prompt_template决定每条样本套什么任务前缀,这里用语法纠错模板。
3. 用 train_on_input 决定 loss 算到哪
train_on_input默认False:用户 prompt 的 token 全部屏蔽,只在 assistant 回复上训练,这是绝大多数 SFT 想要的行为。如果你的任务是"接着 prompt 继续写"(比如代码补全风格),把它设成True,prompt 也计入 loss。另外new_system_prompt可以给每条样本前置一条系统消息,起指令引导作用。
4. torchtune 数据集配置文件怎么写
上面的 Python 调用在 YAML 里长这样(参考 recipes/configs/qwen3/0.6B_lora_single_device.yaml 的完整结构,这里只保留必要项):
tokenizer: _component_: torchtune.models.gemma.gemma_tokenizer path: /tmp/gemma-7b/tokenizer.model prompt_template: torchtune.data.GrammarErrorCorrectionTemplate max_seq_len: 8192 dataset: _component_: torchtune.datasets.instruct_dataset source: csv data_files: data/my_data.csv split: train column_map: input: incorrect output: correct train_on_input: False packed: False seed: null shuffle: True epochs: 1 batch_size: 2注意 dataset 里不传 tokenizer——recipe 会把顶层 tokenizer 注进来。然后一条命令启动:
tune run lora_finetune_single_device --config my_qwen3_lora这里为什么配 LoRA:全参微调要更新全部权重,LoRA 用两个低秩矩阵近似这次更新、只训这两个小矩阵,显存占用低得多,所以单卡 SFT 的默认选择是 LoRA。
5. 打印一次样本,确认 loss 范围没配错
接入后最值得做的验证:
sample = ds[0] print(g_tokenizer.decode(sample["tokens"])) print(sample["labels"])第一行还原成文本,确认模板拼得对不对;第二行里-100出现的位置就是"不算 loss"的位置。以train_on_input=False为例,-100应该恰好覆盖系统消息和用户 prompt,assistant 回复部分则是真实 token id。这一步 10 秒钟,却能抓住八成数据接错的 bug。
拆开数据工厂:一条样本在 torchtune 里走哪四步
整条流水线就是"取数 → 说人话 → 分词 → 打包"四个工位,前三个工位由SFTDataset的三个构造参数定义(torchtune/datasets/_sft.py):
def __init__( self, *, source: str, # 工位 1:HF 仓库名,或 "json"/"csv"/"text" + data_files message_transform: Transform, # 工位 2:原始样本 -> Message 列表 model_transform: Transform, # 工位 3:Message -> tokens + mask filter_fn: Optional[Callable] = None, **load_dataset_kwargs: dict[str, Any], # 透传给 HF load_dataset ) -> None: ...工位 1 的加载完全委托给 Hugging Face 的load_dataset:source传仓库名就从 Hub 拉,传文件类型 +data_files就读本文件或 https 远程文件;**load_dataset_kwargs(split、name等)原样透传,所以 HF 支持的加载方式 torchtune 天然支持。
工位 2 的产物是Message列表。Message是 torchtune 的统一消息结构(torchtune/data/_messages.py),角色分四类:system(系统提示)、user(输入 prompt)、assistant(模型回复,即你要训练的部分)、ipython(工具调用返回)。选它当中间表示的原因很直白:数据格式和模型分词器从此解耦——换数据集不用动模型代码,换模型不用动数据代码,所有模型 tokenizer 只需要认识Message这一种输入。每条Message上的masked属性在这里就决定了哪些内容进 loss。
工位 3 是 tokenizer(文本任务里它本身就是model_transform):套 prompt 模板、编码成 token,同时产出mask标记哪些位置不计 loss。多模态任务则换成模型的专属 transform,顺带做图像预处理。
工位 4 是 collater——把处理好的样本 padding 到等长再堆成 batch 的打包函数,在 torchtune/data/_collate.py 里按任务类型分派:padded_collate(通用)、padded_collate_dpo(偏好对)、padded_collate_packed(打包样本)、padded_collate_tiled_images_and_mask(多模态 tile 图像)。recipe 会按数据集类型自动选对。
疑问一:labels 里的 -100 到底哪来的
它不是数据本身带的,而是mask=True的位置被替换成了 -100(交叉熵 loss 的忽略索引CROSS_ENTROPY_IGNORE_IDX)。逐样本执行这段逻辑的是SFTTransform:
# 伪代码,完整实现在 torchtune/datasets/_sft.py labels = [ -100 if m else t for m, t in zip(mask[1:], tokens[1:]) ] labels.append(-100)右移一位的原因:模型在第 i 个位置预测的是第 i+1 个 token,所以 labels 必须相对 logits 错开一位才能对齐预测目标;末尾补的-100是占位符,因为最后一个位置的"下一个 token"在序列里已经不存在。
疑问二:model_transform 的输入输出约定
契约只有一条:输出字典必须至少包含"tokens"和"mask"两个键,缺了直接抛ValueError(见SFTTransform.__call__)。文本任务把模型 tokenizer 直接当model_transform传即可;多模态任务必须传模型的专属 transform(比如视觉编码器需要的图像 tiling 逻辑就在那里),这是它和纯文本路径唯一的区别。
torchtune 微调数据格式速览:chat / DPO / 持续预训练 / 多模态
torchtune 多轮对话数据格式:一列消息,两种标准风格
chat 数据 = 一列存消息列表,风格二选一:sharegpt或openai。
{"conversations": [ {"from": "human", "value": "What is the answer to life?"}, {"from": "gpt", "value": "The answer is 42."} ]}ds = chat_dataset( tokenizer=g_tokenizer, source="json", data_files="data/my_data.json", conversation_column="conversations", conversation_style="sharegpt", split="train", )最容易配错的两个参数:
conversation_style:sharegpt期望{"from": "human"|"gpt", "value": ...}(对应 torchtune/data/_messages.py 里的ShareGPTToMessages),openai期望{"role": "user"|"assistant", "content": ...}(对应OpenAIToMessages);conversation_column:指向存对话的那一列,列名不叫conversations时必须显式指定。
风格写错会直接报Unsupported conversation style,两种都不匹配你的 schema 时,就得写自定义 message transform(见下一节)。
torchtune DPO 数据格式:chosen 与 rejected 必须同 prompt
DPO 样本 = 同一个 prompt 下的两条回复,一列chosen(偏好)、一列rejected(非偏好),且 prompt 部分必须逐字一致,否则对比失去意义:
{ "chosen": [ {"role": "user", "content": "What do I do when I have a hole in my trousers?"}, {"role": "assistant", "content": "Fix the hole."} ], "rejected": [ {"role": "user", "content": "What do I do when I have a hole in my trousers?"}, {"role": "assistant", "content": "Take them off."} ] }column_map = {"chosen": "chosen_col", "rejected": "rejected_col"} ds = preference_dataset( tokenizer=m_tokenizer, source="json", data_files="my_preference_dataset.json", column_map=column_map, train_on_input=False, split="train", )输出是四组张量:chosen_input_ids/chosen_labels/rejected_input_ids/rejected_labels,prompt 部分全部屏蔽为-100——这就是 DPO 对比学习的输入形态。注意两个坑:DPO 目前不支持 packing,设packed=True会直接报错;对话必须 openai 风格,自定义格式要自己仿ChosenRejectedToMessages写 transform。
torchtune 持续预训练数据格式:只要一列文本
text completion 数据没有结构要求:一个文本列就是全部,默认列名text:
ds = text_completion_dataset( tokenizer=m_tokenizer, source="json", data_files="odyssey.json", column="input", split="train", )本地纯文本文件把source换成"text"、data_files指向.txt即可。容易配错的参数:column指定文本所在列(本地 txt 用默认text即可);add_eos默认True给每条样本追加 EOS;split_across_pack只在packed=True时起作用,控制装不进当前包的超长样本是拆到下一包还是整体后移。
torchtune 多模态数据格式:图文两列 + 占位符
多模态数据 = sharegpt 风格对话列 + 图像路径列,文本里用占位符标出图片位置:
{ "dialogue": [ {"from": "human", "value": "<image>What time is it on the clock?"}, {"from": "gpt", "value": "It is 10:00 AM."} ], "image_path": "images/clock.jpg" }model_transform = llama3_2_vision_transform( path="/tmp/Meta-Llama-3-8B-Instruct/original/tokenizer.model", prompt_template="torchtune.data.QuestionAnswerTemplate", max_seq_len=8192, image_size=560, ) ds = multimodal_chat_dataset( model_transform=model_transform, source="json", data_files="data/my_data.json", column_map={"conversations": "dialogue", "image": "image_path"}, image_dir="/home/user/dataset/", image_tag="<image>", split="train", )最容易配错的两处:
model_transform:这里传的不是纯文本 tokenizer,而是模型专属 transform(包含图像预处理与 tiling 逻辑);image_dir+image_tag:相对路径会拼上image_dir(/home/user/dataset/images/clock.jpg),image_tag指定文本里的占位符。图像暂只支持单个路径列、挂在第一条 user 消息上;packing 暂不支持。
YAML 配置参考 docs/source/basics/multimodal_datasets.rst 的写法,_component_指向torchtune.datasets.multimodal.multimodal_chat_dataset,顶层 tokenizer 用llama3_2_vision_transform。
内置 builder 都覆盖不了时:自定义 message transform 兜底
自定义数据集 = 一个 message transform + 一个 builder 函数,官方推荐做法见 docs/source/basics/custom_datasets.rst:
class MyMessageTransform(Transform): def __call__(self, sample): return { "messages": [ Message(role="user", content=sample["input"], masked=True, eot=True), Message(role="assistant", content=sample["output"], masked=False, eot=True), ] }def custom_dataset(tokenizer, **load_dataset_kwargs) -> SFTDataset: return SFTDataset( source="json", data_files="data/my_data.json", split="train", message_transform=MyMessageTransform(), model_transform=tokenizer, **load_dataset_kwargs, )dataset: _component_: data.dataset.custom_datasetmasked=True/False在这里直接决定哪些 token 计入 loss;eot=True表示该消息后追加结束符。YAML 里_component_指向你 builder 函数所在的模块路径即可。
动手前的 8 项自查清单
- builder 和数据形态匹配:两列用
instruct_dataset,对话列用chat_dataset,偏好对用preference_dataset,纯文本用text_completion_dataset column_map的键是 builder 期望的列名(input/output、conversations/image、chosen/rejected),值才是自己文件里的真实列名- loss 范围想清楚了:
train_on_input(builder 参数)或masked(自定义 transform),先定谁算 loss 再开训 packed=True时 tokenizer 配了max_seq_len;DPO 与多模态别碰 packingsource是 HF 仓库名,或是"json"/"csv"/"text"且配了data_files- YAML 里
_component_指向的模块路径真实可导入(自定义 builder 尤其要确认) train前对第一条样本做过冒烟测试:decode(tokens)文本符合预期、labels里-100只出现在该 mask 的位置- 对话类数据确认了
conversation_style与列名一致,图像类确认了image_dir拼接后路径存在
想继续深入,建议按顺序读:docs/source/basics/datasets_overview.rst(流水线总览)→ docs/source/basics/message_transforms.rst(Message 与 transform 细节)→ torchtune/datasets/(源码)。
【免费下载链接】torchtunePyTorch native post-training library项目地址: https://gitcode.com/GitHub_Trending/to/torchtune
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考