1.概述
LLaVA 是一个由威斯康星大学麦迪逊分校、微软研究院和哥伦比亚大学的研究人员开发的大型语言和视觉助手。它是一个端到端训练的大型多模态模型,结合了视觉编码器和语言模型,用于通用的视觉和语言理解。
微软研究院、威斯康星大学的研究人员在LLaVA基础之上,继续开源了LLaVA-1.5版本。与前一代相比,LLaVA-1.5引入了跨模态连接器和特定格式的学术视觉问答数据集,全面提升了多模态理解和生成能力。
LLaVA-1.5在多模态理解和生成能力上实现了开源模型中的最高水平,可媲美GPT-4V效果。该模型由视觉模型、大语言模型和视觉语言连接器组成,其中视觉模型使用CLIPViT-L/336px提取图像特征,大语言模型使用Vicunav1.5理解文本内容。训练流程分为预训练和调优两个阶段,数据集覆盖视觉问答、语言对话等典型应用,使用约65万条多模态指令数据进行视觉指令调优。
为了评估LLaVA-1.5的性能,研究人员在MMEMM、BenchMM、SQA、POPE等11个知名数据平台中对视觉问答、自然语言处理、图像生成等进行了测试。结果显示,LLaVA-1.5皆实现了开源模型中的最高水平,可媲美GPT-4V效果。
接下来,本篇博客将为你详细介绍LLaVA的架构。我将从LLaVA的核心设计理念出发,逐步解析其如何通过结合视觉编码器和语言模型,实现对视觉和语言信息的深度理解和处理。通过本篇博客,你将获得对LLaVA架构的全面认识,理解它是如何在多模态人工智能领域中占据领先地位的。
github代码:https://github.com/haotian-liu/LLaVA
clip权重:openai/clip-vit-large-patch14-336 · HF Mirror
其他人写的教程:第一节 LLaVA模型安装、预测、训练详细教程-CSDN博客
其他人写的教程:多模态大模型 LLaVA 微调教程-大语言模型8 - vanilla阿草 - 博客园
其他人写的报错信息总结:https://zhuanlan.zhihu.com/p/656307174
2.论文
数据集构建
对于图像Xv及其相关联的Caption Xc,作者创建了一组问题Xq,其意图是指示助手描述图像内容。我们提示GPT-4编制此类问题列表。因此,将一个图像-文本对扩展为
Human:Xq Xv <STOP>Assistant:Xc<STOP>
尽管构建成本低廉,但这种简单的扩展版本在指令和响应方面缺乏多样性和深入的推理。
作者利用纯语言GPT-4或ChatGPT作为教师模型(两者都只接受文本作为输入)来创建涉及可视内容的指令遵循数据。具体地说,为了将图像编码成其视觉特征以提示纯文本GPT,作者使用了两种类型的符号表示:(i)字幕(Caption)通常从各种角度描述视觉场景;(ii)边界框通常定位场景中的对象,并且每个框编码对象概念及其空间位置。如下图所示。
作者使用COCO图像,生成三种类型的指令跟随数据。对于每种类型,作者首先手动设计一些示例。它们是在数据收集过程中唯一的人工注释,相当于few-shot。
- 首先,作者设计了一个对话,在助手和一个人之间询问关于这张照片的问题。回答的语气就好像助理看到了图像并回答了问题。询问关于图像的视觉内容的一组不同的问题,包括对象类型、对对象的计数、对象动作、对象位置、对象之间的相对位置。只考虑有明确答案的问题。
- 接着,为了包含丰富而全面的图像描述,作者创建了一个带有这种意图的问题列表。对于每个图像,我们从列表中随机抽取一个问题,让GPT-4生成详细的描述。
- 在此基础上,作者进一步打造深度推理题。答案通常需要遵循严格的逻辑,一步一步地推理。
架构
简单来说,按照数据的不同,LLaVA会分开处理:
- 文本:因为是大语言模型,文本按正常方法,给大模型处理即可
- 图片:使用CLIP-ViT转化为向量,再通过一个线性层Projection转换到大模型的理解空间,然后输入到大模型
模型的组成:
- 视觉编码器(Vision Encoder):LLaVa 架构的视觉部分是预训练的 CLIP 视觉编码器,具体来说是 ViT-L/14 变体。该组件通过 Transformer 层处理输入图像 (Xv) 以提取特征 (Zv),使模型能够有效地理解视觉信息。
- 大语言模型 (Vicuna):LLaVa 的语言能力依赖于 Vicuna,它是大型语言模型 (LLM) 的一种。Vicuna 根据输入语言指令 (Xq) 理解并生成语言响应 (Xa),补充视觉编码器的功能。
- 线性投影(Projection):此组件充当视觉特征 (Zv) 和语言模型的嵌入空间之间的桥梁。它将视觉特征转换为视觉标记 (Hv),并将其与语言模型的词嵌入空间对齐,以促进多模态对话
训练
stage1:预训练
在这个阶段中,保持视觉编码器和LLM权值不变,只训练线性层projection。这样,图像特征可以与预训练的大语言模型的词嵌入空间对齐。
stage2:fine-tuning
在这个阶段中,始终保持视觉编码器权值不变,并不断更新投影层和大语言模型的预训练权值。
数据集:
- 通过对158K language-image instruction-following数据集进行微调来开发Chatbot。在这三种类型的应答中,会话是多轮的,而另外两种是单轮的。
- Science QA:每个问题都以自然语言或图像的形式提供上下文。该助手以自然语言提供推理过程,并从多个选项中选择答案。
LLaVA-1.5
LLaVA-1.5 在 LLaVA 的基础上做了以下改动:
- 使用 CLIP-ViT-L-336px 视觉编码器替换原先的 CLIP-ViT-L/14
- 将原先的一层线性层替换为 MLP 层(两层线性层)
此外,为了支持更高的图像分辨率(可以提升模型的性能)且不影响 LLaVA-1.5 的推理效率,在 LLaVA-1.5 的基础上提出了 LLaVA-1.5-HD,它采用了创新的AnyRes策略,可以接受各种高分辨率的图像作为输入。具体步骤如下:
- 首先高分辨率图像被智能分割成多个小块(Patch),以便单独处理每个块。例如,CLIP-ViT-L/14型号的视觉编码器能够处理的最大分辨率为224x224像素。
- 同时,将高分辨率图像调整至视觉编码器能够处理的尺寸,并利用编码器对其进行编码。
- 将上面两步的结果拼接在一起作为视觉特征,输入到projection。
这个过程可以看做特征金字塔,如下图所示
3.代码
环境配置
conda create -n llava python=3.10 -y
conda activate llava
pip install --upgrade pip # enable PEP 660 support
pip install -e .
我们可以遵循官方的指示,安装环境。我这里直接使用CogVideoX的环境,可以直接使用。
权重方面,大家需要下载llava的权重,有1、1.5、1.6三个版本,每个版本又分7b等多个版本,我这里使用1.5-7b:
liuhaotian/llava-v1.5-7b at main
大模型权重:lmsys/vicuna-7b-v1.5 · HF Mirror
除此之外,还需要下载openai的clip模型,这是llava的视觉编码器部分,然后把config.json里面的相应位置做更改:
openai/clip-vit-large-patch14-336 · HF Mirror
推理
inference.py
输入:
What are the things I should be cautious about when I visit here?
我这里是直接按照官方文档里面的方法,自建了一个inference.py,代码如下:
需要注意的是,官方文档还有模型导入的代码,这部分不需要加进inference.py,如果加了模型会导两次!我就是在这里被坑惨了。
from llava.model.builder import load_pretrained_model
from llava.mm_utils import get_model_name_from_path
from llava.eval.run_llava import eval_model
model_path = "llava-v1.5-7b"
prompt = "What are the things I should be cautious about when I visit here?"
image_file = "datasets/view.jpg"
args = type('Args', (), {
"model_path": model_path,
"model_base": None,
"model_name": get_model_name_from_path(model_path),
"query": prompt,
"conv_mode": None,
"image_file": image_file,
"sep": ",",
"temperature": 0,
"top_p": None,
"num_beams": 1,
"max_new_tokens": 512
})()
eval_model(args)
文件位置如下图所示:
运行即可打印结果。
When visiting this location, which features a pier extending over a large body of water, there are a few things to be cautious about. First, be mindful of the weather conditions, as the pier may be affected by strong winds or storms, which could make it unsafe to walk on. Second, be aware of the water depth and any potential hazards, such as submerged rocks or debris, that could pose a risk to your safety. Additionally, be cautious of the presence of wildlife in the area, as there might be birds or other animals that could pose a threat or disturbance. Finally, be respectful of the environment and other visitors, and follow any posted rules or guidelines to ensure a safe and enjoyable experience for everyone.
具体来说,模型会直接调用run_llava.py里面的eval_model,接下来,我们一步步来看里面的代码。
代码解读
eval_model
run_llava.py里面的函数
具体来说,这部分负责处理提示词,主要操作就是将<image>这个占位符放入文本提示词中,最终qs=<image>提示词
qs = args.query # 提示词
image_token_se = DEFAULT_IM_START_TOKEN + DEFAULT_IMAGE_TOKEN + DEFAULT_IM_END_TOKEN # '<im_start><image><im_end>'
if IMAGE_PLACEHOLDER in qs:
if model.config.mm_use_im_start_end:
qs = re.sub(IMAGE_PLACEHOLDER, image_token_se, qs)
else:
qs = re.sub(IMAGE_PLACEHOLDER, DEFAULT_IMAGE_TOKEN, qs)
else:
if model.config.mm_use_im_start_end:
qs = image_token_se + "\n" + qs
else:
qs = DEFAULT_IMAGE_TOKEN + "\n" + qs # <image>提示词
这部分是将system、user、assistant的信息拼接起来
conv = conv_templates[args.conv_mode].copy() # system
conv.append_message(conv.roles[0], qs) # user
conv.append_message(conv.roles[1], None) # assistant
prompt = conv.get_prompt() # 完整提示词
"A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions. USER: <image>
What are the things I should be cautious about when I visit here? ASSISTANT:"
处理图片
image_files = image_parser(args) # 地址
images = load_images(image_files)
image_sizes = [x.size for x in images]
images_tensor = process_images(
images,
image_processor,
model.config
).to(model.device, dtype=torch.float16)
process_images的核心部分如下,简单来说,这里就是将图像转换为正方形,然后转换为tensor格式,并转换为336*336的大小
image = expand2square(image, tuple(int(x*255) for x in image_processor.image_mean)) # 转变为正方形[3,336,336]
image = image_processor.preprocess(image, return_tensors='pt')['pixel_values'][0]
其中expand2square将图像转换为正方形,如果已经是正方形了,直接返回;如果不是,则选最长的边创建一个新的正方形图像,背景颜色为background_color,并将原始图像粘贴到新图像的中间。
def expand2square(pil_img, background_color):
width, height = pil_img.size
if width == height:
return pil_img
elif width > height: # 如果图像是横向的(宽度大于高度),则创建一个新的正方形图像,背景颜色为background_color,并将原始图像粘贴到新图像的中间。
result = Image.new(pil_img.mode, (width, width), background_color)
result.paste(pil_img, (0, (width - height) // 2))
return result
else:
result = Image.new(pil_img.mode, (height, height), background_color)
result.paste(pil_img, ((height - width) // 2, 0))
return result
将文本转化为token id,大语言模型的基本操作,不多赘述
input_ids = ( # 使用tokenizer将prompt转换为token
tokenizer_image_token(prompt, tokenizer, IMAGE_TOKEN_INDEX, return_tensors="pt")
.unsqueeze(0)
.cuda()
)
将文本和图像输入到generate函数
with torch.inference_mode():
output_ids = model.generate(
input_ids,
images=images_tensor,
image_sizes=image_sizes,
do_sample=True if args.temperature > 0 else False,
temperature=args.temperature,
top_p=args.top_p,
num_beams=args.num_beams,
max_new_tokens=args.max_new_tokens,
use_cache=True,
)
generate
具体来说,这里包含两部分,其中prepare_inputs_labels_for_multimodal会处理文本和图像,用于生成适合多模态模型输入的数据;而super().generate会调用父类的generate方法进行预测生成。
我们先来看prepare_inputs_labels_for_multimodal
if images is not None:
(
inputs,
position_ids,
attention_mask,
_,
inputs_embeds,
_
) = self.prepare_inputs_labels_for_multimodal(
inputs,
position_ids,
attention_mask,
None,
None,
images,
image_sizes=image_sizes
)
else:
inputs_embeds = self.get_model().embed_tokens(inputs)
我们进入这个函数详细看看
首先是将图像转换为大模型空间的函数,主要包括clip和线性层两部分的转换
image_features = self.encode_images(images) # [1,576,4096]
def encode_images(self, images):
image_features = self.get_model().get_vision_tower()(images) # clip [1,3,336,336]->[1,576,1024]
image_features = self.get_model().mm_projector(image_features) # 线性层 ->[1,576,4096]
return image_features
接着处理文本、标签相关信息。因为现在是推理,label、position_ids、mask都是None,这里会生成全1的注意力矩阵和全-100的labels,然后利用注意力矩阵做筛选(其实啥也没变)
_labels = labels # None
_position_ids = position_ids # None
_attention_mask = attention_mask # None
if attention_mask is None:
attention_mask = torch.ones_like(input_ids, dtype=torch.bool) # 全1的注意力矩阵
else:
attention_mask = attention_mask.bool()
if position_ids is None:
position_ids = torch.arange(0, input_ids.shape[1], dtype=torch.long, device=input_ids.device) # 0-len
if labels is None:
labels = torch.full_like(input_ids, IGNORE_INDEX) # 全是-100
# remove the padding using attention_mask -- FIXME
_input_ids = input_ids
input_ids = [cur_input_ids[cur_attention_mask] for cur_input_ids, cur_attention_mask in zip(input_ids, attention_mask)] # 利用掩码筛选[len]
labels = [cur_labels[cur_attention_mask] for cur_labels, cur_attention_mask in zip(labels, attention_mask)] # 利用掩码筛选[len]
接下来就是把图片插入到文本的token_id中,首先是把文本的token和label按照cur_input_ids == IMAGE_TOKEN_INDEX(-200)进行拆分,拆分结果为cur_input_ids_noim和cur_labels_noim。
# 将文本从图像的位置分开
image_token_indices = [-1] + torch.where(cur_input_ids == IMAGE_TOKEN_INDEX)[0].tolist() + [cur_input_ids.shape[0]] # [-1,35,59]
cur_input_ids_noim = [] # 2个,长度分别为:[35],[23]
cur_labels = labels[batch_idx]
cur_labels_noim = []
for i in range(len(image_token_indices) - 1):
cur_input_ids_noim.append(cur_input_ids[image_token_indices[i]+1:image_token_indices[i+1]])
cur_labels_noim.append(cur_labels[image_token_indices[i]+1:image_token_indices[i+1]])
split_sizes = [x.shape[0] for x in cur_labels_noim]
cur_input_embeds = self.get_model().embed_tokens(torch.cat(cur_input_ids_noim))
cur_input_embeds_no_im = torch.split(cur_input_embeds, split_sizes, dim=0) # [35,4096],[23,4096]
cur_new_input_embeds = []
cur_new_labels = []
然后把图片的信息image_features依次插入,变成cur_new_input_embeds和cur_new_labels;最后直接concat起来,变成[len,c]=[634,4096]的tensor,这个tensor中包含了图片和文本的信息。
for i in range(num_images + 1): # 添加照片
cur_new_input_embeds.append(cur_input_embeds_no_im[i])
cur_new_labels.append(cur_labels_noim[i])
if i < num_images:
cur_image_features = image_features[cur_image_idx]
cur_image_idx += 1
cur_new_input_embeds.append(cur_image_features)
cur_new_labels.append(torch.full((cur_image_features.shape[0],), IGNORE_INDEX, device=cur_labels.device, dtype=cur_labels.dtype))
cur_new_input_embeds = [x.to(self.device) for x in cur_new_input_embeds]
cur_new_input_embeds = torch.cat(cur_new_input_embeds) # [634,4096]
cur_new_labels = torch.cat(cur_new_labels)
new_input_embeds.append(cur_new_input_embeds)
new_labels.append(cur_new_labels)
接下来将输入嵌入(cur_new_embed
)和对应的标签(cur_new_labels
)填充到指定的最大长度(max_len
),同时更新label、注意力掩码、位置编码信息。
for i, (cur_new_embed, cur_new_labels) in enumerate(zip(new_input_embeds, new_labels)):
cur_len = cur_new_embed.shape[0] # 634
if getattr(self.config, 'tokenizer_padding_side', 'right') == "left":
new_input_embeds_padded.append(torch.cat((
torch.zeros((max_len - cur_len, cur_new_embed.shape[1]), dtype=cur_new_embed.dtype, device=cur_new_embed.device),
cur_new_embed
), dim=0))
if cur_len > 0:
new_labels_padded[i, -cur_len:] = cur_new_labels
attention_mask[i, -cur_len:] = True
position_ids[i, -cur_len:] = torch.arange(0, cur_len, dtype=position_ids.dtype, device=position_ids.device)
else:
new_input_embeds_padded.append(torch.cat(( # 它将一个张量cur_new_embed填充到一个指定的最大长度max_len。
cur_new_embed,
torch.zeros((max_len - cur_len, cur_new_embed.shape[1]), dtype=cur_new_embed.dtype, device=cur_new_embed.device)
), dim=0))
if cur_len > 0:
new_labels_padded[i, :cur_len] = cur_new_labels # 将标签(cur_new_labels)填充到预先定义的张量
attention_mask[i, :cur_len] = True # 更新注意力掩码(attention_mask)以反映非填充部分的有效位置。
position_ids[i, :cur_len] = torch.arange(0, cur_len, dtype=position_ids.dtype, device=position_ids.device) # 将位置ID(position_ids)填充到预先定义的张量
然后跳出prepare_inputs_labels_for_multimodal,进入父类的generate进行生成
return super().generate(
position_ids=position_ids, # None
attention_mask=attention_mask, # None
inputs_embeds=inputs_embeds, # [1,634,4096],包含图片和文本信息
**kwargs
)
父类方法里面的很多步骤都是在设置参数,我们就不多赘述,主要来看看关键步骤
result = self._sample(
input_ids,
logits_processor=prepared_logits_processor,
stopping_criteria=prepared_stopping_criteria,
generation_config=generation_config,
synced_gpus=synced_gpus,
streamer=streamer,
**model_kwargs,
)
进入里面的_sample,这个函数的核心部分在while循环里面,这里的操作包括:
- 首先准备模型的输入数据model_inputs,
- 然后模型预测output,其尺寸为[1,1,32000],
- 然后通过argmax选出最大概率的token,得到其token id作为结果,并加入到inputs_ids里(注意这个是外部的,不是model_inputs里面的),最后更新model_kwargs。
其中model_inputs的主要参数如下:
- 'attention_mask':掩码
- 'input_ids': 模型的输出。第一次是None,后面每次都是上一轮模型预测的结果,即一个token id
- 'inputs_embeds': 第一次是[1,634,4096],后面都是None
- 'position_ids': 位置,当前预测的token的位置,每次+1
代码如下,XXX表示不重要的部分,被我省略了。
while self._has_unfinished_sequences( # 自回归循环输出
this_peer_finished, synced_gpus, device=input_ids.device, cur_len=cur_len, max_length=max_length
):
# prepare model inputs
model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs) # 包含掩码、inputs_embed
XXX
# forward pass to get next token
outputs = self(**model_inputs, return_dict=True) # 模型预测 第一次[1,634,32000],后面都是[1,1,32000]
XXX
next_token_logits = outputs.logits.clone()[:, -1, :].float() # 下一个token的概率
# pre-process distribution
next_token_scores = logits_processor(input_ids, next_token_logits)
XXX
# token selection
if XXX
else:
next_tokens = torch.argmax(next_token_scores, dim=-1) # 取最大概率的token
# finished sentences should have their next token be a padding token
if has_eos_stopping_criteria:
next_tokens = next_tokens * unfinished_sequences + pad_token_id * (1 - unfinished_sequences)
# update generated ids, model inputs, and length for next step
input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1) # 输出结果拼接
if streamer is not None:
streamer.put(next_tokens.cpu())
model_kwargs = self._update_model_kwargs_for_generation( # 更新序列
outputs,
model_kwargs,
is_encoder_decoder=self.config.is_encoder_decoder,
)
unfinished_sequences = unfinished_sequences & ~stopping_criteria(input_ids, scores)
this_peer_finished = unfinished_sequences.max() == 0
cur_len += 1
# This is needed to properly delete outputs.logits which may be very large for first iteration
# Otherwise a reference to outputs is kept which keeps the logits alive in the next iteration
del outputs # 删除
这里的核心当然是outputs = self(**model_inputs, return_dict=True),我们来看一下
首先会跳转到forward函数,乍一看和generate很像,但请注意这确实是两个不同的函数,而且函数里面调用其他方法时传入的值也不同。
class LlavaLlamaForCausalLM(LlamaForCausalLM, LlavaMetaForCausalLM):
def forward(XXX) -> Union[Tuple, CausalLMOutputWithPast]:
if inputs_embeds is None:
(
input_ids,
position_ids,
attention_mask,
past_key_values,
inputs_embeds,
labels
) = self.prepare_inputs_labels_for_multimodal(
input_ids,
position_ids,
attention_mask,
past_key_values,
labels,
images,
image_sizes
)
return super().forward(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
labels=labels,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict
)
接着会跳入父类的forward方法,这里会调用模型,输出预测结果,尺寸为[1,1,4096],然后通过线性层转换为[1,1,32000]。这个结果输出出来,也就是上文中_sample里面的output
outputs = self.model( # LLama
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds, # 微调[b,len,4096],推理时是None
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
)
hidden_states = outputs[0] #[1,len,4096]
else:
if XXX
logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]).float() # 4096->32000
进入self.model,可以看到里面实际上是LlamaModel.forward(),这里就是将输入数据、掩码、位置编码等输入到模型里,进行预测。
causal_mask = self._update_causal_mask(
attention_mask, inputs_embeds, cache_position, past_key_values, output_attentions
)
hidden_states = inputs_embeds # 输入[b,len,4096]
# create position embeddings to be shared across the decoder layers
position_embeddings = self.rotary_emb(hidden_states, position_ids) # 两个[1,len,128]
# decoder layers
all_hidden_states = () if output_hidden_states else None
all_self_attns = () if output_attentions else None
next_decoder_cache = None
for decoder_layer in self.layers:
if output_hidden_states:
all_hidden_states += (hidden_states,)
if self.gradient_checkpointing and self.training:
layer_outputs = self._gradient_checkpointing_func(
decoder_layer.__call__,
hidden_states,
causal_mask,
position_ids,
past_key_values,
output_attentions,
use_cache,
cache_position,
position_embeddings,
)
else:
layer_outputs = decoder_layer(
hidden_states,
attention_mask=causal_mask,
position_ids=position_ids,
past_key_value=past_key_values,
output_attentions=output_attentions,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
)
hidden_states = layer_outputs[0]
if use_cache:
next_decoder_cache = layer_outputs[2 if output_attentions else 1]
if output_attentions:
all_self_attns += (layer_outputs[1],)
hidden_states = self.norm(hidden_states)
# add hidden states from the last decoder layer
if output_hidden_states:
all_hidden_states += (hidden_states,)
然后通过线性层将模型的输出维度从4096转为32000
logits = self.lm_head(hidden_states[:, -num_logits_to_keep:, :]).float() # 4096->32000
模型会循环预测每一个token id。最后,_sample会返回input_ids,也就是模型预测的所有token id,不包括输入部分。到这里,模型的整个推理过程就算结束了,,然后代码会返回run_llava.py的eval_model函数,执行decode。
输出
使用tokenizer进行转换,转换为字符
outputs = tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0].strip()
return [
self.decode(
seq,
skip_special_tokens=skip_special_tokens,
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
**kwargs,
)
for seq in sequences
]
输出
When visiting this location, which features a pier extending over a large body of water, there are a few things to be cautious about. First, be mindful of the weather conditions, as the pier may be affected by strong winds or storms, which could make it unsafe to walk on. Second, be aware of the water depth and any potential hazards, such as submerged rocks or debris, that could pose a risk to your safety. Additionally, be cautious of the presence of wildlife in the area, as there might be birds or other animals that could pose a threat or disturbance. Finally, be respectful of the environment and other visitors, and follow any posted rules or guidelines to ensure a safe and enjoyable experience for everyone.
lora微调
数据集构建
官方文档:LLaVA/docs/Finetune_Custom_Data.md at main · haotian-liu/LLaVA · GitHub
图片:我选择的是大连理工大学和苏州大学的标志性地点,一共两张
JSON:u.json,只需要一个就够了,一个json文件可以保存多张图片的信息。
[
{
"id": "dut",
"image": "dut.jpg",
"conversations": [
{
"from": "human",
"value": "<image> Where is this picture?"
},
{
"from": "gpt",
"value": "This is the statue in front of the main building of Dalian University of Technology."
}
]
},
{
"id": "su",
"image": "su.jpeg",
"conversations": [
{
"from": "human",
"value": "<image> Where is this picture?"
},
{
"from": "gpt",
"value": "This photo shows the gate of the former Soochow University."
}
]
}
]
注意:一定要关注好图片和json文件的相对位置,然后编辑json串里面的value值。文件位置如下:
具体操作
首先使用train_mem.py,生成lora矩阵,参数如下:
--lora_enable
True
--lora_r
128
--lora_alpha
256
--mm_projector_lr
2e-5
--model_name_or_path
./llava-v1.5-7b
--version
v1
--data_path
./datasets/lora_data/u.json
--image_folder
./datasets/lora_data
--vision_tower
/media/good/4TB/mn/model/llm/Show-o-main/openai/clip-vit-large-patch14-336
--pretrain_mm_mlp_adapter
./llava-v1.5-7b/mm_projector.bin
--mm_projector_type
mlp2x_gelu
--mm_vision_select_layer
-2
--mm_use_im_start_end
False
--mm_use_im_patch_token
False
--image_aspect_ratio
pad
--group_by_modality_length
True
--bf16
True
--output_dir
./checkpoints/llava-v1.5-7b-lora
--num_train_epochs
10
--gradient_accumulation_steps
1
--evaluation_strategy
"no"
--save_strategy
"steps"
--save_steps
10
--save_total_limit
1
--learning_rate
2e-4
--weight_decay
0.
--warmup_ratio
0.03
--lr_scheduler_type
"cosine"
--logging_steps
1
--tf32
True
--model_max_length
2048
--gradient_checkpointing
True
--lazy_preprocess
True
--report_to
wandb
注意:
- 上面的参数里面选择了wandb,所以会将训练曲线保存至wandb,可以自行查看。如果这里报错,首先检查有没有安装wandb这个库,再检查网络(要挂梯子)。
- 如果运行成功了,但是模型没有走完10轮,可能是你之前lora微调过,把之前的文件夹删了重试。
训练结果:
with torch.enable_grad(), device_autocast_ctx, torch.cpu.amp.autocast(**ctx.cpu_autocast_kwargs): # type: ignore[attr-defined]
10%|█ | 1/10 [00:03<00:32, 3.63s/it]{'loss': 2.6773, 'grad_norm': 8.648663520812988, 'learning_rate': 0.0002, 'epoch': 1.0}
20%|██ | 2/10 [00:05<00:19, 2.40s/it]{'loss': 2.6773, 'grad_norm': 8.589770317077637, 'learning_rate': 0.00019396926207859084, 'epoch': 2.0}
30%|███ | 3/10 [00:06<00:14, 2.02s/it]{'loss': 0.6295, 'grad_norm': 2.518785238265991, 'learning_rate': 0.0001766044443118978, 'epoch': 3.0}
40%|████ | 4/10 [00:08<00:11, 1.88s/it]{'loss': 0.1859, 'grad_norm': 2.069459915161133, 'learning_rate': 0.00015000000000000001, 'epoch': 4.0}
50%|█████ | 5/10 [00:09<00:08, 1.75s/it]{'loss': 0.0535, 'grad_norm': 0.5916153788566589, 'learning_rate': 0.00011736481776669306, 'epoch': 5.0}
60%|██████ | 6/10 [00:11<00:06, 1.70s/it]{'loss': 0.0271, 'grad_norm': 0.24089542031288147, 'learning_rate': 8.263518223330697e-05, 'epoch': 6.0}
70%|███████ | 7/10 [00:13<00:05, 1.69s/it]{'loss': 0.0165, 'grad_norm': 0.25461411476135254, 'learning_rate': 5.000000000000002e-05, 'epoch': 7.0}
80%|████████ | 8/10 [00:14<00:03, 1.68s/it]{'loss': 0.0066, 'grad_norm': 0.10266758501529694, 'learning_rate': 2.339555568810221e-05, 'epoch': 8.0}
90%|█████████ | 9/10 [00:16<00:01, 1.66s/it]{'loss': 0.0032, 'grad_norm': 0.03881971910595894, 'learning_rate': 6.030737921409169e-06, 'epoch': 9.0}
{'loss': 0.0026, 'grad_norm': 0.03475469723343849, 'learning_rate': 0.0, 'epoch': 10.0}
100%|██████████| 10/10 [00:18<00:00, 1.67s/it]You are using a model of type llava to instantiate a model of type llava_llama. This is not supported for all configurations of models and can yield errors.
100%|██████████| 10/10 [00:24<00:00, 2.44s/it]
{'train_runtime': 26.1553, 'train_samples_per_second': 0.765, 'train_steps_per_second': 0.382, 'train_loss': 0.6279424592852593, 'epoch': 10.0}
训练完成后需要合并lora权重,所谓合并模型权重,就是先加载一遍base权重,再加载lora权重,最后再将两个权重加起来,重新保存。
使用scripts/merge_lora_weights.py。
--model-path
"./checkpoints/llava-v1.5-7b-lora"
--model-base
"./llava-v1.5-7b"
--save-model-path
"./checkpoints/llava-v1.5-7b-merged"
训练结果:
Loading LLaVA from base model...
Loading checkpoint shards: 100%|██████████| 2/2 [00:00<00:00, 6.17it/s]
Loading additional LLaVA weights...
Loading LoRA weights...
Merging LoRA weights...
Model is loaded...
这样,就能得到可以直接用于推理的模型了,这个模型现在存储在./checkpoints/llava-v1.5-7b-merged
文件夹下。
然后我们调用inference.py,使用lora合并后的权重
from llava.model.builder import load_pretrained_model
from llava.mm_utils import get_model_name_from_path
from llava.eval.run_llava import eval_model
# model_path = "llava-v1.5-7b"
model_path = "checkpoints/llava-v1.5-7b-merged"
# prompt = "What are the things I should be cautious about when I visit here?"
prompt = "Where is here?"
# image_file = "datasets/view.jpg"
image_file = "datasets/lora_data/su.jpeg"
args = type('Args', (), {
"model_path": model_path,
"model_base": None,
"model_name": get_model_name_from_path(model_path),
"query": prompt,
"conv_mode": None,
"image_file": image_file,
"sep": ",",
"temperature": 0,
"top_p": None,
"num_beams": 1,
"max_new_tokens": 512
})()
eval_model(args)
This photo shows the gate of the former Soochow University.
如果没有微调,模型不会知道这张照片展示的是苏州大学旧址的门。
代码解读
训练lora矩阵
train_mem.py没什么好说的,我们直接进入train.py。
train.py里面的代码主要是配置参数,这里不多赘述,微调时进行配置的部分是下面的代码。
if training_args.lora_enable:
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=training_args.lora_r,
lora_alpha=training_args.lora_alpha,
target_modules=find_all_linear_names(model),
lora_dropout=training_args.lora_dropout,
bias=training_args.lora_bias,
task_type="CAUSAL_LM",
)
if training_args.bits == 16:
if training_args.bf16:
model.to(torch.bfloat16)
if training_args.fp16:
model.to(torch.float16)
rank0_print("Adding LoRA adapters...")
model = get_peft_model(model, lora_config)
我们主要来看train.py的核心代码,这里主要是构建数据集信息,然后构建llava的训练器对象trainer,通过trainer开始训练,最后保存结果。
data_module = make_supervised_data_module(tokenizer=tokenizer, # 数据集信息
data_args=data_args)
trainer = LLaVATrainer(model=model, # llava的训练器类
tokenizer=tokenizer,
args=training_args,
**data_module)
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
trainer.train(resume_from_checkpoint=True)
else:
trainer.train() # 进入这里开始训练
trainer.save_state() # 保存
进入train()函数,其余部分都是配置信息,代码在最后会跳入inner_training_loop()
return inner_training_loop(
args=args,
resume_from_checkpoint=resume_from_checkpoint,
trial=trial,
ignore_keys_for_eval=ignore_keys_for_eval,
)
inner_training_loop()内部的训练代码是一个双层for循环,包括epoch和dataloader的循环迭代,除了配置信息外,核心是跳入self.training_step()进行训练,然后计算损失,最后进行反向传播。因为代码太长了,这里使用XXX略去不重要的部分。
for epoch in range(epochs_trained, num_train_epochs): # epoch的迭代
XXX
step = -1
for step, inputs in enumerate(epoch_iterator): # dataloader的迭代
XXX
with self.accelerator.accumulate(model):
tr_loss_step = self.training_step(model, inputs) # 返回当前的loss
if XXX
else:
if XXX
tr_loss += tr_loss_step # 总损失
self.current_flos += float(self.floating_point_ops(inputs))
XXX
if XXX
# 反向传播
self.control = self.callback_handler.on_pre_optimizer_step(args, self.state, self.control)
self.optimizer.step()
self.control = self.callback_handler.on_optimizer_step(args, self.state, self.control)
optimizer_was_run = not self.accelerator.optimizer_step_was_skipped
if optimizer_was_run:
# Delay optimizer scheduling until metrics are generated
if not isinstance(self.lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
self.lr_scheduler.step()
model.zero_grad()
self.state.global_step += 1
self.state.epoch = epoch + (step + 1 + steps_skipped) / steps_in_epoch
self.control = self.callback_handler.on_step_end(args, self.state, self.control)
self._maybe_log_save_evaluate(tr_loss, grad_norm, model, trial, epoch, ignore_keys_for_eval)
else:
self.control = self.callback_handler.on_substep_end(args, self.state, self.control)
我们继续来看training_step(),training_step首先会跳入compute_loss,compute_loss会调用model进行预测,以上函数不包含太多有用信息,故不一一展示了。主要是这里的model正是推理时跳入的LlavaLlamaForCausalLM,详细情况请看上面的3.代码/推理/generate部分。
outputs = model(**inputs)
这里的model最终调用的是LlavaLlamaForCausalLM的forward方法,详情请看推理/generate部分
主要的区别是这里多了label的部分,这部分内容用于计算损失函数,然后作为结果返回。
- 需要注意的是:这里的shift_labels里面,前面的输入和后面的pad都是-100,只有预测部分(模型的回答)有数值,可供损失函数计算。因为在自然语言处理(NLP)任务中,-100通常用作一个特殊的标记,表示在该位置的标签不应该被计算在损失中。这种情况通常出现在序列到序列(seq2seq)模型中,其中某些标记(如填充标记)不应该对损失函数产生影响。
if labels is not None:
# Upcast to float if we need to compute the loss to avoid potential precision issues
logits = logits.float()
# Shift so that tokens < n predict n
shift_logits = logits[..., :-1, :].contiguous() # 将对数几率向右移动一位,以匹配标签的对应位置。
shift_labels = labels[..., 1:].contiguous() # 将标签向右移动一位,以匹配移动后的对数几率。
# Flatten the tokens
loss_fct = CrossEntropyLoss()
shift_logits = shift_logits.view(-1, self.config.vocab_size)
shift_labels = shift_labels.view(-1)
# Enable model parallelism
shift_labels = shift_labels.to(shift_logits.device)
loss = loss_fct(shift_logits, shift_labels)
最后调用trainer.save_state()保存模型
def save_state(self):
"""
Saves the Trainer state, since Trainer.save_model saves only the tokenizer with the model
Under distributed environment this is done only for a process with rank 0.
"""
if not self.is_world_process_zero():
return
path = os.path.join(self.args.output_dir, "trainer_state.json")
self.state.save_to_json(path)
merge_lora_weights.py
合并的步骤在load_pretrained_model()
import argparse
from llava.model.builder import load_pretrained_model
from llava.mm_utils import get_model_name_from_path
def merge_lora(args):
model_name = get_model_name_from_path(args.model_path)
tokenizer, model, image_processor, context_len = load_pretrained_model(args.model_path, args.model_base, model_name, device_map='cuda')
model.save_pretrained(args.save_model_path)
tokenizer.save_pretrained(args.save_model_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model-path", type=str, required=True)
parser.add_argument("--model-base", type=str, required=True)
parser.add_argument("--save-model-path", type=str, required=True)
args = parser.parse_args()
merge_lora(args)
我们直接来看load_pretrained_model(),具体来说,这里会加载原始矩阵和lora矩阵,然后做一些调整,然后使用peft库进行合并。最后如果模型是llava模型,则对分词器添加几个特殊标记,同时加载CLIP。
if 'llava' in model_name.lower():
# Load LLaVA model
if 'lora' in model_name.lower() and model_base is None:
warnings.warn('There is `lora` in model name but no `model_base` is provided. If you are loading a LoRA model, please provide the `model_base` argument. Detailed instruction: https://github.com/haotian-liu/LLaVA#launch-a-model-worker-lora-weights-unmerged.')
if 'lora' in model_name.lower() and model_base is not None:
from llava.model.language_model.llava_llama import LlavaConfig
lora_cfg_pretrained = LlavaConfig.from_pretrained(model_path)
# 首先还是和没有lora一样加载base模型预训练权重
tokenizer = AutoTokenizer.from_pretrained(model_base, use_fast=False)
print('Loading LLaVA from base model...')
model = LlavaLlamaForCausalLM.from_pretrained(model_base, low_cpu_mem_usage=True, config=lora_cfg_pretrained, **kwargs) # llava
# 如果模型头部的输出特征数量与输入特征数量不匹配,则根据需要调整模型头部和嵌入层的权重
token_num, tokem_dim = model.lm_head.out_features, model.lm_head.in_features
if model.lm_head.weight.shape[0] != token_num:
model.lm_head.weight = torch.nn.Parameter(torch.empty(token_num, tokem_dim, device=model.device, dtype=model.dtype))
model.model.embed_tokens.weight = torch.nn.Parameter(torch.empty(token_num, tokem_dim, device=model.device, dtype=model.dtype))
print('Loading additional LLaVA weights...')
# 如果lora权重(即model_path)在本地就直接从本地加载至`non_lora_trainables`变量,否则把路径当huggingface hub仓库名从远端下载至`non_lora_trainables`变量内。
if os.path.exists(os.path.join(model_path, 'non_lora_trainables.bin')):
non_lora_trainables = torch.load(os.path.join(model_path, 'non_lora_trainables.bin'), map_location='cpu') # lora
else:
# this is probably from HF Hub
from huggingface_hub import hf_hub_download
def load_from_hf(repo_id, filename, subfolder=None):
cache_file = hf_hub_download(
repo_id=repo_id,
filename=filename,
subfolder=subfolder)
return torch.load(cache_file, map_location='cpu')
non_lora_trainables = load_from_hf(model_path, 'non_lora_trainables.bin')
non_lora_trainables = {(k[11:] if k.startswith('base_model.') else k): v for k, v in non_lora_trainables.items()}
# 调整非LoRA部分的模型参数字典的键名,并加载这些参数到模型中。
if any(k.startswith('model.model.') for k in non_lora_trainables):
non_lora_trainables = {(k[6:] if k.startswith('model.') else k): v for k, v in non_lora_trainables.items()}
model.load_state_dict(non_lora_trainables, strict=False)
from peft import PeftModel
print('Loading LoRA weights...')
model = PeftModel.from_pretrained(model, model_path) # 从model_path中加载LoRA权重
print('Merging LoRA weights...')
model = model.merge_and_unload() # 合并LoRA权重到模型中
print('Model is loaded...')
XXX
if 'llava' in model_name.lower(): # 如果模型是llava模型,则对分词器添加几个特殊标记,同时加载CLIP
mm_use_im_start_end = getattr(model.config, "mm_use_im_start_end", False)
mm_use_im_patch_token = getattr(model.config, "mm_use_im_patch_token", True)
if mm_use_im_patch_token:
tokenizer.add_tokens([DEFAULT_IMAGE_PATCH_TOKEN], special_tokens=True)
if mm_use_im_start_end:
tokenizer.add_tokens([DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN], special_tokens=True)
model.resize_token_embeddings(len(tokenizer))
vision_tower = model.get_vision_tower()
if not vision_tower.is_loaded:
vision_tower.load_model(device_map=device_map)
if device_map != 'auto':
vision_tower.to(device=device_map, dtype=torch.float16)
image_processor = vision_tower.image_processor
# 返回的max_sequence_length为model.config.max_sequence_length,如果没有这个属性则无脑返回2048
if hasattr(model.config, "max_sequence_length"):
context_len = model.config.max_sequence_length
else:
context_len = 2048
return tokenizer, model, image_processor, context_len
最后返回merge_lora_weights.py,保存矩阵。
训练:预训练Pretrain
训练部分的内容其实和微调很像,这里就简要介绍一下。
具体操作
预训练数据集:liuhaotian/LLaVA-CC3M-Pretrain-595K · Datasets at HF Mirror
预训练所需的vicuna权重:lmsys/vicuna-7b-v1.5 · HF Mirror
根据官方消息,在1x A100 (80G)上预训练LLaVA-13B,所需时间大约为33小时.
在单卡训练时,还需要保证per_device_train_batch_size
* gradient_accumulation_steps=128
具体的参数如下:
--model_name_or_path
./checkpoints/vicuna-7b
--version
v1
--data_path
./datasets/LLaVA-CC3M-Pretrain-595K/chat.json
--image_folder
./datasets/LLaVA-CC3M-Pretrain-595K/images
--vision_tower
/media/good/4TB/mn/model/llm/Show-o-main/openai/clip-vit-large-patch14-336
--tune_mm_mlp_adapter
True
--mm_vision_select_layer
-2
--mm_use_im_start_end
False
--mm_use_im_patch_token
False
--bf16
True
--output_dir
./checkpoints/llava-7b-pretrain
--num_train_epochs
1
--per_device_train_batch_size
16
--per_device_eval_batch_size
4
--gradient_accumulation_steps
8
--evaluation_strategy
"no"
--save_strategy
"steps"
--save_steps
1
--save_total_limit
1
--learning_rate
2e-3
--weight_decay
0.
--warmup_ratio
0.03
--lr_scheduler_type
"cosine"
--logging_steps
1
--tf32
True
--model_max_length
2048
--gradient_checkpointing
True
--lazy_preprocess
True
--report_to
wandb
输出结果如下:
0%| | 0/4651 [00:00<?, ?it/s]
0%| | 1/4651 [01:07<87:44:38, 67.93s/it]{'loss': 7.5866, 'grad_norm': 21.333703994750977, 'learning_rate': 1.4285714285714285e-05, 'epoch': 0.0}
0%| | 2/4651 [02:13<85:35:34, 66.28s/it]{'loss': 7.5451, 'grad_norm': 21.18494415283203, 'learning_rate': 2.857142857142857e-05, 'epoch': 0.0}
代码部分没什么好说的,基本上和lora微调的生成lora矩阵一模一样,故不再赘述。
flash-attention
我在调试代码时注意到了一点,似乎训练时,llava是一次性输出全部token,而不是自回归的输出?带着这个疑问,我详细拆解了代码,其实这里是llava调用了flash-attnention库,这个库可以不完整初始化attention-mask矩阵,从而实现快速计算,因此,这里不是一次性输出整个句子,而是通过flash-attention内部快速计算得到的。相对的,推理时能明确看到自回归推理的步骤。
Flash Attention 通过引入统计量和改变注意力机制的计算顺序,避免了实例化完整的注意力矩阵,将显存复杂度从O(N^2)降低到了O(N),其中N是序列长度。
训练:指令微调Visual Instruction Tuning
具体操作
projector权重:liuhaotian/llava-336px-pretrain-llama-2-7b-chat at main
数据集的json:liuhaotian/LLaVA-Instruct-150K · Datasets at HF Mirror
coco数据集:COCO - Common Objects in Context
官方提供的sh文件:https://github.com/haotian-liu/LLaVA/blob/main/scripts/finetune_full_schedule.sh
具体参数如下:
--model_name_or_path
./checkpoints/vicuna-7b
--version
v1
--data_path
./datasets/LLaVA-Instruct-150K/llava_instruct_150k.json
--image_folder
./datasets/coco/train2017
--vision_tower
/media/good/4TB/mn/model/llm/Show-o-main/openai/clip-vit-large-patch14-336
--pretrain_mm_mlp_adapter
./checkpoints/llava-336px-pretrain-llama-2-7b-chat/mm_projector.bin
--mm_vision_select_layer
-2
--mm_use_im_start_end
False
--mm_use_im_patch_token
False
--bf16
True
--output_dir
./checkpoints/llava-vicuna-v1-3-7b-finetune
--num_train_epochs
3
--per_device_train_batch_size
16
--per_device_eval_batch_size
4
--gradient_accumulation_steps
1
--evaluation_strategy
"no"
--save_strategy
"steps"
--save_steps
50000
--save_total_limit
1
--learning_rate
2e-5
--weight_decay
0.
--warmup_ratio
0.03
--lr_scheduler_type
"cosine"
--logging_steps
1
--tf32
True
--model_max_length
2048
--gradient_checkpointing
True
--dataloader_num_workers
4
--lazy_preprocess
True
--report_to
wandb
4.总结
LLaVA,这个大型语言和视觉助手,以其卓越的多模态能力在人工智能领域脱颖而出。它不仅集成了视觉编码器和语言模型,还通过端到端训练的方式,实现了对视觉和语言信息的深度融合与理解,这在多模态交互领域是一个巨大的飞跃。
LLaVA的核心优势在于其能够处理和理解复杂的视觉和语言数据,提供更为精准和丰富的信息处理能力。它的设计允许模型在保持高性能的同时,对高分辨率图像进行有效处理,这一点在LLaVA-1.5-HD版本中得到了显著的提升。通过AnyRes策略,LLaVA能够灵活地处理各种分辨率的图像,无论是分割处理还是尺寸调整,都能确保图像信息的完整性和准确性。
此外,LLaVA的意义不仅在于其技术层面的突破,更在于它为未来人工智能的发展提供了新的可能性。它在视觉问答、自然语言处理、图像生成等多个领域的应用,展示了多模态人工智能在解决复杂问题时的巨大潜力。LLaVA的性能在多个权威数据平台上的测试结果均达到了顶尖水平,与业界领先的GPT-4V相媲美,这不仅证明了其技术的成熟度,也为多模态人工智能的进一步研究和应用奠定了坚实的基础。
总的来说,LLaVA不仅是一个技术成就,更是一个里程碑,它标志着人工智能在理解和处理复杂世界信息方面迈出了重要的一步,为未来的技术创新和应用开辟了新的道路。
标签:模态,1.5,cur,--,self,ids,LLaVA,model,image From: https://blog.csdn.net/sherlockMa/article/details/143186119