BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与“踩坑”指南)
BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与“踩坑”指南)

在学习完 BLIP/BLIP2 模型原理后,如何将其应用于工业开发至关重要。本文将作为“跌倒检测”实战项目的第一篇,详细记录如何从零到一,使用 Salesforce 的 LAVIS 框架实现一个完整的工业化流程:模型部署 -> 数据构建 -> 微调 -> 验证。
本文最大的特色是**“踩坑”与“避坑”。我们将首先复现一个“失败”的预训练模型推理,然后重点展示如何自定义数据集 (Dataset)** 和 Builder,以及如何配置
yaml文件来微调 BLIP2。我们将详细分析每一个关键步骤中(特别是 JSON 构建和 Windows 训练)我曾犯过的错误,帮助读者节省宝贵的调试时间。希望本文项目对您有所帮助。
文章目录
- BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与“踩坑”指南)
- 一、引言:项目目标与环境配置
- 1.1 项目背景与“踩坑”忠告
- 1.2 环境配置
- 二、步骤一:“翻车”的基线推理
- 2.1 基于 LAVIS 加载模型
- 2.2 第一次推理:意料之中的失败
- 三、步骤二:构建 VQA 微调数据集
- 3.1 目录结构
- 3.2 JSON 文件构建与踩坑
- 四、步骤三:自定义 LAVIS 的 Dataset 与 Builder
- 4.1 为什么要自定义?
- 4.2 🧱 构建 Dataset
- 4.3 🧱 构建 Builder
- 4.4 注册 Builder
- 五、步骤四:配置 YAML 与执行微调
- 5.1 训练 YAML 配置文件
- 5.2 执行微调与结果验证
- 六、总结与下一步
一、引言:项目目标与环境配置
我们的目标是实现一个工业级的“跌倒检测”项目,完整流程规划为:部署 -> 数据构建 -> 微调 -> 量化 -> 部署。本篇(一)将重点覆盖前三个环节。
1.1 项目背景与“踩坑”忠告
本文将以“编者”(即作者我)的亲身经历,记录从零开始的整个微调过程。
编者忠告:
我认为自己已经把整个微调过程能犯的错误都犯过了。因此,跟着我的思路进行数据集构建和配置,可以帮助你避开绝大多数的 Bug。
1.2 环境配置
不同库版本间有很强的依赖性。如果条件允许,请尽量使用我的这套环境,可以避免在配置上花费不必要的时间。
表格 1:关键环境依赖
| 库 (Library) | 版本 (Version) |
|---|---|
python | 3.10 |
torch | 2.7.1 |
torchvision | 0.22.1 |
transformer | 4.31.0 |
huggingface-hub | 0.25.2 |
LAVIS | main branch |
二、步骤一:“翻车”的基线推理
我们首先尝试直接使用 LAVIS 加载预训练的 BLIP2 模型,看看它在“跌倒检测”这个特定任务上的“零样本” (Zero-shot) 表现。
2.1 基于 LAVIS 加载模型
我们选择 blip2_t5 模型和 pretrain_flant5xl 类型。
# 代码块 1: 加载预训练 BLIP-2 模型
from lavis.models import load_model_and_preprocess
import torch
from PIL import Image
import requests# 1. 加载 BLIP-2 模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")# 注意:LAVIS 目前支持 flant5xl, flant5xxl, opt2.7b, opt6.7b 等
model, vis_processors, _ = load_model_and_preprocess(name="blip2_t5",model_type="pretrain_flant5xl",is_eval=True,device=device,
)
2.2 第一次推理:意料之中的失败
我们加载一张本地的跌倒图像,并向模型提问。
# 代码块 2: 执行零样本推理
# 2. 加载本地图像
url = "../dataset/falldown/images/1.jpg" # 换成你自己的图像路径
raw_image = Image.open(url).convert("RGB")# 3. 构造问题 (VQA)
question = "Is someone falling in this image? please answer yes or no:"# 4. 预处理
image = vis_processors["eval"](raw_image).unsqueeze(0).to(device)
sample = {"image": image,"text_input": question
}# 5. 生成答案
output = model.generate(sample)
print(f"提问: {question}")
print(f"回答: {output[0]}")# 预期(错误)回答: a man is being helped by another man on the ground
🔴 失败分析 (The Problem)
我们想要的回答:
"yes"或者"no"。模型的实际回答:
"a man is being helped by another man on the ground"。可以发现,这个回答和我们想要的答案相差甚远。模型没有在“回答问题”,而是在“描述图像”。无论我们怎么修改提示词 (Prompt),模型都只会返回对图像的完整阐述。
结论 🟢:预训练模型无法理解我们的特定任务(VQA 式的跌倒判断)。为此,构建一份新的数据集来微调模型至关重要。
三、步骤二:构建 VQA 微调数据集
这是整个项目中最关键、也是“坑”最多的一步。
3.1 目录结构
我们采用 LAVIS VQA 任务的标准格式:images 文件夹 + json 标注文件。
/dataset/falldown/
├── images/
│ ├── 1.jpg
│ ├── 2.jpg
│ └── ... (编者用了10张图)
└── train.json


