从零开始之三,在AutoDL上微调Qwen2.5-VL-7B

说在前面

本章与参考教程差异:

  • 我用了两张vGPU-32GB,参考教程为一张,为了速度快些,减少爆显存的情况。

  • 脚本从Qwen2改成了Qwen2.5的代码了

  • 调整了执行步骤

  • 增加了一些命令的描述,新手友好

  • 修改swanlab安装命令,原教程中的版本已经没有了。

如果具备以下条件,就可以达成标题的目标了:

  • 需具备的环境,不确定的可以先通关:从零开始,基于Autodl云服务器,通过网页与Qwen2.5-vl-7b-instruct对话-CSDN博客

    • 基础镜像:PyTorch/2.3.0/3.12(ubuntu22.04)/12.1    #在创建服务器时选择

    • 基础模型:Qwen2.5-VL-7B-Instruct                               # 通过 魔搭社区 下载后上传至服务器指定目录

    • 基础项目:Qwen2-VL                                                     # 通过https://github.com/QwenLM/Qwen2-VL  下载后上传至服务器指定目录(需要科学上网,服务器不可以,建议先下载到本地再上传)

    • 其他Python 包

pip install transformers                                    #Hugging Face Transformers 库,用于加载和使用各种预训练的 Transformer 模型         
pip install 'accelerate>=0.26.0'                            #Hugging Face 提供的accelerate工具,用于在多 GPU 或分布式设置下更高效地训练你的模型,并提供很多开箱即用的优化和加速功能。  
pip install qwen-vl-utils[decord]                           #用于视觉-语言任务的工具库,支持视频数据的处理和转换同时安装了 Decord 扩展,ecord 是一个高效的视频解码库。

  • AutoDL需要充值50块钱,才能升降配置,实际使用5块钱以内可以搞定。

  • 半天时间

————————————————

目录

配置环境与安装包

准备数据集

完成此步骤将得到

准备获取图片与图片描述CSV清单的脚本

执行获取图片与图片描述CSV清单的脚本

准备记录图片路径与描述的json格式的脚本

执行记录图片路径与描述的json格式的脚本

准备微调脚本

执行微调

监控显存

处理可能的报错


配置环境与安装包

如下图所示,使用无卡开机模式终端上安装必要的python包,

执行命令:

pip install sentencepiece==0.2.0 #用于安装 SentencePiece 库,这是一个由 Google 开发的 文本预处理 库,常用于 自然语言处理(NLP) 任务中的 分词(tokenization)。
pip install datasets==2.18.0 #Datasets 是一个开源库,旨在简化和加速 自然语言处理(NLP) 和其他机器学习任务中的数据处理。它提供了一个方便的接口来加载、共享和使用各种公共数据集。
pip install peft==0.13.2 #用于安装 PEFT 库,PEFT 是 Parameter-Efficient Fine-Tuning 的缩写,主要用于 高效微调(Fine-Tuning)大型预训练模型,尤其是在计算资源有限的情况下。PEFT 库通过引入一些策略来减少微调时需要更新的模型参数数量,从而节省显存和计算开销。
pip install swanlab # 开源的模型训练记录工具,常被称为"中国版 Weights&Biases + Tensorboard"。SwanLab面向AI研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。

准备数据集

完成此步骤将得到

/root/autodl-tmp/Qwen/Qwen2.5-VL/coco_2014_caption 目录,且里面有很多图片

/root/autodl-tmp/Qwen/Qwen2.5-VL/coco-2024-dataset.csv 文件,里面有图片路径和图片描述

/root/autodl-tmp/Qwen/Qwen2.5-VL/data_vl.json 文件,json格式的图片路径和图片描述

准备获取图片与图片描述CSV清单的脚本

如图下图所示,在/root/autodl-tmp/Qwen/Qwen2.5-VL 目录下创建data2csv.py 文件,

命令如下:

cd /root/autodl-tmp/Qwen/Qwen2.5-VL
touch data2csv.py

如下图所示,双击data2csv.py打开编辑界面,并复制脚本代码后保存

