更新:已支持Gradio WebUI! 求Star!另:内部是支持lora微调的
代码库: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进行拼接
先简单放一小段代码,然后再解释小标题里的“绝对前缀”:
# 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中的:
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之后再进行替换。
值得一提的是,同时,还可以用:
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来标识图像插入到文本中的位置,比如:
图像<Img><ImageHere></Img>中的内容是什么?
# 注意:这里<Img>和</Img>并没有作为special tokens,
# 只是一个正常的文本,可以一定程度上让模型理解到这里是图像的输入!
# 图像被插入到文本中央啦!
# 或者
图像1<Img><ImageHere></Img>和图像2<Img><ImageHere></Img>中的不同之处是什么?
# 多图推理!虽然MiniGPT4Qwen并没有支持多图
#(是因为我在代码中加了强约束),其实应该是可以支持的,但没来得及做hhh
运行示例
可以看到,还是有不错的caption和reasoning能力的,中文支持也不错~
WebUI 运行示例
开启do_sample和beam search
复制README中的Tutorials,能访问github的就直接跳转吧~
因为考虑到一些同学github访问不稳定,所以这里也贴上了README Tutorials,这也同时能方便不太确定能不能用我代码的同学们现在知乎看一下,判断下,然后再跳转,记得点star哦~
Getting Started
模型下载
请将模型权重下载后都放在 cache/ckpt下
mkdir cache
cd cache
mkdir ckpt
mkdir dataset
1.下载BLIP2的相关权重 (a) eva vit-g eva_vit_g.pth
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
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,可以直接下载
wget https://github.com/Coobiw/MiniGPT4Qwen/releases/download/instruction-data_and_checkpointv1.0/ckpt.zip
unzip ckpt.zip
目录结构:
├── 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
运行命令行demo
python cli_demo.py --checkpoint-path xxxxxx
运行后需要输入图片路径,输入后进入对话 常见操作:
:help 查看help :clear 清空当前命令行 :clh 清空对话历史(但图像输入不会更改) :his 查看对话历史 :img 查看输入的图像路径
训练
数据准备
本数据集共含有18.8k个图文对,来自MMPretrain根据llava和minigpt4处理得到,下载链接:huggingface 为了支持当前的 lavis库的训练框架,我对数据集的annotations进行了重新处理,放到了本仓库的release中,下载链接:instruction_data
wget https://github.com/Coobiw/MiniGPT4Qwen/releases/download/instruction-data_and_checkpointv1.0/instruction_data.zip
unzip instruction_data
最后需要将数据集放入 ./cache/dataset中,目录结构如下:
├── cache
│ └── dataset
│ ├── llava
│ │ │ ├── llava_minigpt4qwen_format.json
│ │ │ ├── image
│ ├── minigpt4
│ │ │ ├── image
│ │ │ ├── minigpt4_minigpt4qwen_format.json
config文件的书写
请参考train.yaml
运行train.py
单卡:
CUDA_VISIBLE_DEVICES=xxx python train.py --cfg-path lavis/projects/instruction_tuning/train.yaml
多卡:
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 提供所需的双语指令微调数据集