Qwen2-VL OCR能力微调与量化

最近,我对 Qwen2-VL-2B 进行了微调,这是一个多模态 LLM,这意味着它可以分析文本和图像。我的目标是使用此模型从图像中提取所需的信息 (OCR)。本博客将涵盖所有内容,包括我如何创建图像数据集(标记和格式化)、训练模型、对其进行量化以及评估模型。

在这一部分中,我仅专注于准备自定义数据集以微调 Qwen2-VL 模型。

1、为什么选择 Qwen2-VL?

许多文章都介绍了 Qwen2-VL 的深入工作原理,因此我不打算在此处包含这些详细信息。

我选择 Qwen2-VL 进行 OCR 主要是因为其增强的图像理解能力(包括视频理解)和 2B 的参数大小(7B 和 72B 也可用),支持我打算在生产中使用的 Nvidia GPU(稍后会详细介绍)。此外,与其他多模型 LLM 相比,基准数字令人印象深刻。

https://arxiv.org/pdf/2410.16261

你可以查看这个博客以了解有关 Qwen2-VL 的更多信息。

我还在这个数据集上对 PaddleOCR-v4 进行了微调。在比较了 PaddleOCR-v4 和 Qwen2-VL 的微调模型的结果后,我得出结论,基于 LLM 的方法在我的用例中实现了更高的准确度。

我还比较了我们的 Qwen2-VL-2B 模型与 Azure Document Intelligence OCR 的结果,我必须说,结果几乎相同,甚至更好。

但是,我还想微调 stepfun-ai/GOT-OCR2_0 并将结果与​​ Qwen2-VL 进行比较。稍后会这样做。

2、自定义数据集准备

关于使用自己的数据专门为 LLM 训练创建自定义训练数据集的资源很少。 在花费大量时间完成此过程后,我决定在此博客中提供全面、深入的指南。

我主要使用 Linux 对模型进行微调、量化和推理,因为尚未提供稳定的 Windows 支持。 但是,如果我在 Windows 上成功构建它,我会更新此博客。

我首先收集了大约 3,000 张图像并对其进行标记。我的主要目标是从车辆铭牌图像中提取型号、车辆序列号和发动机号,从底盘图像中提取底盘号。

以下是示例图像及其 OCR 标签格式。

车辆铭牌图像:

“vinplate.jpg”: 
{
"Vehicle Sr No": "MA1TA2YS2R2A13882",
"Engine No": "YSR4A38798",
"Model": "SCORPIO CLASSIC S5 MT 7S"
}

底盘图像:

"chassis.jpg": 
{
"Vehicle Sr No": "MA1TA2YS2R2A17264",
"Engine No": null,
"Model": null
}

最后,在合并所有标签后,最终的 JSON 文件将如下所示:

我们将此 JSON 文件命名为 combined-ocr-json.json

这并不是微调 Qwen2-VL 模型所需的格式。我们仍然需要将其转换为 Qwen2-VL 所需的格式。我使用 Llama-Factory 进行数据集准备和模型微调。此处的存储库中提到了所需的格式,我们需要创建类似的格式。

我们模型中单个图像数据条目的输入格式如下:

{
    "messages": [
        {
            "content": "<image>Can you find and provide the Vehicle Sr No, Engine No, and Model from the image?",
            "role": "user"
        },
        {
            "content": "{\n    \"Vehicle Sr No\": \"MA1TA2YS2R2A13882\",\n    \"Engine No\": YSR4A38798,\n    \"Model\": SCORPIO CLASSIC S5 MT 7S\n}",
            "role": "assistant"
        }
    ],
    "images": [
        "path/to/imagefolder/vinplate.jpg"
    ]
}

此处,“messages”键包含主要用户查询和相应的 LLM 输出,“images”键包含图像路径列表。在我的例子中,每个查询只传递一张图片。如果每个查询有多张图片,则需要在“内容”中添加相应数量的标签。

例如,对于 2 张图片,