data2csv.py脚本代码如下:

# 导入所需的库

from modelscope.msdatasets import MsDataset

import os

import pandas as pd



MAX_DATA_NUMBER = 1000      # 设置下载图片的数量



# 检查目录是否已存在

if not os.path.exists('coco_2014_caption'):

    # 从modelscope下载COCO 2014图像描述数据集

    ds =  MsDataset.load('modelscope/coco_2014_caption', subset_name='coco_2014_caption', split='train')

    print(len(ds))

    # 设置处理的图片数量上限

    total = min(MAX_DATA_NUMBER, len(ds))



    # 创建保存图片的目录

    os.makedirs('coco_2014_caption', exist_ok=True)



    # 初始化存储图片路径和描述的列表

    image_paths = []

    captions = []



    for i in range(total):

        # 获取每个样本的信息

        item = ds[i]

        image_id = item['image_id']

        caption = item['caption']

        image = item['image']

        

        # 保存图片并记录路径

        image_path = os.path.abspath(f'coco_2014_caption/{image_id}.jpg')

        image.save(image_path)

        

        # 将路径和描述添加到列表中

        image_paths.append(image_path)

        captions.append(caption)

        

        # 每处理50张图片打印一次进度

        if (i + 1) % 50 == 0:

            print(f'Processing {i+1}/{total} images ({(i+1)/total*100:.1f}%)')



    # 将图片路径和描述保存为CSV文件

    df = pd.DataFrame({

        'image_path': image_paths,

        'caption': captions

    })

    

    # 将数据保存为CSV文件

    df.to_csv('./coco-2024-dataset.csv', index=False)

    

    print(f'数据处理完成,共处理了{total}张图片')



else:

    print('coco_2014_caption目录已存在,跳过数据处理步骤')

执行获取图片与图片描述CSV清单的脚本

如下图所示,在终端中执行命令:

pip install addict   #提供了一种简单的方式来处理嵌套字典,可以通过属性访问的方式更加直观地操作字典数据

  • 如下图所示,在终端中执行脚本:

python data2csv.py

准备记录图片路径与描述的json格式的脚本

在/root/autodl-tmp/Qwen/Qwen2.5-VL 目录下创建csv2json.py 文件,

命令如下:

cd /root/autodl-tmp/Qwen/Qwen2.5-VL
touch csv2json.py

将以下代码覆盖到csv2json.py 文件中

import pandas as pd

import json



# 载入CSV文件

df = pd.read_csv('./coco-2024-dataset.csv')

conversations = []



# 添加对话数据

for i in range(len(df)):

    conversations.append({

        "id": f"identity_{i+1}",

        "conversations": [

            {

                "from": "user",

                "value": f"COCO Yes: <|vision_start|>{df.iloc[i]['image_path']}<|vision_end|>"

            },

            {

                "from": "assistant",

                "value": df.iloc[i]['caption']

            }

        ]

    })



# 保存为Json

with open('data_vl.json', 'w', encoding='utf-8') as f:

    json.dump(conversations, f, ensure_ascii=False, indent=2)

执行记录图片路径与描述的json格式的脚本

如下图所示,在终端中执行脚本:

python csv2json.py

准备微调脚本

在/root/autodl-tmp/Qwen/Qwen2.5-VL 目录下创建tran.py 文件,

命令如下:

cd /root/autodl-tmp/Qwen/Qwen2.5-VL
touch tran.py

将以下代码覆盖到tran.py 文件中

import torch

from datasets import Dataset

from modelscope import snapshot_download, AutoTokenizer

from swanlab.integration.transformers import SwanLabCallback

from qwen_vl_utils import process_vision_info

from peft import LoraConfig, TaskType, get_peft_model, PeftModel

from transformers import (

    TrainingArguments,

    Trainer,

    DataCollatorForSeq2Seq,

    Qwen2_5_VLForConditionalGeneration,

    AutoProcessor,

)

import swanlab

import json





