视觉-语言大模型VLM实践——保姆级教程

在这里插入图片描述

理论部分可参考我的上篇博客:视觉-语言大模型VLM理论——保姆级教程

1. 多模态大模型Qwen2.5-VL本地部署指南

Qwen2.5-VL 是通义千问系列的最新多模态大模型,具备图文理解、视觉推理、文档解析等强大能力,广泛应用于智能搜索、内容生成、企业文档处理等领域。
🔹 主要功能
✅ 多模态问答:解析图片、图表、文档,回答问题,支持 OCR 识别。
✅ 复杂文档解析:提取发票、合同、PPT、表格等文件中的结构化信息。
✅ 高级视觉推理:理解图像中的关系,如因果推理、数据分析。
✅ 智能摘要与生成:自动生成图片描述、文档摘要,提高信息获取效率。
✅ 代码与 UI 解析:识别截图中的代码/UI 设计,生成可执行代码或交互说明。

1.1 环境准备

机器:4090
python: 3.10
cuda: 12.2

# 网络不好,可能需要尝试几次
pip install git+https://github.com/huggingface/transformers accelerate
pip install qwen-vl-utils[decord]

# 跑代码时缺少包
pip install torchvision==0.19.0

1.2 下载模型与代码封装

模型下载

from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen2.5-VL-7B')

推理代码封装

from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
import torch


class QwenVLModel:
    def __init__(self, model_path="./Qwen2.5-VL-7B-Instruct", use_flash_attention=False):
        """
        初始化Qwen VL模型
        Args:
            model_path: 模型路径
            use_flash_attention: 是否使用flash attention加速
        """
        # 加载模型
        if use_flash_attention:
            self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
                model_path,
                torch_dtype=torch.bfloat16,
                attn_implementation="flash_attention_2",
                device_map="auto",
            )
        else:
            self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
                model_path, torch_dtype="auto", device_map="auto"
            )

        # 初始化处理器
        min_pixels = 256*28*28
        max_pixels = 1280*28*28
        self.processor = AutoProcessor.from_pretrained(
            model_path, 
            min_pixels=min_pixels, 
            max_pixels=max_pixels, 
            use_fast=True
        )

    def process_image(self, image_path, prompt):
        """
        处理图片并生成输出
        Args:
            image_path: 图片路径
            prompt: 提示文本
        Returns:
            生成的文本输出
        """
        messages = [
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "image": image_path,
                    },
                    {"type": "text", "text": prompt},
                ],
            }
        ]

        # 准备推理输入
        text = self.processor.apply_chat_template(
            messages, tokenize=False, add_generation_prompt=True
        )
        image_inputs, video_inputs = process_vision_info(messages)
        inputs = self.processor(
            text=[text],
            images=image_inputs,
            videos=video_inputs,
            padding=True,
            return_tensors="pt",
        )
        inputs = inputs.to(self.model.device)

        # 生成输出
        generated_ids = self.model.generate(**inputs, max_new_tokens=512)
        generated_ids_trimmed = [
            out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
        ]
        output_text = self.processor.batch_decode(
            generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
        )
        return output_text



if __name__ == "__main__":
    model = QwenVLModel()
    img_path = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg"
    output_text = model.process_image(
        img_path,
        "请用中文描述一下这张图片"
    )
    print(f"输出信息: {output_text}")

1.3 效果测试
图片
图片
模型输出结果:

输出信息: ['这张图片展示了一位女士和一只狗在海滩上互动的场景。女士坐在沙滩上,穿着格子衬衫和黑色裤子,面带微笑,似乎在与狗进行友好互动。狗戴着彩色的项圈,正伸出前爪与女士的手相触碰,显得非常亲密和愉快。背景是广阔的海洋和天空,夕阳的余晖洒在沙滩上,营造出一种温馨和谐的氛围。整个画面给人一种轻松愉快的感觉。']

2. VLLM部署多模态大模型

在这里插入图片描述

2.1 vLLM简介

vLLM 是一个高效的推理和部署框架,专为大规模语言模型(LLM)优化。它采用 PagedAttention 技术,显著提高 GPU 显存利用率,支持高吞吐量推理。vLLM 兼容 Hugging Face Transformers 和 OpenAI API 接口,便于集成现有模型。其高效的 KV 缓存管理减少重复计算,适用于流式生成、批量处理和多用户推理场景。vLLM 还支持 FlashAttention,可进一步提升推理速度。相比传统推理框架,vLLM 具备更低的延迟和更高的并发能力,适用于部署 GPT-4 级别的大模型。

