继续。下面进入第 6 讲:评估、推理、LoRA 合并与复现闭环。
这一讲解决一个实际问题:
训练完成后,怎么把模型真正跑起来? 怎么合并 LoRA? 怎么测试 LaSeRS? 怎么得到 mask? 怎么判断复现是否成功?第 6 讲总目标
前面我们已经吃透了训练链路:
train.sh ↓ train.py ↓ dataset.py ↓ llava_phi.py forward ↓ loss 训练现在进入推理链路:
训练 checkpoint ↓ merge_lora_weights.sh ↓ 合并后的 HF 模型 ↓ test.sh / eval.py ↓ model.inference() ↓ eval_seg() ↓ pred mask ↓ IoU / gIoU官方测试脚本使用deepspeed --include localhost:0 --master_port=29500 segearth_r2/eval/eval.py启动,并需要传入base_data_path、model_path、vision_tower_mask、mask_config和output_dir。(GitHub)
一、先理解训练后为什么要合并 LoRA
你训练时不是全量微调整个大模型,而是:
基础模型 Mipha-3B + LoRA adapter + 训练后的分割模块参数训练完后,checkpoint 里通常不是一个完整可直接部署的 HuggingFace 模型,而是包含 LoRA、DeepSpeed 分片、部分可训练模块权重。
所以需要:
merge_lora_weights.sh它的作用是:
读取训练 checkpoint ↓ 重新构建 SegEarthR2 ↓ 加载 LoRA 配置 ↓ 从 DeepSpeed ZeRO checkpoint 恢复 fp32 权重 ↓ merge_and_unload() ↓ 保存成完整 HuggingFace 模型目录官方merge_lora_weights.sh调用的是segearth_r2/train/merge_lora_weights_and_save_hf_model.py,参数包括model_path、vision_tower、vision_tower_mask、mask_config、save_path和lora_r。(GitHub)
二、merge_lora_weights.sh逐项解释
官方脚本核心形式是:
CUDA_VISIBLE_DEVICES=0python segearth_r2/train/merge_lora_weights_and_save_hf_model.py\--model_path=your_model_path\--vision_tower=pretrained_model/CLIP/siglip-so400m-patch14-384\--vision_tower_mask=pretrained_model/mask2former/model_final_54b88a.pkl\--mask_config=segearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml\--save_path=your_save_path\--lora_r=41.CUDA_VISIBLE_DEVICES=0
只使用第 0 张 GPU。
RTX 5090 单卡复现时保留即可。
2.--model_path
这是你训练输出的 checkpoint 目录,例如:
--model_path=outputs/debug_5090_lora_r4或者如果你训练到了某个 checkpoint:
--model_path=outputs/debug_5090_lora_r4/checkpoint-5000这个路径必须包含 DeepSpeed / LoRA 训练保存出来的权重。
3.--vision_tower
这是 SigLIP 路径:
--vision_tower=pretrained_model/CLIP/siglip-so400m-patch14-384注意,它虽然叫 vision tower,但这是给多模态语言模型使用的视觉塔,不是 Mask2Former 分割视觉骨干。
4.--vision_tower_mask
这是 Mask2Former / Swin 分割分支权重:
--vision_tower_mask=pretrained_model/mask2former/model_final_54b88a.pkl如果这个路径错了,合并时初始化分割模块就可能失败。
5.--mask_config
这是分割头配置:
--mask_config=segearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml它必须和训练时一致。
如果训练时用的是 Swin-B 配置,合并时不能突然换成 Swin-L 配置,否则 predictor、pixel decoder、hidden dim 都可能不匹配。
6.--save_path
合并后的完整模型保存目录,例如:
--save_path=outputs/merged_segearth_r2_lasers后续eval.py的:
--model_path就应该指向这个目录。
7.--lora_r
必须和训练时一致。
如果训练时:
--lora_r4合并时也要:
--lora_r=4如果不一致,LoRA 层 shape 可能不匹配。
三、合并脚本内部做了什么
merge_lora_weights_and_save_hf_model.py里有几个关键步骤。
1. 重新构建模型
脚本会:
model=SegEarthR2.from_pretrained(...)然后:
model.initial_mask_module(mask2former_ckpt,model_args)model.get_model().initialize_vision_modules(model_args)也就是说,它不是简单读取权重,而是先按模型结构把 SegEarthR2 重新搭起来。源码里load_pretrained_model()会读取 mask config,构建SegEarthR2,初始化 mask module,再初始化视觉模块。(GitHub)
2. 重新注入 LoRA
脚本里也会调用:
find_linear_layers()LoraConfig()get_peft_model()这一步必须和训练时的 LoRA 目标层一致。
它默认还是找:
q_proj v_proj并排除:
vision_tower vision_tower_mask lm_head pixel_decoder predictor SEG_token_projector这和训练时逻辑一致。
3. 从 DeepSpeed ZeRO checkpoint 恢复
核心代码是:
fromdeepspeed.utils.zero_to_fp32importload_state_dict_from_zero_checkpoint model=load_state_dict_from_zero_checkpoint(model,model_path)意思是:
DeepSpeed ZeRO 保存的分片权重 ↓ 恢复成完整 fp32 state_dict这是使用 ZeRO-2 / ZeRO-3 后必须理解的一步。源码中合并脚本明确调用load_state_dict_from_zero_checkpoint(),然后执行model.merge_and_unload()。(GitHub)
4. 合并 LoRA
核心代码:
model=model.merge_and_unload()含义:
基础权重 W + LoRA 增量 ΔW ↓ 合并成新的 W'合并后就不再需要 LoRA adapter 单独存在。
5. 保存完整模型
最后:
model.save_pretrained(args.save_path,state_dict=state_dict)tokenizer.save_pretrained(args.save_path)这一步会保存成 HuggingFace 格式模型目录。
最终你应该得到类似:
outputs/merged_segearth_r2_lasers/ ├── config.json ├── generation_config.json ├── model-00001-of-000xx.safetensors 或 pytorch_model.bin ├── tokenizer_config.json ├── tokenizer.model / tokenizer.json ├── special_tokens_map.json └── ...四、合并阶段最容易错的地方
错误 1:lora_r不一致
表现:
size mismatch shape mismatch解决:
训练时 lora_r 是多少,合并时就是多少。错误 2:model_path指错
如果你传的是空目录,或者不是 DeepSpeed checkpoint 目录,会报:
checkpoint not found latest file not found zero checkpoint not found解决:检查目录里是否有类似:
global_step* zero_pp_rank* mp_rank* latest checkpoint-*错误 3:mask_config和训练时不一致
表现:
pixel_decoder key mismatch predictor key mismatch hidden dim mismatch解决:
合并、训练、评估三阶段必须使用同一个 mask_config。错误 4:vision_tower_mask权重版本不匹配
表现:
unexpected key missing key load_state_dict error官方代码初始化分割模块时使用strict=False加载部分模块,但如果权重结构差异太大,仍会影响效果。合并脚本会传入vision_tower_mask并调用initial_mask_module()。(GitHub)
五、合并后的测试命令
官方test.sh是:
deepspeed--includelocalhost:0--master_port=29500segearth_r2/eval/eval.py\--base_data_pathdata_path\--model_pathmodel_path\--vision_tower_maskpretrained_model/mask2former/model_final_54b88a.pkl\--mask_configsegearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml\--output_diroutput/res其中model_path应该指向合并后的模型目录,而不是原始 Mipha-3B,也不是未合并的 LoRA adapter。官方评估文档同样要求运行segearth_r2/eval/eval.py,并修改base_data_path、mask_config、model_path、output_dir等路径。(GitHub)
你可以改成:
deepspeed--includelocalhost:0--master_port=29500segearth_r2/eval/eval.py\--base_data_path/your/path/LaSeRS\--model_pathoutputs/merged_segearth_r2_lasers\--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384\--vision_tower_maskpretrained_model/mask2former/model_final_54b88a.pkl\--mask_configsegearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml\--output_diroutputs/eval_res注意:官方test.sh没有显式传--vision_tower,但eval.py的Arguments里有默认vision_tower="pretrained_model/CLIP"。如果你的 SigLIP 不在这个默认路径,一定要自己传--vision_tower。(GitHub)
六、eval.py总体流程
eval.py可以分成 5 段:
1. 解析 Arguments 2. load_pretrained_model() 加载模型 3. 构造 SigLIP image processor 和 DataCollator 4. 遍历 LaSeRS test annotations 5. do_eval() 逐样本推理并计算 IoU源码中main()会调用load_pretrained_model()加载 tokenizer、model、image processor 和 context length,然后设置 conversation template,构造SiglipImageProcessor、DataCollatorForCOCODatasetV2,再遍历base_data_path/rs_reason_seg/LaSeRS/test/annotations下的各个 split。(GitHub)
七、Arguments参数类解释
eval.py中定义了:
@dataclassclassArguments:local_rank:int=0vision_tower:str="pretrained_model/CLIP"vision_tower_mask:str="pretrained_model/mask2former/model_final_54b88a.pkl"base_data_path:Optional[str]=field(default='your_data_path')model_path:Optional[str]=field(default="SegEarthR2_LaSeRS/hfweights-50000")mask_config:Optional[str]=field(default="...")model_map_name:str='segearth_r2'version:str='llava_phi'temperature:float=0.2num_beams:int=1max_new_tokens:int=128do_sample:bool=Trueoutput_dir:str='save_folder'dataloader_num_workers:int=8重点参数:
| 参数 | 含义 |
|---|---|
vision_tower | SigLIP 路径 |
vision_tower_mask | Mask2Former / Swin 权重路径 |
base_data_path | 数据集根目录 |
model_path | 合并后的 SegEarth-R2 模型路径 |
mask_config | Mask2Former 配置 |
temperature | 生成随机性 |
num_beams | beam search 数量 |
max_new_tokens | 最大生成 token 数 |
do_sample | 是否采样生成 |
output_dir | 输出目录 |
这里有个小细节:官方docs/Evaluation.md命令里写了--eval_batch_size 1,但当前eval.py的Arguments片段中没有看到eval_batch_size字段,而代码里 dataloader 的 batch size 是固定写成1。因此如果你运行时发现--eval_batch_size被识别为未知参数,就把这个参数删掉,按源码当前逻辑使用 batch size 1。(GitHub)
八、preprocess_input()推理输入构造
eval.py推理时并不是直接使用 dataloader 里的input_ids,而是重新构造了一次输入。
核心函数:
preprocess_input(text,image_path,tokenizer,clip_image_processor)它返回:
input_ids images images_clip也就是和训练时一样,仍然有两路图像:
images → Swin / Mask2Former 分割分支 images_clip → SigLIP / LLM 视觉塔源码中preprocess_input()会先调用preprocess_image(image_path)构造主分割图像,并用 ImageNet mean/std 归一化;然后调用preprocess_instruction()构造文本 input_ids;最后调用preprocess_image_clip()构造 SigLIP 输入图像。(GitHub)
九、preprocess_instruction()做了什么
推理时的 prompt 结构是:
sources=[[{'from':'human','value':prefix_inst+'\n'+text},{'from':'gpt','value':''}]]其中prefix_inst是:
This is an image , please doing Reasoning Segmentation according to the following instruction:然后把用户指令text拼进去。
这说明推理时模型输入是:
Human: This is an image, please doing Reasoning Segmentation according to the following instruction: {遥感分割指令} GPT:模型需要继续生成 answer,并在生成过程中产生[SEG]。源码中preprocess_instruction()使用 conversation template 拼接 prompt,并调用tokenizer_special_tokens()保留图像和 refer 特殊 token。(GitHub)
十、do_eval()推理主循环
do_eval()是评估核心。
主流程是:
model.eval() torch.no_grad() ↓ 获取 SEG_token_id ↓ 遍历 eval_dataloader ↓ 读取 text 和 image_path ↓ preprocess_input() ↓ model.inference() ↓ 得到 output_ids 和 masks_pred ↓ 读取 gt mask ↓ 对齐 pred / gt 数量 ↓ 计算 IoU ↓ 输出 split gIoU源码中do_eval()会从inputs['seg_info'][0]['instruction']取文本,从inputs['seg_info'][0]['image_path']取图像路径,然后调用model.inference(...)得到output_ids和masks_pred。(GitHub)
十一、model.inference()关键理解
虽然我们这次重点看eval.py,但你要把它和llava_phi.py接上。
推理阶段大致是:
input_ids + images_clip ↓ LLM generate ↓ 生成 output_ids ↓ 找到 output_ids 中的 [SEG] ↓ 重新或同步取 [SEG] hidden state ↓ eval_seg() ↓ 输出 masks_pred训练时[SEG]来自 ground truth answer。
推理时[SEG]来自模型自己生成的 answer。
这就是训练和推理最大的区别:
训练: answer 已知,里面本来就有 [SEG] 推理: answer 未知,模型必须自己生成 [SEG]如果推理时模型没有生成[SEG],那就没有 mask query,最终masks_pred可能为空。
十二、为什么评估里要处理masks_pred is None
源码中有这样的逻辑:
ifmasks_predisNone:H,W=images.shape[-2],images.shape[-1]masks_pred=np.zeros((n_gt,1,H,W),dtype=np.uint8)含义是:
如果模型没有预测出 mask, 就用全 0 mask 作为预测结果, 避免评估程序崩溃。这也说明一个重要问题:
模型如果没有生成 [SEG],评估不一定报错,但 IoU 会很差。所以复现时不能只看程序能跑完,还要看模型输出是否真的包含[SEG]。
源码中原本有打印模型输入和输出文本的代码,但被注释掉了。它会把生成文本中的[SEG]用颜色标出来。(GitHub)
你调试时建议把这段打开:
input_token_len=input_ids.shape[1]generated_ids=output_ids[0][input_token_len:]output_text=tokenizer.decode(generated_ids,skip_special_tokens=True)print("Model Input:",text)print("Model Output:",output_text)你要确认输出里有:
[SEG]十三、IoU 是怎么计算的
源码中每个预测 mask 和 GT mask 做:
inter=np.logical_and(pred_bin,gt_bin).sum()union=np.logical_or(pred_bin,gt_bin).sum()IoU+=(inter/union)ifunion>0else1.0最后:
print(f"{split}gIoU:{IoU/overall_mask_num}")也就是说这里的gIoU不是严格意义上目标检测里的 generalized IoU,而是所有 mask 的平均 IoU。源码中do_eval()会累计overall_mask_num、I、U和IoU,最后打印split gIoU。(GitHub)
你可以理解为:
每个 mask 计算一个 IoU ↓ 所有 mask 求平均 ↓ 输出 split gIoU十四、评估时 pred 和 gt 数量如何对齐
源码中有一段很实用:
n_gt=len(gt_masks)ifmasks_predisNone:masks_pred=zeros n_pred=masks_pred.shape[0]ifn_pred<n_gt:masks_pred=np.concatenate([...],axis=0)elifn_pred>n_gt:masks_pred=masks_pred[:n_gt]含义:
如果预测 mask 少于 GT,就复制最后一个预测补齐; 如果预测 mask 多于 GT,就截断。这让评估程序更加稳健,但从研究严谨性看,你要注意:
预测 mask 数量不等于 GT mask 数量,本身就说明模型生成 [SEG] 数量不稳定。所以你复现时除了看 IoU,还应该统计:
生成 [SEG] 数量 预测 mask 数量 GT mask 数量 三者是否一致十五、复现评估前的检查清单
在正式跑eval.py前,按下面顺序检查。
1. 合并模型目录是否完整
lsoutputs/merged_segearth_r2_lasers至少应该有:
config.json tokenizer_config.json special_tokens_map.json 模型权重文件2.[SEG]token 是否存在
运行:
fromtransformersimportAutoTokenizer tokenizer=AutoTokenizer.from_pretrained("outputs/merged_segearth_r2_lasers")print(tokenizer.encode("[SEG]",add_special_tokens=False))print(tokenizer.convert_tokens_to_ids("[SEG]"))你希望看到一个有效 id,而不是 unknown token。
3. 数据路径是否符合代码预期
eval.py当前写死查找:
json_folders=os.path.join(data_args.base_data_path,'rs_reason_seg/LaSeRS/test/annotations')也就是说你的实际目录应该类似:
base_data_path/ └── rs_reason_seg/ └── LaSeRS/ └── test/ ├── annotations/ └── images/这一点和有些文档里直接写train/images、test/images的理解可能不同。以当前eval.py源码为准。源码中main()明确使用base_data_path/rs_reason_seg/LaSeRS/test/annotations获取测试 split。(GitHub)
4.vision_tower路径是否正确
如果你的 SigLIP 放在:
pretrained_model/CLIP/siglip-so400m-patch14-384评估命令必须传:
--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384否则SiglipImageProcessor.from_pretrained(data_args.vision_tower)可能找不到正确配置。源码中main()会用data_args.vision_tower初始化SiglipImageProcessor。(GitHub)
5. batch size 先保持 1
评估代码中 dataloader batch size 固定为 1。不要一开始改 batch size,因为当前推理代码大量使用:
inputs['seg_info'][0]inputs['mask_num'][0]说明作者默认逐样本评估。源码中dataloader_params里batch_size固定为1。(GitHub)
十六、建议你修改eval.py的几个调试点
为了真正吃透项目,我建议你临时加这些打印。
1. 打印输入文本
在do_eval()里:
print("instruction:",text)print("image_path:",image_path)确认模型看到的是正确指令。
2. 打印生成文本
取消官方注释,或者加:
input_token_len=input_ids.shape[1]generated_ids=output_ids[0][input_token_len:]output_text=tokenizer.decode(generated_ids,skip_special_tokens=False)print("output_text:",output_text)print("[SEG] count:",output_text.count("[SEG]"))重点看:
output_text 是否包含 [SEG] [SEG] 数量是否等于 gt mask 数量3. 打印 mask 数量
print("n_gt:",n_gt)print("n_pred:",Noneifmasks_predisNoneelsemasks_pred.shape[0])如果长期masks_pred is None,说明模型没有生成[SEG]或model.inference()中[SEG]解析失败。
4. 保存预测 mask 可视化
官方评估当前主要计算 IoU,没有明显保存彩色可视化结果。你可以在do_eval()中加:
save_dir=os.path.join(data_args.output_dir,split)os.makedirs(save_dir,exist_ok=True)pred_vis=(pred_bin*255).astype(np.uint8)cv2.imwrite(os.path.join(save_dir,f"{idx}_pred.png"),pred_vis)gt_vis=(gt_bin*255).astype(np.uint8)cv2.imwrite(os.path.join(save_dir,f"{idx}_gt.png"),gt_vis)这样你能直观看到:
模型到底分割了哪里 是完全空白 还是偏移 还是边界粗糙 还是目标类别错了十七、完整复现闭环建议
你不要直接从全量训练开始,而是按下面做。
第一步:只验证模型能加载
python -<<'PY' from segearth_r2.utils.builder import load_pretrained_model model_path = "outputs/merged_segearth_r2_lasers" mask_config = "segearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml" print("start load") # 这里按项目 builder 的实际参数补齐 print("load ok") PY先不跑数据,确认模型权重没问题。
第二步:只取一张图推理
临时改eval.py:
ifidx>=1:break确认:
能读图 能读文本 能生成 answer 能生成 [SEG] 能得到 masks_pred第三步:跑一个 split
例如只跑一个测试 annotation 文件。
如果test/annotations下有多个 split,不要一开始全部跑。
第四步:跑完整测试集
完整跑:
deepspeed--includelocalhost:0--master_port=29500segearth_r2/eval/eval.py\--base_data_path/your/path\--model_pathoutputs/merged_segearth_r2_lasers\--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384\--vision_tower_maskpretrained_model/mask2former/model_final_54b88a.pkl\--mask_configsegearth_r2/model/mask_decoder/mask_config/maskformer2_swin_base_384_bs16_50ep.yaml\--output_diroutputs/eval_res\--dataloader_num_workers2十八、评估常见报错与解决
报错 1:unrecognized arguments: --eval_batch_size
原因:官方文档命令提到--eval_batch_size 1,但当前eval.py的 dataclass 里没有该字段。(GitHub)
解决:删掉这个参数。
报错 2:FileNotFoundError: rs_reason_seg/LaSeRS/test/annotations
原因:base_data_path指错。
解决:让路径满足:
base_data_path/rs_reason_seg/LaSeRS/test/annotations或者改eval.py里的json_folders路径。
报错 3:SiglipImageProcessor找不到配置
原因:--vision_tower默认是pretrained_model/CLIP,但你的 SigLIP 可能在更深目录。
解决:
--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384报错 4:masks_pred is None很多
可能原因:
模型没有生成 [SEG] [SEG] token 没有正确加入 tokenizer LoRA 没有合并好 推理 prompt 和训练 prompt 不一致 训练步数太少优先检查:
print(output_text)print(output_text.count("[SEG]"))报错 5:IoU 很低但程序正常
可能原因:
mask 和 [SEG] 顺序错位 GT mask resize 有问题 模型只学会生成 [SEG] 但 mask decoder 没学好 Mask2Former 权重没正确加载 训练步数太少检查:
pred mask 可视化 gt mask 可视化 [SEG] 数量 pred mask 数量 gt mask 数量十九、训练、合并、评估三阶段参数必须一致
这是复现的铁律。
| 参数 | 训练 | 合并 | 评估 |
|---|---|---|---|
model_name_or_path / model_path | Mipha-3B | 训练 checkpoint | 合并后模型 |
vision_tower | SigLIP | SigLIP | SigLIP |
vision_tower_mask | Mask2Former 权重 | Mask2Former 权重 | Mask2Former 权重 |
mask_config | 同一个 yaml | 同一个 yaml | 同一个 yaml |
lora_r | 例如 4 | 必须 4 | 合并后无需传 |
[SEG]tokenizer | 添加 | 保存 | 必须存在 |
如果你只记一句话:
训练、合并、评估的 mask_config、vision_tower、vision_tower_mask 必须一致。二十、到目前为止的完整复现路线
现在我们已经把项目主线串起来了。
1. 安装环境 ↓ 2. 准备 Mipha-3B / SigLIP / Mask2Former / LaSeRS ↓ 3. 修改 train.sh ↓ 4. 跑 debug 20 steps ↓ 5. 确认 loss_llm / loss_mask / loss_dice / loss_attention 正常 ↓ 6. 跑完整训练 ↓ 7. merge_lora_weights.sh 合并 LoRA ↓ 8. 检查 tokenizer 中 [SEG] ↓ 9. 修改 test.sh / eval.py 路径 ↓ 10. 单样本推理 ↓ 11. 可视化 pred mask ↓ 12. 跑完整 test split ↓ 13. 得到 IoU / gIoU二十一、你现在应该掌握的核心结论
到这一讲为止,你应该能讲清楚:
1. 为什么训练后不能直接 eval,要先合并 LoRA。 2. merge_lora_weights.sh 如何恢复 ZeRO checkpoint。 3. 合并时 lora_r 必须和训练一致。 4. eval.py 重新构造 input_ids、images、images_clip。 5. 推理时 [SEG] 是模型自己生成的。 6. 如果没有生成 [SEG],masks_pred 可能是 None。 7. eval.py 当前默认 batch size 是 1。 8. IoU 是逐 mask 计算后平均。 9. 评估时必须检查生成文本里的 [SEG] 数量。 10. 训练、合并、评估的 vision_tower、vision_tower_mask、mask_config 必须一致。下一讲:进入 RTX 5090 复现环境与编译问题
下一讲建议专门解决你最可能遇到的问题:
第 7 讲:RTX 5090 上复现 SegEarth-R2 重点: 1. CUDA 12.8 / PyTorch cu128 选择 2. Detectron2 编译 3. MSDeformAttn 编译 4. flash-attn 是否能装 5. DeepSpeed 安装 6. BF16 / TF32 设置 7. 常见 nvcc、sm_120、gcc、CUDA_HOME 报错 8. 单卡 RTX 5090 的推荐 train.sh这部分非常关键,因为 SegEarth-R2 不是纯 Python 项目,真正卡人的地方往往不是模型逻辑,而是Detectron2 + Mask2Former + MSDeformAttn + RTX 5090 编译环境。