{
    "messages": [
        {
            "content": "<image><image>Can you find and provide the Vehicle Sr No, Engine No, and Model from the image?",
            "role": "user"
        },
        {
            "content": "{\n    \"Vehicle Sr No\": \"MA1TA2YS2R2A13882\",\n    \"Engine No\": YSR4A38798,\n    \"Model\": SCORPIO CLASSIC S5 MT 7S\n},
                        {\n    \"Vehicle Sr No\": \"MA1TA2YS2R2A13883\",\n    \"Engine No\": YSR4A38799,\n    \"Model\": SCORPIO CLASSIC S5 MT 7S\n}",
            "role": "assistant"
        }
    ],
    "images": [
        "path/to/imagefolder/vinplate1.jpg",
        "path/to/imagefolder/vinplate2.jpg",
    ]
}

等等。

现在,为了将我们的标签格式转换为所需的格式,我创建了如下Python 代码:

import json
import random

def generate_user_query():
    variations = [
        "Extract out the Vehicle Sr No, Engine No, and Model from the given image.",
        "Can you provide the Vehicle Sr No, Engine No, and Model for this image?",
        "What is the Vehicle Sr No, Engine No, and Model in the given image?",
        "Please extract the Vehicle Sr No, Engine No, and Model from this image.",
        "Find the Vehicle Sr No, Engine No, and Model in this image.",
        "Retrieve the Vehicle Sr No, Engine No, and Model from the image provided.",
        "What are the Vehicle Sr No, Engine No, and Model present in this image?",
        "Identify the Vehicle Sr No, Engine No, and Model from this image.",
        "Could you extract the Vehicle Sr No, Engine No, and Model from the attached image?",
        "Provide the Vehicle Sr No, Engine No, and Model from the image.",
        "Please determine the Vehicle Sr No, Engine No, and Model for this image.",
        "Extract the information for Vehicle Sr No, Engine No, and Model from the given image.",
        "What is the Vehicle Sr No, Engine No, and Model information extracted from this image?",
        "Can you pull the Vehicle Sr No, Engine No, and Model from the image?",
        "Retrieve the Vehicle Sr No, Engine No, and Model details from this image.",
        "Get the Vehicle Sr No, Engine No, and Model from the provided image.",
        "Please extract and provide the Vehicle Sr No, Engine No, and Model for this image.",
        "What Vehicle Sr No, Engine No, and Model can be identified in the image?",
        "I need the Vehicle Sr No, Engine No, and Model from this image.",
        "Can you find and provide the Vehicle Sr No, Engine No, and Model from the image?"
    ]

    return random.choice(variations)


def convert_to_format(input_data, image_folder_path):
    formatted_data = []

    for image_name, details in input_data.items():
        user_query = generate_user_query()
        formatted_entry = {
            "messages": [
                {
                    "content": '<image>'+user_query,
                    "role": "user"
                },
                {
                    "content": json.dumps(details, indent=4), 
                    "role": "assistant"
                }
            ],
            "images": [
                f"{image_folder_path}/{image_name}" 
            ]
        }

        formatted_data.append(formatted_entry)
    
    return formatted_data

def load_input_json(input_json_path):
    with open(input_json_path, 'r') as json_file:
        input_data = json.load(json_file)
    return input_data

def main(input_json_path, image_folder_path, output_json_path):
    input_data = load_input_json(input_json_path)
    formatted_output = convert_to_format(input_data, image_folder_path)
    with open(output_json_path, 'w') as json_file:
        json.dump(formatted_output, json_file, indent=4)

    print(f"Formatted output saved to {output_json_path}")


input_json_path = 'combined-ocr-json.json'  
image_folder_path = 'path/to/imagefolder'  # Folder where images are stored
output_json_path = 'final-llm-input.json'  # Path for the output JSON file

main(input_json_path, image_folder_path, output_json_path)

组合后的 JSON 文件如下所示。它将包含 JSON 列表。文件名为 final-llm-input.json

请注意每个样本的内容有何不同;这对于提供用户查询的变化非常重要。

这是用于微调 Qwen2-VL 模型的主要 JSON 文件。

注意:你可以直接创建此 JSON 文件,而无需遵循中间步骤;我只想解释我是如何创建用于微调的数据集的。

在下一部分中,我们将开始微调模型。

3、为什么选择LoRA微调?

有趣的部分来了:我选择使用 LoRA 微调 Qwen2-VL-2B,但您也可以根据您的 GPU 可用性选择 7B 模型。我尝试在 RTX 4090 24GB 上使用 LoRA 微调 7B 模型,大约需要 20GB 来加载模型和用于微调的图像批次。

是的,但为什么是 LoRA,为什么不完全训练模型?

当然可以,但是你有足够的 GPU 来做到这一点吗?

(下一部分将详细介绍)。

首先让我们了解一下 LoRA 到底是什么?

低秩自适应 (LoRA) 是一种用于高效微调大型语言模型 (LLM) 的技术,它只调整模型权重的一小部分低秩子集,而不是更新所有参数。LoRA 不会修改整个模型权重集,而是引入一小组低秩矩阵来捕获特定于任务的信息,然后在推理过程中将其与现有模型权重相结合。

LoRA 相对于完全微调的优势:

  • 更低的 GPU 内存要求:通过仅更新一小部分参数,LoRA 大大减少了微调所需的 VRAM,从而可以在有限的 GPU 资源上使用大型模型。
  • 效率:LoRA 降低了微调所需的计算成本和时间,因为训练的参数更少,从而可以更快地将模型适应新任务。
  • 参数效率:LoRA 允许在不修改整个模型的情况下进行微调,因此它保留了原始模型的知识,同时添加了特定于任务的学习,可以轻松存储和重用。
  • 模块化:由于 LoRA 矩阵很小,因此可以轻松加载和交换多个特定于任务的 LoRA 适配器而无需重新训练,从而增加了模型的灵活性。

4、训练 LoRA Qwen2-VL

设置好之后,让我们深入研究如何使用 LoRA 对 Qwen2-VL 进行微调。

我使用 Linux 进行微调、量化和推理。

首先,我们必须克隆并安装 Llama-Factory 存储库:

git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

要求在存储库中有提到,但我列出了我的工作环境:

OS                                 Linux
Python                             3.10.12
nvcc --version                     cuda_12.2
accelerate                         1.0.1
bitsandbytes                       0.43.1
datasets                           2.20.0
llamafactory                       0.9.1.dev0
torch                              2.4.0+cu121
peft                               0.12.0
trl                                0.9.6
flash-attn                         2.6.3

在前面,我们准备了名为 final-llm-input.json 的数据集。

首先,转到 data\dataset_info.json ,找到 mllm_demo,并将其修改为:

  "mllm_demo": {
    "file_name": "path/to/final-llm-input.json",
    "formatting": "sharegpt",
    "columns": {
      "messages": "messages",
      "images": "images"
    },

然后转到 example/train_lora/qwen2vl_lora_sft.yaml

按下面代码修改:

### model
model_name_or_path: Qwen/Qwen2-VL-2B-Instruct  ## Modify according to the model you want to finetune

### method
stage: sft
do_train: true
finetuning_type: lora
lora_target: all

### dataset
dataset: mllm_demo ## this should be same as mentioned in dataset_info.json
template: qwen2_vl
cutoff_len: 1024
max_samples: 1000
overwrite_cache: true
preprocessing_num_workers: 1

### output
output_dir: saves/qwen2_vl-7b/lora/sft  ## modify save path here
logging_steps: 10    ## modify logging sets according to training dataset
save_steps: 500      ## modify weight save steps according to training dataset      
plot_loss: true
overwrite_output_dir: true

### train
per_device_train_batch_size: 12  ## modify batch size according to GPU
gradient_accumulation_steps: 8
learning_rate: 1.0e-4            ## modify learning rate 
num_train_epochs: 100            ## modify number of epochs
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000

### eval
val_size: 0.1
per_device_eval_batch_size: 1
eval_strategy: steps
eval_steps: 500

你可以在此文件中更改训练超参数和模型保存路径。

最后,运行命令:

llamafactory-cli train examples/train_lora/qwen2vl_lora_sft.yaml

现在等待训练完成。

注意:如果你收到以下错误(我收到的)

IOError: image file is truncated

这时你需要跟踪 PIL 导入的位置,可能在 src/llamafactory/data/mm_plugin.py 内的 Python 文件中,粘贴以下导入 PIL 的位置:

from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

5、合并 LoRA 权重

我假设你已将适配器权重保存在: saves/qwen2_vl-2b/lora/sft

训练完成后,我们需要将 LoRA 权重合并在原始权重之上以获得微调后的权重。

为此,请转到 examples/merge_lora/qwen2vl_lora_sft.yaml

修改以下内容:

adapter_name_or_path : saves/qwen2_vl-2b/lora/sft   #path to your adapter weight

然后运行以下命令:

llamafactory-cli export examples/merge_lora/qwen2vl_lora_sft.yaml

假设你将合并后的权重保存在 saves/qwen2_vl-2b-merged

恭喜!!!你成功地在自定义数据集上微调 Qwen2-VL。

合并完成后,最终的微调权重文件夹应包含以下文件:

是的,我知道是 Windows!!别介意

以下是使用 LoRA 对 Qwen2-VL 进行微调后获得的每个文件的简要说明。

  • added_tokens.json:包含有关添加到标记器的任何自定义标记的信息,这些标记超出了标准词汇表。这些可能是特定于领域的术语、罕见词或特殊符号,可以提高模型在特定任务中的性能。
  • chat_template.json:此文件可能包含与生成聊天响应相关的配置数据,例如用于在交互式对话会话期间提示模型的模板。
  • config.json:保存模型架构配置,包括层数、隐藏维度、注意头和其他超参数等设置。它对于正确加载模型结构至关重要。
  • generation_config.json:包含特定于文本生成的设置,例如温度、top-k、top-p 和其他影响模型生成文本方式的参数。可用于控制输出的创造性或随机性。
  • merges.txt:此文件是 tokenizer 数据的一部分。它包含字节对编码 (BPE) 的合并规则,可帮助 tokenizer 将单词分解为子单词标记。
  • model.safetensors.index.json:SafeTensors 格式的索引文件,可帮助管理和加载分片的 SafeTensor 文件(model-00001-of-00003.safetensors 等)。它包含有关每个分片中张量形状和位置的元数据。
  • model-00001-of-00003.safetensorsmodel-00002-of-00003.safetensorsmodel-00003-of-00003.safetensors:这些是包含实际模型权重的分片文件。 SafeTensors 将大型模型拆分为多个文件,以简化加载和处理。
  • preprocessor_config.json:此文件可能保存在标记化之前应用于输入数据的预处理设置,例如小写或删除特殊字符。这对于确保在预处理新输入以进行推理时的一致性很有用。
  • special_tokens_map.json:将特殊标记(如 [CLS][SEP][MASK] 等)映射到标记器使用的特定值。特殊标记对于定义语言模型中的句子的开始或结束、填充和其他特定标记至关重要。
  • tokenizer.json:存储词汇表和标记化规则。此文件将单词或子单词映射到模型用于处理输入文本的唯一 ID。
  • tokenizer_config.json:包含标记器的其他配置参数,例如标记类型、最大长度以及任何特殊的预标记规则。
  • vocab.json:这是一个词汇表文件,列出了所有标记及其对应的 ID。标记器使用它来将输入文本转换为模型可以理解的标记 ID。
  • model.safetensors.index.json 文件是存储在 SafeTensors 格式中的模型的索引文件。它提供元数据,允许模型加载器从不同的分片 SafeTensors 文件(在本例中为 model-00001-of-00003.safetensorsmodel-00002-of-00003.safetensors 和  model-00003-of-00003.safetensors)中定位和加载模型的特定部分。

就我而言,最终微调后的权重文件夹为 4.12 GB。

6、推断微调后的 Qwen2-VL-LoRA

我已经在 Linux 中完成了推断,但你可以在 Windows 上尝试。使用以下 Python 代码进行相同操作:

import requests
import torch
from PIL import Image
from io import BytesIO

from transformers import AutoProcessor, AutoModelForVision2Seq
from transformers.image_utils import load_image
import time 
import json 
import re 

def extract_json_from_string(input_string):
    # Using regex to extract the JSON part from the string
    json_match = re.search(r'({.*})', input_string, re.DOTALL)

    if json_match:
        json_str = json_match.group(1)  # Extract the JSON-like part
        try:
            # Parsing the extracted string as JSON
            extracted_data = json.loads(json_str)
            return extracted_data
        except json.JSONDecodeError as e:
            print(f"Error decoding JSON: {e}")
            return None
    else:
        print("No JSON found in the string.")
        return None


DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"

imagepath = "path/to/vinplate.jpg"
image = load_image(imagepath)

model_path = "saves/qwen2_vl-2b-merged"
processor = AutoProcessor.from_pretrained(model_path)
model = AutoModelForVision2Seq.from_pretrained(
    model_path, torch_dtype=torch.float16, device_map= DEVICE
)
model.to(DEVICE)

# Create inputs
messages = [
    {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "text", "text": 
                                    '''
                                    Please extract the Vehicle Sr No, Engine No, and Model from this image. 
                                    Response only json format nothing else.
                                    Analyze the font and double check for similar letters such as "V":"U", "8":"S":"0", "R":"P".
                                    '''
            }
        ]
    }      
]

