赞
踩
Transformer 最开始是应用于 NLP 的翻译任务,而后续的实践则证明了 Vision Transformer 也能够用于物体探测等 CV 任务。
本文基于 Vision Transformer,创建了一个不使用卷积、无预设框 anchor free 的物体探测器,下面是用该探测器在 COCO 2017 数据集上做探测任务的效果。
训练环境配置: Keras/TensorFlow 2.9,Python 3.10,WIN 10,Anaconda,Pycharm 以及 Jupyter Lab.
根据原作论文 《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》→ https://arxiv.org/abs/2010.11929,Vision Transformer 的架构如下图。
Vision Transformer 主要由 3 大部分组成:
包括 Vision Transformer 在内的各种 Transformer 模型,其最关键的特点,是使用了 Multi-Head Attention 机制。Multi-Head Attention 的主要作用,是把输入的每一个局部信息进行组合,得到整体信息。
而每一个 Multi-Head Attention,都包含了几个 self attention。
Transformer 论文中,attention 的表达式如下:
上式是一个很抽象的表达。不过因为 self attention 本身借用了数据库中的名词 query,key,value 进行类比,所以也可以从直观 intuition 的角度来帮助理解。
在将图片切割成多个小块 patches 之后, self attention 操作的 5 个要点如下:
关于 self attention 的详细解释,可以参看吴恩达讲解 Transformer 的几个视频:→ https://www.bilibili.com/video/BV1Co4y1279R?p=34
Multi-Head Attention 就是把多个 self attention 拼接 concatenate 起来,再乘以一个权重矩阵 WO 即可。Head 的数量,就是 self attention 的数量。
直观上来理解,Multi-Head Attention 可以表示对图片提出的多个问题,以及对不同问题的答案编码。这些问题可能是 “图片中是否有猫”,“图片的颜色是什么” 等等。
——————————————————————————————————————————————————————————————
在 2021 年 v2 版的 Vision Transformer 论文中,是把 ViT(Vision Transformer) 用于物体分类任务。而如果要把 ViT 用于物体探测任务,并且要设计成无预设框的物体探测器 anchor free object detector,则需要在 Vision Transformer 的基础上,对模型、标签以及损失函数等进行重新设计。
要得到一个可用的深度学习模型,需要经过 2 个阶段:
第 1 个阶段是设计模型阶段,该阶段的目标是使得模型具备过拟合 overfitting 的能力(这个阶段也常叫做 optimization)。
第 2 个阶段是大规模训练阶段,目标是提高模型的泛化能力 generalization,一般通过使用海量的数据进行训练,以及数据增强手段来实现。
下面主要介绍设计模型阶段的内容。为了快速展现模型的过拟合能力,只使用了 COCO 2017 数据集的 8 张图片,即训练集的第 8 张到第 15 张图片进行训练。
使用 Vision Transformer 作为模型主体。2 个需要改动的地方包括:
训练样本因为是图片,所以是 4D 张量。
而标签部分,则需要用 3D 张量。张量形状为 (batch_size, objects_quantity, 6)。其中 objects_quantity 表示需要探测的物体数量。
对于每一个物体,使用一个长度为 6 的向量来表示。第 0 位表示是否有物体,第 1 位表示类别,后面 4 位则用来表示物体框。
这里并没有使用常见的 one-hot 编码来表示类别。我的考虑是:当使用 one-hot 编码来表示 COCO 数据集的 80 个类别时,类别编码将会是一个稀疏矩阵 sparse tensor,其中大部分数值为 0,这并不是一个很高效的编码方式。因此我只使用了 1 个数值来表示类别。
如果使用 one-hot 编码,通常会配合使用多类别交叉熵 Categorical Crossentropy 来计算损失值。这里因为使用了 1 个数值来表示类别,则需要另外设计一个损失函数来计算分类损失。
模型的完整损失值可以表达为下式:
Ltotal = Ldata + Lregularization
其中 Ltotal 为总的损失值。Ldata 代表数据损失,即模型预测值与标签之间的差距导致损失。而 Lregularization 则代表权重的正则 regularization 损失(对权重进行 L1 或 L2 regularization 导致的损失)。
在 Ldata 部分,Vision Transformer 的损失函数包括 3 部分。这一点和 YOLOv4 系列模型的损失函数类似:
对于分类损失,常用的是多类别交叉熵 Categorical Crossentropy,但并不是一定要用 Categorical Crossentropy 来计算分类损失。
损失函数的作用,是 表达预测结果和标签之间的差距
。因此,只要设计的函数能够很好地表达这个差距,就能够用来做损失函数。下面的实践也证明,用其它函数来计算分类损失,同样可以有较好的效果。
这个分类损失函数,可以表达为:
classification_loss = [c * (xpred - xlabel)]exponent,其中 exponent = 2n (n ≥ 1),即一个偶数。c 是一个常数。
xpred 是预测结果,而 xlabel 则是标签。
上面式子要求 exponent 是一个偶数,是为了保证损失函数关于直线 x = xlabel 对称。
下面是用 plotly 画的几个分类损失函数曲线,其中 xlabel = 8,即标签的类别编号是 8。
当预测结果和标签的距离大于 0.5 时(| xpred - xlabel | > 0.5),应该把预测结果理解为另一个类别,认为此时预测发生了错误。设计的损失函数,应该使得此时的损失值很大。
反之,当预测结果和标签的距离小于 0.5 时(| xpred - xlabel | < 0.5),则认为此时预测正确。设计的损失函数,应该使得此时的损失值很小。
使用权重约束 weight constraint 代替 weight regularization(包括 L1 和 L2 regularization),就可以避免权重的正则损失 Lregularization。
之所以要避免权重的正则损失 Lregularization,是因为当权重的正则损失比较大时,它会对训练模型产生干扰。具体解释如下:
损失值的完整表达式是: Ltotal = Ldata + Lregularization
在反向传播的过程中,这两部分损失起的作用不同。Ldata 会使得模型的预测值越来越接近标签,即模型变得越来越准确。而 Lregularization 则会使得权重越来越接近 0(因为只要权重不为 0,就会有 Lregularization 损失。)
假设在训练的某一时刻,模型达到完美,AP 指标为 100%,Ldata = 0,此时权重也达到了最优化。但是因为 Lregularization 始终存在,会使得权重继续不断地发生变化。如果权重的正则损失 Lregularization 比较大,就会使得本来完美的模型再次变得不完美,模型也就永远达不到完美的状态。所以说使用 weight regularization,容易对训练模型产生干扰。
下面是 2 个模型的对比。两个模型基本完全相同,唯一的差异在于:第二个模型对权重使用了 0.05 的 L2 regularization,使得第二个模型的 AP 指标发生了下降。所以从实验的结果也能够看出,当权重的正则损失 Lregularization 较大时,它会对训练模型产生干扰。
这是第一个模型训练过程的损失曲线和指标曲线,AP 指标达到了 100%。
下面是第 2 个模型,对权重使用了 0.05 的 L2 regularization。在所有其它条件都完全相同的情况下,AP 指标只能达到 86.67%。
weight regularization 的作用是限制过大的权重,使得模型更简单。而如果将其替换为权重约束 weight constraint,可以达到同样的效果。
一个使用 keras.constraints.Constraint 进行权重约束的例子如下。使用 tf.clip_by_value,即可把权重限制在一个较小的范围。
class ClipWeight(keras.constraints.Constraint): """限制权重在一个固定的范围,避免出现过大权重和 NaN。 Attributes: min_weight: 一个浮点数,是权重的最小值。 max_weight: 一个浮点数,是权重的最大值。 """ def __init__(self, max_weight): self.min_weight = -max_weight self.max_weight = max_weight def __call__(self, w): # 下面 4 行代码,用于找出权重为 NaN 的位置,把 NaN 权重替换为数值 1。 nan_weight = tf.math.is_nan(w) w = tf.cond(tf.reduce_any(nan_weight), true_fn=lambda: tf.where(nan_weight, 1.0, w), false_fn=lambda: w) return tf.clip_by_value(w, clip_value_min=self.min_weight, clip_value_max=self.max_weight) def get_config(self): config = super().get_config() config.update({'max_weight': self.max_weight}) return config
对于物体探测器,可以使用 COCO 2017 数据集进行训练,并使用 COCO 的 AP 指标。
为了在训练过程中,看到 AP 指标实时的变化情况,可以用 Keras 创建一个 AP 指标。并且最后可以将训练过程的 AP 指标和损失值一起画出来。
创建 AP 指标的方法,可以参见我的另外一篇文章:→
《用 Keras/TensorFlow 2.8 创建 COCO 的 average precision 指标》,https://blog.csdn.net/drin201312/article/details/123615334
对训练数据的前处理操作,包括把 COCO 数据集的图片从硬盘加载到内存,并生成训练样本及标签等。
这些前处理操作,涉及到大量的 I/O bound 和 CPU bound 操作,最高效的方式是用 TensorFlow 的 tf.data.Dataset 来做这些前处理。如果是手动编写一个生成器来做这些前处理,效率会比较低。
具体来说,把大量图片从硬盘加载到内存,这是一个很慢的 I/O 过程(相比于 CPU 的计算速度来说),一般可以用异步编程 asynchronous programming 来提高效率。
而把训练数据提供给模型之后,这些前处理操作就处在了等待状态,要一直等待到模型把这批次数据训练结束,才会开始对下一个批次数据做前处理。对这种“等待”的情况,也应该用异步编程 asynchronous programming 来提高效率。
另一方面,把图片转换为张量,并从 COCO 数据集的 annotation 生成标签,则涉及到大量的 CPU 操作,属于 CPU bound 类型的任务。一般可以使用并发多个 Python 进程 processes 的方法,来提高效率。
要使用异步编程,可以用 Python 的 asyncio 模块。而要并发多个 Python 进程,则可以用 Python 的 concurrent.futures 模块。如果想自己手动做这些前处理操作,并且用异步编程和并发多进程来提高效率,可以参见文章:
→ 《Python 中的并发编程和异步编程》, https://blog.csdn.net/drin201312/article/details/126393423
当然,最简便的方法,是直接使用 tf.data.Dataset,可以省去自己很多的编程工作;而且它使用了多进程并发和异步操作,效率很高。tf.data.Dataset 的具体使用方法,可以参看我上传的 vision_transformer_utilities.py 文件。
要想在训练过程中计算实时的 AP 指标,可以用 keras.callbacks.Callback 实现,3 个步骤如下:
之所以要单独创建一个 “评价模型” evaluation_model,是因为计算 AP 的模型计算图极大,实际无法用图模式来实现。因此要在 eager 模式下,用另外一个 “评价模型” evaluation_model 来计算 AP。
要在 keras.callbacks.Callback 中创建一个模型,可以参见:
→ 《用 Keras/TensorFlow 2.9 创建深度学习模型的方法总结》, https://blog.csdn.net/drin201312/article/details/125098197
前面的步骤都设置好了之后,就可以开始调参。
调参时可以使用 2 个策略,第一个策略是分组,第二个策略是使用快速排序算法和网格搜索。
训练模型中通常会有很多的超参,可以将它们先分成几个组,每一个组的超参和其它组基本无关。然后分组搜索超参,在搜索一个组时,其它组的超参保持不变。
例如某个模型有 20 个超参,可以把它们分成 6 个组,不同组之间的超参没有直接关系。进行超参搜索时,可以先搜索一个组内部的 3 到 4 个超参,其它 5 个组的超参保持不变。这样就可以避免同时搜索 20 个超参,这 20 个超参组合带来的庞大搜索空间的问题。
将超参进行分组之后,下一步就是使用快速排序算法和网格搜索。
快速排序算法应用最明显的例子,是猜数字游戏。
假设有 1 到 100 之间的 100 个数字,最佳超参是这 100 个数字中的某一个。要求你以最快速度猜出最佳超参。你在猜了一次之后,会告知你猜的结果比最佳超参更大或者更小。
此时使用快速排序算法,就应该每次都猜中间的数字。比如说第一次猜 50,假如反馈的结果是最佳超参比 50 小,那么可以立即将搜索空间缩小一半,变为 [0, 50] 之间。
下一次则直接猜 25,继续把搜索空间缩小一半。如此不断循环,就能以最快速度找出最佳超参。
具体实施时,每次使用 4 或是 5 个数值,把搜索空间均匀地划分为 3 段或 4 段。
例如对于 [1, 100] 这个搜索空间,使用 1, 25, 50, 75, 100,这 5 个数字进行尝试。如果数字 25 使得模型的损失值最小,则可以把搜索空间缩小到 [1, 50] 这个范围,即搜索空间缩小了一半。然后不断重复这个搜索方式即可。
使用网格搜索的原因在于,有些参数是直接相关的,它们要相互组合才能得出最佳结果。在经过前面参数分组之后,就可以使用网格搜索。
例如损失函数由 3 部分组成:objectness 损失,分类损失和 CIOU 损失。假设这 3 个损失的比例系数为 a, b, c,且 a + b + c = 1。这种情况下,3 个超参 a, b, c 就是直接相关,它们的某一个组合能使得模型的性能最好。所以应该同时搜索这 3 个超参 a, b, c。
和网格搜索相对的是随机搜索 random search。随机搜索的问题是,对于那些相关度很高的超参,它可能会错过超参的最佳组合。比如上面的损失函数比例系数 a, b, c 的情况。
使用网格搜索时,会面临搜索次数较大的问题。此时可以配合使用更少量的数据,以及较少的迭代次数,来加快超参的搜索。
比如对于 COCO 数据集的 12 万张图片,搜索超参时可以使用 50 张或 100 张图片,并且使用 5 或 6 个 epochs。这是因为一个好的超参组合,在图片数量不多,迭代次数也不多的情况下,依然能够最快地降低模型的损失值。
训练结束后,可以用 plotly,画出训练过程的损失值和 AP 指标。如下图。
和 matplotlib 相比,plotly 更为好用。plotly 有 3 个特点:
Ablation test 是切除实验,也就是切除掉模型中的某个部分,看模型的性能是否发生变化。如果模型性能基本不变,则说明被切除的部分可能作用不大。
5 个有意思的发现如下:
需要注意的是,上面 5 个 ablation test 是针对物体探测任务的。如果是其它任务,这些 ablation test 也可能不成立。
这也说明,Vision Transformer 本身,还有很多有趣的地方可以研究。
从 ablation test 可以得到的启发,或许有 2 点:
一共有 8 个相关文件,如下图。下载地址是:
Gitee →: https://gitee.com/drin202209/vision_transformer_detector
GitHub →:https://github.com/westlake-moonlight/vision_transformer_detector
将 8 个文件放在同一个文件夹,然后要在 2 个 Python 文件中进行设置。
训练模型时,用 Jupyter Lab 打开下载的 vision_transformer_detector.ipynb,直接运行即可。
在计算 AP 指标时,计算会比较慢。为了避免浪费时间,可以使用较小的参数来计算 AP,这时就需要对 COCO 数据集进行统计。
具体操作如下:
testcases_vision_transformer_detector.py 是一个测试文件。里面放了 2 个测试盒 testcase,一个盒子用来测试 AP 指标,另外一个盒子用来测试损失函数。
如果需要修改 AP 指标或者是损失函数,那么在修改完成后,应该用测试盒测试一下,尽量减少程序中的 bug。在企业中,还需要测试团队进行完整的测试。
7.1 Vision Transformer 论文:
《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》→ https://arxiv.org/abs/2010.11929
7.2 Transformer 论文:
《Attention Is All You Need》→ https://arxiv.org/abs/1706.03762
7.3 吴恩达教授讲解的 Multi-Head Attention:
《Self Attention》 → https://www.bilibili.com/video/BV1Co4y1279R?p=34
《Multi-Head Attention》 → https://www.bilibili.com/video/BV1Co4y1279R?p=35
吴恩达教授总是能够深入浅出,用简单的语言解释清楚复杂的概念。非常好!
7.4 《Deep Learning with Python, Second Edition》 →https://www.manning.com/books/deep-learning-with-python-second-edition
在 Transformer 模型中,FRANÇOIS CHOLLET 没有使用原论文硬编码方式的 positonal encoding,而是使用了一个更简单高效的 embedding 向量来进行 positonal encoding,是一个很巧妙的做法。
7.5 Keras 官网示例代码:
《Object detection with Vision Transformers》 → https://keras.io/examples/vision/object_detection_using_vision_transformer/
这个官网代码展示了一个简单的 Vision Transformer 模型,它能够预测出一个物体框。可以作为学习 Vision Transformer 的入门代码。
虽然它只能用于 1 个类别,但是它展现了一种可能,即可以把 Vision Transformer 应用到物体探测任务上。
——————————本文结束——————————
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。