Qwen3-SmVL

GitHub
565 54 较难 1 次阅读 5天前语言模型图像其他
AI 解读 由 AI 自动生成,仅供参考

Qwen3-SmVL 是一个超轻量级的中文多模态大模型,旨在让小型语言模型具备“看图说话”的能力。它巧妙地将 SmolVLM2 高效的视觉编码模块与通义千问 Qwen3-0.6B 强大的中文语言理解能力进行了“拼接微调”。这一方案主要解决了当前端侧小模型(如 SmolVLM2)虽能低显存运行却不懂中文的痛点,让用户能在仅占用约 1GB 显存的设备上,使用流畅的中文进行图文交互。

该工具特别适合资源受限场景下的开发者、研究人员以及希望在本地部署多模态应用的技术爱好者。其核心技术亮点在于创新的“模型拼接”思路:保留了成熟的视觉特征提取与映射架构,仅替换并微调了语言模型部分及特征对齐层。通过复用现有高质量组件,Qwen3-SmVL 以极低的训练成本实现了视觉与中文语义的高效融合,为构建低成本、高性能的端侧多模态应用提供了全新的技术路径和可复现的代码范例。

使用场景

一位嵌入式开发工程师正试图在显存仅有 1GB 的国产边缘设备上,部署一个能理解中文说明书图片的智能助手。

没有 Qwen3-SmVL 时

  • 语言障碍严重:现有的超小多模态模型(如 SmolVLM2)虽能端侧运行,但完全无法理解中文,面对中文设备说明书束手无策。
  • 模型体积过大:主流的中文多模态模型参数量巨大,远超 1GB 显存限制,无法在资源受限的边缘设备上落地。
  • 开发成本高昂:若要同时满足“中文能力”与“极小体积”,开发者需从零训练或进行复杂的全量微调,对算力和时间要求极高。
  • 架构复用困难:难以直接利用现有的优秀中文小语言模型(如 Qwen3-0.6B)快速赋予其视觉能力,缺乏灵活的拼接方案。

使用 Qwen3-SmVL 后

  • 原生中文支持:通过拼接微调,Qwen3-SmVL 完美继承了 Qwen3-0.6B 的中文理解能力,能准确解读中文图文信息。
  • 极致轻量部署:模型整体参数量极小,成功将多模态推理门槛降至 1GB 显存,轻松运行于沐曦等国产边缘芯片上。
  • 高效定制路径:利用“拼接微调”思路,仅需调整特征映射层即可将视觉模块与中文 LLM 结合,大幅降低训练算力需求。
  • 灵活架构扩展:验证了将 SmolVLM2 视觉头与任意小型中文 LLM 拼接的可行性,为后续更多端侧应用提供了标准化模板。

Qwen3-SmVL 通过巧妙的模型拼接技术,打破了中文能力与端侧轻量化不可兼得的僵局,让超低资源设备也能拥有聪明的“中国眼”。

运行环境要求

操作系统
  • 未说明
GPU

必需,需 40GB 及以上显存(文中明确提及需要 40G 及以上的 GPU 显存才能运行训练代码),基于沐曦或 NVIDIA 架构测试

内存

未说明

依赖
notes本教程涉及 VLM 微调训练,对算力要求极高,必须拥有 40GB 及以上显存的 GPU 方可运行训练代码。模型拼接涉及修改 Chat Template (Jinja)、替换文本模型骨干及重新初始化特征映射层。数据集 'the_cauldron' 完整下载约 169GB,部分子集可能存在加载异常需手动处理。训练时采用冻结视觉和文本模型参数,仅微调特征映射器和语言模型头的策略。
python未说明
transformers
torch
SwanLab
Qwen3-SmVL hero image

快速开始

Qwen3-"VL"——超小中文多模态模型的“拼接微调”之路1(附代码和SwanLab记录)

💚 特别感谢
感谢 @zhihuazhao-bit 帮助审阅和修复了代码中众多的小 bug,并在 NV 上完成了测试!

感谢 @lovelyyoshino帮助填补了数据集中的部分错误,并且提升了模型效果!

摘要

最近Huggingface团队发布了超小多模态模型SmolVLM2,可以做到端侧1GB显存推理。在怀着惊喜试用后发现,虽然模型有极其强大的视觉文本理解能力,但是模型却无法理解中文。这对一个“四六级压线过”的笔者来说十分不友好。刚好前段时间做SwanLab硬件检测适配时有一台未到期的沐曦曦云C500服务器,因此萌生了使用沐曦GPU芯片微调、把当前中文小模型扛把子Qwen3与SmolVLM2直接微调拼接的想法。