3.2 JSON 文件构建与踩坑
JSON 文件记录了图像、问题和答案。以下是最终的正确格式:
# 代码块 3: train.json (正确格式)
[{"instance_id": 1,"image": "1.jpg","text_input": "Is anyone falling","answer": ["yes"]},{"instance_id": 2,"image": "2.jpg","text_input": "Is anyone falling in this image?","answer": ["yes"]}
]
⚠️ 踩坑警告:LAVIS 数据集构建的核心 Bug
在创建 JSON 文件时,我犯了两个致命错误,浪费了大量时间:
- 键名错误 (Key Names):LAVIS 默认的 Builder (如
COCOVQADataset) 对 JSON 中的key有严格要求。如果你使用的键名(如"question")和 Builder 期望的(如"text_input")不符,它根本读不到数据。
- 解决方案:要么修改 JSON 键名,要么(像我一样)重新构建一个自己的 Builder。
- 答案维度错误 (Dimension Error):这是最隐蔽的 Bug!
- 错误写法:
"answer": "yes"- 正确写法:
"answer": ["yes"]- 报错:
RuntimeError: shape '[24, -1, 32, 64]' is invalid for input of size 655360- 原因:LAVIS 默认
answer是一个列表 (List),它会遍历列表中的每个答案。如果你写了"yes",它会把这个字符串当作列表,遍历为"y","e","s"三个独立的答案,导致后续处理(如text_processor)时数据维度彻底崩溃。
四、步骤三:自定义 LAVIS 的 Dataset 与 Builder
由于我们的 JSON 键名和默认 Builder 不匹配,我们必须自定义 Dataset 和 Builder。
4.1 为什么要自定义?
LAVIS 在执行 train.py 时,必须通过 yaml 中指定的 Builder 来构建Dataset。当现有的 Builder 无法满足我们的任务需求(如键名不匹配、数据处理逻辑不同)时,我们必须自定义。
4.2 🧱 构建 Dataset
在 LAVIS/lavis/datasets/datasets/ 路径下创建你自己的 Python 文件(例如 my_vqa_dataset.py)。
# 代码块 4: my_vqa_dataset.py (自定义 Dataset)
import os
import json
import random
from PIL import Image
from lavis.datasets.datasets.vqa_datasets import VQADataset
from collections import OrderedDict# __DisplMixin 是可选的,用于美观地显示样本,这里省略以简化
# ... (省略 __DisplMixin) ...class FalldownVQADataset(VQADataset):def __init__(self, vis_processor, text_processor, vis_root, ann_paths):super().__init__(vis_processor, text_processor, vis_root, ann_paths)def __getitem__(self, index):ann = self.annotation[index]image_path = os.path.join(self.vis_root, ann["image"])image = Image.open(image_path).convert("RGB")image = self.vis_processor(image)# 对应 JSON 中的 "text_input" 键question = self.text_processor(ann["text_input"])# 对应 JSON 中的 "answer" 键 (注意它是一个列表)answer_weight = {}for answer in ann["answer"]:if answer in answer_weight.keys():answer_weight[answer] += 1 / len(ann["answer"])else:answer_weight[answer] = 1 / len(ann["answer"])answers = list(answer_weight.keys())weights = list(answer_weight.values())return {"image": image,"text_input": question,"answers": answers,"weights": weights,}# -----------------------------------------------------------------
# 关键!LAVIS 的 VQA 任务在训练时需要 "text_output" 键
# -----------------------------------------------------------------
class FalldownVQAInstructDataset(FalldownVQADataset):def __getitem__(self, index):data = super().__getitem__(index)if data is not None:# 从答案列表中随机选一个作为训练目标data['text_output'] = random.choice(data["answers"])return data# collater 貌似在新版 LAVIS 中非必须,但保留有好处# def collater(self, samples):# data = super().collater(samples)# if 'answer' in data:# data['text_output'] = data['answer']# return data
✅ 关键点
必须要有 FalldownVQAInstructDataset 这个子类!
FalldownVQADataset(基类) 负责从 JSON 加载数据,键为"answers"。- LAVIS 的 VQA 训练管道 (task) 在训练时,会去寻找一个名为
"text_output"的键作为 T5 模型的 Decoder 输入。FalldownVQAInstructDataset的作用就是从"answers"列表中随机选一个,并将其赋值给"text_output",从而桥接数据和模型。
4.3 🧱 构建 Builder
在 LAVIS/lavis/datasets/builders/ 路径下创建 my_data_builder.py。
# 代码块 5: my_data_builder.py (自定义 Builder)
from lavis.common.registry import registry
from lavis.datasets.builders.base_dataset_builder import BaseDatasetBuilder
# 关键:导入我们刚创建的 Dataset 类
from lavis.datasets.datasets.my_vqa_dataset import FalldownVQAInstructDataset# 注册一个新名字,这个名字将在 YAML 文件中用到!!!
@registry.register_builder("my_falldown_dataset")
class MyDatasetBuilder(BaseSbuilDatasetBuilder):train_dataset_cls = FalldownVQAInstructDataseteval_dataset_cls = FalldownVQAInstructDataset# 跳过 LAVIS 默认的数据集下载步骤def _download_data(self):return
4.4 注册 Builder
最后,在 LAVIS/lavis/datasets/builders/__init__.py 文件中,导入我们刚创建的 Builder。
# 代码块 6: 在 __init__.py 中注册
from lavis.datasets.builders.base_dataset_builder import load_dataset_builder
# ... (其他 builders) ...# 添加这一行,让 LAVIS 框架能找到我们的 Builder
from lavis.datasets.builders.my_data_builder import MyDatasetBuilder
五、步骤四:配置 YAML 与执行微调
数据和代码准备就绪,现在我们配置训练文件。
5.1 训练 YAML 配置文件
在你的项目目录下创建一个 my.yaml 文件。
# 代码块 7: my.yaml (训练配置文件)
model:arch: blip2_t5model_type: pretrain_flant5xlload_pretrained: True# T5 模型路径t5_model: google/flan-t5-xl# BLIP2 预训练权重路径 (你需要自己下载)pretrained: "E:/root/autodl-tmp/blip2_pretrained_flant5xl.pth"freeze_vit: True # 微调阶段,冻结 ViTdatasets:# 关键:这里的名字必须和 Builder 注册的名字一致my_falldown_dataset: data_type: imagesvis_processor:train:name: "blip_image_train"image_size: 224text_processor:train:name: "blip_instruction"build_info:images:# 图像文件夹的绝对路径storage: E:/LLM/dataset/falldown/imagesannotations:train:storage:# 标注文件的绝对路径- E:/LLM/dataset/falldown/train.jsonval:storage:- E:/LLM/dataset/falldown/train.jsontest:storage:- E:/LLM/dataset/falldown/train.jsonrun:distributed: False # Windows 下必须为 Falsetask: vqa # 关键:指定任务为 VQAlr_sched: "linear_warmup_cosine_lr"init_lr: 1e-4min_lr: 1e-5warmup_lr: 1e-6weight_decay: 0.05max_epoch: 50batch_size_train: 2 # 根据你的显存调整batch_size_eval: 2num_workers: 4warmup_steps: 2000seed: 42output_dir: "output/BLIP2/FallDown_stage1"amp: Trueresume_ckpt_path: nullevaluate: Falsetrain_splits: ["train"]device: "cuda"world_size: 1dist_url: "env://"
⚠️ 踩坑警告:YAML 配置与 Windows 训练
- Builder 注册名错误:
datasets:下的键名my_falldown_dataset必须和MyDatasetBuilder类@registry.register_builder("my_falldown_dataset")中注册的名字完全一致。否则会报错:AttributeError: 'NoneType' object has not attribute 'default_config_path'。- 缺少 Task:
run:下的task: vqa不能省略!LAVIS 依赖这个task来确定使用哪个训练流程(如RunnerVQA)。- Windows 分布式错误:如果你在 Windows 下训练 (如我),
run:distributed必须设为False。
- 报错:
ValueError: Default process group has not been initialized...- 解决方案:除了设为
False,还需要注释掉LAVIS/lavis/runners/runner_base.py文件中第 422 行 (或附近) 的dist.barrier()。
5.2 执行微调与结果验证
一切就绪,开始训练!
# 代码块 8: 启动训练
# 假设你在 LAVIS 的根目录
python train.py --cfg-path E:/LLM/dataset/falldown/my.yaml
(图 2:模型训练日志截图)
训练完成后,权重保存在 output_dir 中。我们加载微调后的权重(例如第 30 个 epoch 的),再试一次。
# 代码块 9: 加载微调后的模型并进行推理
from lavis.models import load_model_and_preprocess
import torch
from PIL import Imagedevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model, vis_processors, _ = load_model_and_preprocess(name="blip2_t5",model_type="pretrain_flant5xl",is_eval=True,device=device,
)# 加载我们微调后的权重
ckpt_path = "./output/BLIP2/FallDown_stage1/checkpoint_30.pth" # 换成你的权重路径
ckpt = torch.load(ckpt_path, map_location="cpu")
msg = model.load_state_dict(ckpt["model"], strict=False)
print(f"Model load status: {msg}")# --- 使用和步骤一完全相同的代码进行推理 ---
url = "../dataset/falldown/images/1.jpg"
raw_image = Image.open(url).convert("RGB")
question = "Anyone falldown?" # 换个问法image = vis_processors["eval"](raw_image).unsqueeze(0).to(device)
sample = {"image": image,"text_input": question
}output = model.generate(sample)
print(f"提问: {question}")
print(f"回答: {output[0]}")# 预期(正确)回答: yes
六、总结与下一步
⛔ 注意:过拟合警告
我们的模型现在会正确回答
yes了。但请注意:我们只用了 10 张图进行微调。这个模型现在很可能只会输出 “yes”,无论你给它看什么(即过拟合)。如果想真正实现一个鲁棒的跌倒检测模型,下一步必须混合通用 VQA 数据集(如 COCO-VQA)和你自己构建的“负样本”(即没有跌倒的图像,答案为
"no"),一起进行微调。
总结:在本篇(一)中,我们成功复现了 BLIP2 的“零样本”失败,并通过自定义 LAVIS 的 Dataset 和 Builder,成功微调了一个(过拟合的)跌倒检测 VQA 模型。
下一步展望:在专栏(二)中,我们将解决过拟合问题,
