当前位置: 首页 > article >正文

视觉语言大模型VisualGLM-6B环境配置与模型部署

1. 概述

VisualGLM-6B 是一个开源的,支持图像、中文和英文的多模态对话语言模型。依靠来自于 CogView 数据集的 30M 高质量中文图文对,与 300M 经过筛选的英文图文对进行预训练,中英文权重相同。模型在微调阶段使用长视觉问答数据进行训练,以生成符合人类偏好的答案。

它由 SwissArmyTransformer(简称sat)库训练,这是一个支持 Transformer 灵活修改、训练的工具库,支持Lora、P-tuning等参数高效微调方法。并且提供了符合用户习惯的 huggingface 接口,也提供了基于 sat 的接口。

Github地址:https://github.com/THUDM/VisualGLM-6B

在这里插入图片描述

Huggingface 地址:https://huggingface.co/THUDM/visualglm-6b

在这里插入图片描述

网页Demo地址:https://huggingface.co/spaces/lykeven/visualglm-6b

在这里插入图片描述

2. 模型架构

VisualGLM 主要由三个模块构成:

  • ViT
  • Q-Former
  • ChatGLM-6B

ViT(Vision Transformer)用于提取图像的视觉特征,Q-Former(Query Transformer) 作为中间模块将视觉特征转化为语言模型可以理解的表示,ChatGLM-6B 用于生成文本或回答问题。

在训练过程中,ViT 和 ChatGLM-6B 的参数几乎是冻结的,主要对 Q-Former 进行微调。在预训练阶段,对 Q-Former 和 ViT LoRA 进行训练。在微调阶段对 QFormer 和 ChatGLM LoRA 进行训练。

训练目标是自回归损失(根据图像生成正确的文本)和对比损失(输入 ChatGLM 的视觉特征与对应文本的语义特征对齐)。

在这里插入图片描述

2.1 ViT

受到 NLP 领域中 Transformer 成功应用的启发,ViT 算法由 Google 于 2020 年提出,尝试将标准的 Transformer 结构直接应用于图像,并对整个图像分类流程进行最少的修改。具体来讲,ViT 算法中,会将整幅图像拆分成小图像块,然后把这些小图像块的线性嵌入序列作为 Transformer 的输入送入网络,然后使用监督学习的方式进行图像分类的训练。这种方法摆脱了 CNN 的局限性,完全依赖于 Transformer 模型的注意力机制。
在这里插入图片描述

2.1.1 图像分块嵌入

考虑到在 Transformer 结构中,输入是一个二维的矩阵,矩阵的形状可以表示为 ( N , D ) (N,D) (N,D) ,其中 N 是图像序列的长度,而 D 是序列中每个向量的维度。因此,在ViT算法中,首先需要设法将 H × W × C H×W×C H×W×C 的三维图像转化为 ( N , D ) (N,D) (N,D) 的二维输入。

ViT中的具体实现方式为:将 H × W × C H×W×C H×W×C 的图像,变为一个 N × ( p 2 ∗ C ) N×(p^2*C) N×(p2C) 的序列。这个序列可以看作是一系列展平的图像块(patch),也就是将图像切分成小块后,再将其展平。该序列中一共包含了 N = H W / p 2 N=HW/p^2 N=HW/p2 个图像块,每个图像块的维度则是 ( p 2 ∗ C ) (p^2*C) (p2C) 。其中 P P P 是图像块的大小, C C C 是通道数量。经过如上变换,就可以将 N 视为序列的长度了。

但是,此时每个图像块的维度是 ( P 2 ∗ C ) (P^2∗C) (P2C) ,而我们实际需要的向量维度是 D ,因此我们还需要对图像块进行 Embedding。这里 Embedding 的方式非常简单,只需要对每个 ( P 2 ∗ C ) (P^2∗C) (P2C) 的图像块做一个线性变换,将维度压缩为 D 即可。
在这里插入图片描述

2.1.2 添加类别嵌入

假设我们将原始图像切分成 3 × 3 3×3 3×3 共 9 个小图像块,最终的输入序列长度却是 10,也就是说我们这里人为的增加了一个向量进行输入,我们通常将人为增加的这个向量称为 Class Token。那么这个 Class Token 有什么作用呢?

我们可以想象,如果没有这个向量,也就是将 N=9 个向量输入 Transformer 结构中进行编码,我们最终会得到 9 个编码向量,可对于图像分类任务而言,我们应该选择哪个输出向量进行后续分类呢?因此,ViT算法提出了一个可学习的嵌入向量 Class Token,将它与9个向量一起输入到 Transformer 结构中,输出10个编码向量,然后用这个 Class Token 进行分类预测即可。 也就是说 Class Token 的作用就是寻找其他 9 个输入向量对应的类别。

