Segearth-R2-06 继续。下面进入第 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_port29500 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_DEVICES0python segearth_r2/train/merge_lora_weights_and_save_hf_model.py\--model_pathyour_model_path\--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\--save_pathyour_save_path\--lora_r41.CUDA_VISIBLE_DEVICES0只使用第 0 张 GPU。RTX 5090 单卡复现时保留即可。2.--model_path这是你训练输出的 checkpoint 目录例如--model_pathoutputs/debug_5090_lora_r4或者如果你训练到了某个 checkpoint--model_pathoutputs/debug_5090_lora_r4/checkpoint-5000这个路径必须包含 DeepSpeed / LoRA 训练保存出来的权重。3.--vision_tower这是 SigLIP 路径--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384注意它虽然叫 vision tower但这是给多模态语言模型使用的视觉塔不是 Mask2Former 分割视觉骨干。4.--vision_tower_mask这是 Mask2Former / Swin 分割分支权重--vision_tower_maskpretrained_model/mask2former/model_final_54b88a.pkl如果这个路径错了合并时初始化分割模块就可能失败。5.--mask_config这是分割头配置--mask_configsegearth_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_pathoutputs/merged_segearth_r2_lasers后续eval.py的--model_path就应该指向这个目录。7.--lora_r必须和训练时一致。如果训练时--lora_r4合并时也要--lora_r4如果不一致LoRA 层 shape 可能不匹配。三、合并脚本内部做了什么merge_lora_weights_and_save_hf_model.py里有几个关键步骤。1. 重新构建模型脚本会modelSegEarthR2.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 modelload_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核心代码modelmodel.merge_and_unload()含义基础权重 W LoRA 增量 ΔW ↓ 合并成新的 W合并后就不再需要 LoRA adapter 单独存在。5. 保存完整模型最后model.save_pretrained(args.save_path,state_dictstate_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 └── ...四、合并阶段最容易错的地方错误 1lora_r不一致表现size mismatch shape mismatch解决训练时 lora_r 是多少合并时就是多少。错误 2model_path指错如果你传的是空目录或者不是 DeepSpeed checkpoint 目录会报checkpoint not found latest file not found zero checkpoint not found解决检查目录里是否有类似global_step* zero_pp_rank* mp_rank* latest checkpoint-*错误 3mask_config和训练时不一致表现pixel_decoder key mismatch predictor key mismatch hidden dim mismatch解决合并、训练、评估三阶段必须使用同一个 mask_config。错误 4vision_tower_mask权重版本不匹配表现unexpected key missing key load_state_dict error官方代码初始化分割模块时使用strictFalse加载部分模块但如果权重结构差异太大仍会影响效果。合并脚本会传入vision_tower_mask并调用initial_mask_module()。(GitHub)五、合并后的测试命令官方test.sh是deepspeed--includelocalhost:0--master_port29500segearth_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_port29500segearth_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_towerpretrained_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:int0vision_tower:strpretrained_model/CLIPvision_tower_mask:strpretrained_model/mask2former/model_final_54b88a.pklbase_data_path:Optional[str]field(defaultyour_data_path)model_path:Optional[str]field(defaultSegEarthR2_LaSeRS/hfweights-50000)mask_config:Optional[str]field(default...)model_map_name:strsegearth_r2version:strllava_phitemperature:float0.2num_beams:int1max_new_tokens:int128do_sample:boolTrueoutput_dir:strsave_folderdataloader_num_workers:int8重点参数参数含义vision_towerSigLIP 路径vision_tower_maskMask2Former / Swin 权重路径base_data_path数据集根目录model_path合并后的 SegEarth-R2 模型路径mask_configMask2Former 配置temperature生成随机性num_beamsbeam 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\ntext},{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,Wimages.shape[-2],images.shape[-1]masks_prednp.zeros((n_gt,1,H,W),dtypenp.uint8)含义是如果模型没有预测出 mask 就用全 0 mask 作为预测结果 避免评估程序崩溃。这也说明一个重要问题模型如果没有生成 [SEG]评估不一定报错但 IoU 会很差。所以复现时不能只看程序能跑完还要看模型输出是否真的包含[SEG]。源码中原本有打印模型输入和输出文本的代码但被注释掉了。它会把生成文本中的[SEG]用颜色标出来。(GitHub)你调试时建议把这段打开input_token_leninput_ids.shape[1]generated_idsoutput_ids[0][input_token_len:]output_texttokenizer.decode(generated_ids,skip_special_tokensTrue)print(Model Input:,text)print(Model Output:,output_text)你要确认输出里有[SEG]十三、IoU 是怎么计算的源码中每个预测 mask 和 GT mask 做internp.logical_and(pred_bin,gt_bin).sum()unionnp.logical_or(pred_bin,gt_bin).sum()IoU(inter/union)ifunion0else1.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_gtlen(gt_masks)ifmasks_predisNone:masks_predzeros n_predmasks_pred.shape[0]ifn_predn_gt:masks_prednp.concatenate([...],axis0)elifn_predn_gt:masks_predmasks_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 tokenizerAutoTokenizer.from_pretrained(outputs/merged_segearth_r2_lasers)print(tokenizer.encode([SEG],add_special_tokensFalse))print(tokenizer.convert_tokens_to_ids([SEG]))你希望看到一个有效 id而不是 unknown token。3. 数据路径是否符合代码预期eval.py当前写死查找json_foldersos.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_leninput_ids.shape[1]generated_idsoutput_ids[0][input_token_len:]output_texttokenizer.decode(generated_ids,skip_special_tokensFalse)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_diros.path.join(data_args.output_dir,split)os.makedirs(save_dir,exist_okTrue)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.pyifidx1:break确认能读图 能读文本 能生成 answer 能生成 [SEG] 能得到 masks_pred第三步跑一个 split例如只跑一个测试 annotation 文件。如果test/annotations下有多个 split不要一开始全部跑。第四步跑完整测试集完整跑deepspeed--includelocalhost:0--master_port29500segearth_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十八、评估常见报错与解决报错 1unrecognized arguments: --eval_batch_size原因官方文档命令提到--eval_batch_size 1但当前eval.py的 dataclass 里没有该字段。(GitHub)解决删掉这个参数。报错 2FileNotFoundError: rs_reason_seg/LaSeRS/test/annotations原因base_data_path指错。解决让路径满足base_data_path/rs_reason_seg/LaSeRS/test/annotations或者改eval.py里的json_folders路径。报错 3SiglipImageProcessor找不到配置原因--vision_tower默认是pretrained_model/CLIP但你的 SigLIP 可能在更深目录。解决--vision_towerpretrained_model/CLIP/siglip-so400m-patch14-384报错 4masks_pred is None很多可能原因模型没有生成 [SEG] [SEG] token 没有正确加入 tokenizer LoRA 没有合并好 推理 prompt 和训练 prompt 不一致 训练步数太少优先检查print(output_text)print(output_text.count([SEG]))报错 5IoU 很低但程序正常可能原因mask 和 [SEG] 顺序错位 GT mask resize 有问题 模型只学会生成 [SEG] 但 mask decoder 没学好 Mask2Former 权重没正确加载 训练步数太少检查pred mask 可视化 gt mask 可视化 [SEG] 数量 pred mask 数量 gt mask 数量十九、训练、合并、评估三阶段参数必须一致这是复现的铁律。参数训练合并评估model_name_or_path / model_pathMipha-3B训练 checkpoint合并后模型vision_towerSigLIPSigLIPSigLIPvision_tower_maskMask2Former 权重Mask2Former 权重Mask2Former 权重mask_config同一个 yaml同一个 yaml同一个 yamllora_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 编译环境。