t1 = time.time()
prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
inputs = processor(text=prompt, images=[image], return_tensors="pt")
inputs = {k: v.to(DEVICE) for k, v in inputs.items()}


generated_ids = model.generate(**inputs, max_new_tokens=500)
generated_texts = processor.batch_decode(generated_ids, skip_special_tokens=True)
t2 = time.time()

response_json = extract_json_from_string(generated_texts[0])
print(response_json)
print('Time Taken')
print(t2-t1)

注意:这将需要大量的 GPU(有关更多信息这个在下一篇中会讲到),因为模型仍然需要量化。

接下来我将重点介绍我们微调的 Qwen2-VL 模型的量化。截至目前,Qwen2-VL 仅支持两种量化方法:激活感知权重量化 (AWQ) 和生成预训练 Transformer 量化 (GPTQ)。我使用了这两种方法来量化我们的模型,并在此博客中分享了我的观察结果。

7、为什么要量化?

让我们首先计算运行我们的微调模型理想情况下需要多少 GPU。这是一个计算推断 LLM 所需 GPU 的流行公式:

  • M:推理所需的 GPU 内存,以千兆字节 (GB) 为单位。
  • P:模型中的参数数量。
  • 4B:4 个字节(相当于 32 位),表示用于存储每个参数的内存。
  • 32:4 个字节中的位数。
  • Q:每个参数用于加载模型的目标位数(例如,16 位、8 位或 4 位)。
  • 1.2:表示在 GPU 内存中加载激活等额外内容的 20% 开销。