本教程将介绍一种模型拼接的思路,将SmolVLM2的视觉模块(0.09B)与Qwen3最小的模型(0.6B)进行对齐微调,最终使得Qwen模型具备一定的视觉理解能力。由于笔者时间有限且考虑到文章篇幅的原因,因此该系列预计将以系列的方式放出。篇幅规划如下:

  • 第一篇:如何构建和微调一个拼接模型(本篇博客
  • 第二篇:模型测评、数据集优化、回答人类对齐
  • 第三篇:微调技巧介绍、视觉位置编码改动与模型结构优化
PPAP
I have a Qwen, I have a SmolVLM...
⚠️关于算力的注意:本教程涉及VLM微调训练,对算力要求较高,需要40G及以上的GPU显存才能运行本教程的训练代码。

目录

SmolVLM2的背景知识

首先,我们先回顾一下SmolVLM2模型的构建方案,SmolVLM2模型的整体包括三大块:视觉模型层,特征映射层和大语言模型层,见下图:

smolvlm2
SmolVLM2的架构图

这个设计是现在比较常见的VLM方案。核心设计思想就是让视觉模型的输出特征与经过embedding的文本特征直接拼接后输入到语言模型(LLM)当中,没有交叉注意力等模块。相比于早期LLaVA等架构,这种最大的优点就是可以最大程度复用已有的语言模型。以Qwen2.5-VL为例,其3B、7B、72B模型大小指的只是LLM部分,并没有包含Vision模块,实际上3B模型的参数量接近4B,视觉模块大概0.4B左右,三个不同大小的VLM使用的是统一的视觉模型。对于一些较大的VLM来说,构建视觉模型时绝大多数的训练都集中在特征映射模块和视觉模块,只在最后阶段为了最终效果进行整体微调时才会调整语言模块。保证了VLM的语言能力。

下面简述一下各个模块的细节:

  • 视觉模型层:SmolVLM2-256M版本用的是Google的SigLip模型,一个基于ViT的视觉模型,选用的是最小的SigLip-93M的版本,HF论文里没具体写是直接用的SigLip的参数还是他们从零构建的(有注意到的读者可以评论留言下)。在SmolVLM2代码中对应的是SmolVLMVisionTransformer

  • 特征映射层:就是一个简单的MLP,不过SmolVLM2中为了降低图像分辨率还做了一个Pixel shuffle来降低图像分辨率,进一步减少视觉的Token占用,减少了文本长度。HF团队在论文里提到对于参数量较小的VLM来说使用Pixel shuffle还能提升性能。但可训练参数其实就是一个单层的神经网络,这个模块的核心作用就是做特征对齐,将视觉特征从768维(SigLip的维度)映射到576维(SmolLLM2的维度)

  • 大语言模型:SmolVLM2-256M模型使用的文本模型是SmolLM-135M版本。可能是由于模型较小,HF团队在论文中说到训练时仅采用两阶段训练:大规模图文训练+针对视频任务的专门微调。为了保障模型的文本能力HF团队在训练数据中参杂了大概14%的纯文本微调数据。不过考虑到视觉模块本身参数量(93M)大小接近于文本模型(135M),因此笔者推测相比于冻结文本模型,数据平衡在这之中会起到更关键的作用。

HF团队在原文中还提到了许多影像小模型VLM性能的trick,感兴趣的读者可以进一步参考SmolVLM2的论文

模型拼接和微调思路简介

正所谓顶级食材(模型)只需要最简单的烹饪。模型拼接的思路非常简单直接,基本就三步:

  1. 调整SmolVLM2的“上下文控制格式”,使得其与Qwen3兼容。

  2. 将模型的文本部分直接从SmolLM2换成Qwen3-0.6B,包括其文本tokenizer和词嵌入、文本模型、以及模型最后输出的语言模型头(LM Head)。

  3. 需要重新初始化特征映射层的MLP,从768->576的单层神经网络改成768->1024的单层神经网络即可。

整体架构和对图文对前后处理依旧保持SmolVLM2的流程不变,具体改动见下图:

concatation
将Qwen3-0.6B替换SmolVLM2的语言模型部分

笔者接下来详细介绍下为了实现“拼接”,具体改动的地方,供之后有类似的任务的读者参考。

模型拼接实现和关键代码讲解

第一处改动:SmolVLM2的Tokenizers部分

首先需要改动的就是需要改动的是SmolVLM2的Tokenizers部分,这里面主要是涉及两个问题:

  • 第一个问题是要将SmolVLM2用于指示图像位置的特殊令牌(Special Token)加入到Qwen3的Tokenizer当中,这么做的目的是防止SmolVLM2的图像Token<image>被切分为<image>三块。幸运的是,Qwen3本身在Tokenizers中预留了未来用于多模态的特殊特殊令牌`

第二处改动:替换SmolVLM2的SmolLM2模型为Qwen3-0.6B

替换模型这块没什么复杂的,主要是需要处理Transformers比较复杂的嵌套逻辑。Tranformers通常建议模型将预训练模型backbone和下游任务分开来。改动逻辑图如下:

change_model
替换smolvlm2的文本模块和语言模型头

以Qwen3为例,预训练Backbone模型为Qwen3Model,仅仅包含embedding层、各个Decoder层,最后输出的是所有输入token的hidden state。负责下游任务的Qwen3提供了包括:用于因果语言序列生成的Qwen3ForCausalLM,也就是大家常用的语言生成。负责句子分类Qwen3ForSequenceClassification,使用最后一个生成的token输入到一个单层MLP做序列级分类,做句子情绪分类等可以用这个下游模型;Qwen3ForTokenClassification用于做Token级分类,比如语言实体抽取任务可以使用这个下游模型。Qwen3ForQuestionAnswering则是专门做抽取式问答任务的模型,核心思想是输入(问题,参考文本)让模型从参考文本中找到与问题最相关的一段,这类任务由于RAG系统的出现没那么流行了,未来笔者专门出一个系列的教程阐述除了因果语言序列生成以外的任务则怎么微调。

关键代码如下

from transformers import (
    AutoProcessor,
    AutoModelForImageTextToText,
    AutoTokenizer,
    AutoModelForCausalLM
)

# 替换text模型和head
smolvlm2_02B_model = AutoModelForImageTextToText.from_pretrained(
    "model/SmolVLM2-256M-Video-Instruct",
    torch_dtype=torch.bfloat16,
    _attn_implementation="eager",
).to(device)

qwen3_06b_model = AutoModelForCausalLM.from_pretrained(
    "model/Qwen3-0.6B", torch_dtype=torch.bfloat16
).to(device)

smolvlm2_02B_model.model.text_model = qwen3_06b_model.model
smolvlm2_02B_model.lm_head = qwen3_06b_model.lm_head
...

接下来比较复杂的是替换所有的关键变量,比如模型内用于在文本序列中为图像特征预留的占位符image_token_id,用于指示停止生成的eos_token_id,和计算loss值会用到的vocab_size,Qwen的词表大小为151936,远远大过SmolVLM2的词表49280。具体代码如下:

...
# 替换词表大小
smolvlm2_02B_model.vocab_size = qwen3_06b_model.vocab_size
smolvlm2_02B_model.model.vocab_size = qwen3_06b_model.vocab_size
smolvlm2_02B_model.config.vocab_size = qwen3_06b_model.vocab_size
smolvlm2_02B_model.config.text_config.vocab_size = qwen3_06b_model.vocab_size
smolvlm2_02B_model.model.config.vocab_siz = qwen3_06b_model.vocab_size
smolvlm2_02B_model.model.config.text_config.vocab_size = qwen3_06b_model.vocab_size
# 替换图像token
smolvlm2_02B_model.image_token_id = 151655
smolvlm2_02B_model.model.image_token_id = 151655
smolvlm2_02B_model.config.image_token_id = 151655
smolvlm2_02B_model.model.config.image_token_id = 151655
# 替换模型生成停止符
smolvlm2_02B_model.generation_config.eos_token_id = 151645
···

上面的代码可以看到在替换各个变量时需要将嵌套模型的变量一起替换掉,笔者之前训练时就因为仅仅替换了SmolVLMForConditionalGeneration而忘记替换SmolVLMModel中的image_token_id,导致语言模型接收不到图像特征,最后表现出来就是loss下降的极快且低,grad_norm看起来也学到位了,一推理效果特别差,附上错误训练的损失图:

fail_train
SwanLab记录训练结果展示:蓝色为错误训练的完整微调loss图,可以看到损失下降很快,然而实际推理会发现模型并没有图像理解能力。冻结语言模型头(红色)后发现grad_norm为零且loss不收敛,正确的应该是黄色

笔者最早没发现改动错误,先做完整微调(蓝色曲线)后发现损失下降很快达到了0.1以下,结果实际一推理发现模型完全没有图像理解能力,就补了一个冻结语言模型只微调视觉模型的实验(红色曲线),结果发现损失完全没下降,才定位到了视觉特征传入有问题。后续修复后正确的损失下降过程见黄色图像。

第三处改动:构建和替换特征映射层

这个相对较简单,只需要重新构建一个维度对齐的SmolVLMConnector即可。Qwen3的hidden_dim是1024,SigLip的hidden_dim是768,因此构建一个768➡️1024映射的SmolVLMConnector即可。代码如下:

···
# 构建配置并且创建连接器
@dataclass
class VisionConfig:
    hidden_size: int = 768

@dataclass
class TextConfig:
    hidden_size: int = 1024

@dataclass
class ConnectConfig:
    scale_factor: int = 4
    vision_config: VisionConfig = VisionConfig()
    text_config: TextConfig = TextConfig()

new_connector_config = ConnectConfig()

# 替换 SigLit 到 LLM 的 connector 层
new_connector = SmolVLMConnector(new_connector_config).to(device).to(torch.bfloat16)
smolvlm2_02B_model.model.connector = new_connector
···

微调数据集构建

笔者最初计划寻找中文多模态数据集,但发现相关的资料比较少。因此决定先用英文的多模态数据集凑合一下。之后再考虑通过数据合成的方式将部分数据翻译为中文。关于数据合成和配比的问题将在之后的博客讨论。

the_cauldron
the_cauldron数据集logo

这里为了方便本项目直接使用HuggingFace团队整合的多模态数据集the Cauldron数据集,Cauldron翻译成中文类似于煮东西的“釜”,不知道HF团队是不是玩“炼丹”的梗。这个数据集整合了50个视觉微调任务数据集的训练集,用于微调Huggingface发布的多模态模型Idefics2模型。这50多个数据集都被处理成了一致的格式(见下图),共有1,880,992条数据,完整下载约169G,非常方便使用。

data_show
数据集样本展示

不过可惜数据集的文本都是英文内容,且绝大多数数据集的回复非常短,只有一个词,这也给后面模型训练带来了麻烦。本篇博客暂时不讨论关于数据构建和配比的问题,后续有时间了专门做相关的实验。本博客先以为Qwen3模型带来视觉能力为核心目标。

数据集的下载链接如下,国内推荐用modelscope下载:

笔者在实际测试时发现"mimic_cgd","localized_narratives","okvqa","ocrvqa","clevr_math"这几个子数据集加载有点异常,建议使用此数据集训练的读者手动处理下,社区也有用户反馈这几个数据可以在原始来源处额外下载,未来笔者将会补全这几个数据集重新上传一次完整版的the Cauldron数据集。

微调方法与代码实现

冻结模型参数微调

整体微调方法采用了CLM模型通常的Teacher Forcing的学习方法,损失就是标准的交叉熵损失。考虑到此次本教程的目标是先确保模型具备中文多模态能力(优化模型性能等之后撰写其他博客),因此为了实验效率,在对齐微调阶段采用冻结视觉模型与文本模型,仅微调特征映射器和语言模型头的方法。

冻结模型参数的核心代码如下:

def freeze_model(qwen_smvl):
    for _, param in qwen_smvl.model.text_model.named_parameters():
        param.requires_grad = False
    for _, param in qwen_smvl.model.vision_model.named_parameters():
        param.requires_grad = False
    return qwen_smvl

冻结后训练参数、模型总参数、与占比如下:

trainable params: 12.00M || all params: 662.87M || trainable%: 1.81

文本长度、损失掩码与截断策略

文本长度

由于视觉特征需要占用大量的文本长度,笔者简单测试了下the_cauldron图像占0.8K到1.3K左右的token。而数据集中大多数文本token数在200-500左右,极少情况会有3-4K的情况。因此笔者统一采用2K的文本长度,超出部分截断处理。

这里有一个不同于文本微调的细节要注意,文本截断长度不能小于图像token,否则会导致模型在进行特征拼接时报错(当然图像特征如果被截断了,这条训练数据也就没意义了)。因此对于显存不足64G的同学如果需要适当缩短文本长度(不建议低于1.5K),最好连同图像分辨率也缩小些。在后面的博客我们会专门增加对减少图片token占用的研究。

同样由于文本长度受限,且图像特征没法截断,我们也没使用“packing dataset”的方法提升模型的训练效率。

考虑到部分数据集存在多张图片的情况,考虑到本次训练仅采用2k的文本长度(与之对比HF在训练SmolVLM-256M版本采用的是8K的文本长度,2.2B版使用了16K的文本长度)。针对单条数据中存在多张图片的情况仅仅选用第一张。

损失掩码

在采用Teacher Forcing的学习方法时,文本微调中损失掩码有两种策略:

  • 对包含“用户问题”和“模型回复”的完整文本进行微调优化
  • 仅对“模型回复”部分进行微调优化

这两种策略的对比如下图:

mask
两种微调掩码策略的差异,通常建议选择“仅微调模型回答部分”以增强泛化性

通常来说使用“仅微调模型回复部分”的策略模型更容易泛化(这点与HF在SmolVLM2的论文提到的trick)。然而笔者为了提高训练效率选择了完整文本微调。可以在后续博客中增加消融实验做进一步对比。

值得注意的是,在进行完整文本微调时,需要单独屏蔽Image Token以防止对图像占位token计算损失,影响模型表现。

关键代码如下:

def data_collate_fix2k(examples, processor, device, max_length=2048):
    batch_text = []
    batch_image = []
    for example in examples:
        images = example["images"][:1]  # 只允许一张图,不然显存压力太大
        batch_image.append(images)
        image_num = len(images)
        chat_texts = example["texts"][0]
        messages = [
            {
                "role": "user",
                "content": [{"type": "image"}] * image_num
                + [{"type": "text", "text": chat_texts["user"]}],
            },
            {
                "role": "assistant",
                "content": [{"type": "text", "text": chat_texts["assistant"]}],
            },
        ]
        text = processor.apply_chat_template(
            messages, enable_thinking=False, add_generation_prompt=False
        )

        batch_text.append(text)

    batch = processor(
        text=batch_text,
        images=batch_image,
        max_length=max_length,
        return_tensors="pt",
        padding="max_length",
        truncation=True,
    )
    labels = batch["input_ids"].clone()
    labels[labels == processor.tokenizer.pad_token_id] = -100
    labels[labels == processor.image_token_id] = -100
    batch["labels"] = labels
    return batch.to(device, dtype=torch.bfloat16)

微调超参数设置

学习率

由于仅仅针对特征映射层(connector)进行训练,且conntector由于要对齐Qwen3的维度因此参数为随机初始化(理论上可以采用一些独特的初始化策略提升性能,但考虑到模型较小因此笔者没关注初始化策略)。因此学习率设置为lora中较为流行的1e-4学习率策略。

为了保障有效收敛,学习率衰减基本是必备的trick,采用的是社区比较流行的cosine学习率衰减,衰减至0。warm up为整体步长的10%(在超过1000k step的情况下固定为50)。

batch size

Batch size通常来说越大越好,然而由于VLM模型的文本长度太大,因此采用每卡1 batch和4梯度累加(grad accelerate),在8卡训练中等效32 Batch size。

训练参数设置代码

training_args = TrainingArguments(
    seed=42,
    data_seed=42,
    max_steps=200,
    # num_train_epochs=1,  # 训练1个epoch 约1k steps
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    dataloader_pin_memory=False,
    warmup_ratio=0.1,
    learning_rate=1e-4,
    lr_scheduler_type="cosine",
    weight_decay=0.01,
    logging_steps=5,
    eval_strategy="steps",
    eval_steps=0.125,
    save_strategy="steps",
    save_steps=0.125,
    save_total_limit=8,
    optim="adamw_torch",
    bf16=True,
    output_dir=f"./model/freeze_except_connector_cocovqa",
    overwrite_output_dir=False,
    report_to="swanlab",
    run_name="freeze_except_connector_cocovqa",
    remove_unused_columns=False,
    gradient_checkpointing=False,
)

训练环境

微调代码基于沐曦的C500国产通用计算GPU实现,显存为64G。沐曦的AI芯片基本完全兼容pytorch和huggingface transformers场景,并且在做多模态训练时相比较其他国产AI芯片罕见的没有兼容性问题。读者在尝试本项目代码时可以采用Nvidia显存40G以上的显卡运行本教程。

笔者个人感觉沐曦的GPU整体适配效果还是非常好的,没遇到适配性的问题。体验上和用NV的GPU做训练没什么区别。笔者自己也用过好几款国产GPU,沐曦的体验肯定是名列前茅的,包括代码中有指定flash attention在沐曦GPU上都能成功迁移,这点非常值得给沐曦团队点个赞。希望国产GPU生态能越发展越好,造福广大炼丹师;)。

muxi-gpu
沐曦国产GPU,笔者用的云端服务器没见过真机,因此找了张网图

训练环境的话除了安装GPU对应的驱动和pytorch外,本教程需要额外安装Huggingface全家桶,如下:

torch   # 推荐版本>=6.0
torchvision
transformers>=4.53.0
accelerate
datasets
num2words   # SmolVLM2需要

额外补充一句,如果采用沐曦GPU训练的话,需要在沐曦官方文档处寻找沐曦版torch的安装方式进行下载。其他HF环境和NV基本一样。附赠一个沐曦查看GPU的命令:

mx-smi

效果如下:

=================== MetaX System Management Interface Log ===================
Timestamp                                         : Sat Jul 12 14:58:51 2025

Attached GPUs                                     : 8
+---------------------------------------------------------------------------------+
| MX-SMI 2.1.12                       Kernel Mode Driver Version: 2.12.13         |
| MACA Version: 2.29.0.19             BIOS Version: 1.22.3.0                      |
|------------------------------------+---------------------+----------------------+
| GPU         NAME                   | Bus-id              | GPU-Util             |
| Temp        Pwr:Usage/Cap          | Memory-Usage        |                      |
|====================================+=====================+======================|
| 0           MetaX C500             | 0000:0e:00.0        | 0%                   |
| 36C         69W / 350W             | 5680/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 1           MetaX C500             | 0000:0f:00.0        | 0%                   |
| 38C         70W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 2           MetaX C500             | 0000:10:00.0        | 0%                   |
| 37C         69W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 3           MetaX C500             | 0000:12:00.0        | 1%                   |
| 37C         71W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 4           MetaX C500             | 0000:35:00.0        | 0%                   |
| 37C         70W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 5           MetaX C500             | 0000:36:00.0        | 1%                   |
| 36C         68W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 6           MetaX C500             | 0000:37:00.0        | 0%                   |
| 39C         73W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+
| 7           MetaX C500             | 0000:38:00.0        | 0%                   |
| 38C         71W / 350W             | 4986/65536 MiB      |                      |
+------------------------------------+---------------------+----------------------+

+---------------------------------------------------------------------------------+
| Process:                                                                        |
|  GPU                    PID         Process Name                 GPU Memory     |
|                                                                  Usage(MiB)     |
|=================================================================================|
|  0                  3496691         python3.10                   4066           |
|  0                  3496692         python3.10                   102            |
|  0                  3496693         python3.10                   102            |
|  0                  3496694         python3.10                   102            |
|  0                  3496695         python3.10                   102            |
|  0                  3496696         python3.10                   102            |
|  0                  3496697         python3.10                   102            |
|  0                  3496698         python3.10                   170            |
|  1                  3496692         python3.10                   4154           |
|  2                  3496693         python3.10                   4154           |
|  3                  3496694         python3.10                   4154           |
|  4                  3496695         python3.10                   4154           |
|  5                  3496696         python3.10                   4154           |
|  6                  3496697         python3.10                   4154           |
|  7                  3496698         python3.10                   4154           |
+---------------------------------------------------------------------------------+

训练代码实现

在构建训练代码时,笔者使用HuggingFace Transfomers框架的Trainer类来完成训练代码。Trainer类实现的训练逻辑基本能完成大部分微调任务。这里唯一需要提到的是笔者使用了Qwen3-0.6B而非通常此类任务该使用的Qwen3-0.6B-Base模型,Qwen3-0.6B相比于Qwen3-0.6B-Base模型经过了指令遵从微调、对齐等,能实现聊天问答功能。

通常来说对经过微调的模型进行持续训练会一定程度带来性能损失,然而此次微调时笔者冻结了LLM参数,因此需要选用经过微调的模型来实现多模态问答能力。

笔者在训练过程中使用的是bfloat16精度,相比于float16来说bfloat16增加了尾数位数,训练过程中精度会更高些。

在前期进行方案验证阶段笔者采用的是cocoqa数据集,并且进行200steps的微调训练。在确定方案可行后笔者计划使用完整数据集进行微调训练,然而考虑到训练数据量仅仅只有整个模型的12M,因此笔者按参数量与训练Token的比值为1:10采样数据集,即总共从数据集中采样出60K条数据用于实际训练(文本长度按照2k计算,实际上有padding部分因此实际参与token数小于120M)。笔者认为参与训练的数量是足以令模型收敛的,后续实验也证明了模型确实能达到我们所期望的效果。

训练关键代码实现

代码比较长是因为增加了断点续训的能力

################
# 开启训练
################
last_checkpoint = None  # load last checkpoint if available
if (
    os.path.isdir(training_args.output_dir)
    and not training_args.overwrite_output_dir
):
    last_checkpoint = get_last_checkpoint(training_args.output_dir)
    if last_checkpoint is None and len(os.listdir(training_args.output_dir)) > 0:
        raise ValueError(
            f"Output directory ({training_args.output_dir}) already exists"
        )
    print(
        f"Checkpoint detected, resuming training at {last_checkpoint}."
    )
# Init Trainer
trainer = Trainer(
    model=qwen_smvl,
    args=training_args,
    train_dataset=raw_data["train"],
    eval_dataset=raw_data["test"],
    data_collator=collate_fn,
)
trainer.train(resume_from_checkpoint=last_checkpoint)
qwen_smvl.save_pretrained(training_args.output_dir)

完整代码见代码及数据集链接汇总

或者直接由完整项目GitHub地址

微调训练&结果展示

环境安装与微调代码执行

代码准备与环境安装

实验的完整代码可以在GitHub仓库地址找到。使用git clone克隆仓库后,运行以下命令安装依赖环境:

pip install -r requirements.txt

数据集和模型下载

笔者提供了一个自动下载脚本,该脚本通过魔塔社区完成模型与数据集的下载:

bash download_resource.sh

小批量微调训练

为了快速验证效果,笔者首先使用cocoqa数据集进行了200步的训练,所有超参数均与前文一致。运行实验命令如下,推荐使用8卡进行训练,在8张沐曦GPU卡上预计需要20分钟:

# 单GPU训练
CUDA_VISIBLE_DEVICES=0 python train.py ./cocoqa_train.yaml
# 8GPU训练
accelerate launch --num_process 8 train.py ./cocoqa_train.yaml

需要注意的是,本项目使用SwanLab记录和分析训练日志。如果尚未登录SwanLab,需先执行swanlab login进行登录。运行成功后,将显示如下结果,表明实验已顺利启动:

run
成功训练后可以看到SwanLab链接

以下是笔者完成小批量微调训练后的训练损失和测试损失曲线图:

cocoqa_swanlab
SwanLab训练可视化分析结果,可以看到最后训练损失和测试损失都收敛在0.65左右

模型在训练完成后会自动使用一张狗狗图片配合问题“图中有什么动物?”让模型根据图片进行推理,推理结果如下:

bad_case
SwanLab记录了模型训练好后的推理结果,可以看到模型能正常理解和回复中文

当时看到模型对着三只狗的图片回答“兔子”时,笔者一度以为炼丹失败了。不过实际上,如果炼丹真的失败,模型不会输出动物类型,而是会输出乱码或提示用户未识别到图片。这次识别错误的原因主要是由于训练步数过少所致。后续增加训练步数和数据量后,模型能够正确识别出狗狗,并准确说出有三只狗。

dog
附上三只眼神忧伤的狗子,难道长得很像兔子吗?

PS:作者已在SwanLab上公开了训练结果,感兴趣的读者可以自行查看。SwanLab还支持克隆作者的训练日志,大家可以在自己训练时克隆笔者的项目作为对照。

完整微调训练结果展示

运行实验命令如下,推荐使用8卡进行训练,在8片沐曦C500芯片上预计需要1.5小时:

# 单GPU训练
CUDA_VISIBLE_DEVICES=0 python train.py ./full_train.yaml
# 8GPU训练
accelerate launch --num_processes 8 train.py ./full_train.yaml

# 分阶段训练
CUDA_VISIBLE_DEVICES=0 python train_staged.py ./staged_training_test.yaml


# 分阶段全量训练
CUDA_VISIBLE_DEVICES=0 python train_staged.py ./staged_training.yaml

下图展示了使用完整微调数据对比于小批量训练的效果,可以看到全量数据微调时损失波动更为明显,这是由于数据类型的丰富性给模型的学习带来了一定的挑战。

fulldata_swanlab
红色为完整训练loss,黄色为小批量训练结果

进一步对比完整训练和小批量训练的训练损失和测试损失,可以看到完整训练的模型训练损失达到了0.61,远低于仅使用cocoqa数据集的效果;评估损失也显著降低,维持在0.58左右。

evalloss
红色为完整训练loss,黄色为小批量训练结果

值得一提的是,由于我们选用的测试集规模较小(仅有64条数据),因此训练损失和测试损失之间的差距并不能直接说明模型存在过拟合现象。实际上,在大模型训练中,如果数据集足够大,通常可以认为训练损失与评估损失基本一致。

此外,通过对1000步之后的训练损失和平均梯度范数(Grad Norm)变化进行分析,可以看出此时训练任务已过半,学习率开始快速衰减。如下图所示,在学习率快速衰减的情况下,模型损失并未出现明显下降,这表明模型已经实现了充分训练。

1kstep
1000步之后模型的训练损失变化

在训练效率方面,可以看到我们仍未完全发挥沐曦GPU的性能。这主要是由于多模态任务的网络架构较为复杂,涉及大量的图像与文本拼接操作,导致GPU资源未能被充分利用。

mx-gpu-use
SwanLab对沐曦C500训练效率的自动记录

同样地,在训练完成后使用狗狗图片进行了测试,这次模型能够理解图片、中文,并给出正确的回复。更为关键的是,模型完全保留了Qwen3-0.6B原有的全部能力,包括函数调用、推理等。在此基础上,仅增加了0.09B参数量,便为模型赋予了图像理解能力!

good_case
同样的图片与问题,更大的数据量和更充足的数据使得模型能够正确给出回复

模型推理与效果分析

待笔者补充完数据集后,未来会进一步完善测试环节 ; )

请关注swanlab教程集合获取最新更新教程!

代码及数据集链接汇总

微调使用的The Cauldron数据集下载链接:

Qwen3-0.6B模型下载:

本实验完整代码GitHub链接:

本实验SwanLab日志:

参考资料

常见问题

相似工具推荐

openclaw

OpenClaw 是一款专为个人打造的本地化 AI 助手,旨在让你在自己的设备上拥有完全可控的智能伙伴。它打破了传统 AI 助手局限于特定网页或应用的束缚,能够直接接入你日常使用的各类通讯渠道,包括微信、WhatsApp、Telegram、Discord、iMessage 等数十种平台。无论你在哪个聊天软件中发送消息,OpenClaw 都能即时响应,甚至支持在 macOS、iOS 和 Android 设备上进行语音交互,并提供实时的画布渲染功能供你操控。 这款工具主要解决了用户对数据隐私、响应速度以及“始终在线”体验的需求。通过将 AI 部署在本地,用户无需依赖云端服务即可享受快速、私密的智能辅助,真正实现了“你的数据,你做主”。其独特的技术亮点在于强大的网关架构,将控制平面与核心助手分离,确保跨平台通信的流畅性与扩展性。 OpenClaw 非常适合希望构建个性化工作流的技术爱好者、开发者,以及注重隐私保护且不愿被单一生态绑定的普通用户。只要具备基础的终端操作能力(支持 macOS、Linux 及 Windows WSL2),即可通过简单的命令行引导完成部署。如果你渴望拥有一个懂你

349.3k|★★★☆☆|昨天
Agent开发框架图像

stable-diffusion-webui

stable-diffusion-webui 是一个基于 Gradio 构建的网页版操作界面,旨在让用户能够轻松地在本地运行和使用强大的 Stable Diffusion 图像生成模型。它解决了原始模型依赖命令行、操作门槛高且功能分散的痛点,将复杂的 AI 绘图流程整合进一个直观易用的图形化平台。 无论是希望快速上手的普通创作者、需要精细控制画面细节的设计师,还是想要深入探索模型潜力的开发者与研究人员,都能从中获益。其核心亮点在于极高的功能丰富度:不仅支持文生图、图生图、局部重绘(Inpainting)和外绘(Outpainting)等基础模式,还独创了注意力机制调整、提示词矩阵、负向提示词以及“高清修复”等高级功能。此外,它内置了 GFPGAN 和 CodeFormer 等人脸修复工具,支持多种神经网络放大算法,并允许用户通过插件系统无限扩展能力。即使是显存有限的设备,stable-diffusion-webui 也提供了相应的优化选项,让高质量的 AI 艺术创作变得触手可及。

162.1k|★★★☆☆|2天前
开发框架图像Agent

everything-claude-code

everything-claude-code 是一套专为 AI 编程助手(如 Claude Code、Codex、Cursor 等)打造的高性能优化系统。它不仅仅是一组配置文件,而是一个经过长期实战打磨的完整框架,旨在解决 AI 代理在实际开发中面临的效率低下、记忆丢失、安全隐患及缺乏持续学习能力等核心痛点。 通过引入技能模块化、直觉增强、记忆持久化机制以及内置的安全扫描功能,everything-claude-code 能显著提升 AI 在复杂任务中的表现,帮助开发者构建更稳定、更智能的生产级 AI 代理。其独特的“研究优先”开发理念和针对 Token 消耗的优化策略,使得模型响应更快、成本更低,同时有效防御潜在的攻击向量。 这套工具特别适合软件开发者、AI 研究人员以及希望深度定制 AI 工作流的技术团队使用。无论您是在构建大型代码库,还是需要 AI 协助进行安全审计与自动化测试,everything-claude-code 都能提供强大的底层支持。作为一个曾荣获 Anthropic 黑客大奖的开源项目,它融合了多语言支持与丰富的实战钩子(hooks),让 AI 真正成长为懂上

143.9k|★★☆☆☆|今天
开发框架Agent语言模型

ComfyUI

ComfyUI 是一款功能强大且高度模块化的视觉 AI 引擎,专为设计和执行复杂的 Stable Diffusion 图像生成流程而打造。它摒弃了传统的代码编写模式,采用直观的节点式流程图界面,让用户通过连接不同的功能模块即可构建个性化的生成管线。 这一设计巧妙解决了高级 AI 绘图工作流配置复杂、灵活性不足的痛点。用户无需具备编程背景,也能自由组合模型、调整参数并实时预览效果,轻松实现从基础文生图到多步骤高清修复等各类复杂任务。ComfyUI 拥有极佳的兼容性,不仅支持 Windows、macOS 和 Linux 全平台,还广泛适配 NVIDIA、AMD、Intel 及苹果 Silicon 等多种硬件架构,并率先支持 SDXL、Flux、SD3 等前沿模型。 无论是希望深入探索算法潜力的研究人员和开发者,还是追求极致创作自由度的设计师与资深 AI 绘画爱好者,ComfyUI 都能提供强大的支持。其独特的模块化架构允许社区不断扩展新功能,使其成为当前最灵活、生态最丰富的开源扩散模型工具之一,帮助用户将创意高效转化为现实。

107.9k|★★☆☆☆|昨天
开发框架图像Agent

LLMs-from-scratch

LLMs-from-scratch 是一个基于 PyTorch 的开源教育项目,旨在引导用户从零开始一步步构建一个类似 ChatGPT 的大型语言模型(LLM)。它不仅是同名技术著作的官方代码库,更提供了一套完整的实践方案,涵盖模型开发、预训练及微调的全过程。 该项目主要解决了大模型领域“黑盒化”的学习痛点。许多开发者虽能调用现成模型,却难以深入理解其内部架构与训练机制。通过亲手编写每一行核心代码,用户能够透彻掌握 Transformer 架构、注意力机制等关键原理,从而真正理解大模型是如何“思考”的。此外,项目还包含了加载大型预训练权重进行微调的代码,帮助用户将理论知识延伸至实际应用。 LLMs-from-scratch 特别适合希望深入底层原理的 AI 开发者、研究人员以及计算机专业的学生。对于不满足于仅使用 API,而是渴望探究模型构建细节的技术人员而言,这是极佳的学习资源。其独特的技术亮点在于“循序渐进”的教学设计:将复杂的系统工程拆解为清晰的步骤,配合详细的图表与示例,让构建一个虽小但功能完备的大模型变得触手可及。无论你是想夯实理论基础,还是为未来研发更大规模的模型做准备

90.1k|★★★☆☆|昨天
语言模型图像Agent

Deep-Live-Cam

Deep-Live-Cam 是一款专注于实时换脸与视频生成的开源工具,用户仅需一张静态照片,即可通过“一键操作”实现摄像头画面的即时变脸或制作深度伪造视频。它有效解决了传统换脸技术流程繁琐、对硬件配置要求极高以及难以实时预览的痛点,让高质量的数字内容创作变得触手可及。 这款工具不仅适合开发者和技术研究人员探索算法边界,更因其极简的操作逻辑(仅需三步:选脸、选摄像头、启动),广泛适用于普通用户、内容创作者、设计师及直播主播。无论是为了动画角色定制、服装展示模特替换,还是制作趣味短视频和直播互动,Deep-Live-Cam 都能提供流畅的支持。 其核心技术亮点在于强大的实时处理能力,支持口型遮罩(Mouth Mask)以保留使用者原始的嘴部动作,确保表情自然精准;同时具备“人脸映射”功能,可同时对画面中的多个主体应用不同面孔。此外,项目内置了严格的内容安全过滤机制,自动拦截涉及裸露、暴力等不当素材,并倡导用户在获得授权及明确标注的前提下合规使用,体现了技术发展与伦理责任的平衡。

88.9k|★★★☆☆|昨天
开发框架图像Agent