def process_func(example):

    """

    将数据集进行预处理

    """

    MAX_LENGTH = 8192

    input_ids, attention_mask, labels = [], [], []

    conversation = example["conversations"]

    input_content = conversation[0]["value"]

    output_content = conversation[1]["value"]

    file_path = input_content.split("<|vision_start|>")[1].split("<|vision_end|>")[0]  # 获取图像路径

    messages = [

        {

            "role": "user",

            "content": [

                {

                    "type": "image",

                    "image": f"{file_path}",

                    "resized_height": 280,

                    "resized_width": 280,

                },

                {"type": "text", "text": "COCO Yes:"},

            ],

        }

    ]

    text = processor.apply_chat_template(

        messages, tokenize=False, add_generation_prompt=True

    )  # 获取文本

    image_inputs, video_inputs = process_vision_info(messages)  # 获取数据数据(预处理过)

    inputs = processor(

        text=[text],

        images=image_inputs,

        videos=video_inputs,

        padding=True,

        return_tensors="pt",

    )

    inputs = {key: value.tolist() for key, value in inputs.items()} #tensor -> list,为了方便拼接

    instruction = inputs



    response = tokenizer(f"{output_content}", add_special_tokens=False)





    input_ids = (

            instruction["input_ids"][0] + response["input_ids"] + [tokenizer.pad_token_id]

    )



    attention_mask = instruction["attention_mask"][0] + response["attention_mask"] + [1]

    labels = (

            [-100] * len(instruction["input_ids"][0])

            + response["input_ids"]

            + [tokenizer.pad_token_id]

    )

    if len(input_ids) > MAX_LENGTH:  # 做一个截断

        input_ids = input_ids[:MAX_LENGTH]

        attention_mask = attention_mask[:MAX_LENGTH]

        labels = labels[:MAX_LENGTH]



    input_ids = torch.tensor(input_ids)

    attention_mask = torch.tensor(attention_mask)

    labels = torch.tensor(labels)

    inputs['pixel_values'] = torch.tensor(inputs['pixel_values'])

    inputs['image_grid_thw'] = torch.tensor(inputs['image_grid_thw']).squeeze(0)  #由(1,h,w)变换为(h,w)

    return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels,

            "pixel_values": inputs['pixel_values'], "image_grid_thw": inputs['image_grid_thw']}





def predict(messages, model):

    # 准备推理

    text = processor.apply_chat_template(

        messages, tokenize=False, add_generation_prompt=True

    )

    image_inputs, video_inputs = process_vision_info(messages)

    inputs = processor(

        text=[text],

        images=image_inputs,

        videos=video_inputs,

        padding=True,

        return_tensors="pt",

    )

    inputs = inputs.to("cuda")



    # 生成输出

    generated_ids = model.generate(**inputs, max_new_tokens=128)

    generated_ids_trimmed = [

        out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)

    ]

    output_text = processor.batch_decode(

        generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False

    )

    

    return output_text[0]





# 在modelscope上下载Qwen2-VL模型到本地目录下

#model_dir = snapshot_download("Qwen/Qwen2-VL-2B-Instruct", cache_dir="./", revision="master")



# 使用Transformers加载模型权重

tokenizer = AutoTokenizer.from_pretrained("/root/autodl-tmp/Qwen/Qwen2.5-VL-7B-Instruct/", use_fast=False, trust_remote_code=True)

processor = AutoProcessor.from_pretrained("/root/autodl-tmp/Qwen/Qwen2.5-VL-7B-Instruct/")



model = Qwen2_5_VLForConditionalGeneration.from_pretrained("/root/autodl-tmp/Qwen/Qwen2.5-VL-7B-Instruct/", device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True,)

model.enable_input_require_grads()  # 开启梯度检查点时,要执行该方法



# 处理数据集:读取json文件

# 拆分成训练集和测试集,保存为data_vl_train.json和data_vl_test.json

train_json_path = "data_vl.json"

with open(train_json_path, 'r') as f:

    data = json.load(f)

    train_data = data[:-12]

    test_data = data[-12:]



