视觉语言大模型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×(p2∗C) 的序列。这个序列可以看作是一系列展平的图像块(patch),也就是将图像切分成小块后,再将其展平。该序列中一共包含了 N = H W / p 2 N=HW/p^2 N=HW/p2 个图像块,每个图像块的维度则是 ( p 2 ∗ C ) (p^2*C) (p2∗C) 。其中 P P P 是图像块的大小, C C C 是通道数量。经过如上变换,就可以将 N 视为序列的长度了。
但是,此时每个图像块的维度是
(
P
2
∗
C
)
(P^2∗C)
(P2∗C) ,而我们实际需要的向量维度是 D ,因此我们还需要对图像块进行 Embedding。这里 Embedding 的方式非常简单,只需要对每个
(
P
2
∗
C
)
(P^2∗C)
(P2∗C) 的图像块做一个线性变换,将维度压缩为 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} fv∈RM×D 。我们给定 K 个可学习的 Query tokens,符号表示为 V Q ∈ R K × D V_Q \in \mathbb{R}^{K \times D} VQ∈RK×D 。经过多个 Q-Former Blocks的处理,输出的 Transferred vision representation 可表示为 E V ∈ R K × D E_V \in \mathbb{R}^{K \times D} EV∈RK×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 将实例中的端口代理到本地。
首先点击自定义服务,然后下载桌面工具包,运行程序。