对于 Qwen2-VL 微调模型(16 位),仅用于模型加载所需的总 GPU 内存计算如下:

M = (2*4)(16/32) * 1.2 = 4.8 GB

因此,我们实际上需要至少 6 GB 的 GPU 来加载和推断我们的模型。虽然这看起来可能不是一个很大的数目,但实现 4 位量化模型可能会降低我们的 GPU 要求。加载 4 位模型的计算是:

M = (2*4)(4/32) * 1.2 = 1.2 GB

这代表 GPU 内存要求理论上减少了约 75%。实际上,我们可以使用 4 GB 的 Nvidia GPU,这将导致我们的 GPU 要求减少 50%。

现在我们了解了量化的重要性,让我们深入研究量化我们微调的 Qwen2-VL 模型的过程。

什么是 LLM 的量化?

大型语言模型 (LLM) 的量化是降低模型权重和激活的精度的过程,通常从 32 位浮点数降低到较低位表示(如 8 位或 4 位)。这减少了模型的内存占用和计算要求,使其能够在性能较弱的硬件上运行得更快,同时保持类似的性能水平。

8、激活感知权重量化 (AWQ)

传统量化通过将权重(模型参数)的精度从高精度格式(例如 16 位浮点数)降低到较小的尺寸(如 8 位或 4 位整数)来减小模型大小。虽然这种方法可以节省内存,但它通常会对所有权重应用相同的精度,从而冒着精度损失的风险——类似于均匀压缩图像并丢失重要细节。