with open("data_vl_train.json", "w") as f:

    json.dump(train_data, f)



with open("data_vl_test.json", "w") as f:

    json.dump(test_data, f)



train_ds = Dataset.from_json("data_vl_train.json")

train_dataset = train_ds.map(process_func)



# 配置LoRA

config = LoraConfig(

    task_type=TaskType.CAUSAL_LM,

    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],

    inference_mode=False,  # 训练模式

    r=64,  # Lora 秩

    lora_alpha=16,  # Lora alaph,具体作用参见 Lora 原理

    lora_dropout=0.05,  # Dropout 比例

    bias="none",

)



# 获取LoRA模型

peft_model = get_peft_model(model, config)



# 配置训练参数

args = TrainingArguments(

    output_dir="./output/Qwen2_5-VL-7B",

    per_device_train_batch_size=4,

    gradient_accumulation_steps=4,

    logging_steps=10,

    logging_first_step=5,

    num_train_epochs=3,

    save_steps=100,

    learning_rate=1e-4,

    save_on_each_node=True,

    gradient_checkpointing=True,

    report_to="none",

)

        

# 设置SwanLab回调

swanlab_callback = SwanLabCallback(

    project="Qwen2_5-VL-finetune",

    experiment_name="qwen2_5-vl-coco2014",

    config={

        "model": "https://modelscope.cn/models/Qwen/Qwen2.5-VL-7B-Instruct",

        "dataset": "https://modelscope.cn/datasets/modelscope/coco_2014_caption/quickstart",

        "github": "https://github.com/datawhalechina/self-llm",

        "prompt": "COCO Yes: ",

        "train_data_number": len(train_data),

        "lora_rank": 64,

        "lora_alpha": 16,

        "lora_dropout": 0.1,

    },

)



# 配置Trainer

trainer = Trainer(

    model=peft_model,

    args=args,

    train_dataset=train_dataset,

    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),

    callbacks=[swanlab_callback],

)



# 开启模型训练

trainer.train()



# ====================测试模式===================

# 配置测试参数

val_config = LoraConfig(

    task_type=TaskType.CAUSAL_LM,

    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],

    inference_mode=True,  # 训练模式

    r=64,  # Lora 秩

    lora_alpha=16,  # Lora alaph,具体作用参见 Lora 原理

    lora_dropout=0.05,  # Dropout 比例

    bias="none",

)



# 获取测试模型

val_peft_model = PeftModel.from_pretrained(model, model_id="./output/Qwen2_5-VL-7B/checkpoint-90", config=val_config)



# 读取测试数据

with open("data_vl_test.json", "r") as f:

    test_dataset = json.load(f)



test_image_list = []

for item in test_dataset:

    input_image_prompt = item["conversations"][0]["value"]

    # 去掉前后的<|vision_start|>和<|vision_end|>

    origin_image_path = input_image_prompt.split("<|vision_start|>")[1].split("<|vision_end|>")[0]

    

    messages = [{

        "role": "user",

        "content": [

            {

            "type": "image",

            "image": origin_image_path

            },

            {

            "type": "text",

            "text": "COCO Yes:"

            }

        ]}]

    

    response = predict(messages, val_peft_model)

    messages.append({"role": "assistant", "content": f"{response}"})

    print(messages[-1])



    test_image_list.append(swanlab.Image(origin_image_path, caption=response))



swanlab.log({"Prediction": test_image_list})



# 在Jupyter Notebook中运行时要停止SwanLab记录,需要调用swanlab.finish()

swanlab.finish()

执行微调

切换到有卡模式后,如下图所示,执行脚本:

python tran.py

大约等待20分钟

监控显存

如下图所示,执行时可以打开另一个终端监控显存,执行命令如下:

watch -n 1 nvidia-smi #每秒更新一次显卡的状态,显示显存使用情况

处理可能的报错

大约20分钟后报错了,如下图所示,需要修改tran.py脚本中第207行的路径

如下图所示,重新执行脚本:

python tran.py

Swanlab官网:https://swanlab.cn/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值