在这里插入图片描述

2.1.3 添加位置编码

按照 Transformer 结构中的位置编码习惯,ViT 也使用了位置编码。不同的是,ViT 中的位置编码没有采用原版 Transformer 中的 sin/cos 编码,而是直接设置为可学习的 Positional Encoding。对训练好的 Positional Encoding 进行可视化,如图所示。我们可以看到,位置越接近,往往具有更相似的位置编码。此外,出现了行列结构,同一行/列中的 patch 具有相似的位置编码。

在这里插入图片描述

将每个 patch 的嵌入向量与位置编码相加,形成最终的 patch 表示。

在这里插入图片描述

2.1.4 输入Transformer

ViT 的主要结构与Transformer的编码器(encoder)非常相似,主要由多头自注意力机制(Multi-Head Self-Attention, MSA)和多层感知机(Multi-Layer Perceptron, MLP)两部分组成。其中:

  • MSA:通过注意力机制学习图像块(patch)之间的相依关系。
  • MLP:将图像表示转换为更抽象的特征。

此外,ViT 还包含以下设计:

  • Layer Normalization(层归一化):与标准 Transformer 略有不同,ViT 将 Layer Normalization 的位置移动到了 MSA 和 MLP 模块之前,而不是模块之后。
  • Residual Connection(残差连接):与 Transformer 一致,ViT 利用残差连接保持梯度传播的稳定性,从而提高训练深度模型的效果。
  • 非线性激活函数:MLP 中使用的激活函数由 ReLU 替换为平滑化的 GeLU(高斯误差线性单元,Gaussian Error Linear Unit),进一步提升了模型的非线性表达能力。

在这里插入图片描述

Transformer 结构中最重要的结构就是 Multi-head Attention,即多头注意力结构。假设有 2 个 head 。

在这里插入图片描述

2.1.5 分类头MLP Head

得到输出后,ViT 中使用了 MLP Head 对输出进行分类处理,这里的 MLP Head 由 LayerNorm 和两层全连接层组成,并且采用了 GELU 激活函数。

2.1.6 代码复现

class VisionTransformer(nn.Layer):
    def __init__(self,
                 img_size=224,
                 patch_size=16,
                 in_chans=3,
                 class_dim=1000,
                 embed_dim=768,
                 depth=12,
                 num_heads=12,
                 mlp_ratio=4,
                 qkv_bias=False,
                 qk_scale=None,
                 drop_rate=0.,
                 attn_drop_rate=0.,
                 drop_path_rate=0.,
                 norm_layer='nn.LayerNorm',
                 epsilon=1e-5,
                 **args):
        super().__init__()
        self.class_dim = class_dim

        self.num_features = self.embed_dim = embed_dim
        # 图片分块和降维,块大小为patch_size,最终块向量维度为768
        self.patch_embed = PatchEmbed(
            img_size=img_size,
            patch_size=patch_size,
            in_chans=in_chans,
            embed_dim=embed_dim)
        # 分块数量
        num_patches = self.patch_embed.num_patches
        # 可学习的位置编码
        self.pos_embed = self.create_parameter(
            shape=(1, num_patches + 1, embed_dim), default_initializer=zeros_)
        self.add_parameter("pos_embed", self.pos_embed)
        # 人为追加class token,并使用该向量进行分类预测
        self.cls_token = self.create_parameter(
            shape=(1, 1, embed_dim), default_initializer=zeros_)
        self.add_parameter("cls_token", self.cls_token)
        self.pos_drop = nn.Dropout(p=drop_rate)

        dpr = np.linspace(0, drop_path_rate, depth)
        # transformer
        self.blocks = nn.LayerList([
            Block(
                dim=embed_dim,
                num_heads=num_heads,
                mlp_ratio=mlp_ratio,
                qkv_bias=qkv_bias,
                qk_scale=qk_scale,
                drop=drop_rate,
                attn_drop=attn_drop_rate,
                drop_path=dpr[i],
                norm_layer=norm_layer,
                epsilon=epsilon) for i in range(depth)
        ])

        self.norm = eval(norm_layer)(embed_dim, epsilon=epsilon)

        # Classifier head
        self.head = nn.Linear(embed_dim,
                              class_dim) if class_dim > 0 else Identity()

        trunc_normal_(self.pos_embed)
        trunc_normal_(self.cls_token)
        self.apply(self._init_weights)
    # 参数初始化
    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight)
            if isinstance(m, nn.Linear) and m.bias is not None:
                zeros_(m.bias)
        elif isinstance(m, nn.LayerNorm):
            zeros_(m.bias)
            ones_(m.weight)
    
    def forward_features(self, x):
        B = paddle.shape(x)[0]
        # 将图片分块,并调整每个块向量的维度
        x = self.patch_embed(x)
        # 将class token与前面的分块进行拼接
        cls_tokens = self.cls_token.expand((B, -1, -1))
        x = paddle.concat((cls_tokens, x), axis=1)
        # 将编码向量中加入位置编码
        x = x + self.pos_embed
        x = self.pos_drop(x)
        # 堆叠 transformer 结构
        for blk in self.blocks:
            x = blk(x)
        # LayerNorm
        x = self.norm(x)
        # 提取分类 tokens 的输出
        return x[:, 0]

    def forward(self, x):
        # 获取图像特征
        x = self.forward_features(x)
        # 图像分类
        x = self.head(x)
        return x