AWQ 通过认识到并非所有权重对模型精度的贡献都相同,将量化更进一步。 AWQ 识别并保护最重要的权重——与更高激活(对预测有显著影响的输出值)相关的权重。在压缩过程中,这些权重会以更高的精度保留,而不太重要的权重则会更积极地压缩,从而保留模型的预测能力。

AWQ 流程从分析在校准阶段收集的激活统计数据开始。这些统计数据揭示了哪些权重对模型的输出影响最大。AWQ 利用这一洞察来应用每个通道的缩放,其中缩放因子是根据激活数据确定的。这种有针对性的方法可以实现高效的内存使用,而不会影响基本性能。例如,在处理视觉和语言任务的 Qwen2-VL 模型中,AWQ 可确保对两种模式都至关重要的权重受到保护,从而保持两个领域的性能。

AWQ 的工作原理是仅压缩模型中对准确性不重要的部分,同时以高细节保留重要部分。它通过在设置阶段对真实数据运行模型来实现这一点,在该阶段它会测量每一层的输出(称为激活)。这有助于确定哪些权重(模型的数字)对于获得准确的结果至关重要,这样只压缩不太重要的权重,从而保持整体性能。

如何使用 AWQ 量化微调的Qwen2-VL 模型

如果你浏览官方 Qwen2-VL 存储库,他们提到了如何使用 AutoAWQ 进行量化。我使用了相同的方法,下面是我们要做的事情。

