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

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

在学习完 BLIP/BLIP2 模型原理后,如何将其应用于工业开发至关重要。本文将作为"跌倒检测"实战项目的第一篇,详细记录如何从零到一,使用 Salesforce 的 LAVIS 框架实现一个完整的工业化流程:模型部署 -> 数据构建 -> 微调 -> 验证。

本文最大的特色是**"踩坑"与"避坑"。我们将首先复现一个"失败"的预训练模型推理,然后重点展示如何 自定义数据集 (Dataset)** 和 Builder ,以及如何配置 yaml 文件来微调 BLIP2。我们将详细分析每一个关键步骤中(特别是 JSON 构建和 Windows 训练)我曾犯过的错误,帮助读者节省宝贵的调试时间。希望本文项目对您有所帮助。


文章目录

  • [BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与"踩坑"指南)](#BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与“踩坑”指南))
    • 一、引言:项目目标与环境配置
      • [1.1 项目背景与"踩坑"忠告](#1.1 项目背景与“踩坑”忠告)
      • [1.2 环境配置](#1.2 环境配置)
    • 二、步骤一:"翻车"的基线推理
      • [2.1 基于 LAVIS 加载模型](#2.1 基于 LAVIS 加载模型)
      • [2.2 第一次推理:意料之中的失败](#2.2 第一次推理:意料之中的失败)
    • [三、步骤二:构建 VQA 微调数据集](#三、步骤二:构建 VQA 微调数据集)
      • [3.1 目录结构](#3.1 目录结构)
      • [3.2 JSON 文件构建与踩坑](#3.2 JSON 文件构建与踩坑)
    • [四、步骤三:自定义 LAVIS 的 Dataset 与 Builder](#四、步骤三:自定义 LAVIS 的 Dataset 与 Builder)
      • [4.1 为什么要自定义?](#4.1 为什么要自定义?)
      • [4.2 🧱 构建 Dataset](#4.2 🧱 构建 Dataset)
      • [4.3 🧱 构建 Builder](#4.3 🧱 构建 Builder)
      • [4.4 注册 Builder](#4.4 注册 Builder)
    • [五、步骤四:配置 YAML 与执行微调](#五、步骤四:配置 YAML 与执行微调)
      • [5.1 训练 YAML 配置文件](#5.1 训练 YAML 配置文件)
      • [5.2 执行微调与结果验证](#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 类型。

python 复制代码
# 代码块 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 第一次推理:意料之中的失败

我们加载一张本地的跌倒图像,并向模型提问。

python 复制代码
# 代码块 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 文件记录了图像、问题和答案。以下是最终的正确格式

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 文件时,我犯了两个致命错误,浪费了大量时间:

  1. 键名错误 (Key Names) :LAVIS 默认的 Builder (如 COCOVQADataset) 对 JSON 中的 key 有严格要求。如果你使用的键名(如 "question")和 Builder 期望的(如 "text_input")不符,它根本读不到数据。
    • 解决方案 :要么修改 JSON 键名,要么(像我一样)重新构建一个自己的 Builder
  2. 答案维度错误 (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)。

python 复制代码
# 代码块 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

python 复制代码
# 代码块 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 = FalldownVQAInstructDataset
    eval_dataset_cls = FalldownVQAInstructDataset

    # 跳过 LAVIS 默认的数据集下载步骤
    def _download_data(self):
        return

4.4 注册 Builder

最后,在 LAVIS/lavis/datasets/builders/__init__.py 文件中,导入我们刚创建的 Builder。

python 复制代码
# 代码块 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 文件。

yaml 复制代码
# 代码块 7: my.yaml (训练配置文件)
model:
  arch: blip2_t5
  model_type: pretrain_flant5xl
  load_pretrained: True
  # T5 模型路径
  t5_model: google/flan-t5-xl
  # BLIP2 预训练权重路径 (你需要自己下载)
  pretrained: "E:/root/autodl-tmp/blip2_pretrained_flant5xl.pth"
  freeze_vit: True # 微调阶段,冻结 ViT

datasets:
  # 关键:这里的名字必须和 Builder 注册的名字一致
  my_falldown_dataset: 
    data_type: images
    vis_processor:
      train:
        name: "blip_image_train"
        image_size: 224
    text_processor:
      train:
        name: "blip_instruction"
    build_info:
      images:
        # 图像文件夹的绝对路径
        storage: E:/LLM/dataset/falldown/images
      annotations:
        train:
          storage:
            # 标注文件的绝对路径
            - E:/LLM/dataset/falldown/train.json
        val:
          storage:
            - E:/LLM/dataset/falldown/train.json
        test:
          storage:
            - E:/LLM/dataset/falldown/train.json

run:
  distributed: False # Windows 下必须为 False
  task: vqa # 关键:指定任务为 VQA
  lr_sched: "linear_warmup_cosine_lr"
  init_lr: 1e-4
  min_lr: 1e-5
  warmup_lr: 1e-6
  weight_decay: 0.05
  max_epoch: 50
  batch_size_train: 2 # 根据你的显存调整
  batch_size_eval: 2
  num_workers: 4
  warmup_steps: 2000
  seed: 42
  output_dir: "output/BLIP2/FallDown_stage1"
  amp: True
  resume_ckpt_path: null
  evaluate: False
  train_splits: ["train"]
  device: "cuda"
  world_size: 1
  dist_url: "env://"

⚠️ 踩坑警告:YAML 配置与 Windows 训练

  1. Builder 注册名错误datasets: 下的键名 my_falldown_dataset 必须和 MyDatasetBuilder@registry.register_builder("my_falldown_dataset") 中注册的名字完全一致 。否则会报错:AttributeError: 'NoneType' object has not attribute 'default_config_path'
  2. 缺少 Taskrun: 下的 task: vqa 不能省略 !LAVIS 依赖这个 task 来确定使用哪个训练流程(如 RunnerVQA)。
  3. 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 执行微调与结果验证

一切就绪,开始训练!

cmd 复制代码
# 代码块 8: 启动训练
# 假设你在 LAVIS 的根目录
python train.py --cfg-path E:/LLM/dataset/falldown/my.yaml

(图 2:模型训练日志截图)

训练完成后,权重保存在 output_dir 中。我们加载微调后的权重(例如第 30 个 epoch 的),再试一次。

python 复制代码
# 代码块 9: 加载微调后的模型并进行推理
from lavis.models import load_model_and_preprocess
import torch
from PIL import Image

device = 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 模型。

下一步展望:在专栏(二)中,我们将解决过拟合问题,

相关推荐
gddkxc3 小时前
悟空AI CRM15版本 自定义字段功能
人工智能
大千AI助手3 小时前
Netflix Prize竞赛:推荐系统的里程碑与机器学习革命的催化剂
人工智能·机器学习·推荐系统·netflix·竞赛·电影推荐·冷启动
Lab4AI大模型实验室3 小时前
【每日Arxiv热文】北大新框架 Edit-R1 炸场!破解图像编辑 3 大难题,双榜刷 SOTA
人工智能·计算机视觉
掘金安东尼3 小时前
从“打标签”到“算行为”:抖音推荐系统的进化逻辑(附打分算法深度解析)
人工智能
腾视科技3 小时前
AI赋能 车行无忧|腾视科技ES10终端,为车辆装上“智慧大脑”
人工智能
货拉拉技术4 小时前
大模型音频水印技术:用AI守护音频数据的“身份指纹”
人工智能·算法·安全
skywalk81634 小时前
简化AI服务构建的Python框架leptonai
人工智能·大模型·lepton
小龙报4 小时前
《赋能AI解锁Coze智能体搭建核心技能(1)--- 初识coze》
人工智能·语言模型·数据分析·交互·文心一言·机器翻译·coze
love530love4 小时前
【笔记】Podman Desktop 部署 开源数字人 HeyGem.ai
人工智能·windows·笔记·python·容器·开源·podman