2.2 Q-Former

Q-Former 是一种在多模态大模型中用于视觉-语言对齐的轻量级 Transformer 结构。在冻结的视觉模型和大语言模型之间引入可学习的查询向量集。它的核心是拿一组预定义好的、可学的、固定数量(K 个)的 Query tokens,通过交叉注意力(cross attention , xattn)层去融合来自从冻结的视觉模型中提取到的关键特征信息(image token)。

具体来讲,图像通过预训练好的视觉编码器进行特征提取后得到视觉语义 f v ∈ R M × D f_v \in \mathbb{R}^{M \times D} fvRM×D 。我们给定 K 个可学习的 Query tokens,符号表示为 V Q ∈ R K × D V_Q \in \mathbb{R}^{K \times D} VQRK×D 。经过多个 Q-Former Blocks的处理,输出的 Transferred vision representation 可表示为 E V ∈ R K × D E_V \in \mathbb{R}^{K \times D} EVRK×D ,作为输入语言模型的输入。并且为了让 E V E_V EV 含有充分的视觉语义,Q-Former采用了交叉注意力机制融合图像语义和可学习Q。

在这里插入图片描述

2.3 ChatGLM-6B

ChatGLM 是一个基于 Transformer 的开源的、支持中英双语的对话语言模型,参数范围从 60 亿到 1300 亿不等。由智谱 AI 和清华大学知识工程组(KEG)联合开发。它以 GLM (General Language Model) 框架为基础,ChatGLM 模型在庞大的中文和英文语料库上进行训练,针对问答和对话交互进行了优化。该系列包括 ChatGLM-6B、ChatGLM2-6B 和最新的 ChatGLM3-6B,每一代都在前一代的基础上进行了性能增强、更长的上下文理解和更高效的推理能力。适用于多种自然语言处理任务,如问答、翻译、摘要和对话生成。

ChatGLM 主要包含 3 个主要模块:embedding、GLMBlock 和 Linear 。
在这里插入图片描述

3. 模型推理

下面笔者在 Linux 和 Windows 环境下对 VisualGLM-6B 模型进行推理和部署。

3.1 Windows

代码下载:

git clone https://github.com/THUDM/VisualGLM-6B.git
cd VisualGLM-6B-main

环境部署:

conda create -n visualglm python=3.8 -y
conda activate visualglm
conda install pytorch==2.0.0 torchvision==0.15.0 torchaudio==2.0.0 pytorch-cuda=11.7 -c pytorch -c nvidia
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

此时默认会安装deepspeed库(支持sat库训练),此库对于模型推理并非必要,同时部分Windows环境安装此库时会遇到问题。 如果想绕过deepspeed安装,我们可以将命令改为如果安装 deepspeed 时报错,尝试禁用 ops 编译:

pip install -r requirements_wo_ds.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install --no-deps "SwissArmyTransformer>=0.4.4" -i https://pypi.tuna.tsinghua.edu.cn/simple

如果 SwissArmyTransformer 安装版本太高,运行代码会报如下错误。

TypeError: sat.model.transformer.BaseTransformer() got multiple values for keyword argument 'parallel_output'

如果使用Huggingface transformers库调用模型,也需要安装上述依赖包,可以通过如下代码调用 VisualGLM-6B 模型来生成对话:

from transformers import AutoTokenizer, AutoModel

tokenizer = AutoTokenizer.from_pretrained("THUDM/visualglm-6b", trust_remote_code=True)
model = AutoModel.from_pretrained("THUDM/visualglm-6b", trust_remote_code=True).half().cuda()
image_path = "13.jpg"
response, history = model.chat(tokenizer, image_path, "描述这张图片。", history=[])
print(response)
response, history = model.chat(tokenizer, image_path, "这张图片可能是在什么场所拍摄的?", history=history)
print(response)