在这里插入图片描述
vLLM 通过这些优化,实现了 高吞吐量、低延迟、大模型推理加速,尤其适用于 多用户并发推理大模型部署

2.2 vLLM部署Qwen2.5 vl多模态大模型实战

环境准备

pip install git+https://github.com/huggingface/transformers accelerate
pip install qwen-vl-utils[decord]

# 跑代码时缺少包
pip install torchvision==0.19.0
pip install vllm==0.7.3

2.3 测试代码封装

from transformers import AutoProcessor
from vllm import LLM, SamplingParams
from qwen_vl_utils import process_vision_info


class QwenVLModel:
    def __init__(self, model_path="./Qwen2.5-VL-7B-Instruct"):
        self.model_path = model_path

        self.llm = LLM(
            model=self.model_path,
            limit_mm_per_prompt={"image": 1, "video": 1},
            tensor_parallel_size=1,      # 设置为1以减少GPU内存使用
            gpu_memory_utilization=0.9,  # 控制GPU内存使用率
            max_model_len=2048,          # 限制最大序列长度
            # quantization="awq",        # 使用AWQ量化来减少内存使用
        )

        self.sampling_params = SamplingParams(
            temperature=0.1,
            top_p=0.001,
            repetition_penalty=1.05,
            max_tokens=512,
            stop_token_ids=[],
        )

        self.processor = AutoProcessor.from_pretrained(self.model_path)

    def generate(self, messages):
        prompt = self.processor.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )
        image_inputs, video_inputs = process_vision_info(messages)

        mm_data = {}
        if image_inputs is not None:
            mm_data["image"] = image_inputs
        if video_inputs is not None:
            mm_data["video"] = video_inputs

        llm_inputs = {
            "prompt": prompt,
            "multi_modal_data": mm_data,
        }

        outputs = self.llm.generate([llm_inputs], sampling_params=self.sampling_params)
        return outputs[0].outputs[0].text


if __name__ == "__main__":
    from tqdm import tqdm
    img_path = "/home/xxx/xxx/workspace/Qwen2.5/test.jpg"  
    prompt_str = "提取图片中的关键信息(包含身份证号码),输出的键值对尽量用中文输出,并以json形式输出"
    image_messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "image": img_path,
                    "min_pixels": 256 * 28 * 28,
                    "max_pixels": 1280 * 28 * 28,
                },
                {"type": "text", "text": prompt_str},
            ],
        },
    ]
    model = QwenVLModel()
    output_text = model.generate(image_messages)
    print(output_text)

输出结果:

{
    "银行名称": "招商银行",
    "卡号": "5236498888888888",
    "持卡人姓名": "XIAO ZHAO",
    "银行卡类型": "MasterCard"
}

3. Swift实战(微调多模态模型Qwen2.5 vl 7B)

在这里插入图片描述
本教程利用Swift框架微调Qwen2.5 vl 7B模型,是用的数据集是OCR识别数据集,一共10万张图片。

3.1 环境安装

尤其注意cuda版本,否则有些包安装不了
在这里插入图片描述

conda create -n swift3 python==3.10
# flash-attn对cuda版本有要求
# pip install flash-attn
CFLAGS="-lineinfo" pip install flash-attn --no-build-isolation
pip install auto_gptq optimum bitsandbytes timm
git clone https://github.com/modelscope/ms-swift.git
cd ms-swift
pip install -e .

# 如果有需要,安装vllm ,对cuda版本有要求
pip install vllm

## 如果是qwen2.5-vl
pip install git+https://github.com/huggingface/transformers.git@9985d06add07a4cc691dc54a7e34f54205c04d40
pip install qwen_vl_utils

3.2 数据准备

import os
import json


# 写入jsonl文件
def write_jsonl(data_list, filename):
    with open(filename, 'w', encoding='utf-8') as f:
        for item in data_list:
            # 将Python对象转换为JSON格式的字符串
            json_str = json.dumps(item, ensure_ascii=False)  
            f.write(json_str + '\n')


