多模态大模型实战-MiniGPT4Qwen:3090+2小时+通义千问=个人版双语多模态大模型

代码库:github.com/Coobiw/Mini...,已更新中文README Tutorials、处理好的数据json文件和训练checkpoint,可以快速进行demo尝试或完整复现

简介

MiniGPT4是最近很火的一个MLLM(Multi-modal Large Language Model)项目,他证明了对于BLIP2的ViT+Q-former这种已经与语言模态做了对齐预训练的结构,只需要重训一个Linear层,便可以接入新的LLM。

这个想法看似简单,实际上,对于现在这个每个月有一个新的更强的LLM出来的时代,这种构建多模态大模型的方式是十分高效的。

然而,MiniGPT4采用LLaMA、Vicuna作为语言模型,它们的中文支持相对较弱,导致训练出的MLLM对中文支持不好。而现在也有许多开源出来的中文LLM,如:通义千问、百川、GLM等。

本项目使用70亿参数的Qwen-7B-Chat作为LLM,用MiniGPT4的对齐方式,更加高效地训练了一个MLLM,名为 Minigpt4Qwen。相比MiniGPT4的两阶段训练(较低质量数据进行图文模态对齐 + 高质量数据指令微调),本项目仅仅采用18.8k的高质量指令微调数据,经过单阶段预训练即可达到很好的效果。(这里参考了InstructionGPT-4,200条高质量指令微调数据战胜MiniGPT-4这一结论)

添加图片注释,不超过 140 字(可选)