首先,我们需要创建我们的校准数据集,它只是我们训练数据集的一小部分。格式如下所述:

dataset = [
    [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": "file:///path/to/your/vinplate.jpg"},
                {"type": "text", "text": "Extract out the Vehicle Sr No, Engine No and Model from the given image."},
            ],
        },
        {"role": "assistant", "content": "{\n    "Vehicle Sr No": "MA1TA2YS2P2M17877",\n    "Engine No": null,\n    "Model": null\n}"},
    ],
    ...,
]

我使用了大约 10 张图像来校准我们的量化模型。因此,最终的组合文件将是一个文本文件,其中包含 10 个与上述格式相同的样本。让我们将此文件命名为 caliber_dataset.txt

注意:您还可以为其创建一个 JSON 文件并相应地加载它。

接下来,我们需要为模型的量化设置环境。

从源克隆并安装以下存储库:

git clone https://github.com/kq-chen/AutoAWQ.git
cd AutoAWQ
pip install numpy gekko pandas
pip install -e .

注意:需要安装其他特定要求。我将编制一份完整清单,准备好后我会在这里分享。

现在,使用以下脚本加载我们经过微调的 Qwen2-VL 模型并使用此 caliber 数据集对其进行量化:

from transformers import Qwen2VLProcessor
from awq.models.qwen2vl import Qwen2VLAWQForConditionalGeneration
from qwen_vl_utils import process_vision_info
import json
import ast
import torch
torch.cuda.empty_cache()
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'

# Specify paths and hyperparameters for quantization
model_path = "saves/qwen2_vl-2b-merged"
quant_path = "saves/qwen2_vl-2b-awq-4bit"
quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"}

# Load your processor and model with AutoAWQ
processor = Qwen2VLProcessor.from_pretrained(model_path)
# We recommend enabling flash_attention_2 for better acceleration and memory saving
model = Qwen2VLAWQForConditionalGeneration.from_pretrained(
    model_path, model_type="qwen2_vl", use_cache=False, attn_implementation="flash_attention_2"
)
model.to('cuda')


# opening the file in read mode 
my_file = open("path/to/caliber_dataset.txt", "r") 
  
# reading the file 
data = my_file.read() 
  
data_into_list = data.split("\n")
dataset = data_into_list[:-1] 

final_dataset = []
for x in dataset:
    x1 = ast.literal_eval(x)
    final_dataset.append(x1)

text = processor.apply_chat_template(
    final_dataset, tokenize=False, add_generation_prompt=True
)


image_inputs, video_inputs = process_vision_info(final_dataset)

inputs = processor(
    text=text,
    images=image_inputs,
    videos=video_inputs,
    padding=True,
    return_tensors="pt",
)

model.quantize(calib_data=inputs, quant_config=quant_config)