以上代码会由 transformers 自动下载模型实现和参数。完整的模型实现可以在 Hugging Face Hub。如果从 Hugging Face Hub 上下载模型参数的速度较慢,可以从本地加载模型。

下面重点介绍在本地加载模型,需要先安装Git LFS

git lfs install
git clone https://huggingface.co/THUDM/visualglm-6b

在这里插入图片描述
如果从 Hugging Face Hub 上下载 checkpoint 的速度较慢,可以先使用git clone 或手动下载所需要的配置文件,然后从这里手动下载模型参数文件。
在这里插入图片描述
将下载好的模型参数文件放到 visualglm-6b 下。注意 Hugging Face Hub 上所有的文件都要下载。

模型下载完成后,安装 triton。注意 Windows 安装会报错。
解决方法:从 Hugging 这个地址下载 triton-windows-builds ,然后使用下面命令进行安装。

pip install E:/triton-2.1.0-cp310-cp310-win_amd64.whl

在这里插入图片描述
在这里插入图片描述
再次运行下面代码,会报如下错误:

TypeError: sat.model.transformer.BaseTransformer() got multiple values for keyword argument 'parallel_output'

解决方法:

from transformers import AutoTokenizer, AutoModel

# 本地模型路径
local_model_path = "visualglm-6b"