资源介绍

  • 模型规模:1B EVA-ViT-G + 180M Q-former(BLIP2) + 7B Qwen-Chat = 约7.3B,采用bf16或fp16的精度,模型本身占用约15G显存
  • 数据规模:minigpt4 指令微调数据(双语) + llava 部分指令微调数据(双语) = 18.8k 高质量图文对话数据(单轮对话,未经过多轮对话训练)(数据源:MMPretrain PR 1758,下载地址:huggingface.co/datasets/de...
  • 计算资源:2h 8 * 3090 24G,也可以单卡3090 24G(单卡的话建议调高一些gradient accumulation,训练曲线会更平稳)

数据简介

数据来自mmpretrain中的一个PR,里面含有中英双语数据,中文数据是用英文的指令微调数据经过chatgpt翻译得到的!

多模态接入的技术方案简介

这里想对比BLIP2和Qwen-VL模型,简单介绍一下MiniGPT4Qwen中,是如何将图像模态接入LLM中的

BLIP2方案:image embedding作为绝对前缀和LLM处指令的word embedding进行拼接

先简单放一小段代码,然后再解释小标题里的"绝对前缀":

ini 复制代码
# 1.
inputs_llm = self.llm_proj(query_output.last_hidden_state[:,:query_tokens.size(1),:])
# 2.
atts_llm = torch.ones(inputs_llm.size()[:-1], dtype=torch.long).to(image.device)

llm_tokens = self.llm_tokenizer(
    prompt,
    padding="longest",
    return_tensors="pt"
).to(image.device)

with self.maybe_autocast():
    # 3.
    inputs_embeds = self.llm_model.get_input_embeddings()(llm_tokens.input_ids)
    inputs_embeds = torch.cat([inputs_llm, inputs_embeds], dim=1)
    attention_mask = torch.cat([atts_llm, llm_tokens.attention_mask], dim=1)
    outputs = self.llm_model.generate(
               # 4.
               inputs_embeds=inputs_embeds,
               attention_mask=attention_mask,
               do_sample=use_nucleus_sampling,
               top_p=top_p,
               temperature=temperature,
               num_beams=num_beams,
               max_length=max_length,
               min_length=min_length,
               # eos_token_id=self.eos_token_id,
               repetition_penalty=repetition_penalty,
               length_penalty=length_penalty,
               num_return_sequences=num_captions,
           )

对上述四处代码,标号1、2、3、4:

  • 1号代码是将Q-former输出的视觉token经过一个linear层,使其通道维度与LLM的通道维度一致
  • 2号代码是将文本的输入tokens对应到其word embedding
  • 3号代码将二者concat到一起
  • 4号代码,二者concat的结果共同作为LLM的attention blocks的输入

这里就明白,BLIP2的限制在于,image tokens永远在最前面,没有办法灵活的进行插入,这对于一些多图推理的情形可能不那么灵活(虽然咱MiniGPT4Qwen也没支持多图哈,但就是觉得不够优雅hhh),所以我称它为"绝对前缀"。

Qwen-VL方案:tokenizer中加入特殊token进行分隔

Qwen-VL引入了和两个special token,因为我个人不是搞NLP的,最开始没咋接触过tokenizer的时候也不太懂special token,所以这里就简单介绍一下special token的大致定义:

因为Qwen-VL采用的BBPE分词器是基于sub-word的,举个不恰当但容易理解的例子:preprocess这个词,由于pre是一个常见的前缀,所以preprocess很可能会被tokenize成pre、pro、cess,具体而言BBPE会更复杂,是byte-level的处理。而special token是不同的,他们是不可拆分的,一定会被作为单独的完成的词去处理。

Qwen-VL使用这两个不可分割的special token单位,来定位image embedding的位置,大致可以参考tokenization_qwen.py中的:

python 复制代码
def _replace_closed_tag(
    input_tokens: List[Any],
    start_tags: Union[Any, Tuple[Any]],
    end_tags: Union[Any, Tuple[Any]],
    inclusive_replace_func: Callable,
    exclusive_replace_func: Callable = lambda x: x,
):
    if isinstance(start_tags, (str, int)):
        start_tags = (start_tags,)
    if isinstance(end_tags, (str, int)):
        end_tags = (end_tags,)
    assert len(start_tags) == len(end_tags)

    output_tokens = []
    end = 0
    while True:
        start = _list_find(input_tokens, start_tags, end)
        if start == -1:
            break
        output_tokens.extend(exclusive_replace_func(input_tokens[end : start]))
        tag_idx = start_tags.index(input_tokens[start])
        end = _list_find(input_tokens, (end_tags[tag_idx],), start)
        if end == -1:
            raise ValueError("Unclosed image token")
        # 1.
        output_tokens.extend(inclusive_replace_func(input_tokens[start : end + 1]))
        end += 1
    output_tokens.extend(exclusive_replace_func(input_tokens[end : ]))
    return output_tokens

def tokenize(
        self,
        text: str,
        allowed_special: Union[Set, str] = "all",
        disallowed_special: Union[Collection, str] = (),
        **kwargs,
    ) -> List[Union[bytes, str]]:
    tokens = []
        text = unicodedata.normalize("NFC", text)

        # this implementation takes a detour: text -> token id -> token surface forms
        for t in self.tokenizer.encode(
            text, allowed_special=allowed_special, disallowed_special=disallowed_special
        ):
            tokens.append(self.decoder[t])

        def _encode_imgurl(img_tokens):
            assert img_tokens[0] == self.image_start_tag and img_tokens[-1] == self.image_end_tag
            img_tokens = img_tokens[1:-1]
            img_url = b''.join(img_tokens)
            out_img_tokens = list(map(self.decoder.get, img_url))
            if len(out_img_tokens) > IMG_TOKEN_SPAN:
                raise ValueError("The content in {}..{} is too long".format(
                    self.image_start_tag, self.image_end_tag))
            out_img_tokens.extend([self.image_pad_tag] * (IMG_TOKEN_SPAN - len(out_img_tokens)))
            out_img_tokens = [self.image_start_tag] + out_img_tokens + [self.image_end_tag]
            return out_img_tokens

        # 2.
        return _replace_closed_tag(tokens, self.image_start_tag, self.image_end_tag, _encode_imgurl)

这部分代码不是很好截取,如果只是了解的话,不需要仔细看,只简单标号了两条代码,是关键的将和中间部分的image tokens取出来的代码。

这样引入special token的不好之处在于:需要重新训练word_embedding层和最后的lm_head输出层,这两个层加起来可是有1.2B的参数量哦,还是很heavy的!

MiniGPT4Qwen方案:使用Qwen-VL的tokenizer里的<|extra_0|> token作为一个占位符,后面用image_embedding代替即可

Qwen-VL中的<|extra_0|>这个token是正常情况下不会被使用的,所以这里用它作为占位符,由于vit+q-former的输出长度是定长的32个tokens,所以将这个占位符复制32次即可,最后得到vision embedding之后再进行替换。

值得一提的是,同时,还可以用:

ini 复制代码
replace_image_idxs = torch.where(llm_tokens == self.replace_image_token_id)
inputs_embeds = self.llm_model.get_input_embeddings()(llm_tokens) # B, L, C
_,_,channels = inputs_embeds.shape
inputs_embeds[replace_image_idxs[0],replace_image_idxs[1]] = \
                inputs_llm.view(-1,channels).to(inputs_embeds.dtype)

可以根据torch.where这一条代码,确定要替换的image tokens的位置,这样图像tokens的位置也是完全灵活的!在MiniGPT4Qwen中,采用instruction中的''这一个word来标识图像插入到文本中的位置,比如:

xml 复制代码
图像<Img><ImageHere></Img>中的内容是什么?
# 注意:这里<Img>和</Img>并没有作为special tokens,
# 只是一个正常的文本,可以一定程度上让模型理解到这里是图像的输入!
# 图像被插入到文本中央啦!
# 或者
图像1<Img><ImageHere></Img>和图像2<Img><ImageHere></Img>中的不同之处是什么?
# 多图推理!虽然MiniGPT4Qwen并没有支持多图
#(是因为我在代码中加了强约束),其实应该是可以支持的,但没来得及做hhh

运行示例

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

可以看到,还是有不错的caption和reasoning能力的,中文支持也不错~

复制README中的Tutorials,能访问github的就直接跳转吧~

github.com/Coobiw/Mini...

因为考虑到一些同学github访问不稳定,所以这里也贴上了README Tutorials,这也同时能方便不太确定能不能用我代码的同学们现在知乎看一下,判断下,然后再跳转,记得点star哦~

Getting Started

模型下载

请将模型权重下载后都放在 cache/ckpt下

bash 复制代码
 mkdir cache
 cd cache
 mkdir ckpt
 mkdir dataset

1.下载BLIP2的相关权重 (a) eva vit-g eva_vit_g.pth

bash 复制代码
wget https://storage.googleapis.com/sfr-vision-language-research/LAVIS/models/BLIP2/eva_vit_g.pth

(b) bert-base-uncased huggingface,下载如下的文件即可

添加图片注释,不超过 140 字(可选)

(c) blip2_pretrained_flant5xxl blip2_pretrained_flant5xxl.pth

bash 复制代码
 wget https://storage.googleapis.com/sfr-vision-language-research/LAVIS/models/BLIP2/blip2_pretrained_flant5xxl.pth

2.下载Qwen7B-chat的权重 Qwen-7B-chat huggingface

3.下载本模型的checkpoint(建议放入 lavis/output/) 在本仓库的release里放有checkpoint,可以直接下载

bash 复制代码
wget https://github.com/Coobiw/MiniGPT4Qwen/releases/download/instruction-data_and_checkpointv1.0/ckpt.zip  
unzip ckpt.zip

目录结构:

csharp 复制代码
 ├── cache
 │   ├── ckpt
 │   │   ├── bert-base-uncased
 │   │   ├── blip2
 │   │   │   ├── blip2_pretrained_flant5xxl.pth
 │   │   ├── eva
 │   │   │   ├── eva_vit_g.pth
 │   │   ├── Qwen7B-chat

运行test_model_chat.py进行初步尝试

复制代码
python test_model_chat.py

你可以修改里面的ckpt_pathimg_path

运行命令行demo

css 复制代码
python cli_demo.py --checkpoint-path xxxxxx

运行后需要输入图片路径,输入后进入对话 常见操作:

:help 查看help :clear 清空当前命令行 :clh 清空对话历史(但图像输入不会更改) :his 查看对话历史 :img 查看输入的图像路径

训练

数据准备

本数据集共含有18.8k个图文对,来自MMPretrain根据llava和minigpt4处理得到,下载链接:huggingface 为了支持当前的 lavis库的训练框架,我对数据集的annotations进行了重新处理,放到了本仓库的release中,下载链接:instruction_data

bash 复制代码
 wget https://github.com/Coobiw/MiniGPT4Qwen/releases/download/instruction-data_and_checkpointv1.0/instruction_data.zip
 unzip instruction_data

最后需要将数据集放入 ./cache/dataset中,目录结构如下:

arduino 复制代码
 ├── cache
 │   └── dataset
 │       ├── llava
 │   │   │   ├── llava_minigpt4qwen_format.json
 │   │   │   ├── image
 │       ├── minigpt4
 │   │   │   ├── image
 │   │   │   ├── minigpt4_minigpt4qwen_format.json

config文件的书写

请参考train.yaml

运行train.py

单卡:

ini 复制代码
 CUDA_VISIBLE_DEVICES=xxx python train.py --cfg-path lavis/projects/instruction_tuning/train.yaml

多卡:

css 复制代码
 CUDA_VISIBLE_DEVICES=xxx python -m torch.distributed.run --nproc_per_node=8 train.py --cfg-path lavis/projects/instruction_tuning/train.yaml

参考

  • Lavis 本仓库是基于lavis进行构建的
  • QwenLM 本仓库的语言模型采用Qwen-7B-Chat
  • MiniGPT4 本仓库的主要思想来自MiniGPT4
  • MMPretrain 提供所需的双语指令微调数据集
相关推荐
介一安全6 小时前
初探 Web 环境下的 LLM 安全:攻击原理与风险边界
安全·web安全·ai·llm·安全性测试
PPIO派欧云10 小时前
PPIO × Lemon AI:一键解锁全流程自动化开发能力
人工智能·自动化·llm
强哥之神13 小时前
一文深入:AI 智能体系统架构设计
深度学习·语言模型·架构·llm·transformer·ai agent
hayson15 小时前
langchaingo用法详解及源码解析(一)
langchain·llm
PPIO派欧云1 天前
为什么主流大模型的上下文窗口都是128k?
llm
Paramita1 天前
LLM的技术底座:Transformer架构
llm
阿里云大数据AI技术1 天前
云上AI推理平台全掌握 (4):大模型分发加速
大数据·人工智能·llm
聚客AI1 天前
💡大模型开发从入门到部署:3个月避开87%的新手雷区
人工智能·pytorch·llm
weikuo05061 天前
【手搓大模型】从零手写Llama3
神经网络·llm
物与我皆无尽也1 天前
Agent交互细节
java·llm·agent·tools·mcp·mcp server