if __name__ == "__main__":
    img_dir = "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages"
    with open("LabelTrain.txt", "r") as f:
        data_list = []
        lines = f.readlines()
        for line in lines[:90000]:
            img_name, text = line.rstrip().split("\t")
            img_path = os.path.join(img_dir, img_name)
            data = {}
            data["query"] = "请识别图片中的文字"
            data["response"] = text
            data["image_path"] = img_path
            data_list.append(data)
        write_jsonl(data_list, "train.jsonl")

        data_list = []
        for line in lines[90000:]:
            img_name, text = line.rstrip().split("\t")
            img_path = os.path.join(img_dir, img_name)
            data = {}
            data["query"] = "请识别图片中的文字"
            data["response"] = text
            data["image_path"] = img_path
            data_list.append(data)
        write_jsonl(data_list, "val.jsonl")
        print("done")

处理后的数据如下,示例:

{"query": "请识别图片中的文字", "response": "在2日内到有效", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090008.jpg"}
{"query": "请识别图片中的文字", "response": "车服务公司", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090009.jpg"}
{"query": "请识别图片中的文字", "response": "宗派排次", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090010.jpg"}
{"query": "请识别图片中的文字", "response": "增加金属蛋白酶,有助于异位组织的侵蚀", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090011.jpg"}
{"query": "请识别图片中的文字", "response": "学历要求", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090012.jpg"}
{"query": "请识别图片中的文字", "response": "防御", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090013.jpg"}
{"query": "请识别图片中的文字", "response": "等:¥476.0", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090014.jpg"}
{"query": "请识别图片中的文字", "response": "余443张", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090015.jpg"}
{"query": "请识别图片中的文字", "response": "中国", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090016.jpg"}
{"query": "请识别图片中的文字", "response": "般10%以下", "image_path": "/home/kas/guopei/dataset/ocr_reg_small_dataset/data/TrainImages/Train_090017.jpg"}

3.3 模型微调

MAX_PIXELS=1003520 \
CUDA_VISIBLE_DEVICES=0 \
swift sft \
    --model Qwen/Qwen2.5-VL-7B-Instruct \
    --dataset /home/kas/guopei/dataset/ocr_reg_small_dataset/data/train.jsonl \
    --train_type lora \
    --torch_dtype bfloat16 \
    --num_train_epochs 1 \
    --per_device_train_batch_size 1 \
    --per_device_eval_batch_size 1 \
    --learning_rate 1e-4 \
    --lora_rank 8 \
    --lora_alpha 32 \
    --target_modules all-linear \
    --freeze_vit true \
    --gradient_accumulation_steps 16 \
    --eval_steps 50 \
    --save_steps 50 \
    --save_total_limit 5 \
    --logging_steps 5 \
    --max_length 2048 \
    --output_dir output_ocr \
    --warmup_ratio 0.05 \
    --dataloader_num_workers 4

模型训练中,正常收敛,如下图所示,内存占用18G左右:

{'loss': 4.36318054, 'token_acc': 0.32325581, 'grad_norm': 5.01398468, 'learning_rate': 9.725e-05, 'memory(GiB)': 16.51, 'train_speed(iter/s)': 0.178871, 'epoch': 0.15, 'global_step/max_steps': '840/5568', 'percentage': '15.09%', 'elapsed_time': '1h 18m 15s', 'remaining_time': '7h 20m 31s'}
{'loss': 4.03473396, 'token_acc': 0.34009009, 'grad_norm': 4.07742596, 'learning_rate': 9.72e-05, 'memory(GiB)': 16.51, 'train_speed(iter/s)': 0.179055, 'epoch': 0.15, 'global_step/max_steps': '845/5568', 'percentage': '15.18%', 'elapsed_time': '1h 18m 38s', 'remaining_time': '7h 19m 35s'}
{'loss': 4.13988152, 'token_acc': 0.3490566, 'grad_norm': 3.48686051, 'learning_rate': 9.715e-05, 'memory(GiB)': 16.51, 'train_speed(iter/s)': 0.179242, 'epoch': 0.15, 'global_step/max_steps': '850/5568', 'percentage': '15.27%', 'elapsed_time': '1h 19m 1s', 'remaining_time': '7h 18m 40s'}
Train:  15%|███████████████████████▏                                                                                                                                | 850/5568 [1:19:01<5:59:45,  4.58s/it]

将训练好的模型与loar融合

# checkpoint-5568-merged  融合会生成这样一个文件夹,和Qwen2.5-vl-7b的使用方式完全相同 
# 这里`--adapters`需要替换生成训练生成的最后一个检查点文件夹。 由于adapters文件夹中包含了训练的参数文件因此,不需要额外指定`--model`:
CUDA_VISIBLE_DEVICES=0 swift export \
      --adapters  ./output_ocr/vx-xxx/checkpoint-5568 \
      --merge_lora true           

3.4 模型测试

# pt推理
NPROC_PER_NODE=1 MAX_PIXELS=1003520 swift infer \
    --ckpt_dir ./output_ocr/vx-xxx/checkpoint-5568-merged \
    --max_new_tokens 300 \
    --temperature 0 \
    --val_dataset val_dataset.jsonl \
    --result_path output_5568.jsonl \
    --max_batch_size 1 \

4. 强化学习从理论到实践

学习目标:

  1. 简单介绍强化学习的基本理论;
  2. 用自己的数据跑通强化学习在多模态大模型上的应用;
  3. 用一个实际的例子给大家展示,利用强化学习微调Qwen2.5 VL多模态模型,用于OCR识别或者信息抽取。

4.1 强化学习简介

强化学习 (RL) 是一种机器学习范式,其中智能体通过与环境交互来学习最优行为,以最大化累积奖励 。与监督学习不同,强化学习不是从显式标签中学习,而是通过反馈(奖励)来指导学习过程。这种学习方式的核心在于通过试错来发现最佳策略,这与监督学习中直接将输入映射到输出有着本质的区别。策略优化是强化学习中的一类算法,其直接目标是找到智能体在环境中采取行动的最佳策略 。策略代表了智能体在给定状态下选择行动的方式,它定义了从状态到行动的映射 。策略优化的重点在于如何行动,而不是评估状态或行动的价值,这与基于价值的方法形成了对比。在基于价值的方法中,智能体学习的是状态或行动的价值,然后根据这些价值来选择行动;而策略优化则直接调整智能体的决策过程,使其能够采取更优的行动。
策略优化在训练大型语言模型 (LLM) 执行复杂的任务(如推理和代码生成)方面发挥着关键作用 。强化学习被用于微调大型语言模型,使其能够更好地符合人类偏好,并提升在特定任务上的性能 。尽管大型语言模型功能强大,但它们在经过初始的预训练和监督微调后,可能仍然无法完全满足特定的用户偏好或复杂的任务要求。强化学习提供了一种机制,可以超越这些初始阶段,根据期望的结果进一步优化语言模型的行为。通过强化学习,可以引导语言模型生成更符合人类价值观、更准确、更具创造性的文本。因此,强化学习在提升大型语言模型的实用性和可靠性方面扮演着至关重要的角色。
DeepseekR1的推理能力,是从强化学习GRPO进行冷启动的,没有采用大量的标注数据,直接通过强化学习就让模型有了很强的推理能力。那GRPO到底是个什么呢。如下图所示, 在传统的近端策略优化算法(PPO)中,通常需要同时训练策略模型和价值模型,后者用于估计每个状态的期望回报,并以此作为优势函数的基线。对于大型语言模型来说,训练与策略模型规模相当的价值网络不仅增加了计算量,还会带来显著的内存开销。为了解决这一问题,GRPO 提出了利用“组内”生成数据的思路:
在这里插入图片描述
多样本生成: 对于每个输入(例如一个问题),模型根据旧策略生成多个候选输出。
奖励评估: 对每个候选输出采用特定的奖励函数进行评估,奖励可以包括答案正确性、格式符合要求、推理过程合理等指标(例如 DeepSeek 系列中常用的准确性奖励和格式奖励)
组内优势计算: 将这组输出的奖励视为一个样本集,直接计算其均值和标准差,并将每个输出的奖励进行标准化(即减去均值、除以标准差),从而获得组内相对优势。这种方式能够反映出同一问题下各个候选答案的“相对好坏”,而不需要单独训练一个价值模型。
其优势省去价值网络,占用资源少,同时训练稳定性较PPO要高。

4.2 环境配置,模型下载,数据准备

与Swifts实战中完全相同,只是模型用Qwen2.5-VL-3B

4.3 注册自己的数据集

路径:./ms-swift/swift/llm/dataset/dataset/llm.py

# 在代码末尾添加,数据路径最好写绝对路径确保不会出问题
class MyDataPreprocessor(ResponsePreprocessor):

    def preprocess(self, row: Dict[str, Any]) -> Dict[str, Any]:
        query = row.get('query', '')
        solution = row.get('response', '')
        query = (f'{query} Output the thinking process in <think> </think> and '
                 'final answer (number) in <answer> </answer> tags.')
        row.update({'query': query, "solution": solution})
        return super().preprocess(row)


register_dataset(
    DatasetMeta(
        dataset_path="/home/kas/guopei/dataset/ocr_reg_small_dataset/data/train.jsonl",
        preprocess_func=MyDataPreprocessor(),
        tags=['qa', 'math', 'vision', 'grpo']))
  • dataset_path:设置自己本地的数据路径。
  • MyDataPreprocessor:表示对自己数据集的每一行(每一条)做预处理。

4.4 修改损失奖励函数

路径:./ms-swift/examples/train/grpo/plugin/plugin.py

# 放在代码末尾,内容可以参考其他代码。
class MyAccuracy(ORM):
    def __call__(self, completions, solution,**kwargs) -> List[float]:
        rewards = xxxxx
        return rewards
orms['external_my_acc'] = MyAccuracy

注意:completions和solution是list。

下面是我写的奖励函数,是基于deepseek R1奖励函数的改进版本,专门用来训练OCR的

# 放在代码末尾
class MultiModalAccuracyORMGuopei(ORM):

    def __init__(self, important_chars="0123456789¥$", important_weight=2.0):
        """
        初始化奖励计算类
        
        Args:
            important_chars (str): 重要字符集合,错误会被加权惩罚
            important_weight (float): 重要字符的错误权重
        """
        self.important_chars = set(important_chars)
        self.important_weight = important_weight
    
    def calculate_cer(self, reference, hypothesis):
        """
        计算字符错误率(Character Error Rate)
        
        Args:
            reference (str): 参考文本
            hypothesis (str): 预测文本
            
        Returns:
            float: 字符错误率
        """
        if not reference:
            return 1.0 if hypothesis else 0.0
        
        # 创建编辑距离矩阵
        dp = [[0] * (len(hypothesis) + 1) for _ in range(len(reference) + 1)]
        
        # 初始化第一行和第一列
        for i in range(len(reference) + 1):
            dp[i][0] = i
        for j in range(len(hypothesis) + 1):
            dp[0][j] = j
            
        # 计算编辑距离,考虑重要字符的加权
        operations = []  # 存储每个操作:(操作类型, 字符, 位置)
        
        for i in range(1, len(reference) + 1):
            for j in range(1, len(hypothesis) + 1):
                if reference[i-1] == hypothesis[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    # 找出最小操作
                    deletion = dp[i-1][j] + 1
                    insertion = dp[i][j-1] + 1
                    substitution = dp[i-1][j-1] + 1
                    
                    min_op = min(deletion, insertion, substitution)
                    dp[i][j] = min_op
                    
                    # 记录操作类型
                    if min_op == deletion:
                        operations.append(("delete", reference[i-1], i-1))
                    elif min_op == insertion:
                        operations.append(("insert", hypothesis[j-1], j-1))
                    else:
                        operations.append(("substitute", reference[i-1], i-1))
        
        # 计算加权错误
        weighted_errors = 0
        for op, char, _ in operations:
            if char in self.important_chars:
                weighted_errors += self.important_weight
            else:
                weighted_errors += 1
                
        # 计算加权错误率
        total_chars = len(reference)
        return weighted_errors / total_chars if total_chars > 0 else 0.0
        
    def extract_confidence(self, content):
        """
        从内容中提取置信度信息
        
        Args:
            content (str): 可能包含置信度信息的文本
            
        Returns:
            float: 平均置信度,默认为1.0
        """
        # 尝试从JSON格式中提取置信度
        try:
            # 查找JSON格式的置信度信息
            confidence_pattern = r'"confidence"\s*:\s*(\d+\.\d+|\d+)'
            confidence_matches = re.findall(confidence_pattern, content)
            
            if confidence_matches:
                # 计算平均置信度
                confidences = [float(match) for match in confidence_matches]
                return sum(confidences) / len(confidences)
                
            # 查找直接的置信度表示,如 "置信度: 0.95" 或 "confidence: 0.95"
            alt_pattern = r'(?:置信度|confidence)\s*(?::|:)\s*(\d+\.\d+|\d+)'
            alt_matches = re.findall(alt_pattern, content, re.IGNORECASE)
            
            if alt_matches:
                confidences = [float(match) for match in alt_matches]
                return sum(confidences) / len(confidences)
        except Exception:
            pass
            
        # 默认返回1.0(完全置信)
        return 1.0

    def __call__(self, completions, solution, **kwargs) -> List[float]:
        """
        奖励函数,检查完成度并计算奖励分数
        
        Args:
            completions (list[str]): 生成的输出
            solution (list[str]): 标准答案
            
        Returns:
            list[float]: 奖励分数列表
        """
        rewards = []
        from math_verify import parse, verify
        
        for content, sol in zip(completions, solution):
            reward = 0.0
            
            # 尝试符号验证(适用于数学表达式)
            try:
                answer = parse(content)
                if float(verify(answer, parse(sol))) > 0:
                    reward = 1.0
            except Exception:
                pass  # 如果符号验证失败,继续下一种验证方法
                
            # 如果符号验证失败,尝试基于CER的评估
            if reward == 0.0:
                try:
                    # 从答案和生成内容中提取文本
                    sol_match = re.search(r'<answer>(.*?)</answer>', sol)
                    ground_truth = sol_match.group(1).strip() if sol_match else sol.strip()
                    
                    content_match = re.search(r'<answer>(.*?)</answer>', content)
                    student_answer = content_match.group(1).strip() if content_match else content.strip()
                    
                    # 提取置信度信息
                    confidence = self.extract_confidence(content)
                    
                    # 计算加权字符错误率
                    cer = self.calculate_cer(ground_truth, student_answer)
                    
                    # 计算最终奖励:正确率 * 置信度
                    reward = max(0.0, (1.0 - cer) * confidence)
                    
                except Exception as e:
                    # 保持奖励为0.0,如果出现任何错误
                    pass
                    
            rewards.append(reward)
            
        return rewards

orms['external_ocr_format'] = MultiModalAccuracyORMGuopei

4.5 训练模型

MASTER_PORT=29501 \
CUDA_VISIBLE_DEVICES=0 \
swift rlhf \
    --rlhf_type grpo \
    --model Qwen/Qwen2.5-VL-3B-Instruct \
    --external_plugins ms-swift/examples/train/grpo/plugin/plugin.py \
    --reward_funcs external_ocr_format format \
    --train_type lora \
    --use_vllm true \
    --vllm_device auto \
    --vllm_gpu_memory_utilization 0.5 \
    --vllm_max_model_len 1024 \
    --lora_rank 8 \
    --lora_alpha 32 \
    --target_modules all-linear \
    --torch_dtype bfloat16 \
    --dataset /home/kas/guopei/dataset/ocr_reg_small_dataset/data/train.jsonl \
    --max_completion_length 1024 \
    --num_train_epochs 1 \
    --per_device_train_batch_size 2 \
    --per_device_eval_batch_size 2 \
    --learning_rate 1e-5 \
    --gradient_accumulation_steps 1 \
    --eval_steps 100 \
    --save_steps 100 \
    --save_total_limit 2 \
    --logging_steps 5 \
    --max_length 2048 \
    --output_dir output_GRPO \
    --warmup_ratio 0.05 \
    --dataloader_num_workers 4 \
    --dataset_num_proc 4 \
    --num_generations 2 \
    --temperature 0.9 \
    --deepspeed zero2 \
    --system 'ms-swift/examples/train/grpo/prompt.txt' \
    --log_completions true

不出意外,你的模型就这样训练起来了,训练日志如下:
在这里插入图片描述
可视化奖励值,说明模型正常收敛。
在这里插入图片描述
在这里插入图片描述

4.6 模型融合与模型测试

模型融合

# checkpoint-xxx-merged  融合会生成这样一个文件夹,和Qwen2.5-vl-3b的使用方式完全相同 
# 这里`--adapters`需要替换生成训练生成的最后一个检查点文件夹。 由于adapters文件夹中包含了训练的参数文件因此,不需要额外指定`--model`:
CUDA_VISIBLE_DEVICES=0 swift export \
      --adapters  ./output_GRPO/vx-xxx/checkpoint-xxx \
      --merge_lora true             

模型测试

from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
import torch


class QwenVLModel:
    def __init__(self, model_path="Qwen/Qwen2.5-VL-3B-Instruct", use_flash_attention=True):
        """
        初始化Qwen VL模型
        Args:
            model_path: 模型路径
            use_flash_attention: 是否使用flash attention加速
        """
        # 加载模型
        if use_flash_attention:
            self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
                model_path,
                torch_dtype=torch.bfloat16,
                attn_implementation="flash_attention_2",
                device_map="auto",
            )
            print("use_flash_attention")
        else:
            self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
                model_path, torch_dtype="auto", device_map="auto"
            )

        # 初始化处理器
        min_pixels = 256 * 28 * 28
        max_pixels = 1280 * 28 * 28
        self.processor = AutoProcessor.from_pretrained(
            model_path, 
            min_pixels=min_pixels, 
            max_pixels=max_pixels, 
            use_fast=True
        )

    def process_image(self, image_path, prompt):
        """
        处理图片并生成输出
        Args:
            image_path: 图片路径
            prompt: 提示文本
        Returns:
            生成的文本输出
        """
        messages = [
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "image": image_path,
                    },
                    {"type": "text", "text": prompt},
                ],
            }
        ]

        # 准备推理输入
        text = self.processor.apply_chat_template(
            messages, tokenize=False, add_generation_prompt=True
        )
        image_inputs, video_inputs = process_vision_info(messages)
        inputs = self.processor(
            text=[text],
            images=image_inputs,
            videos=video_inputs,
            padding=True,
            return_tensors="pt",
        )
        inputs = inputs.to(self.model.device)

        # 生成输出
        generated_ids = self.model.generate(**inputs, max_new_tokens=512)
        generated_ids_trimmed = [
            out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
        ]
        output_text = self.processor.batch_decode(
            generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
        )
        return output_text


def jsonl_reader(jsonl_path):
    """
    读取jsonl文件并提取image_path和solution字段
    
    Args:
        jsonl_path: jsonl文件路径
    
    Returns:
        tuple: (image_paths, solutions) 两个列表,分别包含所有的image_path和solution
    """
    import json
    
    image_paths = []
    solutions = []
    
    try:
        with open(jsonl_path, 'r', encoding='utf-8') as f:
            for line in f:
                try:
                    # 解析每一行的JSON
                    data = json.loads(line.strip())
                    
                    # 提取image_path和solution字段
                    if 'image_path' in data and 'solution' in data:
                        image_paths.append(data['image_path'])
                        solutions.append(data['solution'])
                except json.JSONDecodeError:
                    # 处理非法JSON行
                    print(f"错误:无法解析以下行为JSON: {line[:50]}...")
                except KeyError as e:
                    # 处理缺少必要字段的情况
                    print(f"错误:缺少必要字段 {e} 在行: {line[:50]}...")
    except Exception as e:
        print(f"读取文件时出错: {e}")
    
    print(f"成功读取 {len(image_paths)} 条数据")
    return image_paths, solutions


def cal_edit_distance(str1, str2):
    if not str1:
        return len(str2) if str2 else 0
        
    # 创建编辑距离矩阵
    dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)]
    
    # 初始化第一行和第一列
    for i in range(len(str1) + 1):
        dp[i][0] = i
    for j in range(len(str2) + 1):
        dp[0][j] = j
        
    # 计算编辑距离
    for i in range(1, len(str1) + 1):
        for j in range(1, len(str2) + 1):
            if str1[i-1] == str2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                # 找出最小操作
                deletion = dp[i-1][j] + 1
                insertion = dp[i][j-1] + 1
                substitution = dp[i-1][j-1] + 1
                dp[i][j] = min(deletion, insertion, substitution)
    
    dis = dp[len(str1)][len(str2)]
    return dis


if __name__ == "__main__":
    # 读取JSONL文件
    val_json = "/home/xxx/xxx/workspace/swift/output_GRPO/vxxx-xxx/val_dataset.jsonl"
    image_paths, solutions = jsonl_reader(val_json)
    
    model = QwenVLModel(model_path="/home/xxx/xxx/workspace/swift/output_GRPO/vxxx-xxx/checkpoint-2800-merged")
    right_ratio = 0
    for i in range(200):
        image_path, label = image_paths[i], solutions[i]
        output_text = model.process_image(image_path, "请识别图片中的文字, 不需要输出无关内容,例如:“图片中的文字是”")
        output_text = output_text[0]
        print(output_text, label)
        dis = cal_edit_distance(output_text, label)
        ratio = dis*1.0/max(len(output_text), len(label))
        ratio = 1 - ratio
        print(ratio)
        right_ratio += ratio

    print("text_acc:", right_ratio*1.0/200)

代码采用平均编辑距离作为指标进行评价,模型经过微调后有5%的提升。

推荐阅读上一篇:视觉-语言大模型VLM理论——保姆级教程

如果喜欢,可以关注作者的微信公众号:AIWorkshopLab

<think>嗯,用户想了解视觉语言大模型VLM)的学习路径和资源。首先,我需要整理现有的引用内容,看看里面提到哪些关键点。引用1提到了PyTorch构建VLM教程和视频资源,引用2讨论了链式推理在VLM中的应用,引用3则详细说明了VLM的预训练策略、训练阶段和数据需求。这些应该是回答的基础。 接下来,用户需要的是学习路线,包括书籍、教程、课程等资源。根据引用3的结构,可能应该分阶段来规划学习路径,比如先基础知识,再进阶学习,然后实践和深入研究。需要确保每个阶段都有对应的资源推荐,比如书籍推荐《深度学习入门》和《计算机视觉》,课程可能包括李飞飞的CS231n,或者吴恩达的深度学习课程。 然后,资源推荐部分要涵盖开源项目,比如CLIP、BLIP,以及PyTorch和Hugging Face库,这些都是构建VLM常用的工具。此外,用户提到的引用1中的视频教程应该被包含进去,作为专门整理的学习资源。 还要考虑用户可能的背景,是否需要先补充基础知识,比如深度学习、计算机视觉和自然语言处理。然后进阶到VLM的具体内容,比如模型架构、预训练策略,再到应用和优化技巧。最后,实践部分可能需要推荐数据集和实战项目,帮助用户巩固知识。 另外,用户的问题中生成的相关问题需要覆盖VLM的核心技术、优化方法、应用场景以及学习难点,这样他们可以进一步探索。要确保引用正确,每个引用对应到回答中的相应部分,比如引用3提到的预训练阶段,在回答中要标注引用标识。 可能还需要检查是否有其他资源未被引用但常用的,比如论文、GitHub项目,但根据给定的引用,可能需要限制在已有信息内。最后,确保回答结构清晰,分阶段和模块,便于用户逐步学习。</think>以下是针对视觉语言大模型VLM)的系统化学习路径规划与资源推荐: ### 一、基础知识准备阶段 1. **深度学习基础** - 必学内容:神经网络基础、反向传播、CNN/RNN/Transformer架构 - 推荐资源: - 图书:《深度学习入门:基于Python的理论与实现》[^3] - 课程:吴恩达《深度学习专项课程》(Coursera) 2. **计算机视觉基础** - 重点掌握:图像特征提取、目标检测、视觉表征学习 - 推荐实践:PyTorch框架下的图像分类项目[^1] 3. **自然语言处理基础** - 核心知识点:词嵌入、注意力机制、生成式模型 - 推荐课程:斯坦福CS224n《自然语言处理与深度学习》 ### 二、VLM核心技术进阶 1. **模型架构专题** - 关键组件学习: - 视觉编码器:CLIP/SigLIP实现原理 - 语言解码器:Llama/GPT结构解析 - 跨模态投影器:向量空间对齐技术 - 推荐论文:《Learning Transferable Visual Models From Natural Language Supervision》 2. **训练策略精讲** ```python # 典型的三阶段训练代码结构示例 class VLM(nn.Module): def __init__(self): self.vision_encoder = FrozenCLIP() # 冻结的视觉编码器 self.text_decoder = LlamaForCausalLM() self.projection = LearnableProjector() # 可训练投影器 ``` - 阶段1:仅训练投影器(1-2周) - 阶段2:联合微调解码器(3-4周) - 阶段3:指令微调(2-3周) 3. **链式推理强化** - 重点突破:CoT(Chain-of-Thought)在跨模态任务中的应用[^2] - 案例实践:VQA任务中的分步推理实现 ### 三、优质学习资源导航 | 资源类型 | 推荐内容 | 特点说明 | |---------|---------|---------| | **视频教程** | [VLM全栈开发教程](引用1) | 含完整路线图与项目实战 | | **开源项目** | CLIP/BLIP代码库 | 官方实现+社区改进版 | | **论文合集** | arXiv最新论文集 | 跟踪SOTA模型进展 | | **实践平台** | Kaggle VLM竞赛 | 真实场景数据挑战 | ### 四、实战提升路径 1. **基础实验** - 使用HuggingFace库复现BLIP模型 - 在COCO数据集上完成图文匹配任务 2. **进阶挑战** - 实现自定义投影器模块 - 探索多模态指令微调策略[^3] 3. **创新方向** - 改进跨模态注意力机制 - 设计新型链式推理框架 ### 五、常见学习误区提醒 1. **不要跳过预训练阶段**:直接微调现成模型会导致基础不牢 2. **警惕数据泄露**:注意训练/验证集划分比例 3. **硬件准备建议**:至少需要16GB显存的GPU进行中等规模实验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值