tokenizer = AutoTokenizer.from_pretrained(local_model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(local_model_path, trust_remote_code=True).half().cuda()

# 载入图片
image_path = "13.jpg"

# 交互对话
response, history = model.chat(tokenizer, image_path, "描述这张图片。", history=[])
print(response)

response, history = model.chat(tokenizer, image_path, "这张图片可能是在什么场所拍摄的?", history=history)
print(response)

模型权重大小太大,可能会因为系统内存不足报错。笔者对模型使用量化进行推理。

# 按需修改,目前只支持 4/8 bit 量化
model = AutoModel.from_pretrained("chatglm-6b", trust_remote_code=True).quantize(8).half().cuda()

3.2 Linux

报错一:

AttributeError: 'ChatGLMTokenizer' object has no attribute 'sp_tokenizer'

降低 transformers 版本,笔者安装了 4.30.0。

报错二:

TypeError: type object got multiple values for keyword argument 'parallel_output'

降低 SwissArmyTransformer 版本,笔者安装了 0.4.7 。

3.3 推理结果

图片:
在这里插入图片描述

推理结果:
在这里插入图片描述

4. 模型量化

默认情况下,模型以 FP16 精度加载,运行推理代码需要大概 几十GB 的显存。如果 GPU 显存有限,可以尝试以量化方式加载模型,但是需要在内存中首先加载 FP16 格式的模型。使用方法如下:

# 按需修改,目前只支持 4/8 bit 量化
model = AutoModel.from_pretrained("THUDM/chatglm-6b", trust_remote_code=True).quantize(8).half().cuda()

随着对话轮数的增多,对应消耗显存也随之增长。模型量化会带来一定的性能损失,经过测试,在 4-bit 量化下仍然能够进行自然流畅的生成。可以使用 GPT-Q 等量化方案进一步压缩量化精度/提升相同量化精度下的模型性能。

5. 模型部署

笔者主要在云服务器上进行部署,生成接口,在 Windows 上使用。

5.1 命令行交互

python cli_demo_hf.py

Huggingface 的模型需要运行 cli_demo_hf.py,而不是 cli_demo.py,后者支持的是sat格式的模型。在命令行中进行交互式的对话,输入指示并回车即可生成回复,输入 clear 可以清空对话历史,输入 stop 终止程序。

如果要使用cli_demo.py,可以直接运行代码,会自动从清华云盘下载模型(一个zip包,支持断点续传)并解压(也可以在visualglm.py中找到zip包的地址手动下载)。

在这里插入图片描述
如果是本地下载的模型,记得修改这里的模型路径。
在这里插入图片描述

5.2 网页交互

python web_demo_hf.py

同cli_demo_hf。

除此自外,网页版接受命令行参数–share以生成 gradio 公开链接,接受–quant 4和–quant 8以分别使用4比特量化/8比特量化减少显存占用。

在这里插入图片描述
问题一:
在这里插入图片描述
gradio 在 4.0 版本以后去除了.style() 函数。因此,笔者安装了 3.35.0 版本。启动web页面生成了地址,但是在网页中无法访问,将在公网访问的功能打开,将 share = False 修改为 share = True 。

python web_demo_hf.py --share

问题二:还是在网页中无法访问。
在这里插入图片描述
首先进入 /site-packages/gradio 文件下,然后下载 frpc_linux_amd64 文件,需要关闭防火墙同时还可能需要代理才可以下载成功。如果下载失败,可以去这个地址 下载,然后将文件重命名为 frpc_linux_amd64_v0.2 ,最后将文件移动到虚拟环境中的 gradio 文件夹中。

wget https://cdn-media.huggingface.co/frpc-gradio-0.2/frpc_linux_amd64
mv frpc_linux_amd64 frpc_linux_amd64_v0.2

可以使用以下命令查看 gradio 的目录。

find / -type d -name "gradio"

最后增加文件可执行权限。

chmod +x frpc_linux_amd64_v0.2

在这里插入图片描述

6. API部署

API(Application Programming Interface)是一组定义好的规则和协议,用于不同软件组件之间的通信。在 Linux 上生成的 API 可能通过多种方式暴露服务,如 RESTful API(基于 HTTP 协议)、RPC(远程过程调用)等。如果是 RESTful API,它会通过 HTTP 请求(如 GET、POST、PUT、DELETE 等方法)来提供服务;如果是 RPC,可能会使用专门的 RPC 协议,如 gRPC。

Linux 服务器(运行 API 的服务器)和 Windows 客户端需要能够通过网络进行通信。这可能涉及配置防火墙规则,允许从 Windows 机器访问 Linux 机器上的 API 服务端口。例如,如果你的 API 在 Linux 上运行在端口 8080,你需要确保防火墙(如 iptables)允许入站连接到该端口。

6.1 Linux生成接口

首先安装 fastapi uvicorn 包。

pip install fastapi uvicorn

同理,本地加载模型,需要修改模型路径。然后运行 api_hf.py。

在这里插入图片描述

python api_hf.py

默认部署在8080端口,通过 POST 方法进行调用。如果害怕8080比较常用会被占用,可以更改为其他端口,api_hf.py 最后一行方法内,port=可以改成你想要的端口。
在这里插入图片描述
在这里插入图片描述
从图中信息可知,一个基于 Uvicorn 的服务已启动,监听在 http://0.0.0.0:8080 。0.0.0.0 表示监听所有可用网络接口。

6.2 Linux测试

测试的时候不能结束命令,需要重新打开一个终端。本地测试用 127.0.0.1 访问。

echo "{\"image\":\"$(base64 13.jpg)\",\"text\":\"描述这张图片\",\"history\":[]}" > temp.json
curl -X POST -H "Content-Type: application/json" -d @temp.json http://127.0.0.1:8080

在这里插入图片描述

6.3 Windows测试

笔者使用 Python,用requests库来调用 API。

首先,需要安装requests库。

pip install requests

然后,运行以下代码:

import requests
import base64

with open('E:/13.jpg', 'rb') as image_file:
    encoded_string = base64.b64encode(image_file.read()).decode('utf - 8')

data = {
    "image": encoded_string,
    "text": "描述这张图片",
    "history": []
}

response = requests.post('http://127.0.0.1:8080', json = data)
print(response.text)

因为笔者使用的是 AutoDL 云服务器,因此需要使用 SSH 将实例中的端口代理到本地。

首先点击自定义服务,然后下载桌面工具包,运行程序。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


http://www.kler.cn/a/524968.html

相关文章:

  • Jackson中@JsonTypeId的妙用与实例解析
  • 嵌入式经典面试题之操作系统(一)
  • 牛客周赛77:A:JAVA
  • 【ComfyUI专栏】通过软件获取PNG图片中的工作流信息
  • h5 网页测试摄像头
  • MySQL 基础学习(3):排序查询和条件查询
  • C语言编译过程全面解析
  • MySQL知识点总结(十四)
  • 计算机网络 IP 网络层 2 (重置版)
  • 物联网智能项目之——智能家居项目的实现!
  • 网络工程师 (7)进程管理
  • 创建要素图层和表视图
  • 爬虫基础(一)HTTP协议 :请求与响应
  • 剑指 Offer II 007. 数组中和为 0 的三个数
  • 每日一题洛谷P1307 [NOIP2011 普及组] 数字反转c++
  • GitHub Actions定时任务配置完全指南:从Cron语法到实战示例
  • 基于Go语言的三甲医院人机与智能体协同环境系统(上.文章部分)
  • JVM01_概述、跨平台原理、分类、三大商业虚拟机
  • 未来无线技术的发展方向
  • Python 切片(Slicing):列表、元组与字符串的高效操作