model.model.config.use_cache = model.model.generation_config.use_cache = True
model.save_quantized(quant_path, safetensors=True, shard_size="1GB")
processor.save_pretrained(quant_path)

恭喜!!你已成功使用 AutoAWQ 量化自定义 Qwen2-VL。

注意量化模型的大小,我的是 2.75GB,而原始模型大约是 4.12G。

请查看本博客后面关于量化模型推理的部分。

9、生成式预训练 Transformer 量化 (GPTQ)

GPTQ,即生成式预训练 Transformer 量化,是一种旨在优化大型语言模型 (LLM)(如 GPT 和 BLOOM)的技术,通过减少内存需求和计算负载而不会造成显著的准确性损失。具有数十亿个参数的 LLM 通常太大且成本太高,无法在标准硬件上运行,即使是简单的任务也是如此。通过使用 GPTQ,可以压缩模型以在单个高性能 GPU 上高效运行,从而更广泛地访问强大的 AI 工具。

作为一种训练后量化 (PTQ) 方法,GPTQ 不需要从头开始重新训练模型。相反,它应用一次性量化来压缩模型的权重,使该过程快速高效。 GPTQ 使用小型校准数据集来帮助确保量化模型保持其原始精度。该过程将权重减少到 3 或 4 位,从而节省多达四倍的内存,同时将激活保持在 float16 中以支持精确计算。

工作原理

GPTQ 或生成式预训练 Transformer 量化是一种训练后量化 (PTQ) 方法,通过量化权重有效地压缩大型语言模型,从而可以在经济实惠的硬件上运行大规模模型,同时将精度损失降至最低。该过程主要依赖于逐层量化和最佳脑量化 (OBQ)。

逐层量化的工作原理是一次量化一层的权重,通过最小化相对于输出的均方误差 (MSE) 来确保每层的变换与原始模型紧密匹配。这是通过校准数据集实现的,使算法能够单独微调每一层,确保保持精度,同时实现显着的压缩。

理解 Hessian 矩阵和二阶信息

Hessian 矩阵是一个方阵,包含标量值函数(例如损失函数)关于其参数(权重)的二阶偏导数。它提供了对损失曲面曲率的洞察,有助于确定临界点是最小点、最大值还是鞍点。从 Hessian 派生出的二阶信息揭示了模型参数的变化如何影响损失函数,使优化算法能够更好地更新以实现更快、更稳定的收敛。在量化中,此信息对于估计量化特定权重对整体模型准确性的影响至关重要。

在最佳脑量化 (OBQ) 中,量化过程是按权重执行的,利用 Hessian 矩阵中的二阶误差信息来评估每个权重对输出误差的影响。OBQ 优先量化异常值权重以最大限度地减少潜在误差,然后动态调整剩余权重以保持较低的累积误差。为了提高计算效率,OBQ 采用了高斯消元法等技术来简化矩阵计算,从而减少处理时间和内存使用量。

GPTQ 还包括效率优化,例如任意权重处理顺序、延迟批量更新和 Cholesky 重新表述,以防止数值不稳定。此外,它使用混合量化方案,其中权重存储为低精度 INT4 整数,激活存储在 FLOAT16 中,从而同时实现内存效率和精度。在推理过程中,INT4 权重在计算单元附近的融合内核中被反量化,从而节省高达 4 倍的内存并减少数据传输时间,使 GPTQ 成为 LLM 部署的高效工具。

如何使用 GPTQ 量化微调的 Qwen2-VL 模型

官方 Qwen2-VL 存储库还提到使用 AutoGPTQ 量化自定义 Qwen2-VL 模型,让我们遵循相同的方法。

从给定的源克隆并安装 AutoGPTQ:

git clone https://github.com/kq-chen/AutoGPTQ.git
cd AutoGPTQ
pip install numpy gekko pandas
pip install -vvv --no-build-isolation -e .

注意:需要安装其他特定要求。我将编制一份完整清单,准备好后我会在这里分享。

你可以在此处使用用于 AWQ 量化的保存口径数据集文本文件,因为格式相同。

现在,使用以下 Python 代码量化我们的 Qwen2-VL 模型:

