LLaVA系列②——从底层构建LLaVA并测试运行(附详细代码+讲解)


引言:感谢 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 个层的主要作用和原理如下:

  1. 视觉层
    作用: 处理输入的图像数据,将图像转换为模型能够理解的特征表示。
    原理: 通常采用预训练的视觉模型,如 CLIP 中的视觉编码器。CLIP 的视觉编码器一般基于 CNN 或 ViT 架构。以 ViT 为例,它会将图像分割成多个固定大小的图像块(patches),然后通过线性投影将每个图像块转换为向量,接着使用 MHA(多头自注意力机制)和 FNN 来学习图像块之间的关系,最终输出图像的特征表示。
  2. 对齐层
    作用: 负责将视觉层输出的图像特征与 后续的语言层输入 进行对齐,使得模型能够在语言和视觉信息之间建立联系。
    原理: 通常会使用一些跨模态的注意力机制或融合方法,常见的做法是将图像特征和语言输入的词嵌入在特定的层进行交互,通过注意力机制让语言模型能够关注到图像中的相关信息。例如,在某些层中,将语言层的查询(query)向量与图像特征(key和value)进行交互,计算注意力权重,从而融合视觉和语言信息。
  3. 语言层
    作用: 用于处理文本输入和生成文本响应。它接收来自对齐层的融合信息,并根据这些信息生成自然语言输出。
    原理: 通常采用 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

指令解释:

  1. --resume-download:启用断点续传。如果下载中断,下次继续下载时将从中断的位置开始,而不是重新下载。
  2. openai/clip-vit-large-patch14-336:这是模型的仓库路径,表示从 Hugging Face 上下载 openai(作者) 发布的 clip-vit-large-patch14-336 模型【这个是视觉层的模型参数】
  3. --local-dir ./model_bank/clip-vit-large-patch14-336/:将下载的模型文件保存到本地的 ./model_bank/clip-vit-large-patch14-336/ 目录中。
  4. --local-dir-use-symlinks 选项用于控制是否使用符号链接(symlinks)来管理下载的文件。当设置为 False 时,意味着不会使用符号链接,而是将模型文件直接复制到指定的本地目录中。如果设置为 True,则会使用符号链接来引用共享的文件,以节省磁盘空间。

  同样的,我们需要把 语言层的模型参数 下载下来。因为 Qwen1.5-4B-Chat(支持中英文) 有一个特点,它的词表空间留有较大的剩余(300左右),也就是说我们可以新添加约 300special 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-Chattokenizer_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 教研室工位 💻

### 关于 LLaVA 大模型的代码实现 对于希望了解或获取 LLaVA 及其改进版本 LLaVA-v1.5 的代码实现,通常这类大型多模态语言模型由研究团队开发开源。这些项目往往托管在 GitHub 或类似的平台上。 #### 获取官方源码 为了获得最准确和最新的代码库,建议访问原始开发者发布的资源链接。根据已知信息,LLaVA 系列模型是由学术界的研究人员所创建,在论文发布的同时一般会带相应的GitHub仓库地址[^1]。 #### 安装依赖环境 大多数现代深度学习框架如 PyTorch 都支持构建此类复杂的神经网络结构。安装必要的 Python 库可以通过 pip 工具完成: ```bash pip install torch torchvision transformers datasets accelerate ``` #### 加载预训练权重 考虑到 LLaVA-v1.5 扩充了数据集规模、增加了模型参数量以及提升了图像处理分辨率,加载经过充分训练后的权重文件至关重要。这一步骤通常是通过 Hugging Face 提供的 `transformers` 库来简化操作: ```python from transformers import AutoModelForVision2Seq, AutoProcessor model_name_or_path = "path_to_llava_v1_5" processor = AutoProcessor.from_pretrained(model_name_or_path) model = AutoModelForVision2Seq.from_pretrained(model_name_or_path) ``` #### 推理过程示例 下面是一个简单的推理脚本例子,用于展示如何使用已经准备好的处理器对象 (`processor`) 和模型实例 (`model`) 来执行预测任务: ```python import requests from PIL import Image from transformers import Blip2Processor, Blip2ForConditionalGeneration def predict(image_url): image = Image.open(requests.get(image_url, stream=True).raw) inputs = processor(images=image, return_tensors="pt") generated_ids = model.generate(**inputs) output_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] return output_text image_url = "https://example.com/path/to/image.jpg" print(predict(image_url)) ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支王同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值