引言:感谢 B站Up主 良睦路程序员 !这篇博客,主要源于他发布的视频 《自定义多模态大模型LLaVA——LLaVA系列》 和我个人的一些总结。⭐️ ⭐️
一起用
30
分钟来实现最原生态的 LLaVA 吧!😄 (ps:主要是下载/保存模型花时间)
LLaVA系列的上一篇文章链接:LLaVA系列①——LLaVA的快速学习和简单调用(附详细代码+讲解)
LLaVA系列的下一篇文章链接:LLaVA系列③——微调训练LLaVA并进行推理(附详细代码+讲解)
✅ NLP 研 2 选手的学习笔记
笔者简介:Wang Linyong,NPU,2023级,计算机技术
研究方向:文本生成、大语言模型
参考视频:https://www.bilibili.com/video/BV1GS411P74b
1 LLaVA 简介
● 原版的 LLaVA
模型主要有 3
个层: 视觉层、对齐层、语言层。
● LLaVA
(Large Language and Vision Assistant)模型是一种多模态模型,它结合了语言和视觉能力,主要基于现有的语言模型和视觉模型进行构建,4
个层的主要作用和原理如下:
- 视觉层
作用: 处理输入的图像数据,将图像转换为模型能够理解的特征表示。
原理: 通常采用预训练的视觉模型,如 CLIP 中的视觉编码器。CLIP 的视觉编码器一般基于 CNN 或 ViT 架构。以 ViT 为例,它会将图像分割成多个固定大小的图像块(patches),然后通过线性投影将每个图像块转换为向量,接着使用 MHA(多头自注意力机制)和 FNN 来学习图像块之间的关系,最终输出图像的特征表示。 - 对齐层
作用: 负责将视觉层输出的图像特征与 后续的语言层输入 进行对齐,使得模型能够在语言和视觉信息之间建立联系。
原理: 通常会使用一些跨模态的注意力机制或融合方法,常见的做法是将图像特征和语言输入的词嵌入在特定的层进行交互,通过注意力机制让语言模型能够关注到图像中的相关信息。例如,在某些层中,将语言层的查询(query)向量与图像特征(key和value)进行交互,计算注意力权重,从而融合视觉和语言信息。 - 语言层
作用: 用于处理文本输入和生成文本响应。它接收来自对齐层的融合信息,并根据这些信息生成自然语言输出。
原理: 通常采用 Transformer 架构,例如大型语言模型 LLaMA。每个 Transformer 层包含 MHA 和 FNN。MHA 允许模型在处理每个词时,考虑到输入序列中其他词的信息,从而捕捉语言的上下文信息。FNN 则对 MHA 的输出进行非线性变换,进一步提取特征。
2 下载 LLaVA 的视觉层和语言层的模型参数
● 我们首先要配置一下 huggingface 和镜像端点。如果不配置的话,我们下载 15G
的 Llama3 的权重文件会很慢很慢。
● 第一步: 安装 huggingface_hub
包。这个包是 Hugging Face 提供的 Python
客户端库,用于与 Hugging Face Hub
进行交互。
pip install huggingface_hub
● 第二步: 配置 Hugging Face API
的镜像端点(直接在终端 Terminal
输出即可),可以加快模型的下载速度,尤其是在中国大陆网络环境。
export HF_ENDPOINT=https://hf-mirror.com
● 第三步: 使用 huggingface-cli
工具从 Hugging Face Hub
下载指定的模型文件。
huggingface-cli download --resume-download openai/clip-vit-large-patch14-336 --local-dir ./model_bank/clip-vit-large-patch14-336/ --local-dir-use-symlinks False
指令解释:
--resume-download
:启用断点续传。如果下载中断,下次继续下载时将从中断的位置开始,而不是重新下载。- openai/clip-vit-large-patch14-336:这是模型的仓库路径,表示从
Hugging Face
上下载openai(作者)
发布的clip-vit-large-patch14-336
模型【这个是视觉层的模型参数】。--local-dir ./model_bank/clip-vit-large-patch14-336/
:将下载的模型文件保存到本地的./model_bank/clip-vit-large-patch14-336/
目录中。--local-dir-use-symlinks
选项用于控制是否使用符号链接(symlinks)来管理下载的文件。当设置为 False 时,意味着不会使用符号链接,而是将模型文件直接复制到指定的本地目录中。如果设置为 True,则会使用符号链接来引用共享的文件,以节省磁盘空间。
同样的,我们需要把 语言层的模型参数 下载下来。因为 Qwen1.5-4B-Chat
(支持中英文) 有一个特点,它的词表空间留有较大的剩余(300
左右),也就是说我们可以新添加约 300
个 special token
。
huggingface-cli download --resume-download Qwen/Qwen1.5-4B-Chat --local-dir ./model_bank/Qwen1.5-4B-Chat/ --local-dir-use-symlinks False
● 结果展示(我安装在 ~/llama/models/Llama3-8B-Chinese-Chat
目录下的):
3 设置图像的占位符
● 一般图像的占位符一般为 “<image>
”,而原本的千问文本模型(Qwen1.5-4B-Chat)没适配多模态,所以默认分词器会将 <image>
拆分成三个词 “<
”、“image
” 和 “>
”,但我只需要一个就行。
● 首先找到 Qwen1.5-4B-Chat
的 tokenizer_config.json
文件:
● 然后,在tokenizer_config.json
文件里面的 added_tokens_decoder
后面,加上 <image>
的 token_ids(即151646
):
"151646": {
"content": "<image>",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
}
● 同时在tokenizer_config.json
文件里面的 additional_special_tokens
中加上 "<image>"
。添加结果如下图所示:
4 初始化 LLaVA 模型
● 官方的 LLaVA
初始化的代码如下,官方链接:https://huggingface.co/docs/transformers/main/en/model_doc/llava#transformers.LlavaConfig
● 也就说,需要把 视觉层的配置 和 语言层的配置 通过 LLaVA
专属的配置函数 LlavaConfig
集合起来,然后再将这个 “配置” 用来初始化 LLaVA
模型。
● 模型构建和保存代码:
from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer, AutoProcessor, LlavaForConditionalGeneration, LlavaConfig
from PIL import Image
# ------------------------------------------------------------------------------------------------
# 加载CLIP模型和Qwen1.5-4B-Chat模型
clip_model_name_or_path = "./model_bank/clip-vit-large-patch14-336" # CLIP模型的路径
qwen_model_name_or_path = "./model_bank/Qwen1.5-4B-Chat" # Qwen1.5-4B-Chat模型的路径
# 使用AutoModel加载CLIP模型,并自动分配到可用设备
clip_model = AutoModel.from_pretrained(clip_model_name_or_path, device_map="auto")
# 使用AutoModelForCausalLM加载Qwen1.5-4B-Chat模型,并自动分配到可用设备
llm_model = AutoModelForCausalLM.from_pretrained(qwen_model_name_or_path, device_map="auto")
# 使用AutoTokenizer加载Qwen1.5-4B-Chat的tokenizer
llm_tokenizer = AutoTokenizer.from_pretrained(qwen_model_name_or_path)
# ------------------------------------------------------------------------------------------------
# 初始化Llava模型的配置
vision_config = clip_model.vision_model.config # 从CLIP模型中提取视觉模型的配置
text_config = llm_model.config # 从Qwen1.5-4B-Chat模型中提取文本模型的配置
# 使用CLIP的视觉配置和Qwen的文本配置初始化Llava配置
llava_configuration = LlavaConfig(vision_config, text_config)
# 使用Llava配置初始化Llava模型
my_llava_model = LlavaForConditionalGeneration(llava_configuration)
# ------------------------------------------------------------------------------------------------
# 将CLIP模型的视觉层参数赋给Llava模型的视觉层
my_llava_model.vision_tower.vision_model = clip_model.vision_model
# 将Qwen1.5-4B-Chat模型的参数赋给Llava模型的语言层
my_llava_model.language_model = llm_model
# 将Qwen1.5-4B-Chat模型的pad_token_id赋给Llava模型,因为原始的pad_token_id为空
my_llava_model.config.pad_token_id = llm_tokenizer.pad_token_id
# 将Qwen1.5-4B-Chat模型的 image_id 赋给Llava模型,因为原始的 image_id 为 32000(应该是151646才对)
my_llava_model.config.image_token_index = llm_tokenizer.encode("<image>")[0]
# ------------------------------------------------------------------------------------------------
print("正在保存 模型权重...")
my_llava_model.save_pretrained("./my_llava_model/model_01") # 保存模型权重
print("正在保存 分词器...")
llm_tokenizer.save_pretrained("./my_llava_model/model_01") # 保存分词器
print("正在保存 视觉的预处理器...")
autoprocessor = AutoProcessor.from_pretrained(clip_model_name_or_path) # 保存视觉的预处理器
autoprocessor.save_pretrained("./my_llava_model/model_02")
print("llava模型已保存")
# ------------------------------------------------------------------------------------------------
● 运行完之后,需要将 文件夹./my_llava_model/model_02
中的 preprocessor_config.json
文件复制到文件夹 ./my_llava_model/model_01
中(原版的构建过程也是这样的)。
5 测试 LLaVA 模型
● 然后新建一个 python 文件,来做一个简单的测试:
# LlavaProcessor用于处理输入的文本和图像数据,将其转换为模型可以接受的格式
# LlavaForConditionalGeneration是基于条件生成的模型类,用于生成文本
from transformers import LlavaProcessor, LlavaForConditionalGeneration
import torch
from PIL import Image # 导入PIL库中的Image模块,用于处理图像
model_name_or_path = "./my_llava_model/model_01" # 定义模型的路径,这里指定了本地存储的模型目录
llava_processor = LlavaProcessor.from_pretrained(model_name_or_path) # 处理器会加载模型对应的分词器和图像处理器等信息
# device_map="auto"表示自动选择合适的设备(如GPU)来运行模型
# torch_dtype=torch.bfloat16指定模型使用bfloat16数据类型,以减少内存使用和加速计算
model = LlavaForConditionalGeneration.from_pretrained(
model_name_or_path, device_map="cuda:0", torch_dtype=torch.bfloat16
)
# 定义用户输入的文本提示,包含了对图像内容的询问
# <image>是一个特殊标记,表示这里会插入图像信息
prompt_text = "USER: <image>\n What's the content of the image? ASSISTANT:"
# 构建消息列表,包含系统提示和用户提示
# 系统提示用于告知模型其角色和任务,用户提示是具体的问题或请求
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt_text},
]
# 使用Llava的tokenizer的apply_chat_template方法将消息列表转换为模型输入的格式
# tokenize=False表示不进行分词操作,仅生成文本格式的输入
# add_generation_prompt=True表示添加生成提示,以便模型知道何时开始生成文本
prompt = llava_processor.tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
# 定义图像的路径,这里指定了本地存储的图像文件
image_path = "./image_data/test_image_1.jpg"
# 使用PIL库的Image.open方法打开图像文件
image = Image.open(fp=image_path)
# 将文本和图像输入到Llava处理器中,生成模型输入
# text参数传入之前生成的文本提示,images参数传入打开的图像
# return_tensors="pt"表示返回PyTorch张量格式的输入
inputs = llava_processor(text=prompt, images=image, return_tensors="pt")
# print("Input keys:", inputs.keys()) # 检查输入字典的键
# print("Pixel values shape:", inputs["pixel_values"].shape) # 检查图像输入形状
# print("Input IDs shape:", inputs["input_ids"].shape) # 检查文本输入形状
# 将输入数据移动到模型所在的设备(如GPU)
# 遍历输入字典中的每个键值对,将张量移动到模型所在的设备上
for tk in inputs.keys():
inputs[tk] = inputs[tk].to(model.device)
with torch.no_grad():
image_features = model.vision_tower(inputs["pixel_values"])
print("Image features shape:", image_features.last_hidden_state.shape)
# 使用模型生成文本,最多生成50个新token
# **inputs表示将输入字典解包为关键字参数传递给模型的generate方法
# max_new_tokens=50限制了模型最多生成20个新的token
generate_ids = model.generate(**inputs, max_new_tokens=20)
# 解码生成的token,得到最终的文本输出
# skip_special_tokens=False表示不跳过特殊标记,保留所有生成的标记
# clean_up_tokenization_spaces=False表示不清理分词过程中产生的空格
gen_text = llava_processor.batch_decode(
generate_ids, skip_special_tokens=False, clean_up_tokenization_spaces=False
)[0]
# 打印生成的文本
print("\ngen_text:\n", gen_text)
● 这是我测试的图片:
● 这是模型的输出: 文本描述的不准确,但也有部分准确。因为在 LLaVA
初始化过程中的 转换层 的参数是随机的(一般要训练一下,结果就要好一点),所以结果不是很准确,这是正常的现象。
The content of the image is a person holding a baby and a dog.
翻译:图片的内容是一个人抱着一个婴儿和一只狗。
● 再测试一张图片呢: 😄 😄
● 得到的输出:(描述的也不准确,但也有部分准确)
The content of the image is a person sitting on a couch with a laptop in front of them.
翻译:图像的内容是一个人坐在沙发上,面前放着一台笔记本电脑。
6 参考资料
[1] 《自定义多模态大模型LLaVA——LLaVA系列》,B站Up主:良睦路程序员
7 补充说明
LLaVA系列的上一篇文章链接:LLaVA系列①——LLaVA的快速学习和简单调用(附详细代码+讲解)
LLaVA系列的下一篇文章链接:LLaVA系列③——微调训练LLaVA并进行推理(附详细代码+讲解)
● 若有写得不对的地方,或有疑问,欢迎评论交流。
⭐️ ⭐️ 写于2025年3月26日 10:57 教研室工位 💻