Showing Posts From

Ai

模型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

RAG系统工作流程的几个问题澄清

本文简单总结了RAG系统的整体工作流程和模块分解,并在其基础上解释了 RAG工作流程以及向量数据量存储与检索向量相关的几个问题。 RAG 系统的基本流程与基本模块组成 下图是一个典型的RAG系统的工作流程分解图: !78e0975c-3248-43c4-a2f6-100c321de106.png 从模块拆解的角度上,RAG系统主要包含有以下几个独立的模块: - Indexing (索引层):将非结构化文档清洗、基于语义拆解为不同长度的chunk、嵌入(Embedding,即转换为向量的操作)并存入向量数据库(Vector DB)。 - Retrieval (检索层):根据用户提问题的 Query 消息计算用户问题文本与向量数据库之间向量的余弦相似度,从向量数据库中召回 Top-K 个相关片段。 - Augmentation (增强层):将从向量数据库中召回的片段与原始的 Query 文本拼接,构建增强后的提示词文本(Prompt)。 - Generation (生成层):把增强后的提示词文本送入 LLM,由 LLM 根据增强后的上下文输出最终答案。 以上工作流程和模块拆解简单直接,本文后续部分对 RAG 系统在工作中的一些基本概念再进行澄清,以确保对整个系统工作的各个方面有更为清晰的理解。 向量数据库中存储向量的维度问题 如上图所示,加入知识库的文档,首先会根据语义被分割为不同长度的 chunk(也就是说,每个chunk包含有不同数量的token),那么如果每个chunk包含的token数量不同的话,他们对应的Embedding 向量的数据维度也就是不同的? _针对以上问题,实际的情况是:无论输入(包括用户输入问题的文本,以及RAG知识库中分解出来的文本Chunk)的文本是长是短(只要在模型的最大上下文窗口内),经过同一个 Embedding 模型处理后,其输出的向量维度(Dimension)永远是固定且相同的。_ 因此,即使两个 chunk 的长度不同,包含的 token 数量不等,经过 Embedding 模型转换以后,转换后的向量维度都是相同而且固定的,这个固定的向量维度都与 Embedding 模型所指定的 token 的维度相同。 在具体的转换流程中,无论是知识库文本的某个 Chunk A 还是 Query,经过模型降维和特征提取以后,输出都是一个 维度的浮点数数组(一维向量)。 也正因为如此,后续才能对两个维度完全相同的向量(即用户输入问题文本对应的向量,以及向量数据库中某个 chunk 对应的向量)进行相似度的计算。 那么,不同长度的文本是如何变成同样长度的向量的? 这是由于底层大语言模型(基于 Transformer 架构)在输出层使用了池化(Pooling)机制。具体实现中包含以下两个步骤: - Token 映射: 不同长度的文本首先被转为 Token 序列,每个 Token 在模型内部会映射为一个隐藏状态向量。此时是一个 的矩阵。N 为Token数量,D 为Token的维度。 - Pooling(降维聚合): 模型会通过某种策略将这个 的矩阵压缩成一个 的向量,代表整段文本的全局语义。这个 Pooling 操作常见的做法包括: - Mean Pooling(平均池化): 将所有 Token 的向量在每一列上求平均值。 - CLS Token: 提取输入序列最前面特殊的 [CLS] (Classification) Token 对应的向量,它被训练用来表征整句话的语义。 向量相似度的计算逻辑 如上所述,无论是用户输入问题的文本 prompt 还是保存在向量数据库中知识库文档文本段落对应的向量,其维度都是相同维度的 。在后续进行两个向量的相似度计算时,主要使用的逻辑是余弦相似度 (Cosine Similarity) 和 内积 (Dot Product)。 余弦相似度衡量的是两个向量在空间中的夹角,以向量 (Query)和向量 (Chunk)为例,它们的维度都是 : > 💡 在自然语言处理或音频检索中,特征向量的模长往往代表文本的长短或音频段的能量大小,而向量的方向才代表真正的语义或特征属性。因此使用以上的余弦相似度计算公式,可实现规模与抽象语义两者之间的的解耦。其中的分子部分 ()实际上就是两个向量之间的内积(Dot Product),用于衡量两个张量在各维度上的绝对重合度。而其中的分母部分 ()是一个标量,作为归一化因子,强制消除因张量范数(模长)的不同而带来的缩放误差。 两个相同维度的向量,经过以上公式计算以后,其结果的范围在 -1 到 1 之间。 - 结果值越接近 1,说明两个向量的夹角越小,向量在空间中接近重合,语义越相似,是 RAG 召回的完美目标。 - 结果值趋近于0,说明向量在空间中相互垂直,可以认为两个向量之间完全无关(即正交),两段文本讨论的是完全不同的主题,特征维度无交叉。 - 结果值接近于-1,说明向量在空间中是背道而驰,语义上是相反/对立的。在某些特定训练的 Embedding 空间中,这种情况代表语义的完全对立(如好与坏的极端)。 然后向量数据库基于这个公式(或其近似的高效算法如 HNSW)对向量数据库中保存的所有向量与用户提问问题的向量进行以上计算,召回计算结果中最匹配的 Top-K 个文档。 而在工程实践中,如果在保存到向量数据库中的向量在入库前,已经对所有的文档向量 进行了 归一化(也就是使其模长为 1),同时对查询向量 也做了相同的归一化操作,那么上面公式中的分母就变成了 1。此时,余弦相似度的计算就简化成了内积的计算: 向量数据库的检索流程 以上部分给出了通过两个向量的余弦相似度计算公式,找到与用户提问问题语义最接近向量的计算过程。但是,如果 RAG 数据库中所保存的内容很多,向量数据库中的记录数量很多的话,如果每次检索都要与所有的数据库进行执行余弦相似度计算的话,那么这个计算量是非常惊人的。 以下详细总结和解释一下在向量高速检索领域应用最主流的HNSW(Hierarchical Navigable Small World,分层可导航小世界)算法。 整体而言,这个所谓的HNSW算法采用了一种多层图(Graph)空间的方式来解决海量向量的计算和搜索问题,在这种多层图结构下,高层网络的节点稀疏,负责快速定位目标节点所在的大致区域,低层网络节点密集,负责执行精细的搜索并准确的定位到目标节点。这样的设计中,绝大多数节点之间并不直接相连,但任两点间只需很少的几步即可找到。 所谓的多层图结构,实际上就是把向量数据库中的每个向量在入库阶段放到不同的层上,高层节点包含有低层的连接信息,这样在进行向量检索的时候,先通过高层节点进行大致定位,然后通过找到的高层节点中的低层节点连接信息下沉到底层,逐步精准定位到距离最近的节点。 每个节点中包含的信息大致使用如下数据结构来进行描述: - Layer 2 的节点(即Max Level = 2): 包含 Layer 0 + Layer 1 + Layer 2 的连接信息。 - Layer 1 的节点(Max Level = 1): 包含 Layer 0 + Layer 1 的连接信息。 - Layer 0 的节点(Max Level = 0): 只包含 Layer 0 的连接信息。 在这种情况下,每次进行向量搜索的时候,先在 Layer 2 的向量列表中找到距离最近的向量 Node-Layer-2,然后拿到向量节点 Node-Layer-2 中保存的 layer_1_neighbors 列表,从中找到 Layer 1 中距离最近的向量 Node-Layer-1,再继续从 Node-Layer-1向量节点中拿到其对应的 layer_0_neighbors 列表,最终从这个列表中找到与目标向量最近距离的 Layer 0 向量。 因此,HNSW 算法的向量搜索流程大致就如下图所述,因为实际的RAG 工作流程中并不只是返回一条记录,而是要返回 Top K 条记录,因此在以上的检索流程中,往往还要在每一层增加一个备选列表,最终返回的结果就是前 K 个距离查询文本向量最近的向量列表: !HNSW_Architecture_Native.png 向量数据库的存储内容 接下来的问题是,对于 RAG 的向量数据库而言,其中保存了文本分割后所有 Chunk 对应的向量。那么在工程实践中,通常这些向量数据库中的每一条记录都保存了哪些信息呢? 通常情况下,一条标准的向量数据库记录至少包含有以下组件: - Primary Key (主键 ID):数据库记录的主键,跟关系型数据库的主键的功能类似,数据格式通常是 Int64 或 String (UUID),作为物理存储和内存索引映射的唯一凭证。 - Dense Vector (稠密向量):这是向量数据库存储记录的核心内容。其数据格式是一个浮点数数组(如 Float32, Float16 或 INT8),对应于各个文本 Chunk 的向量内容。向量数据的检索就是对这个稠密向量与用户输入 prompt 文本对应的向量进行的余弦相似度计算。 - Scalar Metadata (标量/元数据):用于保存伴随该向量存储的一些结构化属性,一般用做属性过滤(例如可以使用该属性区分当前的文本 Chunk 是哪个文本文件中的内容),数据类型通常为 Int, Varchar, JSON 等格式。 可以看到,以上向量数据库中的记录中并不包含原始的文本 Chunk 数据,那么这样的话,后面在进行向量搜索的时候,通过余弦相似度算法搜索到某个向量以后,它如何定位找到这个向量对应的 Chunk 文本以方便与用户输入问题 Prompt 文本进行拼接呢? 针对以上问题,最直接的做法就是:把文本 Chunk 的内容直接作为上述 Scalar Metadata 部分的一个特殊字段(如 VARCHAR 类型),将其与向量一起存储在向量数据库内部。这样后续检索到向量的同时,底层就可以直接把这个向量对应的 Chunk 文本字符串一起返回。 但是,以上方案存在的问题是,向量检索计算的本质是密集的浮点运算,它要求参与计算的特征数组必须驻留在 L3 Cache 或高速 RAM 中。纯文本内容的 Chunk 数据对于向量计算过程毫无用处,但如果把整个向量数据库的内容(包括以上提到的 Chunk 文本内容)一起加载在内存里,会导致严重的内存碎片和计算过程中的 Cache Miss。 因此,目前的主流向量数据库(如 FAISS、Qdrant)在进行向量存储的时候,只会存储 [Primary Key, Dense Vector]部分,而原始的文本 Chunk 内容、Metadata(如来源文档名、页码等)等海量文本信息,会存储在另外一个专门的 Key-Value 数据库(如 RocksDB, Redis)或文档数据库(如 MongoDB, Elasticsearch)中。此时,Primary Key 充当了连接两个异构数据库的外键(Foreign Key),等到向量检索计算找到向量以后,就可以通过 Primary Key 从另外一个数据库找到其对应的 Chunk 文本以及其他的 metadata 。

YOLOv8-Pose模型的网络结构及其输出数据解读

在YOLOv8模型网络架构解读一文中,我已经在对于YOLOv8与YOLOv5模型结构差异的基础上,详细解释了YOLOv8模型的网络结构尤其是检测头部分的设计与计算流程,本文则重点在对比YOLOv8与YOLOv8-Pose网络结构的基础上,对其检测头部分及其输出数据的维度和结构进行详细总结和说明。 回顾标准YOLOv8模型的检测头结构 首先给出结论:YOLOv8-Pose 与标准 YOLOv8 模型在 Backbone 和 Neck 上是完全一致的,其核心架构差异 100% 集中在 Head(检测头)的设计、损失函数的定义以及对应的数据后处理上。所以对YOLOv8-Pose模型的学习,需要建立在对标准的YOLOv8模型有比较清楚的理解的基础上,具体可以参考YOLOv8模型网络架构解读。 标准YOLOv8模型的检测头结构如下图所示。简单来讲,就是整体网络的BackBone部分对输入图像的特征进行提取,Neck部分对不同尺度特征图中所包含的特征信息进行充分的融合,最终把充分融合后的特征信息送入到检测头部分。YOLOv8模型的检测头部分分为两个独立的处理路径,回归头采用CIoU+DFL(抛弃了之前几代延续使用的Anchor-Based的机制)计算和输出检测框的坐标位置,分类头则采用标准的BCE计算各个检测框对应的类别分类。 最终YOLOv8模型的整个检测头输出8400个检测框坐标及其类别信息(置信度直接使用类别信息中的最大值),在后处理阶段则从这8400个检测框中过滤出来最终的有效检测框作为最终检测输出。 !image.png YOLOv8-Pose模型的检测头结构 如上所述,YOLOv8-Pose模型与标准的YOLOv8模型的差异只在于其检测头Head部分: - 标准 YOLOv8 Head: 与YOLOv5等前代版本最大的不同在于采用了解耦头(Decoupled Head),Head部分分为两个并行分支:Box Branch(回归边界框与 DFL)和 Cls Branch(分类)。 - YOLOv8-Pose Head: 在标准的YOLOv8 Head的基础上演变为三分支结构 (Tri-Branch)。在原有 Box 和 Cls 分支的基础上,新增了一个独立的关键点分支 Keypoint Branch。 下图是YOLOv8-Pose模型的检测头结构示意图: !e6de4f91-bf0b-4cbf-bb68-b57d9aa15400.png 可以看到,YOLOv8-Pose模型的输出头的结构中,坐标回归头分支Box Branch和分类分支Cls Branch与标准的YOLOv8模型的检测头部分没有任何差异,区别只在于Pose模型额外增加了一个独立并行的关键点分支KeyPoint Branch。 YOLOv8模型的输出及其过滤 要搞清楚YOLOv8-Pose模型输出数据的过滤逻辑和后处理流程,首先要彻底搞清楚标准YOLOv8模型的后处理流程。 YOLOv8 模型默认是使用COCO数据集来进行预训练的,COCO数据集有80个分类,所以对于标准的YOLOv8模型而言,其坐标回归头输出的每个检测框的坐标位置数据维度为64(具体可参考YOLOv8模型网络架构解读),其类别分类头的输出每个检测框对应的数据维度为80(因为有80个类别),而三个尺度(P3/P4/P5)的特征图总共输出8400个检测框,所以整个检测头的输出维度为 [8400,144]。 在进行后处理的NMS流程之前,坐标回归头的每个检测框的长度为64维度的数据,首先会通过 DFL 的积分操作(Softmax 加权求和)还原成 4 维的绝对坐标(通常是 )。因此,送入后处理阶段(包含 NMS)的模型原始输出张量,其每一个检测框所包含的数据是:4 维坐标 + 80 维类别概率 = 84 维。 至此,YOLOv8模型的输出的维度为 [8400,80+4]。然后,基于以上类别分支输出的80维度的类别数据,对其求 Max,只保留每个检测框的最高类别得分及其索引,把这个最高类别得分作为其置信度。至此的数据维度变成: - 坐标: - 得分: - 类别: 在此后的后处理流程中,主要包含有两个串行的核心步骤和两个阈值(conf_thres和 iou_thres): - 置信度初筛: 基于前述的置信度进行粗筛,去掉所有置信度小于conf_thres阈值的检测框,这样就可以使用极小的算力剔除绝大多数背景框。 - IoU 抑制 (IoU-based NMS): 进一步的精筛过程,这一步是利用几何交并比IoU(如果两个检测框的IoU大于iou_thres阈值,就认为是同一个物体)剔除同一个物体的冗余框。 最终通过以上IoU NMS过滤后的检测框就是模型检测最终输出的结果。 YOLOv8-Pose模型的输出及其过滤 YOLOv8-Pose 模型预训练使用的是 COCO Keypoints (Pose) 数据集。在这个数据集的官方标注中,只有人这一个类别拥有 17 个关键点的完整标注,其他 79 个类别在官方数据集中是没有关键点标注的。因此,Ultralytics 官方在训练 yolov8n-pose.pt 时,其配置文件(如 coco-pose.yaml)中硬编码了 nc: 1(Number of Classes = 1,即仅限人这一个检测类别)。 这就是为什么在官方提供的 YOLOv8-Pose 预训练模型中,Cls 分支的维度是 1,而不是 80 的原因所在。 而关键点分支的输出,在使用 COCO Keypoints (Pose) 数据集的情况下,标注数据中共包含有 17 个人体关键点。每个关键点需要回归 3 个值: - 特征图上的坐标位置 x - 特征图上的坐标位置 y - 可见度置信度 visibility,用于判定该点是否被遮挡或在图像视野外 因此可以看到,关键点分支针对每个检测框输出 17 x 3 = 51维度的关键点数据。 那么,对于YOLOv8-pose模型而言,增加了一个关键点分支的话,模型输出到最终的NMS过滤输出的后处理逻辑是什么呢? 答案是,在YOLOv8-Pos模型的后处理逻辑的运行过程中,整个后处理流程根本不看关键点长什么样,关键点仅仅是被当做边界框的附属数据。框生则点生,框灭则点灭。 也就是说,在YOLOv8-Pos模型的后处理流程中,只有 Box 坐标回归分支和 Cls 类别分支的输出参与置信度初筛和 IoU 计算(这部分流程与标准的YOLOv8模型完全相同),以上的后处理过程最终得到存留框的索引。最终在输出数据的组织中,再利用上一步生成的留存框索引,从原始的 [8400,56] 维张量中,把留存框的坐标、类别、置信度连同它的 51 维关键点一次性全部捞出来。 - 经过后处理流程过滤后得到了3个Person的检测框,也就会同时输出这3个Person框中每个关键点的位置。 所以从根本上讲,YOLOv8-Pos模型的后处理流程与标准的YOLOv8模型没有任何区别,只不过是在最后输出的时候“顺便”把关键点分支的数据一起输出了。 从源码的角度看待YOLOv8-Pose的输出 以下是一段使用YOLOv8-Pose模型对图像数据进行推理的代码: 以上代码中模型的推理结果results ,实际上是一个 Python的列表对象,这个列表的长度等于输入数据的 Batch Size。如果每次调用模型进行推理输入的是单张图像,其推理结果的核心数据就都在 results[0] 中。 results[0] 是一个 ultralytics.engine.results.Results 类的实例,它就像一个集装箱,其内部按任务装载了不同的子对象: - results[0].boxes: 封装了经过 NMS 过滤后的所有边界框信息。 - results[0].keypoints: 封装了跟随边界框存留的所有关键点信息。 - results[0].orig_img: 存储了原始输入的 Numpy 图像(主要用于后续调用 plot 绘制边界框和关键点的底图)。 - results[0].names: 检测类别的映射字典(例如 {0: 'person'})。 其中边界框所包含的主要数据信息及其维度(模型输出的有效检测框数量为N)为: - boxes.xyxy:维度为 [N, 4],对应每个框的绝对坐标,其中左上角 x1, y1,右下角 x2, y2。 - boxes.xywh:维度为 [N, 4],对应每个框的中心坐标与宽高(cx, cy, w, h) - boxes.conf:维度为 [N], 这 N 个框各自的最大类别置信度得分。 - boxes.cls:维度为 [N],这 N 个框各自对应的类别索引。 - boxes.data:维度为 [N, 6],原始的拼接张量,按照 x1, y1, x2, y2, conf, cls 顺序依次排列。 其中关键点数据所包含的主要数据信息及其维度(模型输出的有效检测框数量为N)为: - keypoints.xy:维度为 [N, 17, 2],N 个人,每个人 17 个关键点,每个点对应的绝对坐标 x, y。 - keypoints.xyn:维度为 [N, 17, 2],归一化坐标。x 除以图像宽,y 除以图像高,范围在 [0, 1] 之间。 - keypoints.conf:维度为 [N, 17],N 个人,每个人 17 个关键点的可见度/置信度。 - keypoints.data:维度为 [N, 17, 3],拼接张量,按 x, y, conf 顺序排列(即前述的 51 维数据的重组形态)。

大模型交互中KV Cache的工作原理总结

本文通过描述Agent与大模型之间的交互流程,详细解释Input Token和Cached Token的区别,理解了这两者的区别,也就能够理解这两种token在算力需求和价格上的差异所在。 Input Token和Cached Token的区别 我们从各个大模型厂商购买Token额度的时候,看到有三种不同的价格:输入价格(缓存命中),输入价格(缓存未命中),输出价格。输入和输出Token的概念很好理解,但是这里的缓存命中和缓存未命中的区别是什么呢? !b8e61c06-b89d-4e35-ae1a-41f50eca9547.png 同样的,我们在使用各种Agent,例如Claude Code、Gemini Cli的时候,在结束本轮会话退出时,Agent都会输出本轮会话的输入Token、缓存Token以及输出Token的用量统计数据,那么这里的Input Tokens和Cache Reads之间的区别究竟是什么呢? !28145375-6db3-447c-a161-6ec65851c5d6.png 如果没有Cached Token的话? 首先简化问题,在不考虑任何缓存机制的情况下,Agent 与大模型之间的通信始终是绝对的无状态函数交互。大模型没有任何记忆,每次的交互和调用对它来说都是一次全新的过程。 在Agent(或者与大模型进行对话的Web页面)与LLM大模型之间进行会话的交互过程中,两种的角色分工如下: - Agent(智能体): 运行在本地机器或云服务器上的一段代码,是整个应用会话的业务控制中心,负责维护双方对话历史记录、管理工具(API 接口)以及控制执行流程。 - LLM(大模型): 云端的逻辑推理引擎。它只负责一件事:通过HTTP协议从Agent上接收一段纯文本输入,根据概率预测并输出下一段文本。 正是因为大模型在整个交互过程中没有任何记忆,所以在 Agent 与大模型之间进行会话的过程中,无论整个会话过程会持续多少轮,Agent在每次发起新一轮的会话时,都需要把所有的历史记录(Agent的问题以及大模型的答复)全部积累起来,发给大模型,这样大模型在收到以后才会对从头开始双方的会话记录理解的基础上,针对新的问题返回与之对应的答复。这样随着会话轮数的增加,Agent 发给大模型的历史会话记录自然会如滚雪球一般越来越大: !%E5%AE%8C%E7%BE%8E%E5%AF%B9%E9%BD%90%E7%89%88_%E4%B8%8A%E4%B8%8B%E6%96%87%E6%BB%9A%E9%9B%AA%E7%90%83%E6%95%88%E5%BA%94.png 这样就会带来两个问题: 问题一,带宽与 Payload 的体积爆炸问题: 每一轮交互,Agent 都要把几十 KB 甚至几 MB 的历史会话记录重新通过 HTTP 发送一遍。如果会话记录中包含有代码库或者PDF文件的话,经过很多轮的累计,其所需要的代码和内存空间是灾难性的。 问题二,算力极度浪费(滚雪球效应导致 Input Token 飙升)问题: LLM 本身是无状态的。在第 N 轮对话时,前 N-1 轮的文本 LLM 已经计算过 N-1 次了,但在没有缓存的情况下,它必须机械地再把这些文本的矩阵乘法重新算一次,这个算力就白白浪费了。 针对以上的这两个问题,就很有必要通过在 LLM 服务器端缓存KV Cache的方式来解决了。 KV Cache 如何解决问题? 目前主流的大语言模型都是 Transformer 架构,Transformer 架构中的内部计算流程细节可参考一文彻底搞懂Transformer模型的Encoder结构与计算流程 和一文彻底搞懂Transformer模型的Decoder结构与计算流程 两篇文章。 简单的总结,每个词对应的向量 Token 进入 Transformer 架构中进行注意力运算时,它的词向量 会分别乘以三个不同的权重矩阵 ,从而得到这个词在当前上下文语境中精准语义表达的的 向量。 当模型在工作的过程中要生成下一个词时,它会生成当前的 ,并使用这个当前的 向量,去和前面所有历史 Token 的 进行点积计算(算相关性分数);算完相关性分数后,再把所有历史 Token 的 按照分数加权求和。 在以上的这个注意力计算过程中,历史 Token 的 Q 是没有用到的,但它们的 K 和 V 则会在后面的 Token 生成中反复使用,而 KV Cache 要存的就是:_输入文本在经过每一层注意力运算时,通过矩阵乘法算出来的所有的_ _向量和_ _向量_。 这样下次新一轮会话进来的时候,历史会话中所有 token 的 KV 值就不需要计算了,直接从内存中读取即可。只需要计算新一轮会话内容的 KV 值,然后与历史会话的 KV cache 合并/拼接在一起即可。这样在 LLM 计算的过程中就少了计算历史会话文本所有 token 的这个环节。 通过下面的流程图来对KV Cache 缓存机制的运作流程进行解释: - 阶段一会对整个会话的完整上下文进行一次全量(总共1000个token)的注意力计算,计算完成后会把其结果的 KV Cache 保存起来。 - 阶段二在生成每个词的时候,就不再需要对上下文的文本内容进行全量注意力计算,只需要使用新的 向量与 KV Cache缓存的内容进行计算得到输出,然后把新生成词对应的 和 向量追加了 KV Cache 在后续环节中继续使用。 !LLM%E5%8F%8C%E9%98%B6%E6%AE%B5%E6%8E%A8%E7%90%86_Prefill%E4%B8%8EDecode.png 以上就是在大模型服务器端把 KV Cache 缓存起来能够极大程度上降低对 GPU 算力的需求和浪费。 隐式缓存问题和显式缓存问题 如上所述,通过在服务器端缓存 KV Cache 的方式确实是可以有效的解决算力浪费的问题(即以上的问题二)。但是需要注意的一点是,服务器的内存是有限的而且极其昂贵的资源,海量用户的海量请求需要在服务器端维持大量的缓存,这样也就意味着,这些缓存不可能一直保存,一定有一个时效性。 如果服务器端缓存的 KV Cache 的时间超过了其时间限制(例如Agent或者用户在当前的会话中超过1个小时没有新的请求),这个KV Cache就会被删除。那么,在这种情况(KV Cache超过时效被删除)下,用户又有新的请求发到大模型服务器的话,大模型会如何处理呢? 目前主流的大模型,针对以上问题有两种处理方案:隐式缓存和显式缓存。 1. 隐式缓存 在采用这种架构中,Agent与大模型之间的每一轮交互,Agent 都会老老实实地把所有的历史对话记录、System Prompt 加上最新的对话请求,全部打包成一个巨大的 JSON,通过 HTTP 发送给大模型。 当这个巨大的 JSON 到达云端的大模型服务器后,其 API 网关会快速计算这段文本的前缀哈希值(Hash)。 - 如果它通过这段前缀发现其内存中已经包含了历史会话的 KV Cache(即Cache命中的情况),大模型在后续的运算中就会直接跳过这部分文本的 Transformer 矩阵运算,只对尾部新增的请求内容进行计算。这种情况下只有新增请求部分会被计入Input Token。 - 而如果它通过这段前缀在内存中找不到这个历史会话记录的KV Cache(即Cache失效了),那么大模型就只好把这个 JSON 中包含的完整历史对话记录、System prompt以及最新的对话请求全部完整进行Transformer运算, 因此,在这种模式Cache命中的情况下,网络 I/O 的成本依然很高(因为每次会话仍然需要把完整的会话历史记录全部发上去),只是算力(GPU 的计算时间)被省下来了,所以大模型厂商会通过 Cache Token 的方式对大模型的使用费用打折,大模型计算的响应速度也变快了。 - 这种模式可以解决以上的问题二(算力浪费的问题),未能解决以上的问题一(网络带宽IO的问题)。 2. 显式缓存 另外一种模式则是显式缓存模式。在这种模式下,那些很厚重、不会频繁变动的静态上下文(比如整个项目的软硬件架构文档、长篇的 API 规范文档、技术手册、复杂的 System Prompt),Agent 只需要在最开始上传一次。服务器收到并计算完 KV Cache 后,会返回一个资源标识符(Cache ID)。 在后续 Agent 与大模型之间的反复对话中,Agent 的 JSON Payload 里不再需要包含那些厚重的静态上下文,而只需要填入一串字符串:"cached_content": "caches/12345-abcde",以及双方之间的会话记录即可。 大模型服务器收到 Agent 发过来的请求以后,首先使用请求包中的 Cache ID 对应的KV Cache是否仍然存在于内存中。如果缓存命中,则可以直接使用KV Cache,只对 Agent 发上来的会话记录进行 Transformer 计算,然后把 KV 值拼接起来,生成新的上下文。 但是如果 Cache ID 对应的KV Cache已经失效被删除,此时大模型服务器会直接返回一个 404 错误,并在附带信息中说明 Cached content not found。此时 Agent 就需要把本地缓存的静态上下文的所有资料重新发给大模型,针对所有的上下文重新进行一次密集的 Transformer 运算,然后返回一个新的 Cache ID 给 Agent。 因此,这种显式缓存的工作模式,可以同时解决问题一和问题二,只有在缓存失效的情况下,才需要把比较大的静态上下文发给大模型执行一次完整的Transformer运算。 !%E9%9A%90%E5%BC%8F%E7%BC%93%E5%AD%98vs%E6%98%BE%E5%BC%8F%E7%BC%93%E5%AD%98%E5%AF%B9%E6%AF%94.png

详解卷积网络中BN层的概念及其计算流程

本文详细总结了在各种卷积神经网络结构中普遍存在的批归一化层(BN,Batch Normalization)存在的理论意义以及在各种计算机视觉网络模型中BN层进行计算的流程。 BN层的概念与存在的意义 自从 2015 年 Google 提出 Batch Normalization(BN)以来,它几乎成为了所有现代 CNN网络结构(如 ResNet, YOLO, EfficientNet 等)的标配层。 !image.png 在BN层出现之前,训练一个几十层的深层神经网络存在的问题是什么? - 模型的训练过程对权重的初始化设置值极其敏感:如果初始权重值设置得稍大或稍小,经过网络多层的传播后,最终输出的数值要么爆炸,要么归零。因此当时的研究员们不得不花费大量精力去研究各种非常复杂的参数初始化方法。 - 学习率必须设得非常小:为了防止网络参数在训练过程中崩溃,训练的学习率只能设置得极其保守(比如 0.0001),这样就导致模型训练时间极长,训练效率很低。 - 梯度消失问题:2015年前后的网络设计中普遍使用 Sigmoid 或 Tanh 等作为激活函数。如果输入的数据过大或过小,很容易就会落入这些激活函数的饱和区(曲线变得极其平缓的地方),从而导致梯度几乎为零,即梯度消失,网络直接停止学习。 出现以上问题的原因在于:一个被命名为内部协变量偏移(ICS)的现象。这个所谓的ICS看起来很复杂,实际上说的就是一个简单的问题:模型在训练的时候,网络第一层的参数稍微更新一点,这点微小的改变,在经过第二层、第三层……一直传递到第一百层时,会被指数级放大。这样的话,对于深层次的网络来说,它每一次接收到的输入数据的统计分布值都在剧烈地、毫无规律地变化中,根本没有办法从这个剧烈变化的数值中学习到任何规律了。 针对以上问题,BN 层裁员了以下直接的方法来解决问题:既然上游传过来的数据分布总是不稳定的,那我们就在每一层前面额外增加一个处理,强制把上游的数据洗牌,统一整理成标准的模样。 具体来讲,无论上一层传过来的数据统计是什么形态,BN 层都会计算当前的完整训练批次数据(即所谓的Mini batch)的均值和方差,强行把输入数据拉回到均值为 0,方差为 1 的标准正态分布的形式。在这种情况下,无论一个网络的层次有多深,每一层都能够清楚的确认,其上一层次发过来的数据的形态一定符合标准的正态分布。 问题是,如果强行把上一层输入的数据变成均值 0 、方差 1的正态分布,那岂不是会破坏掉之前网络层次辛辛苦苦学到的特征区分度吗? 针对以上问题,在进行归一化计算流程的处理以后,还需要增加另外一个线性变换的环节和两个可学习的参数 (Scale) 和 (Shift) 来恢复网络的表达能力,把“究竟是否需要对输入数据归一化”以及“归一化的程度”的选择权交还给了网络自己去学习和决定。具体的计算流程如下所述。 另外一个问题是:BN层一般放在CNN网络中的什么位置呢? 答案是,BN层一般会夹在线性计算(如卷积层)和非线性激活函数(如 ReLU)之间。如下图所示: !image.png 这是因为:卷积计算操作后所产生的数据往往在统计分布上非常散乱。如果在送入激活函数之前,先用 BN 层把数据聚拢到 0 附近,就能够完美地让数据落在激活函数最敏感、最有效的区域,从而获得最大化且健康的梯度反馈。 BN层的计算流程 总体来讲,BN 层的核心算子包含两个阶段:正态归一化(Normalization)计算阶段;线性仿射变换(Affine Transformation)阶段。 在计算流程上,对于一个给定的 Mini Batch 内的特征 ,BN 层会首先求其均值 和方差 ,然后进行归一化计算,归一化计算的结果就是对输入数据进行以0为中心、方差为1的线性映射;最后再通过两个可学习的参数 (Scale) 和 (Shift) ,重新定义一个新的线性映射来恢复网络的表达能力。 > 💡 > 所以说到底,无论 BN 层在训练时看起来多么花哨,BN 层计算的两个阶段都是线性变换。所以在数学本质上,整个 BN 层对输入数据的处理也仍然是一个简单的线性变换 。 整个BN层的计算公式及其计算流程如下所示:前三个公式为归一化计算流程,最后一个公式为线性映射流程。 从数据维度上讲,输入和输出数据的维度没有发生任何变化,语义信息的结构也得到了完整保留。在进行归一化计算时,整个计算过程始终以输入特征图的通道为单位来进行计算,也就是说, 在计算单个通道的均值和标准差时,不仅仅是跨越 张图片,而是要把这 张图片在同一通道下的所有像素点全部汇总在一起计算。 > 💡 为什么要以通道为单位进行归一化计算?这是因为,在 CNN 中,每一个通道代表着一种独立且特定的语义特征(Semantic Feature),按通道独立归一化,是为了保护不同特征之间的物理独立性,避免两个独立的语义特征被强行平均。 下图为标准的CNN网络中BN层进行计算的各个阶段的数据维度状况: - Stage 1:首先输入数据为上一个卷积层所输出的特征图,其维度为 - Stage 2:以通道为单位,取出 张图片某个通道的特征图,对这 张特征图的所有数据求平均值和方差,最终得到 个平均值和方差 - Stage 3:仍然是以通道为单位,对该通道每一张特征图中的像素基于该通道的平均值和方差进行归一化计算,这样每个通道的特征图数据都转换成了满足均值为0,方差为1的标准分布形态 - Stage 4:线性映射阶段,每个通道有一组可在训练过程中学习的参数 和 ,基于这一组参数对该通道的特征图进行线性映射 - Stage 5:最终输出的数据形态仍然保持与输入一致。 !bd0b18312f67600e745c52997774bc43.png 为了进一步让整个计算流程更为清楚,下图以一组张3通道640x640分辨率的图像作为输入,进行完整的 BN 层的计算流程进行说明: - 整个计算过程中,因为输入数据是3通道,所以整个输入批次的数据就有3组均值和方差,同样,也就有三组可学习参数 和 ,分别对应于三个通道。 - 当然下图只是为了进行计算流程的说明,BN 层在CNN网络中极少直接作用于网络的原始输入图像。对于原始图像(),通常会在输入图像数据的预处理阶段,使用固定的常数(如 ImageNet 的全局均值和标准差)直接进行减均值除方差的操作。BN 层真正的舞台,是放置在网络深层的特征图上,对卷积层输出的特征图进行处理。 !4f65a87e3daa101a9c82653891b7ee26.png BN层在推理过程中的行为 需要注意的是:BN 层的计算在训练和推理时的行为是完全不同的。 在训练流程的前向传播过程中,训练代码会基于当前 Mini-batch 的数据计算其真实的统计量 和 。同时,为了后续在推理过程中使用,BN 层在训练时还会默默在后台维护全局的滑动平均均值(running_mean)和滑动平均方差(running_var)。 而在推理(即 model.eval() )模式下,BN 层就不再计算当前 batch 的统计量(每次推理的输入数据可能只有一张图片,推理结果可能会因为 batch size 的不同而波动),而是会直接使用在训练时所积累的 running_mean 和 running_var来进行归一化计算。 同样的,BN 层计算的线性映射阶段的可学习参数 (Scale) 和 (Shift) 到了推理阶段也是固定的,因此整个 BN 层的线性运算就可以完美地、数学上等价地合并(Fusion)到上一层卷积的权重()和偏置()之中。 因此,在ONNX导出的时候,一般会结合使用 ONNX-Simplifier,这个过程会自动把 Conv 和 BN 相融合,这样导出的 *.onnx 模型图中一般就看不到独立的 BatchNorm 节点了。

如何把PT格式的模型转换为TFLite/LiteRT模型?

之前在尝试把Ultralytics的YOLOv8模型运行在Android手机上的过程(参考总结Android Flutter项目中YOLO模型的执行流程)中,直接使用了YOLO模型的export接口即可实现把该模型导出为ONNX、TFLite等格式。但是如何我们在开发过程中遇到的模型没有官方所提供的export接口,如何能够把pt格式的模型文件转换为TFLite,从而方便的部署在Android手机上呢? 因此,本文尝试解决这样的问题:如何把一个通用的 PyTorch 模型权重(.pt)文件,通过模型转换处理,变成在 Android 端侧能跑的 TFLite 模型文件(.tflite)? Google的AI Edge Torch/Litert 针对以上所提出的问题,一个比较流行的解决方案是: - PyTorch (.pt) → ONNX: ONNX 是模型交换的通用格式,所以这一步转换相当于是把模型文件脱离了 PyTorch 的框架限制。 - ONNX → TensorFlow (SavedModel): 这是一个转译过程,从ONNX的生态语言转换为TensorFlow生态,确保目标平台(即Google 手机APP)能正确的识别模型文件中的算子。 - TensorFlow → TFLite: 这有点类似于 Linux 下的 strip 和 objcopy,这一步操作会进行常量折叠、算子精简以及最重要的量化操作(Quantization),把默认的 FP32 的权重参数变成 INT8,以适应移动端 CPU/NPU 的算力和存储方面的限制。 但是,以上这个传统的 PT -> ONNX -> TF -> TFLite 路径并非是一个完美的解决方案。这是由于, PyTorch 坚持使用 NCHW (即所谓的Channel First) 而 TensorFlow 则默认使用 NHWC (即所谓的Channel Last),那么转换工具在以上传统的转换路径中为了保住数学上的等价性,会疯狂插入 Transpose 算子。这在手机 CPU/GPU 上运行过程中,会导致频繁的内存重排,严重拖慢推理速度。 因此,Google 在2024年4月推出了一个AI Edge Torch的工具,用于实现从 PT 模型文件向TFLite格式模型文件的直接转换。这个工具利用了 torch.export(PyTorch 2.x 的核心特性)直接捕获计算图,并将其映射为 TFLite 算子,更重要的是,它可以直接在模型转换过程中对齐推理过程中的数据格式,这样就消除了大量不必要的 Transpose 算子,避免后续推理过程要执行的大量内存重排。 到了2025年,Google 对移动端 AI 生态进行了一次品牌重塑:之前的TensorFlow Lite被重新命名为LiteRT。这主要是因为,Google 认为 TensorFlow Lite 这个名字太受限于 TensorFlow 框架了。现在整个AI模型生态的趋势是 PyTorch 统治科研与训练,而 TFLite 统治移动端部署。因此为了吸引 PyTorch 用户,Google 强调 TFLite 这个 Runtime 是通用的端侧AI部署工具,不只是给 TensorFlow 用的。所以现在新的品牌叫 LiteRT (Lite Runtime)。 !image.png 环境安装 要在PC端把一个Pytorch的模型转换为 TFLite/LiteRT 格式,主要是需要依赖于litert-torch和ai-edge-litert 这两个包。 - litert-torch:对应之前的 ai-edge-torch,负责将 PyTorch 算子映射到 LiteRT。 - ai-edge-litert: 对应之前的 tflite-runtime,是推理引擎,负责在 Android 或 PC 上执行生成的 .tflite (LiteRT) 文件。 截至目前(2026年3月份),litert-torch和ai-edge-litert 这两个包的接口变动仍然非常剧烈,导致其对环境的要求非常苛刻。所以一定要创建一个独立的Python环境,并且提前安装好其指定的torch版本,否则后面安装这两个包的过程中很容易出现包冲突的情况。经过仔细检查,目前比较稳定的环境安装步骤为(在Windows的WSL2环境中安装): 注意:这个模型转换和PC端推理的环境必须安装在Linux环境中,我自己在Windows中安装环境总是会以下错误。从报错信息上看,litert-torch依赖于ai-edge-tensorflow,而ai-edge-tensorflow 目前主要发布在 Google 的 Nightly/Experimental 仓库中,对 Windows 平台的原生支持非常滞后。所以这个转换工具最好还是在Linux或者Windows的WSL2环境中来安装。 模型转换 环境安装好以后,就可以开始把Pytor下的预训练pt格式的模型文件转换为TFLite格式了。我这里直接使用Ultralytics/YOLO项目中的yolov8s.pt这个文件做转换测试。 以上模型导出脚本能够成功的导出 FP32 的模型文件,FP32 虽然精度最高,但由于其对内存带宽和存储空间的占用比较大(对于YOLOv8s这个模型而言,转换后的模型文件大小为44MB),在端侧 CPU/NPU 上运行肯定不是性能最优选,因此接下来还需要进行量化(Quantization)处理。 量化 对于YOLO模型自带export接口而言,量化是一个很简单的事情,只需要设置导出时的fp16、int8等导出格式就可以了。但是使用litert_torch 完成模型导出之后,对TensorFlow Lite模型文件进行量化操作的接口发生的变化非常剧烈,导致现在在网上看到的几乎所有量化参考代码在最新的litert-torch 都无法正确、成功的完成量化操作。 通过学习https://github.com/google-ai-edge/litert的ReadME文件: > > > I have a PyTorch model... > > - Goal: Convert a model from PyTorch to run on LiteRT. > - Path1 (classic models): Use the LiteRT Torch Converter to transform your PyTorch model into the .tflite format, and use AI Edge Quantizer to optimize the model for optimal performance under resource constraints. From there, you can deploy it using the standard LiteRT runtime. > - Path2 (LLMs): Use LiteRT Generative Torch API to reauthor and convert your PyTorch LLMs into Apache format, and deploy it using LiteRT LM. > 可以看到对于目前比较新的LiteRT实现而言,量化需要用一个新的工具ai-edge-quantizer(https://github.com/google-ai-edge/ai-edge-quantizer)。这个工具是 Google 专门为了对LiteRT模型进行 INT8/INT4 权重压缩打造的新一代利器。在最新的 Edge AI 理念中,单纯的文件体积 FP16 截断已经不再流行,权重 INT8 + 激活值浮点 (即所谓的Dynamic Quantization) 才是能在手机等端侧设备上上平衡速度、体积和精度的终极王者。 以上的量化流程中选择了wi8-afp32的模式(即权重参数使用INT8量化,激活值仍然使用FP32量化),最终得到的量化后的模型文件只有11MB的样子,是量化前的FP32模型文件的1/4。 除了以上的wi8-afp32量化模式,ai-edge-quantizer还支持其他的量化模式,如下图所示,Google 的工程师用了一种非常直白的命名约定来定义这些量化模式: - w = Weight(权重参数,即模型训练出来的固定参数) - a = Activation(激活值,即模型在推理时流动的中间特征图) - i8 / i4 = 8位 / 4位整数(Integer) - fp32 = 32位浮点数(Float) !8a772873-2af8-4892-8f32-b1998f10d39e.png 推理测试 基于 LiteRT模型文件已经转换并量化完成,下一步就是在PC端的LiteRT runtime中对其进行推理测试。 如上所述,在 LiteRT生态中,ai-edge-litert包对应于之前的 tflite-runtime,也就是负责在 Android 或 PC 上基于LiteRT模型文件执行前向推理的运行时框架。因此,在PC端进行前向推理的流程大致也就是:加载模型以及推理的图片文件→数据预处理→执行推理→对推理结果执行NMS后处理→把NMS过滤的结果叠加在输入图像上。 参考资料 - AI Edge Torch: High Performance Inference of PyTorch Models on Mobile Devices - Google Developers Blog - Convert Models From Pytorch to TFLite With AI Edge Torch | by David Cochard | ailia Tech BLOG (EN) | Medium - https://github.com/google-ai-edge/litert-torch/blob/main/docs/pytorch_converter/README.md - https://github.com/google-ai-edge/ai-edge-quantizer

TFLite如何处理算子版本和兼容性的问题?

在瑞芯微NPU平台上进行模型板端适配的时候,在进行模型板端转换的时候,ONNX 导出时设置的 Opset 版本,与板端模型转换工具(RKNN-Toolkit2)能够支持的 Opset 版本的一致性非常重要,如果板端模型转换工具不支持onnx文件中的某些算子类型的话,这个转换过程就会报错。那么,TFLite是否也存在 Opset 版本的问题,也就是模型导出为 TFLite 格式的时候也应该有个 Opset Version,这个 Opset 的版本需要与 interpreter 能够支持的 Opset 版本相对应,否则也会出现以上瑞芯微平台模型转换的问题?以及在TFLite的工作流程中,究竟是处理Opset版本一致性问题的?本文尝试总结以上问题的答案。 相关的背景信息可参考: - ONNX的Opset版本: 如何理解ONNX模型导出的Opset版本? - TFLite框架的核心概念和工作流程: 如何理解TFLite/LiteRT的核心概念和框架流程? TFLite的Opset版本信息 针对TFLite框架下,是否也有与ONNX相同的OpSet版本的概念和处理这个问题,简单的答案是:TFLite同样有算子版本的概念,但是与算子版本的处理流程与ONNX有较大的差异。 .tflite 文件本质上是一个序列化后的 FlatBuffer 二进制文件。在这个文件头部,有一个专门的 OperatorCode 数组(也就是所谓的算子注册表)。当模型被导出时,TFLite 转换器会把这个模型文件中用到的所有算子登记在这个表里,并且每个算子独立记录其算子版本号,而不是像ONNX那样有一个统一的算子集版本号。 如下图所示的两者的显著差异:TFLite采用的是细粒度的基于各个算子的独立版本号,模型文件本身没有算子集版本的概念;而ONNX采用的这是全局的算子集版本号,通过模型文件来规范和管理整个算子集的版本。 !%E6%97%A0%E6%A0%87%E9%A2%98-2026-03-26-1350.png 为什么TFLite要采用这种单个算子集别的版本管理模式,而不是采用ONNX这样的全局算子集Opset版本的方式? 最主要的原因是,算子级别的版本管理方式可以更好的向前兼容性,这一点对于软硬件碎片化非常严重的Android生态系统非常重要。假设我们的模型中仅包含有最基础的 Conv2D 和 Relu 算子,使用最新版本的模型转换器导出的时候: - 如果是 ONNX的全局Opset模式:模型转换器会将这个ONNX文件标记为比较新的 Opset 18。这样的话,即使 Conv2D 这个算子在 Opset 1 到 18 里没有发生任何变化,那么一个老版本的 ONNX Runtime 在看到 Opset 18 版本的情况下,可能就会直接报错并拒绝加载。 - 而在 TFLite 这种单算子版本的管理模式中:模型转换器在转换过程中,发现该模型没用到 Conv2D 的新特性,就会在TFlite模型文件里将 CONV_2D 这个算子标记为 Version 1。这种情况下,即使用户的Android手机里包含的是很早版本的 TFLite Interpreter,也能完美加载并运行这个最新导出的模型。 算子版本的匹配 与其他嵌入式平台的模型导出、适配以及板端的加载运行相同,在Android系统上运行TFLite格式的模型,在算子的版本匹配上,需要关注三个环节:模型导出和转换工具的算子版本(Converter),Interpreter支持的算子版本,以及delegate支持的算子版本。只有以上三者的算子版本信息相互匹配,才能确保这个TFlite模型在Android系统上成功的跑起来。 - TFLite 模型转换工具、Interpreter以及Delegate等相关的概念及其在Android系统中的结构,可以参考如何理解TFLite/LiteRT的核心概念和框架流程? 这一篇笔记。 Converter TFLite 模型的 Converter 工具内部有一套算子版本试探的算法,来自动检测模型文件中各个算子的版本信息。例如对于 FullyConnected 这个算子,在没有开启 Keep_num_dims 特性的情况下,Converter 在进行模型转换的时候,就会给这个算子打上 Version 1 的标签;而如果模型的推理计算过程需要开启这个新特性,Converter 才会给该算子打上 Version 2。这就相当于是一个“能用低版本绝不用高版本”的算子版本决定策略,这个策略是保证模型和 App 拥有最广设备兼容性的核心所在。 总结起来,对于Converter而言,无论这个Converter工具有多新,它的工作逻辑都是:在进行模型TFLite格式转换的过程中,它会扫描和试探出来每个算子在模型计算图中所要求的最低版本,然后把这个最低版本作为该算子的版本,以此来尽量保证后续这个算子以及模型能够支持到范围最广的设备。 Interpreter Interpreter 也有自己的版本支持情况,那么它在加载TFLite模型文件和构建模型的计算图的过程中,同样会去扫描各个算子的版本信息,并检查该版本与自己支持版本的适配情况。 Interpreter 在加载 TFLite 模型文件的 FlatBuffer 结构的过程中,提取每个算子的版本信息,与自己内部的 OpResolver(算子注册表)进行比对。对于一个算子而言,如果在TFLite文件中该算子需要的版本是 Version 5,而 Interpreter 的代码库里算子注册表只实现了 Version 4,它会立即抛出 Unresolved builtin op 异常,并拒绝执行。 因此,一个模型究竟能否在Android系统中跑起来,Interpreter 对于算子版本的支持情况非常关键,这就是为什么在使用 Flutter 框架开发Android App 时,应该尽量使用最新版本的 tflite_flutter 插件。 > 💡 Interpreter本身就是一个普通的 C++ 动态链接库(比如 libtensorflowlite_c.so),它是被硬打包进 APK 里的,而不是由 Android 系统底层提供的。这样的好处是,Interpreter与APP里面的模型强绑定,只要APP完整测试通过,就可以确保运行在不同硬件平台和Android系统版本上。因此,如果系统中包含有多个跑不同模型的APP,也就有多个各自独立的Interpreter副本。当然,TFLite 的核心解释器被设计得极其精简,去除了所有训练相关的代码。一个基础的 C++ Interpreter 库通常只有 1MB 到 2MB,对现代 App 的体积影响微乎其微。 在 Interpreter 中实际上包含了其所支持的所有算子的CPU实现,这样的话,如果 Delegate 不支持某个算子的话,Interpreter 就会自动调用其内部实现的CPU算子。 Delegate 而在 Delegate方面,同样有自己所支持的各个算子的版本信息,在Delegate与Interpreter握手的过程中,Interpreter会拿到了Delegate所支持的各个算子的版本信息。在这种情况下,Interpreter 在模型前向推理的算子调度过程中,就能够清楚的知道哪些算子 Delegate 能够支持,哪些算子Delegate不支持必须要放在CPU中执行。 当然,不同于 Interpreter 是内置在Android APP层面中,Delegate 对于算子的支持是固定在Android系统中的。这也很好理解,因为 Delegate 之下的 NPU 驱动的实现需要依赖于SOC厂家的实现,是不可能放在APP层面来解决这个问题的。因此,除非升级整个系统,Delegate层面对算子版本的支持情况就是固定的,Delegate没法支持的算子就只能被Interpreter调度到CPU上去执行。 TFLite前向推理与算子版本相关的完整流程 至此,对于TFLite的算子版本管理,TFLite模型文件导出时的算子版本推定,在Android系统中Interpreter和Delegate进行算子版本匹配和调度的完整流程就呼之欲出了: !TFLite_Architecture_Flowchart.png

总结Android Flutter项目中YOLO模型的执行流程

之前基于 Flutter 实现了一个在Android APP中调用YOLO模型来实现图像目标检测功能,本文的 YOLO 模型的 TFLite 导出以及在 Android Flutter 项目中进行调用的完整执行流程进行总结。 在Android上跑AI模型,标准的做法是导出模型文件为 TFLite 格式,然后在 Android 的 TFLite+Interpreter+NCAPI 框架下执行AI模型的推理流程,与该框架相关的知识可参考 如何理解TFLite/LiteRT的核心概念和框架流程?一文。 YOLO模型的TFLite导出 因为 Ultralytics 项目中的模型导出的实现已经非常完备,所以可以直接在ultralytics项目中通过其export接口导出 tflite 模型文件即可,此处选择导出为half即float16的格式: 此外,后续在Flutter项目中调用以上模型文件进行推理,还需要创建一个与模型对应的label.txt文件,其中包含有模型检出类别对应的类别名称字符串,按顺序每行一个。因为我测试中直接使用的是YOLO的预训练pt文件,预训练模型文件对应的是COCO数据集,所以这里的label.txt中包含的类别名称字符串大致就是如下80个类别: 依赖管理 因为要在 Android Flutter APP 增加跑AI模型的的能力,首先需要在项目根目录下的pubspec.yaml和 android/app 目录下的build.gradle.kts增加相关的依赖项。 flutter项目中的pubspec.yaml是该项目的元数据中心(Metadata Center)和依赖管理器(Dependency Manager)。其中包含的主要内容是:项目的基本配置信息,生产环境的依赖库,开发/测试阶段的依赖库,资源与资产等。对于以上提到的依赖项,每次在构建项目时,都会自动调用 flutter pub get 从pub.dev下载指定依赖库的源代码到本地。 该项目中需要对手机camera和本地图片的图像调用YOLO模型进行检测,所以pubspec.yaml中相关的设置项包括: 因此,对于在 Android 项目中跑YOLO模型而言,以上配置文件最关键的就是其中导入了 flutter_vision 这个插件。该插件内置了 TensorFlow Lite C SDK,并针对 YOLO 这种特定模型,包含了输入输出的 ByteBuffer、维度转换(Tensor Shape)和 NMS(非极大值抑制)等的相关处理,对于应用层程序而言,只需要调用 vision.loadYoloModel() 接口,它就可以在 Android 原生层创建 TFLite 的 Interpreter并执行推理任务。 模型在Flutter中的完整推理流程 与其他板端模型的完整推理流程类似,在Android Flutter项目中调用YOLO模型执行推理任务同样包含模型加载、前向推理以及后处理这三个环节。 模型加载 基于flutter_vision插件对AI模型进行加载的整体流程如下: 整体的模型加载过程很简单,调用flutter_vision的loadYoloModel接口创建一个模型加载的异步任务,传递模型文件和label文件等参数,然后等待这个异步任务执行完成即可。 需要注意的是,flutter_vision 插件只能够支持ultralytics项目export接口直接以tflite格式导出的YOLOv5,YOLOv8,YOLOv11这几个模型,而且参数中指定的模型文件与modelVersion的参数必须设置正确,否则后续的后处理流程会出错: !074e2e0e-d2b9-48e6-b0b2-4a1f06769c23.png 那么如果我们要在 Android APP 中跑其他模型呢?那就只能使用 tflite_v2 插件、flutter_tflite插件或者底层 TensorFlow Lite API。 前向推理和后处理 flutter_vision插件的前向推理过程主要就是调用其yoloOnFrame(针对摄像头的图像)和yoloOnImage接口(针对本地图片)。 以上的推理接口执行中本身已经包含了YOLO不同版本的后处理流程,所以其输出结果是一个的数组。这个数组中的每一项都对应于一个最终检测框的类比和坐标+置信度,如下所示: 应用层拿到以上的这个数组以后,就可以对其中的内容进行解析并在图像上画框标注了。因为 yoloOnFrame 和 yoloOnImage 接口中已经包含了后处理的NMS等环节,所以不需要在应用层再执行后处理了。 参考资料 - flutter_vision | Flutter package

如何理解TFLite/LiteRT的核心概念和框架流程?

本文整理了TFLite的核心概念,与ONNX框架的对比,及其内部各个组件的关键工作流程,尝试建立对这个Android系统下的标准AI推理框架的整体概念和工作流程建立完整的理解。 TFLite是什么?以及与ONNX的比较 Google推出的 TensorFlow Lite 是一个专为移动、嵌入式和边缘设备设计的开源深度学习框架。它能够支持开发者在手机(Android/iOS)、微控制器(MCU)及各类物联网设备上,以极低的延迟和较小的二进制体积运行高性能的推理任务。 !image.png 众所周知,在边缘侧的板端设备上运行各种 AI 模型,已经有一个非常流行的 ONNX 框架,那为什么不在 Android 上直接使用 ONNX?原因主要还是 Facebook的 Pytorch/ONNX 与Google的 TensorFlow 之间生态竞争的问题。 虽然 ONNX Runtime 现在也能够支持在 Android上运行,但在 Android APP 中跑AI Model还是应该首选使用 TFLite,原因如下: - 兼容性的问题:在 Android 系统的实现中,Google 提供了一层 NNAPI (Neural Networks API) 的封装。TFLite 是 Google 主推的端侧机器学习框架,它对 NNAPI 的支持是第一优先级的。如果用 ONNX 的话,在调用不同 SOC 厂家的 NPU 时,可能会遇到各种驱动不兼容或性能折损的情况,兼容性的处理会非常头疼。 - 内存与包体积的问题:要在 Linux 或者 Android 系统中使用ONNX来运行模型,就需要把 ONNX Runtime 内置进去,相关的库文件通常很大(几十 MB),这样就会直接增加 App 的安装包大小和内存占用情况。而如果使用TFLite的话就没有这个问题,TFLite 的核心解释器非常小(只有几百 KB),而且 Android 系统中已经内置了相关的运行时环境。 TFLite模型部署的整体工作流程 将一个复杂的深度学习模型部署到手机上,通常需要经历从训练、精简/格式转换/量化再到板端执行的完整的过程。这个流程对于所有的设备端 AI 框架的工作流程而言都是大致相同的。 例如对于瑞芯微平台上的模型适配而言,其整体的适配流程如下: - 首先在PC/AI服务器端完成模型训练,导出为ONNX格式的模型; - 把以上导出的ONNX模型再利用瑞芯微的RKNN-Toolkit工具转换为RKNN格式的板端模型; - 在瑞芯微的处理器中,板端模型与其运行时库librknnrt.so、应用程序相互配合完成板端的推理任务。 以上的这个流程,对于在手机的的 Android 系统中使用 TFLite 框架来运行 AI 推理的流程是完全相同的,只不过整个过程稍微简化了一些而已: - 模型转换部分:首先把原始的训练好的 PyTorch 模型(通常是 .pt 或 .pth)使用 TFLite Converter 之类的工具转换为扁平缓冲区格式的 .tflite 文件。与其他板端模型转换工具的工作流程类似,在这个转换过程中,它会进行算子融合(将多个连续的操作合并为一个,以减少内存访问次数和量化(将原始模型文件中的 32 位浮点数权重转换为 16 位浮点数 FP16 或者 8 位整数 INT8,在微弱损失精度的前提下可换取数倍的速度提升同时降低对模型大小和显存的需求)等操作。 - 板端推理执行部分:在 Android App 运行时,内置在Android 系统中的 TFLite 解释器(Interpreter)会负责加载模型、管理内存缓存并调度计算算子。 > 💡 注:在模型转换部分,Pytorch 框架下训练出来的模型,一般需要先转换为 ONNX 模型,再利用类似 onnx2tensorflow 之类的工具转换为 tflite 格式。不过 Goggle 在2024年也出了一个 AI edge torch的工具可以实现直接的转换。 !image.png TFLite模型在Android框架下是如何运行的? 从以上的框架图中可以看到,模型转换为 TFLite 格式后再Android APP中的运行,需要依赖于两个关键的组件:Interpreter 和 Android Neural Networks API(NNPI)。 其中的Interpreter (解释器)是 TFLite 模型在系统中运行的核心控制中枢。它负责加载模型,解析模型的计算图(Graph),并管理内存缓冲区。在整个模型的工作流程中,Interpreter 并不直接做复杂的浮点和算子运算,而是负责将模型中定义的计算任务分发出去。 - 其实这个Interpreter有些类似于瑞芯微的librknnrt.so,负责辅助应用程序加载模型,解析模型的计算图和管理内存,然后在接收到输入数据的时候,对模型每一层的计算任务进行调度管理,分配到合适的处理器(CPU/GPU/NPU)上,并根据具体的计算调度情况不同的处理器上在处理器之间管理好内存的拷贝。所以其任务就是:加载并解析模型,分发计算任务,管理不同计算任务和处理器上的内存。 而 NNAPI 则是 Android 系统提供的一个 C API。它的存在是为了屏蔽底层 NPU、GPU、DSP 硬件的碎片化。Google 在 Android 系统中所提供的 NNAPI 只是一个与 Interpreter 相互配合的API调用接口的框架,而对于瑞芯微、高通、联发科等 SoC 厂商,要让 Android 手机能流畅地使用 TFLite 执行模型的推理任务,他们的核心工作就在其SOC的设计中进行 NNAPI 接口的具体实现。 具体来讲,这些 SOC 厂商需要根据 Android NNAPI 模块所提供的 NeuralNetworks.h 标准,编写对应的 NPU 驱动指令。这些驱动指令的关键任务就是实现模型的算子映射(将 Android 定义的 ANEURALNETWORKS_CONV_2D 等标准算子,翻译成自己的 NPU 能理解和执行的具体指令)和内存管理(实现不同类型和区域的内存共享控制,尽量避免模型在执行推理过程中数据在 CPU 和 NPU 之间来回拷贝)。 总结起来,就形成了这样的调用关系和流程: !5a379fcc-72eb-471f-9001-bf892b8a6120.png - 首先是APP的应用程序逻辑代码调用 Interpreter 加载和解析模型文件; - 在 Interpreter 对输入数据执行推理流程的过程中,会通过 Delegate 机制调用 Android系统中的 NNAPI 框架中所定义的API。NNAPI中定义了不同的标准算子,Interpreter 在推理中针对具体不同的算子调用 NNAPI 中与之对应的API接口。 - NNAPI之下则是不同 SOC 针对算子的具体实现,这部分由各个 SOC 厂家根据自己SOC中 CPU/GPU/NPU 的能力来进行实现。相当于 NNAPI 定义了一层标准的算子接口,屏蔽了不同 SOC 厂商在其硬件上实现这些算子的差异性,这样的话,对于应用程序和Interpreter而言,看到的就只是 NNAPI 所定义的标准接口。 Interpreter 调用 NNAPI框架的Delegate机制 以上所描述的 Interpreter 和 NNAPI 框架的调用流程只是一个简化的解释,实际上Interpreter并不是直接调用NNAPI所定义的API接口。如果 Interpreter 直接以硬编码的方式去调用 NNAPI的话,会产生两个问题: - 某些算子可能不支持 (Fallback):NNAPI 并不一定能够支持 TFLite 模型文件中的所有算子。如果直接调用的话,遇到不支持的算子程序就崩溃了。 - 跨平台的兼容性问题:TFLite 所谋甚大,不仅要运行在 Android 上,还能够支持在 Windows、Linux、iOS 上跑,而这些系统中根本就没有NNAPI(例如上面架构图中的iOS APP部分根本就没有 NNAPI)。 所以 Interpreter 在 Android 中对于 NNAPI 框架的调用,采用了一种 delegate 的机制。 本质上,Delegate 就是 Interpreter 为了实现跨平台兼容性而设计的一个抽象插件接口。 - 对上(与Interpreter之间),它通过TfLiteDelegate 定义了一个标准的 C API,通过这个C API屏蔽了不同硬件平台、SOC、NPU的差异性。Interpreter始终只需要调用这个C API接口来实现对下层不同算力的调用,实现完整的推理过程。 - 而对下(各种平台不同的NPU实现),它作为客户端,负责调用各个不同系统和硬件平台上特有的深度学习算子运算的加速库(如 Android 的 libneuralnetworks.so 或 iOS 的 Metal.framework)。 至此,TFLite 框架得到完整结构及其调用流程就比较清楚了: !%E6%97%A0%E6%A0%87%E9%A2%98-2026-03-26-1350.png 参考资料 - Google Play 服务 C 和 C++ API 中的 LiteRT  |  Google AI Edge  |  Google AI for Developers - TFLite基础知识 - VitoYeah - 博客

手部特征点检测模型MediaPipe Hand Landmarker

MediaPipe Hand Landmarker模型简介 Google 的 MediaPipe Hand Landmarker(基于Mediapipe推理框架的手部特征点检测模型)是目前计算机视觉领域最流行、最高效的实时手部特征点检测和追踪解决方案之一。它能够运行在移动设备(Android/iOS)、网页端甚至算力受限的边缘设备上,以实时帧率输出手部的 21 个 3D 关键点(Landmarks)在图像中的坐标位置。 下图是该手部特征点模型软件包可以检测检测到的手部区域内 21 个手指关节坐标的关键点定位索引。该模型是基于约 3 万张真实图片以及叠加在各种背景上的多个渲染合成手部模型进行训练的。 !image.png Google 针对Hand Landmarker模型提供了iOS/Android/Web/Python这四种平台的官方实现,所以如果是在这四种平台中使用这个模型的话,可以直接参考官方的示例代码方便的调用模型来实现应用需求。 那么如果希望把这个模型运行在其他平台和语言,例如在其他的嵌入式平台上使用C/C++语言来调用呢? 答案是完全可以,只不过稍微复杂一点。Hand landmarker模型的运行需要依赖于Google开源的 MediaPipe 推理框架,而这个推理框架的底层本质,是一个以 C++ 为核心底座,以 TensorFlow Lite为推理后端,通过图计算(Graph)管理数据流的流媒体 ML 框架。 MediaPipe本身是开源的,而且对商业应用极其友好,其代码仓库托管在 GitHub 上:https://github.com/google-ai-edge/mediapipe。开源协议采用 Apache 2.0 License,可以自由使用、修改、分发,甚至将其闭源集成到自己的商业软件中,而无需开源业务代码。 所以要使用C/C++语言运行这个模型,就需要先把 MediaPipe 框架交叉编译到自己的系统中,然后在这个框架的基础上调用模型的推理能力。 模型的工作流程及其输出数据解析 MediaPipe Hands Landmarker模型在运行中之所以能做到极高的性能和检测帧率,其核心在于,它采用了一种级联的两阶段网络架构: 1. 手掌检测模型 (Hand Detector): - 该模型是一个极其轻量级的 SSD(Single Shot Detector)架构模型,其任务是在整张图像中快速找到整个手掌的边界框(Bounding Box),返回一个带方向的矩形框(Oriented Bounding Box)。 - 因为在视频图像中,手部的形变极大(手指可以弯曲、交叉),如果直接检测整只手的话难度很高。而手掌是一个相对刚性的结构,正方形特征明显,极其容易被网络捕获。 2. 手部特征点检测模型 (Hand Landmark Detector): - 一旦第一阶段的 Hand Detector 模型找到了手掌的区域,整个系统会将该区域(第一阶段模型返回的矩形框)裁剪并旋转对齐,然后送入第二阶段的 Landmarker 模型,这个模型专门在裁剪后的小图上精确检测出来 21 个 3D 坐标。 - 因为第二阶段模型只需要在裁剪后的局部区域进行推理检测,这样就极大地减少了计算量,过滤了无关的背景干扰,这是该模型能够跑得又快又准的根本原因。 !mediapipe_architecture.png 在模型的接口设计和执行流程中,MediaPipe Tasks API 包含有中三大运行模式(IMAGE、VIDEO、LIVE_STREAM),分别对应于单图、视频文件中的图像帧、带回调的实时流这三种应用场景。相应的也就对应于以下三个 API 调用接口: - detect(image):单图模式 (IMAGE)。 执行流程没有任何记忆,每一张图片输入后都需要先在整个图片中找手,然后再检测手部的21个特征点,适用于处理磁盘里的静态照片集。 - detect_for_video(image, timestamp_ms):视频模式 (VIDEO)。 带有记忆且在执行过程中同步阻塞,在执行推理的过程中其内部会维护了一个 ROI 缓存。上一帧找到了手部区域后,下一帧可直接找到那个区域跑轻量级的 Landmark 检测关键点。适用于处理本地录制好的视频文件。 - detect_async(image, timestamp_ms):异步直播流模式 (LIVE_STREAM)。 带有记忆且异步非阻塞,为摄像头的实时图像量身定制的。主线程发图像给模型推理框架后立刻返回,不等待结果。推理框架中的底层 C++ 线程池算完后,通过 Callback(回调函数)把结果返回给主线程。因此,使用这种模式需要在初始化 HandLandmarkerOptions 的时候传入一个 result_callback 参数,指明哪个函数负责接收 C++ 传回来的异步结果。 下图为通过以上接口对图像中手部及其关键点检测后的返回结果的数据结构: !MediaPipe_Result_Structure.png 以下是对接口返回结果进行解析和调用的代码示例: 模型运行示例 要在本地PC上使用 Python 运行这个模型,需要安装Google的mediapipe支持包以及下载一个hand_landmarker.task的模型包文件。 hand_landmarker.task模型包的下载地址为:https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task 这个hand_landmarker.task模型包文件以task为后缀名,但是本质上是一个压缩包,其中包含有两个模型文件:hand_detector.tflite(手掌检测模型)和hand_landmarks_detector.tflite(手部特征点检测模型)。只需要把这个模型加一个zip后缀名然后打开即可看到: !f15c88bc-9cbc-4f6a-937d-bb545d301cb6.png 以下提供的代码实现了使用opencv对本地PC摄像头的图像进行手部及其特征点检测并标记的应用案例: 参考文档 - https://ai.google.dev/edge/mediapipe/solutions/vision/hand_landmarker?hl=zh-cn - hand_landmarker.ipynb - Colab - mp.tasks.vision.RunningMode  |  Google AI Edge  |  Google AI for Developers

基于Insightface的人脸识别功能的完整实现流程总结

本文详细解释了计算机视觉领域中的人脸识别功能的工作流程,并通过InsightFace这个完整的人脸检测+识别的解决方案对以上流程的具体实现进行代码级别的演示和解释。 人脸检测+识别功能的完整工作流程 在人脸识别功能在实际产品的工业落地实践中,并非是通过一个独立的模型来解决完整的问题,而是一套级联的流水线:人脸检测(Detection)+对检测到的人脸图像进行几何修正(Alignment)+人脸识别(Recognition)。 而在目前的工业界,尤其是在移动端(手机)和边缘计算设备(如瑞芯微 RK3588)上,人脸识别的标准化解决方案是 InsightFace 开源生态体系。该方案能够支持在算力受限的 NPU 上实现实时且高精度的人脸识别,其通常采用的 Pipeline 是:SCRFD(轻量级人脸检测与关键点定位) + 仿射变换对齐 + MobileFaceNet-ArcFace(轻量级特征提取)。因此整个人脸识别的流程可以分为三个阶段: - Phase 1 (检测器 - SCRFD):输入整张大图,通过 SCRFD 模型检测到人脸区域以及定位其中的 5 个关键点(双眼、鼻尖、两嘴角),该阶段输出人脸区域所在的边界框(Bounding Box)和以上的 5 个面部关键点(Landmarks)。 - Phase 2 (对齐器 - 几何变换):这部分就是进行单纯的图像几何变换,不是深度学习模块(可以直接使用Opencv的warpAffine接口执行这个变换),计算检测关键点与标准模板关键点之间的变换矩阵,主要的目标是将前一阶段检测出来的人脸图像摆正。 - Phase 3 (识别器 - MobileFaceNet):输入摆正后的人脸区域小图,输出该小图对应的 512 维特征,并且与数据库中保存的人脸特征数据进行余弦距离计算,进行人脸特征比对,找到对应的人脸ID。 !Face_Recognition_Pipeline.png 从数据流图的角度上讲: - 检测阶段:输入一张高清分辨率的大图(如1920 x 1080),经过 SCRFD 模型检测,找到 个人脸,输出与之对应的个人脸的坐标框位置()以及每个脸 5 个关键点的 (x, y) 坐标()。 - 对齐阶段:该阶段会将任意大小、角度的人脸,通过矩阵乘法裁剪缩放并旋转至绝对正脸,输出的每张人脸图像的尺寸强制锁定为 112x112,因此输入数据的维度为 。 - 识别阶段;基于对齐阶段输出的人脸图像,使用 MobileFaceNet 对其提取特征向量,每张人脸输出 512 维特征向量,因此输出数据维度为 。接下来就是基于这 512 维特征向量从向量库中通过余弦距离进行比对在,找到对应的人脸ID。 人脸识别和匹配的流程总结 以上从输入图像中检测到人脸区域,对人脸区域图像对齐以及从对齐后的人脸图像提取特征向量的过程,是比较好理解的。那么提取到人脸图像的特征向量后,进一步是如何进行人脸识别的呢? 其实这个人脸识别的过程就是一个从向量数据库中进行图像搜索的过程,这一步与OpenAI的CLIP方案(实例讲解OpenAI的轻量级多模态模型CLIP)基于自然语言进行图像搜索的实现流程如出一辙。 - 首先需要有一个人脸特征向量的注册库,其中预先预先存储好了已知人员的特征向量。这些已知人员的特征向量同样采用以上的检测-对齐-特征向量生成的步骤进行,得到注册人员的512维特征向量后保存在这个注册库中。需要注意,特征向量生成需要使用相同的 MobileFaceNet 模型来生成,也就是说,注册人员的人脸登记流程和后续进行人脸识别流程在人脸图像的特征向量生成应使用同一个 MobileFaceNet 模型,否则特征向量会不匹配。 - 后续的人脸识别的流程实际上就是一个查询向量注册库的流程。从当前摄像头抓拍并经过 MobileFaceNet 提取出人脸的实时 512 维特征,从注册库中使用余弦距离比较和判断来找到人脸在注册库中的ID。 !Vector_DB_Matching_LR.png 如果注册库中的已知人员数量不多的话,可以直接以线性的方式保存所有已知人员的特征信息。这种传统的线性遍历算法复杂度为 ,如果已知人员的数量很多的话,可以采用现代的向量数据库(如 Faiss, Milvus),这些数据库通过引入近似最近邻(ANN)算法,将搜索复杂度降至 。 在进行人脸特征向量入库以及人脸特征向量通过余弦距离来进行匹配时,需要先将向量的模长统一缩放为 1(这一步也就所谓的L2归一化),使其投射到超球面上,这是余弦相似度等价于向量内积的先决条件。这样余弦相似度的计算就简化成了: 基于InsightFace方案实现人脸识别的完整流程记录 以上详细描述了人脸检测、几何校正和缩放、人脸信息入口、人脸识别等环节的执行流程,以下通过 InsightFace 给出的完整方案来实现人脸识别的功能。 首先需要安装 InsightFace 包及其项目演示所需要的各种依赖包: 人脸检测 基于 InsightFace 的人脸检测的流程代码如下所示: FaceAnalysis API首次执行会自动下载buffalo_l模型包,其中包含了多个人脸检测+识别流程的onnx模型文件,其中: - det_10g.onnx:SCRFD 人脸检测模型。 - w600k_r50.onnx:ResNet50人脸识别模型。 - w600k_mbf.onnx:MobileFaceNet识别模型。 - genderage.onnx: 基于人脸图预测性别和年龄。 - 2d106det.onnx: 稠密关键点模块,用于预测面部的 106 个精细轮廓点(常用于美颜、贴纸)。 - 1k3d68.onnx: 3D 姿态模块,用于 3D 人脸重建和深度俯仰角计算。 进行人脸检测时,只需要调用app.get就可以得到这一个图像帧中所包含的所有人脸的坐标及其关键点信息。 几何变换 上一步已经通过SCRFD模型得到了图像中所包含的人脸区域的坐标位置以及关键点信息,下一步就是把其中包含的任意角度(俯仰、偏航、旋转)的人脸强制拉平到统一的正视面部拓扑坐标系中,以确保后续的人脸特性信息提取环节面对的人脸图像都是规范化的前向正视人脸。 这一步大致的数学原理是:在一个112x112分辨率的画布上,基于 InsightFace 提供的标准正脸的 5 个基准点常量,与前一步检测到的人脸的5个关键点的坐标位置,进行两组坐标之间的矩阵变换,只允许旋转、平移和等比例缩放(不允许拉伸变形),变换的目标是使得两组关键点之间的误差最小。 具体的工程实现中,可以直接调用OpenCV的cv2.estimateAffinePartial2D来实现这个几何变换。 !Face_Affine_Transform.png 这一步对应的代码如下: 人脸识别 经过上一步的几何变换处理以后,从原图中检测出来的每一个人脸,就变换成了112x112分辨率的正向面部人脸图像。接下来就是从这个标准化处理过后的人脸图像中提取其特征向量,并查询向量数据库得到其对应的ID。 特征向量提取的任务,在InsightFace中提供了两套模型,分别针对算力需求不同的硬件: - w600k_r50 (ResNet-50 backbone):使用经典的ResNet-50深度残差网络,特征提取能力强,但计算量大,适合服务器/高性能设备。 - w600k_mbf (MobileFaceNet backbone):专为移动端设计的轻量级网络,使用深度可分离卷积(Depthwise Separable Conv),参数量极少,推理速度快,适合嵌入式/移动端设备。 无论始终哪种模型,最终都是从前一阶段的112x112的人脸图像提取出来 512 维的特征向量。 现在有了人脸对应的 512 维特征向量,下一步就是通过计算检测人脸的特征向量与人脸向量库中特征向量的余弦距离来判断两者是否一致。两张人脸的特征向量 和 (且已经过 L2 归一化,即 ),判断它们是否为同一个人的数学依据是: 如果 (通常设为 0.45 或 0.5),则可以判定为同一人。因此计算两个特征向量之间的相似度就非常简单了,直接可以使用 numpy 的点积计算公式并与阈值进行比较就可以了:

百度RT-DETRv2模型的训练以及在RK3588平台上的适配

百度RT-DETRv2模型的官方Github地址为:https://github.com/lyuwenyu/RT-DETR,在该项目中包含了该模型基于Pytorch和百度paddlepaddle的两套独立实现。本文记录了该模型pytorch实现的自定义数据训练、推理测试、ONNX导出以及在RK3588开发板上适配的完整记录。 源码下载和训练数据集的准备 百度RT-DETRv2模型的官方Github地址为:https://github.com/lyuwenyu/RT-DETR,首先从以上地址下载源代码。可以看到该项目中实际上分别包含了RTDETR和RTDETRv2两个模型各自的paddlepaddle和pytorch的两种实现。此处只关注其中rtdetrv2_pytorch这个目录,如红框所标记。 !331b29d9-52fd-470e-ba95-31042d04f330.png 因为RTDETR模型的训练数据默认采用COCO标注格式,所以在训练之前要提前要把自己的数据集标注格式转换为COCO格式。 在Roboflow上找到一个baby五官检测的数据集,以COCO标注格式下载该数据集,后续使用这个数据集对RT-DETRv2模型进行训练:https://universe.roboflow.com/pavel-txwf8/baby_monitoring-x30nd-1hjmq/dataset/1。 下一步开始修改训练和数据集的配置文件。在 RT-DETRv2 的源码中,rtdetrv2_pytorch\configs\rtdetrv2 在目录下包含有多个训练模型和数据集的配置文件: !image.png 这些配置文件的命名遵循了典型的 “模型缩放 + 骨干网络 + 训练策略” 的工程化逻辑。 - 模型 Backbone 核心版本: - rtdetrv2_r18vd / rtdetrv2_r34vd / rtdetrv2_r50vd:其中的r18、r34、r50 指的是Backbone部分使用的 ResNet 骨干网络的层数。其中的vd 表示使用了 ResNet-vd 结构。相比原始 ResNet,它在下采样部分做了优化,这对保持特征信息更有利,且对 NPU 非常友好。 - hgnetv2:百度自研的轻量化骨干网络。 - 模型缩放: - rtdetrv2_s / rtdetrv2_m / rtdetrv2_l:其中的s、m、l类似于 YOLO 的 Small/Medium/Large 缩放。通常涉及到隐藏层维度和编码器层数的设置。 - 训练周期数量: - 120e / 72e / 30e:其中的 e 代表 Epochs(训练轮数)。120e 表示训练120个epoch,一般用于从零开始训练或者长周期微调,精度最高;30e 训练30个epoch,通常用于快速验证或基于预训练模型的微调。 - 数据集标注格式: - coco:表示该配置默认适配 MS-COCO 数据集的标注格式。 此处以其中的rtdetrv2_r18vd_120e_coco.yml为基础进行自定义数据集的训练配置文件的修改。主要的修改内容,就是在以上配置文件的末尾增加自己数据集的相关配置信息如下: RTDETRv2模型的训练以及本地推理测试 训练之前要提前下载好模型的预训练文件,后续基于这个预训练文件进行继续训练。因为我们训练配置文件选择的是rtdetrv2_r18vd_120e_coco ,因此此处下载的预训练文件应该是rtdetrv2_r18vd_120e_coco.pth。下载地址为:https://github.com/lyuwenyu/storage/releases/download/v0.1/rtdetrv2_r18vd_120e_coco.pth。 至此,训练的数据配置文件以及预训练文件都已经准备好,下一步就是调用rtdetrv2_pytorch\tools\train.py脚本来执行训练过程: 训练命令: 训练中断后继续训练: 训练过程中和完成后生成的文件保存在rtdetrv2_r18vd_120e_coco.yml 中设置的output_dir目录中,其中包含的文件: - checkpoint0xxx.pth:训练过程中每一轮的权重文件。 - last.pth: 最后一轮的权重文件。 - best.pth: 训练过程中 mAP 最高的那一轮权重,即训练结果文件。推理测试和后续的模型转换应使用这个模型权重文件。 - log.txt: 该文件中记录了训练全过程的 Loss 和 AP 变化。 训练完成后可以使用以下命令进行推理测试,以确认训练结果的有效性。该项目中包含了一个本地推理脚本文件:rtdetrv2_pytorch\references\deploy\rtdetrv2_torch.py 。 训练过程中存在的问题: 因为训练数据集中有一些图片的分辨率太大了(6000x4000),需要修改train.py的代码,解除对输入图像分辨率大小的限制,否则在训练过程中会报错。在train.py的开头部分包含: 模型的ONNX导出 在百度的RTDETRv2_pytorch的实现中提供了一个onnx模型文件导出的脚本:rtdetrv2_pytorch\tools\export_onnx.py。但是因为我们后续要移植到RK3588平台上,需要对这个文件做一下改动,去掉其中后处理部分的逻辑,这一点对于后续进行RK3588的模型转换和板端推理而言至关重要。 基于rtdetrv2_pytorch\tools\export_onnx.py 对onnx转换脚本进行修改,主要涉及到Model的声明以及onnx导出配置方面的修改 当然因为该模型中包含有Grid Sample算子,与 RT-DETR模型的自定义数据训练以及在RK3588上的适配 一文遇到的问题相同,最新版本的RKNN运行时库(v2.3.2)不支持这个算子,所以按照相同的处理路径,在导出模型之前仍然使用Monkey Patch的办法替换掉Grid Sample算子: 然后执行以上脚本进行ONNX格式的导出(每次导出ONNX以后最好再使用onnxsim进一步简化模型文件中的算子): RK3588板端模型转换与板端运行 后续针对RK3588平台所进行的RKNN模型的转换流程和板端运行的流程与步骤与RT-DETR模型的自定义数据训练以及在RK3588上的适配 没有差异,只不过需要注意,模型转换应设置为FP16量化格式。如果设置为W8A8这样的量化模式,在板端运行的结果很差,应该是量化影响了算法的精度。

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

RT-DETR(RealTime DEtection TRansformer)网络架构与计算流程详细总结

本文详细介绍了Baidu提出的基于Transformer架构的图像目标检测模型RT-DETR的网络结构及其数据在网络中的计算流程。 RT-DETR模型简介 之前在DETR(DEtection TRansformer)网络架构与计算流程详细总结整理了基于Transformer的计算机视觉目标检测模型DETR的网络架构。 RT-DETR(Real-Time DEtection Transformer) 由百度团队开发,旨在解决传统 DETR 模型由于计算量巨大而无法满足实时场景需求的问题,RT-DETR 的出现标志着 Transformer 架构正式进入了工业级实时检测的战场,实际上 RT-DETR 是第一个真正实现实时性能的端到端 Transformer 检测器。 !image.png 与 YOLO 系列通过缩放逻辑提供不同规模的模型版本相同,RT-DETR模型也包含有两个版本:RT-DETR Large(32M参数)和RT-DETR Extra-Large(67M参数),前者性能稍差,但对于算力资源的需求也相对较低,适合在嵌入式边缘端部署。 与Facebook的DETR模型相同,RT-DETR模型的整体架构也由三个部分组成: - 由卷积网络组成的主干网络Backbone。 - 混合编码器。 - 解码器与检测头。 下文对上面三个部分的结构以及数据在其中的计算流程进行详细总结和解释。 Backbone 与DETR架构相同,在RT-DETR模型中的Backbone 是整个模型的特征提取引擎,负责从输入的原始图像中提取多尺度(标准的DETR模型只会提取最小分辨率的尺度特征)的特征图。 RT-DETR 官方实现的Backbone支持两种网络结构选项:HGNetv2 和 ResNet 系列。 - DETR的Backbone网络使用的也是相同的ResNet50和ResNet101。与DETR在主干网络只输出一个 1/32 尺度的特征图不同的是,RT-DETR 的主干网络会输出三个不同比例的特征图(如上图标记的 S3, S4, S5),分别对应原图尺寸的 1/8, 1/16, 1/32,这些特征图包含了从局部纹理到全局语义的不同层级信息。 - HGNetv2 (Higher-order Geometric Network)是百度自研的基于卷积神经网络的Backbone网络结构。它的核心思想是利用深度可分离卷积和特定的结构重参数化来提升精度,同时保持低延迟。 无论主干网络是使用 ResNet 还是 HGNetv2 ,输入的图像数据进入主干网络后,都会经历 5 个阶段(Stage 1-5),而每经过一个 Stage,特征图的长宽减半,通道数增加(这一点跟YOLO模型的主干网络也是相同的)。在输入图像分辨率为640x640的情况下,主干网络的输出为: - Stage 3 (S3): 输出特征图尺寸为 (C_3, 80, 80) —— 尺度为 1/8。 - Stage 4 (S4): 输出特征图尺寸为 (C_4, 40, 40) —— 尺度为 1/16。 - Stage 5 (S5): 输出特征图尺寸为 (C_5, 20, 20) —— 尺度为 1/32. 以上的C_3,C_4,C_5分别为三个阶段的通道数。 位置编码 在特征图的位置编码方面,RT-DETR 与 DETR的计算公式与流程相同,两者默认都使用正余弦位置编码 (Sine-Cosine Positional Encoding)。 对于特征图上的一个点 (x, y),其编码由不同频率的 sin 和 cos 函数生成,每个x和y各生成128维的向量,最后两个向量合并成一个长度为256的向量。具体的计算公式和流程可以参考DETR(DEtection TRansformer)网络架构与计算流程详细总结的相关部分。 但是需要注意的是,对于 RT-DETR Backbone部分输出的特征图而言,只会将 S5 特征图拉平(Flatten)成一个长序列,对这个最小分辨率的特征图上的所有像素点应用位置编码,并送入 Encoder 进行自注意力计算,因为 RT-DETR 认为只在最低分辨率特征图上做全局注意力计算最划算。S3和S4这两个分辨率较大的特征图会直接进入会直接进入混合编码器中的CCFM模块进行融合计算,不会参与全局注意力运算,如下所述。 混合编码器:AIFI_+CCFM 接下来就是 RT-DETR 的混合编码器部分,这是 RT-DETR 模型最核心的部分,这是这部分的设计能够使得模型兼顾 Transformer 的全局建模能力与实时性。 !image.png 如上图所示,混合编码器主要由两个核心模块组成:AIFI 和 CCFM。其中主干网络输出的 S5 特征图会送入AIFI模块进行处理,输出相同维度的 F5 特征图;而 F5 特征图与主干网络输出的S4,S5 这两个特征图一起送入CCFM模块进行更充分的特征融合。 但无论是在AIFI还是CCFM中进行处理之前,首先要对主干网络输出的 S3, S4, S5 特征图进行维度对齐。 维度对齐 为什么要进行维度对齐?这是因为,主干网络(如前述的 ResNet 或 HGNetv2)的不同阶段输出的通道数( C_3,C_4,C_5 )是不一致的: - S3 层输出 512 通道 - S4 层输出 1024 通道 - S5 层输出 2048 通道 在 RT-DETR 的架构中,为了让不同尺度的特征能够进行在后续的处理过程中相加融合以及送入统一维度的解码器,所有尺度的特征图最终都必须统一到相同的通道数(在 RT-DETR 中默认为 256)。 因此,这一步就是要把不同阶段输出的通道数都统一对齐到256。具体的对齐方式很简单,就是对 S3, S4, S5 的每一层分别应用一个 1 x 1 的卷积层,将其通道数从 512/1024/2048 统一压缩到 256 通道即可。经过这个1 x 1卷积后,就得到了三组通道数完全相同(即数据维度)、但空间分辨率不同的特征图。 AIFI AIFI:基于注意力的内尺度特征交互模块(Attention-based Intra-scale Feature Interaction)。该模块专门负责处理来自主干网络最深层、分辨率最低的特征图(也就是主干网络输出的最小分辨率特征图S5)。 > AIFI的核心本质上就是一个单层的 Transformer 编码器结构,这个编码器的输入只有Backbone的 S5 特征图。 为什么不需要对 S3 和 S4 进行自注意力计算? 这是因为,高分辨率特征图(S3, S4)包含的是局部细节,只有最低分辨率的特征图(S5)才包含了丰富的语义信息和全局上下文,因此全局相互作用只需要在低分辨率特征图中进行提取。最重要的是,编码器部分往往是计算瓶颈,如果对所有尺度的特征图进行全量的自注意力计算,复杂度随像素数量呈平方级增长,只在最低分辨率的 S5 特征图上进行自注意力计算,极大地降低了计算量(FLOPs),同时确保了模型能提取到图像中目标之间的全局关系。 经过以上的维度对齐操作后,输入到AIFI模块的 S5 特征图的维度是(256, 20, 20)。接下来的计算流程与标准的Vision Tranformer Encoder的计算流程相同,首先把这个(256, 20, 20) 特征图展平为长度400,维度为256的向量序列,然后给这个向量序列叠加位置编码信息。在特征图的位置编码方面,RT-DETR 与 DETR的计算公式与流程相同,两者默认都使用正余弦位置编码 (Sine-Cosine Positional Encoding)。具体的计算公式和流程可以参考DETR(DEtection TRansformer)网络架构与计算流程详细总结的相关部分。 然后就是标准化的多头注意力计算(默认为8个头)和前馈网络(FFN)的处理,最终经过 AIFI 处理后的 S5 特征序列会重新 Reshape 回 (256, 20, 20) 的张量形状,这就是上面架构图中的 F5 。 CCFM 在 RT-DETR 中,CCFM 舍弃了传统 DETR 计算复杂的自注意力计算,转而使用基于卷积神经网络的结构来处理不同分辨率特征图之间的融合,这一点与YOLO中使用的PANet的概念非常类似。既然是融合不同尺度的特征,自然也就包含两个融合路径: - Top-down (自上而下):将高层语义特征向下传递。 - Bottom-up (自下而上):将底层定位特征向上传递。 CCFM进行两个方向的特征融合操作的核心,在于其 Fusion 模块,如下图所示。 !image.png 假设输入Fusion模块的两个特征图分别为 F_high(来自高层的特征)和 F_low(低层的原始特征),且它们的通道数都已经对齐为 c,并且高层和低层特征图的分辨率已经通过之前的 Upsample或者Downsample 环节变换成了相同的分辨率。那么Fusion模块的工作流程如下: - 特征拼接 (Concatenate):将两个输入通道数为 c 的特征图在通道维度合并,输出一个通道数为 2c 的特征张量。 - 支路分流(Feature Splitting):拼接后的通道数为 2c 的特征图进入两个并行的支路,这两个支路都分别使用了一个 1 x 1 卷积处理,把通道数由 2c 压回到 c。 - 对于下支路而言,还要额外进行连续N个RepBlock的处理。这个RepBlock本质上就是一个3 x 3的卷积,通过连续的卷积层提取局部空间特征。 - 特征融合 (Element-wise Add):将上支路的基础特征图与下支路经过 RepBlock 处理后的增强特征图逐元素进行相加,通道数和分辨率均保持不变。 因此,对于每个Fusion模块而言,其输入的两个特征图的分辨率与通道数,与其输出的特征图的分辨率和通道数相同。 而对于整个CCFM模块而言,其输入可以分为三个部分:AIFI 模块对S5特征图处理后的输出F5,主干网络 S4 和 S3 特征图降维后的特征图。那么CCFM要实现的功能就是充分融合三个不同尺度的特征图中的信息,融合方式与YOLO的PANet大同小异,都是通过UpSample(下图中的黄色方块)和DownSample(下图中的蓝色方块)把不同尺度的特征图分辨率变换到相同的分辨率,然后再Fusion Block中进行融合操作。 !image.png 经过 CCFM 模块的双向充分融合,混合编码器部分最终从三个Fusion Block上输出了三个融合增强后的特征图: - P3 (80 x 80 x 256):包含丰富的细节,利于检测小目标。 - P4 (40 x 40 x 256):中等尺度。 - P5 (20 x 20 x 256):包含深层语义,利于检测大目标。 最后,再将这三个特征图进行展平操作,得到三个维度为256的序列化向量(6400 x 256,1600 x 256,400 x 256),然后把三个序列拼接(这就是CCFM模块的输出C的作用)起来,最终输出一个 8400 x 256 的长序列。 解码器与检测头 RT-DETR 最后部分负责基于前一阶段(即混合编码器部分的输出)输出的 8400 x 256 长序列(针对640x640分辨率的图像输入)进行目标检测的端到端检测,这部分主要由 IoU 感知查询选择模块 (IoU-aware Query Selection) 和 Transformer Decoder 组成。即混合编码器输出的数据并不会直接送入解码器,而是要先使用一个IoU 感知查询选择的模块对其进行筛选,从Encoder的输出里挑选最有希望的query作为Decoder的输入,以此来降低Decoder的运算量。 !image.png IoU 感知查询选择 IOU-aware Query Selection这个部分的整体工作逻辑就是:利用混合编码器的输出结果(8400 x 256),按预测 IoU 质量筛选出 Top-K 个(默认为前300个)最可能是真实目标的候选,作为 Decoder 的 queries。 首先,为了实现 IoU 的感知,RT-DETR 在 混合编码器的输出端设计了一个轻量级的检测头。这个检测头并不是一个深层次的网络,而是一个极其精简的线性预测层集合,以混合编码器输出的 8400x256(可以认为是三张特征图包含有8400个patch) 的长序列做为输入,通过3个独立的分支,分别使用各自的线性预测层,得到8400个Patch对应的分类预测、边界框预测以及IOU预测结果。 三个分支都把输入的256维特征映射为其输出的目标维度: - 分类分支:输出维度 8400 x num_classes,预测每个点属于各个类别的置信度。 - 回归分支:输出维度 8400 x 4,预测每个点相对于其坐标原点的偏移量 (x, y, w, h)。 - IoU 分支: 输出维度 8400 x 1,预测该点对应的预测边界框与潜在检测目标真值框之间的IoU 质量分。 有了以上三个分支的输出数据后,就可以按照以下公式构建出来8400个质量分数: 前者为当前Patch在分类分支上置信度数值最大的那个类别对应的置信度数值,后者则是这个Patch的IoU质量分。通过这个质量分数来进行评估,只有当模型既确定它是这个类别,又确信自己能框准它时,这个点才会被选入后续的 Decoder。 然后对以上的8400个质量分数进行 Top-K 降序排序,然后选出质量分数最高的300个做为该阶段的输出。因此,通过以上流程,就相当于从8400个候选框中,以质量分数作为标准,选出来其中的300个作为后续进一步筛选的基础,而不是把所有的8400个候选框全部作为备选框,这样就大幅度的降低了Deocder阶段的计算量。 最终 IoU-aware Query Selection 模块的输出不是一个张量,而是一组结构化信息。对于以上的 Tok-k 计算流程中选择出来的每个 query,在这一环节都输出两部分信息: - Content Query:每个特征点对应的 256 维特征,这些向量被用作下一阶段Decoder中自注意力模块的输入,维度为 300 x 256。 - Reference Points:由前述回归分支所输出的坐标位置 (x, y, w, h),维度为 300 x 4。 Decoder Decoder部分的结构仍然是标准的:自注意力模块+交叉注意力模块+前馈模块。只不过为了兼顾计算速度,这里的交叉注意力模块并没有采用标准 Transformer 那种全量的 Cross-Attention(即每个 Query 都要和 8400 个点算一遍),而是采用了Deformable Attention(可变形注意力)。 与标准的Transformer Decoder的架构类似,RT-DETR模型的Decoder部分的网络结构包含有6层相同网络模块的堆叠结构,每一层的包含有独立的自注意力计算模块+跨尺度采样交叉注意力模块+前馈网络FFN模块+由线性层构成的分类头和回归头。 1. 自注意力模块 这一步的作用是让 300 个 Content Query 之间充分地进行信息交换。 这个自注意力模块以 300 x 256 维度的Content Query作为初始输入,以Reference Points作为参考生成对应的位置编码,与Content Query相加后,作为自注意力计算模块的输入,自注意力模块的计算流程可以参考一文彻底搞懂Transformer模型的Encoder结构与计算流程。 自注意力模块的输出为经过充分信息交换后的Content Query,维度仍然为 300 x 256。 1. 跨尺度采样交叉注意力模块 交叉注意力模块的输入数据包含三个部分: - 前面的自注意力模块输出的更新后的Content Query,维度为 300 x 256。 - 300个预测框的位置坐标Reference Points,来自于IOU-aware Query Selection模块的输出(第一个decoder层)或者来自于前一个decoder模块回归头的更新坐标(后面的五个decoder层)。 - Encoder Features,混合编码器部分输出的全局特征序列(包含 S3, S4, S5 三个尺度),其维度为 8400 x 256。 需要注意的是,RT-DETR模型Decoder部分中的交叉注意力模块,不同于标准的Decoder中的交叉注意力运算,它所进行的交叉注意力运算不会对Encoder Features所包含的所有的 8400 x 256 维度的数据做矩阵乘法,而是以 300 个预测框的中心作为参考点,在这个参考点的附近有选择地进行采样,在每个参考点附近的采样点对其进行交叉注意力运算(也就是所谓的Deformable Attention,可变形注意力运算),这样就大大减少了交叉注意力的运算复杂度,从而实现了计算效率与精度之间的平衡。 这个可变形交叉注意力模块的设计,是RT-DETR模型能够实现实时性能的关键之一。 无论如何,经过了以上这个可变形交叉注意力模块的计算之后,其输出数据为融合了多尺度局部图像特征后的新的 Content Query 向量,维度仍然是 300 x 256。 1. 前馈网络FFN模块 该模块的目的是增强特征的非线性表达能力,类似于 CNN 中的全连接层。 与标准的Encoder以及Decoder模块相同,FFN 模块的输入数据为上一步交叉注意力计算输出的 Content Queries (维度为[300, 256])。 FFN模块的计算流程是标准的 Linear -> ReLU -> Linear 结构,通常中间维度会扩大(如 1024 或 2048)。而输出数据则是深度映射后的 Content Queries,维度仍然保持为 [300, 256]。 1. 分类头/回归头 输入数据均为前一层前馈网络模块输出的Content Query,维度为 300 x 256。分布通过独立的线性层映射分类预测值和坐标偏移量。 - 分类头:经过线性层映射到目标类别数量 num_classes。输出数据为分类概率 Pred Logits,维度为 [300, num_classes]。 - 回归头:同样通过线性层预测一个预测框坐标的偏移量 ,然后将这个坐标偏移量更新于本层的输入 Reference Points上,下一个模块使用的 Reference Points就是经过偏移量更新后的值。 模型输出 RT-DETR模型最终的输出,就是Decoder部分最后一个Decoder block的分类头和回归头,这两个独立的检测头分布输出300个检测框的分类判断值和检测框的坐标值。 - 分类头输出分类预测分数 (Pred Logits): 维度为 [300, num_classes]。每一个 Query 都会对所有类别给出一个得分。 - 回归头输出预测坐标 (Pred Boxes): 维度为 [300, 4]。这是经过Decoder部分 6 层Decoder Block的不断修正后的最终坐标 (x, y, w, h)。 接下来如何从这300个检测框得到最终呈现给检测结果呢? 首先,对以上分类头输出的 Pred Logits 进行 Sigmoid 处理,将其转化为 0 到 1 之间的概率值。然后以每个 Query 在所有类别中的最大得分作为该框的置信度。此时,就拥有了 300 个带类标和置信度的预测框。 然后进行阈值过滤 。通常会设置一个置信度阈值 (例如 0.5)。系统会直接丢弃所有得分低于该阈值的框,并输出置信度大于该阈值的检测框。 - 这一部分的关键点在于,由于 Transformer 的自注意力机制已经在内部完成了去重(抑制了指向同一目标的重复 Query),所以理论上剩下的检测框通常就是互不重叠的高质量检测结果。 参考资料 - RT-DETR: Paper Explanation and Inference - lyuwenyu/RT-DETR: [CVPR 2024] Official RT-DETR (RTDETR paddle pytorch), Real-Time DEtection TRansformer, DETRs Beat YOLOs on Real-time Object Detection. 🔥 🔥 🔥

如何理解ONNX模型导出的Opset版本?

在把PC或服务器上基于Pytorch训练好的模型导出为ONNX格式的时候,需要在导出的脚本中设置opset的版本,不同的opset版本设置会对后续的NPU适配过程造成很大的影响,那么这个opset的版本就是是个什么概念?不同的opset版本究竟有哪些差异,以及这些差异对后续的NPU适配过程如何造成影响?本文尝试回答以上问题。 从总的概念上来讲,在嵌入式AI模型的部署流程中,Opset(即Operator Set,算子集) 是 ONNX(Open Neural Network Exchange)所定义的算子集合的版本标准。在将PC上训练好的 PyTorch(.pt/.pth)模型导出为 ONNX 格式时,设置正确的 opset 版本至关重要,因为它直接决定了导出的模型能否在后续的NPU板端模型转换工具成功转换,并高效运行在 NPU 上 。 !image.png 要理解不同的Opset对后续的NPU模型适配和部署流程会造成什么样的影响,首先要深入的理解究竟什么是算子和什么是算子集。 什么是算子(Operator/Op)和算子集(Operator Set)? 在 AI 模型的语境之中,算子(Operator)是模型里的最小执行单元。一个典型的深度学习模型(例如 YOLO 模型),其本质就是一个巨大的有向无环计算图(DAG)。这个图由无数个节点组成,而每个节点就是一个算子。 这就有点类似于我们在写软件程序的时候,如果要实现某项功能,通常会把这个功能封装为一个函数来进行调用。那么_算子就类似于一个可被应用程序调用的标准库函数,对这个算子(函数)的调用,就相当于是在NPU上执行一次最小粒度的标准化操作。_ 常见的算子包括: - Conv (卷积):提取特征,相当于嵌入式里的滤波算法。 - ReLU (激活函数):实际上就是一个简单的 if(x

PANNs音频分类模型在RK3588平台上的适配全记录

之前在PANNs音频分类模型的预研信息汇总 一文中,对PANNs这个音频分类模型的信息进行了汇总,在PC端做了一些简单的测试。 在基于ECS50数据集对PANNs语音分类模型进行的训练流程 一文中,记录了基于PANNs模型的CNN14架构对ECS50数据集进行训练的完整流程。 本文进一步尝试把前一阶段基于ECS50数据集训练好的模型移植到瑞芯微RK3588平台上,并且使用瑞芯微的rknn-toolkit-lite2工具使用python脚本实现板端推理。 模型导出为onnx文件 经过基于ECS50数据集对PANNs语音分类模型进行的训练流程 一文所描述的模型训练流程,经过训练后就得到了使用ECS50数据集训练后的模型文件 cnn14_esc50_best.pth 。 下一步调用以下脚本把这个模型文件导出为onnx格式,导出文件为cnn14_esc50_rk3588.onnx。 > 💡 > PANNs这个模型实际上是把音频数据先转换为频谱图,然后在基于标注的CNN网络做分类的训练。那么对于这种模型,前面的音频数据转频谱图部分的运算是不适合在NPU上运算的。所以合理的做法是:音频数据采集完成后,在CPU上完成音频数据转频谱图的计算,然后再把频谱图在NPU上进行CNN分类识别。在这种情况下,要适配到NPU上的模型应该就只包含CNN网络部分,不包含前面的频谱图计算部分。每次送入CNN网络部分进行识别的频谱图应该是1s的音频数据(对应101个音频帧)所生成的频谱图,频谱图的mel刻度范围为64,因此频谱图的分辨率为101x64。 模型转换校准文件的准备 在执行模型转换的过程中,需要从之前的训练集(也就是ECS50数据集)中抽取出来20个音频文件,作为模型转换过程中的校准数据集。 从ECS50数据集中随机抽取其中的20个音频文件,放入dataset_ecs50子目录中,然后使用一个文本文件dataset_ecs50.txt记录所有校准音频文件的路径名称: 使用以下脚本把其中的校准音频文件转换为npy格式保存在dataset_ecs50_npy目录中,并自动修改dataset_ecs50.txt为生成的npy文件的路径名称:因为后续模型转换过程中只能使用npy格式的音频文件进行校准: 此时模型转换的校准文件就准备好了: - dataset_ecs50_npy :其中包含有所有20个音频校准文件的npy格式文件。 - dataset_ecs50.txt :其中包含有所有npy校准文件的路径名称。 模型转换 执行以下代码把PC端导出的ONNX模型文件转换为可以在RK3588上运行的rknn模型,转换完成后得到的端侧模型文件cnn14_esc50.rknn。 板端rknn-toolkit-lite2环境的安装 我的RK3588上的系统是debian,其中已经包含了基本的python 3.12环境。要能够使用rknn-toolkit-lite2在板端直接使用python执行推理代码,还需要安装板端的rknn-toolkit-lite2环境。 先查询RK3588板端的Python版本,然后根据版本信息在rknn-toolkit2-master/rknn-toolkit-lite2/packages 路径中找到与之对应的rknn-toolkit-lite2板端安装包。因为我的板端Python环境是3.12.4,所以此处应该选择的安装包是rknn_toolkit_lite2-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl 。 使用以下命令安装rknn-toolkit-lite2的板端环境支持包以及推理代码所需要的python包numpy和librosa。 为什么要增加--break-system-packages选项? 因为板子上运行的Debian系统引入了PEP 668规范,系统为了防止直接使用 pip 安装的包与系统自带的 apt 包管理工具发生冲突导致系统崩溃,锁定了全局 Python 环境,否则会报以下错误,系统推荐在独立的虚拟环境中安装python包。 但是因为这里只是做一个测试,所以就不用费劲去折腾虚拟环境了,直接使用--break-system-packages选项忽略以上错误即可。 板端推理 至此板端的rknn-toolkit-lite2环境以及板端的模型(cnn14_esc50.rknn)都已经准备好了,接下来就是写一段基于rknn-toolkit-lite2的板端推理代码来进行测试: > 💡 此处需要注意,因为ESC50数据集中的wav文件的长度都是5s,而默认情况下模型的输入为1s音频文件的频谱图,所以在板端对ESC50数据集中的wav文件进行推理的时候,就需要使用滑动窗口(滑动重叠的时间长度为0.5s)的方式把5s的wav文件分割为多段1s的音频,然后针对每个1s音频计算频谱图、推理得到结果,再把所有的结果合并在一起得到最后的识别结果。 以上代码对1-977-A-39.wav 这个音频文件的推理结果如下: 检测结果的类别ID是39,查询ECS50的ID对应表,39对应于Clock Tick,与音频文件中所包含的声音相吻合。 !image.png

基于ECS50数据集对PANNs语音分类模型进行的训练流程

本文使用ECS50数据集对语音检测和分类模型PANNs进行自定义训练的完整流程进行了记录。 ECS50数据集简介 在PANNs音频分类模型预研信息总结一文中,详细介绍了在语音分类应用中非常流行的模型PANNs,该模型使用Google的AudioSet数据集上进行了充分的训练,从而在不同种类的语音检测、辨识和分类应用上取得了非常好的效果。本文基于该模型(CNN14结构)的预训练文件Cnn14_mAP=0.431.pth,使用ECS-50语音数据集对预训练模型进行微调,使之适应ECS-50数据集的检测和分类需求。 ESC-50数据集包含有2000个环境录音文件,该数据集适用于环境声音分类的基准方法。数据集中的每个音频文件由5秒长的记录组成,可分为50个语义类,每个类型有40个示例,松散地分为5个主要类别: !image.png ECS-50数据集的下载地址:karolpiczak/ESC-50: ESC-50: Dataset for Environmental Sound Classification > 需要注意的是,ESC-50 数据集中的音频文件原始采样率是 44.1kHz,而 PANNs Cnn14 的预训练权重是基于 32kHz 的。 因此在读取这些音频文件送入模型训练时,必须强制重采样,从44.1Khz转换为32KHz。 ECS-50数据集的训练流程记录 客观的讲,PANNs模型对于自定义数据集的训练支持并不是很友好,所以需要自行设计和处理整个训练过程(数据集准备,模型检测头修改,训练流程)的各个阶段的处理流程。 训练数据集的准备 首先从以上ECS-50数据集的下载地址下载到这个数据集的压缩包,整个数据集有600多MB的样子。 然后基于Pytorch的Dataset类派生专门的ECS50数据集的Dataset类,在其中实现len和getitem接口。此处值得一提的是,因为PANNs模型是基于32KHz的音频文件进行训练的,所以此处在加载训练数据之前,需要把ECS50数据集的44.1KHz的音频文件转换为32KHz。 此外为了进一步规范化训练数据集中的音频数据长度,统一把训练数据集中的音频长度固定为5s,过长的音频文件直接截断,过短的因为文件则在后面补零。 CNN14检测头的修改 PANNs模型是在Google的AudioSet数据集的基础上进行设计和训练的,预训练文件Cnn14_mAP=0.431.pth对应的模型检测头支持527种音频分类。而ECS50数据集支持50种音频分类,因此要在训练之前对检测头部分进行修改,以适应ECS50数据集的要求。 整个过程分为以下步骤: - 首先基于PANNs模型源代码实例化一个Cnn14结构的模型,并加载预训练模型文件。 - 然后修改以上Cnn14模型的检测头部分,最后一层使用一个50个神经元的线性层替代之前默认的527神经元的线性层。 - 最后对新修改的检测头的线性层的权重参数进行初始化。 至此,预训练数据集和待训练的模型结构就准备好了。下一步就开始进行训练。 模型的训练 模型的训练流程的执行与普通的卷积神经网络模型的训练流程没有太大区别,需要注意的是:因为是分类模型,所以训练过程使用标准的交叉熵损失函数,优化器使用Adam。 训练过程中的log信息如下: 可以看到,经过30个epoch的训练以后,在验证集上的识别准确率达到了93.5%。训练完成后会自动把训练好的模型参数保存为cnn14_esc50_best.pth文件。 推理 最后基于以下代码对训练出来的模型进行推理测试: 参考资料 - karolpiczak/ESC-50: ESC-50: Dataset for Environmental Sound Classification

PANNs音频分类模型的预研信息汇总

本文整理了经典的音频分类模型PANNs的基本情况,模型架构及其预训练参数文件的分类,开发环境的搭建和基本的推理测试,以及对该模型主推的CNN14模型的网络架构进行了说明。 PANNs模型简介 PANNs (Large-scale Pre-trained Audio Neural Networks),大规模预训练音频神经网络。该模型由Qiuqiang Kong等学者在 2019-2020 年间提出,其核心意义在于:它打破了音频领域“小样本、难训练”的僵局,为各种音频任务提供了一套强大的通用特征提取器。从整体工作流程上讲,其工作原理就相当于是先对音频文件生成与之对应的梅尔刻度频谱图,然后再使用标准的卷积神经网络基于这个频谱图进行分类操作。该模型在Github上的仓库地址:qiuqiangkong/audioset_tagging_cnn PANNs模型之所以强大,是因为它在Google的AudioSet数据集上进行了极其充分的训练,学会了分辨世界上几乎所有常见声音的频谱特征。该模型的预训练参数文件下载地址:PANNs: Large-Scale Pretrained Audio Neural Networks for Audio Pattern Recognition (Pretrained Models)。从以上的预训练参数下载文件列表中可以看到,该模型提供了多个不同网络模型和参数的预训练参数文件: !image.png 这些预训练参数文件的命令格式大致是:模型架构_采样率_嵌入向量长度_性能指标.pth。 - 模型架构:支持的模型架构版本包括CNN14(推荐模型架构),CNN6/CNN10(轻量级版本),Resnet/MobileNet,Wavegram-Logmel-Cnn14(性能强大,但计算量大),DecisionLevelMax(适合用于Sound Event Detection推理模式)。 - 采样率:PANNs的官方实现中预置了对 8kHz、16kHz 和 32kHz 这三种采样率的支持,默认为32KHz采样率。 - 嵌入向量长度:进入检测头部分的嵌入向量的维度,默认为2048,官方的权重文件库中还提供了对 32、128、512这几种维度的支持。 - 性能指标:在Google的AudioSet数据验证集上测试到的mAP指标。 环境安装与基本推理测试 环境与依赖安装: 从源代码目录的scripts/0_inference.sh脚本中可以看到,该模型的推理模式可以分为两种模式:Audio Tagging和Sound Event Detection。 - Audio Tagging (音频打标): - 解决的问题是:这段录音的内容是什么。 - 模型输出结果为整个音频片段的总体分类概率,对应于Google音频分类数据集AudioSet中的527个音频分类。 - 其技术判断逻辑是对音频文件在整个时间维度上进行全局池化,只给出一个总的分类概率判断。 - Sound Event Detection (SED, 声学事件检测): - 解决的问题是:什么时候发生了什么音频事件。 - 模型的输出结果为一条时间轴,在这个时间轴上标注出异常声音出现的起始和结束时间。 - 其技术实现逻辑是在Audio Tagging的基础上,保留了时间分辨率,然后像扫描仪一样逐帧判断整个时间周期上的音频事件。 audio_tagging推理模式测试: CNN14模型结构 如上所述,PANNs 并不是单一的一个模型,而是一系列不同网络架构的全家桶。可以在其模型预训练权重文件下载地址 Zenodo 页面看到的各种模型的预训练权重文件,对应了不同的网络深度和结构: - CNN 系列 (CNN6, CNN10, CNN14):这是PANNs最常用的系列。其中的 CNN14 是性能最强的旗舰型号,也是该项目主推的网络结构。 - ResNet 系列 (ResNet22, ResNet38, ResNet54):借鉴了残差网络的结构设计,适合更深层次的特征提取。 - 轻量化系列 (MobileNetV1, MobileNetV2):专为手机或嵌入式设备设计,牺牲少量精度换取极快的速度。 - 混合系列 (Wavegram-Logmel-CNN):该网络架构不仅关注频谱图,还直接参考原始波形,捕捉更精细的时间信息。 以下是CNN系列的三种网络架构组成: !image.png 以下是CNN系列不同架构配合不同的采样率、梅尔频谱刻度数量、数据增强策略的情况下的验证指标: !image.png 对于CNN14的网络结构而言 - 首先是基于torchlibrosa库中的Spectrogram和LogmelFilterBank生成与音频文件相对应的梅尔刻度频谱图。 - 接着是连续6个卷积块: - 前5个卷积块的组成都是:3x3卷积+BN层+Relu激活函数+3x3卷积+BN层+Relu激活函数+2x2池化层。 - 最后一个卷积块的组成是:3x3卷积+BN层+Relu激活函数+3x3卷积+BN层+Relu激活函数+1x1池化层。 - 第一个卷积块输出64通道,第二个卷积块输出128通道,以此类推,最后一个卷积块输出2048通道。 - 最后一个卷积块的输出维度数据为[batch_size, channels, time_steps, freq_bins]。默认情况下,channels为2048;time-steps为时间维度的步长,经过前面的5轮2x2池化以后就是音频采样帧数的1/32;freq_bins为频率特征,经过前面的5轮2x2池化后已经很小,甚至为1。此时,每个时间段(连续32个音频采样帧)就对应一个 2048 维的特征向量,它就代表了该时刻声音的指纹信息。 - 然后是在时间维度上,对以上连续卷积块提取的特征向量信息同时进行全局的最大池化和平均池化操作,把两者加起来,得到一个2048维的特征向量。 - 最后是一个从2048到num_classes的全连接层,用于基于2048维特征向量执行最后的分类操作,通过sigmoid给出分类判断的概率。 参考资料 - 【音频分类与检测】PANNs:用于音频模式识别的大规模预训练音频神经网络-CSDN博客

详细解释空洞卷积和可变形卷积的计算流程

有关卷积神经网络中的标准卷积核的计算流程,在详细解释卷积神经网络中的卷积与池化计算一文中已有非常详细的总结,本文主要整理了两种相对比较特殊的卷积操作:空洞卷积和可变形卷积。 空洞卷积 简单来说,空洞卷积相比于标注的卷积计算,可以在不增加参数量的情况下,强行拉大卷积计算的感受野(Receptive Field)。 在标准卷积的计算中,卷积核的元素是紧挨着的: !image.png 而在进行空洞卷积的计算时,会在卷积核的各个权重元素之间插入了空洞(即填充 0,但实际计算时直接跳过)。如以下的动图所示,3x3的空洞卷积,在计算的过程中,每个元素在计算时会都被插入空洞元素,这样的话相当于之前3x3卷积的感受野变成了5x5,但是参数量和计算量仍然保持为3x3。 !%E7%A9%BA%E6%B4%9E%E5%8D%B7%E7%A7%AF%E4%B8%8E%E5%8F%AF%E5%8F%98%E5%BD%A2%E5%8D%B7%E7%A7%AF-%E7%A9%BA%E6%B4%9E%E5%8D%B7%E7%A7%AF%E7%9A%84%E8%AE%A1%E7%AE%97.gif 上图中进行空洞卷积计算的核心参数是膨胀率(Dilation Rate),也就是说在进行卷积计算的过程中,在两个连续的权重元素进行计算的过程中要插入的空洞个数,也就是元素之间的间距: - d=1:标准卷积。 - d=2:每两个权重元素之间跳过一个像素,依次类推。 在上图所展示的空洞卷积的计算流程中,采样点之间进行计算时会跳过一定数量的像素,即膨胀率参数。 假设输入特征图为 ,二维卷积核为 ,大小为。对于输出特征图上位置为的值 ,其进行空洞卷积的计算公式为: - 其中的 和 就是空洞的体现,它改变了读取输入 时的索引步长。当 时以上公式也就是普通的卷积计算公式。 - 需要注意的是,卷积核 的参数量依然是 ,并没有因为空洞而增加额外的权重参数。 在 PyTorch 中,nn.Conv2d 内部已经集成了 dilation 参数用于指定卷积计算过程中的膨胀率。 可变形卷积Deformable Convolution 在传统的卷积(包括空洞卷积)计算的过程中,无论物体的形状轮廓是什么,以及无论物体如何旋转、缩放或变形,执行卷积计算的卷积核的采样形状永远是方形(如3x3,5x5等)。 而可变形卷积的理论则认为:卷积核应该具备形状感知和自适应的能力,也就是说,卷积核的采样点不应该被限制在固定的方形里面,而应该根据物体的实际轮廓和大小去偏移,通过让网络自己学习这个偏移量(Offset),来实现对感受野的精准定制。 可变形卷积的计算公式如下: 下图是可变形卷积的计算流程示意图: !image.png 从总的流程上讲,可变形卷积的计算分为两个阶段:第一个阶段负责偏移量的生成,第二个阶段基于偏移量来进行可变形卷积的计算。 以下基于一个实际的例子来解释上面两个阶段的的计算流程。假设输入的特征图是以下5x5分辨率的特征图,要执行的是3x3的可变形卷积计算。计算过程中关注的是下面输入特征图的中心点(2,2)坐标位置,即值为13的这个坐标点: !image.png 步骤1:偏移量的生成 阶段1的计算比较简单,实际上就是一个标准的3x3卷积,在这种情况下,输出的通道数必须因为18,因为这个阶段1的目的是要为输入特征图的每个像素点生成两个偏移量(dx,dy),3x3卷积的情况下对应的输出通道数就应该是3x3x2=18。 - 注意,在这个偏移量的计算过程中,涉及到一个独立的offset卷积核。 因此偏移量的计算过程,最终的输出结果为与输入特征图分辨率相同,通道数为18的偏移量张量。对于位置(2,2)这个特征点而言,这个偏移量张量包含了18个偏移量: !image.png 这样,针对中心点(2,2)这个位置的卷积计算,其周围9个像素(包含它自己)的位置就对应了9组(dx,dy)偏移量。 步骤2:可变形采样 下面以(2,2)位置的3x3区域的左下角点(-1,-1)来解释可变形采样的计算流程。按照以上可变形卷积的计算公式: 左下角点对应的,那么上面的公式实际上就是。这是一个非整数的坐标(如下图中的蓝点)。从输入的特征图上,找到其周围的四个像素点(如下图中的红框): !image.png 接下来就是进行双线性插值计算,距离越近,权重越高,插值权重为 (即去掉整数部分,仅保留小数)。双线性插值的计算如下: 这个 20.476 只是左下角点(-1,-1)的可变形卷积计算结果。接下来按照步骤2完全相同的流程,对所有的9个位置全部进行以上的计算,得到9个变形采样值: !image.png 步骤3:主卷积核加权求和 最后针对(2,2)这个位置最终的变形卷积的计算结果,还要对以上9个值,与主卷积核的权重参数逐元素相乘,得到最终的结果: - 是以上步骤2得到的9个变形采样值, 是主卷积核的权重参数。 以上的步骤1到步骤3只算出来了输入特征图中心位置(2,2)所对应的可变形卷积的计算结果,对于其他位置的所有像素同样要按照步骤1到步骤3的完整流程计算后,才能得到整张输入特征图的可变形卷积计算结果。由此可见,可变形卷积的计算结果,相比普通的卷积计算要多了很多。 在 PyTorch 中并没有提供原生的可变形卷积的实现,但是在 torchvision.ops 中提供了DeformConv2d可以用于实现可变形卷积的计算。 参考资料 - Deformable Convolution - Dilated Convolution

QWen3-VL-4B多模态大模型在RK3588上的适配流程记录

本文详细记录了在瑞芯微RK3588上进行Qwen3-VL-4B-Instruct多模态大模型的完整适配记录,以及在适配过程中容易出现问题的重点事项。 QWen3-VL-4B模型的下载与测试 阿里在 2025 年 9 月推出了Qwen3-VL 系列视觉语言模型。相较于其前代产品(如 Qwen2-VL),Qwen3-VL 的发布标志着多模态大模型从简单的视觉感知正式跨越到了深层的逻辑推理与自主 Agent 交互的新阶段。Qwen3-VL 延续了阿里的开源精神,推出了涵盖从端侧到云端的多种规模模型。 !image.png 要进行Qwen3-VL-4B-Instruct这个模型在瑞芯微平台上的适配,首先需要下载模型文件,并在PC端进行基本的推理测试,验证下载到的模型文件的正确性,这部分工作在详解QWen3-VL模型的测试以及vLLM生产环境部署流程一文中已有详细介绍。 Qwen3-VL-4B-Instruct模型在HuggingFace中的链接路径如下,按照上文使用transformers库从huggingface上下载并执行本地的推理验证: !image.png > 注:下文中我已经下载好的本地模型文件的路径为/mnt/d/HuggingFace/hub/models--Qwen--Qwen3-VL-4B-Instruct/snapshots/ebb281ec70b05090aa6165b016eac8ec08e71b17/ 环境的搭建 多模态大模型在瑞芯微平台NPU上的适配,需要同时用到瑞芯微的rknn-llm和rknn-toolkit2工具包。截至2026年1月份,这两个工具包在github上的最新版本是: - rknn-llm:1.2.3 - rknn-toolkit2:2.3.2 以下工作就基于这两个版本展开。比较奇葩的是,这两个工具包的最新版本,对于python环境的版本要求是相互冲突的。所以最好基于上面的两个工具包分别搭建各自的开发环境rknn和rkllm,在后续的不同步骤切换到不同的环境中执行。 rknn-toolkit2的环境搭建可以参考瑞芯微RK3588 NPU开发环境搭建笔记,rkllm的环境搭建流程可以参考轻量级LLM模型QWen2.5-0.5B在RK3588上的适配流程记录。 _整体来讲,QWen3-VL-4B-Instruct这个模型在RK3588开发板上的适配,主要是基于rknn-llm这个仓库中的__rknn-llm-main/examples/multimodal_model_demo__目录中所包含的文件展开。_ - 该目录下有一个README.md可以作为参考,但是其中缺少一些细节。 模型转换 针对QWen3-VL-4B-Instruct这类多模态模型的转换,实际上分为两个步骤,分别对应rknn模型和rkllm模型的转换。最终上板运行的demo,既需要同时加载独立的rknn模型文件和rkllm模型文件,也需要依赖于各自的运行时库:librkllmrt.so和librknnrt.so。 rknn模型的转换 这部分转换操作在rknn环境(即安装rknn-toolkit2工具包的环境中)执行。 这部分分为两个子步骤:模型的Vision部分转换为onnx文件,在从onnx格式的模型文件转换为上板运行的rknn模型文件。 在rknn-llm-main/examples/multimodal_model_demo目录下的export子目录中执行export_vision.py脚本转换模型的Vision部分到onnx文件中: 以上执行完成后会在当前export目录下生成一个onnx子目录,其中包含有转换好的模型Vision部分的onnx文件: 接下来,继续使用相同目录下的export_vision_rknn.py脚本把这个onnx文件转换为板端可运行的rknn模型文件: 以上执行完成后会在当前export目录下生成一个rknn子目录,其中包含有转换好的模型Vision部分的板端运行的rknn模型文件: 至此模型Vision部分的rknn上板运行的模型文件qwen3-vl_vision_rk3588.rknn就已经准备好了。 rkllm模型的转换 这部分转换操作在rkllm环境(即安装rknn-llm工具包的环境中)执行。 这部分同样可以分为两个子步骤:校准数据的准备,rkllm板端模型的转换。 校准数据准备的这部分工作主要在data子目录中进行,该目录下包含一个make_input_embeds_for_quantize.py脚本用于准备校准数据,但是这个脚本是针对Qwen2-VL的,直接在Qwen3-VL模型上使用会报错,所以我基于上面的脚本文件修改了一个make_input_embeds_for_quantize_qwen3-vl.py专门用于生成Qwen3-VL模型的校准数据,然后在export下执行: 以上执行完成后会在data/inputs.json和data/inputs_embeds/生成校准数据,后续rkllm板端模型的转换过程要用到这个校准数据。 接着就是模型转换的最后一步,也是最耗时的一步(主要原因是我的显卡型号比较新,与rkllm环境中的torch和cuda版本有冲突,所以只能使用CPU来执行模型转换):rkllm板端模型文件的转换。主要用到export下的export_rkllm.py脚本: 注意,以上脚本执行的过程中对内存需求巨大,如果在WSL中执行的话会在运行在出现Killed的问题,原因就是内存OOM了,此时需要修改WSL的内存和虚拟内存的配置:在自己的用户目录C:\Users[用户名]下创建一个.wslconfig文件,写入以下内容然后重启WSL,然后重新执行以上转换命令即可: 以上执行完成后会在当前目录下生成rkllm的板端执行文件_w8a8_rk3588.rkllm: 至此,板端推理demo运行所需要的两个板端模型文件qwen3-vl_vision_rk3588.rknn和_w8a8_rk3588.rkllm就准备好了。 板端推理应用程序编译 板端通用的推理应用程序代码在deploy目录下,需要注意RKLLM v1.2.3版本要求交叉编译的gcc版本在10.2以上,需要先下载工具链gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu: !image.png 在deploy目录下,修改其中的build-linux.sh文件中的交叉编译工具链路径GCC_COMPILER,然后执行build-linux.sh脚本编译板端执行文件即可。这个编译过程完成后会把所有上板运行的文件(除以上两个板端模型文件以外)放入一个install目录下。 上板运行测试 上板运行测试的过程,把以上的install目录、两个板端模型文件qwen3-vl_vision_rk3588.rknn和_w8a8_rk3588.rkllm上传到板子的models目录下上。然后执行: 最终demo在rk3588上运行的效果如下图所示: !image.png 参考资料 - airockchip/rknn-llm - airockchip/rknn-toolkit2 - 【硬核部署】Qwen3-VL 在 RK3588 上的部署-CSDN博客 --- 尝试从底层原理的角度去理解和解释技术问题:音视频/摄像头/智能家居/蓝牙/WiFi/无线通信/AI。 敬请关注微信公众号:Pavel Han。

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

轻量级LLM模型QWen2.5-0.5B在RK3588上的适配流程记录

本文详细学习和总结基于瑞芯微的RKLLM工具及其官方的相关指导文档,把轻量级大语言模型Qwen2.5-0.5B适配到RK3588上的完整过程及其注意事项记录。 总体而言,在瑞芯微NPU上部署 LLM 和部署 YOLO 的核心逻辑与流程是一致的,都是:PC端转换模型 -> 推送模型和库到板端 -> 调用 API 运行。 但是,对于大语言模型的适配而言,不再使用 RKNN-Toolkit2 工具链,而是使用专门为大语言模型优化的 RKLLM-Toolkit 工具链。 - RKNN-Toolkit2: 处理卷积神经网络(CNN)和简单的 Transformer(Vision Transformer)。 - RKLLM-Toolkit: 专门处理大语言模型,支持权重量化(W4A16/W8A8)和 KV Cache 优化。 下图是瑞芯微RKLLM Toolkit及其软硬应用生态的框架图,与RKNN类似: !image.png - RKLLM-Toolkit:用于在PC上进行模型转换和量化。 - RKLLM Runtime:为瑞芯微NPU平台提供C/C++编程接口,帮助用户在NPU上部署RKLLM模型,加速LLM应用的运行。 - RKNPU内核驱动:负责与NPU硬件交互,可以在Rockchip的内核代码中找到。 环境搭建 从rkllm的Github仓库(airockchip/rknn-llm)上下载rkllm代码。 PC端环境 创建一个独立的rkllm环境,并进入rknn-llm仓库代码中的rkllm-toolkit/packages目录下,安装rkllm-toolkit及其依赖包: 板端环境 对于LLM大模型在瑞芯微NPU上的运行,在板端主要需要NPU驱动以及librkllmrt.so链接库(在rkllm-runtime/Linux/librkllm_api/aarch64下)的支持。 NPU驱动要求在v0.9.8版本以上,可以通过以下命令检查自己板端的驱动是否符合要求,否则就需要重新编译内核: 下载模型文件并在PC上推理测试 使用transformers从Huggingface上下载Qwen2.5-0.5B模型的模型文件,并在PC本机上执行推理测试,验证下载文件的正确性: HuggingFace下载速度慢的问题可以通过设置国内镜像来解决,在执行以上下载脚本之前设置环境变量: 下载后的模型文件列表如下图所示: !image.png 模型转换 基于环境搭建部分安装的rkllm-toolkit包,调用以下代码完成模型的转换: > 注意:在量化类型的支持上,RK3588不支持w4a16, 可支持w8a8, w8a8_g128, w8a8_g256, w8a8_g512这四种。 以上模型转换结束后会在当前目录下生成一个qwen2_5_0_5b_w8a8.rkllm文件,大小800M的样子。 板端编译与推理 以上的板端模型转换完成以后,接下来就是准备板端的推理代码: 对于板端推理代码的交叉编译,可以按照常规的开发方法,在PC上编译好以后上传到板子上执行。但是我使用的1.2.3版本的RKLLM-Toolkit,在文档中明确推荐的交叉编译工具版本是GCC10.2,而我PC上的瑞芯微平台的编译工具链是gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu。所幸我用的瑞芯微RK3588板子上运行的Debian系统有满足条件的编译工具: 所以这样就比较简单了,可以直接把板端推理代码的源文件(main.cpp)、头文件(rkllm.h)、链接库(librkllmrt.so)以及前面转换好的板端模型上传到板子上,直接在板子的debian系统中使用g++进行编译: 执行后的打印信息如下: 可以看到,这个模型在RK3588上已经跑起来了。但是0.5B规模的大语言模型所生成的内容还是存在比较大的问题,不仅轻松的解决了西咸一体化这个困扰西安发展30年的痼疾,而且明显把榆林的资料张冠李戴到了咸阳头上。 推理过程中的内存占用上,这个模型只用到了680MB的内存,而实际上光是板端转换后的模型文件就已经800MB了,这就说明瑞芯微RKLLM在执行时并没有把完整的模型文件全部加载到内存中,而是按需加载,这样的话对于嵌入式设备这类内存非常紧张的应用还是非常实用的。 参考资料 - airockchip/rknn-llm - RKLLM SDK User Guide v1.2.3

瑞芯微RK3588 NPU开发环境搭建笔记

本文以RK3588+Debian Linux系统为基础,搭建瑞芯微平台的NPU开发环境与板端推理环境,并以Model Zoo中的Yolov5模型为例进行该模式上板运行流程的总结。 瑞芯微NPU应用开发框架 瑞芯微的NPU应用开发的整体开发流程上,基本上与君正AI应用开发的后处理流程(Post Processing)大致一致,首先是在PC上基于Pytorch或者tensorflow等进行模型的开发和训练,完成后把这个模型(如onnx格式或者pt格式)使用一个 RKNN-Toolkit2 的工具转换为RKNN格式,这个RKNN格式才是能够在瑞芯微芯片上跑起来的端侧模型格式,后续在生产环境中使用C/C++语言调用这个模型进行前向推理。 RKNN的软件框架如下图所示: !image.png RKNN-Toolkit2、RKNN-Toolkit-Lite2、RKNPU2 C++ API之间的关系: - RKNN-Toolkit2用于把PC或者服务器上训练好的模型转换为可以在瑞芯微NPU上运行的RKNN格式,运行在PC端。 - RKNN-Toolkit-Lite2是一个可以在瑞芯微NPU上基于RKNN格式的模型进行前向推理的工具,运行在瑞芯微处理器上。 - RKNN-Toolkit-Lite2本质上是一个在瑞芯微处理器上运行的Python包,所以其执行的前向推理是在瑞芯微硬件上跑的python环境中运行。如果对性能有更高要求,应该使用RKNPU2的C++ API来运行AI而不是RKNN-Toolkit-Lite2。 - 所以:RKNN-Toolkit2用于进行模型转换;RKNN-Toolkit-Lite2用于对转换后的模型在板端进行快速验证和测试;RKNPU2 C++ API才用于最终的项目实际落地。 > 我自己的看法,在板端的推理上,C/C++代码的效率要远远高于Python。所以其实没有太大必要在板端用RKNN-Toolkit-Lite2的python脚本去做推理验证,直接用RKNN-Toolkit2进行模型转换,然后在板端用C/C++进行推理才是一步到位的最成熟的做法。当然,使用RKNN-Toolkit-Lite2的python脚本在板端去做推理验证,优势则在于有大量Python包的支持,而且与PC端环境一致,写推理代码更容易。 RKNN-Toolkit2 能够支持的芯片平台列表: - RK3588 Series - RK3576 Series - RK3566/RK3568 Series - RK3562 Series - RV1103/RV1106 - RV1103B/RV1106B - RV1126B - RK2118 如上所述,瑞芯微NPU应用开发主要依赖于RKNN生态。在瑞芯微芯片的NPU上进行应用开发主要依赖于以下两个在Github上维护的代码仓库: - RKNN-Toolkit2: airockchip/rknn-toolkit2 ,模型转换工具。 - RK Model Zoo: airockchip/rknn_model_zoo ,模型上板运行的支持示例。 RKNN-Toolkit2开发环境的搭建 截至2026年初,最新的RKNN-Toolkit2的版本是2.3.2。 进行瑞芯微RKNN NPU生态的应用开发与测试,首先要进行RKNN-Toolkit2开发环境的搭建。从Github上下载RKNN-Toolkit2和RKNN Model Zoo仓库: 模型转换的Python环境 新建一个专门的rknn环境(Python版本指定为3.12)并安装RKNN-Toolkit2及其所有的依赖包(rknn-toolkit2-master/rknn-toolkit2/packages/x86_64): > 注意,按照以上流程安装的Python3.12环境,在后续在Model Zoo中把Yolov5的onnx文件转换为rkkn时会报错:AttributeError: module 'onnx' has no attribute 'mapping',这是因为Python 环境中安装的 onnx 库版本过高,与当前的 rknn-toolkit2 (v2.3.2) 产生了不兼容。应该在requirements_cp312-2.3.2.txt修改onnx依赖版本为:onnx==1.16.1, 而不是安装onnx的最新版本。 交叉编译环境 板端C/C++代码的交叉编译工具(rk3588为aarch64架构,所以安装64位系统的编译工具):https://releases.linaro.org/components/toolchain/binaries/6.3-2017.05/aarch64-linux-gnu/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu.tar.xz ,后续在编译模型上板推理的C/C++ demo时要用。 板端NPU驱动环境 此外,如以上RKNN的软件框架图所示,在RK3588板端上所执行的AI推理,同时还需要依赖于板端NPU的相关支持环境,包括板端kernel的NPU驱动、瑞芯微RKNN网络的运行时库以及其他的支持库。 首先一点,需要在板端的Kernel中启动NPU驱动支持,并且确保kernel的NPU驱动版本与RKNN-Toolkit2及其运行时库的版本相匹配。可以在Kernel的启动信息或者通过dmesg得到NPU驱动的版本信息: !image.png 除了以上NPU驱动支持以外,板端应用程序在NPU上执行推理还需要运行时库librknnrt.so的支持。librknnrt.so在rknn-toolkit2-master/rknpu2/runtime/Linux/librknn_api/aarch64位置,在进行板端推理的C/C++ demo编译时应该链接到这个动态链接库,并且把这个库提前放到板端系统的LD_LIBRARTY_PATH路径中。 此外,对于模型上板推理的C/C++ demo运行,除了以上的NPU驱动以及NPU运行时支持动态链接库librknnrt.ko,可能还需要其他的多媒体与硬件加速库: - RGA (Rockchip Graphics Architecture):用于硬件加速的图像缩放、裁剪、格式转换(如从摄像头拿到的 YUV 转为 NPU 需要的 RGB)。需要 librga.so 库文件及其头文件。 - MPP (Media Process Platform):用于进行硬件编解码(H.264/H.265),需要 librockchip_mpp.so。 至此,在瑞芯微平台上进行NPU开发的Python、C/C++交叉编译环境以及板端运行环境就已经准备好了。接下来通过Model Zoo中的Yolov5 demo对环境和NPU开发的流程进行总结。 基于Model Zoo中的YOLOv5进行上板测试 在前面已经下载的model zoo的仓库rknn_model_zoo-main的examples中,包含有多个已经在瑞芯微平台上适配好的模型案例,经过简单修改编译后即可上板运行测试。整个流程可以分为两个步骤:模型转换和板端C/C++ demo编译。 模型转换 在examples/yolov5/model目录下,执行download_model.sh,这个脚本的功能就是从指定的网络地址上下载一个onnx的yolov5s模型,所以这一步如果自己有已经训练好的yolov5s模型,完全可以使用自己训练的模型来替代。 接下来在examples/yolov5/python目录下,执行convert.sh脚本来进行模型转换(onnx->rknn)的动作。convert.sh脚本中指定了数据集描述文件路径DATASET_PATH和模型转换输出文件路径DEFAULT_RKNN_PATH,有必要的话可以对这两个值进行修改。然后执行python convert.py ../model/yolov5s_relu.onnx rk3588进行模型转换,转换后的模型会放到DEFAULT_RKNN_PATH所指定的路径中(以rknn为后缀)。 至此上板运行的模型文件就转换好了。 C/C++推理demo 在rknn_model_zoo-main目录下有两个build脚本文件:build-linux.sh和build-android.sh,分别用于针对板端的Linux和Android环境编译C/C++推理demo。此处以RK3588的Debian Linux环境为例,所以首先修改build-linux.sh,在最前面增加交叉编译工具链的路径: !image.png 然后调用build-linux.sh脚本进行yolov5板端推理demo程序的编译:./build-linux.sh -t rk3588 -a aarch64 -d yolov5 -b Release。build-linux.sh实际上会跳转到examples/yolov5/cpp/目录下,找到其中的CMakeLists.txt,调用cmake来执行完整的编译动作。 build-linux.sh的参数说明如下: build-linux.sh执行完成后会在rknn_model_zoo-main目录下生成一个install目录,其中包含了demo在板端运行所需要的所有文件:demo可执行程序,运行时依赖库librknnrt.so,板端rknn模型文件,测试图片文件。所以只需要把这个install目录打包后上传到板端执行其中的demo程序即可:

一文彻底搞懂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

详解QWen3-VL模型的测试以及vLLM生产环境部署流程

本文对阿里于2025年9月份发布的Qwen3-VL视觉语言模型进行了简单介绍,并对其预训练模型进行了本地推理测试以及在生产环境中使用vLLM进行部署的流程进行了总结。 QWen3-VL模型简介 阿里在 2025 年 9 月推出了Qwen3-VL 系列视觉语言模型。相较于其前代产品(如 Qwen2-VL),Qwen3-VL 的发布标志着多模态大模型从简单的视觉感知正式跨越到了深层的逻辑推理与自主 Agent 交互的新阶段。Qwen3-VL 延续了阿里的开源精神,推出了涵盖从端侧到云端的多种规模模型。 !image.png 在模型的总体设计架构上,Qwen3-VL 延续了阿里一贯的Dense(稠密)+ MoE(混合专家)双线并行的策略,参数量从2B到235B不等。 - Dense:该模型中所有的参数在处理每一个 Token 时都会被全部激活并参与计算。 - 其代表版本是Qwen3-VL-2B / 8B / 32B。 - 优点是在同等总参数量下,知识密度最高,部署相对简单。但缺点则是当参数量变大时,计算的开销线性增长,推理会变得非常缓慢。 - 所以适用于参数量不是很大的应用场景。 - MoE:该模型结构在包含很多专家(Experts)子模型,在处理特定输入时,会由门控网络(Gating)动态挑选其中的一小部分参与计算,大部分参数是不需要激活的。 - 其代表版本是:Qwen3-VL-30B-A3B (表示总参数30B,激活参数3B);Qwen3-VL-235B-A22B (表示总参数235B,激活参数22B)。 - 优点是拥有超大规模模型的知识库,但推理速度和计算开销却像小模型一样快。缺点则是对显存的要求仍然很高,虽然计算时只激活一小部分,但推理时必须将全部参数加载进显存。 此外,Qwen3-VL首次引入了 Thinking 版本:当面对复杂的 STEM、数学题或深层因果分析,模型不再是快思考直接给出直觉答案,而是通过强化学习(RL)生成的推理链,逐步拆解图像细节。因此 Qwen3-VL模型支持两种交互范式:Instruct和Thinking。 - Instruct (指令微调版):快思考,接收指令后直接生成并给出最终结果。 - 其特点是低延迟、生成速度快、输出简洁。 - 适用场景为日常对话、简单的图片描述、基础 OCR、实时 UI 操控等对实时性要求高的任务。 - Thinking (推理增强版):慢思考,在给出最终答案前,模型会先生成一段不可见的(或可展开的)思维链 (Chain-of-Thought, CoT)。 - 其特点是准确率更高(尤其在 STEM、数学、逻辑推理的复杂任务中),但生成时间长,会消耗更多 Token。 - 适用场景是复杂的数学几何题、逻辑推理、长视频的因果分析、需要严谨证明的场景。 QWen3-VL的推理测试 以下基于HuggingFace的Transformers下载 Qwen3-VL-4B-Instruct-FP8 这个版本的预训练模型文件,并在本机上做一些简单的推理测试。以下给出一个对手写文字图片进行OCR识别的推理测试。 首先import必要的包并使用Transformer从Huggingface上下载预训练模型文件列表到本地,并加载进来: 以上代码会自动下载预训练模型文件到本地 HF-HOME 环境变量所指向的位置,模型权重文件比较大,下载需要比较长的时间。下载后的模型文件如下所示: !image.png Qwen3VLForConditionalGeneration和AutoProcessor在每次启动加载模型文件的时候,都会对比本地下载的模型文件与HuggingFace Hub仓库中文件的文件的md5是否匹配,这个过程需要的时间会比较长,所以第一次下载模型文件完成后,可以通过以下代码设置HF Offline模式,避免每次加载的校验过程: 写一个接口函数用于对传入的图片进行OCR识别: 向上面的接口函数传入一张图片并打印出来OCR识别的结果: 这里传递了一张手写文字的图片: !image.png 最终OCR识别的结果如下:可以看到,在图片中字迹很潦草的情况下,模型的识别正确率仍然非常高。 QWen3-VL的vLLM生产环境部署 vLLM 是目前支持 Qwen 系列最好的引擎。它可以把 Transformers 模型包装成一个高性能的 OpenAI 兼容接口,后续直接使用HTTP协议来对模型进行访问。 首先安装vllm和qwen_vl_utils支持包。因为vllm的运行对于pytorch以及cuda的版本有要求,所以最好在一个独立的环境中安装和运行vllm: > 注意,vllm在windows环境下的安装总是会报配置文件的错误,但是在Ubuntu下面没有这个问题,一般生产环境部署用的都是Linux,所以我也就没有追究这个问题了。 以上vllm包安装完成后,使用以下命令就可以一键启动QWen3-VL模型在vLLM的部署和运行了: 其中: - -model参数用于指定模型文件所在的目录。 - -served-model-name:指定后续通过HTTP API访问模型的模型名称 - -trust-remote-code:必须加上,因为 Qwen3-VL 有自定义的视觉处理逻辑。 - -gpu-memory-utilization 0.75:告诉 vLLM 占用 75% 的显存,vLLM 会预先占满显存来管理PagedAttention。如果显卡显存少的话需要调低这个值。 - -max-model-len: 设置最大上下文长度。如果显存小,可以调小这个值。 - -limit-mm-per-prompt: 使用json格式限制一次对话最多传几张图。 如果有以下打印信息,就表示vLLM环境中部署的Qwen3-vl模型已经运行起来了: 使用下面的代码测试本机所部署的模型: !image.png 针对上面url参数所指定的图片,从模型得到的返回信息如下,模型部署测试验证OK: 参考资料 - Introduction to Qwen3-VL

从头实现一个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。

一文入门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

如何理解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

一文彻底搞懂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

YOLOv5模型的损失函数计算总结

本文以YOLOv5模型的v6.0/v6.1版本为基础,详细总结和解释了损失函数各个部分的计算流程。 YOLOv5模型的损失函数由三个部分组成: - 目标框定位损失,Box Loss - 目标置信度损失,Obj Loss - 分类损失,Cls Loss 其中,边界框回归损失采用了CIoU(Complete IoU)损失,而目标置信度和分类损失均使用二元交叉熵(BCE)损失。 目标置信度损失Obj Loss YOLOv5的目标置信度损失Obj Loss是唯一一个正样本和负样本(背景)都要参与计算的损失项。它的核心任务是教模型回答一个问题:“这个网格里到底有没有物体?如果有,这个预测框的可信度有多高?” 此外,在进行损失计算的过程中,build_targets所返回的正样本列表中,所有正样本的置信度标签值并非固定是1,而是作为正样本的这个预测框与真值框的IOU。也就是说,正样本与真值框之间的重合度越高,这个正样本的置信度标签值也就越高。这就是YOLOv5损失计算所采用的软标签(Soft Label)策略: - 所有的负样本 Target () = 0 - 所有的正样本 Target () = CIoU Score 整个目标置信度损失 L_{obj} 的计算可以分为四个步骤。 1.预测值 (p_{obj})的预处理 YOLOv5模型的前向推理流程在三个特征层(P3, P4, P5)输出预测张量。对于置信度通道,其推理输出的是未经激活的数值。因为计算置信度损失值使用二元交叉熵BCE算法,因此要首先对模型输出的 Logits 值进行 Sigmoid 激活,将数值映射到 (0, 1) 区间。 2.构建目标值 (t_{obj}) 如YOLOv5模型损失函数的背景知识总结一文所总结的,YOLOv5模型损失函数计算类ComputeLoss中的build_targes负责基于训练样本的真值框列表以及各个检测层的Anchor列表构建并返回一个正样本列表。 此处计算目标置信度损失需要构建目标值 : - 首先初始化一个与预测特征图形状完全一样的全零张量。 - 然后遍历以上build_targets返回的所有正样本: - 计算当前作为正样本的预测框(Pred Box)与真值框(GT Box)的CIoU值。 - 然后将该 CIoU 值填入全零张量中对应的坐标位置 (b, a, gj, gi)。 - 最终得到的 的结果就是一个大部分位置是 0(背景),少数位置是 浮点数(前景 IoU)的 Target 张量。 3.计算二值交叉熵 (BCE Loss) 上面有了预测值 和 目标值 ,就可以计算其二元交叉熵损失了。YOLOv5 使用 PyTorch 的BCEWithLogitsLoss。对于针对单个预测框而言,其计算公式是: - 对于负样本 ():前半部分消失,只优化后半部分 ,迫使 趋近于 0。 - 对于正样本 ():前后两部分都存在,迫使 趋近于 CIoU 值。 此外,如YOLOv5模型损失函数的背景知识总结所描述的,如果通过超参数开启了Focal Loss功能,还需要对其进行Focal Loss处理。目的是压制负样本背景框的损失,提升正样本的损失权重。 以上公式计算的是一个预测框的置信度损失值,而一个检测层的置信度损失,则是该层输出的所有预测框计算置信度损失的总和: 其中 B 是 Batch Size,G 是网格尺寸,A=3 是每个网格设置的 Anchor 数量,默认为3。 4.层级加权平衡 (Layer Balancing) 因为小目标的特征信息少,背景干扰多,回归难度大,模型需要更强的梯度信号来学习如何检测它们。所以在对三个检测层的置信度损失求和得到总的置信度损失时,还需要对各个检测层的损失值加权求和:对输出小目标特征信息的 P3 层乘以更高的权重,对输出大目标特征信息的 P5 层乘以较小的权重,以强迫模型更加重视高分辨率特征图(P3)上的预测结果,从而提升小目标的召回率和准确性。 再次强调一下,只有置信度损失的计算流程中,会被负样本考虑在内,以下的类别损失以及定位框损失均不考虑负样本。 类别损失Cls Loss 类别损失的目标非常清晰:对于被确定为正样本的预测框,它必须正确识别出该物体属于哪个类别。 类别损失 的计算流程具有以下两个关键特征: - 只计算正样本: 和定位框损失 一样,只有那些被分配了真实框的正样本(也就是build_targets所返回的正样本列表)才参与类别损失的计算。模型推理输出的两万多个负样本检测框(即背景框)的类别损失为零,不参与梯度回传。 - 多类别支持: 其目标是从所有可能的 C 个类别中,能够正确识别出目标的具体类别。 在YOLOv5中,类别损失同样采用标准的二值交叉熵损失BCE,但在工程实现上有一些巧妙的处理,使其能应用于多类别分类。其详细计算流程包含以下四个步骤。 预测值 ()的预处理 与置信度损失的计算流程相同,首先是要定位正样本。基于模型前向推理的输出张量以及模型在各个预测层所预设的Anchor框列表,调用build_targets函数提取出所有正样本的列表。后续类别损失值的计算只在这些正样本上进行计算。 因为要进行BCE计算,从build_targets返回的正样本列表中得到其中所包含的类别预测 Logits,然后对这些 Logits 进行 Sigmoid 激活。模型针对每个预测框,都会输出一个 C 维度的向量,其中 C 是总类别数(例如 COCO 数据集 C=80)。对每个类别输出的预测值都通过 Sigmoid 激活,将每个类别的分数独立地映射到 [0, 1] 区间,从而得到每个类别的预测概率_ 。 构建目标值 () 构造一个与正样本 维度相同的目标张量 。其维度为。对其中的标签赋值: - 对于其中的每个正样本,找到它对应真值框所标注的真实类别 k,在 向量的第 k 个位置上赋值 1,其余 C-1 个位置全部赋值0。 最终的结果就是,我们得到了一组稀疏的 one-hot 编码来作为下一步进行BCE计算的目标值。 在YOLOv5的损失计算流程中,为了防止模型对标签过拟合,其在训练中还会使用一种标签平滑(Label Smoothing)的技术。 大致就是将真实类别所设置的 t=1 的目标值稍微降低,将 t=0 的目标值稍微提高。 - 例如,对于某个正样本的类别标签设置而言,不是设置 ,而是 。 其目的是鼓励模型预测的概率不要过于绝对(如 1.0),从而提升模型的泛化能力和鲁棒性。 计算二值交叉熵 (BCE Loss) 的计算依赖于 BCE,但BCE的计算是针对每个类别独立计算的,然后对所有类别的损失求和。 假设 i 是正样本的索引,k 是类别的索引, 是模型预测的第 i 个样本属于类别 k 的概率, 是真实标签(0或1)。那么针对单个样本的单个类别 k 的损失公式就是: 由于 t_{i,k} 只有在真实类别 处为 1,在其他 C-1 个类别处为 0,这个计算可简化为: - 对于真实类别 :损失为 ,迫使模型提高对正确类别的预测概率。 - 对于错误类别 :损失为 ,迫使模型降低对所有错误类别的预测概率。 类别总损失的聚合 最后,将所有正样本在所有类别上的损失进行聚合(将每个正样本在 C 个类别上的损失加起来),就得到最终的 。 目标框定位损失Box Loss 在YOLOv5中,定位损失采用的是 CIoU Loss(Complete IoU Loss,完整交并比损失)。它的核心目标是:对于被确定为正样本的预测框,准确地调整其位置、大小和长宽比,使其尽可能地与真实框(GT)重合。 相比于普通的IOU指标,CIoU Loss 不仅仅关注预测框与真值框的重叠面积,还引入了中心点距离和长宽比一致性这两个惩罚项,从而提供了更全面、更稳定的梯度信号,极大地提升了模型的定位精度。CIoU Loss 的完整定义是: 其中, 是 CIoU 的惩罚项,它由中心点距离项和长宽比一致性项构成。 的计算流程如下所述。 筛选正样本并解码 这一点与以上计算类别损失相同。首先是定位正样本,即仅从 build_targets 函数生成的正样本列表中,取出对应的预测框(Logits),然后将模型输出的偏移量解码成真实的归一化坐标 (x, y, w, h)。 与类别损失相同,定位损失的计算同样只考虑build_targets所返回的正样本列表,不考虑其他的背景负样本。 计算 IoU 和 CIoU 基于每个正样本预测框,计算这个预测框与真值框之间的 IoU ,以及针对CIoU计算流程的距离和长宽比惩罚项。最终将所有项代入 CIoU Loss 的计算公式,计算每个正样本的 CIoU 损失 。 目标定位框的损失值聚合 对所有正样本的 值进行求和(通常会进行平均)得到总的定位框损失 。 总损失 经过以上三个部分的独立计算,就可以得到一个批次(batch size)的目标定位框总损失 ,目标置信度总损失 以及分类总损失 。整体的损失则是这三个损失的加权和: 其中的 , 和 分别对应于三个损失的加权权重,在超参数文件中进行设置,分别对应于超参数文件中的box,obj和cls设置项: 至此,针对训练过程中一个批次输出图片推理输出的完整损失值就计算出来了。

YOLOv5模型损失函数的背景知识总结

本文以YOLOv5模型的v6.0/v6.1版本为基础,详细总结和解释损失函数的计算流程及代码实现中所设计到的各项背景知识。 如下图所示的YOLOv5的检测头部分的结构,YOLOv5模型每个网格输出的检测框数据与其前几代类似,每个检测框包含的信息(5+NC)就是三个部分:检测边界框的坐标位置,检测的置信度,类别判断概率。 !image.png 相对应的,其损失函数也就由三个部分组成: - 边界框回归损失,Box Loss - 目标置信度损失,Obj Loss - 分类损失,Cls Loss 其中,边界框回归损失采用了CIoU(Complete IoU)损失,而目标置信度和分类损失均使用二元交叉熵(BCE)损失。 二元交叉熵(BCE) 首先以最简单的二分类问题解释二元交叉熵BCE的计算流程。假设训练数据的真实标签为 (为0或1),模型预测为 (为0到1之间的概率值),那么BCE损失函数的计算公式为: _因为对0到1之间的小数进行log计算,其结果永远小于0,所以以上的BCE公式的计算结果总是正数。对于log(x)的计算而言,当x=1时,log(x)=0;当x在0到1之间时,log(x)小于0且随着x变小,log(x)越来越小;当x接近0时,log(x)趋向于负无穷大。_ 因此: - 当标签 时,以上损失函数的计算可以简化为 。此时预测概率 越高,损失值越小越接近0。 - 而当 时,其损失可以简化为 。此时预测概率 越低,损失值越小越接近0。 如上所示,进行交叉熵计算的预测值 应该在0-1之间,所以对于YOLOv5模型三个检测头所输出的logits数据而言,在执行BCE损失计算之前,需要先对logits值进行sigmoid计算,把sigmoid的计算结果再进行BCE计算,因此实际上YOLOv5源代码中计算目标置信度损失和分类损失均使用了BCEWithLogitsLoss函数: BCEWithLogitsLoss函数的实现实际上对应于以下公式: 其中, 是模型输出的logit原始值, 是sigmoid激活函数, 是目标值。 正样本损失计算的权重控制 需要注意的是,以上的BCEWithLogitsLoss函数定义通过超参数的形式传递了两个pos_weight参数:cls_pw和obj_pw,默认情况下,这两个参数在超参数文件中的值均为1.0。此时,以上BCE计算公式中,正样本(t>0)的损失会被乘以 γ,负样本(t=0)的损失保持不变,实际上的等效公式变为: 因此,如果在模型训练的过程中,需要对类别损失和置信度损失计算过程中的正样本损失值的比重进行调节,就可以适当调节cls_pw和obj_pw这两个参数。设置这两个参数为大于1.0的值,将会强化训练正样本的损失值;否则将会弱化训练正样本的损失值。 - 例如,如果训练中观察到高漏检率或正样本预测置信度偏低的情况,可以尝试设置 obj_pw > 1.0(如 1.5~5.0),以加强正样本惩罚。而反之,如果出现过多较多的假阳性即把环境误判为正样本的情况,可以略微降低obj_pw( 总的思路是,如果模型对这个样本很有把握(置信度比较高),在进行损失计算时就把这个样本的Loss权重降到接近0;如果对这个样本没有把握(置信度比较低),损失计算时就保留这个样本的Loss。 具体的计算上,Focal Loss 在标准的 BCE 结果上乘了一个系数 : 这里 (gamma,也就是超参数fl_gamma的设置值) 是一个超参数,通常取值为 2。 - 对简单样本来说,模型的预测输出非常准,比如 = 0.99。在这种情况下按照以上算式计算出来的Focal Loss系数为: 。其结果就是原本的 Loss 被缩小了 10000倍,对梯度几乎没有贡献了。 - 而对于困难样本而言,模型的预测很差,例如 。其Focal Loss的系数为: 。结果就是Loss 只是稍微变小了一点,但依然保持了较大的数值,保留了对模型的指导意义。 当然,在 YOLOv5 中,Focal Loss 只应用于 Objectness Loss ()和 Classification Loss ()的计算,不用于 Box Regression Loss。

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开发实战——最简图像分割 - 知乎

机器学习领域的Python Web UI框架Gradio

Gradio是目前机器学习和数据科学领域最流行、最易上手的 Python Web UI 框架之一。简单来说,它的核心使命是:让你用极短的 Python 代码(通常只需几行),瞬间为你的机器学习模型构建一个可视化的演示界面。它特别适合用于快速原型设计、向团队/客户展示模型效果,或者在 Hugging Face Spaces 上托管 Demo。 Gradio的官方网站是:Gradio !image.png Gradio的重点特性 - 纯 Python 体验 (No HTML/CSS/JS): 不需要任何前端知识。所有的组件(输入框、按钮、滑块、图像上传等)都可以通过 Python 类调用。 - 极速构建: 对于标准的“输入 -> 模型 -> 输出”流程,通常 3-5 行代码即可完成,非常简单,容易上手。 - 丰富的组件库: 针对机器学习场景内置了大量专用组件,如 Image(图像)、Audio(音频)、Video(视频)、Dataframe(表格)、Code(代码)等。 - 一键分享 (Shareable Links): 运行 .launch(share=True) 后,Gradio 会自动生成一个公开的临时 URL(有效期 72 小时),你可以直接发给客户或同事,让他们在自己的浏览器里测试你的模型(虽然模型实际运行在你的电脑上)。_这个功能对于做一些demo的临时演示而言确实太方便了,不再需要浪费时间去搭建基于云服务的演示环境了。_ - 生态整合: 它由 Hugging Face 收购并维护,因此与 Hugging Face 的生态系统(Spaces, Transformers)结合得非常紧密。 > 以上的这个一键分享的功能对于模型demo的演示和试用实在是太方便了,只需要启动web应用的时候,传递一个share=true的参数,外网就可以直接通过系统生成的一个public URL访问,不再需要浪费时间去搭建基于云服务的演示环境了。* 开发环境搭建与Hello World gradio的开发环境安装非常简单,直接使用pip安装即可,如果需要在Jupyter notebooks中使用的话,还需要安装ipykernel: 总体上就,Gradio构建UI界面主要有两种构建方式: - gr.Interface:提供最高级的抽象,使用起来最简单,比较适合单功能。 - gr.Blocks:更加灵活,如果需要更复杂的布局(例如多列排版、多个标签页、控制组件间的数据流向),可以使用 Blocks 像搭积木一样控制页面的每一个显示细节。 以下代码以两种方式分别演示基本的Hello World功能: 以上代码执行后,就可以在本地浏览器http://127.0.0.1:7860看到Web UI界面。此外,在launch的share选项设置为True的情况下,Gradio还会自动产生一个外网可以访问的公开URL,这样就可以把这个URL分享给其他的同事或者客户,请他们体验当前模型的功能状况,而模型及其实际的运算仍然是在本地计算机上(_实际上Gradio只是在公共URL服务和本地计算机之间创建了一个tunnel,所有访问这个公共URL的流量都会被转发到本地计算机上_)。这个公开的URL可在1周内使用,对于演示和体验性的测试而言足够了。 以下是上面的Blocks模式的UI界面: !image.png Gradio的主要组件 Gradio框架的使用主要建立在以下四个核心概念上: - Blocks/Interface: 负责整个Web UI架构的布局管理。 - Component:超过30个内建的UI组件,用于方便地提供对文本、音频、图像等的输入和输出管理。 - Functions:标准的Python功能函数,从输入组件中得到数据,在functions中进行各种处理,最终通过输出组件输出和呈现出来。 - Launch:启动整个Gradio Web App的运行。 Gradio内置了 30 多种封装好的组件,这些组件通常既可以作为输入,也可以作为输出。具体取决于是将其放在inputs参数中还是outputs参数中。以下按照功能类型进行整理: 文本与对话类 gr.Textbox:最常用,主要用于显示文本框。 - 在输入模式中使用时,用于提供用户输入文本的界面,可支持设置 lines (行数)、placeholder (占位符)、type="password" (密码掩码)。 - 在输出模式中使用时,用于显示处理后的文本结果。 gr.Chatbot:大语言模型必备,专门用于显示对话历史。 - 通常作为输出组件,接收一个包含 (用户消息, 机器回复)元组的列表。 gr.Markdown:可支持 Markdown 语法的文本内容展示。常用于页面顶部的说明、公式渲染、或者富文本输出。 gr.Code:专门用于展示代码片段,支持语法高亮和一键复制按钮。 多媒体类 (Video和Audio) gr.Image:上传和显示图片。 - 在输入模式中使用时,可支持上传静态图片、使用 Webcam 拍照或者提供画板 (canvas) 让用户手绘图像。且可以自动将图片转为numpy数组或PIL对象。 - 在输出模式中使用时,可用于展示处理后的图片。 gr.Audio:上传和播放音频文件。 - 在输入模式中使用时,可支持上传音频文件或者使用麦克风录音。 - 在输出模式中使用时,可用于播放Functions功能所生成的语音(TTS)或音乐,并提供波形图显示和下载按钮。 gr.Video:与Image组件类似,可支持上传或录制视频,以及播放生成的视频流。 gr.Gallery:用于以网格的形式同时展示多张图片,非常适合图像生成模型(一次生成 4 张图)或图像检索任务。 数值与控制类 (参数控制) 这部分组件通常作为输入,用于调节模型的参数或运行选项。 gr.Slider:滑动条组件,适合用于选择一定范围内的数值(如 0 到 1 之间的置信度阈值)。 gr.Dropdown:下拉菜单组件,适合从预定义的列表中选择一个值。 gr.Radio:单选按钮组。功能同 Dropdown,但所有选项直接展开显示。 gr.Checkbox/gr.CheckboxGroup:复选框,用于作为布尔值开关(True/False)或多选标签。 gr.Number:用于作为精确的数字输入框。 数据与文件类 (分析与通用) gr.File:通用文件上传/下载。如果处理的是 PDF、Zip 包或特定格式的文件,可以用这个组件上传到程序中。可支持多文件上传。 gr.Dataframe: - 在输入模式中使用时,类似 Excel 的表格编辑器,用户可以手动输入数据或粘贴 CSV。 - 在输出模式中使用时,可用于展示 Pandas DataFrame 数据,可支持排序和筛选。 gr.Plot:专门用于展示图表,可完美支持 matplotlib、plotly、bokeh等绘图库生成的对象。 gr.JSON:用于以树状结构漂亮地展示 JSON 数据或 Python 字典,可方便查看复杂的 API 返回结果。 基于Gradio演示OpenCV图像模糊化效果 下面使用Gradio的多个不同组件,演示一个使用Opencv实现对输入图像进行模糊化处理并显示出来的Web UI效果。代码如下所示: 最终的UI显示效果如下: !image.png 参考资料 - Gradio - Building User Interfaces For AI Applications with Gradio in Python | DataCamp

开源数据集管理工具FiftyOne

本文对Voxel51所开发的开源数据集可视化和管理引擎Fiftyone的工作特性、环境搭建以及常用的应用场景进行总结。 FiftyOne是由 Voxel51 开发的一个开源数据集可视化和管理引擎,其官网地址为:Maximize your Computer Vision Performance | Try Voxel51。 !image.png 对于数据集的管理而言,Fiftyone的核心功能包括: - 数据集的可视化(The App): Fiftyone提供了一个网页界面,可以让你像刷朋友圈一样查看图片及其叠加的标注框(Bounding Boxes)信息,检查和审核标注数据的质量。 - 高效过滤(Querying): 允许你用 Python 语句快速筛选数据。例如使用简单的代码快速找出数据集中所有包含指定目标类型且图片分辨率大于指定分辨率的数据。 - 模型分析:训练完自己的模型(例如YOLO)后,可以把预测结果倒回 FiftyOne,直接在其中对比标准答案和模型预测,一眼看出模型在什么情况下会出现漏判或者误判,从而更有针对性的改善数据质量。 环境的准备 首先,需要安装 FiftyOne 及其依赖。建议在一个独立的虚拟环境中进行。 浏览数据集 使用 FiftyOne 浏览本地及其在线数据集非常直观。通过使用Fiftyone,不仅能通过其Web页面看到数据集中所包含的图片及其标注信息,还能通过强大的查询语句像操作数据库一样过滤和操作这些视觉数据。 以下代码分别演示了对Fiftyone维护的在线数据集以及本地的YOLOv5格式的数据集进行访问的代码流程。 执行以上代码后,直接在本地通过浏览器访问 http://localhost:5151/ 即可对对应的数据集机器中的标注信息进行浏览和管理。 !7815904f-7a16-4925-8e6a-46ec239303b7.jpg 其实fiftyone来访问数据集的代码比较适合在Jupiter Notebook中运行。如上图所示,通过 launch_app 启动fiftyone的运行以后,在其Web UI界面中可以浏览数据集,此外,launch_app返回一个session变量,那么在Jupiter Notebook中运行以上代码时,就可以在代码中通过这个session访问打开的数据集,对其进行一些在线的处理和操作。 例如,我们可以在web UI界面中选择质量不好的图片,然后在notebook中对这些选中的图片执行删除操作,并导出到一个新的数据集中: !image.png session支持的选项很多,通过以上方式,就可以实现与已打开的数据集进行动态的交互。 从公开数据集中下载并清洗数据 FiftyOne中内置了一个叫做 Dataset Zoo 的模块。该模块预先写好了对接 COCO、Open Images、VOC 等几十个主流公开数据集的下载和解析脚本。在没有 FiftyOne的情况下,对于这些公开数据集的访问和使用,往往需要你去官网注册、下载几十 GB 的压缩包、研究它们复杂的 JSON 或 CSV 格式并写脚本解析和转换不同格式的坐标和标注信息。在FiftyOne的帮助下,只需要一行命令 foz.load_zoo_dataset("coco-2017", ...),它就会自动完成筛选、下载、解压、格式对齐等工作。 通过使用以下代码,就可以实现从fiftyone官网上自动下载COCO-2017数据集中前1000张人形的标注照片,并且把标注信息格式自动修改为YOLOv5格式,保存在当前脚本目录的my_yolo_dataset/coco_subset/train/子目录下。 > 注意,按照以下代码的流程,从指定数据集(coco-2017)中下载指定数量(1000张)的指标类别(human)的图片集,FiftyOne 在下载时只会流式下载用户指定的这个图片子集(先下载到C:\Users\用户\fiftyone,然后整理到当前脚本所在目录下),不需要把完整的几百个GB的数据集全部下载下来,这样的按需下载方式可以极大地节省本地硬盘空间和下载所需要的时间。 此外,从在线数据集中下载数据的过程中,为了提升数据集的质量,还可以在导出之前,对下载的数据集进行一定程度的清洗,例如以下代码,可以实现从下载的数据集中自从删除掉无标注框的样本,以及删除掉那些过小的标注框: 对数据经过以上清洗操作以后,再通过export命令把这些清洗后的数据导出来,可以有效的提升训练数据集的质量。 辅助YOLOv8模型微调 不只是可以简化训练数据集的下载、数据清洗和浏览数据集的过程,Fiftyone还可以与模型的推理过程相结合,以可视化的形式直观的显示出模型在推理数据上的表现以及分析其失效模式。例如,通过配合使用Fiftyone和YOLO模型,可以在YOLO模型训练完成后,可视化和评估该模型在验证数据集上的预测结果,从而更好地了解模型的预测能力在哪里失效,并针对性的改善模型的训练数据和指标。 以下代码演示了把YOLO模型的验证集数据导入FiftyOne,在FiftyOne中同时显示标注框和预测框的流程。这样训练人员就可以非常直观的从FiftyOne的Web UI界面上看到验证集上的标注框和训练模型的推理框,直观的了解到当前模型会在哪些数据上失效: 以下为在FiftyOne中显示YOLO模型验证集数据、标签以及推理结果的效果。 !a95a1162-1e30-459e-8466-b3e44ed840f6.jpg 参考资料 - Maximize your Computer Vision Performance | Try Voxel51 - Fine-tune YOLOv8 models for custom use cases with the help of FiftyOne — FiftyOne 1.11.0 documentation

实例讲解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

开源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

YOLO模型的配置参数文件详细解释

本文对ultralytics/YOLO项目中的配置参数文件default.yaml文件中所包含的所有参数(可视化和导出类的参数除外)进行详细总结和解释。 ultralytics/YOLO项目中的配置参数文件路径在ultralytics/cfg/default.yaml。default.yaml文件是Ultralytics项目的核心配置文件,理解其中各项参数的含义对于成功训练自定义数据集以及执行推理、导出、部署等环节至关重要。 task与mode default.yaml配置文件的开头,使用 task 和 mode 这两个参数设定了模型的基本运行模式。这两个参数共同决定了YOLO模型的运行方式,不同的task会加载不同的模型架构和损失函数,而不同的mode则会执行相应的工作流程(训练、验证、推理等)。 task参数用于指定模型的任务类型,指定task参数以后,项目会根据指定的任务类型创建网络结构。当前的Ultralytics项目的实现中,包含有五种任务类型: - 目标检测(detect):目标检测任务,用于检测和定位图像中的目标对象。detect是该项目最常用的任务类型,也是default.yaml中的默认task。 - 实例分割 (segment):实例分割任务,不仅检测对象位置,还提供像素级的分割掩码。 - 图像分类 (classify): 图像分类任务,仅对整个图像进行分类。 - 姿态估计 (pose):姿态估计任务,用于识别人或动物的关键点/关节位置,提供类似火柴人一样的识别和检测效果。 - obb :旋转边界框检测任务,用于检测非轴向对齐的目标(如无人机视角下的物体) 对于以上各种不同类型的task,可以参考计算机视觉类AI应用领域及其主流模型总结一文。 mode参数用于指定当前的运行模式: - train:训练模式,用于执行模型训练过程。 - val:验证模式,用于评估当前训练好的模型在验证集上的性能,生成验证指标文件。 - predict:预测推理模式,用于使用训练好的模型对指定的数据进行推理输出。 - export:导出模式,用于将模型导出为不同的部署格式(如ONNX、TensorRT等)。 - track:在detect模型上启用跟踪模式,可用于对视频中的为每个检测到的目标分配唯一ID并持续跟踪其运动轨迹。启用后需通过tracker参数指定跟踪配置文件。 - benchmark:基准测试模式,用于更全面地评估模型在不同硬件和配置下的性能、速度与精度。 训练相关参数 default.yaml文件中与模型训练流程相关的参数及其解释如下表所示: !image.png 以下对上面的部分参数做更为详细的解释和说明。 save(默认为True)和save_period(默认为-1)参数用于指定是否保存训练的权重文件到save_dir/weights/目录下。在save设置为true的情况下,每个epoch结束以后,都会把当前训练的权重覆盖保存在last.pt文件中,而如果当前训练的效果比之前的指标更好的话,该权重文件也会保存在best.pt文件中;当save_period参数设置为N时(N不等于-1),每隔N个epoch就会在save_dir/weights/目录下多保存一个epoch_N.pt的中间训练权重文件。 cache参数决定了是否以及如何在训练开始的时候,将数据集预先加载并缓存起来,以避免在训练过程中重复进行耗时的磁盘读取和数据处理操作。设置为False(默认值)表示不做缓存处理,每次都是从磁盘加载新一个batch的训练数据;设置为True或'ram'表示提前把训练集中的数据缓存在RAM中,每次从RAM中取数据;设置为‘disk’时在第一个epoch期间,训练程序会将处理好的图像以特殊格式(.npy)保存到硬盘的临时目录,后续的epoch从该缓存读取。 project和name参数搭配起来可以按照自定义的方式指定模型训练文件的保存路径。不设置这两个参数的话,默认训练流程所产生的文件会保存在runs/detect/train/exp这样的目录下,而如果设置以后,这些文件的保存路径就是project/name。exist_ok这个选项则表示多次执行的时候是覆盖这个project/name还是在name指定的目录名后面加执行次数的索引后缀名来对多次执行的结果进行区分。 pretrained参数设置为False的情况下,模型训练只会使用model参数所指定的模型,模型各层的权重使用完全随机的参数进行初始化;pretrained参数设置为True的情况下,表示直接使用model参数所指定的模型文件中的权重;pretrained参数设置为某个权重文件路径时,表示使用model参数所指定的模型,但是模型的各层参数用pretrained所指定的参数文件进行设置和初始化。 seed和deterministic这两个参数用于控制模型训练过程和结果的随机性和可复现性。默认的配置下(seed=0,deterministic=False),模型训练的流程和结果保留了一定的随机性,比较适合日常的开发情况。而对于一些要求更为严谨的应用(如论文发表等),模型的训练需要提供严格的确定性和可复现性,此时应该把这两个参数设置为seed=42,deterministic=True。 single_cls参数是一个布尔值,默认为False。当设置为 True时,模型的输出会强制将所有对象都视为同一个类别,即模型只输出一个类别。如果应用只关心画面中是否有某个类别的物体存在时,可以把这个参数设置为True以将复杂的多类别检测问题简化为更基础的目标存在性检测问题。 rect参数用于指定是否使用固定的正方形长宽比作为模型训练图像的输入。在rect=False(默认值)的情况下,所有进行训练的图像无论其长宽比是什么,都会先通过缩放+填充的手段转换为正方形送入模型进行训练,这种模式一般是针对数据集长宽比不一致且差异比较大的情况;而设置rect=False的情况下,YOLO会首先会对每个批次的图像长宽比信息进行统计,找到最合适的长宽比,然后在训练的过程中,把这个批次的所有图像转换到新的长宽比送入模型进行训练,针对数据集中所有图片长宽比基本相同的情况(例如监控摄像头一般为4:3或者16:9),这种方式可以提升训练效率。设置rect参数的作用主要是可以根据数据集长宽比的情况对输入图像尺寸进行调整,尽量减少GPU计算的浪费,提升训练的效率和速度。 close_mosaic参数用于指定在训练过程的最后N轮训练禁用mosaic数据增强的功能。mosaic数据增强技术可以通过把四张训练图片以不同的、每个批次随机的方式剪切并且合并到一起,形成一张新的训练图片对模型参数进行训练,这种方式可以通过训练数据的多样性变化提升训练的效果。但是在训练的末期,我们一般希望参数尽快稳定下来,而mosaic增强会导致参数发生的变化过于剧烈,所以此时可以通过设置close_mosaic参数指定训练过程提前N轮禁用mosaic增强,确保参数在训练过程的最后阶段尽快稳定下来。 amp参数用于控制训练过程中所使用的数据及其计算精度。在传统的模型训练过程中,一般在全流程中使用FP32精度进行训练。当开启AMP模式以后,对于前向、后向运算等环节使用更节省内存的FP16精度,仅对梯度等计算相关的部分采用更高的FP32精度。这样做的好处是可以基本保持训练指标不变的情况下,大幅度减少训练过程对于内存的需求,以及加快训练速度。默认情况下一般推荐使能AMP模式,但是需要注意的是部分比较老旧的硬件不支持AMP模式。 fraction参数用于指定训练过程中参与训练的数据量占总的数据集数量的比例。一般情况下,我们对于模型的训练肯定都是要把100%的训练集数据送入模型对模型参数进行训练。但是在某些需要快速验证、并且数据集数据量很大的情况下,可以通过设置fraction参数只使用一定比例的训练集数据对模型进行训练,在这种情况下,每个epoch就只会随机的从训练集中提取出来fraction参数指定比例的数据进行训练。 Profile参数用于控制是否启用训练过程中的性能指标分析。在启用(Profile=true)后,整个训练过程中会详细的针对每一步的操作进行细粒度统计和分析,最终给出非常详细的性能分析数据。因此这个过程会需要更多的内存保存这些统计信息,并且大幅度拖慢训练的速度。因此除非是要对模型的框架进行调整,需要从很详细的程度上了解模型的执行效率,否则不建议开启profile模式。 freeze参数用于指定在训练的过程中哪些层的权重需要冻结,在训练中不更新这些层的参数。如果是在官方预训练的YOLO模型的基础上,基于自己的数据集进行模型的微调,因为官方的模型参数一般都是COCO这类比较大的数据集上进行了非常充分的训练,因此其预训练参数已经包含了非常强大的特征提取能力。在自己的数据集数量不是很大(几千张之内)、并且检测任务区别不大的话,建议可以使用freeze参数把网络的主干网络部分冻结,只使用自己的数据训练颈部网络和检测头部分。可以使用freeze参数控制冻结前N层或以列表的形式冻结指定的层参数。 multi_scale参数用于控制训练过程中输入图像的尺寸大小。如果multi_scale设置为False,训练过程中输入的图像尺寸大小固定为imgsz设置为640x640。而在multi_scale设置为True的情况下,首先针对每个batch随机选择一个图像尺寸大小(必须以32为单位),然后在这个batch的图片的训练过程中,图像在送入模型进行训练之前,首先会被缩放为这个新的尺寸。这样每个batch的输入图像尺寸大小都不同,可以随机调整训练数据的多样性,从而提升模型的适应性和泛化能力。 compile参数是YOLOv8基于Pytorch2.0的torch.compile引入的新功能,在开启的情况下,可以在训练的过程中优化模型的计算图代码来加速训练执行的代码运行效率,降低训练时间。如果训练数据集比较大、且训练所使用的硬件规格比较新的话,可以开启这个选项来加速训练过程。 图像分割与分类参数 以下的overlap_mask,mask_ratio以及dropout这三个参数仅在图像分割和图像分类任务中有效。 验证和测试参数 下表为default.yaml配置文件中Val/Test settings部分的配置参数、默认值及其简单的解释说明。 > 需要注意的是,下表中的部分参数虽然被归类在配置文件的Val/Test settings部分,但是这些参数同样会被用于predict过程。例如NMS算法相关的iou、max_det、conf等参数。 !image.png split参数用于指定当验证功能启用时(即val参数设置为True),验证过程执行时所使用的数据集。其取值范围为[val, test, train],分别表示使用验证集、测试集或训练集进行模型评估。绝大多数情况下应设置为val,以避免测试集数据泄露和确保评估的公正性。 conf,iou,max_det这几个参数是NMS算法的过滤参数。conf是目标检测的置信度,iou用于对重叠框进行过滤,max_det用于限制一张图片最大输出的检测结果数量。 half参数用于指定在模型前向推理的过程中使用的精度是FP32还是FP16。dnn参数用于指定在模型执行前向推理的过程中使用OpenCV的dnn模块还是使用pytorch框架。 预测参数 下表为default.yaml配置文件中Predict settings部分的配置参数、默认值及其简单的解释说明。 !image.png visualize参数用于控制在验证和推理执行过程中是否生成可视化统计分析结果以及图像识别的中间层特征图、热力图等信息,通过查看这些结果,可以辅助调试和定位模型所存在的问题。当设置为True的情况下,因为要生成这些额外的文件并写入磁盘,所以会显著增加内存需求以及影响推理的速度。 augment参数用于控制推理过程的执行,是否需要对输入的图像执行增强操作。在augment=False的情况下,模型在推理的过程中只会把原始图像输入后执行一次推理并给出结果;如果设置为True,模型会对输入的图像做各种增强操作(如翻转、缩放等)后执行多次推理操作,最后合并多次推理的结果为最终输出,因此预测的精度会有所提升,但是耗时会大幅增加。 agnostic_nms参数是一个NMS算法执行流程中的参数,用于在NMS对重复框过滤的过程中,是否需要区分不同的类别。agnostic_nms参数设置为False的标准NMS流程中,NMS算法会对不同类别进行区分,各个类别的检测框不会相互影响;如果设置为True,NMS会在过滤的过程中,把所有的类别放在一起进行过滤,这种情况下当两个不同类别的物体相距比较近的时候,有可能会被错杀。 classes参数指定推理过程中需要检测的目标类别索引列表。该参数未设置的情况下,表示对模型所支持的所有目标类型进行检测并输出;设置为某个列表时表示在执行NMS过滤的过程中仅关注该列表中的目标类型,其他目标都会被过滤掉。 超参数 模型在训练过程中的学习率与优化器方面的参数、默认值及其解释说明如下所示。 !image.png lr0和lrf这两个参数是与模型训练过程中反向运算时对参数调整的学习率参数。训练过程中的学习率参数会依据训练的优化器optimizer动态调整,其中lr0是模型开始训练时的初始学习率,此后学习率会逐渐变小,最终降低到lr0 * lrf的程度。momentum参数同样是一个optimizer优化器算法对模型参数进行调整的技术。引入momentum机制后,每次对参数的更新不仅依赖于当前计算的梯度和学习率,而且依赖于历史梯度,momentum参数用于指定历史梯度在本次参数调整中的加权系数。momentum的值越高,历史梯度对参数调整的影响越显著。 weight_decay参数用于控制权重参数的大小,避免权重参数过大导致过拟合问题。具体在训练过程中,通过把weight_decay参数和当前模型的总体权重信息增加到损失函数计算出来的损失值以及参数的调整量中,确保在权重参数过大的情况下,刻意的抑制参数的大小。 模型刚开始训练时,模型内部的部分参数处于随机初始化的状态,这个时候如果直接使用最大的学习率lr0对参数进行调整,会容易导致模型参数调节幅度过大而处于不稳定的状态。因此YOLO模型的warmup机制,就是指在刚开始训练的前几个epoch,把参数更新的学习率从0逐渐线性上升到lr0设置值,一开始的学习率很低,然后逐渐增大到lr0,有助于模型的参数快速达到基本稳定的状态,然后再开始正式的训练。warmup_epochs、warmup_momentum、warmup_bias_lr这三个参数均为warmup执行流程的相关参数。其中warmup_epochs是指该机制在前几个epoch中发挥作用,在warmup阶段,momentum会从warmup_momentum逐步线性上升到momentum参数的设置值,学习率(非偏置项)会从0逐步上升到lr0参数的设置值,偏置项学习率会从0逐步上升到warmup_bias_lr设置值,最后warmup阶段完成后,统一从学习率lr0参数开始正式训练。 与损失函数相关的参数、默认值及其解释说明如下所示: !image.png box,cls和dfl参数分别对应于YOLO模型的三个损失函数值的权重,其中cls对应于分类准确性的损失值,box和dfl一起对应了预测框位置准确度的损失值,box主要是从IOU的角度评估预测框与真值框的重合性,dfl则从上下左右坐标偏移量的角度来评估定位的准确性。适当调节box,cls和dfl参数可以用于调整不同类型的损失值在整体损失值中所占的比例,从而调整不同损失值所对应的指标。 nbs参数是名义上的batch size。实际的训练过程中,因为训练所使用的硬件限制方面的原因,batch size没办法设置的比较大,一般会设置为16。在batch size设置相对比较小的情况下,模型在训练过程中往往会存在波动较大、难以收敛的问题。比较理想的情况是使用batch size=64这样的配置。那么针对硬件配置限制的问题,batch size以及实际送入模型进行训练的图像batch仍然是按照实际设置的batch size(例如16)来进行训练,然后训练结果得到的损失值按照名义batch size与实际batch size的比例进行放大,当nbs设置为64时,两者相差4倍,把损失值乘以4倍送入模型反向传递进行参数更新。使用这种方法也可以解决实际batch size不足带来的模型训练稳定性问题。 训练数据颜色与几何增强相关的参数、默认值及其解释说明如下所示: !image.png hsv_h,hsv_s,hsv_v这三个参数对应的是训练过程中针对图像输入数据的颜色增强参数。HSV颜色空间包含色调H、色彩饱和度S和亮度V三个维度,以上三个参数以比例因子的形式定义了在训练过程中,训练图片数据会在原始图像的色调、饱和度以及亮度这三个维度上随机调整的幅度,通过这种对图像HSV空间三个维度的动态随机调整,来提升模型针对不同光照、色彩环境下的泛化能力。 degree参数用于指定训练集图片送入模型训练之前,在原始图像角度的基础上,以多大的幅度随机旋转一定的角度再进行训练。translate参数用于指定训练图片在原始图像的基础上,以多大的幅度向上下左右四个方向平移一定的比例再进行训练。scale参数用于指定在训练图片的原始图像基础上,以多大的比例范围内进行随机缩放后再进行训练。 shear和perspective这两个参数用于控制图像增强处理的几何变形功能,用于应对自动驾驶、无人机这类运动场景下的图像状况。其中shear是指把原始的长方形图像变形为平行四边形的拉伸动作,而perspective是指把原始的长方形图像从上到下或者从下到上逐渐变小的俯仰视角的变形动作,这两个参数的大小分别指定了两者变形的幅度大小。 图像变换增强参数部分的参数名、默认值及其解释说明如下所示: !image.png 以上三个参数比较简单。flipud表示训练图像在送入模型训练时有多大的概率上下翻转,fliplr表示有多大比例左右翻转,bgr表示训练图像有多大比例在训练之前首先把RGB通道顺序的图像转换为BGR通道顺序。三个参数均用于基于现有图像提升图像的多样化和模型的泛化能力。 高级混合增强参数部分的参数名、默认值及其解释说明如下所示: !image.png mosaic参数用于控制在训练过程以多大的比例开启mosaic增强。mosaic增强是指把四张训练图片缩放、裁切等处理后按照2x2的方式拼接为一张图像送入模型进行训练。该参数设置为1.0表示100%的训练数据都使用mosaic拼接处理后的图片,设置为0表示禁用mosaic增强,0-1之间表示训练数据采用mosaic增强处理的比例。 mixup参数用于控制多大比例的训练图片在训练之前首先进行mixup增强处理。mixup是指通过两张图片的像素值大小,乘以随机大小的比例,把两张图片的像素叠加在一起形成一张图片(两张图片的标签置信度也按照相同的比例处理),送入模型进行训练。cutmix参数用于多大比例的训练图片在进行训练之前首先进行cutmix增强处理。cutmix增强处理是指从一张图片随机裁切出来一部分矩形区域,粘贴到另外一张图片上,形成新的训练图像。 copy_paste和copy_paste_mode这两个参数仅用于图像分割模型。 auto_augment和earsing这两个参数仅用于图像分类模型。 配置文件相关参数 如果通过命令行或者在此处设置cfg参数,在YOLO模型的各项任务执行时,会把此处配置的yaml格式的cfg配置文件中的参数合并到default.yaml中。同名参数会以cfg传递的配置文件中的参数优先。 此外如之前部分对于mode中的track模式选项所解释的,track模式用于在detect模型上启用跟踪模式。此时需要在此通过tracker参数来指定跟踪模式运行的跟踪配置文件: tracker参数对应的配置文件所在的目录为ultralytics\cfg\trackers,当前版本中包含两种跟踪模式:botsort和bytetrack。前者是默认的跟踪模式,跟踪精度更高;后者主要针对实时性要求较高的跟踪类型,跟踪计算的速度更快。

YOLOv8模型网络架构解读

本文从YOLOv8和YOLOv5模型对比的角度,来详细阐述和总结两个模型在网络架构设计上的异同点,从而对YOLOv8模型的结构有比较全面和深入的理解。 以下是YOLOv5模型的总体架构图: !image.png 以下是YOLOv8模型的整体网络架构图: !image.png 主干网络 对比以上YOLOv5和YOLOv8架构图左侧的主干网络BackBone部分,可以看到这两个模型的主干网络的整体结构非常类似。区别主要只是以下两点: - 在P1层:YOLOv5使用的是FOCUS模块(老版本)或者6x6,步长为2的卷积层(新版本)实现输入图像640x640x3到320x320x64特征图的初始下采样处理;而YOLOv8模型使用的与主干网络中其他降维模块相同的3x3,步长为2的卷积层实现分辨率的降低,输入仍然是相同的320x320x64特征图。 - YOLOv8模型使用C2f模块替换了YOLOv5模型的CSPLayer模块(也就是C3模块),而C2f模块是YOLOv8的核心创新之一。 C2f模块 如上所述,YOLOv8主干网络中的C2f模块是其架构设计上的最大创新之一。首先对比YOLOv5的C3模块与YOLOv8的C2f模块在结构上的差异: !image.png 可以看到,C3模块采用的是双分支并行结构,将输入的特征图分为两部分分别给主分支和旁路分支分别处理: - 主分支:通过1×1卷积降维后,经过多个Bottleneck残差模块(残差模块数量可以通过depth_multiple参数设置)进行特征提取。 - 旁路分支:直接通过1×1卷积处理,以保持原始的特征信息。 - 最后进行特征融合:最后把两个分支的输出在通道维度直接拼接起来,再通过1×1卷积融合后输出。 而C2f模块采用的是多阶段串联结构,其设计看起来更加复杂一些: - 初始处理:输入的特征图首先统一通过1×1卷积生成中间特征图。 - 特征拆分:将中间特征图拆分为两部分,一部分特征通过多个Bottleneck模块串联处理,另一部分直接传递到末端。 - 多阶段处理:后续连续多个Bottleneck残差模块,每个残差模块的输出都分为两部分,一部分继续在下游的残差模块中进行处理,另外一部分直接传递到末端合并。 - 多尺度融合:最后将所有处理阶段的特征图拼接在一起,再通过1×1卷积融合。 比较以上两者的结构,C2f相比C3最大的差异就在于,C2f模块通过把内部残差模块输出的多阶段特征图拼接在一起,融合了包含中间处理阶段在内的不同深度的特征信息,特征更丰富;此外通过引入更多的跨层连接,形成更丰富的梯度流设计,使得在训练过程中梯度信息能够更有效地在网络中反向传播,缓解了深层网络的梯度消失问题,提升了训练稳定性和收敛速度。 但无论是C3模块还是C2f模块,其输出和输出特征图信息的维度都是相同的。 与YOLOv5模型相同,YOLOv8模块也是在8x,16x以及32x三个维度上分别输出三路不同分辨率的特征信息给颈部网络:80x80x256,40x40x512,20x20x512(YOLOv5默认的最小分辨率特征输出维度为20x20x1024)。 颈部网络 在颈部网络方面两者的区别不大,YOLOv8在YOLOv5新版本的基础上做了一些简化,但是整体的结构基本上是一致的:都是采用PANET的结构,通过自顶向下和自底向上的路径聚合,增强了主干网络输出的三种不同尺度特征之间的信息流动,最终把充分融合后的特征信息输出到检测头部分。 YOLOv8相比YOLOv5版本在颈部网络上的改进主要是: - 简化网络结构,去掉了上采样UpSample路径中的两个卷积块。 - 与主干网络相同,在颈部网络中同样使用C2f模块替换掉C3模块。 在颈部网络中对于三种不同尺度的特征信息进行各种融合处理后,YOLOv8模型最终从颈部网络输出三路不同尺度的特征信息给检测头部分:80x80x256,40x40x512,20x20x512。 检测头 相比于前面的主干网络和颈部网络,YOLOv8模型在检测头部分相比YOLOv5就做出了革命性的改进,主要体现在解耦头(Decoupled Head)和Anchor-Free机制这两个方面。 在YOLOv5网络的检测头设计上,其采用了耦合头(Coupled Head)的结构,即针对类别判断的分类任务和针对预测框坐标的回归任务共享同一组卷积层,最后在输出上通过不同的输出通道分别预测类别和边界框坐标。而YOLOv8的检测头采用了解耦头(Decoupled Head)的设计思路,将分类和回归任务完全分离为两个独立的网络分支,分类分支专门负责目标类别判断,使用独立的卷积层处理特征;回归分支专注于边界框位置预测,通过专门的卷积层输出坐标信息,避免两个任务目标在训练的过程中相互影响。两者的对比如下: !image.png 从上图可以看到,对于YOLOv8的解耦检测头的设计而言,分类分支和回归分支分别包含各自独立的两个3x3,步长为1的卷积模块的处理,最后再通过一个1x1卷积按照各自输出信息的维度进行降维处理。对于COCO数据集而言,模型针对一张图片所进行的推理,在检测头部分最终的输出数据维度为[1,8400,144]。 其中的1表示batch size,因为是对一张图片进行处理所以batch size为1。第二个8400表示从颈部网络送入检测头的三种不同尺度的特征信息,这些特征图上所有网格点的总和就是80x80+40x40+20x20=8400,特征图上的每一个网格点输出一个一维的张量信息,表示在这个网格点上的检测结果。最后的144可以分为两个部分:类别判断部分的长度为80,对应与COCO数据集的80个类别,类别分数越高,表示该网格点检测到的物体类别越有可能是什么;边界框位置检测部分长度为64,这个64的长度就与YOLOv8检测头的Anchor-Free机制相关。 从YOLOv2模型开始,YOLO的前几代都使用了基于Anchor的预测框机制,YOLOv8则采用了Anchor-free的设计,不再依赖于基于数据集聚类出来的长宽比来辅助定位物体。YOLOv8 Anchor-Free模式的实现则需要依赖于(DFL, Distribution Focal Loss)机制。 所谓的DFL机制,就是在预测框分支路径所输出的每个网格中,包含一个由这个网格检测到的物体预测框的坐标位置。以当前网格的中心点在原图分辨率(640x640)中的位置坐标(x,y)为基准,目标坐标位置由四个偏移量l(left),t(top),r(right),b(bottom)四个偏移量来进行计算,得到的最终预测框的坐标位置左上角是(x-left,y-top),右下角是(x+right,y+bottom)。l,t,r,b这四个偏移量中每一个使用16维的向量来表示,四个偏移量就是64维向量,这就是预测框分支路径中针对每个网格的输出数据。 例如对于P3层(stride=8)这一路的特征输出而言,锚点网格索引为 (10, 20)的情况下,该锚点网格的中心点在原图中的坐标就是((i+0.5)×stride, (j+0.5)×stride)=(84,164)。同样的道理,对于P4和P5的特征输出而言,就需要把stride分别修改为16和32来计算其锚点网格在原图中的坐标位置。这个锚点坐标位置加上l,t,r,b这四个偏移量就是一个检测框的左上角和右下角在原图中的位置输出。 那么对于l,t,r,b这四个偏移量而言,如何使用它的16维向量来进行计算呢? 这涉及到两个参数: - reg_max:通常为16,表示将偏移量的距离范围从0到最大距离均分为16个区间。 - stride:特征层的步长(P3=8, P4=16, P5=32) 以P3层为例解释偏移量的计算逻辑,其stride=8,能够支持的最大偏移量是16x8=128像素,这个128像素被按照8像素的间隔分为16段: !image.png 预测框回归分支针对每个偏移量输出的16维向量表示实际偏移量落在这16段各段范围内的概率。最终计算出来的实际偏移量是这16个概率与16段中心点的加权平均值(而不是取最大概率对应的那一段中心点),以下是偏移量的计算逻辑流程: !image.png 按照以上逻辑就可以计算出来四个偏移量,然后与锚点网格的中心点一起,就可以计算出来这个锚点网格检测出来的物体预测框的左上角和右下角的坐标。 对于P4和P5层的特征输出而言,偏移量的概率输出逻辑和计算逻辑是完全相同的,只不过stride分别变成16和32而已。 按照以上逻辑,YOLOv8模型最终就从检测头部分输出8400个预测框信息。这么多的预测框后续还要再经过NMS算法的过滤才会作为模型最终的检测输出。 至此基本上算是把检测头部分的解耦输出算是彻底搞清楚了:对于P3、P4、P5这三路不同尺度的特征输出而言,在检测头部分解耦分为两个分支,类别判断的分支以及预测框坐标的分支。对于COCO数据集而言,前者的每个网格输出了80维度的类别判断信息;后者则使用64维长度的向量表示出来这个预测框检测到的物体的坐标位置信息。所以每个网格的总的输出维度的长度是144。 但是目标检测置信度信息呢? 答案是,YOLOv8没有独立的置信度预测分支,而是把目标检测的置信度信息隐藏在类别预测信息中。最终每个网格锚点输出的目标检测置信度就是其类别预测分支输出的80维张量中的最大值,即可能性最大的那个类别的识别概率,当成该锚点存在目标的置信度。 参考资料 - 【YOLOv8】YOLOv8结构解读-腾讯云开发者社区-腾讯云 - What is YOLOv8? A Complete Guide - YOLOv8 原理和实现全解析 — MMYOLO 0.6.0 文档

PyTorch中的自动微分特性与计算图解释

本文基于对Pytorch基础基础数据结构torch的学习,整理出来torch的梯度与自动微分特性背后的理论,并通过示例说明来阐述清楚基于Pytorch框架计算图以及训练流程的运行逻辑。 Torch的梯度与自动微分 PyTorch框架中的 torch.Tensor是其核心数据结构。该数据结构的设计核心在于,其不仅用于承载计算过程中的各种数据,还内置了自动微分机制,这样就使得深度学习模型的训练变得高效而灵活。而_深入的理解torch.Tensor类型及其梯度、自动微分的机制,理解深度学习以及使用Pytorch进行深度学习模型开发的基础所在_。 PyTorch中的张量(Tensor)是一个多维数组,在概念上与NumPy的ndarray类似,这两者的关键区别在于PyTorch张量能够支持在GPU上的加速计算,以及自动微分特性。 以下简单的总结PyTorch框架中torch.Tensor类型中所包含的核心属性。 data data是存储张量包含的实际数据。这一点与Numpy的ndarray没有区别,连计算和声明的接口API都非常类似。 device torch.Tensor通过deice属性指示张量存储在哪个设备上,例如CPU('cpu')或GPU(‘cuda’)。可以在创建Tensor时通过device属性指定张量数据的保存位置,在程序的运行中,也可以通过该变量的to()、cpu()、cuda()等接口在不同设备之间转移存储位置。 需要注意的是:所有参与运算的张量必须位于同一个设备上。尝试对不同设备(如一个在CPU,一个在GPU)上的张量进行运算会导致运行时错误(RuntimeError)。 dtype dtype属性用于表示张量中保存的实际数据的数据类型,例如torch.float32、torch.int64、torch.int32等。默认数据类型是32位浮点数(torch.float32)。 需要注意,因为模型的训练过程通常需要浮点数提供的精度和动态范围,所以只有浮点数类型的张量才能计算梯度。所以一般是在网络的构建和训练过程中使用浮点类型的张量,训练结束后再通过量化过程转换为INT类型的张量,以方便模型的部署。 requires_grad requires_grad是一个布尔属性,用于决定该张量在计算过程中是否参与梯度计算。当设置为True时,PyTorch会在程序的运行中会跟踪在该张量上执行的所有操作,从而构建一个计算图,为后续的梯度计算做准备。 默认情况下,叶子节点(即用户创建的Tensor张量)的requires_grad属性为False,也可通过requires_grad属性手动设置。而非叶子节点(即基于叶子节点的中间计算结果张量)的requires_grad由计算过程自动推断,如果操作数中任意一个需要梯度,其计算结果对应的非叶子节点就需要梯度。 总而言之,torch.Tensor的requires_grad及其相关的梯度相关属性是神经网络通过反向传播、更新参数、最终完成模型训练过程的核心所在。 grad grad属性用于存储当前torch.Tensor在反向计算过程(即调用backward函数的计算过程)中得到的梯度值。在执行模型的反向传播计算时,叶子张量(由用户直接创建,而非运算结果产生的中间张量)的梯度会累积在这里。 在Tensor的初始状态或者requires_grad=False的情况下,该属性为None。每次对计算图执行backward()时,计算结果会累加到张量的grad属性,而不是替换。这就是为什么在训练循环中必须手动进行梯度清零的原因所在:optimizer.zero_grad()。 在执行模型的反向传播backward计算时,默认情况下只有叶子张量的grad属性会被自动填充。而由运算产生的中间张量(非叶子节点)的grad属性默认始终为None,这样做主要是为了节省内存,因为它们在参数更新中通常不起直接作用。如果需要保留中间节点的梯度,可以在该张量上调用retain_grad()方法。 grad_fn grad_fn属性指向一个Function对象,该对象记录了创建此张量时所进行的操作。模型中各个节点的grad_fn属性实际上是计算图中的边,记录了模型从输入到输出的完整计算历史,构建出了对应于模型的动态计算图,因此是自动求导(Autograd)系统能够反向传播梯度的关键。 对于用户直接创建的叶子张量而言,其grad_fn始终为None。对于非叶子节点,其grad_fn属性为一个具体的反向传播函数对象,其函数名称(如 AddBackward, MulBackward等)指明了创建该张量的前向运算类型。 代码示例如下: 对应的打印信息: - 以上的UserWarning部分的打印信息,表示在打印非叶子节点的梯度信息时报错。为了节省内存,非叶子节点的梯度在运算中不会自动保存。 - z 的grad_fn属性为以PowBackward0命名的函数,表示 z 的来源是一个指数函数。 - 在执行backward操作之前,叶子节点 x 的梯度grad始终为None,执行以后为8.0。这是因为z=x^2 ,所以 z 对 x 的导数计算式就是 2x ,x 节点的梯度计算结果就是8.0。导数计算的相关理论可参考深度学习的数学基础知识总结。 计算图与自动微分 PyTorch的自动微分特性是其作为深度学习框架的核心竞争力,因此基于PyTorch进行模型的训练和推理的流程,就只需要关注前向传播的执行逻辑,PyTorch框架会自动完成梯度的计算,然后在反向传播的过程中基于各个参数梯度的大小去更新参数,达到逐步收敛的状态。如下图所示。 !image.png 基于以上的正向传导和反向传导流程,可以看到,完整的模型计算图和PyTorch框架自动微分的核心组件包括: - 可携带梯度信息的torch.Tensor数据结构。如上所述,在requires_grad设置为True的情况下,该数据结构会维护自己的梯度信息(grad属性),而且在该数据结构上的操作(grad_fn)会被框架自动追踪以构建完整的计算图。 - 计算图。PyTorch框架在模型代码的前向运算(forward)的过程中动态生成有向无环计算图,这个计算图记录了从输入到输出的完整计算过程。 - 自动微分引擎。在模型上调用反向传播函数(backward)时,PyTorch内部维护的自动微分引擎会沿着计算图反向执行每个Function的backward()方法,其作用是计算各个节点的梯度,然后把计算出来的梯度写入该节点Tensor对应的grad属性中。 - 优化器。优化器负责基于当前各个节点的梯度信息、模型超参数所设置的学习率等参数,对各个节点的参数值进行更新。 深度学习模型的训练过程,本质上是自动微分在背后驱动着参数的迭代优化。如下就是整个模型前向和反向传播的过程中,基于计算图计算和更新各个节点的梯度信息,并且使用优化器对各个节点的参数不断进行更新以达到收敛状态的完整过程。 - 前向传播(forward)构建计算图:在对requires_grad=True的张量(通常是模型的参数和输入数据)进行操作时,PyTorch框架会在后台动态地构建出来一张完整的计算图。这个计算图记录了从输入数据到最终损失函数(一个标量)的所有操作步骤。 - 执行反向传播(backward):在得到损失值(一个标量)后,调用loss.backward()。该操作会启动Autograd引擎,从根节点出发,反向遍历计算图。对于图中的每一个节点,引擎会调用其预定义的backward()方法,计算该操作对输入的局部梯度,并将梯度乘以来自输出端的梯度(链式法则),然后传递给输入节点。 - 梯度累积与更新:以上的反向传播过程最终的执行结果,就是把计算得到的梯度累积到各个叶子张量(即模型参数)的 .grad属性中 。 - 因为是累积,所以在每次参数更新前,需要调用optimizer.zero_grad()将先把各个节点的梯度清零,防止上一次的梯度影响本次计算的更新。 - 参数更新:最后一步就是使用优化器(如 torch.optim.SGD),根据前一步骤计算出来的各个参数的.grad属性来执行更新参数的操作(optimizer.step),从而一步步减小损失,最终达到收敛的状态。 完整示例 以下基于Pytorch实现手写数字识别的卷积神经网络一文中实现的一个识别MNIST数据集的简单网络的训练流程进行说明。 以上简单的训练流程中可以清楚的看到PyTorch的自动微分与计算图特性在模型训练过程中所起的作用和执行步骤: - output = net(data):在模型中对输入数据执行一次前向计算,得到输出结果。在这个前向计算的流程中,也一步步构建出来整个模型的计算图。 - loss = criterion(output, target):基于上一步的计算输出与target的标签输出计算出来一个误差标量。 - optimizer.zero_grad() :一定要在执行反向计算之前把整个网络中所有参数的梯度全部清零,避免梯度累加。 - loss.backward():基于误差标量执行一次反向计算,这个反向计算基于计算图从输出到模型的各个叶子节点,把各个节点的梯度写入其grad属性中。 - optimizer.step():最后调用优化器的step接口,以各个节点的梯度(grad属性)、学习率等参数,更新节点参数。本次训练对参数的更新至此结束。 参考资料 - 《PyTorch深度学习应用实战》陈昭明,洪锦魁 3-3 自动微分

YOLOv5模型的推理输出结果及其NMS算法解析

本文总结了YOLOv5模型对输入图片数据执行前向推理流程后,在其检测头部分输出的数据结构,以及在其输出数据上所执行的NMS算法流程。 YOLOv5模型的输出数据 在YOLOv5模型网络架构解读一文中详细整理了该模型的网络架构,对于分析和总结YOLOv5模型的输出数据而言,最重要的就是其中的检测头部分的结构: !image.png 从上图可以看到,YOLOv5采用了多尺度检测的机制,在检测头部分输出三个不同尺度的特征图,分别用于检测不同大小的目标。对于标准的640×640输入图像,其输出张量的形状为: - 大尺度特征图(P3层):[batch_size, 3, 80, 80, 5+NC] - 中尺度特征图(P4层):[batch_size, 3, 40, 40, 5+NC] - 小尺度特征图(P5层):[batch_size, 3, 20, 20, 5+NC] 每个输出张量的维度含义如下: - batch_size:批处理大小,表示同时处理的图像数量。 - 3:每个网格单元预测的锚框数量(anchor boxes),YOLOv5模型默认为3个。 - H, W:特征图的高度和宽度(如80×80、40×40、20×20)。YOLOv5模型输入为640x640,分别在8倍、16倍、32倍尺度输出特征图,对应的特征图分辨率就是80x80,40x40以及20x20。 - 5+NC:每个预测框的输出维度。 每个预测框的输出所包含的信息如下: !image.png 总结起来,对于P3层输出而言,共计输出80x80x3=19200个预测框;对于P4层输出,共计输出40x40x3=4800个预测框;对于P5层输出,共计输出20x20x3=1200个预测框。每个预测框都包含有(5+NC)长度的信息用于描述该预测框的信息。 NMS算法 如上所述,YOLOv5模型对一张图像进行的推理,最终会在三个尺寸维度上总共输出超过两万个预测框,其中通常会为同一个物体生成大量重叠的预测框。因此要经过过滤后才会输出真正的检测框,这个从模型输出张量数据过滤出来最终检测框的过程就是NMS算法。NMS算法的任务就是从重叠的框中筛选出最准确的一个,并抑制掉(删除)其他冗余的框,最终得到清晰、不重叠的检测结果。 NMS算法的基本执行流程如下: - 排序:将模型输出的所有预测框按照其置信度(Confidence Score)从高到低排序。 - 选取:选取置信度最高的框,将其添加到最终输出框的列表中。 - 抑制:以前一步骤选出来的最高置信度的预测框为基础,计算该框与剩余所有框的交并比(IoU)。删除那些IoU超过预设阈值(如0.5)的框,因为它们很可能指向的是同一个物体。 - 循环:重复步骤2和3,直到没有剩余的框。 YOLOv5在基础的NMS算法上做了一些改进: - 多尺度预测框的统一处理。因为YOLOv5模型的推理输出包括三个不同特征图(P3, P4, P5)的预测框,在进行NMS算法处理时就把所有的预测框合并在一起,再统一进行NMS过滤。在这种情况下,无论一个预测框是来自负责小目标的P3层,还是来自负责大目标的P5层,只要它的置信度高,它就有机会被保留。最终选出来的就是针对这个目标的置信度最高的框。 - 可支持多标签与单标签检测。该选项通过multi_label参数进行控制。在multi_label=False的情况下(单标签),每个预测框只属于一个置信度最高的类别,这是最常见的目标检测模式。而如果multi_label=True(多标签),一个预测框可以同时属于多个类别(例如,一个物体既是汽车又是出租车),在具体实现上,它会将一个原始预测框根据超过阈值的类别数拆分成多个检测条目。 - Merge-NMS(加权框融合)。该步骤是一个可选的后处理步骤,通过merge参数控制。对于IoU超过阈值的预测框,不是删除低置信度的框,而是将它们与高置信度的框进行加权平均。此举理论上可以提升定位的精度,因为融合了多个框的位置信息。 non_max_suppression源代码解读 以下结合YOLOv5源代码中的non_max_suppression函数来解释清楚对模型输出所进行的NMS过滤算法的执行逻辑和流程。 > YOLOv5源代码中的non_max_suppression函数在utils/general.py文件中。 non_max_suppression函数从YOLOv5模型得到的prediction预测数据是一个[batch_size, num_boxes, 5 + nc]结构的张量。其中的batch_size是一次送入模型进行推理的图片数量,则num_boxes对应于每张图片推理结果的预测框总数25200(19200+4800+1200),5+nc表示每个预测框的预测输出数据(center_x, center_y, width, height, object_confidence,class_1,...,class_n)。 以下对于non_max_suppression函数实现的重点流程代码截取出来进行解释。 以上就是YOLOv5源代码中non_max_suppression函数的声明,其中: - prediction:模型推理输出的原始预测数据,包含了大中小三种尺度的所有检测框的信息。 - conf_thres:置信度阈值,用于在执行NMS算法时控制和过滤低置信度的检测框。只有当检测框的置信度(包括物体置信度和类别置信度)大于此阈值时,才会被保留进行后续处理。提高此值会减少检测框数量,但可能会遗漏一些真实物体;降低此值会增加检测框数量,但可能引入更多误检。基于对精确度和召回率的要求设置该值。 - iou_thres:IoU(交并比)阈值,用于NMS算法中判断边界框是否重叠。当两个检测框的IoU大于此阈值时,NMS会保留置信度较高的那个,移除置信度较低的那个。提高此值会保留更多重叠的检测框;降低此值会更积极地移除重叠框。 - classes:指定只保留特定类别的检测结果。如果设置为某个类别索引列表,函数将只返回这些类别的检测框,其他类别会被过滤掉。 - agnostic:控制是否进行类别无关的NMS处理。 - 当为False(默认)时,NMS在每个类别内部独立执行,不同类别的重叠框不会相互抑制。 - 当为True时,NMS将所有类别视为同一类,重叠的框会相互抑制,不管它们的类别如何。 - multi_label:控制是否允许多标签检测(即一个物体可能属于多个类别,如检测结果同时是人和男人)。 - 设置为False(默认)时,每个检测框只保留置信度最高的一个类别。 - 设置为True时,一个检测框可以保留所有置信度大于conf_thres阈值的类别。 - labels:用于自动标注模式,使用该参数传入先验标签信息。 - max_det:限制每张图像最多输出的检测框数量。默认情况下一张图像最多输出300个有效检测框。 - nm:指定掩码的数量,只用于分割模型。 从参数prediction中读取出来批量大小bs(batch size),模型可以一次对多张图像预测,其预测结果张量的第一个维度就是图像的张数bs。nc是模型输出的类别数量,其中的nm仅用于分割模型,对于常用的目标检测模型而言nm为0。xc是比较prediction输出的所有检测框的置信度与参数传递的置信度阈值conf_thres,置信度大于阈值的框被标记为true,否则为false。实际上prediction输出的绝大多数预测框都在置信度阈值比较这一步被过滤掉了。 声明一个output变量用于保存后续NMS过滤后的有效检测框,每个检测框的检测信息长度都是6(坐标位置+置信度+类别)。然后用一个for循环从prediction变量中取出每一帧图像进行单独处理,后续的操作都是在这个for循环中针对一帧图像的过滤。 通过x = x[xc[xi]]就会在x中仅保留第xi张图片置信度大于conf_thres阈值的检测框,其他检测框都被去掉了。然后判断x列表中的检测框数量是否为0,为0的话就表示当前这一帧图像中没有检测到有效的目标,直接继续对下一帧图像进行处理。 此时,x列表中保存的就是当前图像帧中检测到的所有置信度>conf_thres阈值的检测框信息。 对x列表中的所有检测框的信息进行处理:给所有类别的判断概率乘以整个检测目标的置信率,得到各个类别的绝对概率;把模型输出的xywh坐标信息转换为nms算法所需要的xyxy格式。 以上代码整理给nms算法所需要的检测框列表信息,并且利用类别的绝对概率进行一轮过滤。此时分为多label和单label两种情况: - 多label的情况下,比较各个类别的绝对概率与置信度阈值conf_thres,保留所有绝对概率大于阈值的类别。 - 单label的情况下,从所有类别中找出绝对概率最大的那个类别,然后与置信度阈值conf_thres进行比较。大于阈值的情况下保留。 此时的x列表中的每个检测框中只包含有一个类别,其格式为[x1,y1,x2,y2,confidence,class]。如果一个检测框同时支持两个类别的话(multi_label开启的情况下),就会在x列表中有两个独立的检测框。 如果设置classes参数的话,就在x列表中仅保留classes所指定的类别列表,其他检测框信息全部过滤掉。然后再检查一下当前剩下的检测框数量,如果等于0的话直接进行下一帧图像的处理。 以上这部分就到了NMS算法的核心处理步骤了。首先对x列表中的所有检测框按照置信度进行降序排列,并且只保留前max_num个检测框;然后根据agnostic参数的设置,决定后续的nms过滤是否需要区分不同类别,在agnostic=false的情况下,给其类别乘一个很大的数字max_wh(默认为7680),确保各个类别在不同的空间中进行计算,这样就不会在NMS过滤中相互影响了。 最后从x列表中分别取出各个检测框的坐标boxes及其检测置信度scores,然后调用torchvision.ops.nms执行NMS过滤。torchvision.ops.nms返回的是一个一维整型张量 ,其中包含了经过NMS算法处理后应该保留的边界框的索引,这些索引按照置信度从高到低的顺序排列。 以上代码对torchvision.ops.nms的返回结果仅保留前max_det个结果作为最终的输出。在merge功能开启的情况下,基于IOU的比较对最终输出检测框的坐标位置进行精调,提升坐标位置的定位精度。 最后一步就是把NMS过滤后的检测框列表赋值给output作为non_max_suppression函数的最终输出。 参考资料 - yolov5 极大值抑制 nms 代码详解 - 金色旭光 - 博客园

机器视觉目标检测模型的P、R、PR、F1指标详解

YOLOv5模型训练结果文件中的F1_curve、P_curve、PR_curve、R_curve这四张图片文件,分别对应于模型训练完成后的F1、P、PR以及R指标的曲线图。本文对这四个指标进行了详细的总结和整理。 P_Curve P曲线对应于模型的精确率(Precision),该指标衡量的是模型预测为正例的样本中,有多少是真正的正例。它关注的是预测结果的准确性,即“查得准不准”。精确率越高,说明模型误检和误判(将背景或其他物体误认为目标)的情况越少。例如,在安防监控中,高精确率就意味着报警的准确性高,减少了误报的情况。下图是一张典型的P_Curve图: !image.png 可以看到,P_Curve图的纵坐标是精确度Precision,横坐标是置信度Confidence。置信度阈值提高,精确率通常呈现出上升的状态。这也是很好理解的事情:置信度越低,就有越多的误判目标被统计在内,这样自然会拉低精确度指标。而随着置信度阈值提高,大量置信度较低的检测结果被过滤掉,误判情况减少,精确度指标自然就上升了。 理想情况下的P_Curve曲线是一条Precision始终在1.0附近运行的直线,这意味着无论你如何设置置信度阈值,模型的预测都非常可靠,误检率很少。当然实际上这种情况是不可能出现的。绝大多数情况下,P_Curve都呈现出Precision随着置信度阈值Confidence迅速上升的曲线,直到某个点上曲线的纵坐标接近为1,并一直平缓运行。但无论如何,P_Curve曲线下所包围的面积越大(实际上也就意味着Precision能够越快达到接近1的程度),说明模型在不同阈值下都能保持较高的精确率,性能越稳健。 如果只有有个别类别的曲线(如上图中的crib曲线)远低于其他曲线,这可能意味着数据集中该类别的样本数量不足(实际上确实如此),或者样本质量较差,导致模型难以准确学习到该类别的特征,这时就需要考虑为该类别收集更多数据或进行数据增强。此外,正常情况下,P_Curve曲线应该是平滑上升的,而如果曲线出现剧烈波动或异常陡降,这可能就暗示训练过程中存在着过拟合或欠拟合的问题,需要做针对性的处理。 P_Curve曲线最主要的价值在于辅助科学地设置模型预测的置信度阈值,在精确率(Precision)和召回率(Recall)之间达到平衡,并且能够满足项目应用需求和目标。例如,在无法接受误检、追求高精度的应用场景中(如安全监控中的误报警成本很高),此时应该根据P_Curve曲线,从中选择一个高精确率(如0.95或更高)所对应的置信度阈值。这种情况下模型的输出会非常保守,只输出它极其确定的结果,但代价就是可能会漏掉一些不太确定的正样本,从而导致召回率降低。 R_Curve 与P曲线相对应的则是R曲线,即召回率(Recall)曲线。召回率衡量的是在所有真实正例中,有多少被模型成功预测出来了。因此,Recall指标关注的是模型发现正例的能力,即“查得全不全”。召回率越高,说明模型漏检(未能检测到真实目标)的情况越少。 与P曲线类似,R曲线的横坐标同样是置信度(Confidence),纵坐标则是模型的召回率(Recall)。因此它展示的是当模型输出的置信度阈值从0到1变化时,召回率是如何变化的。下图是一张典型的召回率曲线图: !image.png P_Curve曲线和R_Curve曲线都是以置信度阈值Confidence作为横坐标,但这两个指标相对比置信度阈值的变化则是刚好相反的。在R_Curve中,随着置信度阈值的提高,召回率会呈现出下降的状态。从直观上也是比较容易理解的:置信度越高,也就会导致越多的正例判定被过滤掉(因为置信度达不到阈值的要求),自然也就导致漏检增加、召回率降低了。 整个R_Curve在上图中的整体位置越高,曲线与两个坐标轴所包围的面积越大,说明模型在不同置信度阈值下都能保持较高的召回率,性能越稳健,漏检越少。 类似的,R曲线的主要价值也在于帮助科学地选择和设置置信度阈值,在满足应用需求的情况下平衡精确率(P)和召回率(R)。例如在追求高召回率、完全不能接受漏检的应用场景中(医疗诊断、自动驾驶中的障碍物检测等),此时就应该根据R曲线,选择一个可接受的高召回率(如0.95或更高)所对应的置信度阈值。 下图是P曲线与R曲线的对比和总结表: !image.png PR曲线 PR曲线(Precision-Recall Curve,精确率-召回率曲线)是评估目标检测模型性能的核心工具,它可以直观地展示模型在不同置信度阈值下,精确率(Precision)和召回率(Recall)之间的权衡关系。 PR曲线以召回率 (Recall)为横坐标,精确率 (Precision)为纵坐标。曲线上的每一个点都代表了在某个特定的置信度阈值下,模型所达到的精确率和召回率组合。下图是一个典型的PR曲线的示例: !image.png 针对某个特定类别的PR曲线的绘制流程如下: - 首先将模型在验证集上所有预测出来的边界框,按照其置信度分数从高到低进行排序。 - 然后遍历阈值,从最高置信度(如0.99)开始,逐步降低阈值(如0.98, 0.97, ..., 0.01)。在每个阈值下,将所有置信度高于该阈值的预测框视为正例,计算此时的精确率和召回率。 - 最后把计算出的所有的(Recall, Precision)点连接起来,就形成了PR曲线。 通常情况下,PR曲线的形态是一条从左上角开始逐渐向右下角下降的曲线。这意味着,随着召回率的提高(想检测出更多真实目标),精确率会逐渐下降(误检会增加)。而反之,如果想要提高精确率(减少误检),召回率就会下降(漏检会增加)。 在对两个模型的性能进行比较的时候(当然是使用不同的模型对相同的验证数据集进行的测试),如果模型A的PR曲线完全包住模型B的PR曲线,则可以断言模型A的性能优于模型B。而如果两条曲线发生交叉,则需要通过分别计算两条曲线与两个坐标轴之间的面积(这实际上就是AP指标)来比较两者的优劣。 PR曲线与AP/mAP指标的关系 PR曲线看上去直观,但是不是一个具体的数字,所以使用起来尤其是对不同的模型进行比较的时候,并不是很方便。所以基于PR曲线进一步引入了AP和mAP指标。 AP (Average Precision)指标是指PR曲线与两个坐标轴之间的面积。AP值越高,说明该模型在保持高召回率的同时也能维持较高的精确率,综合性能也就越好。当然,AP指标是针对单个检测类别的指标。 而大多数目标检测模型都支持对多个不同类别的目标类型进行检测,因此要比较模型的整体优劣,就要评估所有类别目标的平均AP指标,这个平均AP指标就是mAP(mean Average Precision)指标。mAP指标是目标检测模型经过训练后所支持的所有类别AP值的平均值,也是目前目标检测领域最权威、最常用的评估指标。 F1曲线 完整理解了以上的P曲线和R曲线,F1曲线就比较好理解了。F1分数是精确率(Precision)和召回率(Recall)的调和平均数,用于综合评估模型的性能,其计算公式为: 因为Precision和Recall指标都是介于0-1之间的小数,所以以上公式计算出来的F1分数的取值范围也在0到1之间。F1分数的值越高表示模型在精确率和召回率上的综合表现越好。下图是一张典型的F1曲线图: !image.png 从上图可以看到,F1曲线图中同样以置信度阈值Confidence作为其横坐标,纵坐标则是不同置信度情况下的F1分数。正常情况下,F1曲线通常呈现一个先上升后下降的山峰形状。在较低的置信度阈值下,模型会预测出很多结果(高召回率R),但其中包含大量误检(低精确率P),因此导致F1分数在低置信度情况下较低。随着阈值提高,误检减少,精确率上升,F1分数也随之上升。当达到某个峰值后,继续提高阈值会过滤掉一些正确的预测(低召回率),从而导致F1分数下降。 - 曲线形态高且宽:如果F1曲线的两侧很陡峭,并且在较高的位置形成一个宽阔的平台,说明模型在很宽的置信度范围内都能保持较高的性能,模型非常稳健。这就是非常理想的F1曲线的形态。 - 曲线形态低且窄:如果F1曲线的整体位置较低,且峰值尖锐,那就说明模型只有在非常特定的阈值下才表现尚可,整体的泛化能力较差。 整体来讲,F1曲线与P曲线(Precision-Confidence)和R曲线(Recall-Confidence)密切相关,它们共同构成了一个完整的评估体系。三者结合起来可以根据应用的具体需求,辅助更为科学的选择出来模型推理的最佳置信度阈值: - 在追求高精确率(避免误检)的应用需求中,可以选择峰值点右侧的更高阈值,牺牲一些召回率来换取更高的精确率。 - 在追求高召回率(避免漏检)的应用需求中,可以选择峰值点左侧的更低阈值,牺牲一些精确率来换取更高的召回率。

YOLO模型训练生成文件解读

本文对YOLO模型基于自定义数据集在训练过程中所生成的文件进行解读,帮助建立对YOLO模型训练流程更加深入的理解。 如一文总结YOLOv5模型的自定义训练、测试与模型导出的全过程所总结的,使用train.py脚本基于自定义的数据集对YOLOv5模型训练后,会在YOLOv5的源代码目录下的runs/train/exp目录(exp是默认的目录名称,如多次训练,可能会生成 exp2, exp3等)下生成诸多训练结果文件。本文从功能分类的角度对这些文件及其文件夹的内容进行详细的解释。 !image.png weights/模型权重文件目录 exp目录下的weights子目录是最重要的输出,其中保存了本次训练好的模型权重文件。包含有last.pt和best.pt两个文件。 best.pt文件是本次训练过程所生成的最佳模型权重文件。这里所谓的最佳,是指在验证数据集上表现最好的模型权重文件。在train.py训练的过程中,每个epoch结束后都会在验证集上使用mAP0.5指标评估模型的性能。如果当前epoch的性能超过了之前的最佳记录,就会使用当前epoch的权重参数更新best.pt文件。而best.pt也是实践中最常用的模型文件,因为它代表了训练过程中的最佳性能。实际生产流程中会把该模型文件导出为ONNX、TensorRT等格式,在生产环境中进行部署。 last.pt文件则是模型训练时最后一个epoch的模型权重文件。无论使用验证集进行验证的性能如何,在每个epoch训练结束后都会把当前的模型权重信息更新一个last.pt文件,确保其中始终保存最新的训练状态。last.pt存在的意义是,当训练过程意外中断或者我们希望在当前训练的模型基础上继续进行训练时,可以基于last.pt文件基于当前的状态继续进行训练。此外,使用last.pt文件还可以分析训练过程中的收敛情况,观察最终训练状态。 训练结果指标 YOLOv5模型训练完成后的训练结果指标对应于results.png和results.csv两个文件,分别对应于训练过程中每个epoch的关键指标数据及其图表。 results.png 下图是results.png的一个示例: !image.png 可以看到在results.png上显示了模型的多个指标: - 训练集/数据集(box_loss,obj_loss,cls_loss):分别对应训练集和数据集上的BOX坐标位置损失,置信度损失以及分类损失。 - 在理想情况下,所有的损失曲线都平稳下降,并且最终保持在一个较低的水平。训练损失略低于验证损失是正常的。 - 过拟合:如果训练损失持续下降,但验证损失在某个点后出现开始上升的情况,说明模型过拟合了训练数据。(上图中的置信度损失就是这种情况)。此时应该尝试增加训练的数据量、降低模型的复杂度或者减少训练的轮次数量。 - 欠拟合:如果训练和验证损失都处于一个较高的水平且不再下降(比较典型的情况就是训练和验证损失都还处于较高水平时就进入了平台期,基本不再下降),说明模型可能欠拟合,需要更复杂的模型、更长的训练时间、适当调整超参数。 - 精度指标 (precision, recall, mAP@0.5, mAP@0.5:0.95): - Precision (精确率):表示模型预测为正例的样本中,有多少是真正的正例。该指标越高越好。 - Recall (召回率):表示所有真正的正例中,有多少被模型预测出来了。该指标越高越好。 - mAP@0.5:在IoU阈值为0.5时的平均精度均值。这是最常用的一个指标,越高越好。 - mAP@0.5:0.95:在IoU阈值从0.5到0.95(步长0.05)的平均精度均值。这是一个更严格的指标,能更好地反映模型定位的准确性。 以上提到的精度指标中,mAP@0.5指标主要衡量的是分类能力和基础的定位能力。只要定位框的位置大致正确,就能在该指标上得到高分。而实际的效果,一个模型可能有很高的mAP@0.5指标,但它的预测框可能很“飘”,不够精确。mAP@0.5:0.95相比之下则更为严格,该指标强制要求模型必须同时具备精确的分类和定位能力。要求模型不仅要能正确地识别出物体,还要能准确地框出物体的边界。一个模型如果mAP@0.5:0.95很高,说明它的预测框非常精准。 > mAP@0.5:0.95指标的计算逻辑是:首先在IoU阈值从 0.5 到 0.95 的范围内,以 0.05 为步长,计算10个不同的IoU阈值(即0.5, 0.55, 0.6, ..., 0.95);然后对每个IoU阈值,都计算一次该阈值对应的mAP指标;最后,将这10个mAP值取平均,就得到可最终的mAP@0.5:0.95指标。 results.csv 如下图所示,results.csv文件中的内容跟上面的results.png图表的内容基本一致,就是每个epoch的损失值和精度指标计算结果的列表。除了以上数据以外,results.csv中还报了三个参数x/lr0、x/lr1、x/lr2。 !image.png x/lr0、x/lr1、x/lr2这三个参数分别对应于YOLOv5模型主干部分、颈部网络、检测头(详情可参考YOLOv5模型网络架构解读一文)三个模块在训练过程中所使用的学习率。在YOLOv5模型的训练过程中,三个部分的学习率是各自独立的,而且每个部分的学习率也并非一成不变。为了帮助模型更好地收敛,训练过程中会使用学习率调度器(Learning Rate Scheduler)来动态调整学习率。x/lr0、x/lr1、x/lr2这三个不同参数组的学习率值,就会根据具体的调度策略(如余弦退火)在每个训练步骤(Step)或每个 Epoch 后发生调整和变化,results.csv文件中就记录了这些学习率值的变化情况。 混淆矩阵 confusion_matrix YOLOv5模型的训练结果文件中有一个confusion_matrix.png文件用于直观的显示,训练出来的模型在验证数据集上对各个类别的预测情况,因此主要用于评估模型的分类性能。 下图是一个典型的包含有6个分类的confusion_matrix.png文件: !image.png 对于以上文件的解读: - 对角线上的值,是模型正确预测样本结果的比例。值越高越好(对应的方块颜色越深),说明该类别能够被正确的识别。 - 非对角线上的值,表示错误预测所占的比例,即模型将真实类A的样本错误预测为类B的数量比例。值越低越好(对应方块颜色越浅),说明类别混淆的情况越少。 从上图可以看到,除了crib类别在判断中容易误判以外,其他类别的类别的混淆都很少。针对产生混淆较多的类别,就需要检查该类别容易与哪些类别之间产生混淆的情况(上图可以看到crib主要是与background产生混淆,此时就应该多提供一些能够区分这些不同类别的训练图片的数量,或者针对性的使用数据增强的方式,模拟两个类别容易产生混淆的场景并重新进行训练。 F1/P/R/PR指标图 YOLOv5模型每次训练完成后,都会在exp目录下生成P_curve、R_curve、PR_curve、F1_curve这四个指标的图形文件。关于这四个指标在机器视觉目标检测模型的P、R、PR、F1指标详解一文中已有非常详尽的介绍,在此不再赘述。 train_batch和val_batch图片文件 YOLOv5训练过程中,还会基于训练数据集生成三张训练样本图train_batch,以及基于验证数据集生成三张真实标签图val_batch_labels和三张验证集预测结果图val_batch_pred,共计9张图。如下所示: !image.png train_batch的三张图展示了训练集中不同批次的样本图像。YOLOv5在训练过程中,会自动对送入模型进行训练的图像进行数据增强操作,这些操作包括缩放、旋转、色彩调整以及拼接等。可以通过这三张图看到训练数据进行增强后的效果,每张图片上的真实边界框(Ground Truth)和类别标签,有助于了解训练数据的多样性。从以上的train_batch的图像中可以看到,YOLOv5模型在训练过程中采用了Mosaic数据增强的技术,其核心思想是:将四张不同的训练图片,通过随机缩放、裁剪和拼接的方式,组合成一张新的、更大的训练图像,然后缩放到模型要求的640x640分辨率图像送入模型进行训练。 val_batch_labels的三张图展示了验证图片集中不同批次的真实标签(Ground Truth)的效果,相当于是模型基于验证集进行测试的标准答案。这些验证集图片直接标注了真实存在的目标物体的类别,以及它们的具体位置。 val_batch_pred图片的显示内容与val_batch_labels的三张图相对应,展示出来针对同一批验证集图片,模型执行验证测试的过程中推理出来的目标物体的类别和位置。通过对比真实标签图val_batch_labels和预测结果图val_batch_pred,可以发现模型在哪些地方容易出错,分析模型的误检、漏检等错误模式。

YOLOv5源代码解读之Detect.py

本文对YOLOv5源代码中用于执行推理任务的detect.py脚本的源代码进行详细解读,完整理解从参数传递与解析、推理执行与结果输出的全过程。 YOLOv5源代码中的detect.py脚本专门用于在模型训练好之后执行推理(Inference)任务,也就是利用训练好的模型对新的图像、视频、实时流等数据进行预测,找出+标记其中感兴趣的目标并存储。 detect.py的典型调用方式 使用detect.py脚本执行基本的推理任务,最重要的传递参数有两个:通过--weights指定模型的pt文件,通过--source指定执行推理的目标任务(图片、视频还是流媒体)。 图片(目录)检测并显示结果: 视频文件检测,保存结果和检测数据: 使用UVC摄像头并只检测特定类别: 以上执行detect.py脚本,生成的结果文件会保存在YOLOv5源代码的runs/detect/exp目录(可以通过--project和--name参数进修改)下。 detect.py脚本的参数列表 与YOLO代码中的其他几个执行脚本相同,detect.py使用argparse包的ArgumentParser Class对诸多的运行参数进行管理。在调用该脚本时,首先需要传递对应的选项参数。 下图整理出来detect.py脚本支持的所有参数选项、默认值以及对该参数的功能解释: !image.png 对以上操作列表中部分不是很直观的参数详细解释如下: - 注意,绝大多数情况下,不需要传递--data参数。因为在--weights参数所传递的模型文件(.pt)中已经包含了进行目标检测和识别所必需的分类信息。如果通过--data强制使用指定的YAML配置文件中的names列表,会覆盖模型文件内嵌的类别信息,这反而会引起混淆。所以默认且推荐的方式是直接使用pt文件内置的分类信息,此时模型使用自身训练时的类别定义,行为正确且简洁。 - dnn参数:在某些情况下可能需要使用OPENCV中的dnn模块(而不是默认的pytorch算子)来执行推理流程的计算,以期望与现有的基于OPENCV开发的C++应用程序结合并产生更好的执行性能,此时可以尝试在推理时传递dnn参数,这样在推理过程中的底层计算就使用opencv的dnn而非pytorch。当然传递这个参数的前提是要保证传递的模型文件是onnx格式才行,其他模型文件格式不支持dnn参数。 - visualize参数:表示在执行推理的过程中,可以把YOLOv5主干网络各个网络层提取出来的特征图也保存下来,这样在通过观察和分析各层特征图来判断推理过程中存在的问题,更直观的了解卷积神经网络的工作流程和原理。 - augment参数:对应于YOLOv5推理过程中的TTA(Test Time Augmentation)操作。众所周知,训练过程中的Augmentation操作是通过对原始图片的随机缩放、剪裁、色彩变化等操作有效的扩展训练数据量,并提升模型的泛化能力,防止过拟合。而推理过程中对于一张待测图片,以类似的逻辑(如缩放、翻转操作)产生多张图片,分别送入模型执行推理操作,生成多个检测结果,然后把这些检测结果映射回原始的图像坐标空间,再通过NMS等步骤集成,从而得到更稳定、更准确的检测结果。当然,可以看到开启这个TTA操作以后,涉及到对对同一张图片的多次前向推理操作,肯定会显著增加单次推理的时间。 - update参数:我们完成自定义训练模型所生成的best.pt文件中,还包含有优化器状态、训练轮数、最近评估结果等信息,这些信息对于最终的部署没有意义。传递update参数后,detect.py在执行预测时会移除这些信息,生成一个只保留模型权重、模型架构定义和必要的元数据(如类别名称)的更干净的模型版本,这使得模型文件更精简,更适合最终部署和分享。当然对于自己训练出来的best.pt文件,也可以strip_optimizer('runs/train/exp/weights/best.pt')命令来精简模型。需要注意,使用--update参数清理模型文件中的优化器信息是不可逆的操作,处理后的模型文件将无法直接用于恢复训练或断点续训,因此在执行前请务必保留原始的训练权重副本。 推理执行流程代码解读 detect.py脚本中主要的函数只有两个:parse_opt用于实现对参数解析,run则用于执行具体的推理过程。 run函数所执行的整体推理过程如下图所示: !image.png 以下从代码执行层面,对detect.py脚本执行流程的重点部分进行总结。 模型的加载 加载模型部分的代码如下所示: 首先使用select_device函数基于--device参数的指定,从CPU、CUDA以及Apple的Metal Performance Shaders (MPS)选择对应的设备类型,根据设备类型设置模型推理运行的环境变量,并创建与之对应的torch.device对象,后续的模型推理流程就执行在这个torch.device上。 DetectMultiBackend就是模型的加载过程而已,之所以要使用这个单独的函数来加载模型,是因为这个推理过程可以支持多种不同的模型文件格式,并不限于训练生成的pt文件: DetectMultiBackend函数会根据--weights参数传递的模型文件名称或者url自动解析其对应的模型文件类型,然后加载到系统中,用于后续的推理流程中。 根据输入源创建dataloader 接下来就是根据--source参数所指定的输入源的类型不同创建对应的dataloader,而后续的推理流程就是不断地从dataloader中读取出来一帧帧的图像执行推理并保存推理结果。 --source所传递的输入源主要可以分为三种类型: - webcam:电脑上的PC摄像头,通过http、https、rtmp、rtsp等前缀指定的流媒体url。 - screenshot:当前电脑的截屏图像。 - 其他:普通图片、视频文件,以及目录。 对于以上三种类型的source,分别调用LoadStreams、LoadScreenshots、LoadImages返回一个dataloader类对象,后续的推理流程就是从这个对象中取出每一帧图像执行推理: 注意以上代码中的bs变量。该变量表示的是batch size,即每次送入模型执行推理的图片数量: - 对于webcam而言,batch size等于同时处理的摄像头或视频流数量(在--source参数中传递摄像头列表)。 - 对于screenshot而言,batch size固定为1,表示每次处理一帧屏幕截图图像。 - 对于通路径场景对应的oadImages类,batch size同样固定为1,表示每次通过模型对一张图片文件或者一帧视频图像进行预测。 推理流程 每一帧图像的推理可以分为三个子过程:图像的预处理,推理,以及对推理结果的NMS过滤。 在执行推理之前首先预热模型和创建三个用于统计各个子过程执行时间的Profile对象: 模型在之前的阶段已经加载,这个warmup操作的作用,是在执行真正的推理流程之前,首先向模型传递2-3个dummy图像执行几次forward操作,从而让模型的参数和运行状态在执行真正的推理之前达到最佳状态。当然,如果只使用detect.py推理一张图片的话,这个warmup就意义不大了,而如果推理连续的视频图像流,在启动推理之前执行一次warmup,可以让正式开启推理之前硬件处于良好的状态下,有利于加快连续视频流的推理。 此外,就是通过创建三个Profile(device=device)对象的dt数组,后续分别用于记录推流流程三个子过程的执行时间。 以下是推理流程三个子过程的执行流程代码,分别使用一个Profile对各个子过程的执行时间进行统计: - 图像预处理过程,主要是把图像格式转换为pytorch张量,拷贝到torch device上,然后把所有的像素数据除以255做归一化处理。 - 推理过程,对于普通的pt文件或者onnx格式的模型而言,实际上就是调用model()操作执行一次前向操作而已。 - NMS过程,对前一步推理过程所产生的推理结果pred执行NMS处理。 推理结果的解析 以上推理流程最后一步通过non_max_suppression()过滤后返回最终的目标检测结果pred。 pred的数据结构是一个PyTorch张量,其中包含有batch size(也就是上面的bs变量)个det数据结构。det同样是一个张量,表示在单张图像在进行目标检测的检测结构。det的格式为(n,6),表示这张图像中检测到n个目标,每个目标对应一个六元素的元组,按顺序描述如下: - x1,y1:表示检测目标的左上角坐标。 - x2,y2:表示检测目标的右下角坐标。 - conf:检测结构的置信度。 - cls:类别判断。 接下来的后处理阶段的代码就比较简单了,主要就是对NMS处理后的pred及其中包含的det数据结构解析,得到所有图片中检测到的各个不同目标的坐标位置、置信度以及类别判断等信息,标注在图像上或者保存在其他文件中。 以上代码首先对pred结构进行循环处理,每次处理对应其中一张图片的检测结果det。而对于每张图片的检测结果,可以按顺序分别得到检测对象的坐标位置、置信度和类别。当然,因为模型的输入固定是640x640分辨率,所以det结果输出的也是基于以上分辨率的坐标,此时要在原图上标注检测坐标信息,就需要通过scale_boxes把模型输出的坐标转换为原图的坐标。

安装Nvidia CUDA支持的Pytorch开发环境记录

操作系统:Windows 11 硬件:HP暗夜精灵Max,Ultra9 275HX+5070Ti 检查Nvidia显卡驱动版本 在终端输入nvidia-smi: 可以看到以上显卡驱动支持的CUDA版本最高是12.9。 - 如果以上命令提示找不到,就需重新安装Nvidia的显卡驱动:Download The Official NVIDIA Drivers | NVIDIA 安装Pytorch的GPU版本 在Pytorch官方页面Get Started根据当前的CUDA版本选择安装对应的torch版本:因为前面查到的当前CUDA版本是12.9,所以这里选择的版本应该是CUDA12.8的Pytorch版本,复制下面的安装命令安装pytorch library。 > CUDA版本的匹配规则:PyTorch的CUDA版本应≤驱动支持的版本 !image.png 测试验证Pytorch的环境 以上安装完成后使用下面的脚本测试当前环境是否已经安装好: 正确的情况下,上面脚本执行后的打印消息为: 参考资料 - PyTorch CUDA支持错误:解决方案与安装指南_文心快码

YOLOv5模型的配置文件详解

本文配合YOLOv5模型的架构图,对该模型的yaml配置文件各个部分的配置参数细节内容进行了详细的总结和解释,有助于更细致的理解模型不同规模版本之间的差异。 在YOLOv5之前的版本中,普遍采用cfg文件对其各种配置参数进行控制。从YOLOv5版本开始,正式采用了yaml配置文件格式。YOLOv5的配置文件在其源代码目录的models子目录下,该目录默认情况下包含有yolov5n.yaml、yolov5n.yaml、yolov5n.yaml、yolov5n.yaml、yolov5n.yaml共五个模型配置文件,分别对应各个不同规模的模型结构。 - 有关YOLOv5模型的不同版本之间的差异,可以参考YOLOv5模型的不同版本总结。 !image.png 从这个配置文件的结构上看,YOLOv5的配置参数信息可以分为全局参数部分和网络结构参数部分,而网络结构参数部分又可以分为backbone和head两个子部分。 全局参数 配置文件的全局参数部分如下所示: nc nc(number of classes)这个参数比较简单,就是用于定义模型需要检测的目标类别数量。因为YOLOv5模型默认是使用COCO数据集训练的,所以这里默认的80就是COCO数据集所支持的目标类别数量。如果要使用自定义数据集对YOLOv5模型进行从头训练的话,nc对应为自己数据集所支持的类别数量(在数据集配置文件中设置并覆盖网络模型配置文件中的nc设置值)。 depth_multiple depth_multiple是一个缩放因子,用于实现对网络结构中主干网路和颈部网络每个CSPBlock(在YOLOv5 v6.0中被称为C3模块)中所包含的残差块(Bottleneck模块)的数量的控制。下图是YOLOv5网络中CSPBlock的结构。这里的depth_multiple就表示下图中n(也就是DarknetBottleneck残差块的数量)的数量。每个CSPBlock中包含的DarknetBottleneck残差块的数量越多,自然就表示网络的深度越深,这就是depth的由来。 !image.png 与depth_multiple参数相关的计算代码如下: 其中的n是在配置文件中预设的模块重复次数,max()函数用于确保计算后的模块数是一个不小于1的整数。以YOLOv5s为例,主干网络部分第三个CSP Layer的配置为[-1, 9, C3, [512]],即这个CSP Layer中包含有连续9个残差块DarknetBottleneck,YOLOv5s的depth_multiple为0.33,因此在YOLOv5s中该层实际的残差块数量 = max(round(9 * 0.33), 1) = max(round(2.97), 1) = 3。这意味着,在YOLOv5s中,这个位置原本设计重复9次的C3模块,最终只会堆叠3次。 YOLOv5各种不同规模模型的depth_multiple参数及其对应的CSP Layer残差块的数量如下图所示: !image.png depth_multiple width_multiple的逻辑与depth_multiple类似,通用是以乘数因子的形式控制着网络中所有卷积层和C3模块(包括主干网络和颈部网络部分)的输出通道数(即特征图的卷积核数量)。在YOLOv5的模型构建过程中,width_multiple参数会与配置文件中的预设通道数相乘,然后通过make_divisible函数确保结果是8的倍数(为了GPU计算效率): 以YOLOv5s的第一个卷积层为例,在配置文件中的预设通道数:[64, 6, 2, 2](表示默认设置的输出通道数为64),YOLOv5s对应的width_multiple参数为0.50,在这种情况下,第一个卷积层实际输出的通道数为:64 × 0.50 = 32,这就意味着在YOLOv5s中,原本设计输出64个通道的卷积层,实际上只输出32个通道。 下图是YOLOv5各个不同规模模型的默认width_multiple参数: !image.png anchor 与YOLOv3和v4版本相同,YOLOv5版本同样通过Anchor锚框机制来预设的目标检测边界框尺寸,以帮助模型更准确地定位和识别目标。YOLOv5模型的主干网络输出大中小三种尺寸分辨率的特征图,每个特征图的每个网格会放三个不同尺寸和长宽比的anchor框: - 配置文件中的anchor配置信息总共三行,分别对应不同尺度的特征图:P3/8、P4/16、P5/32。 - 每行包含3组宽高值,每组代表一个anchor的[width, height]。 - 小特征图(P5/32)使用大anchor检测大目标,中特征图(P4/16)使用中等anchor检测中等目标,大特征图(P3/8)使用小anchor检测小目标 对比以上YOLOv5模型配置文件中提供的默认anchor参数,会发现其与YOLOv3-输出、Anchor与损失函数的进化总结中YOLOv3模型基于COCO数据集使用k-means机制统计出来的anchor框尺寸完全一致,也就是说,这个配置文件中所设置的anchor框的尺寸是基于COCO数据集的。 实际上在使用自定义数据集进行训练的时候,YOLOv5模型会首先使用一个Auto Learning Bounding Box Anchors的机制,检查自定义数据集的anchor先验框的尺寸与配置文件中提供的anchor参数的设置值(基于COCO数据集)是否基本匹配。如果匹配的话继续使用参数文件指定的默认anchor框进行训练,如果不匹配就会重新使用k-means方法计算这个自定义数据集自身的anchor,并在后续训练过程中使用自己计算的anchor。 与数据集配置选项的冲突 除了YOLOv5各个不同规模的模型配置文件之外,在调用train.py对模型进行自定义数据训练时,还需要设置自定义训练数据集的配置文件(数据集配置文件在data目录下)。而各个数据集的配置文件中也有nc或者anchors等配置选项,那么数据集的这些配置选项与模型的配置选项之间是什么关系呢? 答案是:YOLO训练脚本会优先采用数据配置文件中的配置选项。如果模型配置文件(如yolov5s.yaml)中的nc值与数据集配置文件中的nc选项不同,终端上会打印出警告信息:Overriding model.yaml nc=80 with nc=5。这表示程序正自动将模型输出层的类别数从默认值(例如COCO数据集的80类)覆盖成训练数据集的实际类别数(例如5类)。 在yolo.py源代码文件DetectionModel Class的实现中有以下代码,可以看到:在数据集配置文件中提供了nc和anchors参数的情况下,YOLO会使用数据集配置文件(如data/VOC.yaml)中的配置选项覆盖掉模型配置文件(如models/yolov5s.yaml)中的配置选项。 网络结构参数 YOLOv5的网络结构通过如下列表的形式对网络的每一层进行定义:[from, number, module, args]。以下对各个参数做详细解释和总结。 from用于指定当前层的输入来自哪一层: !b7e45d3d-2326-42fd-90b9-a1c3fbec9413.png 主干网络backbone的设置,每一层基本上都是从上一层接收数据,因此这里的from设置为-1就可以了。在head部分中定义的颈部网络结构,包含有很多类似 [-1, 6]这样的设置,这表示将上一层(通常是上采样后的结果)与骨干网络中的第6层(一个高分辨率的特征图)进行拼接,用于融合深层语义信息和浅层位置信息。 number参数定义了当前模块需要重复堆叠的次数。对于普通的卷积层Conv和SPPF这类不需要重复堆叠的结构,该参数设置为1。对于C3模块,该参数与前面详细解释的depth_multiple参数协同工作,最终的堆叠次数= number× depth_multiple (结果四舍五入取整,且至少为1)。 module参数指定了该网络层使用的网络模块。在YOLOv5网络结构的定义上用到的网络模块的类型如下图所示: !a84965e0-5355-429a-9b59-8c0836d384e2.png args通过列表的形式详细定义module所需要的模块参数。参数的具体含义取决于模块类型,其设置格式如下图所示。 !e57c69f2-d646-461b-b7b1-0a2fb3a08b22.png 在配置文件的网络结构参数部分,分别使用Backbone和Head两个段定义了主干网络和颈部网络的结构。 Backbone 主干网络结构的配置如下所示: 对比以上主干网络的配置,与主干网络的结构图就比较好理解了,整个网络结构由上到下: - 卷积块,6x6,stride=2,64通道 - 卷积块,3x3,stride=2,128通道 - C3块,128通道 - 卷积块,3x3,stride=2,256通道 - C3块,256通道 - 卷积块,3x3,stride=2,512通道 - C3块,512通道 - 卷积块,3x3,stride=2,1024通道 - C3块,1024通道 - 最后是一个SPPF模块,1024通道 !image.png Head Head部分实际上主要定义的是YOLOv5颈部网络的结构。以下为YOLOv5模型配置文件默认的head部分的参数: 看上去比backbone部分稍微难理解一点,但是仍然结合YOLOv5模型网络架构图中的Neck网络部分就比较清楚了: !image.png 对于上面配置文件中配置信息的解读,从架构图左下角第10层的卷积块开始,这个卷积层的from参数为-1,表示从主干网络最后的SPPF层接收输入在第10层(也就是卷积层)进行卷积处理,然后送到第11层上采样模块Upsample 2来进行上采样......以此类推,直到最后在第23层即最后一个CSPLayer上输出。 配置文件的最后一句[[17, 20, 23], 1, Detect, [nc, anchors]]`表示从17、20、23层这三层输出检测信息,用于最终在模型的检测头上输出最终的检测结果。 参考资料 - YOLOv5 原理和实现全解析 — MMYOLO 0.6.0 文档

YOLOv5模型网络架构解读

本文详细以YOLOv5的P3版本为基础,详细总结了该模块的整体网络架构、各个子模块的工作逻辑,可为后续更深入的学习YOLOv5模型打好基础。 注:本文所总结的YOLOv5版本为YOLOv5l,v6.0,P5版本。具体各个版本的差异可以参考YOLOv5模型的不同版本总结。 主干网络Backbone 在完整总结整个主干网络的结构之前,首先在下面以小节的形式整理主干网络中的各个子模块。 Focus与6x6卷积 YOLOv5的早期版本在主干网络的起始部分,使用了Focus结构(一种切片操作)来减少计算量并保留信息。该模块主要用于图像的下采样和特征提取,在减少空间分辨率的同时,保留了更多的原始信息,这个操作的本质就是将空间信息(宽和高)转换到通道维度。 Focus模块的操作分为三个步骤: - 切片操作(slice):将输入图像在宽度和高度方向上每隔一个像素进行采样,得到4个互补的子特征图。 - 通道拼接:将4个子特征图在通道维度拼接在一起,实际上就是通道数x4。 - 卷积操作:对拼接后的特征图进行一次标准的卷积。 以YOLOv5s的结构为例,原始的608x608x3的图像输入Focus模块,经过切片操作,变成304x304x12的特征图,然后再经过一次32个卷积核的1x1卷积操作,变成304x304x32的特征图输出。 切片操作的处理如下图所示:可以看到这个Slice操作跟YOLOv2模型(参考YOLOv2模型网络架构解读)中的Reorg操作完全相同。 !image.png Focus处理的流程如下图所示: !image.png 但在YOLOv5的后续版本(如v6.0之后)中,Focus结构被一个标准的、卷积核大小为6、步长为2的卷积层(Conv2d 6x6, stride=2)所取代。这也就是我们在YOLOv5不同版本的架构图上看到有些是Focus模块,有些是6x6卷积模块的原因所在。尽管Focus模块在理论上可以减少信息丢失,但在实际应用中,一个精心设计的卷积层(使用较大的6x6卷积核)也能有效捕获类似的空间信息,并且在GPU等硬件上,标准卷积通常能得到极佳的优化,实际的推理速度可能反而更有优势或相当。因此,在YOLOv5的新版本中,使用通用的卷积层替换掉特殊的Focus结构,使得整体网络结构可以更加统一和简洁,减少了自定义模块,降低了代码的复杂性和维护成本。 激活函数 在YOLOv5的主干网络中,普遍使用SiLU激活函数(也称为Swish函数)代替了之前版本常用的LeakyReLU。相比于LeakyReLU,SiLU激活函数在0点附近更为平滑,有助于提升梯度的流动性和模型的表达能力。 SiLU激活函数的表达式为: 其中的Sigmoid(x)就是标准的sigmoid函数,它的值在0-1之间。SiLU函数的特点是非线性,连续可导。 !image.png 下图是通过Netron查看YOLOv5的onnx文件,主干网络图像输入的前两个卷积块的结构。这个结构图中可以清楚的看到SiLU激活函数的计算流程,其计算可分解为两个清晰的步骤: - Sigmoid变换:首先,将卷积层的输出送入到Sigmoid函数中。Sigmoid函数会将其中的每个元素的值映射到 (0, 1)的区间内。 - 逐元素乘法:然后,将原始的输入与第一步得到的Sigmoid权重进行逐元素相乘。这一步就是下图中的 act/Mul 操作。其效果是:根据Sigmoid计算出的权重,对原始特征进行放大(权重接近1)、抑制(权重接近0)或部分保留(权重在0-1之间)。 !image.png 但是仍然需要注意的是,YOLOv5的不同版本中使用的激活函数是不同的,其早期版本(如v1.0-v3.0)默认配置的激活函数仍然是LeakyReLU,所以可以看到这些早期版本的主干网络中仍然采用了大量的CBL(Conv-BN-LeakyReLU)块;从大约v5.0版本开始,官方将默认的激活函数全面切换为SiLU,CBL块也随之变为Conv-BN-SiLU。 这就是网上看到的多数YOLOv5的模型架构图中仍然大量采用CBL块和LeakyReLU激活函数的原因所在。 !image.png CSPLayer 与YOLOv4相同,YOLOv5在主干网络的实现中也采用了CSP结构。也就是说,将特征图在通道维度上拆分为两部分,一部分进行复杂的卷积变换,另一部分直接抄近道走捷径,最后在输出之前将两部分结果合并。概念上与YOLOv4没有太大区别,与CSP处理的更详细解释可以参考YOLOv4模型网络架构解读. YOLOv5中的CSP块的结构如下图所示:可以看到,YOLOv5模型结构中使用的CSP块有两种类型: - CSP1用于主干网络,其中的卷积变化处理部分使用Resnet的残差块结构(如下图中的DarknetBottleneck add=true部分)。 - CSP2用于颈部网络,其中的卷积处理部分就只是两个连续的卷积块而已(如下图中的DarknetBottleneck add=false部分)。 !image.png SPPFBottleneck SPPF模块的全称是Spatial Pyramid Pooling-Fast(快速空间金字塔池化),本质上仍然是一个SPP模块,只不过是在计算速度和性能上进行了优化而已。 SPPF和SPP模块的核心目标都是要扩大特征信息的感受野,融合多尺度上下文信息。两者的区别只不过是:传统的SPP模块是在多个大池化核(如5x5, 9x9, 13x13)对输入的特征图并行处理并把处理结果拼接在一起,而SPPF模块则是使用同一个5x5池化核重复工作,多次池化,然后把不同阶段的池化结果拼接在一起。 - 有关SPP模块的工作逻辑,可以参考另外一篇笔记YOLOv4模型网络架构解读。 相比于前者,SPPF模块串联相同池化核的工作方式在硬件上更易优化,计算速度更快,可有效提升模型对不同尺度目标的适应性。在保持甚至提升模型性能的同时,实现更快的推理速度。 !image.png 至此,YOLOv5模型的主干网络部分就呼之欲出了:在输入图像为640x640分辨率的情况下,主干网络的输出仍然是以8倍,16倍,32倍的倍率分别输出三路分辨率和通道数不同的特征信息,由颈部网络对特征信息进行检测。 !image.png 总结起来,从上到下整个主干网络可以分为五个部分: - Stem Layer:也就是上面所介绍的Focus模块,只不过在较新的模型版本中,Focus模块已经变成了6x6,步长为2的普通卷积网络。在输入图像分辨率为640x640x3的情况下,该模块的输出为320x320x64维度的特征图。 - 从Layer 1到Layer 3都是3x3卷积+多个连续CSP块的结构,前面的步长为2的3x3卷积模块负责把输入特征图的分辨率减半,通道数加倍。 - Layer 1包含1个3x3卷积+连续的3个CSP块,其输出为160x160x128维度的特征图。 - Layer 2包含1个3x3卷积+连续的6个CSP块,其输出为80x80x256维度的特征图。该层输出作为大尺寸分辨率特征图送入颈部网络。 - Layer 3包含1个3x3卷积+连续的9个CSP块,其输出为40x40x512维度的特征图。该层输出作为中等尺寸分辨率特征图送入颈部网络。 - 最后的Layer 4,前半部分的处理仍然跟Layer1-3相同,包含有1个3x3卷积+连续3个CSP块,只不过在最后还额外增加了一个SPPFBottleneck模块进一步扩展输出特征图的感知野。该层的输出为20x20x1024维度的特征图。最终该层输出作为小尺寸分辨率特征图送入颈部网络。 颈部网络Neck 颈部网络部分是YOLOv5模型实现多尺度目标检测的核心组件,其工作目标是把主干网络提取的特征进行高效融合,使模型能够同时识别不同大小的目标。 整体来讲,YOLOv5和YOLOv4在颈部网络设计上的架构和思想没有太大变化,但其核心区别在于,YOLOv5在颈部网络中引入了上面主干网络部分所介绍的CSP2模块等方面的改进,显著加强了特征融合能力并提升了计算效率。 网络的整体架构上,YOLOv5的颈部网络仍然采用了与YOLOv4相同的FPN (特征金字塔网络) + PAN (路径聚合网络) 的双向结构。其中FPN负责自顶向下传递强语义特征,而PAN负责自底向上传递精确定位特征,形成强大的特征金字塔。 - FPN处理:在通道维度上通过Concat操作,将上采样(Upsample)后的深层特征图与对应的浅层特征图进行拼接,结合了浅层的细节信息和深层的语义信息。 - PAN处理:在PAN路径中,使用步长为2的卷积层,将融合后的特征图再次下采样(Downsample),以便与更高层级的特征进行进一步融合。 !image.png 检测头Head 检测头部分相比之前的版本仍然是没有太大差异,输出的三路特征图经过各自独立的1x1卷积处理,输出不同分辨率的检测结果张量。 !image.png YOLOv5模型的整体网络架构 最后再放一张YOLOv5模型的整体网络架构图,结合以上各个子模块的描述,理解整体的架构图就比较容易了: !image.png 参考资料 - 深入浅出Yolo系列之Yolov5核心基础知识完整讲解 - 知乎 - YOLOv5 詳細解讀. 前言 | by Steven Meng | Medium - YOLOv5 原理和实现全解析 — MMYOLO 0.6.0 文档

YOLOv5模型的不同版本总结

本文基于模型规模、算法迭代、输出结构等各方面的维度,对YOLOv5模型的不同版本及其相互之间的差异进行了总结。 YOLOv5模型是由Ultralytics公司的Glenn Jocher推出的开源模型,在Github上的地址为:GitHub - ultralytics/yolov5: YOLOv5 🚀 in PyTorch > ONNX > CoreML > TFLite。 模型规模 YOLOv5模型根据其参数量的多少可以分为nano(参数量1.9M)、small(参数量7.2M)、medium(参数量21.2M)、large(参数量46.5M)、X-large(参数量86.7M)五种类型,其参数量依次上升,效果自然也是越来越好。基本上如果是要部署在移动端或者嵌入式处理器的边缘计算节点,唯二可以选择的就是YOLOv5s和YOLOv5n。 - _模型的选择取决于对图像识别和检测的精度要求_:对于简单的物体存在性检测,YOLOv5s可能已足够。但对于需要区分细微差别或检测小物体的复杂场景(如医学影像)而言,则需要选择更大的模型,例如YOLOv5l或YOLOv5x。 !image.png 以上各个版本模型的差异,都源于YOLOv5源代码配置文件中两个核心参数的不同:depth_multiple(深度系数)和width_multiple(宽度系数)。这两个参数通过控制主干网络(Backbone)和颈部网络(Neck)中卷积层的通道数(即宽度)以及CSP块中残差层/卷积层的堆叠数量(即深度),来生成不同复杂度的YOLOv5模型(n, s, m, l, x)。 - width_multiple(宽度系数):这个系数主要用于控制网络特征图的通道数。系数越大,每一层卷积的通道数就越多,模型的宽度和参数容量就越大,学习能力更强,但计算量也随之增加。 - depth_multiple(深度系数):这个系数主要控制某些关键模块(如主干网络和颈部网络中的C3模块)的重复堆叠次数。系数越大,网络层数越多,模型的深度越深,就能提取更复杂的特征,当然也需要更多计算资源。 算法实现迭代 不同于YOLO模型的前几代,YOLOv5模型直接以一个基于Pytorch的软件项目推出,并且在2020年推出之后的几年中,该项目的具体实现不断更新和优化,从而形成了从v1.0到v7.0的算法实现迭代序列。 截止到目前为止,YOLOv5项目对应的几个软件版本、发布的时间线以及重点更新特性如下图所示。可以看到这些版本中,从模型结构角度上最大的更新是其中的v6.0版本。该版本中包含了:使用普通的步长为2的6x6卷积网络代替Focus模块,在主干网络末端输出的小分辨率特征图上使用SPPF替换了SPP,通过激活函数也是用SiLU替换到之前默认的LeakReLU。 !image.png 目前对于新的项目和实际部署,建议直接使用v6.0或v7.0等最新版本。这些版本不仅包含了所有重要的架构优化,通常还具有更好的稳定性和更完善的文档支持。 输出结构 除了以上在模型规模和软件项目迭代版本的差异之外,YOLOv5还提供了两种输出结构配置(P5和P6),用于进一步平衡检测精度、模型大小和推理速度,以适应不同的应用场景和硬件条件。P6版本可以在计算资源充足的情况下(如智能驾驶,医疗影像等领域),为大量微小目标的检测和识别提供极致的检测精度。当然,目前在产业领域中最常用的仍然是P5版本。 P5和P6版本的差异如下图所示: !image.png 下图是YOLOv5l P5输出结构的网络架构图: !image.png 下图是YOLOv5l P6输出结构的网络架构图: !fbd0e5d7-8c19-4e31-aba9-961e0077388a.png 从架构图上可以看到,P5版本的默认输入为640x640x3分辨率图像,输出为三路经过8倍、16倍、32倍降采样的特征信息;而P6版本的默认输入为1280x1280x3分辨率的更高清图像,输出为四路分别经过8倍、16倍、32倍、64倍降采样的特征信息。 版本命名规则与任务类型 基于以上总结的YOLOv5模型在不同维度上的版本差异,在实际的使用中,YOLOv5的模型文件名就包含了这些维度的信息,其命名遵循一个清晰的命名规则:yolov5{规模}{输出结构}-{任务}.pt。 例如: - yolov5s.pt:一个小规模(s)、P5输出结构、用于目标检测的模型。 - yolov5x6.pt:一个超大规模(x)、P6输出结构、用于目标检测的模型。 - yolov5m-seg.pt:一个中等规模(m)、P5输出结构、用于实例分割应用的模型。 以上提到的实例分割应用,是因为在YOLOv5的最新版本中,除了基本的目标检测功能以外,还通过不同的任务后缀支持图像分类(-cls) 和实例分割(-seg),这些变体通常基于P5结构开发。 参考资料 - YOLOv5 原理和实现全解析 — MMYOLO 0.6.0 文档

YOLOv4模型网络架构解读

YOLO模型的前三个版本v1(2015年6月),v2(2016年12月),v3(2018年4月)全都都是由Joseph Redmon提出和开发,但该作者因为YOLO模型不可避免地被用于军事领域,所以决定退出计算机视觉领域(为作者伟大的博爱精神点赞)。新的YOLOv4版本则于2020年4月由俄罗斯的Alexey Bochkovskiy以及中国台湾的两名研究员廖弘源和王建尧联合推出。 下图是YOLOv4与YOLOv3以及EfficientDet等竞品模型在COCO2017数据集上的对比测试结果:可以看到,YOLOv4模型在检测速度FPS大体一致的情况下,检测精度AP指标相比YOLOv3版本有了接近10%的提升。 !image.png YOLOv4官方源代码的仓库地址:GitHub - AlexeyAB/darknet: YOLOv4 / Scaled-YOLOv4 / YOLO - Neural Networks for Object Detection (Windows and Linux version of Darknet )。 同之前的YOLO版本相同,YOLOv4的网络架构也可以分为主干、颈部和检测头三个部分。 - 主干网络:YOLOv4的主干网络是CSP Darknet53,YOLOv3是Darknet53 - 颈部网络:YOLOv4的颈部网络是PANet,YOLOv3是FPN(特征金字塔) - 检测头:YOLOv4沿用YOLOv3的检测头YOLO HEAD 主干网络CSPDarknet-53 在主干网络的设计上,YOLOv4借鉴了Cross Stage Partial Network(CSPNet)的设计理念,为YOLOv4构建出一个全新的高效且强大的主干网络:CSPDarkNet-53。CSPDarkNet-53相比于YOLOv3的Darknet-53而言,结构上没有太大变化,主要的区别是使用CSP残差块代替了普通残差块,以及使用Mish激活函数替代了LeakyReLU激活函数。 CSP block 如YOLOv3模型网络架构解读一文对YOLOv3主干网络结构Darknet-53的学习和总结,Darknet-53网络结构的设计引入了当时在Resnet中流行起来的残差结构,所以可以把Darknet-53网络看成是连续很多个残差块的堆积。CSPDarkenet-53则借鉴CSPNet的概念,用CSP残差块代替了普通的残差块。 下图是CSPDarknet-53所使用的CSP残差块的结构: !image.png 从以上结构上可以看到,所谓的CSP残差块,实际上就是把输入的特征图分为两个部分C1和C2,C1不做任何处理,C2经过一个普通残差块的处理以后,与C1拼接聚合后输出。整个CSP残差块的输入和输出张量的维度完全相同,这一点跟Darknet-53中普通残差块的处理是相同的。 以上CSP结构的合理性在于:卷积神经网络的特征图中往往存在很多冗余信息,尤其是不同的通道之间的信息很有可能是相似的,那么在这种情况下,对这些特征信息的处理就没有处理全部的信息,只需要处理其中一部分,另外一部分保持不变即可,这样既可以保证不损失模型的性能,还能够从很大程度上降低模型的计算量和参数量。 具体到CSP残差块在CSPDartnet-53主干网络上的实现,可以参考下图:对于每个CSP残差块输入的特征图张量,先使用两个独立的1x1卷积块,得到两个通道数减半的特征图,其中一条不做任何处理,另外一条经过一个普通的残差块处理,然后把两部分在拼接合并在一起,最后再通过一个1x1卷积处理后作为输出。 !image.png 从以上的CSP残差块结构可以看到,这个结构比之前的普通残差块多增加了输入和输出环节的三个1x1卷积块。 Mish激活函数 如YOLOv2模型网络架构解读一文所总结的,YOLOv2在卷积层的实现上导入了BN层,因此其基本的卷积块就是CBL块:卷积层+BN层+LeakyReLU激活函数。这一设计在YOLOv3中也得以继续沿用。在YOLOv4的主干网络中,则使用Mish激活函数代替了在之前版本中普遍使用的LeakyReLU激活函数。 Mish激活函数的公式为: !image.png 该函数的曲线如下图所示: !image.png 与ReLU相比,Mish在负值区域不是直接置零,而是保留一个较小的负值,这使得网络能够学习到更丰富的特征。而与LeakyReLU相比,Mish最大的区别是更为平滑的梯度流,尤其是在0点附近。由于其函数曲线是光滑的,在梯度的反向传播时就能提供更稳定、更平滑的梯度。这有助于缓解ReLU在负区间梯度为零所导致的神经元死亡问题,让权重更新更加顺畅,从而加速收敛并提升模型的泛化能力。 当然与ReLU和LeakyReLU相比,Mish函数的计算量明显加大,但其在深度神经网络中的应用显示了比ReLU更好的结果,所以被YOLOv4的作者导入到主干网络中。 > 需要注意,YOLOv4只是在主干网络中使用了Mish激活函数,在后面的网络部分仍然采用LeakyReLU激活函数。 下图就是YOLOv4主干网络CSPDarknet-53的网络架构。可以看到,这个网络架构与Darknet-53完全相同,区别就只是把其中的RES块替换成CSP块,把LeakyReLU激活函数替换成Mish激活函数而已。CSP残差块的堆叠结构仍然是跟Darknet-53一样的分为五个部分,每个部分的CSP块的数量仍然是经典的1,2,8,8,4。按照这个堆叠,主干网络仍然向后续的颈部网络输出8倍、16倍以及32倍的特征信息。 !image.png 颈部网络:SPP+PAN YOLO模型中的颈部网络负责收集来自主干网络所输出的不同阶段、不同分辨率的特征图信息,经过融合处理以后,并将它们传递到头部进行目标位置检测和类别判断。 SPP SPP,Spatial Pyramid Pooling,空间金字塔池化,其核心思想在于:使用不同大小的池化核来捕捉不同尺度的特征。具体实现中,就是使用不同尺寸的最大池化层(在YOLOv4中通常是5x5, 9x9, 13x13)对输入的特征图同时进行处理,提取多个尺度的特征,最终这些不同尺度的特征在通道维度上融合,就形成一个融合了多尺度信息的丰富特征。如下图所示: !image.png 在YOLOv4的网络结构中,主干网络所输出的特征信息,再经过这个SPP模块的并行多尺度池化处理,在不牺牲速度的前提下,有效扩大了模型的感受野并融合了丰富的上下文信息,从而显著提升了模型的特征表达能力和最终的目标检测性能,特别适用于大尺寸目标和复杂场景。 PANet 颈部网络的另外一个改进是采用了路径汇聚网络(Path Aggregation Network,PANet)。 在YOLOv3版本的颈部网络中,其采用的传统特征金字塔结构(FPN),仅包含自顶向下(top-down)的特征融合结构,也就是说,只会把主干网络尾端输出的低分辨率深层特征图向中段输出的高分辨率浅层特征图部分融合,这样的情况下,浅层输出的高分辨率特征图中就包含了深层的特征信息,但网络深层输出的低分辨率特征图并不包含浅层的特征信息。 YOLOv4的颈部网络引入的PANet,在FPN的结构的基础上,增加了一条自底向上(bottom-up)的特征融合路径,这样的结构就可以让不同尺度的特征信息得到更加充分的融合,在所有的输出上都同时包含了深层和浅层的特征信息。 !image.png 检测头 YOLOv4的检测头与YOLOv3相同,都是在三路输出的最后使用一个1x1卷积层同时完成坐标、置信度和类别等信息的三部分预测。 YOLOv4的完整网络结构 基于以上对于网络三个部分的详细总结,理解如下所示的YOLOv4整体网络架构就比较简单了: !image.png 参考资料 - YOLO演進 — 3 — YOLOv4詳細介紹. 之前有介紹過 YOLOv1… | by 李謦伊 | 謦伊的閱讀筆記 | Medium - YOLOv4 中的 Mish 激活函数-腾讯云开发者社区-腾讯云 - YOLO目标检测》杨建华,李瑞峰 第8章 YOLOv4

YOLOv3-输出、Anchor与损失函数的进化总结

本文对YOLOv3模型在输出数据的组织格式、Anchor预测框以及损失函数的计算等方面的内容进行了总结。 YOLOv3模型的输出 如下图所示的YOLOv3模型的整体架构图。不同于之前的YOLOv1和YOLOv2模型仅输出一组三维张量,然后在这一组三维张量上学习和解释待检物体的位置类别等信息,YOLOv3模型最终的输出是三组独立的三维张量,分别用于对不同尺寸的待检物体进行定位和检测。 !image.png 以上图输出的13x13x255的这一组三维张量为例,输入图像被分割为13x13个网格,每个网格会预置三个大小与尺寸比例不同的预测Anchor框(关于YOLOv3 Anchor框的信息参考下一节),然后通过一个长度为255的一维向量对该网格中检测到的物体信息(该物体的中心位于该网格)进行描述。也就是说,这个255长度的向量中包含了3个预测框的信息,其解释如下图所示。 !image.png 每个预测框中所包含的信息与YOLOv2的单个预测框的输出没有区别,都是中心坐标位置+长宽+预测置信度+80个类别(针对COCO数据集)的识别信息。这样的话每个预测框的信息长度就是4+1+80=85,一个网格中包含的3个预测框的信息长度就是255。这就是13x13x255这个三维张量的维度解释和由来。 > 如果每个网格放置B个Anchor Box,模型要支持的待检物体的类别数量为C,那么每个网格输出的数据长度就是B x (4+1+C)。 YOLOv3的Anchor框 在网格的Anchor框设计方面,YOLOv3仍然沿用了在YOLOv2模型上导入的先验Anchor框。只不过,在YOLOv2模型中,每个网格会提前放置5个Anchor框,到了YOLOv3模型中,则是给每个网格放置3个Anchor框(这一点在前一节解释YOLOv3模型的张量输出时已有解释)。 至于Anchor先验框的尺寸选择,YOLOv3仍然采用YOLOv2一样的做法,在训练数据集上使用 k 均值聚类方法来得到与这个数据集相匹配的9个先验框的尺寸。 - 关于如何使用 k 均值聚类法得到数据集的先验框,可以参考YOLOv2的Anchor锚框机制详解一文,对于如何从数据集得到Anchor框的长宽比的计算流程,已经有非常详细的描述。 在YOLOv3的论文中,作者基于COCO数据集,计算出来了9个Anchor先验框的尺寸。并且面积大小排序,均匀分为3份,分别针对不同分辨率的特征图: - 大框3个,针对13×13分辨率的特征图,共有13x13x3=507个预测框,负责大目标的检测:(116×90), (156×198), (373×326) - 中框3个,针对26×26分辨率的特征图,共有19x19x3=2028个预测框,负责中目标的检测:(30×61), (62×45), (59×119) - 小框3个,针对52×52分辨率的特征图,共有52x52x3=8112个预测框,负责小目标的检测:(10×13), (16×30), (33×23) !image.png YOLOv3的损失函数优化 如以上所总结的,YOLOv3模型会输出三组独立的三维张量,用于对不同大小的待检物体进行检测。那么其损失函数也就需要针对这三组输出独立进行。三个输出计算损失值的损失函数是完全相同的,而且各个输出对于损失值的计算流程完全独立进行互不干扰,最终得到三个损失值Loss_13,Loss_26,Loss_52,整个模型总的损失值就是这三个损失值之和。 而无论是以上输出的哪个尺度,YOLOv3的损失函数都由以下三个部分构成: - 边界框回归损失(lbox):负责评估预测框位置(中心坐标x, y)和大小(宽高w, h)的准确性。 - 置信度损失(lobj):负责判断预测框内是否包含目标物体。 - 分类损失(lcls):负责判断预测框内物体的具体类别。 因此整个模型总体的损失函数的计算流程如下图所示: !image.png 以下以13x13尺度特征图为例来解释损失函数各个部分的计算流程。 首先进行YOLOv3训练过程中样本类型的定义解释。总体来讲,YOLOv3在训练过程中进行损失函数计算时,把样本分为三种类型:正样本,负样本和忽略样本。不同类型的样本会被用于或者不用于计算不同类型的损失值。如前所述,对于13x13分辨率的模型输出而言,实际上总共会输出13x13x3=507个预测框。而对于正负以及忽略样本的判定,实际上也就是对这507个预测框进行判断和分类,然后基于分类结果决定后续是否用于不同损失值的计算。 - 正样本:对于每个训练样本标记的真值框而言,首先会计算真值框的中心位于哪个网格,然后在这个网格输出的三个预测框中,再找出来与真值框IOU最大的那个预测框作为正样本。所以每个真值框对应于一个正样本。正样本会用于所有的损失计算类型(边界框、置信度和分类)。 - 忽略样本:与任何一个真值框的IOU大于指定的阈值(如0.5),但是又不是该真值框的正样本。忽略样本不会参与任何损失计算,因此对于最终的损失值没有任何影响。 - 负样本:与所有的真值框的IOU都小于指定的阈值(如0.5),实际上也就是除了正样本和忽略样本的其他所有样本。负样本会被用于置信度损失,但是不会参与边界框和分类损失计算。 与之前的YOLO版本相同,YOLOv3的损失函数同样包含三个部分:坐标损失,置信度损失以及类别损失。 对于坐标损失的计算而言,因为只有正样本会参与坐标损失的计算,所以实际上坐标损失值就是所有正样本坐标与真值框坐标之间的损失值。因为已经有了所有真值框及其对应的正样本的中心位置坐标和长宽,那么计算坐标损失就比较简单了: !image.png 接下来是置信度损失的计算。在YOLOv3中,使用了二元交叉熵BCE来作为置信度损失的计算函数。模型为每个预测框输出一个置信度 logit 值。这个值需要通过 Sigmoid 函数激活,将其映射到 (0, 1) 区间,才能作为表示含有目标概率的置信度分数 \hat C。然后基于以下公式计算单个预测框的置信度损失,最后把所有预测框的置信度损失加起来就是总的置信度损失: 如前所述,在置信度损失计算时,需要考虑正样本和负样本,不需要考虑忽略样本。 - 对于正样本(C=1)而言,置信度损失可简化为。这意味着模型预测的 越接近1,损失越小。 - 对于负样本(C=0)而言,其损失可简化为 。这意味着模型预测的  越接近0,损失越小。 最后是类别损失的计算。类别损失的计算逻辑与置信度损失基本相同,都是采用二元交叉熵BCE来进行其损失值的计算。不过,对于类别损失的计算而言,只有正样本需要参与进来。同样的,模型会为每个预测框输出一个包含所有类别信息的原始logit向量。这个向量的长度等于数据集的类别数量(例如,COCO数据集是80类)。这些原始 logit 值需要通过Sigmoid 函数进行激活,将其映射到 (0, 1) 区间,作为类别损失计算的参数 。 - 需要注意的是,YOLOv3对于类别的判断可以允许一个框同时属于多个类别(如人+男人两个标签)。 然后使用以下公式对该正样本在所有的类别上计算类别损失,然后把所有正样本的类别损失加起来就是总的类别损失。 参考资料 - 【YOLO系列】YOLOv3论文超详细解读(翻译 +学习笔记)-阿里云开发者社区

YOLOv3模型网络架构解读

本文详细总结了YOLOv3模型的整体架构方面的内容,包括其主干网络Darknet-53、特征金字塔的概念以及多级特征图检测头的设计思路和实现理论。 全新主干网络DarkNet-53 从YOLOv2开始,YOLO模型的作者为模型设计了专门的主干网络Darknet。在YOLOv2版本中所使用的主干网络是DarkNet-19(在YOLOv2模型网络架构解读一文可参考YOLO2模块的网络架构设计),即该网络结构中总共包含了19个卷积层。到了YOLOv3阶段,DarkNet的结构设计借鉴了当时已成业界主流设计理念的Resnet的残差网络结构,退出DarkNet-53并将其作为YOLOv3的主干网络。 - 关于Resnet的残差网络结构可以参考CNN经典网络模型架构学习之Resnet一文。 在DarkNet-53的设计上,其卷积层的实现仍然采用了与Darknet-19一样的卷积层+BN层+LeakyReLU激活函数的CBL组合结构。Darknet-53网络包含有53个卷积层并由此得名。 !image.png 在实现特征图空降降采样操作时,不同于DarkNet-19所采用的最大池化层,DarkNet-53普遍采用了步长为2(stride=2)的3x3卷积层来实现该功能。这种方式能够在减少特征图尺寸的同时,能够保留更多的空间信息。因此,在整个Darknet-53的网络结构中不包含有在传统卷积网络中非常常用的MaxPool层,所有的特征图空间降采样操作都适应步长为2的 3×3 卷积来完成。 此外就是以上提到的Darknet-53的网络设计上,借鉴了ResNet的残差块的设计理念。Darknet-53网络所使用的残差块结构如下图所示。残差块的输出和输出在特征图的尺寸和通道数量上完全相同,残差块内部的实现就是连续的1x1卷积块与3x3卷积块的堆叠,然后其输出再与输入相加实现残差连接。 !image.png 与Resnet类似,DarkNet-53网络的大部分实现实际上就是残差块的堆叠。下图是YOLOv2主干网络Darknet19与YOLOv3主干网络Darknet53的对比。可以看到,Darknet19中的降采样操作全部使用传统的最大池化层,而Darknet53的降采样操作则全部使用了步进为2的3x3卷积层。 !image.png Darknet-53的网络结构就是反复堆叠以上所描述的残差块。从整体结构上,Darkent-53依据于各层的特征图尺寸,可以分为5个部分(如上图右侧圈起来的5个部分)。从第一个到第五个部分各自所包含的残差块数量分别为1,2,8,8,4。各个部分的输入和输出特征图的尺寸与通道数量均保持一致,然后各部分之间使用一个步进为2的3x3卷积层来实现特征图尺寸降维的操作。 这种“12884”的堆叠数量配置也成为后续诸多YOLO框架的设计范式之一。 Darknet-53本质上仍然是一个对图像特征提取以及进行类别分类的网络,所以在最后仍然通过全局平均池化+全连接层配合Softmax来实现对ImageNet数据集的识别分类输出。当然,作为YOLOv3模型的主干网络使用时,全局平均池化及其后的部分会被拿掉,只保留前面的特征提取部分输出不同阶段的特征图进行目标检测。 以下是Darknet-53网络与Darknet-19、Resnet-101、Resnet-152等网络架构在识别率、运行速度等指标方面的比较。可以看到,尽管Darknet-53的速度相比Darknet-19下降不少,但是也能够达到实时的水准,而且识别正确率有了比较明显的提升。 !image.png 特征金字塔与特征融合 所谓的特征金字塔(FPN,Feature Pyramid Network),来源于另外一种目标检测网络结构SSD,其设计背后的理念在于:随着神经网络层数的加深以及降采样操作的增多,其各层输出的特征图中包含有不同的信息,浅层特征图的分辨率更高,所以包含有更多的位置信息,同时因为降采样操作比较少而导致感受野较小,对于目标类别的识别而言也就更困难;而深层特征图的分辨率更低,目标的位置和细节等信息在逐级降采样过程中丢失了,但是因为感受野相对更大,对于大目标的识别和检测性能就更好。因此,针对以上存在的矛盾,解决方案也就应运而生:综合使用浅层特征和深层特征,其中浅层特征负责检测较小的目标,深层特征负责检测较大的目标。 - 特征图分辨率越大,位置信息的细节越丰富,但是因为感受野过小,对于目标类别的判断越困难。 - 特征图分辨率越小,在多级降采样的过程中,目标位置信息的损失越多,但感受野逐步增大,更容易判断待检物体(尤其是大尺寸物体)的类别。 下图是特征金字塔的结构:左侧的特征提取网络输出三种分辨率和维度的特征数据,在右侧的检测网络中分别实现对大中小三种尺寸的目标进行检测和识别。 !image.png 对于大尺寸的目标进行检测,只需要用到最小分辨率的特征信息(上图最上层的预测输出)就足够了。因为对于较大尺寸的物体而言,尽管小分辨率特征图的位置信息损失比较多,但是对于定位大尺寸物体而言足够了,而且小分辨率特征图的感受野更大,分辨物体的类别更容易,所以不需要依赖于大分辨率的特征信息。 而对于较小尺寸的目标进行检测,则不仅需要用到特征提取网络中输出的较大分辨率的特征信息(利用其中包含的丰富位置信息对小分辨率问题进行准确定位),还需要用到较小分辨率的特征信息(用于辅助判断定位物体的类别)。所以就需要把小分辨率的特征信息和大分辨率的特征信息融合在一起,来实现对中小尺寸目标进行准确的定位和类别判断。 那么接下来的问题就是,不同分辨率的特征图如何进行融合处理呢? 答案是,要对深层次网络输出的小分辨率特征图,执行空间上采样(Upsample,也就是上图中的2x up)操作,把其分辨率大小提升到与浅层分辨率相同的配置,然后再与浅层分辨率特征图进行融合。这个过程主要涉及到两个操作:空间上采样(Upsample)和特征图融合(Concat)。 以下以YOLOv3模型13x13分辨率特征图与26x26分辨率特征图的融合来解释Upsample操作和Concat操作。 - Upsample操作:基于13x13分辨率的特征图,使用_近邻插值_的方法,执行2x2插值处理,每一张13x13分辨率特征图都通过这样的插值方法,上采样为26x26分辨率的特征图。 - Concat操作:以上13x13分辨率的特征图,通过Upsample操作以后插值为16x16分辨率。这样就有了两组16x16分辨率的特征图,只需要把两组特征图在通道轴上_首尾相接拼接_在一起就可以了。 不同分辨率的特征图所进行的融合处理以及输出流程如下图所示: !image.png YOLOv3的架构图 基于以上对主干网络Darknet-53以及特征金字塔多路特征图输出结构的理解,再来学习YOLOv3的网络结构就比较容易了: !image.png 上图的左上角是YOLOv3的主干网络部分Darknet53,去掉了全局平均池化和用于分类的全连接层。在主干网络的第三部分残差块、第四部分残差块、第五部分残差块的输出上分别输出8倍、16倍、32倍降采样的特征图。 - 对于输入图像分辨率为608x608的情况下,以上三部分所输出的特征图分辨率也就是76x76,38x38以及19x19。 - 而对于当时在目标检测场景中更常见的416x416输入图像分辨率而言,以上三路特征图分辨率则为52x52,26x26,13x13。 上图的右半部分则是基于特征金字塔理论的特征检测部分,以三路不同分辨率的特征图为输入,在不同曾经对其进行上采样和Concat操作,最终输出不同疏密度的检测网络:越密的网格越适合检测小而密集的物体,而越疏的网格越适合检测大而稀疏的物体。 参考资料 - 《YOLO目标检测》杨建华,李瑞峰 第7章 YOLOv3 - YOLOv3 詳細解讀. YOLOv3… | by Steven Meng | Medium - 《深度学习之PyTorch物体检测实战》 董洪义 6.3 多尺度与特征融合 YOLOv3 - YOLOv3 — You Only Look Once (Object Detection) | by Moris | Computer Vision Note | Medium

YOLOv2特性总结-从YOLOv1的进化

本文对YOLOv2在YOLOv1基础上所进化的特性进行了完整的总结,解释模型在进化中各项指标得以提升的特性和工作逻辑。 YOLOv2模型在YOLOv2的设计基础上进行了多项模型设计、训练等方面的调整和优化,在VOC2007数据集上的mAP指标由63.4%大幅度提升到了78.6%,同时保持了YOLOv1 one-stage进行目标检测+类别识别的设计,推理速度依然很快,并且能够识别更多的物体类别。 - 关于mAP指标的概念和计算逻辑,可以参考目标检测模型的评价指标详解:mAP,召回率,准确率 下图中总结了YOLOv2在YOLOv1基础上的所有改进项的列表,本文对其中的每一项展开描述: !image.png Batch Norm YOLOv2为模型中的每一个卷积层之后都增加了批归一化的处理,所以从之前的卷积+LeakyReLU激活函数的形式,变成了卷积+批归一化+LeakyReLU激活函数的CBL块。 具体的细节在YOLOv2模型网络架构解读一文中有详细描述。 !image.png 在加入了BN层之后,YOLOv1的性能得到了第一次提升。在VOC2007测试集上,其mAP性能从原本的63.4%提升到65.8%。 Hi-res Classifier 因为ImageNet数据集有着海量的图像数据标注信息,所以大多数目标检测算法,都会首先基于ImageNet数据集对模型的主干网络部分进行预训练,把这个预训练出来的参数作为主干网络的初始参数,即所谓的Image Pretrained技术。 但是基于以上技术所进行的预训练过程,主干网络进行预训练的过程中,所接受的图像尺寸是224x224,这个小的分辨率,对于目标检测应用而言,很容易丢失一些小物体的细节,给后续的检测环节带来困难。所以在YOLOv2的设计上,采取了先使用ImageNet数据集在224x224分辨率上进行预训练,预训练完成后还要增加一个模型的微调(fine tune)阶段,在预训练的参数上,对网络使用448x448分辨率继续训练,使之适应更高分辨率的图像。 具体而言,YOLOv2模型预训练的流程是: - 先使用224x224分辨率在ImageNet数据集上从头开始训练网络,大概训练160个epoch。 - 然后把模型的输入调整为448x448,继续在ImageNet数据集上,进行10个Epoch的训练就好了。 在这样的策略之下,YOLOv1的性能又一次得到了提升:mAP从65.8%提升到69.5%。 > 为什么不直接从448x448分辨率从头进行网络的训练,而要先使用224x224的小分辨率进行预训练,然后再使用448x448的大分辨率进行微调?一方面是因为从224x224到448x448,分辨率增大四倍,在当时算力的情况下模型的训练时间会拉长很多,另外一方面则是因为224×224分辨率是当时分类网络的标准输入,有大量预训练权重与超参数经验可供参考,因此先沿用成熟配置做热身,再小幅微调,在工程上最稳妥。 全卷积神经网络和Anchor先验框机制 在网络结构上的设计上,YOLOv2相比YOLOv1最大的变化,就是在网络的最后阶段去掉了全连接层,将其改成全卷积网络结构。具体的网络结构可参考YOLOv2模型网络架构解读. 此外,YOLOv2借鉴了Faster R-CNN网络的思路,引进了先验框Anchor的机制,在图片的推理阶段,模型就只需要学习能够将先验框映射到待检物体目标框的尺寸的偏移量,无须再学习整个目标框的完整尺寸信息,这使得训练变得更加容易。YOLOv2导入的Anchor先验框的机制详细总结可参考YOLOv2的Anchor锚框机制详解。 基于模型网络结构上的优化以及Anchor框的引入,最终由主干网络输出的特征图的尺寸由YOLOv1提升为YOLOv2的13x13,每个网格提前放置 K 个Anchor框(在YOLOv2模型中默认的Anchor框数量是5个),这样也就意味着整幅图可以输出13x13xK=169K个Bounding Box,并且每个Bounding Box的预测信息都是完全独立的,各个Bounding Box都有自己的中心点x和y坐标,Box的长宽w和h,预测置信度confidence以及各个类别的识别概率(在YOLOv1模型中一个网络的两个Bounding Box共享同一个类别识别概率,因此实际上每个Bounding Box只能检测一个物体)。所以YOLOv2同一网格输出的各个Bounding Box的相互独立的,这样就可以解决两个物体的中心在同一个网格中会存在的漏检问题。YOLOv1和YOLOv2模型的输出信息差异如下图所示: !image.png Multi-Scale training 经过对网络结构的改造,去掉全连接层,YOLOv2采取了全卷积网络的结构,网络中只有卷积层和最大池化层,这样的情况下,整个网络的输入分辨率就可以不再局限于只使用416x416的图片,可以使用各种不同分辨率的图像尺寸,因此,在训练过程中,也就可以不断更改输入图像的尺寸,使之适应不同分辨率的图像以及待检物体的大小。 YOLOv2模型主干网络即图像特征提取部分的的下采样步长为32倍,即由图像特征提取部分提取出来的特征图尺寸是输入图像分辨率的1/32,因此输入图像的分辨率需要是32的倍数(最小尺寸为320x320,最大尺寸为608x608)。因此在YOLOv2模型的训练过程中,每执行10个epoch迭代,模型就从320、352、384、416、448、480、512、544、576、608中依次选择一个新的图像尺寸,用作后续10个epoch训练的图像尺寸。如下图所示: !image.png 实践证明,这个Multi Scale Training的训练策略,可以使得同一个网络能够对不同分辨率的图片都可以更高的进行检测,从而提升了网络的检测性能。 参考资料 - YOLOv2 詳細解讀. YOLOv2 是在 YOLOv1 的基礎上使用了上圖紅色框框裡的方法改進,在… | by Steven Meng | Medium - 《YOLO目标检测》杨建华,李瑞峰 第6章 YOLOv2

YOLOv2的Anchor锚框机制详解

本文总结了YOLOv2模型基于Faster R-CNN的理论,在其模型网格预测框设计上增加的Anchor机制的来源、在YOLOv2模型中的计算和训练流程,尝试更清楚的认识YOLOv2模型的工作原理。 Anchor锚框的概念 首先给出Anchor框的定义:所谓的Anchor就是在每一个图像分割网格中,在训练之前提前固定放置一些尺寸大小和长宽比例不同的边界框,通常情况下,对于每一个网格而言,提前放置的边界框的大小和数量都是相同的。在推理的时候,模型基于输入图像中待检物体的情况,对每一个提前放置的边界框预测出若干的偏移量(中心位置的偏移量,宽高的偏移量等),然后基于这些偏移量调整之前放置的边界框,得出最终的预测边界框。因此,在每个网格处提前放置的边界框就是anchor框,其本质就是提供边界框的尺寸预计,然后用模型在推理过程中计算出来的偏移量来调整其位置和尺寸,从而得到最终的边界框尺寸。 从下图中可以直接的看出所谓的Anchor框究竟是什么:下图中把整幅图像分成与YOLOv1一样的7x7网格,然后在每个网格上提前放置了三个大小和比例不同的anchor框。为了方便演示,下图中仅对每六个网格放一组anchor框,实际中应该是每个网格都有一组anchor框,全画出来密密麻麻,演示反而不够直观。 !image.png 从Anchor框这个概念来讲,YOLOv1版本并没有提前放置anchor框的设计,而只是为每个网格定义了两个BOX,这两个BOX的中心位置和尺寸大小都是需要模型在基于输入图像推理的过程在预测出来的。所以从这个角度上讲,YOLOv1就是典型的Anchor-free(不依赖于Anchor)的模型。 每个网格提前预设Anchor框的设计,首先来源于另外一种目标检测模型:Faster R-CNN。 Faster R-CNN是一个两阶段的目标检测模型,在第一个阶段定位物体,第二个阶段识别物体的类别。其对物体定位检测的思路与YOLOv1是类似的,也是把输入图像分为多个网格,然后逐网格找物体,只不过Faster R-CNN的第一阶段只查找哪个网格中有物体,不关注该物体的类别,在第二个阶段再去识别检测到的物体的具体类别,而YOLOv1则是同时定位物体和识别物体的类别。这就是Faster R-CNN与YOLO各版本的目标查找和预测流程的最大差异所在。 对于Faster R-CNN模型而言,首先会把输入分割为多个网格,然后为每个网格预置 k 个不同尺寸和不同宽高比的ancher框(如上图所示)。在YOLOv2版本中,也把这个预置Anchor框的机制导入了进来,显著的提升了模型的mAP指标。增加Anchor框以后,在推理阶段,模型就不需要从头直接预测待检物体的尺寸与坐标,而只需要预测Anchor框到真实物体的偏移量即可,这样就降低了预测的难度。 YOLOv2锚框的k均值聚类算法 但问题是,如果确定每个网格应该放几个Anchor框,以及各个Anchor框的大小和长宽比又如何确定呢? 对于Faster R-CNN模型而言,其Anchor框的数量、尺寸等参数都是作为超参数由人工设定的。这种设计很难确保人工指定的Anchor框的参数与数据集之间是相互匹配的,所以对于不同数据集的输入,其网络性能的表现很难稳定。 对于以上问题,YOLOv2模型的做法,是采用 k 均值聚类算法,从指定的数据集中提取出来最适合的Anchor框。 首先把整个训练集中图片的所有真值标记框(GT,Ground Truth)全部提取出来,然后缩放到统一的输入尺寸(416x416):该算法只关心真值框的width和height信息,也就是长宽比。 !image.png 传统的K-means聚类算法在进行聚类计算的过程中使用的是欧氏距离,但是这样也就意味着较大尺寸的Anchor框相比较小尺寸会产生更多的错误,聚类计算的结果也就会发生偏离。而YOLOv2聚类计算的目的,是让预先选定的Anchor框的尺寸与其接近的真值框有更大的IoU,与Anchor框和真值框的尺寸大小无关。因此YOLOv2模型定义了以下距离公式执行聚类算法: !image.png 从以上统计出来的所有真值框列表中随机挑选出来5个做为初始Anchor框,执行以下所描述的k均值聚类算法: - 基于以上所描述的聚类算法,计算数据集中所有真值框与5个Anchor框之间的dist值,每个真值框与哪个Anchor框之间的dist值最大,就把这个真值框与该Anchor框归为一类。直到所有的真值框按照以上逻辑计算完毕,被分为以Anchor框为标准的5个集合。 - 对每个集合中所包含的真值框列表进行以下计算: - 取出所有真值框的width(之前已经统一缩放真值框的分辨率为416x416),计算出来所有width的几何平均值,把该组合Anchor框的width替换为计算出来的这个几何平均值。 - 同样的,取出所有真值框的height,计算出来所有height的几何平均值,把该组合Anchor框的height替换为计算出来的这个几何平均值。 - 经过以上运算以后,就得到5个新的Anchor框的(width,height)。 - 然后基于这5个新的Anchor框重新从头执行这个聚类算法,得到新的Anchor框的(width,height)。经过以上的反复迭代计算,最终新Anchor框的width和height基本不再变化,这就是最终由算法自动基于数据集所自动生成的Anchor框。 以上的整个k均值聚类算法的执行流程可以简化为下图所示: !image.png 基于以上逻辑和流程计算出来5个Anchor框的长宽参数以后,就可以按照其面积大小为顺序,写入YOLOv2模型的cfg文件中,后续在模型的训练和推理过程中,每个网格都会放置5组以上长宽比例的Anchor框用于对图像的中物体进行检测。 > 不过从以上计算逻辑也可以看出来这个算法存在的严重局限性:通过以上算法选出来的Anchor框是基于指定的数据集的,那么也就只能在这个数据集上有良好的表现。如果新的数据集的待检对象尺寸等与训练数据集存在较大差异化的,初选的anchor框就不合适了。 基于Anchor的Bounding Box预测 经过如上所述的Anchor框的聚类计算以后,在模型开始正式训练之前,就得到了5组长宽比确定下来的Anchor框。在训练过程中有了这个Anchor边界框的先验尺寸信息,网络就不必再从头去学习整个目标框的宽高了。 假设预定义Anchor框的尺寸为 p_w 和 p_h , 待检物体中心所在网格的左上角坐标位置为 grid_x,grid_y ,网络推理输出的中心偏移量是 (t_x,t_y) ,网络输出宽高的偏移量是 (t_w,t_h) 。那么YOLOv2模型使用以下公式计算待检物体的中心坐标和边界框: !image.png YOLOv2模型预测输出的中心偏移量 (t_x,t_y) ,是其相对所在网格左上角位置的相对偏移量。而且在计算实际中心位置时,还会使用Sigmoid函数限制叠加到 grid_x,grid_y 之上的偏移量在0-1之间,这样就可以使得预测出来的bounding box的中心点保持在该网格内部,不会偏移很多超出到网格之外。 可基于下图来理解上面的公式:虚线框为Anchor框,蓝色框为模型预测输出计算出来的Bounding box。 !image.png 参考资料 - 《YOLO目标检测》杨建华,李瑞峰 第6章 YOLOv2 - 《深度学习之Pytorch物体检测实战》 董洪义 6.2 依赖锚框:YOLOv2 - YOLOv2 詳細解讀. YOLOv2 是在 YOLOv1 的基礎上使用了上圖紅色框框裡的方法改進,在… | by Steven Meng | Medium

YOLOv1模型的损失函数与训练过程解读

本文对YOLOv1模型的训练正样本标记方法、损失函数以及基于训练样本对模型进行训练的完整过程进行了总结,为理解YOLO后续版本的处理流程打好基础。 YOLOv1训练正样本的标记 如YOLOv1输出信息以及NMS过滤算法详细解读一文所总结的,YOLOv1对于图像中物体的检测逻辑是,如果一个物体的中心点落在某个网格内,就由这个网格负责检测该物体,这样也就意味着,在制作YOLOv1训练样本的时候,只有包含待检物体中心点的网格被认为是有物体的。 以下图进行解释: !image.png 尽管图中的小猫这个物体的标记检测框占据的多达24个网格,但是因为这个标记框的中心位置落在粉色网格中,所以,对于YOLOv1模型在制作这个图像的训练样本时,只有粉色网格会被标记为有物体,该网格的检测置信度接近1;其他网格均为无物体,网格的检测置信度接近0。 > 有物体的网格会被标记为正样本候选区域,也就是说,在训练过程中,训练的正样本(positive sample)只会从此网格处的预测中得到,而其他区域的预测都是该目标的负样本。 YOLOv1的损失函数解读 从总体上看,YOLOv1的损失函数的计算主要包含三个部分: - 坐标损失(x,y,w,h):评估模型输出的检测框与训练样本标记框的贴合程度。 - 置信度损失(confidence):衡量模型输出的每个预测框的confidence值与该框中是否真正包含待检目标之间的吻合程度。 - 类别判断损失(class):判断模型输出的类别判断是否与训练样本的标记分类一致。 下图是损失函数计算过程中用到的各个参数的含义: !image.png 下图是YOLOv1损失函数的计算公式: !image.png YOLOv1损失函数的前两行表示预测边界框坐标位置参数的损失,第三和第四行表示正样本和负样本边界框置信度的损失,最后一行则是正样本处的类别损失。 以上损失函数的计算逻辑看上去很复杂,但是实际的计算逻辑还是比较直观的,以下分为3个部分分别详细解释。 坐标损失 坐标损失的计算,关键是要理解 的含义: - 对于训练样本的每一个标记框(也就是所谓的ground truth),先找到这个标记框中心所在的网格。然后计算这个网格输出的两个预测框与该标记框的IOU,把IOU值较大的那个预测框的 值设置为1,另外一个预测框的 值为0。 - 在这种情况下,实际上对于训练样本的每一个标记框,实际上都只会找到一个值为1的预测框。 以上逻辑就是找到训练样本每一个标记框所对应的负责预测框。然后就是按照对应的公式,分别计算标记框和负责预测框中心坐标以及长宽的损失值,两个损失值的和就是总的坐标损失值。 从以上的损失函数的计算公式可以看到,坐标损失值还需要乘以 的系数,这是为了加大预测框定位精度损失在整个损失值中的权重,优先更精确的对预测框定位。 置信度损失 置信度损失的计算包含两个部分:负责预测框的损失,不负责预测框的损失。 负责预测框的损失:对于训练样本的每一个标记框(ground truth),仍然是按照与以上计算坐标损失相同的步骤,找到这个标记框对应的负责预测框。每个标记框只会有一个负责预测框,然后计算标记框与负责预测框的置信度损失,此时标记框的置信度为1,负责预测框的置信度则是模型对这个预测框的置信度输出值。 不负责预测框的损失:不负责预测框的逻辑很简单。模型输出的98的预测框,除了上面的负责预测框以外,其他的97个框都是不负责预测框。 按照以上公式分别计算负责预测框的损失值和不负责预测框的损失值,然后把不负责预测框的损失值乘以0.5以压制背景框对真实预测框损失值的影响,两者相加就是整个置信度的损失值。 类别损失 类别损失的计算,仍然是仅对负责预测框进行类别损失的计算,其他预测框的类别损失值均为0。 对于每一个负责预测框,计算这个预测框的20种类别识别概率与训练样本标记框(ground truth)类别的损失。值得注意的是,训练样本标记框的类别是以one-hot形式提供的20维标签(只有一个类别为1,其他全部为0),所以类别损失就是每个负责预测框对应的20个类别的识别概率,与标记框one-hot类别的20维概率标签之差的平方差。 参考资料 - 《YOLO目标检测》杨建华,李瑞峰 第三章 YOLOv1 - YOLO演進 — 1. 在上篇文章有提到過 YOLO 的作法以及如何訓練,在這篇再更詳細的說明YOLO… | by 李謦伊 | 謦伊的閱讀筆記 | Medium

YOLOv1输出信息以及NMS过滤算法详细解读

对于YOLOv1模型的网络架构,在[[YOLOv1模型网络架构解读]]一文中已经有非常详尽的解释。以下是YOLOv1网络的架构图: !image.png 从以上架构图上可以看到,相比于对图像类别进行识别的各种经典卷积神经网络模型而言,YOLOv1的网络结构并没有特别大的创新点,仍然是在网络的前半段通过反复的卷积+池化操作提取图像中的特征,在网络的后半段通过连续两个全连接层进行类别识别和位置判断。但相比其他网络,YOLOv1架构在设计上的精髓在于其最后的7x7x30的输出解读、对模型参数的训练及其损失函数的评估设计方面。 YOLOv1模型的输出信息 YOLOv1的输出是一个7x7x30的三维张量,可以认为该模型把输入的448x448分辨率切分为7x7的网格,从每个网络中搜索待识别对象的中心点及其Bounding Box、置信度、对象类别等信息,每个网络通过一个30个元素的向量来表征该网格中检测到的这些信息。 如下图所示,每个网格会输出两个Bounding BOX的信息,每个BOX的信息包含检测物体中心点坐标(相对于该网格左上角)以及该检测物体的长宽(相对于整幅图即448x448的长宽比例),这个检测框的置信度信息。 !image.png 需要注意,YOLOv1对于图像中物体的检测逻辑是,如果一个物体的中心点落在某个网格内,就由这个网格负责检测和输出该物体对应的Bounding Box等信息。因此,每个网格输出BOX信息中,x和y表示这个待检物体的中心点相对于当前网格左上角的位置,而w和h则表示这个网格预测Bounding Box的相对于整个输入图像分辨率的长和宽。 每个BOX都有一个置信度Confidence的参数,这个参数用于表征当前BOX中真正包含待检物体的概率,如果这个BOX中包含有某个待检物体的中心点的话,这个置信度参数会比较高,达到接近1的程度;而如果这个BOX中不包含任何待检物体的中心点,那么其置信度会接近0,这样就会在后面的过滤算法中被过滤掉。 20 Classes则用于表示各个待检物体类别的最终识别概率。对于VOC数据集而言,可以支持对20种类别的检测识别,如果是COCO数据集的话,各个网格输出的类别数量就需要修改为80个种类的识别概率了。 两个BOX,一组类别识别概率? 从以上所总结的YOLOv1模型各个网格的输出信息可以看到,每个网格的输出包含了两个预测BOX的信息,以及一组识别待检物体的识别概率。那么如果一个网格中刚好包含了两个待检物体的中心,那这组类别识别概率究竟表示的是哪一个物体的识别类别呢? 答案是:这种情况下肯定会漏检,这也是YOLOv1在密集小目标场景下召回率骤降的根本原因所在,也是后续版本(YOLOv2起)引入anchor、把“负责检测单元”从网格细化到“网格+anchor”甚至“pixel”级别的动机。 基于下图来理解这个事情: !image.png 对于每个网格所输出的两个BOX而言,这个网格在输出时只会把两个BOX与实际待检物体吻合度(实际上就是IOU指标)最好的那个框标记为正样本。对于上图而言,实际物体的Bounding Box是上图中的浅灰色框,那么预测框A的吻合度远好于预测框B。那么,这个网格的输出中,预测框A的置信度就会比较高,预测框B的置信度会被压得比较低,而这个网络中所包含的类别信息对应的也就是预测框A所圈选的部分图像的类别识别概率。 因此,YOLOv1整个输入图像被划分为7x7个网格,每个网格输出两个BOX,看起来YOLOv1最多可以实现对7x7x2=98个物体的检测,但实际上,每个网格最多只能输出一个BOX及其对应的识别类别,所以整个图像最多也就只能识别49个独立物体。即:每个网格输出的两个BOX不是为了确保在一个网格中检测出来两个物体,而是为了在一个网格中检测出来物体以后,通过两个BOX提供二选一的选项,让模型能够选择匹配性最好的框来定位其位置。 NMS NMS:Non Maximal Suppression,非极大值抑制算法,顾名思义就是抑制不是极大值的边框,这里的抑制通常是过滤掉针对同一个检测物体的冗余边框。 如上所述,对于每一个输入YOLOv1模型进行推理和检测的图片而言,模型会通过7x7x30这个三维张量输出7x7x2=98个预测框。这98个预测框还需要经过NMS算法的过滤才真正输出最终的检测结果,否则一个物体可能会包含多个输出检测框。 NMS算法的基本执行逻辑:利用得分高的边框抑制得分低且重叠程度高的边框。 NMS算法的执行,按照其所支持的类别分类独立进行处理,每进行一次NMS运算,得到一个类别的最终检测框。但不同类别的计算逻辑是完全相同的,最终把所有类别的最终检测框汇总在一起就是模型最终的检测结果。 步骤0: 首先针对某个指定的类别,通过BOX的置信度confidence和这个类别的识别概率相乘得到这个BOX针对这个类别的分数score。因为模型输出98个BOX,所以每个类别也就有98个score。 步骤1:对这个类别的98个score与NMS的检测阈值(例如0.2)进行比较,小于这个阈值的BOX删除掉,只保留大于阈值的BOX放入备选列表。 步骤2:对备选列表中所有BOX基于score的大小降序排序,把备选列表中score最大的那个BOX直接放入该类别的检测输出中。 步骤3:计算备选列表中score值最大的BOX,与备选列表中所有其他BOX计算IOU指标,当计算出来的IOU值>IOU阈值(一般为0.5)时,表示两个BOX检测的是同一个目标,此时把score较小的那个BOX从备选列表中删除。 步骤4:从当前备选列表中把score最大值的BOX删除掉。 反复执行以上的步骤2-步骤4,直到备选列表的BOX为空。此时就得到了这个类别的检测输出结果,然后对所有的20个类别全部执行完成以上步骤,把所有类别的检测输出结果汇总,就是模型最终的检测输出。 !image.png 如上图是针对小狗这个类别的NMS算法过滤流程。 - 首先针对小狗这个类别,通过每个BOX的置信度及其所属网格中小狗这个类别的识别概率相乘,得到98个分数score。 - 对这98个score与阈值0.2进行比较,删掉阈值小于0.2的BOX,把其他的BOX放入备选列表中。 - 对备选列表中的BOX基于score进行降序排序,score最大的BOX直接放入小狗这个类别的检测输出中,同时执行以上描述的NMS算法,每次都是把score最大的BOX放入该类别的输出列表,并通过IOU去掉与该BOX重合度较大的其他BOX(避免一个检测物体出现多个检测框),直到整个备选列表中的BOX全部清空。 参考资料 - 《深度学习之Pytorch物体检测实战》6.1 无锚框预测YOLOv1 - All of the start: YOLOv1. YOLO: you only look once. | by John Shen | Medium - YOLOv1 詳細解讀. 設計概念: | by Steven Meng | Medium

YOLOv2模型网络架构解读

本文对YOLOv2模型的BN层、Darknet-19主干网络,以及模型的总体结构进行了详细的总结和学习。 批归一化BN层的引入 在YOLOv1模型的主干网络所使用的卷积结构,仍然采用的是传统的线性卷积+非线性激活函数的模式,并没有用到在后来十分流行的各种归一化层的设计。因为批归一化层(Batch Normalization,BN)在越来越多的模型中应用,其工作性能得到了业界的广泛认可,因此逐步成为卷积神经网络的标配设计。YOLOv2模型的架构设计中也就顺势引进了被广泛验证的BN层。 具体来讲,就是在网络架构的卷积结构由之前的线性卷积+非线性激活函数的模式,变成了线性卷积层+BN层+非线性激活函数的形式。这种结构也称之为CBL(Conv-BN-LeakyReLU)结构。 !image.png Darknet-19主干网络 如[[YOLOv1模型网络架构解读]]所介绍的,YOLOv1模型的主干网络采用了类似GoogLeNet的设计。到了YOLOv2版本,作者设计了一个新的名为DarkNet-19的网络结构取代了前一代的主干网络。 Darknet-19网络包含有19个卷积层,而且每个卷积层都是以上所描述的CBL结构,即卷积层+BN层+激活函数的形式。下图为Darknet-19网络架构各层的配置参数: - 下表Output对应的特征图维度,对应的是输入分辨率为224x224的图片。当Darknet-19作为YOLOv2的主干网络使用时(输入图像分辨率416x416),其各层输出的特征图维度要对应发生变化。 !image.png > 以上的架构图中,darknet-19网络可以分为两个部分,前半部分是特征提取部分,预训练完成后会被作为YOLOv2的主干网络;最后的卷积层+全局平均池化层+Softmax部分是分类部分,在YOLOv2模型中会被去掉。 可以看到,以上的darknet-19网络架构中,共包含了19个卷积层(3x3卷积和1x1卷积),5个2x2最大池化层,最后借鉴了NiN网络(参考[[CNN经典网络模型架构学习之NiN]])的做法,使用全局平均池化层代替全连接层,在最后一层使用Softmax输出1000个分类的预测结果。 Darknet-19的作者团队用ImageNet数据集去训练网络参数,最终的检测精度上,DarkNet-19网络达到了VGG网络相同的水平,但模型更小(参数量只有25.5M),浮点运算量更是只有其1/5左右,因此运算速度极快。 Darknet-19网络在ImageNet数据集上预训练完毕后,去掉其中用于分类任务的最后一层卷积、平均池化层和Softmax层等,然后将其用作YOLOv2的新主干网络。 Passthrough与Reorg 在主干网络的设计上,除了设计了一个新的darknet-19主干网络以外,YOLOv2还借鉴了SSD模型使用更高分辨率特征图的设计。 > 通常,不同的特征图的分辨率不同,越是浅层的特征图,越被较少地做降采样处理,因而分辨率就越高,所划分的网格就越精细,这显然有助于去提取更多的细节信息。 对于YOLOv2的主干网络设计而言,除了主干网络darknet-19特征提取部分最后一个卷积层(第19个卷积层,如下图红色箭头所示)输出的低分辨率特征图以外,YOLOv2还从darknet-19网络的第13个卷积层输出(如下图绿色箭头所指)高分辨率特征图,然后把两个特征图合并在一起作为整个主干网络的特征提取输出。在这种情况下,主干网络的特征输出中网络尾段低分辨率的特征和网络中段高分辨率的特征,提取出来的信息更加完整。这就是YOLOv2模型的所谓Passthrough架构。 !image.png 在输入图像分辨率为416x416的情况下,主干网络中段输出的高分辨率特征图的维度是26x26x512,而其末端输出的低分辨率特征图的维度是13x13x1024。不同分辨率的特征图要如何才能合并在一起输出呢?答案是Reorg操作。 Darknet-19末端的低分辨率特征图维度为13x13x1024,再经过一个3x3,stride=1的CBL卷积块处理,仍然是13x13x1024维度。 而Darknet-19中段的高分辨率特征图维度为26x26x512,则要执行一个reorg的操作,总体数据量不变,但是分辨率会减半,通道数增加为原来的四倍。对于单通道特征图的Reorg操作(相当于只是把数据重排一下而已)如下图所示,一个通道变成了4个通道。通过这样的操作,中段高分辨率的维度就变成了13x13x2048。 !image.png 经过以上的各自处理后,末端和中段的特征图的空间分辨率都变成了13x13,现在就只需要把所有的特征图合并在一起就可以了。因此经过以上合并后,输出的特征图的通道数就是1024+2048=3072通道,整体的特征图数据维度就是13x13x3072。 YOLOv2模型的总体架构 YOLOv2模型的总体网络架构如下图所示: !image.png > 需要注意的是,上面的总体网络架构图与Passthrough部分所提供的网络架构图,在主干网络经过merge以后输出的通道数有分歧。上图中中段高分辨率特征图在Passthrough的处理过程中会通过一个卷积CBL块先降低到64通道,再reorg为13x13x256维度,因此Merge后输出的总的特征数据维度就是13x13x1280;而前一节的网络结构图中,Passthrough的处理过程中对高分辨率特征数据不做降通道操作,直接reorg为13x13x2048维度,这样Merge后的总的特征输出数据的维度就是13x13x3072。不过Merge后输出无论是1280通道还是3072通道,后续都会通过一个CBL卷积块把通道数降低为1024。真正用于最终预测的特征数据维度都是13x13x1024。 输入图像的分辨率为RGB 3通道416x416分辨率的图像,经过其主干网络darknet-19处理并合并其末端和中段特征提取数据后,最终输出的特征图为13x13x1024维度。然后再基于这个13x13x1024维度的特征数据进行图像中待检物体类别和位置的预测。 最后的这个预测过程就比较简单了:就是对13x13x1024的特征提取数据执行1x1卷积操作。最终的输出数据维度根据待检物体的类别数量来确定。YOLOv2的每个网格中包含有5个预测BOX(在YOLOv2的术语也称为anchor),每个BOX所包含的数据有:中心坐标x和y,长宽度w和h,置信度confidence,各个类别的概率(依据待识别物体的类别种类数量而定)。 - 对于COCO数据集而言,可以支持80个类别的检测。那么每个anchor的数据量是85,每个网格包含5个anchor,因此每个网格的数据量就是425,针对COCO数据集而言模型输出推理结果的数据维度就是13x13x425。 - 而对于VOC数据集而言,支持20种类别,每个网格的数据量是125,针对COCO数据集而言模型输出推理结果的数据维度就是13x13x125。 从以上的YOLOv2的总体架构图可以看出,所有的网络层都是卷积网络,YOLOv1的全连接层被彻底拿掉了,所以YOLOv2也就成了全卷积网络。 参考资料 - 《YOLO目标检测》杨建华,李瑞峰 第6章 YOLOv2 - DAY28 深度學習-卷積神經網路-Yolo v2 (二) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

YOLOv1模型网络架构解读

YOLOv1的架构总结 YOLOv1的架构设计逻辑是:仅使用一个完整的卷积神经网络结构来端到端地检测图像中所包含的目标及其位置,对应于当时在目标检测领域主流的R-CNN系列为代表的two-stage流派,YOLOv1的架构可以说是开启了one-state来实现目标位置检测+类别识别的架构设计思路的先河。 YOLOv1的网络架构参考了GoogLeNet网络来设计主干网络,但并未采用GoogLeNet的Inception模块,而采用了串联的1x1卷积和3x3卷积层所组成的模块,因此其主干网络的架构非常简单。 YOLOv1网络的架构图如下所示: !image.png 从以上YOLOv1的架构图可以看到,YOLOv1模型包含有24个卷积层用于提取输入图像的特征信息,以及两个全连接层用于预测图像中所包含的目标位置、置信度以及类别识别几率信息。卷积层部分,采用了多个1x1卷积层用于压缩特征图的通道数量。而激活函数方面,除了最后一层采用线性激活以外,其他各层均采用了Leaky ReLU。 整个网络架构与各种经典的基于卷积神经网络的图像识别模型没有太大差异,其差异主要在图像的分类和输出部分。最后一个卷积层输出的特征图是1024通道7x7分辨率。这个特征图展平(flatten)为7x7x1024的一维向量,经过连续的两个全连接层(分别包含4096个和1470个神经元)进行处理,最后的1470个神经元的全连接层输出最终的预测。因为YOLOv1执行的目标检测任务是一个空间任务,所以这个全连接层的1470个输出数据转换为7x7x30的三维张量,其中7x7是这个三维张量的空间分辨率,30则是这个张量的通道数量。 后续部分对这个7x7x30所代表的目标检测信息详细解释。 因为在图像在进行目标位置及其类别的检测和识别需要更细粒度的视觉信息,所以YOLOv1的图像输入分辨率从各种图像识别模型标准输入的224x224分辨率,提升为448x448。 YOLOv1的参数量 YOLOv1模型存在的一个明显的问题就是参数量实在是太大了。 仅计算最后一个卷积层的输出(维度为7x7x1024),与第一个全连接层(4096个神经元)之间的连接,其参数量就达到了:7x7x1024x4096,大概2亿个参数。这样势必导致模型的推理速度很慢,以及模型运行需要的内存空间也会比较大。 不过这也是没办法的事情,毕竟在YOLOv1模型所推出的2015年,卷积神经网络架构的标准设计就是卷积层与全连接层相连,各自分工,那么全连接层参数众多的问题就很难避免,直到全局平均池化层的出现,才解决了这个问题。 YOLOv1的执行流程 YOLOv1对进行推理的图片所进行的目标检测流程主要可以分为输入图像的预处理、基于卷积神经网络提取特征、基于全连接层进行目标及其位置检测、最终输出检测结果这几个步骤。 !image.png 输入图像的预处理环节主要包含两个方面。首先如上所述,YOLOv1的输入图片的分辨率为448x448的RGB图像,所以对于不同分辨率的图像,首先要缩放到这个尺寸。同时为了避免输入图像的长宽比失真,对于长宽比非1:1的输入图像,还需要通过填充等步骤来进行处理。经过以上分辨率缩放等环节的处理后,再执行像素归一化,把输入图像中各个像素的值从[0,255]缩放到[0,1]区间,或进一步标准化为均值为0、方差为1的分布。 接下来经过以上预处理以后的RGB三通道448x448分辨率的图像,经过连续24个卷积层(包含对应的池化层)处理,逐步提取其中所包含的特征,输出7x7x1024的特征图。这个7x7x1024的特征图展平之后,再经过两个全连接层基于特征数据进行目标种类和位置检测,检测结果resize为7x7x30的三维张量,对这个输出的三维张量进行解析就可以得到图像中所包含的目标种类及其位置。 _整个执行流程的关键,实际上就在于卷积层输出的7x7x1024特征图中所包含的信息,与整个模型最终输出的7x7x30三维张量的映射_。 从以上的这一点上,可以把整个流程看成是对输入的448x448分辨率的图像切分为7x7的网格,无论是卷积层输出的7x7x1024的特征图,还是最终输出的7x7x30的三维张量,其中的1024和30对应的都是每一个网格的特征信息和检测结果:对于每一网格而言,经过24个卷积层的处理,得到一个长度为1024的特征向量,这个特征向量中包含了该网格中的某种高级特征信息。然后在全连接层中再通过遍历这些网格、处理其中的特征信息来预测每个网格处是否有目标的中心点坐标,以及相应的目标类别,把结果输出为一个长度为30的向量,用以表征该网格的检测信息。 !image.png 因此,从以上流程可以看到,图像的输入以及卷积层的检测,与之前的图像识别模型如VGG、GoogLeNet等并无区别,YOLOv1的最大差异在于全连接层的处理结果,需要resize为一个三维张量进行输出。这个三维张量中所包含的数据,就对应了从输入图像中检测到的目标种类和位置。 YOLOv1的输出信息解读 如上所述,YOLOv1的输出为一个7x7x30的三维张量,对应为7x7个网格,那么每个网格所包含的输出信息就是一个长度为30的一维向量。这个一维向量包含以下信息:两个目标检测边界框BOX1和BOX2的参数,以及20个目标检测种类的识别概率。 每个BOX的参数包含其中心坐标(x,y)和长宽参数(w,h),以及这个BOX检测的置信度confidence,因此每个BOX有5个参数。两个BOX的5个参数,再加上20个目标检测类别的识别概率,总共加起来就是长度为30的一维向量。如下图所示。 > 为什么是20个种类?这是因为YOLOv1的原论文是使用VOC数据集进行训练的。VOC数据集总共支持对20个种类的目标进行检测,所以默认YOLOv1模型的输出就是20个类别的识别率。如果使用支持80个类别的COCO数据集,那么每个网格的输出长度就应该是2x5+80=90。 !image.png 如何检测? YOLOv1在“谁来负责进行目标预测”的这个问题上有一个最核心的设计:待检物体的中心点落在哪个网格cell里,就由那个cell负责把这个物体找出来。 这一点,对于理解YOLOv1的输出而言非常关键。 以下图为例,尽管小猫这个物体在整个图像中占据的范围跨越了多个网格,但其中心点在粉色网格中,那么YOLOv1模型的输出中,就只有粉色网格所输出的BOX包含有小猫这个待检物体的相关检测信息,其他网格就都成为背景框。 !image.png BOX的坐标位置参数 如上所述,每个网格中包含有两个BOX的检测结果,其中的每个BOX用x,y,w,h来表示其在网格中的位置。仍然使用上面的小猫图对x,y,w,h参数进行解释。 粉色网格输出的其中一个BOX会对应对小猫的检测框信息,其中(x,y)表示这个预测BOX的中心相对于其所在网格左上角的偏移量。以上图的小猫为例说明,粉色框中的红点为小猫的中心点,那么小猫这个检测框(大的红框)所应对的(x,y)就是红点相对于粉色框左上角的偏移量。w和h则表示这个预测BOX(大的红框)的width和height在整幅图像(448x448分辨率)中的长和宽上所占的比例。 BOX的置信度Confidence如何理解? BOX边界框的置信度参数Confidence,本质上就是用来判断这个BOX中是否包含有某个待检物体的中心点。理想情况下,依据YOLOv1的设定,包含有待检物体中心点的网格所输出的边界框置信度很高,而不包含中心点的网格则输出的边界框置信度很低甚至为0。 > BOX的置信度直接决定一个网格中是否包含有某个待检目标的中心点。 在后期处理中,通过判断Confidence这个参数,就可以知道当前网格中是否包含有某个待检物体的中心点,在确认该网格包含有待检物体的中心点的情况下,再检查20个目标检测类别的识别概率,判断这个待检物体的类别。 如果同一个网格中包含了两个物体的中心点? 按照YOLOv1的设计,每个网格的输出信息只能把两个预测框里最好的那一个标记成“正样本”,并且只能给它分配 1 个类别标签。 因此,当两个不同物体的中心不幸落在同一个网格里时,YOLOv1网络只能: - 选其中一个物体(通常是与默认框 IoU 更大的那个)当作该网格的回归/分类目标; - 另一个物体会直接被当成不存在,训练时既无坐标损失也无类别损失; - 在推理阶段,这个网格最多只能输出第一个物体的检测框,第二个物体必然漏检。 这就是 YOLOv1 在密集小目标场景下召回率骤降的根本原因。 参考资料 - YOLOv1 詳細解讀. 設計概念: | by Steven Meng | Medium - 《YOLO目标检测》第3章 YOLOv1 - YOLO演進 — 1. 在上篇文章有提到過 YOLO 的作法以及如何訓練,在這篇再更詳細的說明YOLO… | by 李謦伊 | 謦伊的閱讀筆記 | Medium

CNN经典网络模型架构学习之Resnet

Resnet网络架构是由微软亚洲研究院的何恺明等人在2015年提出,其论文为《Deep Residual Learning for Image Recognition》,在2015年的ImageNet图像识别竞赛中获得了分类、检测、定位等多个任务的冠军。这个模型的核心贡献在于解决了深层网络的训练难题,使得训练上百层甚至上千层的网络成为可能,极大地推动了计算机视觉的发展。 深层网络的训练问题 在Resnet网络架构提出之前,人们在进行深度神经网络的研究和训练过程中发现: - 网络的层次结构越深,层次越多,从理论上看其表达能力就越强,就越能够用于处理复杂的问题。 - 但是,当网络层数达到一定程度之后,模型训练和识别的准确率反而会下降。这种情况下的下降不是过拟合的问题,因为无论是训练还是验证数据的准确率都会变差。网络层数的增加,不仅没有优化网络的性能,反而导致其输出的精度指标变差,这个问题被称为深度网络的退化问题。 如下图所示,更深的网络层数(56层 vs 20层)不仅没有带来更加的效果,反而在训练集和测试集上均又有所“退化”。 !image.png > 需要注意的是,resnet所面对的问题,并非是深度网络训练过程中梯度反向传递时存在的梯度消失和梯度爆炸的问题。梯度消失和梯度爆炸的问题,是因为深度网络的层数增加之后,在训练过程的梯度向前反向传递流程中,梯度数值趋向为0或者始终保持为无穷大,导致网络参数几乎无法更新或者保持为发散状态的情况。实际上这两个问题在网络中增加BatchNorm的方式已经得到解决。Resnet面对的网络退化”问题,是一个独立于梯度消失/爆炸的全新问题。 Resnet通过在模型的架构设计中引入残差块的方式,解决了以上的网络退化问题,自此深度网络的层数得到了大幅度的提升。从下图可以看到,2014年的ImageNet数据集图像识别竞赛的冠军得主GoogLeNet只有22层,而到了2015年的冠军ResNet,其网络深度就达到了152层。 !image.png 残差块 传统的神经网络结构,一般都是将不同的卷积层、池化层、全连接层等按照一定的顺序简单的连接在一起,并且网络中的每一层只接收来自其上一层的信息,不会跨越层次结构接收更靠前的层的输出,并且每一层的输出也只会传递给下一层。如下图左侧正常块的各层连接模式。这样的层次结构模式,在网络层次加深以后,这种单一的连接方式会导致网络性能的退化。 而ResNet引入了残差块的概念。在传统的网络连接中,加入了短连接,模型的输入数据可以通过跨层连接的方式,更快的向前传播。如下图右侧所示。 !image.png 残差映射为何可行? 假设我们的原始输入为 x ,而我们期望网络学习到的理想的映射为 f(x) , 如果采用传统神经网络连接模式的正常连接结构的话,就需要直接从网络块的输入 x 拟合出来其映射 f(x) ,而如果采用残差块的模式,对于这个残差块本身而言,需要拟合出来的就不是 f(x),而只需要拟合出来残差映射 f(x)-x 就可以了。而残差映射的学习在现实中更容易实现和优化,这就是残差块能够解决问题的逻辑所在。 以下图为例进行解释。以下网络块要学习的映射是 H(x),如果直接用其输入 x 学习和训练出来对应的映射 H(x),这就是传统网络连接的做法。对于Resnet所使用的残差块而言,输入仍然是 x,输入也仍然是需要学习的映射 H(x),把输入 x 通过短连接(short connection)的方式,直接连接到输出,与残差块的输出 F(x) 相加,作为整个模块的输出 H(x),这样的话,对于残差块内部参数的学习的目标,就不再是直接从 x 到 H(x) 的映射,而是 x 到 F(x) 也就是 H(x)-x 的映射。_而在神经网络中学习和优化残差而言,会比直接学习最终的映射要来得更加容易。_ !image.png 举个简单的例子。假设输入 x 为2.9,模型的期望输出 H(x) 为3.0,两者之间的差值为3.0-2.9=0.1。 当使用传统网络结构的普通块来进行以上的映射,其从输入到输出的误差的变化是以其期望输出的3.0来进行评估;而如果按照残差块来实现以上映射,对于残差块内部参数的训练而言,其要训练的目标是残差 F(x)=H(x)-x=0.1,也就是说这个网络块的训练误差的变化是以这个差值的0.1来进行评估。 由此可以发现,采用残差的方式,可以更加凸显出来模型训练过程中的误差变化,模型也就更加容易训练和收敛。 典型残差块的结构 在作者的论文中,作者提出了两种残差块的结构,如下图所示: !image.png 对于层数不是很多的网络,使用左侧的连续3x3卷积-relu-3x3卷积的方式构成基础的残差块。而对于大于50层的网络,考虑到训练的深度和时间,作者提出了右侧的这种bottleneck的设计方式,增加了1x1卷积降维后再升维的方式,在节省大量参数的同时,兼顾考虑了深层网络的性能和时间复杂度。 Resnet模型架构 Resnet有不同网络层数的版本,例如18层,34层,50层,101层,152层等。其中最常用的是50层的网络结构,即Resnet-50。 Resnet模型的整体架构,基本上可以分为三个部分:输入层,中间连续的残差块,以及最后的输出层。 其中,输入层和输出层对于Resnet的所有版本都是相同的,各个版本的区别主要就是中间连续的残差块的数量和结构。 ResNet的输入层,也就是其网络结构的前两层,跟CNN经典网络模型架构学习之GoogLeNet笔记所介绍的GoogLeNet中的一样: 输入图像首先进行7x7,步幅为2的卷积处理,,输出通道数为64的特征图,然后接步幅为2的的2x2最大池化层。 Resnet中间的残差块部分,就由连续相连的基本残差块组成。只不过,对于各个不同版本的Resnet模型而言,其所包含的残差块的配置和数量各有区别。 !image.png 以Resnet34为例,残差块部分包含有:连续三个64通道3x3卷积残差块,连续四个128通道3x3卷积残差块,连续六个256通道3x3卷积残差块,以及连续三个512通道3x3卷积残差块。最终输出7x7x512的特征图给输出层部分。整个Resnet34模型的结构,共计1个7x7卷积层,32个3x3卷积层,以及一个全连接层,总共就是34个参数层。 下图为Resnet34模型的网络架构图: !image.png 残差块部分所输出的特征图,再继续经过全局平均池化层的处理,送入最后一层的1000个神经元的全连接层,配合Softmax激活函数实现对ImageNet数据集1000个识别类型的识别概率输出。 参考资料 - ResNet: Revolutionizing Deep Learning in Image Recognition - The Annotated ResNet-50 | Towards Data Science - ResNet详解与实践-CSDN博客 - 直觀理解ResNet —簡介、 觀念及實作(Python Keras) | by Chi Ming Lee | Medium

目标检测模型的评价指标详解:mAP,召回率,准确率

对于计算机视觉中的目标检测领域而言,主要的评估指标就是mAP与召回率、准确率。本文对IoU、准确率、召回率、AP以及mAP的概念和计算逻辑进行了详细的整理。 IOU IOU:Intersection Over Union。 对于目标检测算法的执行而言,每一张训练图片中的每个待检测物体,都会标注一个该物体位置对应的bounding box,这个标注的bounding box就是真实物体区域对应的检测区域。而目标检测算法对图片中物体所进行的检测和推理,会输出一个检测出来的Bounding box。IOU指标就是通过两个Bouding Box重合的程度,来评估目标检测算法对于该物体的检测精度。标注Bounding Box与检测Bounding Box的重合度越高,表示检测的精度越好。 !image.png 用下图可以非常直观的解释IoU如何计算。具体的解释就是:系统检测到的物体区域与真实物体区域的交集(如下图中Area of Overlap部分,也就是Intersection部分),除以两个区域之间的并集(如下图中Area of Union部分,也就是Union部分)。 !image.png 因此,对于一个检测图片中的某个待检物体而言,其IoU指标的计算,就是用检测框与标记框的交集面积,处于两个框的并集面积。这个值的计算结果当然是在0-1之间,0表示检测结果与真实的标记框完全无关,检测完全错误,而1则表示检测框和标记框完全重合,对于算法的评估而言,这个值当然是越高越好。一般使用IoU=0.5来作为判断识别率的基准,大于这个阈值表示是有效的检测,否则是无效的检测。 在理解以上IoU概念的基础上,对一张图片中的多个物体进行目标检测,就可能存在多种检测结果: !image.png - TP(True Positive):算法准确的检测到了图片中的物体,并且两个检测框的IoU重合度大于0.5,因此是一个正确的预测。 - FP(False Positive):算法检测出来的检测框,与该图片所有物体的标记框的重合度都小于0.5,最典型的就是把背景识别成了物体,因此是一种误检的情况。 - TN(True Negative):本身是背景,算法也没有输出对应的检测框,因此是正常现象,在目标检测的指标计算中不需要考虑。 - FN(False Negative):本来需要算法检测出来的物体,算法没有检测出来,所以相当于是漏检的情况。 以上检测结果的分类整理成表格就是: !image.png 召回率与准确率 准确率:Precision。召回率:Recall。 有了以上对于IoU以及目标检测算法的各种不同检测结果的类型的理解,关于目标检测算法执行准确率和召回率的概念及其计算就比较简单了。 所谓的准确率,表示的就是:目标检测算法所输出的检测结果中,其类别判断和位置检测(IoU>0.5)均正确的检测所占的比例。准确率的计算公式如下图所示: !image.png 而所谓的召回率,则表示的是:目标检测算法对图片中的物体进行的检测,把多少个物体正确地检测(类别判断正确,且IoU>0.5)出来了。召回率的计算公式如下图所示: !image.png 以上图中的茶杯的检测为例,准确率表示的就是,算法输出的茶杯检测结果,真的是茶杯的情况所占的比例。而召回率则表示的是,图片中所有的茶杯,被算法准确检测出来所占的比例。 AP与mAP AP:Average Precision,表示某个类别的检测精度。mAP:mean Average Precision,表示所有类别的平均检测精度。 下面以一个例子来更详细地解释说明AP的计算过程。 以某个数据集进行小狗目标的检测为例。数据集中真实标注的小狗目标有8个,分别对应于GT1-GT8(这里的GT表示Grounded Truth的意思,也就是通过手动标注出来的真实的小狗目标所在的位置)。而通过算法检测到的小狗目标及其位置有10个,使用BB1-BB10来表示(BB表示检测算法检测到的每个小狗目标的Bounding Box)。 使用算法对数据集中小狗目标的检测结果,部分检测位置的Bounding Box只检测到小狗的头或者脚,也就是说IoU很低(例如BB3, BB4, BB5, BB7, BB8这个检测输出的IoU均比较低),而且两个真实的小狗目标GT6和GT7没有能够检测到(FN=(GT7, GT8)),也就是说这两个真实目标漏检了。基于以上信息整理目标检测算法的执行结果如下图所示:需要注意的是,目标检测算法输出的每个检测结果,不仅包含这个检测结果对应的检测类别,Bounding Box的坐标信息,还有一个检测的置信度(Confidence),也就是这个检测目标正确的概率。 !image.png 从上图可以看到,对于一个真实的待检小狗目标,检测算法可能输出多个独立的检测结果与之对应,例如BB1和BB3对应的都是GT1。而一般情况下,只有在IoU>0.5的情况下,我们才会认为算法输出的检测结果是正确的TN,因此对以上算法检测所输出的10个检测结果BB1-BB10进行IoU判断如下: !image.png 然后按照流程,基于检测结果的置信度(即上图中属于狗的概率)对其进行排序: !image.png 然后按照以上排序,在不同的rank上计算各自的准确率Precision和召回率Recall。 - 对于Rank1而言,其准确率=(>=rank1的TP数量)/(>=rank1的TP+FP的总数量)=1/1=1,其召回率=(>=rank1的TP数量)/(TP的总数量)=1/8=0.125。 - 对于Rank2而言,其准确率=(>=rank2的TP数量)/(>=rank2的TP+FP的总数量)=2/2=1,其召回率=(>=rank2的TP数量)/(TP的总数量)=2/8=0.25。 - 对于Rank3而言,其准确率=(>=rank3的TP数量)/(>=rank3的TP+FP的总数量)=3/3=1,其召回率=(>=rank3的TP数量)/(TP的总数量)=3/8=0.375。 - 对于Rank4而言,其准确率=(>=rank4的TP数量)/(>=rank4的TP+FP的总数量)=3/4=0.75,其召回率=(>=rank4的TP数量)/(TP的总数量)=3/8=0.375。 - 以此类推,得到rank5的准确率=4/5=0.8,其召回率=4/8=0.5。 - rank6的准确率=5/6=0.833,其召回率=5/8=0.625。 - rank7的准确率=5/7=0.714,其召回率=5/8=0.625。 - rank8的准确率=5/8=0.625,其召回率=5/8=0.625。 - rank9的准确率=5/9=0.556,其召回率=5/8=0.625。 - rank10的准确率=5/10=0.5,其召回率=5/8=0.625。 因此,以上根据目标检测算法输出的10个检测结果,得到10组准确率和召回率的数据如下: !image.png 根据10组(Precision,Recall)就可以画出目标检测算法输出以上结果对应的P-R图: !image.png 所谓的AP指标计算上的定义,就是上面P-R中标记的蓝色P-R曲线下的面积或者积分。因为Precision和Recall指标都是在0-1之间,所以以上计算出来的AP指标当然也是在0-1之间。 !image.png P-R曲线面积或者积分的计算比较复杂,所以可以采用上图提供的两个离散序列公式来计算AP指标。 第一个公式中,P(k) 是第k个rank的准确度,而 Δr(k) 是指第k个rank的召回率与第k-1个rank的召回率的差值,因此按照该公式计算AP指标就是: 第二个公式中,P(k) 是第k个rank的准确度,而 rel(k) 的定义则是,在第k个rank为有效检测TP时, rel(k) 为1,否则为0。number of relevant documents表示总的TP个数,这是是8个(GT1-GT8)。因此基于该公式计算AP指标就是: 所以可以看到,以上的两个公式虽然算式不同,但是最终的结果殊途同归,最后计算出来的AP值都是0.579。 以上AP的计算说完了,那么mAP的计算就比较简单了。上面所进行AP的计算,只是针对小狗这个类别计算出来的AP,一般情况下目标检测算法可以输出多个不同类别的检测结果,例如在COCO数据集中就包含了80个类别的标注信息,那么一个目标检测算法的mAP指标,就是先独立的计算它所支持的每个类别的检测AP指标,然后再把所有的AP值加起来求平均值就是整个算法对所有类别检测性能的mAP指标。 !image.png 参考资料 - 《深度学习之Pytorch物体检测实战》1.2 - Yy's Program: 影像辨識常見的IOU、AP、mAP是什麼意思? - Learning Model :什麼是AP/mAP/IoU?[轉錄] | by Ryan Lu | AI反斗城 | Medium

CNN经典网络模型架构学习之GoogleNet

本文对经典卷积神经网络存在的问题,以及计算机视觉图像识别领域经典的GoogLeNet架构设计的相关知识点进行了完整和详细的总结。 AlexNet和VGGNet存在的问题 在CNN经典网络模型架构学习之AlexNet和CNN经典网络模型架构学习之VGGNet两篇笔记中,分别总结了经典CNN网络中的AlexNet和VGGNet两种模型的架构和实现。 相对而言,AlexNet的层次和参数都比较少,这也就限制了该网络从数据中提取和学习复杂模式的能力,最终的识别和检测效率不高。 VGGNet加大了CNN神经网络的深度,参数层达到了16层和19层,但是因为深度而导致的其他问题也就应运而生: - 首先是梯度消失问题。众所周知,在神经网络的训练过程中,通过损失函数来评估模型的运行效果,根据损失值使用反向传递法+梯度下降法从输出层向前一层层更新其运行参数,通过这种方式来实现最终损失函数输出值的降低。但是,在网络深度加大的情况下,相同的损失值在反向向前一层传递的过程中会越来越小,最终导致网络中的参数(尤其是前半部分的参数)在训练中很难得到有效的更新。 - 其次是深度模型对算力要求过高。 以VGG16网络模型为例,其参数高达138M,模型参数大小超过500MB,模型的训练和推理所需要的算力都比较高。 GoogLeNet的架构设计,对传统的CNN网络架构进行了重构,来尝试解决以上问题,并得到了较好的应用效果。GoogLeNet,也称为Inception-v1,首次在2014年提出,在2014年的ILSVRC竞赛中得到了第一名,Top-5 Error只有6.67%。_更为难能可贵的是,GoogLeNet的参数只有6.8M,比AlexNet少9倍,比VGG16更是少了20倍_。 GoogleNet的最大创新点是引入了Inception模块,这也是GoogLeNet同样被称为InceptionNet的原因所在。 Inception Module Inception模块是一种允许在多个尺度上并行处理数据的构建块,这使得网络能够比以前经典卷积神经网络架构更有效地捕获不同尺度的特征。 一个Inception模块通常由多个卷积层组成,每个卷积层使用不同的卷积核尺寸。而且不同于之前的网络模型,Inception Module中的多个卷积层以并行的方式工作,因此对于输入的同一张特征图,网络可以使用不同尺寸的卷积核进行计算。这些卷积层计算完成后,其输出结果拼接在一起,然后传递给一个池化层进行处理。以上就是Inception模块的总体设计理念。 从Inception的结构上看,GoogLeNet提出的Inception Module可以分为两种类型:Native类型和降维类型。 !image.png 两种Inception Module的共同特征是:Inception Module从前一层特征图得到的输入数据,并行进行1x1卷积、3x3卷积、5x5卷积以及3x3的最大池化处理,以上四个部分的处理结果拼接在一起,做为整个Inception的输出。而其不同点则在于:_降维版本的Inception Module在3x3卷积、5x5卷积之前,以及3x3最大池化之后分别增加一个一个1x1的卷积操作。_ 其中: - 1×1 卷积块用于在保持输入特征图分辨率不变的情况下,在特征图的像素级上捕捉局部细节。 - 3×3 卷积块从输入的特征图中捕捉中等范围特征。 - 5×5 卷积块则用于捕捉更大范围的特征。 - 独立的3×3最大池化块用于保留原始特征的显著响应(如边缘、纹理)。 如前所述,native模块和降维模块的差异,在于后者增加了三个1x1的卷积操作,分别位于3x3卷积和5x5卷积之前,以及3x3最大池化之后。_放在3x3卷积和5x5卷积操作之前的那两个1x1卷积操作,其目的是降低计算的复杂度和参数量;而放在3x3最大池化之后的1x1卷积操作,则用于控制3x3最大池化输出的通道数(否则池化后的通道数与输入的通道数相同,总的通道数就太多了)。无论如何,以上四条路径输出的特征图分辨率相同,这样后续四条并行处理路径输出的特征图才能方便的拼接在一起。_ > 由此可见,在降维版本的inception module中,独立路径中的1x1卷积操作,与其他路径中额外增加的1x1卷积操作的作用是完全不同的。 1x1卷积如何降低计算量? 在降维Inception模块中的3x3以及5x5卷积操作之前,增加的1x1卷积的目的主要是用于降低参数量和计算量。 以下图为例,在不使用1x1卷积的情况下,从14x14x480到14x14x48的特征图转换,使用5x5卷积的情况下,其计算量达到了:_(14×14×480)×(5×5)×48 = 112.9M_。 !image.png 而在两个特征图中间增加一个1x1卷积层,如下图所示,其计算量大幅度下降: !image.png 其中1x1卷积的计算量为:(14x14x480)x(1x1)x16=1.5M,5x5卷积的计算量则为:(14x14x16)x(5x5)x48=3.8M。_两个加起来也只有5.3M,只有不增加1x1卷积计算的1/20!_ Inception模块如何实现输出拼接? 如上所述,Inception模块对于输入数据而言,有四条独立的处理路径。输入数据经过这四条独立的处理路径处理以后,在Inception模块的输出部分要拼接在一起。四条路径的处理结果要进行拼接,首要的条件也就是各个路径输出特征图的分辨率要相同。 因此,四个独立通道在设计时已经统一设计为stride=1并且配好了相应的padding,来确保在相同输出数据维度的情况下,各个通道的输出数据的分辨率也是相同的: - 1x1卷积路径:padding=0 - 3x3卷积路径:padding=1 - 5x5卷积路径:padding=2 - 3x3最大池化路径:padding=1 !image.png 在以上设置的情况下,可以保证四个通道对相同输入数据处理后,其输出特征图的分辨率相同,只不过各个通道的通道数(也就是各个通道的特征图数量)不同。四个通道输出特征图的分辨率相同的情况下,对于四通道输出的拼接就比较简单了:只需要把四个通道输出的特征图并排堆积起来就好了。 以GoogLeNet(Inception V1)论文中的第一个Inception模块(3a)为例进行解释。前一层的输入张量形状为,192x28x28,由前面的卷积+池化层处理后输出。 在Inception模块的四条路径中分别进行处理: - 1x1卷积,输出64通道28x28分辨率的特征图(64x28x28) - 3x3卷积,先通过1x1卷积降维到96通道(96x28x28),然后通过3x3卷积+padding为1输出128通道(128x28x28) - 5x5卷积,先通过1x1卷积降维到16通道(16x28x28),然后通过5x5卷积+padding为2输出32通道(32x28x28) - 3x3最大池化,先进行3x3最大池化(padding=1)继续输出192通道(192x28x28),然后通过1x1卷积把通道数降低到32通道(32x28x28) 最后把以上四个独立流程的输出拼接在一起,总共就得到了64+128+32+32=256通道28x28的特征图作为整个inception模块的输出。 全局平均池化 在传统的卷积神经网络架构中,使用连续的全连接层基于卷积层提取出来的特征数据,对图像进行分类操作。全连接层存在的最大问题就是参数太多,过拟合问题比较严重。针对这个问题,GoogLeNet采取了与Networ in Network相同的做法,使用全局平均池化的操作来代替全连接层,既解决了传统模型参数太多和过拟合的问题,而且从实际数据的测试结果来看,其分类的准确率也有了比较明显的提升。 下图是输入特征向量相同的情况下,全连接层与全局平均池化层不同的计算逻辑。有关全局平均池化更详细的内容可参考CNN经典网络模型架构学习之NiN。 !image.png Auxiliary Classifier GoogLeNet总共有22层,在网络深度较深的情况下,通过反向梯度传递和计算的过程中,梯度在反向传递的中间层逐级递减,最终导致梯度消失以致网络前半部分层的参数无法得到有效更新的问题。 GoogLeNet通过引入一个Auxiliary Classifier来尝试解决以上问题。Auxiliary Classifier的思路是:在网络的中间部分挂一个也能够进行分类操作的小网络(Auxiliary Classifier),这个小网络基于前面部分的特征图数据执行分类操作,得到自己的损失函数值。在进行模型训练的过程中,把这个损失值加到模型最终输出的损失值上,以增大梯度信号,缓解梯度消失的作用。 Auxiliary Classifier仅用于训练过程,在训练完成的推理过程中只保留主干网络的输出,因此不会影响推理速度。 在GoogLeNet的网络架构中,分别在4a和4d这两个inception module后面增加一个Auxiliary Classifier(可参考后续GoogLeNet的网络整体结构部分)。_每个Auxiliary Classifier的结构如下:_ - 输入为以上两个Inception Module所输出的特征图,例如512通道14x14分辨率的特征图 - 5x5平均池化:对输入的特征图首先进行5x5平均池化操作,stride=3,得到512通道4x4分辨率的特征图 - 1x1卷积:通过1x1卷积把通道数从512降低为128,输出128通道4x4分辨率的特征图 - 展平以上特征图以后。连接到一个1024神经元的全连接层,激活函数为ReLU - 增加一个Dropout操作,dropout比例为0.7 - 连接到一个1000神经元的全连接层,对应ImageNet数据集的1000个类别。 - 1000个神经元的乘加输出,通过softmax操作得到1000维的预测向量。 以上Auxiliary Classifier的预测输出与标签值对比,得到这个Auxiliary Classifier的损失值。把网络中两个Auxiliary Classifier输出的损失值分别乘以权重0.3,增加到网络主干网络分类输出的损失值,得到整个网络的损失值: 在进行网络参数训练的过程中,会把整个TotalLoss作为其损失值反向传递以更新网络参数。 GoogLeNet的整体结构 有了以上信息储备,理解GoogLeNet网络的整体架构相对就比较容易了: !image.png 从总体架构上可以看到: - GoogLeNet的开头部分通过连续的卷积和池化操作实现输入图像分辨率的降低。 - 然后通过连续9个inception module不断提取图像特征,9个inception module分为3段(2+5+2)。每个inception段内的连续module直接连接,段之间通过一个3x3的最大池化进行连接。 - 最后一个inception module的输出再通过全局平均池化+全连接层实现最终的图像分类操作。 - 此外,在inception 4a和4d的输出上再各自通过一个Auxiliary Classifier得到用于辅助训练过程的阶段性损失值。 GoogLeNet中各层的数据维度以及参数数量等信息如下表所示: !image.png 参考资料 - GoogLeNet: A Deep Dive into Google’s Neural Network Technology | by Siddhesh Bangar | Medium - Neutrino's Blog: GoogLeNet 簡介與小實驗 - 7.4. 含并行连结的网络(GoogLeNet) — 动手学深度学习 2.0.0 documentation - Understanding GoogLeNet Model - CNN Architecture - GeeksforGeeks - GoogLeNet: Revolutionizing Deep Learning with Inception - Inception-v1 (GoogLeNet) — Winner of ILSVRC 2014 (Image Classification) | by Moris | Computer Vision Note | Medium - 深度学习经典网络解析:6.GoogLeNet-腾讯云开发者社区-腾讯云

CNN经典网络模型架构学习之NiN

本文总结了卷积神经网络经典模型中的NiN(Network in Network)的设计理念、整体网络架构和具体的网络实现细节。 LeNet、AlexNet以及VGGNet这三种卷积神经网络模型的设计模式都是相同的:通过一系列的卷积层和池化层的组合进行特征提取,然后把提取到的特征数据展平(Flatten)后,送入一系列的全连接层进行分类操作,最终输出图像识别的概率。 以上这种经典的卷积神经网络设计模式存在两个主要的设计挑战: - 基于特征数据进行图像分类任务的全连接层所包含的参数数量太多了。例如,在VGG16网络中,参数数量高达138MB,模型参数所占据的存储空间也超过了500MB。 - 无法在网络的前半段添加全连接层来增加非线性程度,这样做会破坏空间结构,而且需要更多的参数和内存。 针对以上两个网络结构设计上的问题,2013年由National University of Singapore的Min Lin所发表的Network in Network(简称NiN),对经典的卷积神经网络的架构设计理念提供了新的设计思路。 NiN架构在传统CNN卷积网络的基础上最大的两个创新是MLP卷积层和全局平均池化层。 MLP卷积层 传统的卷积层的卷积计算逻辑在详细解释卷积神经网络中的卷积与池化计算一文中已经有非常详细的描述,在此不再赘述,仅以一个卷积计算的动图来进行解释: !CNN%E5%8D%B7%E7%A7%AF%E8%AE%A1%E7%AE%97%E5%8A%A8%E5%9B%BE.gif 总结起来,传统的卷积层计算公式如下图所示,(i,j) 是输入特征图中像素位置的索引,x_{i,j} 表示对应位置的像素,W 为卷积核的参数。那么卷积计算实际上就是 (i,j) 位置的像素及其周边像素与卷积核对应位置的权重之间的乘加结果,再通过ReLU激活函数进行处理,得到卷积计算的特征图。 !image.png 在NiN网络的实现中,卷积计算按照MLP Block的方式来进行组织。一个MLP Block的典型结构如下图所示: !image.png 对于每一个MLP Block而言,其输入是原始的3通道RGB图像或者前一个MLP Block输出的多张特征图。每个MLB Block的内部包含有三个连续的卷积层,每一层的输入都是前一层的输出。其中第一个卷积层的卷积计算与普通的卷积计算别无二致,而后面的两个卷积层则是1x1卷积层,1x1的卷积计算仍然是计算特征图像素值与权重的乘加,然后使用ReLU激活函数进行非线性处理。 以下的代码解释了对于每个MLP Block的结构和计算逻辑: > 每个MLP Block包含有3个卷积层,一个普通的卷积层+ReLU激活,再加上两个连续的1x1卷积层+ReLU激活。在网络架构上,一般每个MLP Block后面再跟一个最大池化层。 全局平均池化层 传统的卷积神经网络架构中,一般使用多个全连接层来实现特征分类的功能,其做法是把最后一个卷积层产生的特征图展平(Flatten)为一维向量,经过连续多个全连接层,最后使用一个softmax层进行逻辑回归分类。这种方式存在的主要问题是:全连接层的参数太多,以及多个全连接层造成的过拟合问题。 NiN网络使用全局平均池化的操作来代替全连接层,以尝试解决经典卷积神经网络后半部分全连接层的参数过多和过拟合的问题。全局平均池化操作具体的做法是最后一层卷积层输出特征图的数量,与分类类别的数量相同(例如对于ImageNet数据集就是1000个,对于MNIST数据集则是10个)。然后针对每张特征图取其平均值以后再输出成一个向量,对这个向量加上softmax操作(softmax操作的计算流程可以参考Softmax分类器的计算流程详细总结),最终输出各个类别的识别概率。 全局平均池化操作的流程如下图所示: !image.png 以ImageNet数据集(该数据集的图像分类结果有1000个类别)为例,全局平均池化层从其前面的MLP卷积层得到1000张特征图,那么在全局平均池化的计算中,就会对每一张特征图进行平均计算得到其平均值,最终得到由1000个平均值所组成的向量,然后对这个向量执行softmax操作,得到每个类别的识别概率输出。 > 可以看到,以上的全局平均池化层的计算与普通的平均池化的计算逻辑是类似的,因此完全不需要参数,这样就极大程度上减少了模型的参数数量(实际上经典卷积神经网络中的大部分参数都来自于全连接层),也可以避免传统的全连接层存在的过拟合问题。 NiN的网络架构总结 NiN网络的总体结构如下图所示。图像输入仍然是224x224分辨率的RGB三通道图像,经过连续四个MLP Block的卷积处理,最后一个MLP Block输出1000张6x6尺寸的特征图,把这个特征图经过全局平均池化层的平均计算以及softmax激活后输出1000个类别的识别概率。 !image.png 可结合下图对以上NiB架构图中各层的特征图尺寸和计算类型进行理解。尽管看起来NiN网络的层数比VGG网络多了不少,但是因为去掉了全连接层,NiN网络的参数数量比VGG少了很多。 !image.png 基于以上定义的nin_block结构实现的完整NiN网络的实现代码如下: 参考资料 - 8.3. Network in Network (NiN) — Dive into Deep Learning 1.0.3 documentation - 卷积神经网络之-NiN 网络(Network In Network)-腾讯云开发者社区-腾讯云 - Review: Network In Network. 這篇論文是由National University of… | by Fan | Medium

CNN经典网络模型架构学习之VGGNet

本文对计算机视觉图像识别领域经典的VGGNet模型的组成架构以及在Pytorch中的实现进行了详细的总结。 VGGNet的网络架构简介 VGG:Visual Geometry Group。VGGNet最初的设计理念是通过加大卷积神经网络的深度,来提升模型的工作性能指标。VGGNet在2014年的ImageNet的图像识别和分类竞赛(ILSVRC)中得到了第二名(GoogleNet是当年竞赛的第一名),top-5 accuracy指标(模型预测出来的概率最高的前5个类别中,是否包含了真实的标签)达到了92.7%,在后续的一些其他模型中,VGGNet的网络结构也往往被用于backbone部分进行图像的特征识别。 VGGNet的网络架构中比较典型的VGG16,VGG19以及VGG34模型,其中的数字表示其模型架构分别包含有16个、19个以及34个参数层(即卷积层和全连接层)。 下图是VGG16的网络架构图: !image.png - 以上VGG16的整个网络架构中,包含了13个卷积层,5个池化层,以及3个全连接层,其中的参数层(即卷积层和全连接层)共计16个。 - 模型的输入的分辨率为224x224的RGB三通道图像数据,最终的输出为针对1000个图像分类类别的预测概率(因为VGG16模型的设计是对ImageNet数据集进行预测,该数据集有1000个分类)。 - 从整个网络架构的设计上讲,仍然是标准的卷积神经网络。整个网络结构分为两个部分:前半部分是卷积层和池化层组成的图像特征提取部分,后半部分则是由全连接层组成的图像分类部分。因此,网络结构与CNN经典网络模型架构学习之LeNet以及CNN经典网络模型架构学习之AlexNet所总结的LeNet以及AlexNet没有本质上的差别,只不过层次更多而已。但因为层次相比较LeNet和AlexNet的层数加深了很多,VGG16的参数数量达到了138M。 VGG19 相比 VGG16 多了 3 个卷积层,在以上的conv3、conv4、conv5部分各加了一个卷积层,其余部分(包括卷积核尺寸、通道数、池化策略、全连接层结构)完全相同。 在VGGNet的原始论文中,作者共提供了从A到E共6种网络配置。其中VGG16对应的是配置D,而VGG19对应的则是配置E。 !image.png 从上表可以看到,A-E这6种配置的网络架构基本相同,差异只在于各个卷积层的数量不同而已。 VGG16各网络层次参数配置 下图为VGG16网络架构中各个层次的超参数设置: !image.png - 可以看到,以上VGG16网络结构的各个层次中,conv1、conv2、conv3、conv4/conv5部分的卷积核数量分别为64、128、256、512个,各个卷积区使用的卷积核的数量在每一层加倍,这是设计VGG16网络架构的一个重要原则。 - 所有卷积层的卷积核大小都是3x3(这一点与AlexNet所使用的更大尺寸的卷积核形成了鲜明的对比),卷积层的步进stride全部为1;所有池化层均为2x2最大池化,池化步进stride全部为2。 - 激活函数方面,除了最后输出层使用多分类的softmax激活函数以外,其他所有的卷积层和全连接层的激活函数均为ReLU。 - 最后的三个全连接层部分,前两个全连接层均有4096个神经元,最后的输出层对应ImageNet数据集的1000个类别的识别概率,包含有1000个神经元。 VGGNet的Pytorch实现 VGG16网络结构基于Pytorch框架的代码实现如下所示: 参考资料 - Understanding VGG: The Backbone of Image Recognition - VGG-Net Architecture Explained. The company Visual Geometry Group… | by Siddhesh Bangar | Medium - VGGNet - HackMD

Softmax分类器的计算流程详细总结

Softmax激活函数一般用于多分类问题的分类器实现,本文详细介绍和总结了基于Softmax激活函数以及交叉熵损失函数来实现神经网络多分类器的完整计算流程。 实际的人工智能项目,经常会遇到多分类问题,最典型的就是基于MNIST手写数字识别数据集,对手写的0-9总共10个手写数字进行识别和分类,以及基于ImageNet的图片数据集,对日常生活中看到的1000个物体进行分类。在人工智能的机器学习领域中,最常用的多分类器的实现就是Softmax分类器。 Softmax分类器的计算流程详细总结-Softmax多分类器.png Softmax激活函数 对于基于Softmax实现的深度神经网络的多分类应用而言,网络最后一层的全连接神经网络的神经元个数就对应了该网络实现分类操作的类别数量。那么Softmax的输入就是这些神经元的输出(也就是所谓的logits),Softmax的输出则是不同类别分类判断的概率。 以下详细解释Softmax操作的计算流程。假设数组 a 中有 n 个元素: 那么针对这个数组 a 进行的Softmax计算的流程如下。首先,对 a 中的每个元素取以e为底数的幂,得到一个新的数组 t : 然后把 t 数组中所有元素相加得到 s: 最后,把数组 t 中的每一个元素除以以上计算出来的 s ,得到一个新的数组就是针对 a 的softmax的计算结果: 因为 s 是 t 数组所有元素的和,所以以上softmax的计算结果中所有元素的和就是1,每个元素对应于多分类识别输出对应的该类别的识别概率。 因此Softmax计算的流程如下图所示: !image.png Softmax计算流程在Python中的实现代码如下所示: One-Hot编码 One-Hot编码,又称为独热编码,在多分类应用中,只用0和1两个数字来表示分类标签对应的概率。 以dog、cat、bird三种分类情况为例,这三种类别在标签数据上,可以分别使用(1,0,1),(0,1,0),(0,0,1)来表示: !image.png 交叉熵损失函数 在深度网络多分类器应用的实现上,一般是在最后一个全连接层采用Softmax激活函数对该层所有神经元的原始输出进行处理,得到每个类别对应的分类概率,而这类应用一般采用的损失函数则是交叉熵损失函数。 交叉熵损失函数的表达式如下图所示: !image.png 其中的 p(x) 表示以One-hot编码表示的训练样本数据对应一维向量,其中只有正确分类对应的元素为1,其他分类对应的元素均为0;q(x) 则是深度网络模型的输出经过Softmax处理后输出的一维向量,其中的各个元素对应了模型预测出来的各个分类的可能性。 所以交叉熵损失函数的计算,本质上就是对训练样本标记的One-Hot分类结果,与模型使用相同样本推理计算出来的各个类别的分类预测结果之间进行比对,判断预测输出的正确分类结果的可能性有多接近1,预测输出的正确分类结果越接近1,损失函数越小。 交叉熵函数的Python代码实现如下: 可以看到,预测输出结果的正确分类2对应的可能性越高(0.8 vs 0.6),交叉熵损失函数的计算结果越小。 Softmax分类器的实现 > 多分类应用:最后一个全连接层的输出使用Softmax激活处理,训练样本的标签采用One-Hot Encoding进行编码,损失函数采用交叉熵Cross Entropy。 下图总结了深度神经网络基于Softmax激活函数和交叉熵损失函数实现多分类器的计算流程。 !image.png - 深度神经网络的最后一个全连接层,从前一个全连接层接收输入的特征变量 x_m ,与最后一层的权重系数 w_{m,k} 以及偏置变量 b_k 进行乘加操作,得到最后一层各个神经元的净输出。 - 最后一层神经元的净输出,作为一个完整的一维向量,执行Softmax操作,输出各个分类推理输出的可能性预测列表 t_k 。 - 深度神经网络输出的多分类预测输出 t_k 与以One-Hot encoding模式编码的样本标签进行比较,使用交叉熵(Cross Entropy)损失函数计算损失。 - 根据交叉熵损失函数的输出反向传播调整整个网络的各级参数。 总结起来,Softmax激活函数在计算流程上,与其他激活函数(如Sigmoid,Tanh,ReLU等)相比,最大的区别是:其他的激活函数都是对每个神经元的乘加输出进行非线性变换,而Softmax激活函数则是对最后一层所有神经元的乘加输出进行的计算。 参考资料 - 《Python深度学习和项目实战》,第三章 Softmax多分类器 - 使用 TensorFlow 學習 Softmax 回歸 (Softmax Regression) | by Leo Chiu | 手寫筆記 | Medium - Cross Entropy Loss: Intro, Applications, Code

CNN经典网络模型架构学习之AlexNet

本文对计算机视觉图像识别领域经典的AlexNet模型的组成架构以及在Pytorch中的实现进行了详细的总结。 在CNN经典网络模型架构学习之LeNet一文中,对Yann LeCun针对手写数字识别的LeNet5网络模型的结构进行了总结。接下来,继续学习计算机视觉领域中的那些经典模型的框架,本文总结了在ImageNet图像识别竞赛中脱颖而出的AlexNet。 AlexNet历史简介 在2012年的ImageNet图像识别竞赛( ImageNet Large Scale Visual Recognition Challenge ,ILSVRC)中,由Alex Krizhevsky, Ilya Sutskever和Geoffrey E. Hinton所提出的AlexNet赢得了冠军。在该竞赛最重要的Top-5 Error Rate指标上,AlexNet取得了断崖式的领先,相比第二名足足提高了10个百分点: !image.png 正是因为AlexNet在该次竞赛中的卓越表现,开启了深度学习这种方式在人工智能计算机视觉领域的研究热潮。 AlexNet的网络架构 AlexNet的网络架构图如下图所示: !image.png 由上图可以看到,AlexNet的整体结构与CNN经典网络模型架构学习之LeNet所描述的LeNet-5网络架构非常类似,整体上可以分为两个部分: - 图像特征提取部分:包含从输入层到最后一个最大池化层,这部分的输出是6x6x256维度的图像特征向量。 - 分类部分:包含最后的三个全连接层。 AlexNet网络的输入是RGB三通道227x227分辨率的图片,输出则是通过输出层的1000个神经元的SoftMax激活函数所输出的1000个分类的概率。之所以是1000个分类,主要是因为AlexNet设计的初衷就是为了参加ImageNet的图像识别竞赛,而ImageNet数据集中包含有1000个图像分类。 AlexNet的总体网络结构总共包含有五个卷积层,三个最大池化层,三个全连接层。 - 输入数据为RGB三通道227x227分辨率的彩色图像数据。 - 第一个卷积层包含有96个卷积核,卷积核为11x11x3的立体卷积核,步进Stride为4,激活函数为ReLU,最终产生96张55x55分辨率的特征图。 - 第一个池化层采用3x3,步进Stride=2的最大池化操作,输出96张27x27分辨率的特征图。 - 第二个卷积层包含有256个卷积核,卷积核尺寸为5x5x96,步进为1,padding为2,激活函数为ReLU,最终产生256张27x27分辨率的特征图。 - 第二个池化层采用3x3,步进Stride=2的最大池化操作,输出256张13x13分辨率的特征图。 - 第三个卷积层包含有384个卷积核,卷积核尺寸为3x3x256,步长为1,padding为1,激活函数为ReLU,最终产生384张13x13分辨率的特征图。 - 第四个卷积层包含有384个卷积核,卷积核尺寸为3x3x384,步长为1,padding为1,激活函数为ReLU,最终产生384张13x13分辨率的特征图。 - 第五个卷积层包含有256个卷积核,卷积核尺寸为3x3x384,步长为1,padding为1,激活函数为ReLU,最终产生256张13x13分辨率的特征图。 - 第三个池化层采用3x3,步进Stride=2的最大池化操作,输出256张6x6分辨率的特征图。 - 以上就是AlexNet的特征提取部分,最后一个池化层进行展平操作为6x6x256个元素的一维向量,最后分类网络层第一个全连接层的输入。 - 第一个全连接层有6x6x256共计9216个输入,有4096个神经元输出,激活函数为ReLU。 - 第二个全连接层接受4096个输入,有4096个神经元输出,激活函数为ReLU。 - 最后一个全连接层有1000个神经元输出,对应于ImageNet数据集的1000个识别分类的概率。 整个网络总的参数数量达到了6000万个,主要集中在最后的三个全连接层:9216x4096+4096x4096+4096x1000=59621952,可见全三个全连接层占去了所有参数总量的绝大多数。 整个网络结构各层数据的维度如下总结: !image.png AlexNet的双层架构实现 另外,从部分资料上看到的AlexNet的架构图如下所示: !image.png 可以看到,以上架构图把整个网络分为完全相同的上下两个部分,输入图像同时输入上下两个子网络,最后在输出层汇合。这个架构图实际上是AlexNet原始实现的架构图,因为当时的GPU配置和处理能力相对较低,如果使用一块GPU进行AlexNet+ImageNet数据集的训练的话,速度会非常慢。所以Alex把网络分为上下两个部分,两个子网络分别在两块显卡上并行运行,最后在输出层合并输出结果,这样可以大大加快训练和推理的速度。 但无论如何,从网络层次和结构设计的本质上,以上的这种上下两个子网络的结构与AlexNet网络的标准网络结构并没有区别。 Alexnet的特点总结 激活函数 在AlexNet的网络架构设计中,激活函数由LeNet-5的Sigmoid变成了ReLU,这一修改可以解决在神经网络的层数过深或者梯度过小的情况下,Sigmoid激活函数所存在的梯度消失问题。同时,ReLU相比Sigmoid和Tanh等激活函数,还有着收敛速度更快、训练效率更高以及计算更为简单等优点。 以下是AlexNet论文中所演示在在CIFAR-10数据集上,ReLU相比Tanh激活函数在训练收敛速度上的巨大优势: !image.png 最大池化 AlexNet与LeNet5另外一个明显的不同,是使用最大池化层代替了LeNet5所使用的平均池化层。 AlexNet所采用的3x3,stride=2的最大池化层,与LeNet5的2x2平均池化相比,更能够保留重要特征,并且因为stride

CNN经典网络模型架构学习之LeNet

本文以用于进行手写数字识别功能的LeNet为例,分析该网络的结构以及典型的卷积神经网络的整体架构,对用于进行图像识别和处理的卷积神经网络建立初步认识。 在卷积神经网络CNN的架构解析入门一文中简单的总结了典型的卷积神经网络的结构和组成,本文以一个非常经典的、用于进行手写数字识别的LeNet-5网络为例,更为详细的解释卷积神经网络的结构和运行流程。 LeNet简介 LeNet是最早发布的卷积神经网络之一,也是架构最简单的卷积网络,非常适合作为入门网络结构用于建立卷积神经网络架构设计理论和运行流程的理解基础。 LeNet模型是由贝尔实验室的研究员Yann LeCun在1989年首次提出并不断优化,其设计和应用的目的是用于识别手写数字(也就是对所谓的MNIST数据集进行的训练和识别)。 LeNet被广泛用于美国的邮政服务和自动取款机(ATM)机中,帮助识别处理手写的邮政编码数字和支票上的数字。 时至今日,一些自动取款机仍在运行Yann LeCun和他的同事Leon Bottou在上世纪90年代写的代码! !image.png LeNet的官网地址:MNIST Demos on Yann LeCun's website LeNet的网络架构说明 下图是LeNet的网络架构图: !image.png 总体上看,以上的网络架构像所有的卷积神经网络一样,由两个部分组成: - 特征提取层:包含两个卷积层+池化层(也就是上图中的汇聚层)的组合。 - 分类层:包含三个全连接层。 每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化层。特征提取层的最后一个池化层的输出是16通道的5x5特征图,该特征图通过展平操作(Flatten)为16x5x5个元素的一维向量,这个一维向量在输入到全连接层执行分类任务,最后一个全连接层有10个神经元,对应0-9共10个数字的识别概率。 LeNet网络各层结构和参数量的详细信息总结如下: - 输入数据为灰度28x28分辨率的图像,实际上也就是一个28x28的二维数组,只有灰度通道。 - 卷积层C1包含6个5x5x1的卷积核,padding为2,所以卷积层的输出为6张28x28分辨率的特征图,卷积计算结果的激活函数为Sigmoid。 - 该层的参数数量就是6个5x5x1卷积核的权重和偏置,也就是6x(5x5x1+1)。 - 池化层S2采用2x2的平均池化操作,对卷积层C1的6通道28x28分辨率特征图进行池化计算,输出为6通道14x14分辨率的池化后特征图。 - 卷积层C3包含16个5x5x6的立体卷积核,padding为0,所以卷积层C3的输出为16张10x10分辨率的特征图,卷积计算结果的激活函数同样为Sigmoid。 - 该层的参数数量就是16个5x5x6卷积核的权重和偏置,也就是16x(5x5x6+1)。 - 池化层S4仍然采用2x2的平均池化操作,对卷积层C3的16通道10x10分辨率特征图进行池化计算,输出为16通道5x5分辨率的池化后特征图。 - 第一个全连接层F5的输入为池化层S4输出特征图的一维展平向量,因此输入数据是一个5x5x16的一维向量,F5全连接层有120个神经元,激活函数仍然是Sigmoid。 - 该层的参数数量就是120个神经元的输入权重和偏置,也就是120x(5x5x16+1)。 - 第二个全连接层F6的输入为F5的120个神经元的输出,F6全连接层有84个神经元,激活函数为Sigmoid。 - 该层的参数数量就是84个神经元的输入权重和偏置,也就是84x(120+1)。 - 最后的输出层有10个神经元,各个神经元的输出对应于数字0-9的识别概率的大小。 - 该层的参数数量就是10个神经元的输入权重和偏置,也就是10x(84+1)。 以下是LeNet在前向计算过程中各个层的输出向量的维度总结: > 对于以上LeNet的网络结构而言,需要注意: > - 以上的网络结构图中,共包含7层:两个卷积层,两个池化层,三个全连接层。但是实际上一般意义上的LeNet指的是LeNet5,这个数字5表示整个网络中包含参数的层数(即两个卷积层+三个全连接层)。 > - 实际的卷积神经网络的实现中,卷积层的激活函数使用ReLU,池化层采用最大池化方式,在网络的实际表现中性能更好。但是无论是ReLU还是最大池化,在上世纪八九十年代均未出现。所以在原始的LeNet5架构中,卷积层的激活函数采用经典的Sigmoid,池化层则采用平均池化。 > LeNet的PyTorch实现 基于以上介绍的LeNet5模型的网络架构,在PyTorch框架下的该网络实现代码如下: 以上网络实现后导出为Onnx模型文件,在netron.app中的网络架构图如下所示: !image.png 参考资料 - 6.6. 卷积神经网络(LeNet) — 动手学深度学习 2.0.0 documentation - MNIST Demos on Yann LeCun's website

一文总结YOLOv5模型的自定义训练、测试与模型导出的全过程

本文详细总结了基于用户自己指定的训练数据集,对训练数据集及其标注按照YOLO的格式进行预处理,然后对YOLOv5模型进行训练、测试以及ONNX导出等的完整流程。 YOLOv5模型的下载和测试 要对YOLOv5模型进行自定义的训练和部署,首先要做的当然就是下载YOLOv5的源代码。在Github上搜索YOLOv5找到Ultralytics的源代码仓库:ultralytics/yolov5: YOLOv5 🚀 in PyTorch > ONNX > CoreML > TFLite 。 可以看到尽管YOLOv5是比较早期的模型了,但是其开发和维护并没有停止,一直到现在仍然在持续更新中: !image.png 通过Git Clone或者直接使用HTTPS下载最新版本的代码到本地。 下载下来以后,可以先使用默认训练好的模型调用其中的脚本进行一些测试。对于YOLOv5模型的基本测试使用而言,主要用到其中的三个脚本文件:trian.py(训练)、detect.py(新样本数据推理)、export.py(模型导出)。 train.py的主要调用参数: - -weights : 初始权重文件路径,默认为 yolov5s.pt (官方基于COCO数据集预训练好的参数文件,首次使用时会自动下载),用于加载预训练模型 - -cfg : 模型配置文件 (model.yaml) 路径,定义网络结构。默认的模型配置文件在models目录下。 - -data:训练数据集配置文件。默认的数据集配置文件在data目录下。 - -batch-size:训练过程中每个mini batch的数量,一般是8或者16 - -epochs:训练的轮次数量 - -device : 指定 CUDA 设备,如 0 或 0,1,2,3 或 cpu 基于以上weights和cfg参数的设置情况,对于模型的训练可以分为以下情况: - 基于YOLO官方提供的预训练模型参数进行训练:此时就只需要通过weights参数传递预训练模型的pt文件即可,其中已经包含了模型的配置信息。 - 基于YOLO模型从头开始训练:将 weights 参数设为空字符串,并提供 cfg 参数设置模型配置。 - 如果同时在命令中提供了 cfg 和 weights 参数,则会优先使用 cfg 定义的模型结构,并尝试加载 weights 中的权重参数(如果结构匹配)。 detect.py的主要调用参数: - -weights : 指定模型的权重文件路径,默认为 yolov5s.pt 。 - -source : 推理数据的输入源路径,可以是单张图片、视频文件、目录、URL(rtsp、rtmp等)、摄像头索引(0)、屏幕(screen)等。 - -device : 运行设备,可指定为 cpu 或GPU索引(如 0,1 ),默认为自动选择。 - -classes : 指定要检测的类别索引列表,如 --class 0 2 3 ,默认为 None (检测所有类别)。 - -project : 推理结果保存的项目目录,默认为 runs/detect。 - -name : 实验名称(用于创建子目录),默认为 exp。 - -conf-thres:置信度阈值。只有识别的置信度大于该阈值才会输出结果并标记出来。 export.py的主要调用参数: - -data : 模型训练所使用的数据集配置YAML文件路径,默认为 data/coco128.yaml ,用于提供类别信息等元数据。 - -weights : 要导出的模型权重文件路径。 - -batch-size : 批处理大小,默认为 1。 - -include : 指定要导出的模型格式列表,默认为 torchscript ,支持的格式包括: - torchscript : PyTorch的TorchScript格式 - onnx : ONNX格式 - openvino : Intel OpenVINO格式 - engine : NVIDIA TensorRT格式 - coreml : Apple CoreML格式 - saved_model : TensorFlow SavedModel格式 - pb : TensorFlow GraphDef格式 - tflite : TensorFlow Lite格式 - edgetpu : TensorFlow Edge TPU格式 - tfjs : TensorFlow.js格式 - paddle : PaddlePaddle格式 YOLOv5源代码目录中的data子目录里面,包含了多个标准图像数据集的配置文件,可以先使用这些标准数据集及其配置文件测试YOLOv5模型的训练流程(此处使用coco128数据集): > 每次调用以上脚本进行的训练流程,都会在run/train下创建一个exp目录,其中包含有训练过程中所生成的各种中间文件。训练完成后,生成的权重参数文件就是这个目录下的weights/best.pt文件。 同样的,可以使用以下命令对Yolov5源代码中自带的图片,使用detect.py进行推理测试: 以上脚本会对data/images下的所有图片进行推理,推理生成的标注图片在runs/detect/exp目录下。 !image.png 训练数据集的准备 前面对Yolo模型的基本使用做了简单的测试。接下来,就是准备要准备自定义的数据集进行模型的训练了。 本文以口罩检测作为学习案例,网络上有大量的这类公共数据集可用,因此可以直接首先搜索可以满足训练需求的公共数据集。 - 网站kaggle,Roboflow,Huggingface等提供了大量的公开数据集,可以根据自己的需求在这些网站上搜索。 本文以Kaggle上的Face Mask Detection数据集对Yolov5模型进行训练:Face Mask Detection。该数据集对图片中的人是否戴口罩进行检测,整个数据集共853张图片,检测结果有三种:未戴口罩,戴口罩,未正确戴口罩。标注格式为Pascal VOC格式。 !image.png Kaggle上的Face Mask Detection数据集下载后,需要首先对其图片及其标注信息进行预处理,预处理为Yolo模型所需要的格式。对于Face Mask Detection数据集而言,要做的预处理包括: - 把该数据集所采用的Pascal VOC标注格式转换为Yolo格式。 - 把该数据集中的图片和标注集以85:10:5的比例切分为训练集、验证集、测试集三个部分。 - 按照Yolo模型训练所需要的训练数据集目录要求分别保存在train、valid、test三个子目录中。 - 准备该数据集用于Yolo训练的配置描述文件FaceMask.yaml。 Pascal VOC是一种常用的目标检测类型的标注格式,与Yolo模型训练所要求的标注格式大相迥异,所以我专门写了一个voc_to_yolo.py的脚本用于实现以上预处理流程的第1-3步:标注格式转换、数据切分、yolo训练目录结构建立。使用该脚本就可以实现从Pascal VOC数据集到Yolo训练数据集的转换: 下图是Face Mask数据集转换前后的目录结构示意图。 - 转换前的原始数据集中,所有的图片在images目录下,所有的Pascal VOC标注格式的标注信息在annotations目录下。 - 转换后的完整yolo数据集在yolo目录下,其中包含有test、train、valid三个子目录,分别对应测试、训练和验证数据集,其下又包含各自的图片文件子目录和Yolo标注文件子目录。 !image.png 基于以上已经准备好的Yolo格式的训练数据集,整理与之对应的数据集描述配置文件FaceMask.yaml如下。后续在训练过程需要使用该文件指定训练数据集的路径、识别和检测类型名称数量等信息。 模型训练和测试 基于以上预处理好的数据集在Yolov5预训练模型上进行训练: 在CPU上进行训练的时间比较长,不到1000张图片,在我的Thinkpad X260笔记本上训练的时间超过了一天。训练完成后生成的模型参数文件在runs\train\exp9\weights目录下的best.pt文件。 使用以上训练生成的模型参数文件对前一阶段分割出来的测试数据集进行测试: 以上脚本会对datasets\FaceMask\yolo\test\images\所指定的测试数据集中的所有图片进行推理,推理结果保存在runs/detect下的一个新建exp目录中,置信度大于0.5的识别结果会画框标记出来是否有正确的戴口罩。 !image.png 模型的ONNX导出 接下来就可以使用Yolov5源代码目录中的export.py,把前一步骤训练出来的best.pt参数权重文件导出为onnx标准模型文件,在onnxruntime等标准化的环境中进行部署了: 以上脚本执行后就会在best.pt相同路径下生成一个best.onnx文件,这就是模型导出的ONNX文件了。 参考资料 - ultralytics/yolov5: YOLOv5 🚀 in PyTorch > ONNX > CoreML > TFLite 。 - Face Mask Detection

计算机视觉类AI应用领域及其主流模型总结

本文总结了目前的人工智能领域中,针对计算机视觉方面的主要应用方向(图像分类,图像定位,目标检测,图像分割,语义分割,实例分割)的差异以及各个应用方向所存在的主流模型。 在当前的计算机视觉领域,图像分类、物体检测以及图像分割是最基础,也是目前发展最为迅速的三大领域。 图像分类 Image Classification 计算机视觉领域的图像分类任务,要解决的问题是:判断图像中所包含图像的类别是什么? 例如,对于下图而言,图像识别任务给出的识别结果是这幅图中包含了一只狗: !image.png 所以对于传统的图像分类任务而言,该任务识别结果对应的就是这幅图中最显著的物体所对应的分类(例如上图中的狗),并把这张图片直接按照这个最显著的物体来进行分类。因此,对于图像分类任务的图片样本而言,一般总是存在一个非常突出、最容易用于进行分类的显著物体存在,如下图的各个子图中均可以一目了然的判断该图片对应的分类(实际上也就是这个图片中所包含的最显著的物体): !image.png 当然,很有可能,我们提交给模型进行分类的图片中包含了多个较为显著的待识别物体,如下图中同时包含了一只狗和一只猫: !image.png 针对这种图像分类任务就需要多目标分类(Multi Label Classfier)。实际上从分类的角度上看,机器学习的分类算法可以分为三种类型: - Binary classification:有/无,是/非 - Multi-class Classification:从多个类别中返回一个 - Multi-Label Classification:从多个类别中返回多个 !image.png 针对Multi-Class和Multi-Label类型的分类算法,模型进行分类识别的判断结果区别就是,Multi-Class的输出结果是从多个类别中的1个,而Multi-Label则是多个类别中的多个: !image.png 无论如何,图像分类任务始终只关注要进行分类的图像中存在的物体类别,并不关注这些物体在图像中的位置和数量。 用于图像分类领域的模型训练数据集有:ImageNet,MNIST,CIFAR等。 图像分类领域的经典模型包括: - VGG,Visual Geometry Group at the University of Oxford - GoogLeNet (Inception),Google - ResNet,Microsoft Research - EfficientNet,Google - DenseNet,Cornell University - MobileNet,Google 目标检测 Object Detection 如果不光要关注图片中物体的类别识别,还需要准确的找到这些物体在图片中的位置,那么就需要用到计算机视觉中的目标检测任务。 如上所述,图像分类应用只是通过识别给图像一个单独的标签,而与之相对的目标检测应用,则需要在图像和视频中对检测到的每个对象提供空间坐标(边界框)和分类标签,这使得在更详细的层面上分析和处理可视化数据成为可能。 - 图像分类应用的输出结果,就只是图像中所包含对象的标签/标签。 - 目标检测应用的输出结果,则需要包含图像中包含对象的标签类别,以及以矩形框标记该对象在图像中所在的位置。 如果是图像中只包含一个最显著的目标,对该目标进行识别并定位的应用,就是Image Localization;而如果图像中有很多目标,要对所有的这些目标进行识别和定位的应用,就是Object Detection: !image.png 目标检测领域的经典模型包括: !image.png 图像分割 Image Segmentation 图像分割是把图像中以像素为单位分割为不同的对象,并使用不同的色彩涂色进行区分。 图像分割应用(Image Segmentation)又可以细分为语义分割(Semantic Segmentation)和实例分割(Instance Segmentation)。这两者之间的区别是: - 语义分割:图像分割的处理中,同一个类别的所有物体构成一个分类,因此,同一类别的所有物体都使用同一种颜色着色。 - 实例分割:在进行图像分割的处理过程中,同一个类别的多个不同实例被认为是单独的段,即同一类的每个对象被视为不同的。因此,即使属于同一类,每个独立的物体也会被涂上不同的颜色。 下图是语义分割的典型案例,整个图像中的不同类别的对象被涂以不同的颜色,但是同一类别的对象的颜色相同。 !image.png 下图是实例分割的典型案例,可以看到在该图片中识别到的三只猫被分割为不同的对象: !image.png 图像分割领域的主流模型: - Mask R-CNN - U-Net - SegNet - Deeplab - PSPNet - Vision Transformers (ViTs) 参考资料 - 人工智能中图像分类、目标检测、语义分割和实例分割等任务是什么? - 知乎 - Image Classification vs. Object Detection vs. Image Segmentation | by Pulkit Sharma | Analytics Vidhya | Medium - Object Detection Models - GeeksforGeeks - Image Segmentation Models - GeeksforGeeks

Pytorch模型的ONNX导出和部署入门

本文详细总结了基于Pytorch框架开发和训练好的模型导出为通用的onnx文件,并且通过onnxruntime进行部署和Python/C++访问的流程和步骤。 本文要解决的问题是:在Pytorch中开发和训练好的网络框架及其参数,如何导出为可以部署的onnx文件?如何在其他的环境中部署这个onnx文件? 本文以[[基于Pytorch实现手写数字识别的卷积神经网络]]一文中所训练的卷积神经网络为例,来说明这个完整的过程。 Pytorch模型导出为pt/pth文件 在Pytorch框架中,已经训练完成或者训练中的模型及其参数可以保存pt或者pth文件,这样后续就可以直接通过加载这些文件快速恢复出来模型及其训练后的参数状态,继续进行训练或者直接用于新数据的推理。 一般而言,使用pt后缀名的文件用于仅保存模型的参数,而pth后缀名文件则同时保存了模型的结构和参数。因此前者保存的文件更小,但在下次加载前需要提前构建模型的框架结构,因为pt文件里面并没有包含模型结构;而后者在使用上更加方便,内部同时包含了模型结构和参数,使用时直接加载即可,但是文件体积更大。 > 当然,在 PyTorch 中, pt和pth这两种文件后缀没有官方严格的区分和定义,它们更多是开发者社区约定俗成的用法。也就是说,你完全可以用 .pt 保存完整模型,或用 .pth 仅保存参数。 以下是基于Pytorch保存训练模型及其参数,并且重新加载模型参数到新的模型对象的代码示例: 模型的ONNX导出 相比于Pytorch的模型参数导出的pt/pth文件格式,ONNX文件格式是模型部署以及在不同模型开发框架之间共享和转换支持上更为普遍的文件格式。因此也可以把训练的模型及其参数导出为ONNX文件,用于进行模型部署以及转换到其他的框架下。 Pytorch模型导出onnx文件并通过ONNXRuntime类似进行新样本推理的前提条件,是需要安装几个依赖库: 把已经训练好的模型及其参数导出为Onnx文件的代码流程如下: 如以上代码中input_sample变量所表明的,在进行ONNX文件导出时需要明确的指定后续进行推理的数据输入格式。以上代码执行后,在当前目录下就会生成一个model.onnx文件,后续就可以基于这个文件进行Onnx部署以及在不同框架下转换和使用。 ONNX文件检查 以上所导出的ONNX文件,可以使用以下代码检测该文件的合法性: onnx.checker.check_model接口用于验证ONNX模型文件的有效性,确保它符合ONNX规范,可以被ONNX兼容的推理引擎正确加载和执行。这是在使用ONNX模型进行推理前的一个常见检查步骤,有助于提高代码的健壮性。 此外,也可以直接使用netron.app工具来对以上onnx文件进行检查,该工具可以以图形的方式输出非常详细的模型及其参数信息,可以非常直观的看到该文件中所描述的模型结构及其当前各层的训练参数信息: !image.png 通过ONNXRuntime部署模型并测试 Onnx文件中所保存的模型结构和参数是模型的静态表示,不能够直接调用PT格式导致后的eval()和predict()方法。当基于Onnx文件加载模型的结构和参数以后,需要通过ONNXRuntime引擎来执行新样本数据的推理。因此要首先安装ONNXRuntime包才能支持对ONNX文件的部署和推理。 > 如前所述,在Pytorch框架下使用ONNXRuntime导入Onnx文件并进行新样本的推理,需要通过pip安装onnxruntime和protobuf两个包。 以前文所导出的模型Onnx文件为例,从MNIST数据集中的某个样本进行OnnxRuntime的推理测试代码如下: 可以看到,除了使用torchvision.datasets和torchvision.transforms来实现对数据集的访问以外(其实完全可以采用其他方式来准备输入该模型进行推理的数据样本,只要数据格式符合ONNX input sample的要求即可),以上代码对于新样本数据的推理仅仅依赖于numpy和onnxruntime,完全不依赖于开发该模型的pytorch框架。 参考资料 - 第一章:模型部署简介 — mmdeploy 1.3.1 文档 - Netron - ONNX Runtime | onnxruntime

基于Pytorch实现手写数字识别的卷积神经网络

本文详细介绍和总结了基于Pytorch框架实现一个卷积神经网络CNN,用于对手写数字数据集MNIST进行识别的代码流程,以此作为对Pytorch框架以及神经网络编程实践的入门基础。 > 本文所解析的MNIST数据集手写数字识别的代码重点参考了《深度学习原理与PyTorch实战(第二版)》一书第五章的相关代码实现,实际上是对这部分代码结合我自身理解的更为详尽的解读。该书对于深度学习和神经网络在各个领域中的工作原理以及基于Pytorch框架的实现进行了非常详尽和精彩的描述,是对机器学习和神经网络领域入门的不可多得的好书,强烈推荐。 手写数字数据集MNIST及其准备 MNIST是一个手写数字的数据集,可以说是进入机器学习和深度学习领域的一个hello world资料库。基于该数据集,无论是通过经典的全连接神经网络,还是针对图像领域的卷积神经网络,都可以非常方便的进行网络结构的设计、测试、训练和验证,从而在代码实践的过程中获得对深度学习和神经网络领域相关理论的更深入理解。 MNIST数据集中的每个样本包括一张手写数字灰度图片(分辨率为28x28),及其对应的数字标签。整个数据集包含6万个训练样本和1万个测试样本,所以从总体上可以分为训练数据集和测试数据集。 !image.png Pytorch中内置了对各种常用开源训练数据集包括MNIST在内的支持,通过执行以下代码即可实现自动下载MNIST数据集到本地当前目录下的data子目录中。首次执行以下代码的时候,Pytorch会自动创建data子目录,并下载数据集到该子目录下。以后再调用这段代码就会直接从data子目录下加载MNIST数据集。 可以看到,以上的代码以train=True/False区分,分别用于下载/加载MNIST数据集的训练数据集train_set和测试数据集test_dataset。此外,因为数据集中的原始数据加载后是普通的数组形式,在该数据集加载的过程中要通过transform=transforms.ToTensor()选项转换为Pytorch的张量Tensor,这是Pytorch模型能够处理的标准数据格式。 Pytorch的数据加载器 Pytorch中专门提供了dataloader和sampler用于对训练数据的加载和调用进行管理,以方便后续在网络的批量训练和测试验证过程中高效的管理和传递训练数据。进行神经网络的训练和测试,一般需要把完整的数据集分为三个完全独立的部分,避免相互干扰:训练集、验证集、测试集。 > 为什么数据集在使用的过程中需要被分割为独立的训练集、验证集和测试集?训练集比较好理解,就是把训练数据送入网络,对比输出结果和该训练样本标签之间的差异,以差异为基础对网络参数进行修正,直到训练数据输出的结果和标签之间的差异最小。测试集也相对比较好理解,就是神经网络中的参数训练好以后,需要使用完全与训练数据不相关的独立数据集对训练好的网络性能进行测试验证,这个用于对训练好的网络进行测试评估的独立数据集就是测试数据集。验证集,则用于在网络参数训练的过程中,定期的使用这个独立的数据集对当前处于训练过程中的网络参数进行测试,这样就可以在训练的过程中实时的了解到网络性能的动态改善状况。 以下代码基于Pytorch的DataLoader创建了三个独立的数据加载器,分别对应于训练数据集(完整的MNIST训练数据集,共6万个样本),验证数据集(MNIST测试数据集的前5000个样本)和测试数据集(MNIST测试数据集的后5000个样本)。后续在进行模拟参数训练和测试的时候,就可以直接调用这三个数据加载器对象来访问对应的数据集了。 batch_size与Mini Batch 以上的三个dataloader变量在定义的过程中,都指定了一个batch_size参数。这个batch_size参数是一个常量,用于指定每次从数据集中取出的样本个数。在这种情况下,后续无论是训练、验证还是测试,每次从数据集中读取到的样本数就是batch_size。 例如设置batch_size=100的情况,每次通过dataloader的接口访问数据集可读取到100个样本,这100个样本一次性送入到网络中通过前向网络运算得到输出,对比这100个样本的标签就得到输出与标签的差异,然后基于差异再通过梯度下降法和反向传播法更新网络中的参数。也就是说,对网络参数的训练是以这个batch为单位进行的,而不是每个样本计算后都更新网络参数,这就是神经网络训练的mini batch概念。对于MNIST训练数据集的6万个样本而言,batch_size为100的情况下,基于整个训练数据集进行一轮训练,会分为600个Mini batch,也就是网络的参数会更新600次。当然,一般情况下,对于网络的训练,通常会基于相同的训练数据集进行很多轮训练,才会得到满意的效果。 基于Pytorch的CNN网络结构设计 用于训练和测试验证的数据集准备好以后,接下来就要开始进行网络设计了。 Pytorch框架中提供了一个torch.nn.Module类作为神经网络模块的基类,我们基于Pytorch框架所构建的网络就应以这个基类派生。该类主要的派生函数包含__init__和forward,前者用于声明网络结构所需要的框架,即每个层的规格定义;后者则实现训练样本从输入到输入的完整过程,也就是对各个层计算的定义以及多个层之间的连接。 以下代码基于torch.nn.Module构建了一个简单的卷积神经网络。通过以下代码可以看到这个神经网络包含两组卷积+池化层,以及两个全连接层,最后以log_softmax激活函数输出对10个数字的识别结果。 - 第一个卷积层包含4个卷积核,卷积核尺寸5x5,通过padding=2确保卷积计算不会缩小尺寸。 - 第二个卷积层包含8个卷积核,卷积核尺寸5x5,通道数为4(这是因为上一个卷积+池化的结果是4张特征图),同样通过padding=2确保卷积计算不会缩小尺寸。 - 两个卷积层使用的池化层规格是一样的,而且池化层的计算不涉及参数训练,所以__init__中只包含了一个池化层,采用2x2 Max池化方式,每次步进幅度为2。 - 卷积+池化层后面跟着两个全连接层,最后一个全连接层通过log_softmax激活函数输出识别结果。 - 最后一个池化层与第一个全连接层之间要通过Flatten操作实现多维特征图向全连接层所需要的一维向量的转换,这个转换通过forward函数中的x.view操作来实现。 - 第一个全连接层的神经元个数为512个,第二个全连接层个数为10个,对应于0-9共10个数字。 - 两个卷积层和第一个全连接层的激活函数都是ReLU,最后一个全连接输出层的激活函数为log_softmax。 除了torch.nn.Module基类中的__init__和forward函数以外,以上卷积神经网络的实现中还包含了一个自定义的retrieve_features,对于该函数的调用,以一个样本数据作为输入,可以读出该样本在当前网络参数状态下的第一层和第二层特征图,方便用户跟踪图像数据在卷积层的计算操作结果。 基于Pytorch的网络参数训练流程 以上的卷积神经网络定义好以后,接下来创建这个网络的示例,并启动和执行完整的参数训练流程。 首先基于之前声明的卷积网络类ConvNet定义一个变量,然后指定在后续训练过程中需要使用的损失函数(交叉熵函数,CrossEntropyLoss)和梯度下降算法参数。 在网络参数的训练过程中,一个epoch就对应于对整个训练数据集6万个样本的一次完整训练,可以多训练几次以提升网络的识别性能,例如设置num_epochs=20或者30。 在训练的过程中,基于以上介绍的mini batch的训练思路,每次对训练数据集的访问,会得到一个batch_size的训练样本(例如一次性得到100个样本),把这个mini batch的样本一次性送入网络进行计算,得到输出结果,然后利用前面指定的损失函数评估输出结果与训练样本标签的差异,在基于差异利用梯度下降法和反向传播法更新网络参数。一个mini batch的样本训练,对应于一次网络参数的更新。 此外,在进行参数训练过程中,还可以定期(例如每100个Mini batch后)基于当前训练的参数状态,利用独立的验证集数据测试当前网络参数的识别性能打印出来,这样就可以实时的了解到在训练过程中网络性能的改善进展。 从以上代码中可以看到,完整的这个过程对于网络而言,可以分为参数训练过程和数据测试/验证过程。分别对应于以下代码: 此外,在以上代码的训练和验证测试过程中,为了了解当前样本batch的正确率,还定义了一个rightness函数,用于比较样本预测输出与样本标签,返回正确的样本数量和样本总数: 模型的测试验证 最后的一步的测试阶段就比较简单了,基本上跟上面的在训练过程中定期使用验证数据集对网络性能的测试相同。 与上面参数训练过程中的验证环节类似,网络进入评估状态,然后从测试数据集中按照batch_size读出样本,送入网络进行推理输出,统计每个batch样本预测的准确率。同时,把训练过程中训练数据的误差率和验证数据的误差率打印出来,可以了解到训练过程中识别正确率的收敛情况。 在num_epochs=20,batch_size=50的情况下,对该网络进行训练,最终再对测试数据集进行测试得到的识别正确率高达98%以上: !image.png 参考资料 - 《深度学习原理与PyTorch实战(第二版)》第五章 - [資料分析&機器學習] 第5.1講: 卷積神經網絡介紹(Convolutional Neural Network) - JamesLearningNote - Medium

卷积神经网络CNN的架构解析入门

本文基于对网络资料的学习,对一个简单的卷积神经网络CNN的架构、数据计算和传递流程进行详细的解析和总结,作为理解卷积神经网络架构的基础。 卷积神经网络CNN架构各个网络层的设计中,最典型的特征就是在经典的神经网络结构中增加了卷积层和池化层。关于CNN的卷积和池化层的运算逻辑与流程,在详细解释卷积神经网络中的卷积与池化计算一文中有详细总结。 卷积神经网络CNN的整体架构设计 对于计算机视觉、图像识别、图像目标检测等领域的人工智能开发而言,卷积神经网络CNN几乎是一个绕不开的话题。因此,要了解这些领域的AI模型算法及参数训练,首先需要对卷积神经网络的整体架构设计、各个层次的作用以及数据计算与传递的流程有清楚的理解。 下图是一个典型的基于卷积神经网络的图像识别模型:图像从左侧的输入层输入,通过一系列处理以后,在输出层以识别概率的方式输出结果。 !cnn_banner.png 从整体的网络结构上可以看到,以上的整个网络可以分为两个部分:特征提取部分和图像分类部分。 特征提取部分的结构非常有规律,二维图像从输入层输入以后,进行卷积计算(Convolution),然后把卷积计算的结构再用激活函数ReLU进行处理,然后进行池化操作(Pooling);池化后反复进行多次卷积-ReLU激活-池化...直到最后通过Flatten操作把多维向量转换为一维向量进行后续的分类操作。 图像分类部分基于特征提取部分提取并经过Flatten操作转换后的一维特征向量,以多个全连接层的方式实现图像分类任务,最终以Softmax作为最后输出层的激活函数,输出图像分类的概率识别结果。 > 相比于普通的全连接层的神经网络结构而言,对图像数据使用卷积神经网络具有先天性的优势。因为全连接神经网络对于数据的输入和处理均不会考虑图像数据的空间结构,例如它同等对待相距很远和彼此接近的相邻像素,这样就只能从后续的训练数据中来推理和学习像素本身应该具有的空间结构。而卷积神经网络直接以二维图像的方式对数据输入和处理,天然考虑了不同像素位置之间的空间结构,因此非常适合用于图像这类二维数据结构,能够更快的进行训练和推理。 以下按照完整网络结构执行的先后顺序对各个层次的处理流程进行详细总结。 卷积层 卷积层的作用主要是用于提取图像中的特征数据,后续的分类层就依据卷积层所提取出来的特征数据对不同的图像进行区分并分类。 卷积层的运算如以下动图所示。以卷积核为单位对原始图像或者上一级的特征图进行扫描,根据进行卷积计算的输入是单通道图像数据还是多通道图像/特征图,分别采用平面卷积核和立体卷积核对多维数据进行卷积计算。在详细解释卷积神经网络中的卷积与池化计算一文中对如何进行平面和立体卷积计算做了非常详细的总结,可进一步参考。 !image.png 通过一个卷积核对输入图像/特征图进行一次完整的扫描(卷积计算),就可以得到一张新的特征图。有多少个卷积核就执行多少次扫描,并输出多少张新的特征图,用于后续的池化运算。例如下图使用16个独立的卷积核对输入的图像数据执行卷积运算,产生16张特征图: !image.png 如上所述,执行卷积计算的结果还需要使用ReLU激活函数处理后,才会传递给下一步进行池化操作。对卷积计算结果的每个像素通过使用ReLE激活函数过滤掉负值,保留图像中物体的形状。 !image.png 池化层 卷积神经网络的池化操作用于对卷积层提取的特征图数据进行降维,目的是保留显著特征的情况下降低数据量,因此就可以降低卷积神经网络的参数数据量和训练以及推理的计算量。 池化所进行的计算非常简单,只需要以 n*n 为单位对前一级卷积层所输出的特征图进行最大化(Max Pooling,选择最大值输出)以及平均化(Average Pooling,对所有值求平均)扫描,每次扫描后步进为 n ,不会与上次扫描相重叠(这一点与卷积操作明显不同),因此最终扫描的结果就是一个在水平和垂直方向上均缩小 n 倍的缩版特征图。 以下图为例,上级卷积层输出的特征图为4x4分辨率,池化操作以2x2对特征图进行扫描,最终池化操作的结果就是一个2x2的缩版特征图。 !image.png 池化操作以单张特征图为单位进行以上的池化计算,所以前一级卷积层输出多少张特征图,池化层就会输出多少张缩版特征图。 详细的池化操作可参考详细解释卷积神经网络中的卷积与池化计算一文。 Flatten 如上所述,无论是卷积层还是池化层,其输出都可以认为是多张二维特征图。经过卷积和池化处理以后,CNN接下来就要把前一阶段经过卷积和池化提取出来的特征图转换为一维数据,方便在后续的环节中进行处理,这个特征图转换为一维数据的动作就是Flatten。 Flatten环节的处理非常简单,就是把二维的特征图从上到下,从左到右展开即可: !image.png 当然,一般而言,最后一个池化层的输出一般会包含多张特征图,那么针对这种多张特征图的Flatten操作,就是先对第一张特征图进行一维转换,然后把后续的特征图按照相同的方式继续展开到这个一维数据后面。无论如何,多张特征图通过Flatten操作最终转换为一个一维的数据序列进行后续的分类操作。 !image.png 识别层 如上所述,经过以上的Flatten操作以后,在卷积层和池化层中所检测出来的多维特征向量就会转换为一维向量进行图像分类任务的处理。 与普通的人工神经网络结构相比,这个识别层的网络没有什么特殊之处,就是通过多个全连接层最后再叠加一个Softmax的输出层: !image.png 卷积神经网络架构中的全连接层网络部分以前一阶段的卷积和池化所提取出来的特征向量作为输入,利用多个全连接层所构成的网络来实现图像分类的功能,在全连接层的最后一层使用Softmax激活函数以概率的形式输出图像对于各个不同类别的识别结果。 从以上CNN网络的整体架构来看,整个网络中所包含的主要训练参数就是特征提取部分各个卷积层的卷积核权重参数以及图像分类部分全连接层各层之间的权重和偏置参数。那么在使用CNN网络结构对图像识别功能进行训练的过程中,基于网络最后的Softmax激活函数的输出,与训练样本标签之间,使用交叉熵函数作为损失函数,对卷积核权重参数以及全连接层权重和偏置参数进行训练,直到网络能够针对输入图像输出稳定、正确的识别判断结果。 - 有关深度学习激活函数和损失函数部分的内容,可参考盘点深度学习神经网络架构中的常用激活函数和盘点深度学习神经网络架构中的常用损失函数。 参考资料 - Convolutional Neural Network | Deep Learning | Developers Breach - Flattening CNN layers for Neural Network and basic concepts | by Muhammad Shoaib Ali | Medium - [資料分析&機器學習] 第5.1講: 卷積神經網絡介紹(Convolutional Neural Network) - JamesLearningNote - Medium

深度学习的数学基础知识总结

本文对学习和理解深度学习所需要的数学基础知识,尤其是微分、偏微分、复合函数等相关的理论知识进行了总结,并基于python做了简单的代码演示。 微分 数学中的微分理论被广泛的引用到机器学习的各种最优化问题的解决方面。 通过基础的微积分计算,我们可以得到一个函数在某个点的斜率,或者说准确的评估函数在某个点的瞬间变化的情况。对于微分概念的理解,以汽车行驶速度为例: - 通过一段时间内,汽车的完整行驶距离,可以得到这段时间里面的平均速度。 - 而通过对这段时间车辆的行驶距离与时间的对应关系求微分,则可以得到车辆在每个时间点上的瞬时速度。 所以,所谓微分的定义,就是函数在某个点上瞬间的变化率,实际上也就是函数在这个点上的斜率。 我们设函数为,设 为一个接近0的数,那么该函数在点 位置的微分(实际上也就是在点 上的斜率)就可以用以下表达式进行计算: 对以上微分计算式定义的解读实际上就是:在输入 上的微小变化,会导致其函数 的值发生多大的变化。 在计算机中使用程序进行函数的微分计算时,往往会给 h 取一个接近于0的固定的数,然后计算函数在某个点上的变化,也就是所谓的数值微分(与之相对应的则是基于数学理论的解析推导): 对于数学微分计算的推导而言,有以下几个常用的特性,在进行深度学习理论的过程中非常重要。 第一个特性是,当 时,对它进行的微分推导可以得到以下表达式: 也就是说,单独常数的微分是0(这是很自然的事情,因为常数是一条水平的直线,其斜率当然是0), 的微分是1 , 的微分是 , 的微分是 ,依次类推。该结论对于负指数也是成立的,例如 的微分是 。 第二个特性是微分的线性化。对于函数 f(x) 和 g(x) ,以及常数 a, 以下表达式成立: 基本上,对于单变量函数的微分计算与推导而言,掌握以上理论知识基本上就已经够用了。 偏微分 以上有关微分计算部分的叙述,针对的都是只有一个变量 x 的单变量函数。但是实际上我们在深度学习理论中所面对的实际问题,绝大多数情况下都是类似以下的这种多变量函数: 针对这种多变量的函数,我们要进行的就是偏微分的计算:在进行偏微分的计算过程中,只需要关注进行微分计算的变量,把其他变量都当成常数来进行处理即可。 例如,设函数 ,对以上函数首先求 变量的偏微分,此时应该把 变量看作是常数(例如把 固定为1),独立常数的微分为0,那么 针对 变量的偏微分(偏微分的符号发生了变化)为: 按照相同的逻辑计算 f(x_1,x_2) 针对 x_1 变量的偏微分: 以下代码示例演示了偏微分的计算逻辑。数值微分的计算过程仍然是相同的,只不过每次计算一个变量的偏微分是要保证只有这个变量是变化的。以下代码演示了函数 在(3,4)位置的偏导数: 而在机器学习理论中反复提到的所谓梯度,实际上就是函数针对所有变量进行偏微分计算的集合: !image.png 复合函数 所谓的复合函数,就是一个函数作为变量出现在另外一个函数中,由多个函数组合而成的函数。例如: 对于复合函数求微分的情况,例如对上式中 f(g(x)) 对变量 x 求微分,可以采用链式法则进行推导:如果某个函数由复合函数表示,那么该复合函数的微分可以用构成该复合函数的各个函数的乘积表示。 上面链式法则的描述比较抽象,可以通过以下对 例子的推导过程来进行理解: 深度神经网络通常都包含很多层,模型训练的目标是要尽可能通过调节各层的参数让最后输出层的损失函数最小,因此从大的概念上讲,可以把各层的权重、偏置等都看做是最后一层损失函数的变量。要让损失函数最小,就是要通过计算各层每个参数的偏微分(也就是导数)来决定参数调节的方向,各层参数偏微分的计算就要依赖于以上的复合函数+链式法则从输出层向上一层层求导得到。 参考资料 - 《白话机器学习的数学》附录 - 《深度学习入门:基于Python的理论与实现》第4章 - 機器/深度學習-基礎數學(二):梯度下降法(gradient descent) | by Tommy Huang | Medium

盘点深度学习神经网络架构中的常用损失函数

本文对深度学习神经网络中常用的损失函数进行详细的学习和总结,对各种损失函数的特性及其适用性进行整理,方便以后理解各种深度神经网络的架构设计。 神经网络损失函数的定义 所谓的损失函数,可以看作是表征当前神经网络工作性能的评估指标,也就是说,当前神经网络对于输入其中的数据进行处理的结果,与实际的真实结果相比较在多大程度上不一致,因此,损失函数的值越小越好。神经网络在训练过程中的目标,就是要找到让损失函数最小的参数(神经网络的权重和偏置)。 例如,如下图所示的简单例子,我们希望通过训练神经网络来拟合一条直线 f(x) ,来最大程度上反映出来给定样本数据输入 x 与对应输出 y 之间的对应关系,后续可以基于这条直线与新的输入 x 找出其对应的 y 。那么怎么来评估这条直线对于样本数据的拟合程度有多好呢?答案就是使用损失函数来评估. 在下图中,损失函数就是所有训练样本数据中 y 与拟合直线对应输出 f(x) 之间的差值 (f(x)-y) 之和。针对所有训练样本数据而言,这个累加的差值之和越大,表示损失函数越大,拟合程度越差;否则损失函数越小,拟合程度越差. !image.png > 上图中如何计算各个样本点与拟合直线之间的差值,有两种方法,一种是使用其插值的绝对值 |f(x)-y| 。另外一种是对其距离的平方 (f(x)-y)^2 (也就是以均方误差作为损失函数)。一般情况下使用后者,因为后续进行参数学习的时候,要对损失函数计算微分,比起绝对值,平方的微分计算更为简单。 为什么要有损失函数? 以图像分类任务为例,很多时候我们会有疑惑,既然我们训练神经网络参数的目标是让其能够更准确的对图像进行分类,那么最直接评估神经网络性能的指标当然就是对图像分类判别的成功率,为什么还需要额外引入一个损失函数的指标呢? 最主要的原因在于,实际应用场景中的神经网络结构往往非常复杂,层数和参数非常多,在这种情况下,每次训练对网络权重和参数的微小修改,对于送入神经网络进行学习和训练并最终进行正确分类的图片数量不会产生任何影响,在这种情况下,下一步对于网络的权重等参数要如何调节就失去了指导方向.因此在实际的神经网络参数学习中,就需要一种能够在输出指标评价上更为精细和平滑的损失函数,来评估每次对参数微小调整后所产生的效果,继而决定下一步的参数调整方向。 均方误差MSE 均方误差(Mean Squared Error, MSE)是机器学习和统计学中最核心的损失函数之一,尤其适合用在回归任务(基于历史数据对模型训练,然后预测新的输入数据所对应的输出)中。 在数学计算上,均方误差的定义如下: 其中,n 为样本数量,为第 i 个训练样本对应的真实值, 为当前模型参数对第 i 个训练样本输入所计算出来的预测值。 那么 就是每个样本的预测误差,通过以上公式计算,就可以得到所有样本预测误差平方的算术平均值,从而反映出来模型在整个训练数据集上的平均误差强度。 训练均方误差的Python实现示例代码如下所示: 交叉熵误差Cross-Entropy 交叉熵误差则主要用于评估样本数据提供的真实概率分布 p 与模型预测概率分布 q 之间的差异状态,其计算公式如下所示: 其中: - C :表示模型输出类别判断的总数。例如对于手写字符判别而言,其识别结果为数字0-9,因此模型输出的类别总数 C 就是10个。 - :表示当前的训练样本对应于类别 i 的真实概率,一般使用one-hot编码。 - :表示当前模型所输出的预测数据属于类别 i 的概率,所有类别的概率之和为1,也就是说 。 > 所谓的one-hot编码,就是只有正确答案的标签为1,其他都是0的数组。例如手写数字6训练样本真实概率所对应的one-hot编码就是[0,0,0,0,0,0,1,0,0,0],只有6这个数字对应的索引位置概率为1,其他全部为0。 因此,对于多分类任务而言,如果当前训练样本的真实概率使用one-hot编码的话,那么实际上也就意味着在进行交叉熵误差计算的时候,只有真实类别 的这一项对于计算误差有贡献,其他类别都是0。 例如,对于某个多分类任务的训练而言,某个训练样本的真实标签为 y=[0,0,1] ,模型针对这个样本输入所预测出来的概率是 y^=[0.1,0.2,0.7] ,则计算该样本对应的交叉熵损失为: ,也就是说,只需要对真实标签概率为1的这一项的预测概率求其log值就可以了。 交叉熵误差损失函数的Python实现示例代码如下所示。需要注意,因为log0在数学上没有意义,所有交叉熵实现代码中需要对这种情况做特殊处理,使用一个很小的正数代替log0。 参考资料 - 《深入浅出神经网络与深度学习》 - 神经网络算法——损失函数(Loss Function)_神经网络损失函数-CSDN博客

盘点深度学习神经网络架构中的常用激活函数

本文对深度学习神经网络中常用的激活函数进行详细的学习和总结,对各个激活函数的特性及其适用性进行整理,方便以后理解各种深度神经网络的架构设计。 激活函数是什么? 一个神经网络的典型神经元的架构如下图所示: !image.png - 输入层的输入与其权重参数各自相乘后累加,然后加上该神经元的偏执参数bias,这就是神经元的输入信号。 - 神经元在其内部处理中,把以上输入信号作为参数,执行其激活函数 f(x) ,激活函数的输出即为神经元的输出。 因此,所谓神经元的激活函数,本质上就是神经节对输入信号加权和的非线性映射和变换而已。通过激活函数的映射,神经网络的计算输出可以逼近任意复杂的函数(如曲线边界分割、多目标重叠检测),解决实际应用中的复杂问题。 为什么需要激活函数? 在没有激活函数的情况下,无论神经网络的层数是多少,其输出都是输入的线性组合,这样就等效为单层的感知机,层数(也就是所谓的深度)失去了意义。激活函数要解决的问题,就是从线性空间向非线性空间的映射。 > _如果没有激活函数的话,实际上无论神经网络有多少层,其输出最终都是对输入数据的线性变换,无法处理复杂的非线性问题。而我们现实生活中要解决的问题基本上都是非线性问题。激活函数通过引入非线性的映射,使得神经网络可以逼近任意复杂的非线性函数。_ 阶跃函数 阶跃函数是神经网络发展早期的感知机所使用的激活函数。阶跃函数以阈值(通常为0)为界,输出二元值:当输入大于等于0时,阶跃函数输出为1;否则输出为0。 其图形如下所示: !image.png 阶跃型激活函数的主要特点: - 其输出呈阶梯状跳跃,在x=0处不连续,形成明显的“断点”。 - 其输出范围只有0和1两种状态(对应于全有和全无两种情况),无法表达中间状态和概率信息。因此比较适合二分类任务。 - 因为在x=0位置跳变,形成了不连续点,所以无法进行微分计算(导数计算要求函数在局部区间内平滑变化),因此无法使用梯度下降法(依赖于对激活函数的偏微分计算)和反向传播法训练参数。 > 想象一下,采用阶跃函数作为激活函数的情况下,对于神经网络中的权重和偏执等参数的微小修改,有可能会导致输出完全翻转,进而引起神经网络对于其他输入数据进行处理的行为发生复杂的巨变。因此,这种情况下,就很难通过逐步修改网络的权重和偏执参数来实现神经网络判断准确性的调优。 因为以上的阶跃函数所存在的只有两个值的取值范围、不可微分等特性,目前阶跃函数已经不用于主流网络的隐藏层,仅在理论教学(感知机)、嵌入式系统轻量级二分类系统等领域有少量的应用。 阶跃函数的Python实现如下: Sigmoid函数 Sigmoid函数也被称为Logistic函数,是一种S型曲线函数。Sigmoid函数是神经网络中经典的激活函数之一,尤其在早期深度学习和二分类任务中应用广泛。 Sigmoid函数的数学表达式如下: 其图形如下所示: !image.png > 从图形的对比上可以看到,Sigmoid激活函数和阶跃函数激活函数类似,但是在使用Sigmoid激活函数的情况下,权重和偏置的微小改动只会引起输出发生微小变化,这一点对于使用sigmoid作为激活函数的神经网络正常学习并调整其参数至关重要。 Sigmoid激活函数的主要特性总结如下: - 与阶跃函数类似,输出值范围在0到1之间,且在(0,0.5)点中心对称,因此非常适合用于表示概率。 - 随输入x的增大严格单调递增,输入增大时输出趋近1,减小时趋近0 - 曲线平滑,处处连续可导,可通过后续的梯度下降法逐次计算逼近极值。 Sigmoid激活函数在神经网络的发展早期阶段应用较广,但因为诸多缺点,在当前的神经网络架构中已经较少使用: - 梯度消失问题。从上面Sigmoid的图形上就可以看出,当输入大于5或者小于-5的时候,整个图形几乎呈现水平的趋势,导数趋近于0,这一点会导致在使用梯度下降法寻求损失函数极值的过程中参数无法更新,训练停滞,这一点在网络层数较多时尤其明显。 - 非零中心化问题。Sigmoid的输出范围为0到1之间,随着输入x的增益严格单调递增,所以无论输入如何,其输出始终大于0。这样就会限制后续基于梯度调整参数的方向,导致调整过程中反复横跳调整参数的方向,训练收敛速度会显著低于零中心化激活函数(如Tanh)。 - 计算开销大。因为涉及到指数运算,相比于ReLU等现代激活函数计算成本过高,拖累训练速度。 Sigmoid函数的Python实现如下: Tanh函数 Tanh函数又称为双曲正切函数,可以认为Tanh函数是Sigmoid函数的变形形式(本质上Tanh是Sigmoid的零中心化改进)。与Sigmoid函数最大的不同在于,Tanh函数的取值范围为-1到1之间单调递增,且是0均值。Tanh函数的数学表达式为: 尽管其数学表达式看上去很复杂,但是它的图形跟Sigmoid非常类似。Tanh函数的图形如下所示: !image.png Tanh激活函数的主要特性总结如下: - 输出范围在-1到1之间,并且以原点对称 (tanh(−x)=−tanh(x)) ,这样就避免了Sigmoid的非零中心化问题,使后续层输入分布更均衡(不再是恒正),从而加速梯度下降的收敛。 - 与Sigmoid一样,输出随输入的增大逐渐单调平滑增大,函数处处可导。 - 在梯度变化方面,相比Sigmoid而言,梯度在相同输入下更大,一定程度上缓解了梯度消失的问题。 总之,Tanh函数解决了Sigmoid存在的非零中心化的问题,可以加速基于梯度下降的参数训练过程,也一定程度上缓解了梯度消失的问题(当输入偏离0过大的时候仍然存在),但是因为要进行指数运算,仍然存在计算成本过高的问题。 Tanh函数的Python实现如下(实际上numpy已经有自己的tanh函数实现了): ReLU函数 Sigmoid是神经网络发展早期应用比较广泛的激活函数,而深度学习时期应用最广泛的激活函数则是ReLU(Rectified Linear Unit)。 ReLU函数在输入大于0的时候,直接输出原值;而输出小于0的时候,直接返回0。其输出范围为0到无穷大,数学表达式为: ReLU函数的图形如下所示: !image.png ReLU激活函数的主要特性总结如下: - 分段线性。正区间保持线性,梯度始终为1,负区间强制归零。 - 计算高效。是ReLU最大的优势,不需要任何三角函数和指数运算,计算速度比Sigmoid和Tanh快10倍以上。 - 稀疏激活。在输入为负的情况下输出为0,此时神经元处于不激活状态,相当于只允许部分关键神经元处于激活状态,从而迫使整个网络优先选择最重要的特征。 - 解决梯度消失的问题。在正区间梯度恒定为1,可以用于支持超深层的网络。而且线性正区间可避免饱和,梯度更新幅度较大,收敛速度显著快。 ReLU最大的问题是死亡神经元问题。该问题是指当使用ReLU激活函数时,某些神经元可能会出现永久失效的现象。在训练的过程中,当神经元的权重更新导致其输入恒为负值时,ReLU的输出恒为0,而梯度也为0,就会导致该神经元无法再更新参数,处于实质性的永久失效的状态。当模型中死亡神经元的占比较大时,就会明显的影响整个网络的工作性能和精度。 ReLU函数的Python实现如下: Leaky ReLU Leaky ReLU(Leaky Rectified Linear Unit)是ReLU激活函数的改进版本,旨在解决其以上提到的死亡神经元的问题。Leaky ReLU针对负区间引入了一个微小的正数斜率 a (一般是0.01): 因此,Leaky ReLU在负区间就不再恒定是0,而是以 a 为斜率的直线,取值范围也变成了负无穷大到正无穷大。 !image.png 以上的这个设计可以有效的解决ReLU存在的死亡神经元问题:通过负区间的斜率 α 可保留微小的梯度,确保反向传播时权重仍然可以更新。 此外Leaky ReLU仍然保持了计算效率高的优势。其计算仅需一次乘法比较,计算复杂度与ReLU相近,远低于Sigmoid/Tanh的指数运算。 Softmax函数 Softmax函数是在神经网络中用于处理多分类任务。例如,对MNIST手写数字数据集进行识别的分类任务)的核心激活函数,其核心作用是将神经网络的原始输出转换为概率分布,使每个类别的概率值非负且总和为1。 Softmax函数的主要作用就是进行多分类的概率转换,其输入是一个多类别的向量数组 z=z_1,z_2...z_k ,其对应的数学表达式为: 在以上softmax的表达式中,在分子方面,对第 i 类输入 z_i 取指数,以放大各个分类的差异;在分母方面,取所有类别输入的指数和,实现归一化,以确保输出的总概率之和为1。 !image.png Softmax激活函数的主要特性总结如下: - 概率归一化。所有输入对应的输出值范围在0和1之间,并且所有输出之和为1,符合概率公理,非常适合用于分类决策。 - 可放大差异。在softmax的计算中通过指数函数可以显著放大高输入的输出权重,这样可以使模型对高置信类别在输出重点表现更明确,减少模糊预测。 - Softmax激活函数与交叉熵损失函数是分类任务的标准组合,梯度计算高效且收敛快。 Softmax函数的Python实现如下: 参考资料 - 神经网络中的激活函数及其各自的优缺点、以及如何选择激活函数_神经网络的激活函数有哪些?他们对神经网络的性能有何影响。-CSDN博客 - 激活函数全解析-CSDN博客

详细解释卷积神经网络中的卷积与池化计算

本文基于对参考资料的学习,以一个简单的卷积神经网络结构为例,对卷积神经网络中的卷积与池化操作的完整计算过程进行了详细的总结和解释。 > 本文实际上是《深度学习原理与Pytorch实战(第二版)》第五章的部分读书笔记,基于自己的语言整理出来加深记忆和理解。这本书是讲解深度学习理论和Pytorch实践的一本不可多得的好书,非常适合入门学习,强烈推荐。 下图是一个典型的用于对手写图像目标进行检测的卷积神经网络的结构示例: !image.png 从以上卷积神经网络的结构上看总体可以分为四层: - 输入层:输入数据是一副由二维矩阵表示的手写图像,矩阵中的每个元素对应图像的一个像素,每个像素包含一个0-255的灰度值,值越大越白,否则越黑。 - 卷积和池化层:对应以上架构中的两组交错的卷积层和池化层,把输入图像一步步提取出来其中包含的特征并降维保存,只保留图像中所包含的重要特征并最终输出给特征层执行分类任务。 - 特征层:也称为全连接层,主要就是基于卷积和池化层的输出执行分类任务。把前面池化层的输出的特征拉伸为一维向量,然后整合这些识别出来的全局特征,使用激活函数(如ReLU、Sigmoid等)进行非线性变换,得到一个分类概率或预测值。 - 输出层:最后的输出层包含10个神经元,针对送入神经网络去识别的每一张手写图像,在这10个神经元中输出0-9各个数字的识别概率,所有神经元的识别概率加起来为1。选取最大概率对应的数字作为输出。 本文的目的,是详细解释清楚在以上架构中,图像通过输入层进入卷积神经网络后,在卷积层和池化层所进行的计算操作是如何展开的。 卷积层的计算 与图像卷积操作密切相关的一个概念是卷积核。卷积核可以认为就是一张很小分辨率的图像,在原始图像上所执行的卷积操作,实际上就是使用这个小分辨率的图像(即卷积核)从左到右,从上到下逐个像素进行扫描和匹配,匹配的结果是一张新的图像,即特征图。特征图是一个灰度图像,每个像素的灰度值表示该位置原始图像与卷积核小图的匹配程度。如下图所示: !image.png 以下图为例说明原始图像与卷积核小图之间进行的卷积计算的过程。此处卷积核为一个3x3的小图,其中的每个像素都是一个实数值,代表卷积神经网络的计算权重。 - 卷积核中每个像素对应的权重值就是后续这个卷积神经网络需要训练的参数。 !image.png 如上图所示,首先是使用卷积核对原始图像左上角的卷积计算。此处进行的卷积计算,也就是把原始图像左上角与卷积核相同的3x3区域的每个像素,与卷积核图像对应位置的像素相乘,然后再把这个3x3区域的9个乘积结果相加,就得到了特征图左上角的第一个像素的值。 然后卷积核在原始图像上向右移动一个像素,继续执行相同的操作,得到特征图第一行第二个像素的值。 !image.png 按照以上计算逻辑,依次向右移动卷积核,每次向右移动一个像素,直到移动到原始图像的最右侧。然后再向下移动一个像素,开始做第二行的卷积。如此循环往复,直到卷积核移动到原始图像的右下角,完成整个图像的扫描,得到一副完整的特征图。 !image.png 按照以上的卷积计算逻辑,当原始图像的尺寸为m x n,卷积核的大小为w,那么经过以上卷积计算以后的特征图的大小为(m-w+1)x(n-w+1)。也就是说,特征图会比原始图像稍微小一点。如果希望特征图大小与原始图像保持一致,就需要在进行卷积计算之前,先在原始图四周填补一圈0: !image.png 以上使用一个卷积核,对原始图像所执行的完整卷积操作,可以得到一副与该卷积核相对应的特征图。而实际上,在卷积神经网络的执行中,往往会使用不同的多个卷积核分别对原始图像进行各自独立的卷积操作,从而产生多个特征图。例如,下图表示对125x125分辨率的原始图像进行100个不同卷积核的卷积操作,生成了100个125x125分辨率的特征图: !image.png 这一点,也是卷积神经网络架构中,从输入的原始图像经过第一个卷积层的卷积计算后,二维图像变成一个长方体的原因所在。 池化层的计算 如以下卷积神经网络整体架构的阴影部分所示,每个卷积层后面都有一个池化层,用于对卷积层所提炼出来的特征进行简化,只保留特征图中的粗粒度信息。卷积层C1经过对原始图像使用4个卷积核卷积之后,得到4个28x28的特征图。池化层P1的操作则是对每一个特征图进行池化操作,将其缩小,最后得到4个14x14分辨率的池化后特征图,在保留重要特征的基础上数据量大大减少。 !image.png 池化的操作非常简单。以下图为例,此处进行的3x3池化操作,是把原始特征图中的每个3x3区域基于取值最大的原则,将其最大值像素取出作为整个区域的池化输出。这样原始特征图的每个3x3区域就对应池化后特征图中的一个像素。 !image.png 需要注意的是,池化运算过程中的移动是无重叠的移动,这一点与卷积不同,所以池化后的输出要比原始特征图小很多。因此,上面9x9的原始特征图,经过3x3的池化操作后,其结果是一个3x3的池化后特征图。同样的,上面架构图中卷积层C1的4个原始28x28分辨率特征图,经过2x2的池化操作后,变成了4个14x14分辨率的池化后特征图。 立体卷积计算 池化层的输出是一个三维的立方体(多张池化后的特征图),那么池化后的数据再送入到下一层的卷积层计算时,其卷积计算的流程就跟原始图像送入第一个卷积层所进行卷积计算不同(原始图像是一个二维矩阵)。池化层的输出,送入到下一个卷积层进行计算时,需要进行的卷积计算是立体卷积计算。 下图是一个立体卷积核的示例。左侧是池化层,右侧是下一个卷积层。对于池化层进行的立体卷积计算而言,需要首先选择一个厚度与池化层输出相同的立体卷积核,然后使用这个立体卷积核对池化层所输出的多张池化后特征图进行立体卷积运算,每次运算结果(进行立体卷积运算的整个长方体相应位置相乘后再相加)对应于卷积层的一个像素,使用立体卷积核对池化层输出进行一次完整的扫描,就对应了卷积层的一张特征图。 !image.png 同样的,也可以有多个立体卷积核,使用一个立体卷积核执行一次立体计算得到一张特征图,那么多个立体卷积核的运算结果就对应于卷积层的多张特征图,因此,卷积层的输出也是一个长方体。 因此总体起来,对于一个简单的卷积神经网络而言,其卷积和池化的计算流程就是:二维图像输出-第一层卷积计算(平面卷积计算)-第一层池化运算-第二层卷积计算(立体卷积计算)-第二层池化计算-第二层卷积计算(立体卷积计算)...如此循环往复,直到最后把卷积计算出来的图像特征整理出来传递给特征层进行图像分类任务。 总结起来,对于卷积神经网络CNN而言,其卷积层的参数就是各个卷积层所包含的卷积核中的参数,这就是CNN网络在训练过程中主要训练的参数,而池化层因为只是简单地进行2x2或者3x3的取最大值的操作,所以没有要训练的参数。 参考资料 - 《深度学习原理与Pytorch实战(第二版)》,第5章手写数字识别器-认识卷积神经网络 - 卷积神经网络各个层的作用

非专业人士的AI课6:LLM大模型的典型应用级别

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而在我们的生活和工作中更好的使用AI。 LLM大语言模型在特定行业领域中的应用,大致可以分为下图所示的几种级别。其中,从头开始设计和构建大模型当然是难度和门槛最高的,而最简单的应用就是我们所谓的提示词工程(Prompt Engineering),即在向LLM大语言模型提问的时候,对提问问题的语言组织结构(提示词,Prompt)进行精心安排,使大模型的输出更符合自己的提问需求。 !image.png 从头开始构建模型 对于从头开始构建大模型的应用而言,整个流程就如[[3. LLM大语言模型项目的典型开发流程详解]]所述,从项目需求分析和定义开始进行规划,选择合适的技术路线或者基于开源的大模型框架进行优化,针对模型和应用开发需求,提前准备训练数据,使用数据对模型的参数分别进行预训练和微调,最终部署到服务器上并推广给目标用户使用。 以上的全过程,对于团队能力、算力资源、数据、开发周期的要求都是非常高的。因此,这种开发模式也是大模型应用中门槛最高的类型。 私有化部署开源LLM后微调 这种方式相比从头进行大模型的开发而言,简单了一些,基本上是把已经完成预训练的开源LLM模型(这种已经完成预训练的模型也称为基础模型,Base Model)进行私有化部署,然后按照应用领域的微调需求,使用与该应用领域相对应的微调标注数据进行微调。微调之后的工作量与从头开始构建模型的模式差异不大。 这种开发模式,省去了大语言模型本身的设计开发,以及已经基于海量通用语义数据对模型进行了预训练,只需要进行微调即可,对于团队能力、算力资源的要求大大降低,因此这种开发模式对中小型团队和个人开发者而言比较友好。 指令微调LLM 首先解释一下基础大语言模型(Base LLM)和指令微调大语言模型(Instruction-Tuned LLM)。 - Base LLM 是一种通过大规模自监督学习(self-supervised learning)预训练的语言模型,主要依赖海量文本数据来学习语言结构和模式。这类模型虽然具备强大的文本生成能力,但在执行具体任务时往往缺乏明确的指令理解能力。也就是说,在[[3. LLM大语言模型项目的典型开发流程详解]]中完成了预训练,但是尚未进行微调的模型就是Base LLM,相当于这类模型具有对语言的语义规律和基础知识有充分的理解,但是缺乏对于用户输入指令的理解能力。 - 相比之下,Instruction-Tuned LLM 是在 Base LLM 的基础上进行了额外的指令微调,目的是使其更善于处理人类提供的任务指令,提高了实用性和交互体验。相比Base LLM Model,经过指令训练集进行指令微调以后,模型就具备了更强的任务执行能力,能够更准确地按照用户的需求生成符合用户预期的、有价值的内容。 基于Base LLM,对该模型进行指令数据集的微调之后,Base LLM就可以进化成为Instruction-Tuned LLM。指令数据集一般包含指令-上下文-响应的完整序列,因此指令微调对于数据的要求很高,一般包含人类标注员针对指令微调任务所编写的大量任务示例,用以训练模型理解任务指令并生成符合预期的响应。 下面是指令微调数据集的比较典型的例子: !image.png - 用于指令微调的数据集对于数据的内容和格式有比较高的要求,databricks-dolly-15k是一个开源的指令微调数据集,其中包含由数千名 Databricks 员工生成的遵循指令的记录。可以从HuggingFace上下载该数据集。 经过以上的指令数据集对Base LLM的微调之后,模型就可以对用户所输入的指令进行识别、执行并输出更符合用户预期需求的答复。而为了进一步约束模型输出的答案,部分模型还会采用人类反馈强化学习(RLHF)的模式让模型生成的答案更加符合人类的价值观和伦理道德偏好。 - 这个过程在[[3. LLM大语言模型项目的典型开发流程详解]]中已经有详细的描述。 其实,微调的含义往往超过了此处重点介绍的指令微调,实际的应用中,我们也会针对具体行业领域和应用场景所需要的专业知识,整理出来针对性的微调训练数据,在Base LLM的基础上针对这些具体的行业领域场景进行微调,使之更适合应用于该领域。这部分微调的过程基本上与指令微调类似,都是在Base LLM的基础上进一步使用额外的训练数据进行训练,使之更加符合应用的要求和预期。 > 在此之上的部分,都可以称之为AI模型开发(AI Model Development);而在此之下的则可以称之为AI应用开发(AI Application Development)。 RAG/AI Agent RAG和AI Agent是当前AI应用开发领域中最为火热的方向。 有关RAG部分的信息,在[[4. 一文说清楚RAG的工作流程以及与微调的区别]]一文中已经有非常详细的解释和描述,在此不再赘述。 有关AI Agent部分的信息,在[[5. AI Agent的框架与工作流程解析]]一文中已经有非常详细的解释和描述,在此不再赘述。 Prompt Engineering 下面的少样本提示和零样本提示都可以归纳入提示词工程(prompt engineering)的范畴。提示词工程就是我们精心设计、优化输入信息,从而引导AI生成高质量、准确、有针对性的回答的语言艺术。如果你写提示词的能力不够,那么AI给出的往往就像是用一堆正确的废话堆砌而成的文字垃圾;反之,它的回答会非常令人惊艳。所以,使用同样的大模型去查询相同的问题,大模型的输出质量与输入的提示词有非常强的关联。 提示词如何书写,并没有一个明确的答案,但是考虑到大语言模型进行输出信息推理的工作原理和流程,一个较好的提示词应该至少包含角色定义、提供上下文以及确定目标/提出问题这三部分信息。以下是参考资料三种提到的一种写好提示词的框架结构,按照这个框架对自己向LLM提出的问题和要求的描述进行精心组织,可以很有效的解决我们在工作和生活中的大量问题。 !image.png 少样本提示 所谓的少样本提示(Few Shot Prompting),就是针对一些稍微复杂的问题,我们使用语言很难轻易的把需求清楚的表达出来的时候,我们就可以在向大模型提问的时候,给大模型提供一些样本,让模型自己去分析样本的结构、逻辑、语气表达形式等,并根据自己学到的东西,给我们生成类似的内容。 下面是一个少样本提示的很经典的例子。用户向大模型输入的提示词是: !image.png 以上提示词从模型返回的结果是: !image.png 零样本提示 零样本提示(Zero Shot Prompting),是指不需要在提示词中包含任何示例,大模型就能够完成用户所指示的任务。这种提示词一般是用于面向比较简单、目标和过程清晰的任务,而且也要求用户的提示词十分明确,大模型具有相当强的能力理解用户的需求。 总之,针对相对比较简单和目标清晰的任务,可以使用零样本提示词尽可能明确、清晰、详尽的定义自己要模型执行的任务,以方便模型理解。 参考资料 - 《大模型应用开发:动手做AI Agent》 - 《大模型RAG实战:RAG原理、应用和系统构建》 - 《AI效率手册:从ChatGPT开启高效能》 - 一文说清楚什么是基础模型(Base LLM)、指令微调模型(Instruction-Tuned LLM)

非专业人士的AI课5:AI Agent的框架与工作流程解析

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而在我们的生活和工作中更好的使用AI。 AI Agent是什么? 在AI的应用方面,目前最成熟和最广泛的仍然是以类似ChatGPT一样的对话应用,系统提供一个可支持文本输入或者语音输入的界面,用户在其中按照提示词工程的要求输入问题,AI系统对其进行学习研究、推理决策、生成答案,最后再通过文本框或者语音的形式把生成的答案展现给用户。 而AI Agent,也称为AI智能体或者AI智能代理,是一个具有一定程度自主性的人工系统。更具体地说,AI Agent是一个能够自动感知环境、做出决策并付诸行动的智能系统。 因此,相比于ChatGPT类型的对话方面的应用(重心在于能够回答用户所提出的问题),AI Agent系统所具备的自主性和适应性是其最大的特色。Agent能够自动通过对输入的分析感知环境、参数变量的变化,针对变化自主思考下一步的行动计划,并自行执行经过深思熟虑的思考后的行动,最终给出结果。 - ChatGPT类应用:会对话 - AI Agent:会做事 AI Agent的架构 下图是参考资料1第二章所提出的一个AI Agent的框架图。从图中可以看到,一个典型的AI Agent包括四大元素:规划,记忆,工具,执行。 !image.png 规划 AI Agent需要具备思考和规划的能力,而LLM所具备的推理能力很好的解决了这个问题,这也是LLM在推理方面逐渐成熟一下,AI Agent也随之爆发的原因所在。 对完整的复杂任务所进行的规划和思考,主要包含对子目标和任务的分解(Subgoal Decomposition)、连续的思考(即思维链)、自我反思和批评(Self-critics),以及对过去行动的反思(Reflection)等。 记忆 AI Agent的记忆包含两个方面,短期记忆和长期记忆。 短期记忆与任务执行的上下文相关,属于提示工程的一部分,一般在AI Agent运行过程中整理在对话的上下文中传递给大模型服务器。 长期记忆,则用于对AI Agent要用的信息长时间保存并能够实现高效率的检索,最典型的就是能够支持外部知识库,把Agent运行任务需要的外部知识以向量化的方式保存在外部数据库中,并且能够支持高效的检索。 工具 由于大模型一旦完成预训练,其内部能力和知识边界就基本固定下来,而且难以拓展。此外作为基于文本和自然语言的处理和生成工具,大语言模型不太适合一些工作(例如复杂浮点数字的计算),因此就需要一些外部工具来扩展Agent的能力,使其能够执行这些它很难胜任的任务。 AI Agent系统可调用的外部工具,典型的包括网络搜索、日历时间、计算器、本地代码执行能力(Function Calling)等。 执行 AI Agent需要具备调用工具执行任务的能力,与外界互动,通过调用工具的方式来完成一个动作或者子任务。 从上图可以看到,AI Agent如何规划和决策制定过程,如何调用工具来执行行动的完整工作流程是其应用的核心所在,这部分工作逻辑就称为AI Agent的推理引擎或者认知框架。推理引擎决定了Agent如何从感知的环境中抽取信息,如何规划未来的任务,如何利用过去的经验,以及如何调用工具。 研究人员提出了多种智能Agent的推理逻辑,如CoT、ToT、LLM+P等。其中,ReAct框架脱颖而出。 AI Agent的React框架 本质上讲,AI Agent的React框架就是一个思考-行动-观察这个完整序列所组成的循环。 - Thought:基于用户任务的总体目标、当前的状态以及可用的工具集,思考下一步的行动计划,确定下来行动方案。 - Action:基于思考的结果,决定具体要采取的行动并执行。 - Observation:收集并观察前一步的执行结果,对结果进行评估,决定下一轮次的思考方向。 !image.png 以上的流程从概念上理解非常简单,但是应该如何与现实中的AI应用的具体实现来相关联呢?以下通过一个基于LangChain开发框架的示例代码进行说明。 在下面的例子中,使用LangChain框架开发一个简单的Agent。这个Agent的工作流程非常简单,大模型首先对用户输入的问题进行分析,如果需要执行浮点数加减乘除的运算时,就调用本地实现的浮点数运算的代码,然后把结果返回给大模型,由大模型来组织语言输出结果。 其实这个功能完全可以基于大模型的API来使用function calling的方式来实现。但是,通过这个例子的打印信息,可以看到大模型进行思考、决策、调用工具并返回结果的过程,其他更复杂的Agent应用实际上也是类似的工作流程和逻辑。 以下例子自测通过,使用月之暗面的Kimi作为LLM服务器,关于LLM模型的API Key设置以及通过API调用可参考[[基于Python快速实现与Kimi的AI对话]]。 以上程序运行的结果是: 以上通过终端打印的信息很值得研究: - 首先就是,在用户问题提出的连续计算的需求,被明确的分成了两步,首先计算加法,然后再计算乘法。每一步的计算逻辑都是:模型给出这一步的计算式,本地代码运行这个计算式,把结果以字符串形式返回模型,模型基于中间结果再执行下一个计算式,直到最终计算完成后输出结果。 - 大模型竟然把每一步的计算式都已经用字符串的形式整理好了,本地代码拿到这个计算式以后只需要使用eval进行计算就可以了,这确实太方便了。 参考资料 - 《大模型应用开发:动手做AI Agent》

非专业人士的AI课4:一文说清楚RAG的工作流程以及与微调的区别

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而在我们的生活和工作中更好的使用AI。 RAG RAG(Retrieval Augmented Generation,检索增强生成),是一种结合了大型预训练语言模型与知识检索技术的新型生成模型,为应对大模型应用开发中的诸多挑战提供了新的思路。 生成式大模型存在的问题 大模型并不具备在环境不断变化的场景中回答特定问题所需的全面知识。例如,早期的ChatGPT 3.5版本的预训练语料库时间截至2021年,这意味着模型无法准确输出2021年以后的事实性问题,这正是现代大模型所面临的知识更新困境。 实际上,对于一个大模型来说,更新基础模型知识库是非常困难的一件事情。首先,需要保证预训练数据的质量;其次,更新知识库后的模型通常都需要重新训练,至少要将新数据与旧数据按照一定的比例进行混合训练,而不能仅仅使用新数据,否则会出现灾难性遗忘的问题。总之,大模型的知识更新问题将成为模型的一个重要痛点,并且是一个值得研究的课题。 RAG则可以很好地解决以上问题。该框架可以使模型访问超出其训练数据范围之外的信息,使得模 型在每次生成时可以利用检索提供的外部更专业、更准确的知识,从而更好地回答用户问题。 RAG的工作流程 RAG的完整工作流程主要涉及3个阶段:数据准备、数据召回和答案生成。输出质量的高低主要取决于数据质量和检索策略。 - 数据准备阶段包括识别数据源、从数据源提取数据、清洗数据并将其存储在数据库中。 - 数据召回阶段包括根据用户输入的查询条件从数据库中检索相关数据。 - 答案生成阶段则是利用检索到的数据和用户输入的查询条件生成输出结果。 下图是一个使用LLM大语言模型和本地知识库数据库(问题相关的知识和信息已经保存在本地知识库中)配合来实现的RAG系统的工作流程。 !image.png - 1.用户通过提示词输入查询RAG系统的问题。 - 2.系统对用户提出的问题进行分解,首先调用搜索引擎从本地知识库数据库中检索与查询问题相关的知识片段。 - 3-4,把查询到的知识片段作为查询的上下文放到用户输入提示词中。 - 最后,LLM对用户输入的查询问题以及上下文信息进行解析,生成文本答案。 > 相比于大模型的直接推理输出,RAG系统还有一个非常突出的优势,就是其输出信息的可溯源性。众所周知,在大模型领域中,模型基本都基于Transformer的解码器结构,因此大模型的生成结果存在不可解释性的问题。而RAG系统配合大语言模型的输出结果,不仅给出了答案,还提供了信息溯源的引用链接。因此,相较于纯粹的大模型生成,RAG具有更强的可解释性。 实际上现在我们使用的大模型普遍都提供了联网搜索的功能,而这个联网搜索的功能本质上就是RAG的实现。在回答问题之前首先配合搜索引擎查询有参考价值的网页,对网页内容进行检索处理,然后把检索到的相关内容放在大模型对话的上下文中,最终由大模型返回结果。 以下是使用deepseek不打开联网搜索功能的情况下,查询2024年美国大选的结果,因此deepseek的当前模型训练数据中只包含2023年10月份之前的信息,所以无法给出准确的答案。 !image.png 以下开启了联网搜索(相当于打开RAG功能),可以通过联网查到最新的信息,并且还给出了信息溯源的链接。 !image.png RAG与微调的对比 模型微调是指在一个特定任务上用一定量的数据来训练模型的过程,通过在特定数据集上微调可以提高模型在特定数据集上的性能。如果有足够大的监督数据集,且数据集不会发生改变,那么微调是一个很不错的方法。然而,如果数据集是动态的,就需要不断地重新训练模型以跟上数据的变化,这种情况下使用微调的方案,成本就会很高,而且系统的灵活性很差。 !image.png 总的来说,RAG位于大模型应用难易链条的中部,它既不像重新训练或从头构建模型或者微调模型那么难,也不像通过简单提示工程直接向大模型提问那么容易。它克服了直接微调模型的3个缺点——成本高、信息更新困难以及缺乏可观察性。相较而言,由于RAG无须训练,因此成本较低;由于数据是实时获取的,因此始终是最新的;由于可以显示检索到的文档,因此结果更加可信。

非专业人士的AI课3:LLM大语言模型项目的典型开发流程详解

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而在我们的生活和工作中更好的使用AI。 本文通过对参考资料的学习,对LLM大语言模型开发流程中的各个环节进行总结和详细的解释,建立对LLM大语言模型开发的初步知识结构。 总体而言,在商业领域中进行的大模型开发而言,其主要的开发流程包括以下几个方面: - 项目需求分析,确定项目的目标 - 基于项目目标进行前期的数据准备 - 基于项目目标进行模型的设计和选型 - 使用数据对模型进行训练 - 部署模型到应用环境中,提供项目需求预定义的服务 - 模型在实际应用中的运维 1. 确定项目目标 在每个与大模型相关的项目立项开发初期,首先要做的事情就是要明确项目的目标,并构建项目的系统框架。因为这涉及到后续工作流程中,针对性的进行模型架构、算法以及训练数据集等的选型。 项目的需求分析和目标制定,是大模型项目开发的起点,为后续的数据准备、模型设计\选型、模型训练提供了明确的方向和约束条件。因此在大模型项目立项之前,应该首先明确该项目需要面对的知识领域和具体要解决的问题,只有项目目标明确确定的情况下,才能选择出来合适的模型和训练数据,进而设计出高效并且成本、效益符合项目需求最大化的系统框架。 下图就是参考资料1中提供的一个财务分析模型的系统设计: !image.png - 针对该系统的功能和性能需求,从开源模型中选择规模和成本合适的模型作为系统设计的基础。 - 以网上收集的公开财报等相关数据为基础,将大模型作为数据集构造器,通过让其学习这些少量标注数据的内容,来自动扩充数据集,为该项目后续的训练阶段提供更多的训练样本。 2. 模型训练数据准备 该阶段的工作实际上包含两个方面,数据收集和数据预处理。 按照项目系统设计的训练所需,开发者可以从网络、公开数据集、用户生成数据、公司内部数据等途径获取训练数据。对于LLM大语言模型的训练而言,数据类型涵盖图片、文字、音频等。开发者可重点考虑开源的数据集,以及针对当前模型要解决的垂直领域问题相关的网络公开数据集。 数据预处理的环节,则需要针对文本、图像等不同的数据进行分别处理,其目的都是构建高质量数据集: - 针对文本数据,常用的数据处理方法一般包含低质过滤(判断和识别低质量的数据并去除)、冗余去除(从不同粒度的角度出发去掉数据中的冗余元素)、隐私消除(识别和去除数据中包含的个人/组织隐私和敏感信息)等。 - 针对图像数据,一般采用图像去噪(降噪)、图像重采样(分辨率缩放)和图像增强(提升对比度、亮度等)等技术,以提升图像数据质量。 分词与嵌入 数据处理完成后,下一步就是要进行分词,也就是Tokenization。所谓的分词就是把文本分割成离散的、有意义的最小语言单元的过程,而这个最小的语言单元就是Token,从训练的文本变成Token的过程使用Tokenizer完成。 - 在平时我们使用LLM大语言模型的过程中,可以清楚的看到模型的输出以一个字或者一个词的节奏按顺序吐出来,这个字或者词就是一个Token。而当我们调用API对LLM大模型进行访问的时候,费用也是基于与大模型交互的Token数量(包括用户向模型提问问题的上行Token数量和模型输出答案的下行Token数量)来进行收费的。 - 为了方便开发者对文本进行分词处理,许多主流的开发框架将分词功能封装成Tokenizer组件或者类,供开发者调用。Tokenizer可以简化开发流程,提高代码复用性,并且确保整个过程中的一致性。因此可以从HuggingFace上下载开源的分词器进行定制化的修改。 !image.png 经过以上的分词器对训练文本所执行的分词操作后,文本语句就会被分割为一个个最小的语言单元。但是这个语言单元仍然无法被大模型识别,此时还需要对每个语言单元执行一个向量化的嵌入操作(Embedding)。而嵌入操作,就是把词汇映射成数值形式的向量的过程。如此一来,模型就能够理解和处理这些向量,并基于这些数值化的特征进行计算和分析,从而实现对自然语言的深入处理和理解。 - 分词和嵌入的这两个概念在基于大模型的RAG应用中,也需要用到,同样是把外部文件或者网络上搜索到的文本文件的内容,执行分词操作(Tokenization)以及嵌入操作(Embedding)后送入大模型进行后续处理。 !image.png 3. 模型的设计和选型 基本上,所有LLM大语言模型的基础都是Transformer架构,而多模态模型中针对图像模块的处理一般则采用Visual Transformer架构。总的来说,大模型的开发、设计和优化调试是整个系统设计与实现过程中技术难度最高的部分。因此: - 从现实的角度来讲,小型开发团队或个人开发者在完成大多数的人工智能业务实现时,已经无须从头构建模型,而是可以根据任务所需选择开源大模型,如Deepseek、Qwen、LLaMA、ChatGLM、Alpaca等。 这种方式可以节省大量的模型设计时间,提升开发效率。 - 而对于有较多资源和能力的团队而言,考虑自行从头开发模型,在模型构建之前,一般需要首先选择合适的深度学习框架,目前主流的深度学习框架有TensorFlow、PyTorch等。其中TensorFlow是由谷歌团队研发的开源架构,PyTorch是Meta团队研发的开源框架,而国内主流的开源框架有百度的飞桨PaddlePaddle等。然后定义模型结构,包括层次结构、参数初始化、定义损失函数、选择优化算法等。 4. 模型的训练 大模型的训练过程一般包含分词器训练、预训练和微调3个基本步骤。 预训练Pre Training: - 基于大型语料库对模型进行训练,目的是让大模型能够掌握基本的自然语言理解和生成能力,以及掌握丰富、广泛的知识,为后续的任务执行打好基础。 - 在预训练阶段,从低成本渠道中获取大量无标注数据,并且把这些数据送入模型,由模型对这些数据的共性知识进行学习,形成一种对于语言的通用理解能力。 微调Fine Tuning: - 微调是一种在预训练语言模型的基础上使用目标任务数据进行训练的模型训练技术。 - 在微调阶段,则是针对模型应用的知识领域和方向,使用少量相关领域的标注数据(一般成本比较高)在预训练阶段的Base Model基础上进行进一步的训练,使其适应目标领域和特定任务的应用需求。 > 当前,无论是在计算机视觉还是NLP领域,广泛采用了“预训练+微调”的开发范式。而一般情况下,尤其是对于中小团队而言,开发者不需要自行对模型进行完整的预训练,而是可以选择合适的开源预训练模型,然后根据具体任务的需求进行微调即可。 !image.png 此外,如果需要对模型的输出进行无害化处理(使大模型输出的内容不会包含有害、不当或者不符合伦理道德的内容),还需要借助RLHF、RLAIF等技术使模型与人类的价值观对齐。 - RLHF,Reinforcement Learning from Human Feedback,基于人类反馈的强化学习。具体工作步骤是,从prompt数据库中采样一部分数据,让模型同时生成多个答案;然后由人类标注员对生成的多个答案进行打分或者排序,然后再使用强化学习算法对模型进行微调。整个过程相当于使用模型生成多个答案的打分或者排序信息,使模型能够更好的与人类的价值观和意图对齐,生成更自然、更符合人类期望的输出。 - RLAIF,Reinforcement Learning from AI Feedback,即基于AI反馈的强化学习。RLAIF是RLHF的一种替代方法,目标是使用AI模型所生产的反馈来代替人类标注员所给出的反馈,从而降低在模型训练中对人工标注的依赖。其工作流程主要是借助于另外一个更为强大的LLM模型,对当前正在训练的模型的输出答案进行评估和打分(在这个环节替代了RLHF的人力工作),然后通过强化学习算法对目标模型进行微调。 5. 模型的部署 模型部署是指将大模型部署到实际应用场景的过程。但是,模型部署并非只是简单的把已经训练好的模型部署在服务器上就好。 在模型部署中,一般需要基于前期训练好的模型进行模型量化、知识蒸馏、模型剪枝等操作,以实现模型压缩,最大限度地减少模型依赖的硬件资源。 6. 模型的应用和运维 最后一步,就是把训练好的大模型服务向目标用户开启。那么针对大模型的使用和大模型服务的运维,就需要提供易用、美观的前端工具。此外,还可以利用LangChain这类大模型开发框架,基于已训练和部署的大模型,开发更加多样化的大模型应用程序以及以训练和部署好的LLM大模型为中枢决策和推理模块,构建更为灵活和强大的AI Agent应用。 参考资料 - 《大模型导论》,张成文 - 一文说清楚什么是预训练(Pre-Training)、微调(Fine-Tuning)

非专业人士的AI课2:对大模型多模态以及开源闭源概念的总结

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而在我们的生活和工作中更好的使用AI。 究竟什么是大语言模型LLM? 目前我们所说的大模型,全称是大语言模型LLM(Large Language Model)。所谓的”大”是指这个模型所包含的参数量非常大,目前市场上主流的LLM模型的参数量都达到了千亿以上的级别。而所谓的“语言”是指自然语言(Natural Language),也就是这个模型可以直接使用自然语言(而不是只有计算机和程序员才懂的程序代码)与人进行交互,用户对这个模型的使用需求,以自然语言的方式输入模型,而模型也以自然语言的方式给出响应和答复。 用户输入自然语言文本信息到模型中,模型根据用户的问题生成用户能够理解的自然语言文本的内容,这种模型就是大语言模型的单模态模型。 最典型的LLM单模态大语言模型就是引爆OpenAI的GPT-3.5模型,参数量高达1750亿。如此大的参数量,意味着可以提供卓越的性能、强大的学习能力,但是同时也意味着对计算和存储资源提出更高的要求,训练和推理的成本也更高。 单模态与多模态 目前大模型的发展趋势,基本上比较流行的大模型都是多模态模型。所谓的多模态,就是模型的输入输出,不仅能够支持GPT3.5这样的单模态模型所能够支持的自然语言文本信息,也能够支持图像、音频甚至视频方面的内容,例如以文生图、视频,以及根据图像生成描述性文本等。 !image.png OpenAI目前的多款在市场上广受欢迎的多模态大模型,包括可识别图像的多模态LLM大模型GPT-4及其以上的版本,文生图模型DALLE3,文生视频大模型Sora,语言转文字模型Whisper等。而国内阿里的通义千问,腾讯的混元,百度的文心一言,字节跳动的豆包等大模型基本也都支持了对图像、音频等的多模态支持。 Yolo 实际上除了LLM大语言模型以外,还有一些专业性更强、专注于解决某一方向问题的其他AI模型。例如用于在视频图像中进行目标检测(人,车辆,宠物等)的Yolo模型,这类模型大概有上千万参数,注重轻量化和实时性,适合边缘计算场景,可以对输入图像进行检测生成目标框和类别预测,当前约78%的自动驾驶企业和65%的智能摄像头厂商选择YOLO作为主力检测框架,但其具体版本需根据硬件性能调整。例如车载系统多采用YOLOv8m(平衡精度与速度),而门禁摄像头则倾向YOLOv5s(更低功耗)。 大模型的开源和闭源 大模型选择开源或者闭源的商业经营模式,决定了这个模型的代码、模型权重、训练数据、训练过程等核心资源是否完全公开,允许任何人查看、修改和分发(如Apache 2.0等传统开源许可证)。 例如,DeepSeek-R1模型采用Apache 2.0协议,开发者可基于其代码自由开发商业应用。这种开放性促进了全球开发者的协作创新,形成“社区驱动”的生态模式。 下表是目前主流大模型的开源和闭源状态列表: !image.png 可以看到,Meta的Llama,阿里的Qwen,还有最近爆火的deepseek都选择了开源的商业模式。而实际上,自从deepseek爆火,国内的腾讯、Kimi等一种大模型厂商也选择了对其开发的大模型进行开源。那么为什么这些厂商选择了开源的商业模式呢? 大模型开源模式的优势 - 大模型开源之后带来的最大好处就是可以快速的建立自己的技术生态,吸引更多中小型的开发者和企业快速的参与到开源模型的改进和优化中。例如,Meta的Llama系列开源后,迅速衍生出多个垂直领域的模型,加速了技术的迭代。 - 开源生态的成熟也可以快速推动上下游厂商之间的合作与适配,例如DeepSeek开源后,迅速得到了云厂商、社区和芯片厂商的跟进。 - 开源模式能够吸引更多用户和开发者,从而提升品牌知名度和行业影响力。例如,DeepSeek的开源举措不仅推动了技术的普及,还重塑了大模型市场的竞争格局。对比技术竞争力相差不大但是采用闭源模式的Kimi而言,尽管Kimi 2024年在市场推广中投入了超过9亿的营销费用,但是推销效果远远比不上deepseek开源模式推广的影响力。 - 开源模式的商业盈利方式更加多元化。开源并不意味着完全免费,厂商可以通过提供增值服务(如企业级解决方案、定制化服务、API接口等)来盈利,还能够通过数据和流量变现,例如通过广告、用户数据分析等方式实现商业化。 - 当然,大模型开源的同时,也会采用专利等方式对自己的核心技术进行保护。例如,DeepSeek在多个关键领域提交了专利申请,涵盖大模型训练优化、系统稳定性优化、网络与硬件优化等。这种“开源+专利”的双轨策略,既保护了核心技术,又避免了被竞争对手抢注或侵权。 大模型以开源的方式发布以后: - 竞争对手和有大模型开发能力的团队,可以研究大模型的相关代码,参与到对这个模型的改进和优化,以及针对某些垂直领域进行定制化的开发和训练。 - 云服务厂商可以把训练好的大模型部署在自己的云服务器中,通过API等方式为用户提供服务来谋求商业利益。 - 中小型团队和个人开发者可以基于开源大模型进行私有部署,并针对当前行业和机构的需求进行定制化训练和微调。 Hugging Face 对于开源大模型,就必须要提到在AI大模型开发领域大名鼎鼎的Hugging Face网站(www.huggingface.co)。Hugging Face类似于AI模型领域的Github,该网站上维护了全世界几乎所有的开源大模型的代码,以及开源的模型训练数据。截至2025年3月份为止,在Hugging Face上已经上传了接近150万个大模型,超过30万个开源的训练数据集。所以,这就为小型团队和个人开发者自行部署和训练开源大模型提供了极大的便利。 !image.png 实际上对于绝大多数小型开发团队或个人开发者而言,如果要参与大模型开发的话,在完成大多数的人工智能业务时无须从头构建模型,比较现实的做法,是根据自己要解决的任务类型,按需选择开源大模型,如Deepseek、通义QWen、LLaMA、ChatGLM、Alpaca等。从Hugging Face这样的网站上下载对应的开源大模型、训练数据集以及其他训练和部署的工作,基于这个基础进行定制化行业应用方面的开发和训练。这种方式可以节省大量的模型设计时间,提升开发效率。

非专业人士的AI课1:深度神经网络学习入门

本系列文章的目标是从非AI从业者的角度出发,去尝试理解AI及其大模型在应用中的基本概念,争取通过对这些概念的深入理解和澄清,建立对大模型的工作机制流程以及应用框架方面的知识结构,从而能够帮助我们在生活和工作中更好的使用AI。 深度神经网络是一切AI模型的基础 目前几乎所有比较流行的AI应用及其模型,都是基于深度神经网络拓扑结构的。如下图所示,深度神经网络网络在层次上可以分为三层:输入层,非常多的中间隐藏层(这就是称之为深度网络的原因),以及最后的输出层。 !image.png 神经网络中的每一个节点都是一个神经元。对于各个神经元的计算而言,大体上可以分为以下两个步骤: - Step 1:从前一层的所有节点按照不同的加权权重(Weight,如下图中的多个w参数)进行乘加运算,再把乘加计算的结果加上不同的偏移量(bias,如下图中的b参数),得到第一步的计算结果。 - Step 2:以第一步的计算结果作为输入,进行激活函数(即下图的f函数)的运算。目前的神经网络多种比较常用的激活函数,其输出范围取决于具体使用的激活函数类型。不同的激活函数有不同的输出范围,大部分激活函数的输出范围是(-1,1)或者(0,1)之间的连续浮点数据。 !image.png 深度神经网络的参数量 在深度神经网络中,我们所说的参数量通常指的是神经元之间连接的权重(Weights)和偏置(Biases)的总数量,而不是神经元的个数(也就是上图中的w参数和b参数)。 以下给出一个参数量计算量的简单例子。假设有一个简单的全连接神经网络,其结构为:输入层有 n 个神经元,隐藏层(单层)有 m 个神经元,输出层有 k 个神经元。 输入层到隐藏层: - 权重数量:n×m(每个输入神经元连接到每个隐藏神经元) - 偏置数量:m(每个隐藏神经元有一个偏置) - 总参数量:n×m+m 隐藏层到输出层: - 权重数量:m×k(每个隐藏神经元连接到每个输出神经元) - 偏置数量:k(每个输出神经元有一个偏置) - 总参数量:m×k+k 因此,整个网络的总参数量为: 总参数量=(n×m+m)+(m×k+k)。 !image.png 无论是ChatGPT这类大语言模型,还是Yolo这样的对图像中的目标进行检测和分类的模型,其实现从本质上讲都是以上所描述的深度神经网络的结构。而从以上深度神经网络的架构来讲,无论是开发中的训练还是部署后的推理,都是从input层输入数据,在中间的隐藏层进行计算和传递,最终在output层输出,其中主要涉及到的运算就是在各层的神经元之间进行的海量乘加运算,因此模型越大,层次越多,参数越多,那么要进行一次计算的计算量都成指数增加,这就是大多数大模型的训练以及推理对于算力要求很高的原因所在。 - 可以认为,LLM大语言模型所学习到的海量知识就保存在各层神经元的参数之中。

以代码案例来详细介绍Function Calling

什么是Function Calling? 众所周知,大语言模型本身只是一个文本生成的工具,虽然有很强的文本生成能力和逻辑推理能力,但是LLM并不具备直接去执行外部功能操作的能力。因此,如果我们要把大模型用于实际产品的应用场景之中,只能利用大模型来生成文本信息的话,那么整个系统真正能够解决问题的能力就会大打折扣。 如果能够将大语言模型所具备的自然语言处理能力,与外部工具/API调用功能相结合的话,就能够显著增强大模型在实际应用场景中解决问题的能力。大模型所具备的function calling就提供了这样一种机制,能够把大模型处理文本的能力,与本地通过function调用的能力相结合,从而为大模型的实际运用提供了极大的拓展空间。 > 大语言模型function calling的工作逻辑,就是在以大语言模型为中心的人工智能系统中,利用大语言模型所具备的推理能力,在系统的运行过程中根据大模型的响应信息,自动调用本地/外部工具或API,从而达到扩展人工智能系统能力的目的。 Function Calling的工作流程解析 以下借助于参考资料1中的一个流程图来对大模型的function calling的执行流程进行说明。 在该流程案例中,当用户向大模型查询”北京今日气温“相关的信息时,因为涉及到对实时信息的查询,因此LLM本身是不可能直接输出正确结果的。此时就需要借助于类似function calling这样的外部工具。 在这种情况下,function calling机制要能够工作,在用户查询这个实时天气信息相关的问题时,大模型需要先知道当遇到这类问题的时候,应该调用的外部函数名称和参数列表是什么。这个信息(也就是给大模型提供的外部工具集Tools参数)与问题一起传递给大模型。 大模型对用户问题(及Tools中包含的外部函数列表中包含的外部函数描述信息)解析时,判断该问题是否需要调用Tools参数中所传递的外部函数。当判断该问题需要调用外部函数时,大模型会在返回的文本信息中包含该外部函数的调用参数列表。如下图的第二步所示。 用户应用程序收到大模型给出的答复后,在本地按照大模型返回的参数列表调用指定的外部函数,得到执行结果,并把执行结果包含在Json结构中,重新返回给大模型。如下图的第三步和第四步所示。 大模型收到外部函数的执行结果后,对其进行数据格式转换,并按照用户问题的语义信息,结合外部函数返回的执行结果信息,重新生成最终的文本答复给用户。如下图的第五步和第六步所示。 !image.png Function Calling的核心工作机制总结 以上所描述的Function Calling完整流程的正常运行,依赖于外部函数能力描述、大模型语义解析以及外部函数调用流程三个方面的支持。 外部函数能力描述 如果需要大模型在应用中能够针对特定的问题,自动生成对外部函数的调用信息,就需要首先向大模型传递外部函数的描述信息。因此,开发者需要预先定义好可供大模型应用在执行过程中可调用的函数清单,其中包含每个函数的API描述、参数规范(类型/格式说明)和返回结构等信息。这样,在大模型遇到相关问题的时候,才能够应该调用哪个外部函数,以及调用这个外部函数应该使用的参数列表。 以下是一个外部函数的描述信息,在向大模型发出问题请求时,在上下文中同时提供给大模型。该外部函数可以用于计算两个浮点数的乘积,调用时应该以字符串的形式提供两个浮点数参数。 大模型语义解析 在这个环节,大模型应该对用户输入的问题及其上下文中包含的外部工具函数集的信息进行解析: - 针对该问题,是直接按照传统LLM的流程来进行处理,还是需要调用问题上下文中中包含的外部函数来辅助? - 针对该问题,如果需要调用外部函数,对用户问题和外部函数集中的函数描述信息进行解析,找到对应于该问题的外部函数。 - 针对该问题,如果需要并且找到解决该问题的外部函数,需要进一步分析语义,并且按照该外部函数描述中的参数信息,整理出来调用该函数的参数列表。 以上信息完整获取后,大模型在其应答信息中把该信息返回给应用程序,下一步要由用户应用程序在本地来执行大模型所指定的外部函数。 外部函数调用流程 这部分就是大模型与本地应用程序配合,能够支持本地应用程序在收到大模型给出的答复信息后,从答复信息中得到要执行的外部函数及其参数列表信息,在本地/通过API执行这个外部功能函数,并把执行结果以及前一步骤完整过程的上下文(再强调一下,LLM没有记忆和状态,每次访问LLM都需要把完整的过程上下文一起上传,方便LLM对该过程有清楚完整的理解)发给大模型,由大模型生成最终的答案并返回。 Function Calling的例子 > Talk is stupid, show me the CODE。 以下以一个Python的代码例子来对大语言模型调用function calling的功能进行说明。该例子的大致运行流程如下。即对于传统LLM可支持的问题仍然通过传统的LLM工作流程来进行处理,但是对于LLM不支持且在function call tools已声明的应用中(此处的例子为查询当日天气、计算复杂浮点数相乘)调用function call功能在本地进行处理并把结果返回大模型,最终统一由大模型来输出答案。 - 蓝色部分模块由应用程序执行,红色部分模块由大模型执行。LLM-function-call-workflow.png 以上代码执行的打印信息为: 可以看到,从该程序的运行流程来看完全符合设计预期: - 当用户输入的问题是“陕西省的省会是哪个城市?”,这个问题是在大语言模型的知识范围和能力之内,因此大语言模型直接给出答案,不需要调用function calling功能。 - 当用户输入的问题是“北京今天天气怎么样?”,这个对于实时问题的查询明显超出了大模型的能力范围,再加上我们在大模型调用时提供了get_weather接口的描述信息,要求大模型在查询天气时,在响应的文本消息中返回调用该函数的参数列表。应用程序收到以后,自动调用get_weather函数并传递该参数列表,得到当前的天气状态,并提供给大语言模型生成最终的答案。 - 同样的道理,当当用户输入的问题是“3.567乘以25.345等于多少“这样的复杂浮点数乘法的计算,这也是明显超出大模型能力的应用,此时按照相同的逻辑,由大模型返回要调用的本次函数名称get_multiply_result和计算参数列表,自动调用本地的get_multiply_result函数得到计算结果以后,再返回给大语言模型生成最终的答案。 参考资料 - 不再混淆了!一文揭秘MCP Server、Function Call与Agent的核心区别

大模型的MCP到底是什么

MCP简介及其核心作用 MCP:Model Context Protocol,即模型上下文协议,是由Anthropic公司(也就是Claude大模型的开发商)在2024年11月份所开源的标准通信协议,其目的是为大语言模型(LLM)与外部服务之间的通信提供一个标准化的接口协议,实现与外部数据源、工具等的无缝连接,从而扩展大模型的能力。 从本质上讲,MCP是一种在智能体Agent和大模型应用程序开发过程中大家共同约定的一种技术规范和协议。它所解决的最大痛点问题,就是AI Agent及其应用开发中调用外部工具的技术门槛过高、碎片化严重的问题。 !image.png MCP通信协议的核心作用可以概括为: - 统一大模型与外部服务之间的通信接口和交互标准。通过这种类似于通用插座的协议设计模式,解决了大模型与不同外部服务(如 GitHub、Slack、本地数据库等)通信数据格式和交互规则不统一的问题。 - 有效的提升了开发效率。对于外部服务的开发者而言,只需要按照MCP规范开发一次MCP Server,所有兼容 MCP 的模型或者其他类型的客户端均可直接调用,无需重复适配。 - 可以优化大模型的工作性能。通过在交互流程中动态加载上下文数据,可以为模型提供更精准的上下文信息输入,从而提升回答的准确性和实用性。 MCP的客户端-服务器架构 MCP协议的实现是基于客户端-服务器架构的。 MCP Client MCP Client一般集成在用户交互英语程序(如基于大模型的应用程序、AI Agent、Chatbox等)之中,负责与MCP Server通信来整合Server所提供的能力和服务,驱动整个应用程序任务的执行。 MCP Client所包含的核心功能如下: - Client在初始化时,要向所有配置的MCP-Server发送能力查询请求,通过其响应消息收集可用的工具列表信息(包括工具名称、描述、参数等)。 - Client根据要执行的任务需求,动态调用合适的工具,并协调多工具之间的协作(例如调用天气查询服务API后结合其结果生成回复)。 - Client可以通过MCP通信协议支持对接本地以及远程的多种Server类型,无需修改代码即可扩展能力。 总之,MCP Client是MCP服务的主动消费方,根据整体应用程序的设计需求以及用户输入的数据信息,有效的对其注册的MCP服务组合进行调用,达成应用的设计目标。 MCP Server MCP Server则是被调用方,负责向外提供具体的能力服务或者数据接口。典型的MCP服务可以包括大模型服务(例如文本、图像生成等)、工具(本地文件、数据读取、爬虫、数据库查询)以及第三方的API(例如天气查询)访问。 MCP Server所包含的核心功能包括: - MCP Server应按照MCP协议所定义的标准化接口来实现,并向Client注册自身所具备的能力(如tool_name、description、参数规范)。MCP Client根据这些标准信息可以了解到MCP Server所能提供的服务,以及应该如何调用这个服务。 - 在运行过程中,MCP Server对外提供服务,处理来自Client的请求并返回结果(如执行Linux命令、查询数据库、调用大模型生成内容等)。 - 此外,作为服务的提供方而言,MCP Server还需要支持资源控制(如限制敏感数据访问)和安全性管理(如身份验证)。 总之,MCP Server是MCP服务的被动提供方,按照MCP协议标准的要求对外其他服务,Client既可以通过标准接口查询MCP Server所提供的服务,也同样可以通过标准接口来调用这些服务。 MCP的通信机制 为适应本地调用和远程调用MCP Server两者方式,MCP协议定义了两种通信机制:针对本地的stdio以及针对远程访问的HTTP/SSE通信。如下图所示: !image.png 针对远程访问的HTTP/SSE方式 SSE(Server Send Events)本身是针对HTTP协议支持长连接通信的一种扩展协议。使能SSE的情况下,HTTP客户端通过发送一个特殊的HTTP GET请求到服务器,请求中包含Accept: text/event-stream头,表明客户端期望接收SSE数据流。服务器响应后会保持连接打开,并可以持续向客户端推送数据。需要注意,SSE连接的情况下,只能由服务器向客户端单向推送数据,客户端不能使用这个链接向服务器发送数据。 当客户端通过HTTP SSE建立持久连接后,服务器就可在有新的数据的情况下,通过该连接向客户端持续推送数据流,实现从服务器到客户端的实时单向通信。而当客户端有新的请求时,仍然需要通过独立的HTTP Post消息向服务器发出请求,服务器则通过之前建立的HTTP SSE长连接向客户端持续返回流式响应。 客户端和服务器之间的所有请求和响应数据均采用JSON-RPC 2.0规范,以确保结构化数据传输。 针对本地访问的stdio方式 针对MCP Sever和Client之间的本地访问需求,MCP则定义了一种stdio(标准输入输出)的本地进程间通信的传输方式。 在这种模式下,MCP客户端在启动的过程中,直接在本地启动MCP服务器应用程序作为其子进程,此后两者之间使用操作系统的标准输入(stdin)和输出(stdout)管道来进行通信。通信流程采用同步交互的请求-响应机制,每条请求需等待对应响应后才返回,客户端通过stdin发送JSON-RPC 2.0格式请求,服务器端则通过stdout返回JSON-RPC 2.0格式的响应。 MCP的典型工作流程解释 对于标准的MCP Client和Server之间的交互流程而言,一般包含两个阶段:初始化及Server能力协商,服务请求与响应。 !image.png 初始化与Server能力协商 在MCP Client应用程序启动时,需要首先向其配置的所有MCP Server发送discover请求,获取其所具备的能力服务列表、描述信息、调用参数详情等。 MCP Server在收到Client的discovery请求后,返回结构化的工具描述信息,例如以下是一个天气查询服务的描述信息:{tool_name: "weather_query", description: "查询实时天气", parameters: {city: str}}。 MCP Client在收到以上信息以后,就能够在后续应用程序的运行过程中需要进行天气查询的时候,按照参数格式的要求调用这个MCP Server所提供的服务。 MCP服务请求与响应 MCP Client在应用程序的执行过程中,根据任务的执行需求来选择合适的工具(可选择的工具列表已经在前面的初始化阶段查询到),然后向MCP Server发送包含访问参数的JSON-RPC请求。例如{"method": "weather_query", "params": {"city": "Beijing"}}。 MCP Server收到Client的请求后,执行与调用参数对应的操作(例如调用OpenWeather API)并向Client返回结果(如温度、湿度数据)。 MCP Client收到Server的响应以后,将多个Server的返回结果整合,最后再传递给大模型以生成最终的响应文本消息(如生成包含天气建议的自然语言回复)。 MCP与function calling的区别 首先明确一点,MCP只是一个通信协议和流程的框架,在MCP应用中并不一定必须要存在AI大模型,用户完全可以开发一个独立的MCP客户端和MCP服务器端,然后在客户端和服务器端基于MCP协议进行通信交互,实现特定的功能。只不过,在AI大模型能够支持MCP协议的情况下,在MCP应用中引入大模型的参与,可以显著提升应用的智能化水平和用户体验。 其次,在MCP应用中,大模型通常扮演MCP Client的角色。具体来说: - MCP Client:大模型作为客户端,可直接通过MCP协议向预先定义MCP Server发起请求,调用外部工具或数据源。这个过程中,大模型负责解析用户的需求,决定需要调用哪些工具,并通过MCP协议与提供MCP服务的服务器进行交互。 - MCP Server:基于MCP协议对外提供外部工具和数据源访问的服务器应用,它将各种资源和服务封装成标准化的接口,供大模型(MCP Client)调用。 从MCP实现的功能上讲,它与function calling非常类似,但是两者有着本质上的区别: - 外部任务的执行主体不同:对于MCP应用而言,外部任务的执行由大模型服务器自行基于MCP协议访问对应的MCP Server,得到答案以后以自然语言返回给用户,而function calling是用户以自然语言提出的问题,大模型解析以后结构化数据的形式返回给用户的应用程序,由用户应用程序在本地执行function功能,因此用户应用程序需要自己处理外部函数的执行和结果的返回。 - 协议和交互的规范性:MCP定义了一个标准化的通信协议和工作流程,规范了MCP Client(大模型)和MCP Server(外部工具和服务)之间的交互逻辑,这使得不同模型和工具之间的交互更加统一和高效;而function calling的具体实现通常依赖于用户应用程序的自行定义,不同的应用程序可能会有不同的调用方式和接口设计,很难在不同的应用之间复用。 参考资料 - 实操干货!MCP全解析,一步步教你借助第三方MCP Server开发Agent - 从0手撕代码搭建MCP Client与Server!详解DeepSeek、ollama、vLLM接入MCP实战!_java_赋范大模型技术社区-DeepSeek技术社区

基于多模态大模型的HTTP API实现图像识别的功能(Python+C)

在之前的笔记中,通过在Python中利用OpenAI SDK访问月之暗面MoonShot的Kimi API,实现了一个单词对话和多轮对话的功能:[[基于Python快速实现与Kimi的AI对话]]。 本篇笔记在以上笔记的基础上,仍然利用OpenAI的API,访问Kimi多模态模型实现图片中对象识别的功能,同时使用Python和C代码实现并提供案例解析。能够利用C语言来访问多模态大模型开放出来的HTTP API接口,也就能够在低成本的MCU处理器上,基于合理的提示词设计,实现需要大模型服务支持才能提供出来的功能, 该例子的功能需求很简单,就是给大语言模型上传一张照片,请大模型给出照片中检测到的对象的描述。本案例使用在网上搜索到的一张小鸟的照片。 !image.png > 相比于与大模型之间进行的简单文本对话,要使用多模态大模型来实现图像识别的功能,最大的不同,在于客户端发给多模态大模型的HTTP请求,需要把图片文件进行base64编码后作为附件,与文本对话的提示词一起上传上去。 多模态大模型进行图像识别的Python实现 以下是基于Kimi多模态大模型进行图像识别的Python实现。需要注意的是: - 代码运行需要先在环境变量中设置MOONSHOT_API_KEY,把自己的API Key设置进去,具体可以参考[[基于Python快速实现与Kimi的AI对话]]。 - 要能够对上传的照片附件中所包含的对象进行识别,需要选择能够支持多模态的大模型,否则会提示模型不支持图像识别的报错信息。对于Kimi而言,此时应该设置的多模态大模型节点应该是moonshot-v1-8k-vision-preview。 可参考代码中的注释对于其运行流程进行详细的理解: 执行以上代码后,大模型返回的信息如下: 多模态大模型进行图像识别的C实现 以上利用Python和Kimi多模态大模型,基于HTTP API的方式可以很方便的实现图像识别的功能。但是大多数低成本的MCU和SOC上是无法运行Python的,所以如果要想在这类处理器上利用大模型来扩展自己的能力,就需要用C语言来实现相同的功能逻辑,这就是这里需要用C语言来重新实现以上功能的原因所在。 以下是使用Kimi多模态大模型进行图像识别功能的C语言代码实现: 可以看到,C语言的实现相对就要复杂很多,以上代码中自行实现了json的解析和base64编码,同时依赖libcurl来实现与Kimi Web API服务器之间的HTTP通信。因此,在编译时应该要链接libcurl库: 同样的,编译后生成的image-recognition要能够正常运行,也需要在Linux系统中设置MOONSHOT_API_KEY环境变量:

基于Python快速实现与Kimi的AI对话

准备工作 对于基于大语言模型的AI应用程序开发而言,用户的应用程序与Kimi或者OpenAI这类大语言模型之间基本上都是使用标准的HTTP接口进行请求,Kimi和OpenAI服务器针对用户应用程序提出的问题进行答复,答复的结果同样封装在HTTP的reponse message中传回给用户应用程序,用户应用程序对结果进行解析后呈现给用户,或者调用其他更加智能化的功能处理(例如AI Agent)。 在此选择国内大语言模型开发商月之暗面Moonshot(也就是Kimi)为例进行与LLM Server之间的AI应用程序交互逻辑的学习和总结。使用Kimi的原因在于: - 在deepseek之前,Kimi一直是我本人稳定使用的AI工具,其稳定性和输出结果的质量,相比于国内的其他大模型而言一直是名列前茅的。 - Moonshot为每个注册用户提供了15元的预充值费用,这个费用对于通过API进行测试开发而言,基本上是够用了。 对于开发基于Moonshot的AI应用程序而言,提前要做的准备工作主要包括两个部分: - 按照Moonshot的要求安装python及其Openai library。 - 申请访问Moonshot服务器的API Key,并且为安全起见以环境变量的方式配置到自己的开发机中。 安装Python及其openai library 在进行以下的调试和开发之前,首先要在本地开发环境中安装python和基于python的openai library: - Moonshot API的使用直接兼容了OpenAI的API和library,所以可以使用openai的python library,修改对接的LLM节点,与Moonshot的server进行通信交互。这样对于AI应用程序开发而言,就可以在很大程度上减少对接不同LLM Server的工作量,算是一个很取巧的设计。 - 与Moonshot Server的对接,要求python版本在3.7.1版本以上,openai library的版本在1.0.0以上。 访问python的官网下载自己系统对应的python:Download Python | Python.org,当前的最新版本是3.13.2。 Python安装以后,直接在系统的终端中输入以下命令安装openai library:pip install openai 以上完成后在终端中查询当前的python环境以及openai library版本是否符合以上要求: !image.png Moonshot API Key的申请与配置 访问Moonshot的用户中心进行API Key的申请:Moonshot AI - 开放平台。点击新建按钮申请API Key,API Key的名称按照自己的使用情况随意填写即可,项目选择默认的default即可,确认后即可生成API Key。 - 需要注意的是,因为API Key涉及到对Moonshot Server和API的访问所产生的费用,一旦泄露出去被滥用后会产生费用。所以为了保密和安全起见,生成的API Key只有在这个时候才能拷贝保存下来,所以务必在此时把API Key拷贝出来并保存在一个安全的地方。 ![[基于Python快速实现与Kimi的AI对话-Moonshot-Key.png]] 为了后续更加方便并且安全的使用,建议把以上API Key以环境变量的方式保存在一个名称为MOONSHOT_API_KEY的环境变量中。 单轮对话通信功能的实现 从通信和代码实现的逻辑上讲,对于单轮对话通信而言,AI应用程序客户端与Moonshot Server之间的通信流程非常简单,就是应用程序把需要向Moonshot Server查询的问题封装在一个HTTP Request消息中,Server收到后会把答复包含在一个HTTP Response消息中,回复给应用程序客户端。应用程序客户端对其消息进行解析后显示出来。无论是Request还是Response消息,都是封装在Json结构的文本内容。 代码具体的实现流程: - 基于OpenAI library创建一个连接Moonshot Server的client,在其中要指定正确的API Key和Server URL; - 把要向Server查询的问题文本字符串按照role+content的格式整理在json结构中; - 基于前面的client和整理好的查询问题json结构体,调用client.chat.completions.create发起与Moonshot Server之间的通信; - 应用程序对Moonshot Server的答复进行解析,正确的情况下显示出来。 以下代码的功能就是实现与Moonshot Server之间的一轮对话,查询当前日期以及截至当前日期中国的人口数量,同时在收到Moonshot Server答复以后,把这个答复信息保存在一个json文本文件中: 值得一提的是以上代码中用messages所包含的请求消息,在与Moonshot Server通信所发出的问题咨询中,默认包含了三种以下角色(role)。如果对prompt engineering有一点了解的话,可以更容易理解各个角色的含义。 - system role:这部分通过文本字符串描述信息定义清楚对于Server角色的定义,如果Server有一个清楚的身份和角色定义,就有可能给出更加准确、帮助性更强的答复。当然这部分是可选的。 - user role:这部分就是用户通过文本字符串形式提交给Server的问题。 - assistant role:这部分是Server针对用户所提出问题给出的答复,同样是文本字符串形式。 执行以上代码以后,在json中所保存的Server响应消息如下: 可以看到,现在测试使用的Moonshot server返回的时间信息是错误的,而且多次调用返回的时间还不一样,可见在应用代码中不能依赖于该服务返回对时间日期要求较高的响应信息。 多轮交互式对话通信功能的实现 以上实现了应用程序与Server之间进行单轮对话的功能。那么如果要实现多轮对话呢?众所周知,HTTP协议是无状态、无记忆的协议,所以服务器端既不会维护通信的连接状态,也不会缓存多轮对话的上下文,因此多轮对话与单轮对话最大的不同,就是要在客户端中自行管理和维护好通话的上下文信息,把问题背景相关的上下文信息全部汇总后发给Server进行问题查询。Server收到后对于多轮通话的内容进行学习和推理,从而给出针对最新问题的答复。 以下代码简单的演示了在多轮会话通信中,客户端如何汇总和整理多轮对话的信息,并且发送给Server进行最新问题的查询。 结语 - 本质上,基于大语言模型的AI应用程序的开发,就是使用HTTP协议与LLM Server之间进行的交互。因为HTTP协议本身是无状态无记忆的,所以每一轮HTTP通信,客户端也就是AI应用程序端都需要把历史会话信息完整的发送给LLM Server进行解析、理解并得到答案。所以,对于AI应用程序的开发而言,既可以采用以上基于Python的OpenAI SDK与LLM Server进行交互,也可以采用任意的HTTP library和框架(例如Kimi的开放平台网站上同样提供了基于Curl的HTTP通信交互的流程参考代码)来实现与LLM Server的通信和交互。 - 总的来说,因为大语言模型服务器使用HTTP协议对外释放API,那么对通过该协议来访问LLM Server的流程开发就非常简单,接下来就是要发挥想象力,根据自己产品面对的应用场景和问题,设计合理的提示词(Prompt Engineering),然后就可以利用LLM Server的强大文本推理和生成能力,实现可以解决问题的人工智能应用。 参考资料 - 基本信息 - Moonshot AI 开放平台