1. 项目概述
最近在NPU平台上完成了InternVL3.5-1B和Intern-S1-mini两个视觉语言模型的微调实践,整个过程从环境搭建到模型部署,踩了不少坑也积累了一些经验。这两个模型在视觉语言理解任务上表现优异,特别是在公式识别这类需要同时处理图像和文本的任务中。本文将详细记录整个微调流程,包括环境配置、数据处理、模型训练和结果提交等关键环节。
为什么选择这两个模型?InternVL3.5-1B是Intern团队推出的1B参数量的视觉语言大模型,在多模态理解任务上表现突出;而Intern-S1-mini则是其轻量级版本,更适合资源受限的场景。两者都采用了先进的视觉-语言对齐技术,能够很好地理解图像内容和文本描述之间的关系。
2. 环境配置与准备
2.1 算力实例创建
在开始之前,我们需要准备一个合适的开发环境。我使用的是NPU平台提供的Ubuntu22.04-Conda镜像,配置了1个NPU计算单元。创建过程非常简单:
- 登录平台后进入【开发机】页面
- 选择【创建开发机】→【个人开发机】
- 填写开发机名称(任意)
- 选择镜像:Ubuntu22.04-Conda
- 资源配置选择1*NPU
- 点击【立即创建】
注意:虽然平台也提供GPU选项,但对于视觉语言模型这种计算密集型任务,NPU通常能提供更好的能效比。如果你的任务对延迟不敏感,也可以选择配置较低的实例节省成本。
创建完成后,我们得到一个干净的Ubuntu环境,接下来需要安装必要的软件包。
2.2 环境安装与配置
进入开发机后,首先创建一个独立的Python环境,避免与系统Python产生冲突:
bash复制cd /root/
conda create -n ms_swift python=3.10 -y
conda activate ms_swift
然后安装PyTorch和相关的依赖包。这里需要特别注意版本兼容性问题:
bash复制pip install torch==2.6.0 torch-npu==2.6.0 torchaudio==2.6.0 torchvision decorator \
-i https://pypi.tuna.tsinghua.edu.cn/simple
pip install ms-swift==3.12 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install timm==1.0.9 msgspec==0.19.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install numpy==1.26.4 -i https://pypi.tuna.tsinghua.edu.cn/simple
提示:如果下载速度慢,可以尝试更换pip源。国内常用的源包括清华、阿里云、中科大等。我实测下来清华源在大多数情况下速度最稳定。
安装完成后,建议运行简单的测试命令验证环境是否正常:
bash复制python -c "import torch; print(torch.__version__)"
2.3 数据集准备
我们使用的是公式识别数据集,可以通过ModelScope直接下载:
bash复制pip install modelscope
modelscope download --dataset JimmyMa99/VLM-formula-recognition-dataset_intern_camp \
--local_dir /root/dataset/VLM-formula-recognition-dataset_intern_camp
下载完成后,数据集中的json文件需要做一些预处理,主要是将图片路径转换为绝对路径。我编写了一个转换脚本convert.py:
python复制import json, os, sys
in_path = "/root/dataset/VLM-formula-recognition-dataset_intern_camp/train/train_mini.jsonl"
out_path = "/root/dataset/VLM-formula-recognition-dataset_intern_camp/train/train_mini_abs.jsonl"
base = "/root/dataset/VLM-formula-recognition-dataset_intern_camp/train/"
def to_abs(p):
if not p: return p
return p if os.path.isabs(p) else os.path.join(base, p)
n=0;m=0
with open(in_path,'r',encoding='utf-8') as fin, open(out_path,'w',encoding='utf-8') as fout:
for line in fin:
if not line.strip(): continue
obj = json.loads(line)
changed = False
if 'images' in obj and isinstance(obj['images'], list):
obj['images'] = [to_abs(x) for x in obj['images']]
changed = True
if 'messages' in obj:
for msg in obj['messages']:
c = msg.get('content')
if isinstance(c, list):
for part in c:
if part.get('type') == 'image' and 'image' in part:
part['image'] = to_abs(part['image'])
changed = True
fout.write(json.dumps(obj,ensure_ascii=False)+'\n')
n+=1
m+=changed
print(f"processed lines={n}, patched={m}, saved -> {out_path}")
执行脚本完成转换:
bash复制mkdir /root/swift_workdir/
cd /root/swift_workdir/
python convert.py
3. InternVL3.5-1B模型微调
3.1 微调配置
微调采用LoRA(Low-Rank Adaptation)方法,这是一种高效的参数微调技术,可以在只训练少量参数的情况下获得不错的性能提升。我创建了以下配置文件internvl3.5_1b_train_npu.sh:
bash复制#!/bin/bash
LOG_DIR="logs"
mkdir -p $LOG_DIR
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="$LOG_DIR/internvl3.5_1b_sft_${TIMESTAMP}.log"
export OMP_NUM_THREADS=1
export ASCEND_RT_VISIBLE_DEVICES=0
export NPROC_PER_NODE=1
export MASTER_PORT=$((10000 + RANDOM % 50000))
echo "Starting training..."
echo "Log file: $LOG_FILE"
echo "Using port: $MASTER_PORT"
nohup swift sft \
--model '/share/new_models/InternVL3.5/InternVL3_5-1B'\
--dataset '/root/dataset/VLM-formula-recognition-dataset_intern_camp/train/train_mini_abs.jsonl' \
--eval_steps 100 \
--train_type lora \
--lora_rank 16 \
--lora_dropout 0.01 \
--lora_alpha 32 \
--torch_dtype bfloat16 \
--num_train_epochs 1 \
--per_device_train_batch_size 2 \
--per_device_eval_batch_size 2 \
--learning_rate 5e-5 \
--warmup_ratio 0.05 \
--gradient_accumulation_steps 4 \
--save_steps 500 \
--save_total_limit 3 \
--gradient_checkpointing_kwargs '{"use_reentrant": false}' \
--logging_steps 10 \
--max_length 8000 \
--output_dir ./swift_output/SFT-InternVL3_5-1B\
--dataset_num_proc 8 \
--dataloader_num_workers 8 \
--metric acc \
--freeze_vit true \
> "$LOG_FILE" 2>&1 &
TRAIN_PID=$!
sleep 2
if kill -0 $TRAIN_PID 2>/dev/null; then
echo "Training started successfully with PID $TRAIN_PID"
echo "To view logs in real-time, use:"
echo "tail -f $LOG_FILE"
echo ""
echo "To stop training, use:"
echo "kill -9 $TRAIN_PID"
else
echo "Failed to start training process"
echo "Check log file for errors: $LOG_FILE"
fi
关键参数说明:
lora_rank: LoRA矩阵的秩,影响可训练参数数量lora_alpha: LoRA缩放因子,影响新知识融入原始模型的程度per_device_train_batch_size: 根据显存大小调整,NPU上2是比较安全的值learning_rate: 5e-5是一个比较通用的起点,可以根据loss变化调整
3.2 启动训练
执行以下命令开始微调:
bash复制cd /root/swift_workdir
bash config/internvl3.5_1b_train_npu.sh
训练过程中可以使用npu-smi info命令监控NPU使用情况:
bash复制npu-smi info
正常情况下应该能看到NPU利用率在80%以上,显存占用稳定。如果发现利用率过低,可能是数据加载成为瓶颈,可以尝试增加dataloader_num_workers。
3.3 权重合并
训练完成后,需要将LoRA权重合并回原始模型:
bash复制swift export --adapters "微调生成的checkpoint文件目录" --merge_lora True
合并过程通常很快,完成后会在原checkpoint目录下生成一个-merged后缀的新目录,里面就是可以直接使用的完整模型。
4. Intern-S1-mini模型微调
4.1 微调配置
Intern-S1-mini是轻量级版本,配置稍有不同。创建配置文件interns1-mini_train_npu.sh:
bash复制#!/bin/bash
LOG_DIR="logs"
mkdir -p $LOG_DIR
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="$LOG_DIR/interns1mini_sft_${TIMESTAMP}.log"
export NPROC_PER_NODE=1
export OMP_NUM_THREADS=1
export ASCEND_RT_VISIBLE_DEVICES=0
export MASTER_PORT=$((10000 + RANDOM % 50000))
echo "Starting training..."
echo "Log file: $LOG_FILE"
echo "Using port: $MASTER_PORT"
nohup swift sft \
--model '/share/new_models/Intern-S1-mini' \
--dataset '/root/dataset/VLM-formula-recognition-dataset_intern_camp/train/train_mini_abs.jsonl' \
--eval_steps 1000 \
--train_type lora \
--lora_rank 16 \
--lora_dropout 0.01 \
--lora_alpha 32 \
--torch_dtype bfloat16 \
--num_train_epochs 1 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--warmup_ratio 0.05 \
--gradient_accumulation_steps 4 \
--save_steps 500 \
--save_total_limit 10 \
--gradient_checkpointing_kwargs '{"use_reentrant": false}' \
--logging_steps 1 \
--max_length 8000 \
--output_dir ./swift_output/SFT-Interns1mini \
--dataset_num_proc 16 \
--dataloader_num_workers 16 \
--model_author JeffDing \
--model_name SFT-camp6 \
--metric acc \
--freeze_vit true \
> "$LOG_FILE" 2>&1 &
TRAIN_PID=$!
sleep 2
if kill -0 $TRAIN_PID 2>/dev/null; then
echo "Training started successfully with PID $TRAIN_PID"
echo "To view logs in real-time, use:"
echo "tail -f $LOG_FILE"
echo ""
echo "To stop training, use:"
echo "kill $TRAIN_PID"
else
echo "Failed to start training process"
echo "Check log file for errors: $LOG_FILE"
fi
主要区别:
- 学习率提高到1e-4(小模型通常需要更大的学习率)
- batch_size减小到1(模型虽小但NPU利用率更高)
- 增加了model_author和model_name参数方便识别
4.2 启动训练
执行命令开始微调:
bash复制cd /root/swift_workdir
bash config/interns1-mini_train_npu.sh
同样使用npu-smi info监控训练状态。由于模型较小,训练速度会比InternVL3.5-1B快很多。
4.3 权重合并与补全
训练完成后合并LoRA权重:
bash复制swift export --adapters "微调生成的checkpoint文件目录" --merge_lora True
然后需要补全模型文件,确保所有必要的配置文件都存在:
bash复制SRC="/share/new_models/Intern-S1-mini"
DST="/root/swift_workdir/swift_output/SFT-Interns1mini/v0-20260112-154339/checkpoint-750-merged"
rsync -ah --ignore-existing --exclude='/proc' --exclude='proc' "$SRC"/ "$DST"/
这一步很重要,因为有些配置文件在微调过程中不会被自动保存,需要从原始模型中复制过来。
5. 结果提交与模型上传
5.1 模型上传
微调完成后,可以将模型上传到ModelScope分享。创建上传脚本upload.py:
python复制from modelscope.hub.api import HubApi
from modelscope.hub.constants import Licenses, ModelVisibility
YOUR_ACCESS_TOKEN = "ms-9fxx7e" # 替换为自己的token
api = HubApi()
api.login(YOUR_ACCESS_TOKEN)
owner_name = "xx" # 你的ModelScope用户名
model_name = "xx" # 模型名称
model_id = f"{owner_name}/{model_name}"
api.create_model(
model_id,
visibility=ModelVisibility.PUBLIC,
license=Licenses.APACHE_V2,
chinese_name=f"{owner_name}的书生大模型实战营-公式微调分类比赛",
)
api.upload_folder(
repo_id=f"{owner_name}/{model_name}",
folder_path="xx", # 微调后模型的文件夹路径
commit_message="upload model folder to repo",
)
执行上传:
bash复制python upload.py
5.2 提示词优化
在公式识别任务中,提示词设计对结果影响很大。经过多次尝试,我发现以下提示词模板效果较好:
code复制请根据图片中的公式生成对应的latex公式文本。公式可能包含数学符号、希腊字母、上下标等元素,请确保准确识别所有细节。
可以根据具体任务需求进一步优化,比如增加对特定符号的说明或格式要求。
6. 常见问题与解决方案
在实际操作中,我遇到了以下几个典型问题及解决方法:
-
NPU利用率低
- 现象:
npu-smi info显示利用率低于50% - 原因:通常是数据加载成为瓶颈
- 解决:增加
dataloader_num_workers,确保数据集放在高速存储上
- 现象:
-
训练过程中断
- 现象:训练突然停止,日志显示内存不足
- 原因:
per_device_train_batch_size设置过大 - 解决:减小batch_size或增加
gradient_accumulation_steps
-
LoRA权重合并失败
- 现象:合并时提示缺少配置文件
- 原因:某些必要文件未被保存
- 解决:使用
rsync从原始模型目录补全文件
-
评估指标不升反降
- 现象:训练loss下降但评估指标变差
- 原因:可能过拟合或学习率不合适
- 解决:尝试减小学习率、增加正则化或早停
-
模型上传速度慢
- 现象:上传大模型时速度很慢
- 原因:网络带宽限制
- 解决:压缩模型文件(如tar.gz)后再上传,或选择非高峰时段
7. 性能优化建议
经过多次实验,我总结出以下几点优化建议:
-
学习率调度
- 使用warmup策略,前5%的训练步骤线性增加学习率
- 后期可以尝试cosine衰减,比固定学习率效果更好
-
批处理大小
- 在NPU内存允许的情况下,尽量使用较大的batch_size
- 配合梯度累积可以达到类似大批量的效果
-
混合精度训练
- 使用bfloat16可以在几乎不损失精度的情况下减少显存占用
- 但要注意某些操作可能需要强制转换为fp32
-
数据预处理
- 提前完成所有预处理工作,避免训练时进行耗时操作
- 使用多进程数据加载充分利用CPU资源
-
模型冻结
- 冻结视觉编码器(ViT)可以显著减少可训练参数
- 对下游任务性能影响不大时推荐使用
8. 扩展应用
这套微调方法不仅适用于公式识别,还可以应用于其他视觉语言任务:
-
图像描述生成
- 修改数据集为图片-描述对
- 调整提示词为"请详细描述图片中的内容"
-
视觉问答
- 使用VQA格式的数据集
- 输入为图片+问题,输出为答案
-
文档理解
- 处理扫描文档或PDF截图
- 同时识别文本内容和版面结构
-
多模态检索
- 微调为双编码器结构
- 实现图搜文或文搜图功能
对于这些应用,只需要替换数据集和少量调整训练配置即可,整体流程完全一致。
