万物 | 炼器 —— 启蒙(一)
采天地至宝于一炉,千锤百炼于器胚,始得神器。
神器既得,则天下之事,莫不可得。
1.1 心法总纲——卷积神经网络整体结构
自卷积神经网络兴起以来,相继有faster-RCNN、YOLO、U-Net等一系列优秀模型现世,线上线下的相关教程、图书不尽其数,然这些图书教程,或专注于模型网络结构,在卷积、采样、池化等精妙设计中流连对比,或从古早讲到现今,刨去卷积网络发展历程,则尽是基础知识,其他或抄或凑者,不能尽数。读者诸君详阅之后,或稍有领悟,但每到实际应用,仍只能调调超参数,当个“调包侠”,至于“魔改”模型,解决实际问题,也只是照猫画虎,知其然不知其所以然。究其原因,实因多数图书教程仅述及模型之一鳞半爪,妙则妙矣,然读者诸君心中,既无网络模型之全貌,又不知数据变化之缘由,对于软件工程实践中的种种巧思,更是知之甚少,如此则难免陷入书越读越迷的困境。
欲要廓清迷雾,首要之事在于理清卷积神经网络的整体结构,心有蓝图,则如行车有导航,想要迷途亦不能够。
1.1.1 造化六道:深度神经网络的底层逻辑
一个工业级的卷积神经网络库,其繁杂之处一方面在于网络模型结构本身的设计,另一方面更在于其极致的模块解耦、动态配置反射、以及面向对象的高度抽象。常见的工业级框架(如 Ultralytics)总是包含了数据集处理、模型结构加载、模型训练实现、可视化展示等具体的工程过程,加之向后兼容、容错处理、多任务类型支持等等,网络的整体结构掩藏在纷繁的工程实现里,一般读者去读源码,即便有诸多源码释读的教程可做参考,也常有乱花迷眼,摸不着头绪之感。
抛开工业级框架源码中那成千上万行的兼容性防御代码与工具类,任何现代卷积神经网络“法宝”的核心,都依靠六个最底层的“齿轮”循环咬合。如果以炼器作比,便是驱动卷积大阵运转的“造化六道”:
-
灵矿管道:数据集(Dataset)与数据供给工具(DataLoader),数据集包含的特征越全面、标注越准确,数据集质量就越高。就好比天才地宝里面蕴含的灵力越多,炼出来的法宝自然威力越大。数据供给工具是数据的搬运工,负责把未经处理的天才地宝洗净(图像归一化)、切分(Resize)、变换(tranform),打包成固定大小(Batch),源源不断地送进炼炉。
-
器胚阵列:网络模型(model),由若干网络基础模块(如卷积层、池化层)组成,是一个负责前向传播的计算机器,将图像张量层层处理,最终输出预测结果,好比法宝由各种阵法重叠而成。阵法越复杂,重叠方式越精妙,法宝上限越高。现代卷积深度网络常将模型结构按功能特点又分为主干(Backbone)、颈部(Neck)、头部(head)等部分。
-
天道量尺:损失函数(Loss Function),它站在炼炉的终点,衡量预测结果与真实标签的误差。法宝每锤炼一遍就要检验一番。天道量尺会精确告诉你法宝离完美还有多大的差距。
-
造化重锤:优化器与反向传播(Optimizer & Scheduler),接收 天道量尺给出的Loss梯度,在网络模型成百上千万个参数的微观层面进行精准的“反向敲击”,不断逐步完善“法宝”。
-
鉴灵明镜:验证与评估(Validator / Metrics),独立于训练体系的考核官,用不同于训练集的真实数据验证完善后的模型,计算出平均精度(mAP)、精确率、召回率等核心性能指标。它决定了你的模型能否真正落地,还是只能在实验室里自嗨。
-
司炉中枢:训练引擎(Trainer Engine),掌控训练全局的中枢,它精确控制以上五大核心循环迭代,控制着前向试错、反向更新的每一次试炼,并在合适的时机保存最佳模型,输出最佳法宝。
万法归一,这就是“炼器”的底层逻辑。
1.1.2 实体凝铸:六大核心的PyTorch代码映射
作为目前应用最广的底层计算引擎之一,PyTorch库为构建卷积神经网络模型提供了极其强大的基础支持。上述6个核心,其中4个在PyTorch库中有直接对应。
-
灵矿管道:“数据集 (Dataset)”对应 torch.utils.data.Dataset,必须实现__len__方法和__getitem__方法,其中__len__方法输出该数据集一共有多少数据),__getitem__返回指定位置的数据内容。 “数据供给工具(DataLoader)” 对应torch.utils.data.DataLoader,专门负责把 Dataset 里的数据按要求(BatchSize)拼成规则的张量,送入GPU或CPU以支持训练或验证。
-
阵法器胚 (Model):对应torch.nn.Module,它是包含着成千上万个浮点数权重(Weights)的矩阵乘法堆叠,必须包含一个 forward 函数来规定数据的流向。
-
天道量尺(Loss): 对应torch.nn.MSELoss(或其他Loss类),每一个loss类的背后,都是一套复杂的数学计算公式,其作用就是把模型“预测值”和“真实值”比较,输出一个量化的误差。
-
造化重锤(Optimizer): 对应torch.optim.Adam(或其他优化器类)。它是微调机器人,它根据Loss算出的梯度,修改模型里的千万个权重参数,使得模型的“预测值”更接近于“真实值”。
6大核心中剩下的两个,鉴灵明镜——验证与评估(Validator / Metrics)和司炉中枢——训练引擎(Trainer Engine),PyTorch库中有没有直接对应的基础类呢。
答案是没有。
原生PyTorch框架只提供基础模块(算子),至于怎么构建训练循环,怎么去验证模型、用什么样的指标去评估模型以及怎么计算这些指标,这些 “业务层”的活要开发者自己完成。
当然,有大量基于pytorch的第三方库,补全了原生PyTorch库在业务层面的“缺失”,比如Ultralytics。在Ultralytics中,对应trainer的实体是ultralytics.engine.BaseTrainer及其相关子类,对应 Validator 的实体是ultralytics.engine.validator.BaseValidator及其相关子类。对应Metrics的实体是则是ultralytics.utils. metrics. OBBMetrics。
本教程最终也将带领大家逐步构建出工业级的6大核心。
1.2 天材地宝——目标检测数据集
也许你现在已经跃跃欲试,迫不及待要打开IDE编写前文所述的6大核心了。Talk is cheap,show me the code,尤其是当你感觉已经蓝图在胸的时候。
但是别急,还有很重要的一点需要啰嗦。卷积神经网络可以完成的任务有很多,常见的比如分类、目标识别、姿态估计、语义分割等等,其所对应的数据集组织方式、标注格式也不尽相同,了解不同数据集的组织方式,后续的编码,尤其是Dataset的编码,才能有的放矢。
我们以分类任务和目标检测任务来略作说明。
1.2.1 任务形态差异:分类与目标检测
分类任务主要用于判别某张图片属于哪一个类别,通常一张图片上只有一个类别,常见的比如区分猫还是狗,区分图片中的香蕉是生是熟还是腐坏。
目标检测任务主要用于从图像上识别出不同类别的事物,通常一张图片上包含多种不同类型的对象,或多个同类的对象,比如一张街拍图像上,通常包含了多个汽车、行人、自行车,乃至于路灯、店铺;一张CT检测图像上,可能包含多种不同类型的病灶区域;一张高分辨率卫星图像上,可能包含不同类型的建筑、地面行驶的车辆、海面上的各种船舶等等。目前检测任务一方面是要将这些对象找到(定位),同时还要准确判断出对象的类型(识别)。
目标检测任务根据定位框(标注框)的不同,又可以分为水平框定位(HBB)和旋转框定位(OBB),显然,对于自然图像而言,旋转框标注比水平框标注更贴合真实对象,因此更准确。但因为旋转框比水平框多了一个旋转角度的属性,所以在工程处理上也更复杂。
某种意义上讲,旋转框定位包含了水平框定位。因为水平框就是一个固定角度(0度)的旋转框。考虑到普适性,本书专注于目标检测中的旋转框标注卷积神经网络的构建,对应着Ultralytics中的YOLO OBB(旋转目标检测)模型。
1.2.2 数据容器:数据集的目录结构
理解了分类任务与目标检测任务的不同,自然可以想见,分类任务与目标检测任务各自的数据组织方式也不相同。毕竟,分类任务只需要标明一张图片属于哪个类别即可,而目标检测,需要在图像上定位每一个目标所在的精确区域以及确认它是什么类别。
与上文所述的“造化六道”中的“灵矿管道”和“鉴灵明镜”相对应,数据集通常也会包含一个训练集和一个验证集。训练集和验证集内部的组织方式是一样的。
1、分类任务数据集组织方式
标明一张图片属于哪个类别可能有很多种方式,但最简单的方式,莫过于根据类别建立对应的文件夹(有几个类别就创建几个),然后将图像放到对应类别的文件夹下就行了,图像的标签(label)就是文件夹的名称。事实上,分类任务的数据集,大多也就是这么组织的。
比如香蕉状态分类训练集的组织方式如下图:
香蕉状态分类验证集的组织方式与训练集类似,如下图:
2、目标检测任务数据集组织方式
目标检测任务数据集的组织方式就要略微复杂一些,因为它既要标注类别,还需要标注对象在图像中的精确位置,简单的把图像放在某一个文件夹下显然已经不能胜任。
目标检测任务数据集,使用一个专门的标注文件来记录一张图片上所有对象的信息,包括对象的类型以及精确位置,这个专门的标注文件通常是纯文本文件,常见的有txt格式或者JSON格式,以txt格式的标注文件为例,每一张图片对应一个同名的txt文件(当然,文件的后缀名不同),文件中的每一行默认对应图像中的一个对象。
因为出现了两类文件,即图像文件和标注文件,所以在文件组织上,目标检测任务数据集通常会有images和labels两个文件夹,顾名思义,images文件夹专门用来存放图像,而labels文件夹专门用来存放标注文件。图像文件与标注文件,通过文件名来对应,即images文件夹中一张图像对应的标注文件,是labels文件夹中与之同名的txt文件。
常见的coco数据集的组织方式如下图。
如前所述,数据集通常会包含训练集和验证集,那么目标检测任务的训练集和验证集分别在哪里呢?聪明如你一定马上就想到了,在images和labels两个文件夹下,都包含了train子文件夹和val子文件夹,分别用来存储训练图像、验证图像以及训练标注和验证标注。
coco数据集中的图像组织方式如下图。
coco数据集中的标签组织方式如下图。
本教程专注于目标检测任务,主要面对的数据集的组织方式,与coco数据集images/labels模式一致,标注文件为txt格式。
1.2.3 目标标注:两种常见的标注格式
前面讲过,目标检测任务根据定位框或者叫标注框的不同,可以分为水平框定位(HBB)和旋转框定位(OBB),这里啰嗦一句,无论水平框定位(HBB)和旋转框定位(OBB),都是一个标准的矩形。
那么如何在图像上标定一个水平框或者一个旋转框呢?
1、xyxyxyxy格式
最容易想到的一种方式是记录标注框也就是矩形的4个顶点坐标。
在一张图像内,一个点的坐标可以用横轴(x轴)的像素值x和纵轴(y轴)的像素值y两个数来表示。
于是4个顶点的坐标可以用 x,y,x,y,x,y,x,y 8个数来表示。
除了标注框,我们还需要标明对象的类别,为了方便计算机处理,对象的类别我们用序号表示。也就是说,我们有一个表示类别的字典(单独的字典文件或者某个配置文件中的一部分内容),记录着所有可能的类别名称,在标注文件中,我们使用对应类别在字典中的序号来表示该类别,而不是直接写类名的中文或者英文名称。
类别序号加上4个顶点坐标,xyxyxyxy格式的标注看起来如下图。这是dota数据集所使用的标注格式。本书将使用dota数据集进行最终的模型训练,当然这是很后面的事情了,所以目前仅知道他的标注格式即可,不用太在意这个数据集更多的信息。
也许您马上就发现了,4个顶点坐标为什么全是小数(浮点数),像素值不应该是整数吗?确实,像素值应该是整数,这里的小数表示的是像素值在整个图像中的比例,只是表示方式不同,实际意义并无区别,知道了图像的宽和高,通过比例反推得到像素值不费吹灰之力。
2、xywhr格式
另一种也不难想到的方式,就是记录矩形的中心点坐标(x,y)以及矩形的宽w、高h以及它的旋转角度r。
(示意图)
这样一个矩形标注框,只需要用5个数字就可以表示,相比较xyxyxyxy格式(需要8个数字)更简洁,占用存储空间更少。对于水平标注框而言,因为旋转角度是固定的0度,所以实际上只需要中心点坐标(x,y)以及矩形的宽w、高h这4个数字就可以表示了。
所以使用水平标注框的coco数据集的标注格式如下:
当然,可以看到,xywh也是比例信息。
顺便说一句,这种把具体的值转换为比例的形式,也就是一个0-1之间的小数的处理方式,叫做归一化处理。