Showing Posts From
Transformer
- 05 Jun, 2026
模型FP16和INT8量化的计算流程总结
本文对Pytorch等模型训练框架训练出来的模型,进行浮点半精度转换FP16和定点量化INT8的相关概念和计算流程进行完整的总结。 Pytorch 模型的训练与存储格式 在我们常见的 PyTorch、TensorFlow 等主流深度学习框架中,默认使用的张量(Tensor)数据类型是FP32 (32-bit floating point, 单精度浮点数)。因此,我们在 PyTorch 中定义一个卷积层 nn.Conv2d 或线性层 nn.Linear 的时候,框架所提供的随机初始化的权重参数(Weights)、偏置(Biases),以及在前向传播时所产生的激活值(Activations)等,默认全部占用 32 个比特(即4 字节,FP32)的内存空间。 当我们在 Pytorch 中完成模型训练,使用 torch.save() 保存模型文件到本地硬盘。由于训练过程中默认的张量 dtype 类型是 FP32,所以通过这个标准流程保存的模型也就是 FP32 的。 但是为什么我们从 Ultralytics 上下载的官方 YOLO 预训练模型文件,其模型文件大小对应的内部参数是FP16,而不是FP32呢? 这是因为,Ultralytics 官方所提供的 YOLO 预训练模型文件(如 yolov8n.pt 或 yolov5s.pt)在发布前,都会被强制转换成了 FP16 格式,其核心目的是为了大幅节省传输带宽和存储空间。 Ultralytics 在模型训练完成后,会调用他们内部一个极其经典的后处理函数:strip_optimizer()。这个函数会调用模型 .half() 将其权重全部转为 16 位半精度浮点数 FP16 格式。大概的流程如下: FP16半精度转换的计算过程 从严格意义上讲,把模型参数的存储格式由 FP32 转换到 FP16 的过程称为类型转换(Type Casting / Downcasting),而不是严格意义上的量化(Quantization)。 要理解这个转换过程,必须要先理解FP16和FP32这两个数据类型在内存中的真实面貌。这两种浮点类型的数据在内存中的形态符合IEEE 754 浮点数二进制标准。 IEEE 574 规范采用科学计数法(例如 可以写成 )在内存中表征浮点数据。该规范将浮点数的表示分为三个区域:符号位 (Sign)、指数位 (Exponent) 和 尾数位/小数位 (Mantissa/Fraction)。其最终还原的统一数学公式为: 其中: - 符号位 S :永远是最高位(第 1 位),规则为用 0 代表正数,1 代表负数。 - 指数位 E:紧随符号位之后,用于控制数字的大小和动态范围。 - 尾数位 M:用于控制数字的精度和有效数字的位数。在规范的二进制科学记数法中,数字小数点前永远是 1(也就是 )。既然始终是 1,IEEE 754 为了极致榨干内存,直接规定这个 1 不存进内存。这样的话,内存里存的仅仅是小数点后面的部分(即上面公式中的 ),但是在计算的时侯,硬件需要自动把那个 1 加回来。 !ce6be79d-c521-45a1-b6d5-add7e59dbb69.png 那么对于FP32和FP16而言: - FP32:单精度,32bit,其中符号位1bit,指数位8bit,尾数位23bit,可表示的数据范围为 。 - FP16:半精度,16bit,其中符号位1bit,指数位5bit,位数为10bit,可表示的数据范围为 。 那么接下来,模型训练完成后保存的FP32的参数,要转换为FP16格式的话,具体的转换流程是什么呢?主要可以分为以下三个步骤: 第一步是指数位检查与溢出处理 (Overflow/Underflow)。 - 溢出 (Overflow) 检查:如上所述,FP16 能表示的最大绝对值是 65504。如果 FP32 的权重值大于 65504 ,转换器通常会执行 Clamp 操作,将超出部分强行截断为 65504。 - 下溢 (Underflow) 检查:FP16 能表示的最小的正规化数大约是 。如果 FP32 的参数值极小的话,转换器会将其强制为 0。 第二步是尾数位的强制截断。 FP32 有 23 位尾数,而 FP16 只有 10 位。转换器在进行转换的时候直接把后 13 个比特砍掉。在二进制里,这个截断操作相当于抹除了小数点后第 4 位到第 7 位的微小有效数字。当然,这一步也就是FP32到FP16转换中精度损失的主要来源。 最后一步是计算图的重构。也就是更换模型计算图中算子的执行逻辑,把其中FP32的算子(如Conv2D_Float32)操作替换为FP16的算子(如Conv2D_Float16)。 以上三个步骤的完整转换流程如下图所示: !FP32_to_FP16_Conversion_Flowchart_RK3588_%282%29.png 从以上的三个步骤的操作来看,从FP32的模型转换为FP16,模型的推理结果和精度等可能被影响的主要来源于两个方面的因素:指数位的溢出和尾数位的强制截断。但实际上,对于绝大多数 CV 模型和音频模型,FP32 转 FP16 的精度几乎是无损的,这是什么原因呢? 针对指数位溢出的问题,无论是 CNN 还是 Transformer,模型在经过初始化训练后,其权重参数 绝大多数都严格服从均值为 0、方差极小的正态分布。而且当前的 CV 和音频模型中普遍配置了 BatchNorm (BN) 或 LayerNorm (LN) 归一化层,这些归一化层在每次卷积或注意力机制计算完成后,会强制将特征图的数据分布重新拉回均值为 0、方差为 1 的标准状态。这样也就意味着,网络中 99.9% 的权重值都在 这个极小的区间内,FP16 的最大值 65504 ,指数位溢出的概率在权重端几乎为零。 而尾数强制截断所带来的精度损失问题,则通过尾数截断时所采用的阶段策略来缓解。在FP32向FP16转换的过程中,对尾数位的截断采用了一种向偶数舍入 (Round-to-Nearest-Even, RNE)的截断策略(这个策略有点类似于十进制的四舍五入)。如果使用简单的直接截断和舍弃,误差会永远偏向一方,这样经过成千上万次累加后,误差就会如滚雪球般变成灾难。但是,RNE 使得截断误差一个均值为 0 的对称随机噪声。这样的情况下,当模型对成千上万个带有零均值、独立噪声的乘积结果进行求和时,正向的误差和负向的误差在统计上会相互抵消,这样就可以极大程度上缓解强制截断的精度损失和误差问题。 INT8量化的计算过程 与 FP16 在 IEEE 754 标准的基础上做位宽截断的处理过程完全不同,INT8 量化是一场精密的数学空间映射。 它需要将 FP32 的连续的、动态范围极大的浮点数空间,塞进只有 256 个刻度()的离散整数空间中。 对于以上的这个映射,业界标准的做法是使用线性映射变换。这个映射变换由两个关键参数决定: - 缩放因子S:FP32 浮点数,代表了 INT8 空间里一个刻度单位对应于 FP32 空间里的真实范围大小。 - 零点偏移Z:INT8 整数,代表了 FP32 空间里的绝对零点,对应于 INT8 空间里的哪个整数刻度。 基于以上两个参数,假设 FP32 空间内的真值为 ,其对应的 INT8 空间内的量化值为 ,那么 和 之间转换的基本等式就是: 从真值 R 向量化值 Q 计算的公式就是: 其中: - 代表四舍五入到最近整数的操作。 - 则代表防溢出操作,把结果死死限制在 INT8 的合法范围内(也就是在 和 之间,如 )。 而从 FP32 到 INT8 的全流程计算过程也可以分解为以下三个步骤。其实 INT8 的量化计算就是执行以上的量化值的计算公式,只不过在执行这个公式之前首先要算出来其中的参数 和 。 首先是步骤一:要计算缩放因子 S ,在量化值 的范围确定的情况下(对于INT8而言就是-128到127共计256个刻度),首先要确定清楚 FP32 空间对应的真实值 的区间范围 和 。这样才能把 的范围准确的映射到 的范围之内。 这里针对参数权重/偏执值和激活值有两种情况: - 对于参数权重值和偏置值而言:因为这些值都是模型训练好后就确定下来的常量,所以在模型转换器(如瑞芯微的RKNN-Toolkit2)在INT8量化的执行过程中,只需要直接扫描 ONNX 文件里这一层卷积核的矩阵,就可以直接找出 和 。 - 而对于激活值而言:激活值是会随着输入图像的特征数据动态变化的,这就是为什么在进行 INT8 量化的时候,必须要基于训练图片集提供一组量化数据集 dataset(通常包含 20-50 张代表性图片)。INT8 量化的过程中,模型转换器会使用这些图片做一次前向推理,统计每一层输出特征图的概率直方图,然后再使用 KL 散度 (KL-Divergence) 等算法,找到一个最能保留信息量的最优截断阈值 和 。当然这样找到的 和 是由我们提供的量化数据集dataset中的数据计算出来的,所以最好我们提到dataset数据集中包含的图片与量化后实际使用的图片形态相差不大。 步骤二:有了步骤一计算出来的 和 ,接下来就可以计算出来参数 和 。计算这两个参数的流程有两种方法,非对称量化(针对激活值)和对称量化(针对权重值)。 针对权重参数的对称量化: 模型训练出来的权重值的分布(即 和 )通常以 0 为中心,左右基本对称。硬件为了计算极速,强制要求量化零点 ,INT8 量化值的目标范围为 。如 和 为,计算出来的 和 Z 就是: 针对激活值的非对称量化: 非对称量化可以充分的利用 INT8 量化的所有刻度,假设 ,计算 和 的流程就是: > 💡 为什么激活值使用非对称量化,权重值使用对称量化?这是因为,现代 CV 网络中大量使用 ReLU 系列激活函数。经过 ReLU 之后,特征图中的所有负数都被置为 0。此时如果强制对这种单边分布使用对称量化,即强制将映射范围的中心定在 。此时为了能表示 这样的数字,量化刻度就必须覆盖 并映射到 INT8 的 。这样的结果就是 INT8 中 这 127 个状态永远不会被用到(因为根本没有负数激活值),一半量化值被白白浪费掉了,8bit的量化实际上变成了7bit_。_但权重的分布天然就是以 0 为中心的(高斯分布),所以用对称量化基本不会浪费空间,还可以更高效的利用硬件计算加速。 步骤三:物理转换并固化。 第二步计算出来了 和 ,接下来模型转换器就会将所有的 FP32 权重,套用如下公式全部计算成 INT8 数值,并打包到量化模型文件中。 以上描述的INT8量化计算流程的整个流程图如下图所示。 !FP32_to_INT8_Quantization_Flowchart_RK3588.png
- 15 Apr, 2026
RT-DETR模型的自定义数据训练以及在RK3588上的适配
本文完整的记录了基于Ultralytics项目中的 RT-DETR 预训练模型在自定义数据集上进行的训练、ONNX导出,以及在RK3588硬件平台上的适配流程。 对于 RT-DETR 模型的实现而言,目前主要有两套独立的实现,即 Baidu 的 PaddleDetection 和 Ultralytics (YOLOv8/v11 生态)。因为我自己对于 Ultralytics/YOLO 较为熟悉,所以本文以 Ultralytics/YOLO 的实现进行试验和记录。 !image.png 1.自定义数据集的训练 因为这里使用的是Ultralytics项目下的RT-DETR实现,所以其训练过程、训练所生成的中间文件组织结构以及对训练数据的表格格式、目录结构等都与YOLO模型没有任何区别,只需要把之前在YOLO模型上训练所使用的数据集目录以及其 data.yaml 直接拷贝过来使用即可。所以如果对YOLO模型的训练流程有经验的话,在这个项目中训练RT-DETR就很容易上手了。可参考一文总结YOLOv5模型的自定义训练、测试与模型导出的全过程。 训练流程的代码也与Ultralytics项目下的其他YOLO模型类似,训练过程的代码如下: 训练流程基本上与普通的YOLO没有多大区别,有两点注意事项: - 在Windows环境中进行训练的话,训练代码需要包裹在__main__里面。这是因为在 Linux 系统中,进程的创建通常使用 fork,它会直接克隆当前的内存状态,不需要重新导入模块;而 Windows 不支持 fork,只能 spawn(冷启动一个新的 Python 解释器并导入以上脚本),这样的话,子进程启动时会再次运行一遍整个 train.py,从而又触发了一个新的训练请求,形成死循环。系统为了保护内存不被耗尽,主动抛出 RuntimeError来结束整个程序。而把训练代码包裹在__main__里面以后,只有主进程会执行train_model函数,子进程只负责执行训练过程,这样就不会进入反复新建子进程的死循环。 - batch和workers设置相对小一点,否则训练过程中很容易卡死。我一开始设置了batch为16,结果发现程序很容易卡死并报显存不足,所以如果出现训练过程稳定性问题的时候可以尝试把workers和batch这两个参数设置小一点。 以上的训练执行过程明显比使用相同数据集训练的YOLOv8模型耗时要长一些,应该是RT-DETR模型的计算复杂度要高于 YOLOv8。训练结果同样保存在runs目录下的best.pt文件。 2.Grid Sample算子的梦魇与Monkey Patch 在RK3588硬件上跑AI模型,大致的路径无非是: - 在PC端基于Pytorch等框架完成模型的训练; - 把训练的pt格式模型文件转换为更通用的ONNX格式; - 使用瑞芯微的RKNN-Toolkit2工具进行板端模型转换,把ONNX格式的模型转换为RKNN格式; - 在RK3588板端使用rknn-toolkit-lite2 工具(Python)或者C/C++语言把模型跑起来,在板端完成推理测试得到结果。 所以既然前面的训练过程已经完成,那么下一步自然就是进行ONNX格式的模型转换。但是这一步出现了一个由Grid Sample算子所导致的很大的问题: - Grid Sample算子是在Opset 16中被支持的,所以要能够导出ONNX成功,必须要设置Opset版本为16。设置为低于16的Opset版本,在导出ONNX模型这一步会报错。 - 如果把Opset设置为16的话,使用RKNN-Toolkit2工具(2.3.2版本)确实可以成功导出RKNN模型,但是这个模型放在RK3588板端跑的话,配合2.3.2版本的librknnrt.so文件会出现段错误的问题,原因是2.3.2版本的librknnrt.so文件根本不支持Grid Sample这个算子。也就是说,2.3.2版本的RKNN-Toolkit2工具认为Grid Sample算子是可以支持的,所以模型转换通过;而2.3.2版本的librknnrt.so文件根本就没有实现这个算子,所以板端运行会出现段错误的问题。瑞芯微的算法团队发布版本的时候不需要同步一下吗? 对于以上问题,当然是可以修改模型的结构,使用其他更通用的算子替换掉Grid Sample算子,然后重新训练,就可以从根本上解决这个问题。 但是,如果不考虑算法在板端的运行速度的话,其实是可以用一个简单的办法绕过这个算子:Monkey Patch。 所谓的Monkey Patch就是对于要导出ONNX的模型,在导出之前,先把某个板端不支持的算子用更常用的算子替换掉,这样在ONNX导出的时候,根本就看不到这个Grid Sample算子。具体来讲,就是在导出ONNX模型之前先用以下的代码进行Grid Sample算子(F.grid_sample)的替换: 3.ONNX模型的导出与RKNN转换 解决了以上的Grid Sample算子的问题,后续的ONNX导出就比较简单了。如如何理解ONNX模型导出的Opset版本?一文所总结的:在 Rockchip NPU的2.3.2版本的模型转换工具和板端NPU驱动上最高能够支持的Opset版本就是15。实际的部署过程中,最好的Opset版本设置应该是12或者13,避免版本过新有可能带来的稳定性和兼容性问题。 以上导出的ONNX格式的模型就是能够在RK3588上得到良好支持的OPSet=13的版本,接下来就是使用RKNN-Toolkit2工具进行板端模型转换,把ONNX格式的模型转换为RKNN格式: 4.板端推理运行 最后在RK3588板端使用rknn-toolkit-lite2 工具进行板端推理就可以看到运行的结果了。不过感觉推理执行的速度比较慢,可能跟手动替换了Grid Sample算子有关: !result_fp16_final.jpg
- 18 Mar, 2026
DETR(DEtection TRansformer)网络架构与计算流程详细总结
本文详细介绍了Facebook在2020年提出的基于Transformer架构的图像目标检测模型DETR的网络结构及其数据在网络中的计算流程。 DETR简介 DETR 是由 Facebook AI Research (FAIR) 在 2020 年提出的针对计算机视觉领域中目标检测方向的AI模型,首次把NLP领域中广泛应用的 Transformer 架构引入到了计算机视觉领域。它彻底改变了目标检测领域的传统设计范式,将目标检测任务从繁琐的各种组件和处理流程的堆砌简化为了一个真正的端到端的集合预测问题。 在 DETR 模型出现之前,在目标检测领域的主流检测器(如 Faster R-CNN, YOLO, SSD)都高度依赖各种设计组件: - Anchor Boxes (锚框):基于训练数据集的总体情况预设检测目标的不同尺度和比例。 - NMS (非极大值抑制):用于在模型输出数据的后处理过程中消除重复的检测框。 - 极其复杂的正负样本匹配策略。 而DETR 的核心创新则在于:通过利用 Transformer 结构的全局建模能力,直接输出一个固定数量的预测集合(通常是 100 个),然后通过二分图匹配 (Bipartite Matching) 算法与真实标签(Ground Truth)进行一对一匹配。 从架构上讲,DETR 网络主要由三个部分组成:CNN Backbone,Tranformer编解码器,检测头。 !image.png CNN Backbone DETR网络架构以卷积神经网络CNN结构的BackBone部分开始,Backbone网络采用标准的Resnet网络(原始论文中,作者使用的 Backbone 网络是Resnet50和Resnet101)。该Bonebone网络作为输入图像的特征提取器,用于处理原始输入图像以产生丰富的视觉特征向量。 - 有关 Resnet 的网络结构可参考:CNN经典网络模型架构学习之Resnet。 在图像处理的处理维度上,假设 Backbone 的输入图像尺寸为 (3, H, W),那么该部分经过多阶段 Resnet 进行的特征提取后会输出一个降维后的特征图。在标准的 DETR 实现中,特征图下采样的倍数通常是 32 倍,因此输出特征图的维度是 (C, H/32, W/32)。而通道数 C 通常使用一个1x1卷积投影到256,用以匹配 Transformer 的输入维度。 - 如果输入图像分辨率为 800 x 800,输出特征图的分辨率就是 25 x 25。 参考ViT架构中Transformer在计算机视觉领域中的应用(一文入门Vision in Transformer(ViT)模型的架构),Transformer 接收的是序列数据(Sequence),而不是矩阵形式的向量。因此,Backbone 中Resnet网络所输出的 (25,25,256) 的特征图会被展平拉直成一个长度为 625 的向量序列,其中的每个元素为长度256的向量。 !image.png 位置编码 同样的,卷积神经网络本身由于平移不变性和局部感知,能隐式的感知位置信息。但 Transformer 处理的序列数据需要额外在展平的向量序列上增加位置编码信息,这一点跟自然语言处理中对文本信息增加位置编码信息是一致的。 DETR 对BackBack输出特征图序列的位置信息采用了固定正余弦二维位置编码(Fixed 2D Sinusoidal Positional Encodings)。大致的做法是:将特征图每个像素位置的编码信息分为 x 方向和 y 方向,分别进行编码,最后将它们拼接(Concatenate)起来。如上所述,特征图经过展平拉直后每个像素的向量长度为256,那么就分配 128 维来编码 x 坐标,另外的 128 维来编码 y 坐标,将两者拼接后形成最终的 256 维位置特征向量。 对于特征图上每个像素的 (x,y) 坐标,每个 x 对应一个 128 维的向量,这个向量由 64 组正余弦对组成,其正余弦对的计算公式分别为: 同样是由64组正余弦对组成的128维向量,采用相同的计算公式计算。两个128维向量再拼接得到一个256维的位置向量,这样得到的向量维度正好与经过 1 x 1 卷积后的特征向量维度对齐,位置编码信息的维度同样是长度为 625 的向量序列,其中的每个元素为长度256的向量。。 Transformer Encoder-Decoder 如上所述,经过Backbone对图像的特征提取、特征图展平拉直以后,送入Transformer Encoder的数据维度就是长度为 625 的序列向量,序列中每个元素的维度维 256。需要注意,BackBone 输出的数据与并不会与位置编码向量直接相加,而是在计算注意力时将位置向量注入到 Query 和 Key 的计算中,这一点是与标注的自然语言处理的Transformer计算流程不同的地方。 下图是 DETR 的Transformer Encoder与Decoder的处理流程架构: !image.png Encoder Encoder的多头自注意力模块采用了 8 个 Head,因此每个 Head 的维度为 。每一个 Head 的Q、K、V矩阵的计算如下: - Query (Q): → 维度: 625 x 32 - Key (K): → 维度: 625 x 32 - Value (V): → 维度: 625 x 32 > 注意:在计算Query和Key的时候,把 Backbone 提取出来的特征向量 Src 与其位置编码 Pos 相加进行计算,Value 的计算不需要加入位置编码,因为 Value 负责提供内容信息,而 Q 和 K 负责定位和匹配。 接下来对于多头注意力模块的计算仍然遵循标准的计算流程,在如何理解Transformer架构中的多头注意力机制?一文中已经非常详细的总结了多头注意力模块的计算步骤。 总之,从输入和输出数据的维度上讲,每个 Encoder 模块(上面架构图左侧虚线框部分)输入的特征图展平向量和位置编码向量的维度是 625 x 256 , 而其输出数据的维度同样是 625 x 256 。DETR 模型默认包含有连续的6个Encoder模块,最终 Encoder 部分输出的数据维度同样是 625 x 256 。 Encoder 的工作逻辑就是:利用全局注意力机制,对前面的Backbone卷积网络提取的局部特征进行上下文建模,最终输出一个与输入尺寸相同的增强特征序列,其中每个点都包含了全局的上下文信息和语义特征。 Decoder 对于 Decoder 部分架构的理解,首先定义Decoder的输入。Decoder 的输入包含两个部分: - Encoder的输出,这一部分比较好理解,就是前面最后一个 Encoder 模块输出的维度为 625 x 256 的张量。 - Object Queries (对象查询向量):这是一个维度为 100 x 256 的张量。在训练阶段,它是可学习的参数(类似于 Embedding),训练完成后这个100 x 256维度的张量就作为固定的参数用于后续图片的推理流程。这 100 个向量就代表着 100 个候选框。 如下图所示的Decoder Block的架构,每个Decoder Block从下到上依次包含有三个部分:多头自注意力模块(Multi-Head Self-Attention),多头交叉注意力模块(Multi-Head Attention),前馈模块(FFN)。 !image.png 首先是自注意力模块(上图中的Multi-Head Self-Attention模块)。这个自注意力模块的计算流程与标准的Transformer自注意力的计算流程稍有差异:标准的Transformer自注意力模块的输入是增加了位置编码信息的输入向量,然后基于这个输入向量分别与 这三个参数矩阵运算得到 Q,K,V 向量;而以上自注意力模块的计算逻辑则是: 以上自注意力计算的逻辑,实际上把输入的Object Queries同时作为输入数据和其位置编码向量来使用,这就是为什么在自注意力计算Q和K的过程中要对Object Queries进行自加操作的原因所在。 > 为什么是对相同的输入Object Queries进行自加,而不是对这个输入直接乘以2呢?这是因为,实际上Decoder部分有6个连续叠加的Decoder block,而只有第一层的decoder在计算Q和K的时候用的是Object Queries的自加作为输入,后续的decoder在进行自注意力模块计算的时候是使用Object Queries(作为位置向量信息)和前一层Decoder的输出相加来作为其输入的。 经过以上自注意力模块的计算后,其输出数据的维度为 100 x 256。 接下来是交叉注意力模块Multi-head Attention部分的计算。在交叉注意力模块中,Decoder 自注意力模块的输出扮演的是主动提问者的角色,而 Encoder 输出的特征图则是知识库。 交叉注意力模块的输入数据有三个部分:Decoder 自注意力模块输出的object_queries(维度为(100,256)),Encoder输出的特征图信息encoder_features(维度为(625,256)),以及与Encoder部分相同的空间位置编码信息(维度为(625,256))。在这种情况下,Q,K,V 三个张量的计算逻辑及其数据维度如下: - Q (Query): → (100, 256) - K (Key): → (625, 256) - V (Value): → (625, 256) 交叉注意力模块后续的计算逻辑以及前馈网络模块FFN仍然与标准的Decoder Block的计算流程相同,具体可以参考一文彻底搞懂Transformer模型的Decoder结构与计算流程,最终以上的整个Decoder Block的输出维度与其输入维度相同,都是(100,256)。DETR网络结构右侧的Decoder部分总共包含有6个连续叠加的Decoder Block,每个block的输出作为下一个block的object queries输入,最终整个Decoder部分的输出数据维度仍然是 (100,256)。 检测头 如上所述,经过整个 Decoder 部分的连续6个Decoder Block的叠加处理后,最终Decoder部分输出的数据维度是(100,256)。那么DETR模型最后的检测头部分的任务,就是将这 100 个高维向量映射为人类可读的类别标签和边界框坐标。 在检测头结构的设计上,DETR 采用了两个并行的 FFN(前馈神经网络)分支,它们共享 Decoder 的输出,但负责不同的任务。 !image.png 分类头 (Classification Head)的结构是一个简单的线性层(Linear Layer)。该线性层的输入数据维度为 100 x 256,输出数据为 100 x (K+1)。其中 K 是目标数据集的类别数(如 COCO 的 80 类),+1 代表背景类(no object)。最后的激活函数使用Softmax,它会为每个 Query 分配一个概率分布,指示该 Query 对应的物体属于哪一类。 回归头 (Bounding Box Regression Head)的结构是一个具有 3 个隐藏层的多层感知机(MLP),每层之间带有 ReLU 激活函数。整个回归头的输入数据维度为100 x 256,输出数据维度为 100 x 4。其数值的含义是归一化后的中心坐标及宽高。 在以上输出头结构的情况下,对于每张图片,DETR模型会输出100个检测框的坐标信息(回归头),每个检测框对应一个长度为 K+1 的分类信息。在进行背景框过滤时,只需要设定一个阈值(例如 0.5),过滤掉那些分类结果为背景类别(no object)或得分过低的预测框即可,不再需要YOLO模型非常复杂的NMS计算流程了。 参考资料 - facebookresearch/detr: End-to-End Object Detection with Transformers - Introduction to DETR - Part 1 | DigitalOcean
- 09 Mar, 2026
一文彻底搞懂Transformer模型的Decoder结构与计算流程
本文详细总结了Transformer模型架构中Decoder部分的总体网络结构,以及数据在其中的运算和流转流程。 之前在一文彻底搞懂Transformer模型的Encoder结构与计算流程一文中,详细整理了Transformer模型的Encoder架构,本文继续学习和解析Transformer的Decoder的结构以及完整的计算流程。因为Decoder与Encoder的模型结构与计算流程非常类似,所以最好参考一文彻底搞懂Transformer模型的Encoder结构与计算流程以及如何理解Transformer架构中的多头注意力机制?把Transformer的自注意力概率与计算流程先搞清楚。 下图是一个完整的Transformer Encoder-Decoder的网络架构: - 左侧为连续N个Encoder Block组成的Transformer Encoder,用于实现把输入的自然语言文本转换为向量表示的语义信息。 - 右侧为连续N个Decoder Block组成的Transformer Decoder,用于基于Encoder输出的语义信息的向量表示来实现自然语言功能的输出,如机器翻译,文本续写等。 !image.png Decoder的输入和输出 Transformer的输入和输出结构如下图所示。 !image.png Decoder的输入包含两部分数据: - 下方输入(Outputs Shifted Right):用于执行掩码自注意力 (Masked Self-Attention)计算。其内容是模型已生成的 Token 序列,从而让模型知道“我已经说了什么”。 - 中间输入(来自Encoder提取的语义向量):用于执行交叉注意力 (Cross Attention)计算。其输入内容是基于 Encoder 的语义向量计算得到的 K 和 V 矩阵,让模型知道“源句子的意思是什么”。其计算逻辑是 Decoder 会用自己的 Q(我想找哪个语义)去跟 Encoder 的 K, V 进行匹配。 Decoder的输出为上图最上方的Output Probilities。输出内容的维度等于模型输出词表大小(Vocabulary Size)的向量。通过最后的 Linear 层和 Softmax 层处理之后,这个向量代表了模型输出词表中每一个单词作为“下一个输出单词”出现的概率分布。通常选取概率最大的那个索引(Index),然后去字典里查出对应的单词作为下一个输出单词。 Outputs shifted right输入与自回归预测 以上提到Decoder下方输入Outputs Shifted Right的内容是模型已生成的 Token 序列,这个在推理流程中要如何理解? 在初始状态下,模型是没有输出的,此时 Outputs Shifted Right 部分的输入内容就是一个起始信号 (Start of Sentence)。此时模型会根据 Encoder 的语义和起始信号,在Decoder端执行一次前向推理,预测生成第一个词(例如 Hello)。接下来,新生成的这个词 hello 就会被接入到decoder的输入端,此时decoder的输入序列就变成:[, Hello],模型继续基于这个新的输入序列执行前向推理,预测出第二个词 world,第二个词同样被接入到输入端,这样输入端的输入序列就变成 [, Hello, World],依此类推,直到最终模型预测输出的结果为 (End of Sentence,结束符),此时推理停止。 以上就是Decoder执行前向推理预测生成一个个单词的完整流程,因为每生成一个单词,这个单词都会被重新拼接到输入预测序列的最后,用于推理新的单词,这个过程也就是所谓的自回归预测过程。 Decoder Block的结构 Transformer 中的Decoder部分由多个相同的Decoder Block组成,其主要功能是根据Encoder所提供的语义表示(如上图中Encoder部分的输出)和先前已生成的输出标记(如上图中的Outputs Shifted Right部分)生成最终的输出序列。 !image.png 每个Decoder Block由三个主要子层组成: - Masked Multi-Head Attention:与编码器的自注意力机制类似,但其主要目的是防止关注未来的 token,以保持自回归属性(确保生成过程中不会作弊)。 - Multi-Head Attention:该子层允许解码器关注编码器输出表示的相关部分。这使得解码器能够专注于输入的相关部分,这对于翻译等任务至关重要。 - Feed Forward:前馈神经网络,该子层处理屏蔽自注意力和编码器-解码器注意力机制的组合输出。 从网络结构和推理的计算流程上,Multi-Head Attention多头注意力模块与Feed Forward前馈模块与Encoder的相应模块没有差异,主要的区别在于Masked Multi-Head Attention模块以及Encoder输出作为Decoder输入的交叉注意力机制上。 Masked Multi-Head Attention 从工程实现的角度看,Masked Self-Attention(掩码自注意力)和 普通的 Self-Attention 在代码结构以及计算流程上 90% 是相同的。唯一的差异只是两个注意力的视野范围不同: - 普通的 Self-Attention (例如Encoder 使用自注意力模型):该模块对一段文本进行语义提取的时侯,可以同时看到从开头到结尾的所有词,因此可以根据全句的语境来理解每一个词,并提取其语义,因此普通的Self-Attention模块的视野范围是全局的。 - Masked Self-Attention (Decoder 使用):在使用掩膜自注意力模块的时候,当前词的输出只能参考之前已经输出的词,不能参考尚未在当前词位置后边的词。也就是说,掩膜自注意力模块的视野氛围是历史视角,即只能看到之前的词。 Masked Self-Attention在计算中的历史视角这一点,对于训练过程非常关键。 因为在训练阶段,训练样本中包含有针对某个问题的完整的正确答案序列。为了提高效率,训练过程的执行,不是像推理解码那样一个词一个词地跑 N 遍模型,而是把整个序列一次性输入,然后执行一次矩阵运算,通过一次推理得到答案序列。这样的话,如果采用普通的自注意力模块就会存在一个问题,在解码器生成文本的过程中,位置在前面的词会看到后面的词,这样的话,后面位置的词就会对前面位置的词产生影响,相当于模型在训练的过程中直接把答案看到了,这当然是模型训练不希望看到的情况。 此时,就可以在执行注意力的矩阵运算的过程中,如下图所示,通过一个三角掩码矩阵,在数学上强制让第 t 个词只能看到第 1 到第 t 个词,无法看到后面的词。这样就既实现了并行化训练,同时又完美模拟了无法看到未来的因果律。 !image.png 相应的,Masked Self-Attention模块的计算公式就变成了: 其中,M 是一个下三角矩阵。其逻辑如下: - 对于所有 j > i 的位置(即当前位置之后的词),将 设为 。 - 对于所有 j ≤ i 的位置(即当前和之前的词),将 设为 0。 > 注意:在正常的前向推理过程中,模型实际上是执行一次前向推理生成一个token,在这种情况下,每次执行前向推理时,通过outputs shift right传递给decoder模块的文本序列自然就不存在未来的token(因为这个时候未来的token还没有生成),这样的话,这个掩码机制其实根本就没必要了。只不过因为训练的过程中,Transformer中的参数都是在有掩码的情况下生成的,所以如果在前向推理的过程中,把这个掩膜自注意力模块去掉,可能会导致参数数值分布的细微偏移,从而影响预测的准确性。所以,即使前向推理推理过程实际上已经不再需要掩膜机制,仍然需要保留这个掩膜自注意力模块机制,确保模型在推理阶段的输出与训练阶段输出的准确性基本一致。 Encoder的输出与交叉注意力 在Encoder的自注意力(Self-Attention)机制的实现中,Q、K、V 全部来自同一个输入。而对于Decoder的多头注意力模块而言,其输入来源于两个不同的来源,这就是所谓的交叉注意力机制: - Query (Q)来自于 Decoder 上一层的输出,实际上也就是掩膜注意力模块的输出。其语义内涵代表“我已经生成了这些词,我现在需要寻找什么样的上下文信息来翻译下一个词?” - Key (K) 和 Value (V)则来自于 Encoder 的最终输出。其语义内涵表示“原文(源语言)中每个词的特征和含义是什么?” 因此,对于交叉注意力机制,可以把它想象成 Decoder 持有问题(Q),去 Encoder 的知识库(K, V)中寻找答案。 如下图所示,基于Decoder前一模块的输出,与 Wq 矩阵运算得到 Queries;基于Encoder 的输出分别于 Wk 和Wv 矩阵运算得到Keys和Values,然后基于Queries、Keys和Values调用相同的注意力机制的计算流程(可参考如何理解Transformer架构中的多头注意力机制?),得到交叉注意力模块的计算结果。 !image.png Decoder部分的工作流程总结 最后再来总结一下整个Decoder部分的工作流程如下。 !image.png 首先是从outputs shifted right端输入模型推理输出的Token序列,这个token序列会使用与Encoder相同的预处理方式,对token进行向量化(Embedding)并增加位置编码信息。 第二步是掩膜自注意力模块的处理流程,其输入是叠加了位置编码信息的Token向量序列,这部分计算流程在前文的Masked Multi-Head Attention 已做了非常详细的解释。掩膜自注意力模块的输出通过标准的Add & Norm进行残差连接和层归一化。 第三步是交叉注意力模块的处理流程,这部分计算流程在前文的 Encoder的输出与交叉注意力 已做了详细的解释。同样,其输出通过标准的Add & Norm进行残差连接和层归一化。 第四步是由两层全连接神经网络所组成的前馈网络Feed Forward。其作用是在经过了两次注意力机制(掩膜自注意力和交叉注意力)后,对提取到的复杂特征进行非线性变换和进一步的维度提取。最后仍然通过 Add & Norm 进行残差连接和层归一化。 以上的第二步到第四步的处理流程构成了一个Decoder Block,每个Decoder Block的输入数据和输出数据的维度相同。整个Decoder 部分包含有N个Decoder Block,上一步Decoder Block的输出作为下一个Decoder Block的输出,直到N个Decoder Block处理完毕。 最后是输出结构的转换层,包含两个部分:线性层和Linear和Softmax层。 - Linear (线性层):全连接映射层,用于将 Decoder 提取出来的抽象向量(例如 512维,768维等)投射到一个巨大的空间,即模块输出所能支持的完整词表的空间。如果输出的字典里有 30000 个单词,那么这一层的输出维度就是 30000。 - Softmax (归一化层):通过Softmax 将线性层输出的原始数值(Logits)转换为概率分布,最终输出一个预测概率向量,每个位置的值代表词典中对应单词成为下一个词的概率。接下来系统就根据预测概率向量,选取概率最高的单词作为结果输出,并反馈回底部的输入端,开始下一个词的预测。 参考资料 - Architecture and Working of Transformers in Deep Learning - GeeksforGeeks - How Transformers Work: A Detailed Exploration of Transformer Architecture | DataCamp - Cross-Attention Mechanism in Transformers - GeeksforGeeks
- 02 Mar, 2026
从头实现一个Vision Transformer(ViT)模型
从头实现一个Vision Transformer(ViT)模型 本文针对简单的MNIST手写数字数据集识别的需求,完成了一个最简单的Vision Transformer模型的实现、训练和验证测试,建立对Vision Transformer模型实现流程的完整理解。 MNIST手写数字数据集是最简单的机器视觉数据集,基于MNIST实现一个Vision Transformer模型来实现手写数字字符的识别,难度不会太大,对于模型训练所需要的数据以及算力资源要求也不高,因此通过训练一个MNIST数据集的ViT识别模型,是一个绝佳的入门Vision Transformer模型的实验。 !image.png 关于Vision Transformer模型的整体架构,可以参加另外一篇笔记[[一文入门Vision in Transformer(ViT)模型的架构]]。 !image.png > _注:参考资料1中提供了一个完整的、对MNIST手写字符集进行识别的Vision Transformer模型,本文实际上是对以上资料的学习记录,并增加自己对于实现过程的理解。_ 如[[一文入门Vision in Transformer(ViT)模型的架构]]这篇笔记所总结的,ViT模型的架构和工作流程大致可以分为以下几个阶段:图像切片、添加Cls Token、位置编码、基于多头自注意力机制的Transformer Encoder、最终的训练头。因此,本文仍然按照以上结构来实现一个完整的Vision Transformer结构,并使用MNIST数据集对其进行训练以及最后的测试验证。 1. 导入必要的包 - 导入torchvision.transforms用于实现对MNIST字符集图像分辨率的resize,确保模型输入分辨率是patch分辨率的整数倍;以及把MNIST字符集的图像数据转换为Tensor格式。 - 训练过程所使用的优化器使用torch.optim中所包含的Adam。 - 利用torchvision.datasets.mnist从Pytorch官网自动下载MNIST数据集。 2. Patch Embeddings 这一步实现把输入图像切片为固定大小的patch,并且序列化。如[[一文入门Vision in Transformer(ViT)模型的架构]]所提到的,对于这部分功能,一般是通过一个步长(Stride)等于卷积核大小(kernel_size)的 2D 卷积(Conv2d)来实现的。 以上的 Conv2d 操作实际上是一个卷积投影操作: - kernel_size=patch_size :是用 patch_size×patch_size 的卷积核提取每个 patch - stride=patch_size :步长等于 patch 尺寸,从而实现非重叠的切分动作 - 输出通道数=d_model :每个 patch 被映射为 d_model 维的向量,实现每个patch的向量化 patch embedding的完整代码如下: 在以上的forward操作中,self.linear_project(x)这一步把一张输入图像的维度从(C,H,W)转换为(d_model,P_col,P_row); !image.png x.flatten(2)这一步把所有的patch展平,得到P个序列化Patch,每个patch的维度长度都是d_model,这实际上就是patch embedding操作的输出结果; !image.png 最后的x = x.transpose操作就只是交换输出数据的维度,以适配 Transformer 架构的标准输入格式。 !image.png 3. Cls Token与位置编码 为Batch中的每张图片的Patch列表的开头,增加一个Cls Token的逻辑相对比较简单,就是新建一个长度与Patch向量相同的cls token,然后把这个token放到patch向量的开头作为其0号token即可。 除了Cls Token以外,还需要为Patch向量中增加其位置编码。每个位置编码对于它所代表的位置来说都是唯一的,这使得模型可以识别每个Patch的位置。为了将位置编码添加到Patch向量中,它们必须具有相同的维度 d_model。位置向量的构建公式如下: !image.png 因此总的来说,这一步包含两个步骤:为Batch中每张图片的Patch向量列表中的初始位置增加一个Cls token;为所有的向量增加其位置编码。完整代码如下: 4. 单头及多头注意力模块的实现 有关多头注意力模块的理论以及计算流程等方面的内容,在[[如何理解Transformer架构中的多头注意力机制?]]一文中已有非常详细的总结和解释,在此不再赘述。 单头注意力机制的计算公式如下: 可结合上面的公式以及代码中的注释来理解单头注意力的计算逻辑和流程,基本上就是以上公式的一步步计算而已: 基于以上的单头注意力模块,实现多头注意力模块就比较简单了,就是对多个注意力模块独立计算,把计算结果合并起来,最后再通过一个线性层把多个单头输出的特征进一步融合并输出: 以上单头和多头注意力模块的流程和结构如下图所示: !image.png 5. Transformer Encoder 接下来是完整的Transformer Encoder的实现。Transformer Encoder的结构如下所示: !image.png 其对应的代码如下: 6.整个Vision Transformer模型 最终实现的整个Vision Transformer的模型架构如下: !image.png - 输入图像首先在PatchEmbedding模块中进行切分和向量化、序列化的操作。 - 序列化的Patch向量再在PositionalEncoding模块中增加额外的Cls Token,以及位置向量。 - 以上数据再经过连续N个TransformerEncoder模块的处理,反复通过多头注意力模块提取和融合图像中的特征。 - 最后在一个分类头处理后通过Softmax输出分类识别的概率。 完整Vision Transformer模型的实现如下: 7. 训练和测试 最后对以上构建出来的模型进行训练,以及对训练出来的模型使用MNIST的测试数据集进行验证。 超参数设置 对模型进行训练的超参数设置如下: 训练以及测试数据集的准备 训练和测试使用从Pytorch官网下载的MNIST数据集,使用以下代码下载数据集并准备dataloader: 模型训练 接下来就是对已经构建出来的模型参数进行训练,整个训练的执行流程实际上与[[基于Pytorch实现手写数字识别的卷积神经网络]]一文所描述的基于卷积神经网络的手写数字识别的训练流程大同小异: 训练了15个epoch,从打印的损失值可以看到其损失在稳定向下收敛过程中: 模型测试 最后就是使用以上训练好的模型,对MNIST的测试数据集进行测试: 经过上面的15轮测试,模型对测试集数据的测试准确度就达到了93%。 参考资料 - Building a Vision Transformer Model From Scratch | by Matt Nguyen | Toward Humanoids | Medium --- 尝试从底层原理的角度去理解和解释技术问题:音视频/摄像头/智能家居/蓝牙/WiFi/无线通信/AI。 敬请关注微信公众号:Pavel Han。
- 25 Feb, 2026
一文入门Vision in Transformer(ViT)模型的架构
本文详细总结了Vision in Transformer模型的设计架构,以及图像其该模型架构中的完整处理流程,帮助建立对ViT模型的基础认识。 Transformer架构在自然语言处理领域(Natural Language Processing)中的各种大语言模型中的到了非常广泛的应用,而2020年Google的一篇论文《An Image is Worth 16×16 Words》,把Transformer架构引入了计算机视觉领域,利用其Patch Embedding和自注意力机制对计算机视觉领域中传统的卷积神经王CNN的统治地位造成了极大的冲击,从而在人工智能的各个领域中已经显示出一统江湖的态势。 要理解ViT模型的架构及其图像数据在其中的处理流程,首先要对Transformer架构尤其是其中的Encoder部分的架构有较为清楚的理解和认识,可参考另外一篇笔记[[一文彻底搞懂Transformer模型的Encoder结构与计算流程]]。 ViT架构简介 Transformer 架构最初是为序列化数据(一维)设计的,而图像是高维的(宽 高 通道)。如果把图像数据送入到Transformer架构中进行处理,首先要解决的问题就是把图像的高维数据转换为Transformer可接受的一维序列化数据。针对这个问题,最直观的做法就是直接把图像中的每个像素都当作一个 Token 喂给模型,但这样存在的问题就是计算量会呈指数级爆炸。因此ViT 的解决方案是:对图像进行切片操作(Patch),把切片后的结果序列化输入Transformer模型。 因此。ViT 模型就是将输入图像分割为一系列固定大小的块(patch),并把这些块序列化以后应用Transformer模型的自注意力机制提取图像特征,这使得模型能够获取图像不同部分之间的长距离依赖关系,而无需依赖CNN的卷积运算。 下图是一个基于ViT架构的图像分类模型。可以看到,输入图像被分割为多个固定大小的块,这些块被序列化并且向量化以后,送入Transformer Encoder中提取特征,最终通过一个MLP Head输出检测结果。所以从模型的结构上讲,与自然语言语义提取的Transformer Encoder架构相比,两者最大的区别就只是ViT架构对图像所做的切片分块处理而已。 !image.png 以上这个基于ViT架构的图像分类模型的处理,可以分为多个步骤,以下注意解析和描述。 1. 图像切片Image Patching 这个步骤有点类似于Transformer在自然语言处理流程中的对语句段落分词(Tokenization)和嵌入(Embedding)的操作。 首先是对图像进行的切片操作(Patching)。例如将一张 224 x 224 的图像切成固定大小 16 x 16的方块(每个方块都是一个Patch),那么整幅图就会被切成 14 x 14 = 196 个块。 !image.png 因为一般来讲,每张输入图像都包含有RGB三个通道,所以经过切分以后的每个切片patch的维度就是 16 x 16 x 3 。这个切片仍然是一个三维的向量,而Transformer的自注意力机制运算需要在固定长度的一维向量上展开。所以下一步仍然需要把这个 16 x 16 x 3 的三维向量展平(Flatten)为一维向量。 在具体的工程实践中,对输入图像的切片以及对各个切片Patch展平的操作通常是通过一个步长(Stride)等于卷积核大小(kernel_size)的 2D 卷积来实现的。例如切片大小为 16 x 16 ,每个patch展平操作输出的一维向量长度是768,就可以使用以下2D卷积操作来执行切片和展平操作: 因为卷积核的小大刚好是一个 Patch 的大小,那么卷积一次就提取了一个 Patch 的特征,这在 GPU 上的计算效率更高。 经过以上操作以后,一张224x224分辨率的输入图像,就被切分为196个patch,并且每个patch都被展平并映射为一个长度为768的一维向量。 2. 添加Cls Token 在对以上得到的196个切片向量进行进一步的处理之前,还需要为其额外增加一个相同长度(768)的分类向量,即Cls Token。这个分类向量不代表任何图片块,它的唯一任务是跟其他 196 个块建立联系,最后要从它的输出中读出整张图的分类结果。 具体的操作是:手动创建一个形状为 (1, 768) 的可学习向量,放在前面得到的 196 个patch向量的最前面。经过这一步处理以后,输入序列就变成了 196 + 1 = 197 个向量,每个向量的长度都是768。 3. 位置编码Positional Encoding 接下来的处理与自然语言文本序列的处理逻辑相同,为197个向量增加位置编码信息,以保留每个patch在原始输入图像中的位置空间感。与自然语言序列中各个字词的位置对于其语义的理解的重要性相同,每个patch位于图像的什么位置,对于整个图像的特征提取同样非常关键。所以在把各个patch的向量序列送入Transformer处理之前,需要为这些向量增加明确的位置信息。 位置向量的维度与输入矩阵( 如197 x 768)的形状完全一致,这一步操作就是直接将这个位置向量矩阵加到(Addition)之前输入的 Patch 向量矩阵上。 !image.png 至此,输入图像数据经过前述的一系列处理以后,就已经做好了进入Transformer Encoder中进行注意力机制运算的准备了。一张 224 x 224 分辨率的图像变成了一个增加了位置编码的 (197,768)序列。 4. Transformer Encoder 在Transformer Encoder中的处理逻辑与流程与自然语言文本的处理没有没有任何区别。整个Transformer Encoder部分包含有连续L个相同的Encoder Block: !image.png 每个Transformer Block中则包含有多头注意力模块、前馈网络模块、残差与归一化层等结构。整个Transformer Encoder的工作逻辑与计算流程可以参考[[一文彻底搞懂Transformer模型的Encoder结构与计算流程]],而其中的多头注意力模块的计算流程可以参考[[如何理解Transformer架构中的多头注意力机制?]]。 每个Encoder的输入和输出数据维度保持不变,因此经过全部 Encoder 层的处理后,其输出依然是一个 (197, 768) 的矩阵。 5. 分类头MLP Head 如上所述,Transformer Encoder模块的输出为一个(197,768)的矩阵,其中包含了输入图像的完整特征提取信息。而在分类任务中,实际上只需要取出第一个位置CLS Token 向量(即我们在第二步所额外增加的那个向量)。 对于图像分类任务而言,分类头的作用本质上就是信息压缩与映射:将高度抽象的 768 维特征向量,转化为对具体类别的概率预测。因此通常情况下,ViT 模型的分类头一般由一个简单的多层感知机(MLP)组成。例如一个标准的分类头网络的结构就是两个线性层中间加一个激活函数以提升非线性映射能力,最后再通过Softmax概率化: - 从Transformer Encoder输出的(197, 768)矩阵中,切片取出索引为 0 的向量。 - Linear (768 → 768) - tanh 或 GELU 激活函数 - Linear (768 → K),K 是分类数量 - Softmax 测试预训练的ViT图像分类模型 以下代码通过使用Huggingface的Transformers库下载一个基于ViT架构的图像分类预训练模型,并基于这个预训练模型执行前向推理,得到最终的推理结果: 其中: - ViTImageProcessor用于对输入图像进行预处理,这个预处理包括resize、归一化等,最终把原始图像转换为ViT模型可以接受的格式。这个预处理过程有些类似于自然语言文本在进行Transdformer处理之前的分词流程。 - ViTForImageClassification是真正的ViT分类模型,其接收经过ViTImageProcessor预处理的图像Tensor输出,输出分类结果的logits数据,因此最终需要自行通过softmax得到分类识别的概率。 参考资料 - Vision Transformers (ViT) Tutorial: Architecture and Code Examples | DataCamp - Building a Vision Transformer Model From Scratch | by Matt Nguyen | Toward Humanoids | Medium - Vision Transformer (ViT) Architecture - GeeksforGeeks
- 23 Feb, 2026
如何理解Transformer架构中的多头注意力机制?
自注意力机制简介 Transformer模型的自注意力机制,在Google AI研究员Vaswani 2017年的论文《Attention is All You Need》中首次被提出,该机制随之成为transformer模型的核心思想,而transformer模型又成为各种大语言模型LLM和机器视觉模型VLM的中心所在。自注意力机制的伟大之处在于,它赋予了模型全局视野:让图像中的每一个像素(或特征点)都能直接与其他所有像素发生联系(ViT),让序列化的自然语言段落中的每一个字词(Token)直接与整个段落中的所有其他字词产生联系(LLM)。 以下面这个例子来进行说明: !image.png 对于上面的这个简单的句子的理解来讲,要理解这个句子,当我们看到sat这个单词的时候,注意力会自动放在与之密切相关的cat和mat这两个单词,而其他单词对于sat的理解意义不大,基本可以忽略。因此,所谓的注意力就是:基于语义或语法关系,帮助模型决定整个文本中其他单词与其正在处理的单词之间的相关性和密切程度。当两个单词之间的关系越密切,其权重就越高,否则其权重就越低。 预处理 当然,在进行自注意力运算之前,首先要把序列化的文本数据进行预处理。对于文本数据而言,数据的预处理环节主要包含三个部分: - 分词(Tokenization)。把整个段落和句子从语义上切分为一个个独立的单词(token)。 - 向量化(Embedding)。把以上切分出来的token向量化,大致的方法就是类似于查字典,基于token找到与其对应的固定长度的向量。 - 增加位置编码(Positional Embedding)。 此外还要给每个token的向量加上与其长度相同的位置编码,位置编码反映了这个token在整个段落和句子中的位置。 !image.png 至此,整个段落的序列化文本就被转换为一个个独立的token,并且每个token都对应于一段固定长度(如512,768等)、增加了编码信息的向量。而这些向量就是下一步进行自注意力运算的输入数据。 Q,K,V矩阵 自注意力机制最核心的设计在于将输入向量变换为三种不同的角色:Query(查询)、Key(键)和Value(值)。 我们可以使用搜索引擎来对这三种不同的角色进行类比: - Query (): 你在搜索框输入的“关键词”(我想找什么?)。 - Key (): 数据库里视频的“标题/标签”(我有什么特征可以被匹配?)。 - Value (): 搜索结果对应的“视频内容”(如果匹配上了,我能提供什么信息?)。 从数学运算流程的角度去看,自注意力运算的第一步,就是把前述的序列化数据所转换的向量作为输入,分别乘以三个权重矩阵 ,得到 Q, K, V 向量。 !image.png 假设输入的段落文本被切分为64个token,每个token的向量长度为512,那么输入数据的维度就是(64,512),三个权重矩阵 的维度均为(512,512),因此最终得到的 Q, K, V 这三个向量的维度与输入数据的维度相同,仍然是(64,512)。 !image.png 对每个Token的输入向量,分别执行以上的QSK公式的计算,得到三个与之对应的Q、S、K向量。然后在对每个token进行处理的时候,可以通过自己的 Q 向量和其他token的 K 向量之间的点积来计算其相似度,以此来衡量两个元素之间的相关性,即 。 例如,针对上面的句子,使用“sat”的查询向量 Q 与语句中其他的每个单词(包括其sat本身)的键向量 K 进行以上计算,其结果是一组相似性分数(这些是标量),这些分数告诉我们每个单词与“sat”之间的相关程度,而这些分数就是所谓的注意力分数。 为了防止以上计算出来的注意力分数梯度消失或爆炸,还要除以一个缩放因子 (其中 是特征维度,如512),然后再通过 Softmax 转化为概率分布(权重之和为 1),最终用这些权重去乘以 V,就得到最终的输出。 以上复杂的计算过程,最终得到了一个在当前的上下文中表示“sat”这个单词的新的向量表达形式,这个新的向量信息中综合考虑了整个句子中所有单词的语义,但主要来自于 cat、mat 等与sat相关的token。 这个计算过程对于句子中的每个token都会进行计算。每个token的语义提取都会同时关注其他所有的token,甚至关注其自身,这就是为什么我们称之为自注意力机制。 !image.png 注意力计算公式如何体现 Q, K, V 矩阵的关系? 以上在解释 Q, K, V 向量时提到: - Q 是Query,代表当前token要从整个段落包含的所有token中要查询的语义是什么。 - K 是Key,代表了整个段落中所有token所具有的特征列表。 - V 是Value,代表了 Q和K 匹配的情况下,其他token能够给当前token所提供的辅助语义是什么。 而QKV矩阵的计算公式则是: 那么以上的这两者之间究竟是如何建立关系呢? 在以上公式中,首先是计算 。对 Q 向量(我想找什么)与 K 向量(我有什么特征)做点积运算时,本质上是在计算:“当前的 Query 描述的需求,与这个 Key 提供的属性有多契合?” 从矩阵计算的角度上讲, 是整个文本段落中token的总数量, 是每个token的向量长度(如512,768等),那么整个段落所有token对应的 Q 是 的矩阵, 是 的矩阵,而所有token的 相乘后得到的 矩阵(这个结果实际上就是所有token之间的注意力分数矩阵),每一行就代表了某个 Token 对整个段落所有token在段落中精确语义解析的匹配分数表。 以上经过点积算出来的分数可能是任何实数,没法直接用来合并信息。此时 Softmax 登场了:Softmax 会放大分数高的,抑制分数低的,然后再归一化,把所有的分数转化成 0 到 1 之间的概率,且总和为 1。 最后一步就是把经过以上处理的注意力分数矩阵,与所有Token的 V 向量相乘。从数学计算的本质上,这是一个加权求和的过程。其输出是经过权重(权重就是注意力分数矩阵)过滤后的、包含了整个段落上下文信息的新特征向量。 > 总的来说,Q, K, V 向量及其注意力计算公式的本质,就是要对当前Token在该段落中所代表的精确语义来进行提取。具体的计算过程,则是首先计算当前token的 Q 向量与段落中所有Token(包括当前token)的 K 向量之间的匹配程度,其计算结果就是在解析当前token精确语义的过程中,段落中所有token的相对重要程度(即注意力分数),然后基于其重要程度提取出所有token的 V 向量,其加权和就是当前token在当前这个段落中的精确语义。 至此,自注意力机制的逻辑概念,以及完整的计算过程就解释完毕了。 多头Multi Head? 接下来思考一个问题:如果只用一组 Q, K, V(单头),当前 token 每次只能从一个维度去提取语义。 但在复杂的文本语义处理中,一个token元素可能在不同的维度上具有多种关系。 例子: “小明在图书馆借了一本厚厚的书。” 那么在提取“书”这个词的精确语义时就包含有多个不同的维度: - 语法方面:关注到“借”,识别出“书”是动词的宾语。 - 属性方面:关注到“厚厚的”,提取出“书”的物理特征。 - 位置方面:关注到“图书馆”,提取出动作发生的场景。 _因此,如果只有一个头,模型必须在同一组 Q, K, V 权重中同时编码语法、属性和位置场景信息。这会导致注意力被稀释,模型很难同时精准地捕捉这些不同维度的特征。而多头机制的提出,就允许模型在不同的子空间去独立学习这些不同维度的关系。_ 多头注意力在具体的操作和计算流程上其实是比较简单的,计算流程上与以上描述的单头注意力机制完全相同,只不过把输入的特征向量切分为多个相同长度的子向量而已: - 拆分 (Split):将输入的高维特征向量(例如 512 维)切分成多个低维的子向量(比如 8 个头,这样的话每个头长度就是 64 维)。 - 并行计算 (Parallelism):以上切分出来的 8 个头独立、并行地进行前述的 Q, K, V 计算。 - 拼接 (Concat):把 8 个头各自的计算输出(每个都是 64 维)重新再拼成一个 512 维的长向量。 - 投影 (Linear):最后乘以一个权重矩阵 ,让这些来自不同维度的计算结果进行最终的融合处理。 !image.png 参考资料 - Understanding Multi-Head Attention in Transformers | DataCamp - What is Attention and Why Do LLMs and Transformers Need It? | DataCamp
- 22 Feb, 2026
一文彻底搞懂Transformer模型的Encoder结构与计算流程
下图是一个标准的Encoder Only类型的Transformer架构的示意图: !image.png 总的来说,Encoder Only类型的Transformer架构的执行流程依次为: - 数据预处理环节,包括输入数据的向量化(Embedding),增加位置编码等。 - 连续N个Encoder Block。 - 数据的输出环节。 对于Encoder Only Transformer架构而言,数据的输出环节很简单,例如上图所示的Linear+Softmax输出,所以本文的重点是前两个部分的计算流程。 1. 数据预处理 对于自然语言处理类型的Transformer架构来说,数据在进入Transformer架构的Encoder和Decoder中处理前,至少要完成以下工作: - 分词(Tokenization),把一个完整的句子或者完整的语句段落,分为一个个有独立语义的字或单词(也就是所谓的token),这一步可以使用目前已经非常输出的分词工具来完成。 - 基于以上分词的结果,把一个个独立的字或者单词转换为相同长度(如512,768等)的向量,这一步操作就是所谓的嵌入(Embedding)。经过这一步向量化后,每个token都对应于一组相同长度的向量。 - 以向量长度768为例,如果要进行处理的文本包含有100个token,经过这一步的向量化后,这段文本就会被转换为(100,768)的向量。 - 在之前的句子中给向量化后的token增加位置编码(Positional Encoding),位置编码的长度与Token的向量长度相同,所以每个token的向量直接与其位置编码的向量直接相加即可,相加以后的数据维度不变。 - 仍然以前面的(100,768)维度的向量为例,增加了位置编码后,其维度仍然是(100,768)。 下图是一个对于文本"How are You"进行数据预处理的完整流程: !image.png 经过以上的数据预处理环节以后,一段文本(分割为N个token,每个token的向量长度为dimension)就会把转换为(N,dimension)维度的向量,做好了进行Encoder处理环节的准备了。 2.1 Multi-Head Attention中的Q、K、V矩阵 众所周知,Transformer 的核心是自注意力机制(Self-Attention),它的工作逻辑是计算输入序列中每一个元素与其他所有元素之间的相关性。这个自注意力机制的关键组件是:Q、K、V,可以把自注意力想象成一个“信息检索”过程: - Query (Q):我想找什么?(当前像素/块的特征) - Key (K):我有什么?(其他所有像素/块的特征标签) - Value (V):我能提供什么?(实际的内容信息) 只是看以上的定义的话,感觉还不是很直观,究竟这个所谓的Q、K、V在数据处理的流程中是如何发挥作用呢?直接说结论:如果每个token对应的向量的长度是dimension的话,Q、K、V就是三组独立的维度为(dimension,dimension)的权重参数矩阵,这三组权重矩阵就是要在训练过程中要训练的参数。 也就是假设一个 Token 的向量 的维度是 ,那么 就是三组维度为 的矩阵,那么针对这个 Token 向量 x 进行Q、K、V的的计算过程就是直接把Token向量与三个参数矩阵各自相乘,这样就可以将输入的 Token 向量映射到新的空间: 如下图所示:“Data visualization empowers users to”这个文本被分为6个词(Token),每个词对应的向量长度是768。在这种情况下,每个token的维度就是(1,768),而 这三个权重矩阵的维度分别是(768,768),三组矩阵合在一起的维度就是(768,2304)。 !image.png 总结起来,在这一步,每个token对应的(1,dimension)维度的向量,经过以上计算以后,就会变成三组独立的(1,dimension),分别对应于这个Token的Q、K、V向量,这三组向量合并起来的维度就是(1,3 x dimension)。 2.2 Multi-Head Attention中的Attention计算流程 经过上一步的处理以后,就有了一段文本中所有单词的Q,K,V向量。下一步要进行的计算就是:基于Q向量和K向量,提取出来对应的V向量。 首先对一段文本中所有Token之间的Q向量和K向量进行点积计算,通过这种方式计算出来第 个 Token 对第 j 个 Token 的关注程度: 如果 和 的向量方向越接近,其计算得到的分值就越高。经过以上计算以后(假设这段文本的token数量为N),得到的分数矩阵的维度是(N,N)。 而为了防止以上的计算结果梯度爆炸,并且需要把以上的计算结果转化成概率,还需要对以上计算结构额外执行两步计算: - 缩放:对以上每个除以 。其中的 是一个缩放因子,它的存在是为了让模型在深层堆叠时依然能够稳定训练。对于一个设计好的Transformer模型而言, 是一个常数,一般是数据维度处于多头数量。 - Softmax:将以上的分数矩阵变成 之间的权重矩阵 。 以上两步的计算流程如下图中的红框所示:以下经过softmax处理后的矩阵 的维度仍然是(N,N),对于下图而言,这段文本只有6个token,所以这个矩阵的维度是(6,6)。 !image.png 有了以上的权重矩阵 ,就可以用这个权重矩阵去融合所有的 Value (即V矩阵): 如下图的红框所示:假设这段文本中的token数量为N,每个token的向量长度为dimension,这部分计算就是一个(N x N)维度的矩阵(以上通过Q和K计算出来的权重矩阵)与一个(N x dimesion)维度的矩阵的乘积,最终的结构仍然是(N x Dimension)维度。 !image.png 至此,Encoder模块中的自注意力Q、K、V矩阵的计算流程就算是结束了,整个Encoder的计算以(N,dimension)维度的数据作为输入,计算完成以后仍然是一个(N,dimension)维度的输出数据,可见数据维度并没有发生变化。 等等!还有个问题,前面对单个Token进行的Embedding嵌入转换的计算过程中,明明提到每个token的向量长度是768,但是上面在进行最后一步V矩阵运算的时候明显可以看到V矩阵的维度是(6,64)而不是(6,768),最终的输出也是(6,64),而不是(6,768)?这就是牵出多头(Multi Head)的概念了。 2.3 Multi-Head Attention中的Multi-Head 下面看这个完整的Multi-Head Self Attention Block的框图:左侧是所有输入token的Q、K、V矩阵,数据维度为(N,3 x dimension);右侧为这个多头注意力模块的处理结果,维度为(N,dimension)。 !image.png 下方用红框的标记,是指整个注意力模块的运算被分为12个独立、并行的计算模块,同时进行计算,最后再把计算结果合并在一起,这就是所谓的多头。 举例而言,上图中包含有6个token,每个token的数据维度为768,那么对于整个自注意力模块而言,其输入为三个独立的(6,768)维度矩阵,即6个token各自的Q、K、V向量。那么在以上的多头处理上,计算流程会分为12个头,把输入数据也同样分为独立的12组,对于每个Token的Q、K、V向量而言,每个长度768的向量都被分为12段,每段的长度就是768/12=64(这就是上面V向量计算中所出现的64)。这样的话,对于每个head的计算而言,其输入数据的维度就变成了三个(Q,K、V向量)独立的(6,64)维度矩阵,单个head的计算输出数据也是(6,64)维度,所有head计算完成后再合并在一起成为(6,768)维度。 > 这就是所谓的多头:就是把输入的每个token的Q、K、V向量平均分成N份,这N份数据同时进行计算,计算完成后再合并在一起。最终输入数据和输出数据的维度保持一致。 可结合下面的这段代码更深入的理解整个多头自注意力机制的工作流程: 3.0 Feed Forward Encoder模块中的前馈网络相对就比较简单了,实际上就是两个线性层中间加一个非线性激活函数来实现输出数据的非线性变换而已,其输入输出数据的维度仍然保持为(N,dimension): 4.0 完整的Encoder Block 一个完整的Encoder Block的框图如下图所示: !image.png 可以看到,一个独立的encoder block,实际上就是一个多头注意力模块及其残差块,加上一个前馈网络模块及其残差块。基于以上对多头注意力模块和前馈网络的模块,理解下面的encoder block的执行流程也就比较容易了: 至此,单个Encoder Block的完整计算流程就算是比较完整了。注意,对于单个encoder block而言,其输入数据和输出数据的维度都是相同的,即都是(N,dimension),这样也就很容易实现把多个encoder block按顺序叠加在一起形成完整的transformer架构。 5.0 完整的Transformer Encoder架构 再回过头来看完整的transform encoder的架构,理解起来就比较简单了: - 首先是数据预处理部分。输入的文本等其他序列数据,经过分词和向量化的处理后,把每个token转换为固定长度的向量,然后再叠加位置编码信息得到相同维度的带位置编码的向量,送给Encoder进行处理。数据维度为(N,dimension)。 - 接下来中Transformer Encoder架构的核心所在,即连续多个Encoder Block的叠加,每个encoder block的输入输出数据维度均为(N,dimension)。 - 最后是输出层,这部分就是根据应用需求来进行针对性的设计了。 !image.png 参考资料 - Transformer Explainer: LLM Transformer Model Visually Explained - How Transformers Work: A Detailed Exploration of Transformer Architecture | DataCamp - Transformer Architecture : Part 1- Encoder | by Abhishek Jain | Medium - Working of Encoders in Transformers - GeeksforGeeks
- 09 Feb, 2026
SAM3模型环境搭建以及图片+视频分割测试
本文详细记录了Meta在2025年11月份开源的图像分割模型SAM3的环境搭建,以及基于其中的Sample进行图片和视频分割的流程。 SAM3(Segment Anything Model 3)是Meta发布的最新版本图像分割模型。它延续了前一代SAM模型的设计理念,进一步提升了在图像分割任务中的性能和适应性。 !image.png 源代码及其权重文件下载 首先是从sam3的Github仓库中下载源代码:facebookresearch/sam3: The repository provides code for running inference and finetuning with the Meta Segment Anything Model 3 (SAM 3), links for downloading the trained model checkpoints, and example notebooks that show how to use the model.。 除了在Github上下载源代码以外,还需要提前下载好模型的权重文件sam3.pt(3.2GB)。按照Github仓库Readme的介绍,正规的流程应该是在Huggingface上提出申请(facebook/sam3 · Hugging Face),申请通过后就可以从Huggingface上进行下载,但是这个申请经常会莫名其妙的reject掉申请,而且不给出任何原因,不清楚是怎么回事: !image.png 所以可以通过其他办法提前把这个权重文件下载到。知乎一位UP主提供了权重文件的下载路径:互链高科,下载速度非常快。因此可以从以上站点先下载权重文件,放到sam3工程的根目录下。 环境搭建 开发环境的需求如下: - Python 3.12版本及其以上 - Pytorch 2.7版本及其以上 - Nvidia显卡,CUDA12.6版本及其以上 按照项目的readme文件创建sam3的conda环境并且激活该环境: 然后安装pytorch,以及进入sam3的源代码安装sam3 package:(我的显卡是5070Ti,Cuda版本是12.9,所以直接按照pytorch官方的命令进行以下安装即可) 需要注意:在sam3环境中要运行以下的静态图片以及视频测试的例程时,会提示No module named 'triton'错误,此时还需要额外装一个triton-windows的包: 图片/视频分割测试 静态图片测试 以下代码对一张静态图片中所包含的熊目标进行检测,并画出结果。 > 以上代码需要在官方源代码plot_results代码的最后增加一句plt.show()才能把图像真正显示出来。 以上代码执行的图像分割效果如下所示: !image.png 以上代码中,processor执行检测和分割操作返回三个变量masks、boxes和scores: - masks:与原图分辨率相同的单通道mask图像,在这个mask图像中以不同的色彩标记各个检测目标的像素位置,有几个分割目标就有几张独立的mask图像。 - boxes:对指定目标类型的检测框,在图像中检测到几个目标,就有几个box,每个box中都包含有检测到目标的检测框的左上角和右下角的坐标。 - scores:检测到的各个目标的置信度。 下图是以上熊图像中使用bear来进行检测的输出信息,可结合以下信息理解三个输出变量的含义: 视频测试 基于对Bilibili Up主Coding茶水间的教学视频(参考资料3)继续测试SAM3的视频分割功能。这个测试需要比较高的显卡配置,我的笔记本32GB内存,RTX5070Ti显卡,跑一段22s的视频足足花了6分40秒,所以做这个测试还是尽量选一些比较短的视频来进行,否则每次调试等待的实际太磨人了。 以下是使用SAM3对视频文件中的每一帧图像基于提示词的提示进行指定目标的检测和分割,整体流程沿用了Coding茶水间的代码逻辑,但是再完成视频文件中每一帧图像分割以后,对分割得到的mask图像帧与原始图像帧之间进行叠加生成分割后保存的图像流程做了一些简化,使用render_masklet_frame来进行mask与原始图像帧的合并操作,代码理解上更简单一些。 本质上,以下代码经过propogate_video_frames处理以后,就得到了目标分割图像mask的列表sam3_output_frames,这个结构中包含有所有检测到指定目标的图像帧列表,video_file_frames结构中保存了从视频文件中所读取到的所有图像帧列表,然后在循环中把分割图像帧mask与原始图像帧进行合并操作,就得到了两帧合并的图像。而没有检测到目标的图像,依然使用原始图像帧,最终把所有的合并图像帧保存在一个mp4文件中。 关键代码如下所示: 参考资料 - facebookresearch/sam3: The repository provides code for running inference and finetuning with the Meta Segment Anything Model 3 (SAM 3), links for downloading the trained model checkpoints, and example notebooks that show how to use the model. - SAM3模型来了,手把手带你运行SAM3模型代码,SAM3模型初探!_哔哩哔哩_bilibili - SAM3源码实战之二,手把手带你执行视频分割处理。_哔哩哔哩_bilibili - 连载1:Meta SAM3开发实战——最简图像分割 - 知乎
- 03 Feb, 2026
实例讲解OpenAI的轻量级多模态模型CLIP
CLIP(Contrastive Language-Image Pre-Training) 模型OpenAI在2021年初发布的用于匹配图像和文本的预训练神经网络模型,是近年来多模态研究领域的经典之作。 CLIP解决的问题是什么? 作为计算机视觉(CV)领域最具里程碑意义的模型之一,OpenAI 在2021年发布的 CLIP 彻底打破了传统视觉模型依赖固定类别标签和类别的局限。因此,它不仅是一个模型,更是一种将视觉与语言深度对齐的范式革命。 在CLIP之前,计算机领域的图像识别、目标检测等领域的主流模型,如Resnet、VGGnet、SSD、YOLO等,通常是在 ImageNet或者COCO 这种人工提前标注好的数据集上进行训练的。如果这个模型训练了一个 1000类别的分类器,那么它就只能识别这 1000个类别。此时如果要增加一个新的类别,就必须重新收集数据并重新训练。 而CLIP是直接利用互联网上现成的海量图片+描述性文本,训练模型对于图像/文字的理解,并建立两者之间的语义关联,即:图片的视觉特征与其对应的文字描述在语义上的匹配性。这意味着,一旦CLIP模型训练完成,你不需要为新任务训练任何参数,只要能用语言描述出来,CLIP 就能识别出来。 !image.png 下面以一个实际案例的方式,解析基于CLIP模型以及Meta的Faiss向量数据库,来实现一个对本地数据集使用自然语言命令进行搜索的实际案例,通过这个案例来强化对CLIP模型概念和工作流程的理解。 以上案例主要包含三部分内容: - 基于CLIP实现图片文件到Embeddings向量的编码 - 保存图片向量数据到本地Faiss向量数据库中 - 基于自然语言的文本命令从向量数据库中查询图片并显示 Image Embedding 对于图片文件所执行的编码,要使用CLIP模型的preprocess和encoder,所以首先要import CLIP模型: CLIP模型支持不同的架构变体,其主要区别在于视觉编码器的架构和规模大小。 > CLIP模型的不同架构变体:'RN50', 'RN101', 'RN50x4', 'RN50x16', 'RN50x64', 'ViT-B/32', 'ViT-B/16', 'ViT-L/14', 'ViT-L/14@336px' 其中ResNet系列为RN前缀,Vision Transformer系列为ViT前缀,一般来说,ViT架构普遍优于同规模的ResNet架构,而更大的模型通常能提供更好的zero-shot分类性能,但也会消耗更多计算资源。这里使用ViT系列中的最小规模模型ViT-B/32,使用32x32的patch。 接下来就是调用CLIP模型的model和preprocess分别对图片和文本进行向量化操作: 可以看到,CLIP对于图片和文本信息进行的encode操作大同小异,都是先进行encode,然后再执行归一化,区别只在于: - 对图片进行的encode操作调用encode_image接口,对于文本则是调用encode_text接口。 - 文本信息要先使用CLIP的tokenize分词操作后再进行encode转换为向量。 使用Faiss保存Image向量 以上通过调用get_image_embedding接口所得到的图片向量,需要保存在一个向量数据库中,这样下次程序启动的时候,就可以直接从向量数据库中加载并进行检索,而不需要从头再生成图片的向量了。 这里使用由 Meta 开发的高性能向量搜索库Faiss(Facebook AI Similarity Search)。大致的思路和流程是:使用 faiss.write_index 将图片文件的向量索引保存为 一个 .index 文件,同时使用 pickle 或 json 保存所有的图片路径列表 metadata。后续在进行检索时,使用get_text_embedding 生成检索文本的向量,并利用 Faiss 的内积搜索(Inner Product)来实现图像和文本向量之间的余弦相似度匹配,通过检索得到的图片文件 id 在图片路径列表 metadata 中就可以找到图像的路径了。 每次程序启动时从硬盘上尝试加载现有的向量库索引文件以及图片路径文件。第一次运行时这些文件还不存在,此时需要创建一个 IndexFlatIP 格式的向量数据库文件。Faiss 能够支持多种向量索引模式,IndexFlatIP 是其中最基础且精度最高的模式。这种模式不压缩向量,也不建立复杂的树状或图状结构,而是原封不动地存储所有的原始向量数据(通常是 float32)。在查询时,它会将查询向量(即查询文本转换的向量)与数据库中的每一个向量进行逐一计算,这种方式虽然速度最慢,但召回率(Recall)是 100%,即结果绝对精确。对于50000张图片以下的图片库基本上足够用了。如果图片库的数量超过了 10 万张,且查询速度明显变慢,可以使用 IndexIVFFlat 模式,它能将查询速度提速 10-100 倍。 以下代码从本地硬盘的图片库中分批次逐一对图片文件进行向量化(调用get_image_embedding),最后把图片向量列表和图片文件的路径名称列表分别保存在index_path指定的向量数据库文件中和image_paths指定的图片路径列表文件中。 Image Search 前面的步骤实现了本地图片库的向量转换并保存在向量数据库中,接下来就可以利用文本检索命令从这个图片向量数据库中检索指定的图片了。 这部分比较简单,主要可以分为三个步骤: - 对文本内容进行分词、向量化以及归一化操作,这一点与前面的图像向量化的操作流程基本一致。 - 以检索文本的向量为参数调用 Faiss 向量数据库的search接口查询图片,返回值为查询结果的相似度和在图片库中的索引。 - 把查询到的图片列表显示出来。 最终在我的数据集中查询”a boy who is playing football“得到的接入如下: !image.png 参考资料 - 讓AI幫你穿搭!用CLIP實作一個時尚穿搭資料庫 - Building Image search with OpenAI Clip | by Antti Havanko | Medium
- 01 Feb, 2026
开源VLM模型MoonDream
本文详细介绍了业界非常流行的开源轻量级视觉语言模型MoonDream,总结了该模型的核心功能列表、基本功能测试流程以及相关的license等情况。 Moondream 是由开发者 Vikhyat Korrapati 发起并维护的一个开源、轻量级的视觉语言模型(Vision-Language Models, VLM)。该模型的定位是专为端侧设备(Edge Devices)和资源受限环境而设计的高性能 VLM,其核心优势是能以极低的能耗和内存占用(不到 4GB VRAM 甚至在 CPU 上流畅运行),实现与大模型(如 LLaVA 甚至早期 GPT-4V)相媲美甚至在特定任务上超越的视觉理解能力。 MoonDream的官网地址:Moondream !image.png 截至2025年底,MoonDream已经演进迭代到第三代,其前两代模型Moondream 1/2的参数规模大约为 1.6B - 2B,甚至还针对资源更紧张的应用提供了 0.5B参数量的版本。而最新的Moondream 3 模型采用了 MoE(混合专家)架构,总参数量达到了 9B,但单次推理的激活参数只有2B。 什么是VLM? 首先要更深入的理解所谓的视觉语言模型VLM究竟是什么?简单来说,VLM(Vision Language Model) 是一类能够同时理解图像和文字并实现跨模态交互的 AI 模型。 更通俗地讲,所谓的VLM模型,从其整体结构和工作的流程上可以如此理解: - 在输入端,可以认为其输入包含两个部分,一张图片,针对这张图片的一段自然语言的命令。 - 在输出端,VLM的输出是一段文本信息(例如以json格式封装的文本内容),即使是要输出位置坐标等数字内容,其输出的也是一段包含数字字符串的文本信息。 - 模型的工作流程:模型会按照自然语言的命令对输入的图像进行描述解释、目标搜索和定位等,最终输出描述这个图像内容和其他检索性信息的文字内容。 - 最典型的应用就是对图片内容进行描述、从图像中检测出特定物体所在的位置坐标、针对图像生成其OCR检测结果等。 其他的VLM模型包括:阿里的Qwen2-VL,开源的LLaVA 系列,微软的Phi-3.5/Phi-4 Vision以及Florence-2,谷歌的PaliGemma,国产AI Lab的InternVL 2。 MoonDream的环境搭建与核心功能演示 以下以应用上最为成熟、License最为宽松的MoonDream2描述环境搭建以及进行功能演示。 首先是从huggingface上下载Moondream2的模型仓库和权重文件,其模型仓库地址为:vikhyatk/moondream2 at main。 但是Moondream2的权重文件接近4GB,从HuggingFace上直接下载耗时很长,国内网络的情况下可以从Huggingface的镜像网站hf-mirror.com上进行下载。 以下是模型及其权重文件的下载代码,模型相关文件会被下载到D:\models\moondream2下: Moondream 的核心功能矩阵包括四种模式:caption, query, detect, point。这四种模式分别对应于不同的图像理解的颗粒度(从整体到局部)和输出形式(从非结构化文字到结构化坐标)。以下代码演示了以上几种模式在本地下载的MoonDream2模型文件上的推理流程。 首先安装演示代码的依赖包: 推理代码如下: 代码的执行结果如下图所示: !image.png MoonDream的License Moondream 的不同版本采用了完全不同的授权策略。 !image.png 可以看到。Moondream的版本1和版本2均采用了最宽松的Apache 2.0 license,可以自由的在商业领域中使用。而最新的MoonDream 3在商业领域中的使用则存在一定的附加条件: - 可自由使用的情况:在服务器上部署后仅供公司内部使用,做为公司产品的功能嵌入使用,其他的非盈利和研究使用。 - 禁止使用的情况(此时需要另外获得商业授权):搭建服务器后通过API方式以HTTP或者SDK等方式对外提供收费服务,与官方的付费服务高度重合的应用。 参考资料 - Overview | Moondream Docs