from transformers import Qwen2VLProcessor
from auto_gptq import BaseQuantizeConfig
from auto_gptq.modeling import Qwen2VLGPTQForConditionalGeneration
from qwen_vl_utils import process_vision_info
import torch
torch.cuda.empty_cache()

# Specify paths and hyperparameters for quantization
model_path = "saves/qwen2_vl-2b-merged"
quant_path = "saves/qwen2_vl-2b-gptq-4bit"
quantize_config = BaseQuantizeConfig(
    bits=4,  # 4 or 8
    group_size=128,
    damp_percent=0.1,
    desc_act=False,  # set to False can significantly speed up inference but the perplexity may slightly bad
    static_groups=False,
    sym=True,
    true_sequential=True,
)
# Load your processor and model with AutoGPTQ
processor = Qwen2VLProcessor.from_pretrained(model_path)
# We recommend enabling flash_attention_2 for better acceleration and memory saving
model = Qwen2VLGPTQForConditionalGeneration.from_pretrained(model_path, quantize_config, attn_implementation="flash_attention_2")
# model = Qwen2VLGPTQForConditionalGeneration.from_pretrained(model_path, quantize_config)
model.to("cuda:0")

import ast
my_file = open("path/to/caliber_dataset.txt", "r")

# reading the file
data = my_file.read()

data_into_list = data.split("\n")
dataset = data_into_list[:-1]

final_dataset = []
for x in dataset:
    x1 = ast.literal_eval(x)
    final_dataset.append(x1)


def batched(iterable, n: int):
    assert n >= 1, "batch size must be at least one"
    from itertools import islice

    iterator = iter(iterable)
    while batch := tuple(islice(iterator, n)):
        yield batch

batch_size = 1
calib_data = []
for batch in batched(final_dataset, batch_size):
    text = processor.apply_chat_template(
        batch, tokenize=False, add_generation_prompt=True
    )
    image_inputs, video_inputs = process_vision_info(batch)
    inputs = processor(
        text=text,
        images=image_inputs,
        videos=video_inputs,
        padding=True,
        return_tensors="pt",
    )
    calib_data.append(inputs)

model.quantize(calib_data, cache_examples_on_gpu=False)
model.save_quantized(quant_path, use_safetensors=True)
processor.save_pretrained(quant_path)

恭喜!!已使用 AutoGPTQ 成功量化你的自定义 Qwen2-VL 模型。

请注意量化模型的文件夹大小,我的是 2.75GB,与 AWQ 量化模型相同。很有趣吧!!!

关于量化模型推理的注意事项:

建议使用 vLLM 进行推理。但是,由于该领域的持续发展,vLLM 尚未发布用于加载和运行我们模型的稳定版本。我正在积极监控最新版本,但到目前为止,每次尝试在加载模型时都会遇到错误。我决定等待稳定版本,一旦可用,我将使用必要的环境设置和模型推理代码更新此部分。如果你在此期间找到了推断量化模型的解决方案,我很乐意收到你的来信。

为了结束本博客,这里有一个简单的例子来说明 AWQ 和 GPTQ 量化之间的区别。

想象一下,你正在为长途旅行打包行李箱,AWQ 和 GPTQ 是两种不同的打包方式:

  • AWQ(以活动为中心的打包):您可以通过组织特定活动的部分来打包——游泳、徒步旅行、外出就餐。行李箱的每个部分只容纳每项活动的必需品,因此不会浪费空间在不必要的物品上。这类似于 AWQ,它根据特定任务对模型中的每一层进行微调,单独优化每一层。
  • GPTQ(整体打包方法):在这里,你将行李箱视为一个空间,并小心地逐层打包所有东西,以避免出现缝隙或褶皱。你不是专注于单个活动,而是平衡所有项目以有效地组合在一起。GPTQ 压缩每一层以实现整体一致性,使整个模型保持优化和紧凑。

本质上,AWQ 专注于单独优化每一层以完美完成特定任务,而 GPTQ 确保平衡的整体压缩,使整个模型中的所有内容保持紧凑和有效。


原文链接:Fine-Tuning | Quantize — Qwen2-VL mLLM on Custom Data for OCR

汇智网翻译整理,转载请标明出处