唐宇迪商品检测课程笔记-全-

唐宇迪商品检测课程笔记(全)

课程P1:目标检测课程要求与目标 🎯

在本节课中,我们将明确学习目标检测课程所需的前置知识,以及完成课程后你将能够掌握的核心技能。了解这些要求与目标,有助于你评估自身基础并规划学习路径。

课程要求 📋

在正式开始课程前,你需要具备以下技术基础。这些是理解后续课程内容的关键。

以下是具体的技术要求:

  1. 机器学习基础:你需要对机器学习的基本算法有一定了解。例如,决策树、随机森林、线性回归、逻辑回归等。同时,你需要有使用 scikit-learn 库编写和训练模型的实际经验。
  2. 深度学习基础:你需要理解神经网络的基本结构、工作原理及其优化过程。
  3. 基础模型知识:你需要熟悉卷积神经网络(CNN)这一最基础的图像分类模型。同时,对 AlexNetGoogleNet 等经典网络架构也需有一定了解。
  4. 框架使用经验:你必须具备使用 TensorFlow 框架的经验。这包括了解其核心模块的使用方法,并曾用它完成过简单的图像识别任务。

如果你尚未达到上述要求,学习本课程可能会感到吃力。建议你先补充相关知识。

课程目标 🚀

上一节我们明确了学习本课程需要具备的基础。那么,完成本课程后,你将能达到哪些目标呢?

以下是完成课程后你将掌握的核心能力:

  1. 掌握项目开发流程:你将能够独立完成一个图像识别或目标检测项目的完整开发流程。
  2. 理解并实践模型训练:你将深入理解模型的原理,并掌握如何构建模型、训练模型,以及如何使用GPU乃至多GPU工具来加速训练过程。
  3. 掌握模型部署与交互:你将学会如何将训练好的模型部署到线上服务器,并了解客户端如何与模型服务器进行交互。

职业发展 💼

基于以上技能,学成之后,你可以从事多种相关岗位的工作,例如:TensorFlow工程师、图像识别/检测工程师,或机器视觉相关的开发工作。

总结 📝

本节课中,我们一起学习了目标检测课程的具体要求与最终目标。我们明确了学习本课程所需的机器学习深度学习CNN模型TensorFlow框架四方面基础。同时,我们也了解了完成课程后,你将能够掌握从模型开发、训练到部署上线的全流程技能,为从事相关技术岗位打下坚实基础。请务必牢记这些要点,以便更好地进行后续学习。

课程 P10:10.03_RCNN:候选区域与特征提取 🧠

在本节课中,我们将要学习RCNN(Region-based Convolutional Neural Networks)模型的前两个核心步骤:候选区域生成特征提取。我们将了解如何从一张图片中找出可能包含物体的区域,并利用卷积神经网络将这些区域转化为可供后续分类和定位使用的特征向量。


第一步:候选区域生成

上一节我们介绍了RCNN的整体流程,本节中我们来看看它的第一个具体步骤——生成候选区域。这部分内容我们作为了解即可。

候选区域(Region Proposal)的目的是从原始图像中筛选出可能包含物体的区域。在RCNN提出时,有多种方法可以实现,其中被广泛采用且效果较好的一种算法是选择性搜索(Selective Search, SS)

选择性搜索的基本原理如下:

  • 它首先基于像素之间的相似性,将图像分割成许多小的、颜色或纹理相近的像素块。
  • 然后,算法根据这些小块之间的邻近和相似关系,逐步将它们合并成更大的区域。
  • 通过这种自底向上的合并过程,最终生成一系列大小不一的候选区域框。

这个过程的主要目的是提供一系列像素相近的区块作为后续算法的输入。以下是其核心要点:

  • 通过选择性搜索(SS)算法进行有效筛选。
  • 生成的候选区域数量通常较多(例如约2000个),为后续处理提供丰富的可能性。


第二步:区域尺寸统一与特征提取

在得到候选区域后,我们需要将这些区域输入到卷积神经网络(CNN)中进行特征提取。然而,CNN通常要求固定尺寸的输入。

2.1 统一区域尺寸

由于选择性搜索产生的候选区域形状和大小各异,在送入CNN之前,必须将它们调整为统一尺寸。RCNN最初使用的是AlexNet网络,其输入要求是 227×227 像素。

为了实现尺寸统一并尽量减少图像变形,RCNN采用了一种称为 “Crop + Warp” 的方法。这个方法的作用是:

  • Crop(裁剪): 截取候选区域的核心部分。
  • Warp(扭曲/缩放): 将裁剪后的区域拉伸或缩放到目标尺寸(227×227)。

这种方法旨在固定输入大小的同时,尽可能减少图像的扭曲变形。如下图所示,第二种方法(Crop+Warp)产生的变形通常比简单的拉伸方法要小。

因此,这一步的核心操作是:通过Crop + Warp方法将所有候选区域统一调整为227×227的固定大小

2.2 卷积神经网络特征提取

尺寸统一后,每个候选区域就可以被送入CNN网络了。CNN的作用是提取高级的、抽象的特征,这些特征能够代表图像中物体的关键信息。

在RCNN中:

  • 使用的CNN结构是 AlexNet
  • 每一个候选区域(共约2000个)都会独立地通过整个CNN网络进行前向传播。
  • 网络最终会输出一个固定长度的特征向量。对于AlexNet,这个特征向量的长度是 4096

一个需要特别注意的细节是:提取出的这些特征向量会被保存到磁盘中。这是因为在原始的RCNN设计中,特征提取与后续的分类、回归步骤是分开进行的。保存特征可以避免对同一张图片重复进行耗时的CNN前向传播,从而提升训练和测试效率。

我们来总结一下特征提取步骤的输出:

  • 每个候选区域生成一个 4096维 的特征向量。
  • 一张图片约有2000个候选区域,因此会生成一个 2000 × 4096 的特征矩阵。
  • 这些特征被存储在磁盘中以供后续使用。


总结

本节课中我们一起学习了RCNN模型的前两个关键步骤:

  1. 候选区域生成: 使用选择性搜索(SS)算法从图像中找出约2000个可能包含物体的区域。
  2. 特征提取
    • 首先,通过 Crop + Warp 方法将所有候选区域统一调整为 227×227 的固定尺寸。
    • 然后,使用 AlexNet 卷积神经网络对每个区域进行独立处理,提取出一个 4096维 的特征向量。
    • 最后,将所有提取出的特征保存到磁盘中。

至此,我们已经完成了从原始图像到高级特征表示的转换。下一节课,我们将探讨如何利用这些保存的特征进行物体分类和边界框的精确定位。

课程 P11:RCNN 中的 SVM 分类器 🧠

在本节课中,我们将要学习 RCNN 目标检测算法的第三步:使用 SVM 分类器对候选区域进行分类。这是整个算法的核心步骤之一,我们将详细解释其工作原理和具体流程。


概述:SVM 分类器的任务

上一节我们介绍了如何从图像中提取候选区域并获取其特征。本节中我们来看看如何利用这些特征进行分类。

第三步是使用 SVM 进行分类。这个步骤是算法的一个重点。

具体做法是:我们已经得到了 2000 个候选区域的特征。每一个候选区域的特征都需要经过 SVM 分类器进行处理。

我们来看 SVM 具体在做什么。算法首先将图像转换为候选区域,并将其定义为一个分类问题。

SVM 分类过程详解

以下是 SVM 分类的具体步骤:

假设你有 2000 个候选区域,每个区域提取出一个 4096 维的特征向量。我需要对这些特征向量进行分类。

使用 SVM 对每一个特征向量进行分类。SVM 主要是一种二分类器。假设有 N 个类别,我们的最终目标就是检测这 N 个类别。

有同学会问,之前提到的数字 20 是怎么来的?就是这么来的。为什么是 20?因为 RCNN 在最初测试时,定义的目标检测任务就是检测 20 个类别。也就是说,目标检测一共需要检测 20 个类别,没有其他类别了。这就是数字 20 的来源。

每个分类器都会对这 2000 个候选区域的特征向量分别判断一次。这样会得出一个 2000 x 20 的得分矩阵。

20 代表你的目标检测任务在当前数据集中一共需要检测 20 种类别。例如,PASCAL VOC 数据集就有 20 个类别。

这个过程有 20 个类别,因此需要 20 个 SVM 分类器。

这 20 个分类器各自做什么呢?首先,第一个分类器需要对 2000 个候选区域(注意,一个分类器处理所有 2000 个区域)进行分类。

例如,这里列举了 2000 个候选区域。每个候选区域都要经过这个 SVM 分类器。假设这个 SVM 是判定“是不是猫”的,它只有两个输出:是猫,或者不是猫(可以理解为背景)。

比如,第一个区域判定为猫,记为正例(如标记为 1)。第二个区域也是猫,记为正例。第三个区域不是猫,记为负例。以此类推。

这个 SVM 分类器训练好后,就可以用来进行判定。

我们保存这个 SVM 分类器对候选区域的分类结果。

这是一个分类器。那么第二个分类器呢?比如,第二个分类器专门用于判断“是不是狗”。这样就能理解了:每一个类别的分类器,都会对 2000 个候选区域的特征进行一次分类

SVM 输出的是一个得分或概率。假设 SVM 分类器已经训练好了。

我们来看一下这个过程得出的结果。首先,特征来自 2000 个候选区域,维度是 2000 x 4096

我们可以这样表示:

  • 行:代表第 1 到第 2000 个候选区域。
  • 列:代表第 1 到第 20 个 SVM 分类器(对应 20 个类别,如猫、狗、车等)。

每个 SVM 分类器(在测试阶段)接收这 2000 个特征,并输出 2000 个得分(例如概率值 P1, P2, ..., P2000)。

这样,我们就得到了一个 2000 x 20 的得分矩阵。

这个得分矩阵表示每个候选区域属于每个类别的可能性。

后续步骤:非极大值抑制 (NMS)

得出 2000 x 20 的得分矩阵后,接下来要进行筛选。

在后续的第四步中,会分别对这个 2000 x 20 的矩阵中的每一类(共20类)进行非极大值抑制,以剔除重叠的候选框。NMS 也是一个非常重要的过程。


总结

本节课中我们一起学习了 RCNN 算法中 SVM 分类器的工作流程。核心要点包括:

  1. 任务:使用训练好的 SVM 对 2000 个候选区域的特征进行分类。
  2. 机制:为数据集中每一个待检测的类别(例如 20 类)训练一个独立的二分类 SVM。
  3. 输出:每个 SVM 分类器对所有候选区域进行打分,最终形成一个 2000 x 20 的得分矩阵,该矩阵记录了每个区域属于每个类别的可能性。
  4. 目的:该得分矩阵为后续的非极大值抑制步骤提供了筛选依据,以确定最终的检测框和类别。

通过这一步,RCNN 将目标检测问题转化为了对候选区域的分类问题,并为精确定位和分类奠定了基础。

课程 P12:12.05_RCNN:非极大抑制(NMS)🚀

概述

在本节课中,我们将要学习RCNN目标检测算法中的一个关键步骤:非极大抑制。它的核心作用是筛选出最有可能包含物体的候选框,避免对同一物体进行重复检测,从而得到最终简洁、准确的检测结果。


非极大抑制的作用

上一节我们介绍了RCNN如何为每个候选框生成类别得分。假设我们有2000个候选框,每个框有20个类别的得分,那么最终会得到一个 2000 x 20 的得分矩阵。但一张图片中不可能有2000个物体,因此我们需要筛选出可能性最大的少数候选框。非极大抑制的目的就是完成这个筛选过程,为图片推荐最终有效的检测框。

NMS的工作原理

非极大抑制是一个迭代过程。它首先对候选框进行概率筛选,然后对剩余的候选框进行交并比计算与比较。

以下是NMS的核心步骤:

  1. 概率筛选:对于每个候选框,取其所有类别得分中的最大值(即该框最可能属于的类别的得分)。然后,从所有候选框中选出得分最高的那个框。
  2. 交并比计算与抑制:将剩余的每个候选框与上一步选出的最高分框进行交并比计算。
    • 交并比公式IoU = (A ∩ B) / (A ∪ B)
    • 其中,A ∩ B 代表两个框的交集面积,A ∪ B 代表两个框的并集面积。
  3. 阈值判断:设定一个阈值(通常为0.5)。如果某个候选框与最高分框的IoU大于该阈值,则认为它们检测的是同一个物体,从而将该候选框删除
  4. 迭代循环:在删除了一部分框后,从剩余的候选框中再次选出得分最高的框,重复步骤2和3,直到所有候选框都被处理完毕。

实例演示

为了便于理解,我们用一个简化的例子来说明。假设RCNN为一张图片生成了5个候选框,标记为A、B、C、D、E。

第一轮迭代:

  • 假设B框的得分最高。
  • 计算A、C、D、E与B的IoU。
  • 假设C和D与B的IoU大于阈值0.5,则认为C和D与B检测的是同一物体。
  • 删除C和D保留B作为第一个预测结果。

第二轮迭代:

  • 在剩余的A和E中,假设A的得分最高。
  • 计算E与A的IoU。
  • 假设E与A的IoU大于阈值0.5。
  • 删除E保留A作为第二个预测结果。

最终,我们从5个候选框中筛选出了B和A两个最终的检测框。这个过程有效地去除了对同一物体的冗余检测。

NMS在RCNN流程中的位置

理解了NMS的步骤后,我们来看它在整个RCNN流程中处于什么位置。

  1. 特征提取:CNN网络从候选框中提取特征。
  2. 分类打分:SVM(或线性分类器)根据特征为每个候选框的每个类别进行打分。
  3. 非极大抑制:对SVM打分后的候选框(例如2000个)应用NMS算法进行筛选。
  4. 输出结果:得到最终数量较少、且最可能包含物体的候选框集合。

预测结果的准确数量并非固定,它取决于图片中实际有多少物体以及模型预测的准确度。NMS确保了每个物体只由一个最合适的框来代表。

总结

本节课中我们一起学习了非极大抑制。我们了解到,NMS通过迭代地选取最高分框抑制与其高度重叠的其他框,有效地解决了目标检测中的冗余框问题。它是RCNN以及后续许多目标检测算法中不可或缺的后处理步骤,保证了检测结果的简洁性和准确性。

课程P13:13.06_RCNN:候选区域修正 🎯

在本节课中,我们将要学习RCNN(区域卷积神经网络)流程中的最后一步:候选区域修正。我们将了解为何需要对检测框进行微调,以及如何通过回归方法使候选框更精确地匹配真实目标框。


概述

RCNN的目标检测流程包含多个步骤。上一节我们介绍了非极大值抑制(NMS),它帮助我们筛选出最有可能包含目标的候选框。然而,这些由选择性搜索算法生成的候选框,其位置和大小可能并不完全准确。即使某个候选框的分类得分很高,它与真实的标注框(Ground Truth)之间仍可能存在偏差。

因此,本节中我们来看看如何通过一个回归过程,自动调整候选框的位置和尺寸,使其更接近真实的目标框。这个过程被称为边界框回归。


为何需要修正候选区域?

选择性搜索等算法推荐的候选区域不一定完全准确。即使某个候选区域的分类得分较高,其边界框与真实标注框之间仍可能存在位置或尺度上的误差。

所以,我们需要对筛选后的候选框进行手动或自动的调整,使其位置更接近于真实标注框。这个过程可以称为回归过程,即边界框回归。


什么是边界框回归?

边界框回归用于修正筛选后的候选框,使之回归于与之对应的真实标注框。该方法默认候选框与真实框之间是线性关系,即它们比较相近,只存在简单的线性偏移。

以下是修正过程的示意图:

我们的目标是让候选框(通常作为输入特征)通过一个回归计算,学习一组参数。这组参数与候选框进行运算后,会产生一个预测框。训练的目标是让这个预测框无限接近真实标注框。


回归过程详解

我们可以通过下图来理解这个过程:

假设绿色框(G)是真实标注框,蓝色框(A)是选择性搜索返回的候选框。回归任务的目标是学习一个变换,将候选框A调整到虚线框(G’)的位置,使得G’尽可能接近G。这种变换就是一种线性回归。

因此,修正候选区域的过程就是一个回归过程。我们通过对边界框进行微调,使其更精确。


如何实现边界框回归?

实现边界框回归的核心是建立一个回归方程。以下是关键要素:

  • 特征值:我们的候选区域。通常使用从该区域提取的CNN特征作为输入。
  • 目标值:与该候选区域对应的真实标注框。
  • 学习目标:建立回归方程,学习一组变换参数(如平移和缩放参数)。这组参数能够将候选框映射到更接近真实框的位置。

公式上,我们通常学习四个参数:dx, dy, dw, dh。它们分别用于调整候选框的中心点坐标(x, y)以及宽度(w)和高度(h)。

一个简化的变换公式如下:

预测框中心x = 候选框中心x + dx * 候选框宽
预测框中心y = 候选框中心y + dy * 候选框高
预测框宽 = 候选框宽 * exp(dw)
预测框高 = 候选框高 * exp(dh)

模型的任务就是根据输入特征,预测出最优的 dx, dy, dw, dh


RCNN完整流程回顾

现在,我们将RCNN的整个流程梳理一遍。修正边界框是流程的最后一步:

  1. 生成候选区域:使用选择性搜索等算法找出可能包含物体的区域,并将这些区域调整到统一尺寸。
  2. 特征提取:将调整后的候选区域输入预训练好的CNN网络,提取出特征向量。
  3. 区域分类:将提取的特征输入到SVM分类器中进行打分,判断该区域属于哪个类别。
  4. 非极大值抑制:应用NMS算法,剔除重叠度高的冗余候选框,保留得分最高的框。
  5. 边界框回归:对保留下的候选框进行位置和尺寸的微调,使其更精确地框住目标。


总结

本节课中,我们一起学习了RCNN目标检测框架的最后关键一步——候选区域修正。我们了解到,由于初始候选框不够精确,需要通过边界框回归技术对其进行微调。这个过程以候选框为输入,以对应的真实标注框为目标,通过线性回归学习一组变换参数,从而得到更准确的目标定位。

至此,我们已经完成了对RCNN整个流程的详细讲解,从候选区域生成、特征提取、分类打分、去冗余到最终的框位置修正,每一步都是为了实现更准确的目标检测。

🧠 课程 P14:RCNN 训练与测试过程详解

在本节课中,我们将学习 RCNN 模型的核心环节:训练过程与测试过程。我们将详细拆解如何准备数据、训练网络以及最终进行预测,确保初学者能够清晰地理解每个步骤。


📋 概述

RCNN 作为一种经典的目标检测模型,其训练过程涉及多个精心设计的步骤,包括数据准备、网络预训练与微调,以及特定分类器与回归器的训练。理解这些步骤是掌握 RCNN 工作原理的关键。


🔧 训练过程详解

上一节我们介绍了 RCNN 的整体流程,本节中我们来看看其具体的训练步骤。RCNN 的训练过程主要分为以下几个部分:正负样本准备、网络预训练、网络微调、SVM 分类器训练以及边框回归器训练。

1. 正负样本准备

首先,我们需要从图像中筛选出用于训练的有效样本。通过 Selective Search 方法获取的约 2000 个候选区域并不会全部用于训练,而是根据它们与真实标注框的重叠程度进行筛选。

以下是正负样本的定义与筛选规则:

  • 正样本:某个候选区域与图像中任意一个真实标注框(Ground Truth)的重叠面积(IoU)最大值 大于等于 0.5。这意味着该候选区域较好地覆盖了目标物体。
  • 负样本:某个候选区域与图像中所有真实标注框的 IoU 值 均小于 0.5。这意味着该候选区域与目标物体几乎没有重叠。

在实际操作中,需要保证正样本与负样本的比例大致为 1:3,以平衡训练数据。

2. 预训练与微调网络

接下来是训练卷积神经网络部分。由于 CNN 参数众多,直接从头开始训练需要海量数据,因此通常采用“预训练 + 微调”的策略。

以下是预训练与微调的核心概念:

  • 预训练:使用大型通用数据集(如 ImageNet)上已训练好的 CNN 模型(例如 AlexNet)及其参数。这相当于获得了一个具有强大特征提取能力的“基础模型”。
    • 公式/代码表示预训练模型参数 = Load_Pretrained_Model(‘AlexNet_ImageNet’)
  • 微调:将上一步获得的基础 CNN 模型,在 我们自己标记好的正负样本数据集 上继续进行训练,以调整参数,使其更适应我们的特定检测任务。这个过程也称为迁移学习。
    • 公式/代码表示微调后模型参数 = FineTune(预训练模型参数, 我们的正负样本数据)

简单来说,预训练是“借用”通用知识,微调是“定制”专属技能。

3. 训练 SVM 分类器

经过微调的 CNN 用于提取候选区域的特征。但这些特征需要送入专门的分类器来判断具体类别。RCNN 为每个目标类别训练一个独立的 SVM 分类器。

以下是训练 SVM 分类器的过程:

  • 假设我们有 20 个待检测类别,则需要训练 20 个 SVM 分类器。
  • 对于“猫”这个类别的分类器,其输入是所有候选区域通过 CNN 提取出的特征(例如维度为 N x 4096,N 为候选区域数量)。
  • 其训练目标是判断每个候选区域是否属于“猫”。我们将之前标记的、属于猫的正样本作为正例,其他所有非猫的样本(包括其他类别的正样本和所有负样本)作为负例进行训练。
  • 最终,每个 SVM 分类器会学习到一组权重参数。

4. 训练边框回归器

最后,为了更精确地定位目标,我们需要训练一个边框回归器(Bounding Box Regressor)。它的作用是微调候选框的位置,使其更紧密地贴合真实目标。

以下是边框回归器的训练要点:

  • 训练数据筛选更为严格:只选择那些与真实标注框 IoU 值超过一个较高阈值(例如 0.6 或 0.7)的候选区域进行回归训练。
  • 回归器学习的是候选框与真实框之间的 位置偏移量(如中心点坐标变化、宽高缩放)。
  • 训练完成后,会得到回归器的参数,用于在测试阶段修正预测框的位置。

🚀 测试(预测)过程

训练完成后,所有组件(CNN特征提取器、SVM分类器、边框回归器)的参数都已就绪,即可进行预测。

RCNN 的测试过程与我们之前介绍的整体流程完全一致:

  1. 输入图像,通过 Selective Search 生成约 2000 个候选区域。
  2. 尺寸调整,将每个候选区域缩放至固定大小。
  3. 特征提取,将调整后的区域输入微调好的 CNN,得到 2000 x 4096 维的特征矩阵。
  4. 分类打分,将每个候选区域的特征分别输入 20 个 SVM 分类器,得到 2000 x 20 的得分矩阵,表示每个区域属于各类别的可能性。
  5. 非极大值抑制:对每个类别,应用非极大值抑制(NMS)去除高度重叠的冗余框。
  6. 边框回归:对保留下的候选框,使用训练好的边框回归器进行位置精修,得到最终的预测框。

公式/代码表示测试流程

候选区域 = Selective_Search(输入图像)
特征矩阵 = CNN_Forward(尺寸调整(候选区域))
得分矩阵 = SVM_Classify(特征矩阵)
精炼框 = NMS(得分矩阵)
最终预测框 = BBox_Regress(精炼框)

📝 总结

本节课我们一起学习了 RCNN 模型训练与测试的全过程。我们明确了训练需要经历的四个主要阶段:准备正负样本、预训练与微调 CNN、训练多类别 SVM 分类器以及训练边框回归器。测试过程则整合了这些训练好的组件,按照“候选区域 -> 特征提取 -> 分类打分 -> 非极大值抑制 -> 边框回归”的流程,最终完成目标检测任务。理解这一流程是后续学习更高效检测模型(如 Fast R-CNN, Faster R-CNN)的重要基础。

课程P15:RCNN总结、优缺点与问题自测 📝

在本节课中,我们将对RCNN模型进行全面的总结。我们将回顾其核心流程,深入分析其显著的优缺点,并通过一系列自测问题来检验你对RCNN关键知识的掌握程度。

流程回顾 🔄

上一节我们详细介绍了RCNN的各个组成部分,本节中我们来看看其完整的端到端流程。理解这个流程是掌握RCNN的基础。

以下是RCNN处理一张图片的完整步骤:

  1. 输入图片:模型接收一张原始图片作为输入。
  2. 生成候选区域:使用选择性搜索算法从输入图片中提取约2000个候选区域,这些区域也称为感兴趣区域。
  3. 区域归一化:将每个大小不一的候选区域通过仿射变换等方法,统一调整为固定尺寸。
  4. 特征提取:将每个归一化后的区域输入到一个预训练的卷积神经网络中,提取出固定长度的特征向量。
  5. 分类与回归
    • 将提取出的特征输入到一组SVM分类器中,判断该区域属于哪个类别。
    • 同时,将特征输入到边界框回归器中,对候选区域的位置和大小进行微调,使其更精确地框住目标。

性能与优缺点 ⚖️

了解了RCNN的流程后,我们来看看它的实际表现和内在的局限性。

RCNN于2014年被提出,在PASCAL VOC 2007数据集上取得了约66%的mAP精度。虽然以今天的标准来看这个精度并不算高,但在当时,相比不使用深度学习的传统方法,这是一个巨大的突破。

然而,RCNN的缺点也非常明显,这些缺点直接推动了后续Fast R-CNN、Faster R-CNN等模型的诞生。

以下是RCNN的主要缺点:

  • 训练过程复杂且阶段多:训练需要分多个独立阶段进行,包括CNN预训练、微调、SVM分类器训练和边界框回归器训练,过程繁琐。
  • 训练与测试速度极慢:由于每个候选区域都需要单独通过CNN进行前向传播以提取特征,效率低下。例如,使用VGG16网络处理一张图片需要约47秒。
  • 占用大量磁盘空间:SVM训练需要将CNN提取的所有区域特征保存到磁盘。对于包含5000张图片的VOC数据集,特征文件可能达到数百GB。
  • 区域变形问题:在将候选区域归一化为固定尺寸时,可能导致图像内容发生形变,影响特征提取的准确性。

核心要点总结 🎯

本节课我们一起学习了RCNN模型的总结性内容。我们来回顾一下需要掌握的核心要点:

  • 解决思路:RCNN采用了“区域提议+深度学习分类”的范式,是两阶段目标检测器的开创性工作。
  • 整体流程:掌握从输入图像到输出检测框的完整流水线。
  • 训练过程:重点理解预训练微调两个阶段的目的与区别,以及SVM和回归器的作用。
  • 模型缺点:深刻理解RCNN在训练速度、测试速度、存储开销和流程复杂性方面的主要缺陷。

知识自测 ❓

为了帮助你检验学习成果,以下是几个关键问题。如果你能清晰回答这些问题,说明你已经掌握了RCNN的核心内容。

  1. RCNN中使用什么算法来获取候选区域?
  2. 如果RCNN为每张图片提取2000个候选框,数据集中共有120个物体类别,那么需要训练多少个SVM分类器?
  3. 边界框回归器的作用是什么?
  4. 在CNN微调阶段,正负样本的比例通常如何设置?
  5. 请简要描述RCNN在测试时的完整过程。

参考答案

  1. 选择性搜索算法。
  2. 120个(每个类别一个二分类SVM)。
  3. 对候选区域的位置和大小进行微调,使其更精确地匹配真实目标框。
  4. 正样本与负样本的比例通常为1:3。
  5. 输入图片 → SS提取约2000个候选区域 → 区域归一化 → CNN提取特征 → SVM分类 → 边界框回归 → 输出最终检测结果。

课程 P16:16.01_SPPNet 🧠

在本节课中,我们将要学习 SPPNet(Spatial Pyramid Pooling Network),这是一个在 RCNN 基础上进行改进的目标检测模型。我们将重点探讨它与 RCNN 的主要区别、其核心的网络流程,并理解其如何解决 RCNN 存在的关键问题。

与 RCNN 的区别

上一节我们介绍了 RCNN 的基本原理。本节中我们来看看 SPPNet 针对 RCNN 做了哪些改进。

RCNN 的速度慢是一个主要问题。其速度慢的原因在于卷积计算部分。在 RCNN 中,通常需要处理约 2000 个候选区域,而每个候选区域都需要独立地输入卷积神经网络(CNN)进行特征提取。这意味着同一张图片的卷积计算需要重复执行数千次,计算量巨大,非常耗时。

相比之下,SPPNet 提出了两点核心改进:

  1. 减少卷积计算:SPPNet 不再让每个候选区域单独通过 CNN。相反,它只将整张原始图片输入 CNN 一次,得到一整张特征图(Feature Map)。
  2. 防止图片内容变形:RCNN 在将候选区域输入 CNN 前,需要将其缩放(Warp)到固定尺寸,这会导致图像扭曲和失真。SPPNet 通过引入新的结构避免了这一操作。

以下是 SPPNet 与 RCNN 流程的直观对比:

  • RCNN:原始图像 -> 提取候选区域 -> 每个区域缩放并独立输入 CNN -> 得到每个区域的特征 -> 后续分类与回归。
  • SPPNet:原始图像 -> 整图输入 CNN 得到特征图 -> 将候选区域映射到特征图上 -> 通过 SPP 层 提取固定大小的特征 -> 后续分类与回归。

可以看到,SPPNet 的核心思路是共享卷积计算,并引入 SPP 层 来处理不同大小的输入。

SPPNet 网络流程分析

了解了基本区别后,本节我们将深入分析 SPPNet 的具体工作流程。其流程可以概括为以下几个关键步骤:

  1. 生成整图特征图:将完整的输入图像一次性通过卷积神经网络(CNN),输出一张对应的特征图。
  2. 生成候选区域:使用 Selective Search(SS)算法在原始图像上生成候选区域(Region Proposals)。
  3. 特征映射:这是关键一步。需要将原始图像上的候选区域,映射到第一步得到的特征图上的对应位置。因为特征图是原图经过卷积下采样后的结果,所以需要计算坐标的对应关系。映射后,每个候选区域在特征图上对应一个特征块(Feature Patch)。
  4. 空间金字塔池化(SPP):映射得到的特征块大小是不固定的。SPP 层的作用就是接收这些任意尺寸的特征块,并输出固定长度的特征向量。这是 SPPNet 的核心创新。
  5. 分类与回归:将 SPP 层输出的固定维特征向量输入全连接层,最终完成目标分类和边界框回归。

这个流程中的重点和难点在于 特征映射SPP 层

特征映射详解

为什么需要映射?因为候选区域是从原始图像(例如 1000x600 像素)中提出的,而特征图是原始图像经过多个卷积和池化层后的结果(例如 60x40 的特征图)。两者尺寸和坐标系统不同。映射就是找到原始图像中候选框的四个角点 (x_min, y_min, x_max, y_max),在特征图上对应的位置。

映射公式通常考虑网络的下采样步长(Stride)。假设从输入图像到特征图的总步长为 S(例如 S=16),那么特征图上的坐标大约可以通过原始坐标除以 S 得到:

特征图_x ≈ 原始_x / S
特征图_y ≈ 原始_y / S

实际计算中还需考虑填充(Padding)等因素进行微调。通过映射,我们无需对每个区域重复卷积,直接从共享的特征图中“裁剪”出对应的特征。

SPP层的作用

SPP 层全称为空间金字塔池化层,它解决了卷积神经网络后接的全连接层要求输入特征尺寸固定这一问题。

以下是 SPP 层工作原理的简单描述:

  1. 对于一个输入的特征块(假设大小为 h x w x c,其中 c 是通道数),SPP 层会对其进行多尺度的池化。
  2. 常见的做法是使用三个尺度的池化窗口:4x4, 2x2, 1x1。这里的数字代表将特征图在空间上划分的网格数。
  3. 对于每个尺度的每个网格,进行最大池化(Max Pooling)操作。这样,4x4 网格产生 16 个池化结果,2x2 网格产生 4 个,1x1 网格产生 1 个。
  4. 将所有池化结果(16+4+1=21个)按通道连接(Flatten)起来,形成一个固定长度的特征向量(长度为 21 * c)。

通过这种方式,无论输入的特征块 hw 是多少,SPP 层总能输出一个固定维度的特征向量,从而满足全连接层的要求。其代码逻辑可简示如下(伪代码):

# 假设输入特征 feature_patch 尺寸为 [h, w, c]
outputs = []
for bin_size in [4, 2, 1]: # 金字塔尺度
    # 计算每个网格的池化结果
    pooled = spatial_pooling(feature_patch, bin_size) # 形状 [bin_size, bin_size, c]
    outputs.append(pooled.flatten()) # 展平
fixed_length_vector = concatenate(outputs) # 连接所有展平后的向量

总结

本节课中我们一起学习了 SPPNet 模型。我们首先分析了 RCNN 速度慢的根源在于对每个候选区域进行重复的卷积计算。接着,我们探讨了 SPPNet 的两大改进:整图卷积共享以极大提升速度,以及引入 SPP 层 来避免图像变形并处理可变尺寸的输入。最后,我们详细梳理了 SPPNet 的网络流程,重点讲解了特征映射的方法和 SPP 层 的工作原理。SPPNet 的这些思想为后续更快的目标检测模型(如 Fast R-CNN)奠定了重要基础。

课程 P17:SPPNet 中的区域映射详解 🗺️

在本节课中,我们将要学习 SPPNet 中的一个核心概念:如何将原始图像上的候选区域映射到卷积神经网络(CNN)生成的特征图上。理解这个映射过程是掌握 SPPNet 工作原理的关键。

映射的概念与背景

上一节我们介绍了 SPPNet 的基本思想,本节中我们来看看“映射”具体指什么。

映射指的是将原始图像上的候选区域,转换到 CNN 输出的特征图上的对应位置的过程。

在 SPPNet 中,我们同时拥有两个输入:

  1. 原始图像通过选择性搜索(Selective Search)算法得到的候选区域。
  2. 原始图像通过 CNN 前向传播后生成的特征图。

我们的最终目标是为每一个候选区域提取一个固定长度的特征向量,用于后续的分类和回归。然而,候选区域是基于原始图像定义的,而特征图是经过多层卷积和池化运算后的结果,其尺寸和空间关系已经发生了变化。

因此,必须找到一个方法,将原始图像上的候选框(例如 (x_min, y_min, x_max, y_max))准确地映射到特征图上的对应区域。这个过程就是“映射”。

映射公式详解

理解了为什么需要映射后,我们来看看这个映射是如何通过数学公式精确计算的。

映射的核心在于找到一个缩放因子 S,它代表了从原始图像空间到特征图空间的总体步长(stride)。假设原始图像上某点的坐标为 (x, y),其在特征图上的对应坐标为 (x‘, y’),则映射关系由以下公式定义:

左上角坐标映射公式:
x‘ = floor(x / S)
y‘ = floor(y / S)

右下角坐标映射公式:
x‘_max = ceil(x_max / S) - 1
y‘_max = ceil(y_max / S) - 1

这里的 S 是 CNN 网络中所有卷积层和池化层的 步长(stride)的乘积。在原始的 SPPNet 论文中,基于其使用的 CNN 结构(如 ZF-5 或 VGG16),这个 S 通常为 16

举个例子,若原始图像上一个候选区域的左上角坐标为 (100, 150),且 S=16,那么该点在特征图上的横坐标 x‘ = floor(100 / 16) = 6

映射过程的意义

通过上述公式,我们可以将原始图像上的每一个候选区域都映射到特征图上。

这意味着,对于通过选择性搜索产生的约 2000 个候选区域,我们都能在特征图上找到其对应的“特征区域”。这些特征区域才是后续输入给 SPP(空间金字塔池化)层,以生成固定长度特征向量的基础。

映射过程解决了 R-CNN 中需要对每个区域独立进行 CNN 前向传播的效率瓶颈。SPPNet 只需对整张图像做一次前向传播,生成一个特征图,然后通过这个映射公式,即可一次性为所有候选区域找到其特征表示,极大地提升了速度。

总结

本节课中我们一起学习了 SPPNet 中至关重要的区域映射机制。

我们首先明确了映射的目的是为了将基于原始图像的候选框与 CNN 生成的特征图在空间上对齐。接着,我们深入讲解了实现这一对齐的核心映射公式,该公式利用网络总步长 S 来计算坐标转换。最后,我们理解了这一过程的意义:它使得 SPPNet 能够高效地在单次前向传播中为所有候选区域提取特征,这是其相比 R-CNN 获得巨大速度提升的关键一步。

掌握这个映射原理,就为理解下一节要介绍的 SPP(空间金字塔池化)层如何工作打下了坚实的基础。

课程 P18:SPPNet - SPP层的作用详解 🧠

在本节课中,我们将要学习SPPNet网络中的一个核心组件——SPP层(空间金字塔池化层)。我们将详细解析它的工作原理,以及它如何解决输入特征图尺寸不固定,但全连接层需要固定长度输入这一关键问题。

上一节我们介绍了候选区域到特征图的映射过程。本节中我们来看看如何将这些大小不一的特征图区域,转换为固定长度的特征向量。

SPP层的引入与作用

映射之后得到的特征变量大小是不固定的。然而,R-CNN等网络的全连接层(FC层)要求输入是固定大小的。例如,R-CNN输入固定为227x227,输出的特征向量也是固定的4096维。

因此,必须有一种方法将不固定大小的输入,转换为固定大小的输出。这个任务就交给了SPP层。

SPP层,即空间金字塔池化层,它能接受任何大小的输入,并输出固定维度的特征向量,然后传递给全连接层。

SPP层的工作流程

这个过程的关键在于变换。对于从原始图像中筛选出的M个候选区域(注意,M不一定是2000,2000是R-CNN的设定),每一个都需要经过SPP层进行相同的变换。

以下是SPP层变换过程的文字描述:
输入图片的一个候选区域,经过卷积后得到一个特征图(例如13x13x256)。SPP层会将该区域对应的特征图块,分别划分成1x1、2x2、4x4的网格。对每个网格内的区域执行最大池化(Max Pooling)操作,然后将所有结果连接起来,最终形成一个固定长度(例如 (16+4+1)*256)的特征向量,传给全连接层。

接下来我们详细讲解这个过程。

SPP层过程详解

假设输入图像经过CNN后,得到一个特征图(Feature Map)。这个特征图通常有多个通道,例如尺寸为13x13,通道数为256,即共有256张13x13的特征图。

对于这M个候选区域中的每一个,都需要执行以下操作:

SPP层对该候选区域对应的特征图块进行多尺度网格划分:

  1. 使用4x4的网格进行划分:将区域均匀划分为16个小块。

    • 每一小块进行最大池化,得到一个值。
    • 对于一个有256个通道的特征图,经过此操作会得到 16 * 256 个值。
  2. 使用2x2的网格进行划分:将区域均匀划分为4个小块。

    • 每一小块进行最大池化,得到一个值。
    • 最终得到 4 * 256 个值。

  1. 使用1x1的网格进行划分:即不划分,将整个区域视为一块。
    • 进行最大池化,得到该区域的最大值。
    • 最终得到 1 * 256 个值。

最后,将这三个尺度池化得到的结果连接(Concatenate)起来,形成最终的特征向量。

其维度计算公式为:
(16 + 4 + 1) * 通道数 = 21 * 通道数

因此,每个候选区域经过SPP层处理后,都会得到一个固定为 21 * 256 的特征向量(这里的256是示例网络的通道数,实际根据网络结构如AlexNet、VGG等会有所不同)。

核心总结

本节课中我们一起学习了SPPNet的核心创新点——SPP层。

我们来总结一下SPP层的关键作用:

  • 输入:任意尺寸的特征图区域。
  • 操作:对每个区域进行1x1, 2x2, 4x4三个尺度的网格划分和最大池化。
  • 输出:固定长度的特征向量(例如 21 * 通道数)。
  • 意义:这使得网络可以一次性对整张图像进行卷积计算,然后通过SPP层处理各个候选区域,避免了R-CNN中需要对每个区域重复进行卷积运算的巨大开销,显著提升了检测速度。

至此,特征向量的长度被固定下来,后续的全连接层和分类、回归操作就可以正常进行了。

🧠 课程 P19:SPPNet 总结、优缺点与问题自测

在本节课中,我们将总结 SPPNet 的核心思想、完整结构,分析其优缺点,并通过几个关键问题来检验你对 SPPNet 的理解程度。


📊 SPPNet 完整结构回顾

上一节我们介绍了 SPPNet 的核心思想,本节中我们来看看它的完整处理流程。

整个结构可以概括为以下几个步骤:

  1. 输入图像:将一张图片输入网络。
  2. 卷积特征提取:图像直接经过卷积神经网络,生成特征图。
  3. 区域映射与 SPP 层处理:将候选区域映射到上一步生成的特征图上。对于每个映射后的特征区域,SPP 层会对其进行多尺度池化。
  4. 固定长度输出与分类回归:SPP 层将每个区域处理成固定长度的特征向量,然后输入全连接层。最后,通过 SVM 分类器和 Bounding Box 回归器完成目标检测。

这个过程与 R-CNN 类似,但关键改进在于SPP 层的引入

以下是 SPP 层多尺度池化的一个示例,它通常包含 4x42x21x1 等不同尺度的池化:

# 概念性代码,表示 SPP 层对同一区域进行不同尺度的池化
spp_output = []
for pool_size in [(4,4), (2,2), (1,1)]:
    pooled_feature = spatial_pyramid_pooling(feature_map, pool_size)
    spp_output.append(pooled_feature)
fixed_length_vector = concatenate(spp_output) # 拼接成固定长度向量

因此,SPPNet 的完整结构主要改善了两点:大幅减少了卷积运算次数,以及生成了固定长度的特征向量


⚖️ SPPNet 的优缺点分析

了解了 SPPNet 的结构后,我们来系统性地分析它的优势与不足。

优点

相对于 R-CNN,SPPNet 的主要改进在于:

  • 计算效率提升:通过将候选区域映射到共享的特征图上,只需对整图进行一次卷积计算,避免了 R-CNN 中每个候选区域都独立进行卷积的重复计算。
  • 避免图像形变:SPP 层可以接受任意尺寸的输入并输出固定长度的特征,因此无需像 R-CNN 那样对每个候选区域进行裁剪或形变,保留了更多原始信息。

缺点

然而,SPPNet 并未解决 R-CNN 的所有固有缺陷:

以下是 SPPNet 存在的主要缺点:

  1. 训练流程复杂且缓慢:训练仍然是多阶段的,需要分别训练卷积网络、SVM分类器和Bounding Box回归器。
  2. 特征需要写入磁盘:在训练SVM和回归器时,中间生成的特征需要保存到磁盘,占用大量存储空间且速度慢。
  3. 无法端到端训练:SVM和回归器无法与前面的卷积网络部分进行联合优化,限制了模型性能的进一步提升。


✅ 学习成果自测

最后,我们通过几个问题来检验你是否掌握了 SPPNet 的核心内容。

请尝试回答以下问题:

  1. 映射过程公式:描述 SPPNet 中将原始图像上的候选区域映射到特征图上的过程,其坐标计算公式是什么?(提示:注意与 R-CNN 的不同之处)
  2. SPP 层过程:SPP 层具体是如何操作的?“4x4, 2x2, 1x1”的池化结构是如何产生固定长度输出的?
  3. 核心改进:相对于 R-CNN,SPPNet 最主要的改进体现在哪里?它解决了 R-CNN 的哪些痛点?


📝 本节课总结

本节课中,我们一起学习了 SPPNet 的完整架构,明确了其通过共享卷积计算和引入空间金字塔池化层来提升 R-CNN 效率的核心机制。我们分析了 SPPNet 在减少计算量避免图像形变方面的优点,同时也指出了它在训练流程端到端学习方面存在的局限。理解 SPPNet 的这些特点,为我们后续学习更先进的检测模型奠定了重要基础。

课程P2:项目结构安排 🏗️

在本节课中,我们将学习整个项目的宏观结构。我们将了解项目从数据准备到最终用户交互的完整流程,并明确后续学习的三个阶段。

项目整体架构概述

上一节我们介绍了项目的背景,本节中我们来看看项目的整体架构。整个流程主要分为三个层次:数据采集层、深度模型层和用户交互层。下图清晰地展示了各层之间的关系与数据流向。

对于这张图,我们目前只需要达到一个整体了解的程度,知道整个过程即可。后续课程会针对每个部分的设计进行详细讲解。

架构详解:三层结构

接下来,我们详细解析架构图中的三个核心部分。

1. 数据采集层 📥

数据采集层负责准备训练所需的数据。以下是该层的主要工作:

  • 数据采集:收集大量的图片数据。
  • 数据标注:为收集到的图片数据进行标注。在深度学习中,训练模型需要已标注的数据,例如为图片标记其所属的类别。
  • 数据存储:将标注好的数据和对应的图片数据存储到本地磁盘中,为后续训练做好准备。

2. 深度模型层 🤖

深度模型层是项目的核心,负责模型的训练与部署。以下是该层的关键步骤:

  • 数据预处理:将存储的数据输入模型层前,先进行必要的预处理。
  • 模型训练:将处理好的图片数据输入到算法模型中进行训练。本项目会使用多GPU进行训练,以加速过程。
  • 生成模型:训练的目的是得到一个可用的模型文件。
  • 模型部署:生成的模型文件需要部署到 TensorFlow Serving 服务器上,以便提供预测服务。
  • 提供接口:部署后,我们会搭建一个简单的Web后台(本项目重点不在此),该后台提供接口,用于接收输入并返回模型的预测结果。

3. 用户交互层 👥

用户交互层是项目的最终呈现部分。以下是其工作方式:

  • 接口调用:用户通过前端(如网页或小程序)进行交互,调用Web后台提供的接口。
  • 输入与输出:用户通过接口输入一张图片,接口则返回模型的预测或识别结果。

项目核心流程总结

现在,我们把刚才介绍的三层结构及其职责总结一下。

  • 数据采集层:核心工作是数据标注与决定数据存储格式
  • 深度模型层:核心工作包括数据预处理多GPU训练得到模型,并使用 TensorFlow Serving 进行模型部署,最终为Web后台提供预测接口。
  • 用户交互层:用户通过网页或小程序等前端,调用接口获取预测或识别结果

这就是项目的整体架构,大家目前对此有一个简单的了解即可。

项目学习三阶段安排 📅

了解了项目结构后,我们来看一下整个课程将如何分阶段展开讲解。项目学习分为以下三个阶段:

以下是三个学习阶段的具体内容:

  1. 第一阶段:算法模型
    在介绍相关背景知识后,我们将首先深入讲解核心的算法模型。这是项目最重要的部分,我们将学习如R-CNN、SSD等当前效果优秀的识别算法。

  2. 第二阶段:数据处理
    掌握了算法原理后,第二阶段将重点处理数据。这包括数据的标记、存储格式的决定与管理等。

  1. 第三阶段:项目实现
    了解了前两个模块后,第三阶段将进行完整的项目实现。这包括:
    • 数据接口的实现
    • 模型接口的实现
    • 训练模块的逻辑实现
    • 测试接口的实现
    • TensorFlow Serving服务端的部署
    • Web服务器与客户端的编写
    • 小程序的最终集成与测试

本节课中我们一起学习了项目的三层整体架构(数据采集层、深度模型层、用户交互层)以及课程后续将遵循的三个学习阶段(算法模型、数据处理、项目实现)。这为我们后续的深入学习奠定了清晰的路线图。

课程P20:Fast R-CNN详解 🚀

在本节课中,我们将学习Fast R-CNN算法。我们将了解其核心改进点、网络结构流程,并理解它如何解决其前身SPP-Net所面临的问题。


上一节我们介绍了SPP-Net,它的性能已得到较大改善,卷积计算无需在多个区域重复进行。但它存在一个显著问题:网络各部分无法统一训练,且需要大量磁盘空间来存储特征图。Fast R-CNN正是为了解决这些问题而提出的。

核心改进点 ✨

Fast R-CNN的主要改进体现在以下两个方面:

  1. 提出了ROI Pooling层:该层整合了整个模型,将SPP变换、分类器和边界框回归统一到一个网络中,实现端到端的训练,使网络结构更加紧凑。
  2. 将SVM分类器替换为Softmax分类器:这使得分类任务能够与边界框回归任务在同一个网络中联合训练。

网络流程详解 🔄

以下是Fast R-CNN的整体工作步骤,我们可以对照流程图来理解:

  1. 特征提取:将整张图片输入卷积神经网络,得到特征图(Feature Map)。
  2. 候选区域映射:提取候选区域(Region Proposals),并将它们映射到上一步得到的特征图上。
  3. ROI Pooling:对特征图上的每一个候选区域(也称为ROI,Region of Interest)进行ROI Pooling操作,将其转换为固定长度的特征向量。
  4. 全连接与输出:将固定长度的特征向量输入全连接层,最终并行输出两个结果:
    • 一个通过Softmax分类器,输出K个目标类别加上一个背景类。
    • 另一个通过边界框回归器(Bounding Box Regression),对候选区域的位置进行精修。

关键概念解析 🔍

上一节我们概述了流程,本节我们来深入看看其中的两个核心概念。

ROI Pooling

ROI Pooling是SPP(Spatial Pyramid Pooling)层的简化版本。它的目的是将任意大小的ROI区域特征,转换为固定尺寸(例如 7x7)的特征图,以便后续的全连接层处理。

其操作可以简化为以下两个步骤:

  1. 根据ROI在特征图上的坐标,划分出对应的区域。
  2. 将该区域均匀划分成 H x W 个网格(如 7x7),并对每个网格内的值进行最大池化(Max Pooling)

代码描述其核心思想

# 伪代码示意
output = []
for grid in divided_ROI:
    pooled_value = max(grid)  # 对每个小网格取最大值
    output.append(pooled_value)
return fixed_size_output  # 例如 7x7 的向量

多任务损失(Multi-task Loss)

Fast R-CNN使用一个多任务损失函数来同时优化分类和边界框回归,这是实现端到端训练的关键。

总损失函数 L 由两部分加权求和构成:
L(p, u, t^u, v) = L_cls(p, u) + λ[u ≥ 1] L_loc(t^u, v)

  • L_cls(p, u) 是分类损失,通常是对数损失(Log Loss),用于衡量预测类别概率 p 与真实类别 u 的差异。
  • L_loc(t^u, v) 是定位损失,用于衡量预测的边界框修正参数 t^u 与真实修正参数 v 的差异,通常使用平滑L1损失(Smooth L1 Loss)。
  • λ 是一个平衡两个任务权重的超参数。[u ≥ 1] 是一个指示函数,当 u 为背景类(u=0)时,定位损失为0。

本节课中,我们一起学习了Fast R-CNN算法。我们了解到它通过引入ROI Pooling层多任务损失函数,成功地将特征提取、区域分类和边界框回归整合到一个统一的、可端到端训练的网络中,显著提升了训练效率和模型性能,并解决了SPP-Net模型训练不统一和存储开销大的问题。

课程 P21:Fast R-CNN 中的 RoI Pooling 结构及其与 SPP 的对比 🧠

在本节课中,我们将要学习 Fast R-CNN 模型中的一个核心组件——RoI Pooling(感兴趣区域池化)。我们将详细探讨它的工作原理、设计目的,并重点对比它与 SPP(空间金字塔池化)层的区别,理解为何 Fast R-CNN 选择了 RoI Pooling 来提升模型效率。

概述:RoI Pooling 的设计目的

RoI Pooling 可以看作是 SPP 层的一个简化版本。它的主要目的有两个:

  1. 减少计算时间
  2. 为后续的全连接层生成固定长度的特征向量

上一节我们介绍了 SPP 层的基本概念,本节中我们来看看它的简化版——RoI Pooling 是如何具体实现的。

RoI Pooling 与 SPP 的结构对比

通过对比两张结构图,可以清晰地理解两者的区别。

SPP层采用了一种金字塔式的分块策略。例如,它会将特征图同时划分成 4x42x21x1 等多种尺度的网格,然后将每个网格内的特征进行池化(如最大池化),最后将所有结果拼接起来。

RoI Pooling层则只采用单个固定尺度的网格进行划分。例如,图中展示的是 4x4 的网格,但这个大小并非固定,它是一个可设置的超参数,我们通常将其记为 K x M

因此,RoI Pooling 层的作用可以总结为:将任意大小的感兴趣区域(RoI)内的特征,通过池化操作,转换为一个具有固定空间大小(H x W)的特征向量。这里的 HW 就是超参数,独立于任何具体的 RoI 尺寸。

为何选择单尺度(Single Scale)?

一个自然的问题是:既然 SPP 的多尺度(Multi-Scale)结构可能捕捉更丰富的特征,为什么 Fast R-CNN 要选择单尺度的 RoI Pooling 呢?

以下是两者的优缺点分析:

  • 单尺度(Single Scale):直接将图像区域调整到单一尺度输入网络。其优点是实现简单、计算速度快
  • 多尺度(Multi-Scale):构建图像金字塔,在不同尺度上提取特征。其优点是可能获得更细致、更准确的特征表示

然而,在算法设计中常常需要在速度和精度之间进行权衡。实验表明,多尺度方法相比单尺度方法,在精度上的提升并不显著,但却会大幅增加计算时间。因此,为了追求更快的检测速度,Fast R-CNN 选择了单尺度的策略,即 RoI Pooling。

核心优化点:Fast R-CNN 通过采用 RoI Pooling(单尺度),在精度损失很小的情况下,显著提升了检测速度,实现了速度与精度的高效平衡。

处理尺寸不匹配:动态调整机制

在实际操作中,RoI 的尺寸可能与全连接层要求的固定输入尺寸(例如 7x7)不匹配。RoI Pooling 通过一种动态调整机制来解决这个问题。

假设全连接层要求输入特征图大小为 7x7,但某个 RoI 经过卷积后得到的特征网格只有 6x6。处理过程如下:

  1. 计算缩放比例:将 RoI 的宽和高分别除以目标尺寸。例如,6 / 7 ≈ 0.85
  2. 重新划分网格:按照这个比例(0.85),将原来的 6x6 网格动态地、近似地划分成 7x7 个区间。
  3. 区间内池化:在每个新划分出的区间内执行最大池化操作,最终得到一个严格的 7x7 输出。

公式描述
若目标池化大小为 H x W,RoI 特征区域大小为 h x w,则每个输出网格在输入上对应的高约为 h/H,宽约为 w/W

这个过程确保了无论输入的 RoI 尺寸如何,都能输出固定大小的特征图,以满足全连接层的要求。

操作过程可视化

下图清晰地展示了 RoI Pooling 的整个过程:

  1. 模型首先提出一个候选区域(RoI)。
  2. 将该区域对应的特征图部分划分成预设的网格(例如 2x2)。
  3. 对每个网格内的值进行最大池化(Max Pooling)。
  4. 将池化后的结果组合起来,形成最终的、固定大小的输出特征。

总结

本节课中我们一起学习了 Fast R-CNN 的关键结构——RoI Pooling。

  • 核心目的:为加速计算并生成固定长度的特征向量。
  • 与 SPP 的区别:RoI Pooling 采用单尺度(如 K x M)池化,而 SPP 采用多尺度金字塔池化。这是为了在精度与速度之间取得更好平衡。
  • 关键机制:通过动态调整网格划分来解决 RoI 尺寸与全连接层要求不匹配的问题。
  • 最终效果:RoI Pooling 是 Fast R-CNN 实现高效目标检测的重要优化步骤之一,它简化了流程,大幅提升了模型的训练和检测速度。

课程 P22:Fast R-CNN 多任务损失 🎯

在本节课中,我们将学习 Fast R-CNN 模型的核心改进之一:多任务损失。我们将了解它如何通过将 SVM 分类器替换为 Softmax,并结合边界框回归,实现端到端的训练,从而提升模型的效率和性能。


上一节我们介绍了 ROI Pooling 的完整过程。本节中,我们来看看另一个关键的改进点:将分类器从 SVM 替换为 Softmax。

这个改进与 ROI Pooling 相结合,带来了显著的好处。Fast R-CNN 因此可以被视为一种 端到端模型

在之前的算法分类中,我们提到过“两步走”和“端到端”两种范式。像 YOLO 和 SSD 是典型的端到端模型。虽然 Fast R-CNN 和 Faster R-CNN 不完全算完整的端到端,但在此处,Fast R-CNN 也可以类似地被称为端到端。

端到端模型是指从输入端到输出端直接由一个网络相连,可以进行整体优化。这意味着整个网络可以放在一起训练,无需单独训练多个模型。那么 Fast R-CNN 是如何做到的呢?

首先,在 R-CNN 中,卷积特征提取和 SVM 分类器的训练是必须分开、独立进行的。SVM 的特征需要存储在内存或磁盘中。

而对于 Fast R-CNN 来说,它使用了 Softmax 进行分类。Softmax 函数自然可以与网络一起训练。同时,ROI Pooling 层能够进行反向传播。相比之下,前代模型 SPP-Net 中的 SPP 层不太适合反向传播。关于这一点有详细的数学推导,我们在此不深入展开,只需记住结论:ROI Pooling 支持反向传播,而 SPP 层不太适合。

因此,Fast R-CNN 可以被视为一个端到端模型。在训练时,衡量损失就变得简单了:Softmax 用于计算分类损失,回归器用于计算边界框回归损失,然后一起进行反向传播,从网络后端一直更新到前端的卷积层参数。

所以,我们的损失函数由两部分组成。

以下是分类损失和回归损失的详细说明:

1. 分类损失

  • 使用一个 N+1 路的 Softmax 输出。其中 N 代表目标类别数(例如 20 个类别),加上的 1 代表“背景”类别
  • 这是因为 Region Proposal 推荐的区域有可能不包含任何有效目标,需要被标记为“背景”。
  • 最终使用 交叉熵损失 来计算分类误差。

2. 回归损失

  • 每个类别 训练一个独立的边界框回归器。例如,针对“猫”类别训练一个回归器,针对“狗”类别训练另一个回归器。
  • 这样可以使每个类别对应的候选框预测得更准确。
  • 这里使用 平均绝对误差,即 L1 损失。这与 R-CNN 中使用的均方误差不同。

这个多任务损失使得在微调网络时,卷积层、ROI Pooling 层、Softmax 分类器和边界框回归器可以一起进行训练。唯一独立的部分是 Region Proposal 的生成过程(仍使用 Selective Search 等方法)。

这就是 Fast R-CNN 的多任务损失。Fast R-CNN 在 SPP-Net 的基础上做出了这些改进,那么这三个算法的性能对比如何呢?

以下是 R-CNN、SPP-Net 和 Fast R-CNN 在训练时间、测试时间和精确度上的对比(基于 PASCAL VOC 2007 数据集):

  • 训练时间:R-CNN 需 84 小时,SPP-Net 需 25 小时,而 Fast R-CNN 仅需 9.5 小时
  • 测试时间(每张图):R-CNN 需 47 秒,SPP-Net 需 2.3 秒,而 Fast R-CNN 仅需 0.32 秒。这个速度已经让用户几乎感觉不到等待。
  • 准确度(mAP):R-CNN 为 66.0%, SPP-Net 为 63.1%, Fast R-CNN 为 66.9%。总体而言,Fast R-CNN 在速度大幅提升的同时,保持了较高的准确度。


总结 📝

本节课中,我们一起学习了 Fast R-CNN 的多任务损失机制。核心要点包括:

  1. 将 SVM 分类器改为 Softmax 分类器,输出为 N+1 路(N个类别 + 1个背景)。
  2. Softmax 分类损失与边界框回归的 L1 损失 共同构成多任务损失。
  3. 得益于 ROI Pooling 支持反向传播,整个网络(除 Region Proposal 生成外)可以实现端到端的联合训练
  4. 与 R-CNN 和 SPP-Net 相比,Fast R-CNN 在训练和测试速度上取得了数量级的提升,同时保持了优秀的检测精度。

课程 P23:Fast R-CNN 总结与问题自测 🧠

在本节课中,我们将对 Fast R-CNN 模型进行总结,回顾其核心流程,分析其存在的缺点,并梳理需要掌握的关键知识点。最后,我们将通过几个自测问题来检验学习成果。


模型流程总结 📊

上一节我们详细介绍了 Fast R-CNN 的各个组件,本节中我们来看看它的整体工作流程。

Fast R-CNN 的整体流程可以概括为以下几个步骤:

  1. 特征提取:输入图像首先通过一个卷积神经网络,生成一张共享的特征图。
  2. 区域映射:通过 Selective Search 等方法生成的候选区域被映射到上一步得到的特征图上。
  3. 区域特征统一:每个映射后的候选区域通过 ROI Pooling 层,被转换为固定尺寸的特征向量。
  4. 分类与回归:这些固定尺寸的特征向量被送入全连接层。网络最终输出两个部分:
    • 通过 Softmax 函数得到的目标类别概率。
    • 通过线性回归得到的边界框坐标偏移量。
  5. 联合训练:分类损失和边界框回归损失共同构成多任务损失函数,反向传播以更新整个网络的参数(区域推荐步骤除外)。

其核心流程可以用以下伪代码描述:

# 伪代码流程
feature_map = CNN(input_image)
rois = selective_search(input_image) # 外部算法
pooled_features = ROI_Pooling(feature_map, rois)
cls_score, bbox_pred = FC_Layers(pooled_features)
loss = classification_loss(cls_score) + regression_loss(bbox_pred)


模型缺点分析 ⚠️

了解了 Fast R-CNN 的优势后,我们来看看它仍然存在的不足之处。

Fast R-CNN 最主要的缺点集中在区域推荐阶段:

  • 非完全端到端:模型依赖外部的 Selective Search 算法来生成候选区域。这一步计算耗时,且没有与网络的主体部分进行联合训练,因此不能称为真正意义上的端到端学习。
  • 速度瓶颈:Selective Search 本身是一个计算密集型的算法,成为整个检测流程的速度瓶颈。

因此,后续算法的改进方向非常明确:将区域推荐步骤也集成到神经网络内部,实现完全的端到端训练与推理。


核心知识点梳理 📝

以下是关于 Fast R-CNN 必须掌握的三个核心改进点及其细节。

1. ROI Pooling 的作用
ROI Pooling 层的主要作用是将任意大小的候选区域特征图池化为固定大小(如 7x7)的输出。其关键意义在于允许梯度通过该层进行反向传播,从而能够训练之前的卷积层。该层的输出尺寸 (pooled_height, pooled_width) 是一个可调的超参数。

2. 多任务损失函数
Fast R-CNN 使用一个多任务损失函数进行联合训练,公式如下:
L = L_cls + λ * L_loc
其中:

  • L_cls 是分类损失,通常为 Softmax 交叉熵损失
  • L_loc 是边界框回归损失,采用平滑 L1 损失。对于每个候选区域,计算其预测边界框与真实边界框坐标的差异。损失值近似于预测偏移量与真实偏移量之差的绝对值总和再取平均。

3. 主要改进总结
Fast R-CNN 相较于 R-CNN 的核心改进在于:

  • 引入了 ROI Pooling,实现了对整张图像特征的一次性提取和共享。
  • 将分类和边界框回归任务合并,使用多任务损失进行端到端的训练(除区域推荐外),大幅提升了训练和测试速度。

学习成果自测 ❓

为了巩固对本节课内容的理解,请尝试回答以下问题:

  1. 请简述 ROI Pooling 的操作过程及其在 Fast R-CNN 中的重要性。
  2. Fast R-CNN 的损失函数由哪两部分构成?分别对应什么任务?
  3. Fast R-CNN 最主要的缺点是什么?后续算法会如何改进它?

课程总结 🎯

本节课中,我们一起学习了 Fast R-CNN 的完整流程。我们首先回顾了其从特征提取到多任务输出的工作步骤,然后指出了其依赖外部 Selective Search 算法这一核心缺点。最后,我们梳理了必须掌握的三个关键知识点:ROI Pooling 的原理与作用、多任务损失函数的构成,以及模型的主要改进点。理解这些内容,是学习后续更先进的物体检测模型(如 Faster R-CNN)的重要基础。

课程 P24:Faster R-CNN 网络结构与步骤详解 🚀

在本节课中,我们将要学习 Faster R-CNN 的核心思想、网络结构以及其工作流程。我们将重点探讨它是如何改进 Fast R-CNN 的,并深入理解其关键组件——区域生成网络(RPN)的工作原理。

概述

Faster R-CNN 是目标检测领域的一个重要模型。上一节我们介绍了 Fast R-CNN,它通过共享卷积特征和引入 ROI Pooling 层提升了效率。然而,Fast R-CNN 仍然依赖外部算法(如选择性搜索)来生成候选区域,这个过程非常耗时。本节中我们来看看 Faster R-CNN 是如何将候选区域生成也整合进神经网络,从而形成一个端到端的、更快速的目标检测框架的。

Faster R-CNN 的改进与结构

Faster R-CNN 的主要改进在于去除了耗时的选择性搜索(Selective Search)步骤。它通过引入一个名为区域生成网络(Region Proposal Network, RPN)的组件,将候选区域生成任务也交由神经网络完成。

我们可以将 Faster R-CNN 视为一个由 RPN 网络Fast R-CNN 网络 组成的统一模型。其核心思想是将候选框的生成过程也融入到端到端的训练中。

以下是 Faster R-CNN 与之前模型的对比:

  • R-CNN:特征提取、SVM分类、边界框回归是分离的模块。
  • Fast R-CNN:将特征提取后的分类和回归整合进一个网络,但候选区域仍需外部生成。
  • Faster R-CNN:将候选区域生成(RPN)、特征提取、分类和回归全部整合到一个统一的深度网络中。

Faster R-CNN 工作流程详解

接下来,我们对照结构图,详细拆解 Faster R-CNN 的四个核心步骤。

第一步:特征提取

输入一张任意大小的图片。图片经过一个基础的卷积神经网络(例如 VGG-16 的 13 个卷积层、13 个 ReLU 层和 4 个池化层),输出一个高维的特征图(Feature Map)。这个特征图包含了图像的抽象信息,并将在后续步骤中被共享使用。

第二步:生成候选区域(RPN)

这是 Faster R-CNN 的创新核心。上一步得到的特征图被输入到 区域生成网络(RPN) 中。

RPN 的核心任务是:在特征图的每个位置上,同时预测该位置是否存在目标(物体/背景)以及生成一系列可能包含目标的边界框(即候选区域,Region Proposals)。它取代了 Fast R-CNN 中耗时的选择性搜索。

RPN 的具体工作原理我们将在下一小节详细展开。

第三步:特征图与候选区域结合(ROI Pooling)

第二步 RPN 会输出一系列候选区域(Proposals)。同时,第一步生成的特征图也被共享到这一步。

这个过程与 Fast R-CNN 相同:将每个候选区域映射回第一步生成的特征图上,然后通过 ROI Pooling 层 从特征图中提取出固定大小的特征向量。这样,每个形状不一的候选区域都被转化为统一尺寸的特征表示,便于后续处理。

第四步:分类与精修

最后,每个经过 ROI Pooling 的固定尺寸特征向量被送入两个并行的全连接层:

  1. Softmax 分类层:判断该候选区域属于哪个具体类别(如人、车、猫等)或是背景。
  2. 边界框回归层(Bounding Box Regression):对候选区域的边界框坐标进行微调,使其更精确地贴合真实目标。

以下是整个流程的步骤总结:

  1. 输入任意大小图片,经过 CNN 网络输出特征图。
  2. 特征图经过 RPN 网络,生成候选区域。
  3. 将候选区域与特征图共同输入到 ROI Pooling 层,得到每个区域的固定尺寸特征图。
  4. 对特征图进行 Softmax 分类和 Bounding Box 回归,得到最终检测结果。

核心:区域生成网络(RPN)原理

上一节我们介绍了 Faster R-CNN 的整体流程,本节中我们重点来看看其核心——RPN 网络是如何生成候选区域的。

RPN 的设计非常巧妙。它在特征图上滑动一个小的网络(例如 3x3 的卷积核),这个滑动窗口在特征图的每个位置上都会输出一个特征向量。

对于这个位置的特征向量,RPN 会同时进行两项任务:

  1. 二分类(物体/背景):通过一个 1x1 卷积层判断该“锚点”位置是前景(包含物体)还是背景。公式可以简化为 cls_score = Conv1x1(feature_vector)
  2. 边界框回归:通过另一个 1x1 卷积层预测基于该位置的一系列“锚框(Anchor Boxes)”的偏移量(dx, dy, dw, dh),以调整锚框更接近真实物体。公式可简化为 bbox_pred = Conv1x1(feature_vector)

锚框(Anchor) 是预定义在特征图每个位置上的、具有不同尺度和长宽比的基准框。例如,在每个位置设置 3 种尺度(128, 256, 512)和 3 种长宽比(1:1, 1:2, 2:1),就会得到 k=9 个锚框。RPN 的任务就是判断这些锚框中哪些可能包含物体,并对其坐标进行初步调整。

以下是 RPN 生成候选区域的关键步骤列表:

  • 生成锚框:在特征图的每个滑动窗口中心,生成 k 个不同尺度和比例的锚框。
  • 计算预测:对每个锚框,通过 RPN 网络计算它是前景的概率以及坐标修正值。
  • 筛选提案:根据前景概率对所有锚框进行排序,选取概率最高的前 N 个,并利用回归值修正其坐标,形成初步的候选区域(Proposals)。
  • 去重优化:使用非极大值抑制(NMS)去除高度重叠的候选框,得到最终输送给后续 Fast R-CNN 网络的候选区域列表。

通过这种方式,RPN 能够快速、高效地从深度特征中直接生成高质量的候选区域,并与检测网络共享卷积特征,实现了端到端的训练和显著的速度提升。

总结

本节课中我们一起学习了 Faster R-CNN 的网络结构与工作步骤。我们了解到它的核心贡献是引入了区域生成网络(RPN),将耗时的候选区域生成过程也整合到神经网络中,与特征提取、目标分类和边界框回归共同构成了一个统一的、端到端的目标检测框架。这使得 Faster R-CNN 在保持高精度的同时,速度比 Fast R-CNN 有了质的飞跃,成为目标检测发展史上的一个里程碑模型。

🎯 课程 P25:Faster R-CNN 中的 RPN 网络原理

在本节课中,我们将学习 Faster R-CNN 模型的核心组件之一——区域生成网络(RPN)。我们将详细探讨 RPN 如何从特征图中生成候选区域,以及它是如何通过训练来优化这些区域的。


📖 概述

RPN 的主要作用是生成高质量的候选区域(Proposals)。它接收卷积神经网络提取的特征图作为输入,输出一系列可能包含物体的候选框,并对这些候选框进行初步的分类(前景/背景)和位置修正。


🔍 RPN 的工作原理

RPN 的工作流程可以分为两个主要步骤。

第一步:生成锚框(Anchors)

首先,RPN 使用一个 3×3 的滑动窗口在输入的特征图上进行扫描。对于滑动窗口中心的每一个像素点,都会生成 K 个不同尺度和长宽比的锚框。在原始的 Faster R-CNN 论文中,K 默认为 9。

以下是锚框生成的代码示意:

# 假设特征图尺寸为 H x W
# 对于特征图上的每个位置 (i, j)
for i in range(H):
    for j in range(W):
        # 以该位置为中心,生成 K 个锚框
        anchors = generate_anchors(center=(i, j), scales=[128, 256, 512], ratios=[1:1, 1:2, 2:1])

锚框的尺度和长宽比是预先定义的:

  • 尺度:128×128, 256×256, 512×512
  • 长宽比:1:1, 1:2, 2:1
    这 3 种尺度和 3 种长宽比组合,共得到 9 种锚框。

如果输入特征图的尺寸是 51×39,那么总共会生成 51 × 39 × 9 个锚框。

第二步:锚框的修正与分类

对于生成的每一个锚框,RPN 会通过两个并行的全连接层进行处理:

  1. 回归层(reg layer):输出 4K 个值,用于修正锚框的位置(中心点坐标 x, y 和宽高 w, h),使其更接近真实物体框。
  2. 分类层(cls layer):输出 2K 个分数,表示每个锚框是“前景”(包含物体)或“背景”的概率。这是一个二分类问题。

公式表示为:
对于每个锚框,回归层输出:(Δx, Δy, Δw, Δh)
对于每个锚框,分类层输出:P(前景), P(背景)


🏷️ RPN 的训练样本标记

RPN 本身是一个需要训练的网络。为了训练它,我们需要为生成的成千上万个锚框标记“正样本”(包含物体)和“负样本”(背景)。

以下是训练样本的标记规则:

  1. 与真实框(Ground Truth Box)重叠度最高的锚框:对于每个真实物体框,与其 IoU(交并比)最高的那个锚框被标记为正样本。
  2. 高重叠度锚框:任何与某个真实框的 IoU 大于 0.7 的锚框,都被标记为正样本。
  3. 低重叠度锚框:与所有真实框的 IoU 都小于 0.3 的锚框,被标记为负样本。
  4. 忽略的锚框:IoU 在 0.3 到 0.7 之间的锚框,在训练时被忽略,不参与损失计算。

这样标记的目的是确保正样本是与真实物体高度相关的锚框,而负样本是明确的背景区域。


🔄 RPN 在 Faster R-CNN 中的角色

上一节我们介绍了 RPN 如何生成和初步筛选锚框。现在,我们来看看它在整个 Faster R-CNN 流程中的作用。

训练好的 RPN 网络可以看作是一个“海选”系统:

  • 输入:任意图像经过主干网络(如 VGG16)提取的特征图。
  • 处理:RPN 快速扫描特征图,生成大量锚框,并预测每个锚框包含物体的概率以及更精确的位置。
  • 输出:RPN 会筛选出概率较高、位置较优的锚框,作为“候选区域”输出给后续的 Fast R-CNN 网络进行精细处理。

而后续的 Fast R-CNN 网络则负责:

  • 精细分类:对 RPN 提出的每个候选区域进行 N+1 类分类(例如 20个物体类 + 1个背景类),而不仅仅是二分类。
  • 最终回归:对候选区域的位置进行最终的微调,得到检测框。

📝 总结

本节课我们一起学习了 Faster R-CNN 中区域生成网络(RPN)的原理。我们了解到:

  1. RPN 通过滑动窗口和预定义的锚框,从特征图上生成大量候选区域。
  2. RPN 通过一个二分类网络和一个回归网络,对这些锚框进行初步筛选和位置修正。
  3. RPN 的训练依赖于根据与真实框的 IoU 来精心标记正负样本。
  4. RPN 的核心作用是替代了传统的选择性搜索(Selective Search)等方法,高效、高质量地生成候选区域,并与检测网络共享卷积特征,从而大幅提升检测速度。

理解 RPN 是掌握 Faster R-CNN 的关键,它巧妙地将候选区域生成这一步骤集成到神经网络中,实现了端到端的物体检测。

课程P26:Faster R-CNN总结与问题自测 🧠

在本节课中,我们将对Faster R-CNN模型进行总结,回顾其核心改进、优缺点,并设置自测问题以巩固理解。


模型性能对比 📊

上一节我们介绍了Faster R-CNN的架构,本节中我们来看看它的实际性能表现。

下图展示了Faster R-CNN与之前模型的对比结果。

以下是各模型在测试速度和准确率上的对比数据:

  • R-CNN:速度较慢,此处未给出具体数值。
  • Fast R-CNN:测试速度约为 2.0秒,准确率为 66.9%。请注意,此处使用的数据集可能与之前讨论的不同。
  • Faster R-CNN:速度相比Fast R-CNN有显著提升,准确率保持在 66.9% 左右。

通过对比可以看出,Faster R-CNN的主要优势在于速度。它在保持高准确率的同时,大幅提升了训练和测试的速度。


Faster R-CNN 优缺点总结 ⚖️

了解了性能对比后,我们来系统总结一下Faster R-CNN的优缺点。

优点 ✅

Faster R-CNN的核心优点是提出了区域提议网络。该网络能够自主训练并生成候选区域,这可以被理解为一个端到端的网络模型。

用代码概念描述其核心思想是:

# RPN 替代了传统的外部候选区域生成方法(如Selective Search)
proposals = RPN(feature_map)  # 直接从特征图生成候选框

缺点 ❌

然而,Faster R-CNN也存在明显的缺点:训练参数过大。对于实际的工业级训练任务而言,使用它依然过于耗时。这意味着,如果用企业场景的真实数据训练Faster R-CNN,可能需要等待非常长的时间。


改进需求与后续发展 🚀

正因为Faster R-CNN在速度上仍有不足,业界产生了进一步的改进需求。

对于RPN网络本身,它在处理小目标时可能存在过滤问题。但更主要的改进方向是追求更快的速度。尽管Faster R-CNN已经提速,但对许多应用来说还是不够快。

这就催生了YOLO系列等算法。这些算法的一个关键改进是直接删除了RPN网络,不再需要独立的候选区域生成步骤,而是直接对图像进行边界框的分类与回归预测,从而实现了速度的飞跃。


理解“参数过大”与RPN原理 🔍

那么,如何具体理解“训练参数过大”这个问题呢?

这主要与RPN生成的候选区域数量过多有关。我们可以参考下图进行分析。

假设特征图大小为 51 × 39,每个位置预设 9 个不同尺度和长宽比的锚点,那么生成的候选框总数就是 51 × 39 × 9。这个庞大的数量是导致计算量大的原因之一。

本部分课程最重要的内容就是理解RPN的原理与过程。可以说,掌握了RPN,就掌握了Faster R-CNN的改进精髓。


课程总结与问题自测 📝

本节课中,我们一起学习了Faster R-CNN的总结。我们对比了其性能,分析了它以RPN为核心的优点,也指出了其训练耗时的缺点,并了解了后续算法为提升速度所做的改进。

为了巩固学习,请尝试回答以下自测问题:

  1. Faster R-CNN相对于Fast R-CNN的主要改进之处是什么?
  2. RPN中的锚点是如何得到的?
  3. RPN网络的训练过程是怎样的?

回答这些问题,将帮助你清晰地掌握Faster R-CNN的改进核心。

课程P27:YOLO算法特点与流程介绍 🎯

在本节课中,我们将要学习YOLO(You Only Look Once)算法的核心特点与基本流程。YOLO是一种著名的目标检测算法,以其“端到端”的设计和极快的处理速度而闻名。我们将了解其网络结构、工作原理以及它为何能在速度上取得显著优势。

算法特点:速度与精度的权衡 ⚖️

上一节我们介绍了其他目标检测算法,本节中我们来看看YOLO的独特之处。YOLO的全称是“You Only Look Once”,意为“只看一次”,这直观地体现了其追求快速和便捷的设计理念。

为了理解YOLO的特点,我们可以参考算法性能对比图。图中纵坐标表示精确度,横坐标表示算法出现的时间。重点关注Faster R-CNN和YOLO等算法。

以下是几个关键观察点:

  • FPS(每秒帧数):FPS值体现了算法的运行速度。YOLO的FPS值显著高于Faster R-CNN,说明其速度非常快。
  • mAP(平均精度均值):mAP值体现了算法的检测准确度。Faster R-CNN的mAP值通常更高。相比之下,YOLO的准确度会有所降低。

因此,YOLO的核心特点是速度特别快,但准确率会有所折衷。这为需要实时处理的应用场景提供了重要选择。

网络结构:简洁的端到端设计 🏗️

了解了YOLO的速度优势后,本节我们来看看其简洁的网络结构是如何实现这一点的。YOLO采用单一网络完成所有任务,无需像Faster R-CNN那样使用区域生成网络(RPN)加检测网络的两阶段设计。

其网络结构可以概括为:输入图片 -> 卷积神经网络(如GoogleNet加若干卷积层)-> 全连接层 -> 输出特征图。整个过程在一个网络内完成,结构非常简单。

工作流程:从单元格到预测 📊

那么,这个简单的网络是如何进行预测的呢?接下来我们详细解析YOLO的工作流程。

整个流程可以分为以下几步:

  1. 输入处理:将原始图片缩放至固定尺寸,例如448x448像素。
  2. 特征提取与输出:图片经过卷积网络后,输出一个 7x7x30 的张量。为了便于理解,我们可以将这个7x7的网格视为将图片划分成了49个单元格。
  3. 单元格预测每个单元格负责预测两个边界框(Bounding Box) 以及这些框的置信度和类别概率。
  4. 结果生成:对所有单元格的预测结果进行筛选(如非极大值抑制,NMS),最终得到检测结果。

关键在于,YOLO摒弃了显式的候选区域筛选步骤,直接让每个单元格进行预测,从而实现了流程的极大简化与加速。

核心概念与表示 🔑

以下是YOLO算法中的几个核心概念及其表示方式:

  • 网络输出:算法最终输出一个三维张量,其尺寸为 S x S x (B*5 + C)

    • S x S:代表将图像划分成的网格数(如7x7)。
    • B:每个网格预测的边界框数量(如2)。
    • 5:每个边界框的预测值,包括中心坐标(x, y)、宽高(w, h)和1个置信度(confidence)。
    • C:待检测的类别数量。
    • 因此,对于PASCAL VOC数据集(20类),输出为 7x7x(2*5+20)=7x7x30
  • 置信度计算:边界框的置信度(Confidence)反映了框内包含目标且位置准确的程度。其公式为:
    Confidence = Pr(Object) * IOUᵗʳᵘᵗʰₚᵣₑᵈ
    其中,Pr(Object)表示该单元格是否存在目标,IOU(交并比)是预测框与真实框的重叠程度。

总结 📝

本节课中,我们一起学习了YOLO v1算法的核心内容。

我们首先了解了YOLO追求高速、在精度上有所妥协的算法特点。然后,剖析了其简洁的端到端网络结构,它通过单一网络直接输出检测结果,省去了候选区域生成步骤。最后,我们逐步拆解了YOLO的工作流程:从图像输入、网格划分、单元格预测到最终结果输出,并理解了其输出张量 7x7x30 的具体含义。

总而言之,YOLO通过将目标检测重构为单一的回归问题,实现了惊人的检测速度,为实时目标检测奠定了基础。

课程 P28:YOLO 单元格原理详解 🧱

在本节课中,我们将深入探讨 YOLO 算法中一个核心概念——单元格。我们将详细解释 YOLO 网络输出的 7x7x30 张量如何被理解,以及每个单元格如何负责预测物体类别和边界框。


单元格概念引入

上一节我们介绍了 YOLO 网络的基本结构。本节中,我们来看看其输出 7x7x30 的具体含义。为此,我们引入“单元格”的概念。

我们可以将 7x7 理解为 49 个像素值。这 49 个像素值可以看作是原始图像被划分成的 49 个单元格。每个像素代表一个单元格。

每个单元格需要完成两件事:

  1. 每个单元格只负责预测一个物体类别,并直接预测该物体的概率。
  2. 每个单元格会预设两个边界框的位置,每个边界框附带一个置信度。

因此,总共有 49 x 2 = 98 个边界框。


30维向量的拆解

接下来,我们详细拆解每个单元格对应的 30 维向量。

每个单元格(即每个像素位置)对应一个 30 维的向量。这 30 个值的构成如下:

以下是 30 维向量的具体构成:

  • 边界框部分:包含两个预设的边界框。
    • 每个边界框需要 4 个值来描述其位置 (x, y, w, h)
    • 每个边界框附带 1 个置信度值。
    • 因此,两个边界框共占 (4+1) x 2 = 10 个值。
  • 类别概率部分:在 PASCAL VOC 数据集中,共有 20 个物体类别。
    • 单元格需要预测它属于这 20 个类别的概率。
    • 因此,这部分占 20 个值。

总计 10 + 20 = 30 个值,与网络输出维度吻合。所以,30 = (5 + 5) + 20。前 10 个值专门用于边界框预测,后 20 个值用于该单元格的类别概率预测,并从中选取概率最大的类别作为预测结果。


边界框的筛选与置信度

一个网格会预测多个边界框。在训练时,我们只选用其中一个边界框负责预测物体。

每个边界框都对应一个置信度。置信度由网络直接输出,其含义如下:

  • 如果该边界框内没有物体,则其置信度应为 0。
  • 如果该边界框内有物体,则其置信度等于该边界框与真实框的 IoU 值。

那么,如何判断一个单元格“有物体”呢?规则是:如果一个真实物体的中心点坐标落在某个单元格内,则该单元格就负责预测这个物体。

在训练过程中,我们会将包含物体中心点的单元格标记为正样本(目标值为1),其他大部分单元格则标记为负样本(目标值为0)。这样,网络在训练时,包含物体的单元格预测出的置信度会越来越高。


单元格的最终输出

每个单元格最终需要输出一个边界框位置和一个类别。

以下是单元格的决策过程:

  1. 选择边界框:从该单元格预设的两个边界框中,选择置信度较高的一个。
  2. 选择类别:从该单元格预测的 20 个类别概率中,选择概率最大的一个。

因此,对于输入图像,网络最终会输出 49 个单元格的预测结果,每个结果包含一个边界框(位置)和一个类别标签。

关于边界框位置 (x, y, w, h) 的理解,它们都是相对于单元格的偏移量和相对于整张图像的归一化比例,具体公式如下:

# (x, y) 是边界框中心相对于单元格左上角的偏移
# (w, h) 是边界框的宽高相对于整张图像宽高的比例
x = (x_center_of_object / image_width) * grid_size - column_index
y = (y_center_of_object / image_height) * grid_size - row_index
w = bbox_width / image_width
h = bbox_height / image_height

总结

本节课中,我们一起学习了 YOLO 算法中单元格的核心原理。

  • 我们将 7x7x30 的输出解释为 49 个单元格,每个单元格携带 30 维信息。
  • 这 30 维信息被拆分为两部分:用于预测两个边界框位置和置信度的 10 个值,以及用于预测 20 个类别概率的 20 个值。
  • 我们了解了如何通过物体中心点确定负责预测的单元格,以及如何利用置信度筛选出最终的边界框。
  • 每个单元格的最终输出是一个置信度最高的边界框和一个概率最大的物体类别。

这就是 YOLO 通过单元格机制实现“只看一次”就能完成目标检测的关键所在。

课程 P29:YOLO 训练过程的样本标记 🎯

在本节课中,我们将学习 YOLO 模型在训练阶段如何对样本进行标记。理解这个过程是掌握 YOLO 如何学习检测目标的关键。

上一节我们介绍了 YOLO 在测试阶段的流程,本节中我们来看看它在训练时是如何工作的。

训练过程的样本标记

训练过程的核心是为模型提供正确的“答案”,即标记。这包括判定每个网格单元内是否有目标,以及目标的类别和位置。

具体来说,训练标记需要为每个网格单元提供以下信息:

  • 该单元格内是否有目标
  • 目标的20个类别概率(针对VOC数据集)。
  • 目标边界框(Bounding Box)的坐标

预测框与真实值的对比

训练时,模型会为每个网格单元生成预测值。这些预测值需要与对应的真实值进行对比,以计算误差并指导模型更新。

以下是训练对比的关键步骤:

  1. 坐标误差:计算预测的边界框与真实的边界框(Ground Truth, GT)之间的坐标误差。
  2. 置信度误差:衡量预测的“是否有目标”的置信度与真实情况是否匹配。有目标的单元格,其真实置信度标记为 1;无目标的则标记为 0。模型预测的是一个与真实框的IOU值相关的概率。
  3. 类别误差:计算预测的类别概率分布与真实的类别标签之间的误差。

样本标记的具体方式

每个网格单元都会根据真实情况获得一个对应的标记。

  • 如果该单元格包含目标中心点,则将其“有目标”标记设为 1,并为其分配真实的类别概率和边界框坐标。
  • 如果该单元格不包含任何目标,则将其“有目标”标记设为 0。在训练时,模型对这些单元格的预测就不会产生目标。

通过这种方式,模型能够学会区分有目标的区域和无目标的背景区域。

理解训练过程图示

为了更直观地理解,我们可以参考以下训练过程的逻辑图示(此处为文字描述):
模型将预测值与真实标记在三个维度上进行对比:边界框坐标、目标置信度和类别概率,并计算综合损失来调整自身参数。

本节课中我们一起学习了 YOLO 训练过程中样本标记的核心机制。关键在于理解每个网格单元如何根据真实目标被标记为“有目标”或“无目标”,并如何通过对比预测值与真实值在坐标、置信度和类别三个方面的误差,来驱动模型的学习。掌握这一过程,就理解了 YOLO 模型能够学会检测目标的原理。

图像识别背景3 - AI前沿技术分享 - P3 🖼️

在本节课中,我们将要学习图像识别领域的核心背景知识。我们将了解图像识别的三大核心任务,并探讨其在实际应用中的两种主要发展模式。

图像识别的三大任务 🎯

上一节我们介绍了课程的整体目标,本节中我们来看看图像识别的具体任务划分。图像识别领域主要包含三大任务。

以下是三大任务的定义:

  1. 目标识别 (Image Classification):对图像中的目标进行分类定性,确定目标是什么。其核心是输出目标的类别。例如,判断一张图片中是“矿泉水瓶 (bottle)”还是“杯子 (cup)”。
  2. 目标检测 (Object Detection/Localization):定位图像中的目标。除了识别目标类别,还需要确定目标在图片中的具体位置。通常用边界框 (Bounding Box) 的坐标来标记位置。
  3. 目标分割 (Image Segmentation):对图像的前景与背景进行分类,将目标从背景中分离出来。它进一步细分为:
    • 语义分割 (Semantic Segmentation):将物体与背景分开。
    • 实例分割 (Instance Segmentation):不仅分离物体与背景,还能区分同一类别的不同个体,并精确标记出物体的整个形状轮廓。

所以,我们可以简单总结一下图像识别的三大任务:

  • 目标识别:输出目标的类别。
  • 目标检测:输出目标的类别及其在图片中的位置。
  • 目标分割:描述目标的精确形状,并将背景进行标记或剔除。

本课程后续的重点将是目标检测。虽然目标分割在像素级精度上更优,但目标检测在平衡精度与计算效率方面应用更为广泛。

图像识别的两种发展模式 📈

了解了图像识别的任务分类后,我们来看看它在实际应用中的发展模式。图像识别的发展主要通过两种应用场景模式来体现。

以下是两种主要的应用场景:

  1. 通用场景 (General Scene):以谷歌、微软、Facebook、百度等大型互联网企业为代表。它们凭借雄厚的人力、财力和数据资源,致力于开发通用的识别产品。例如,通用的物体识别、内容审核、人脸检测等。这些企业通常会搭建开放平台(如百度AI开放平台),提供基础的图像技术服务。
  2. 垂直场景 (Vertical Scene):基于具体行业或公司的特定业务需求进行开发。由于不同行业(如医疗影像、林业树种识别、工业质检)的数据具有私密性和专业性,大公司的通用模型往往难以直接适用。因此,需要利用特定领域的私有数据构建数据集并训练模型。目前大量的图像应用开发都集中在垂直领域。

通用场景的平台提供的功能能达到基础效果,但难以满足企业级深度定制的需求。而垂直领域的行业特质常被通用方案忽略,因此催生了大量针对特定场景的图像应用开发。我们后续的学习和实践也将主要围绕垂直场景的目标检测展开。


本节课中我们一起学习了图像识别的三大核心任务:目标识别目标检测目标分割,并了解了图像识别在通用场景垂直场景下的两种不同发展模式。理解这些背景知识,有助于我们明确后续技术学习的方向和应用落地的场景。

课程 P30:YOLO 总结 🎯

在本节课中,我们将对 YOLO 算法进行总结,并将其与 Fast R-CNN 进行对比,以明确其优缺点。我们将通过图表和要点分析,帮助初学者理解 YOLO 的核心特性。

YOLO 与 Fast R-CNN 的对比 📊

上一节我们介绍了 YOLO 的网络结构,本节中我们来看看它与另一种经典目标检测算法 Fast R-CNN 的区别。下图展示了两种算法在相同数据集上的性能对比。

从图中可以看出,YOLO 在准确度上不及 Fast R-CNN。YOLO 的主要特点是速度快,但并未保证达到最佳的准确率。相对于 Fast R-CNN,YOLO 的结果存在一定误差。

YOLO 的优缺点总结 ✅❌

基于上述对比,我们可以对 YOLO 算法进行总结。

以下是 YOLO 的主要优点:

  • 速度快:这是 YOLO 最显著的优势。

以下是 YOLO 的主要缺点:

  • 准确率会打折扣:这是追求速度所付出的代价。
  • 对相互靠近的物体检测效果不佳:当多个物体落在同一个预测网格(grid cell)时,YOLO 可能无法正确区分。
  • 对小群体检测效果不好:对于尺寸较小的物体,检测效果不理想。

核心概念回顾 🔑

最后,我们回顾一下 YOLO 的核心设计。YOLO 的网络结构及其输出 7x7x30 的张量是需要理解的关键。

本节课中我们一起学习了 YOLO 算法的总结。我们将其与 Fast R-CNN 进行了对比,明确了 YOLO 速度快但准确率稍逊的特点,并分析了其在处理密集和小物体时的局限性。理解这些优缺点有助于在实际应用中根据需求选择合适的检测模型。

课程31:SSD网络结构与Detector结构详解 🎯

在本节课中,我们将学习一种在速度和精度之间取得良好平衡的目标检测模型——SSD。我们将重点解析其网络结构,并深入理解其核心组件“Detector”与“Classifier”的工作原理。


SSD简介

上一节我们介绍了YOLO模型,其速度表现优异。那么,是否存在一种算法能同时兼顾速度与准确度呢?答案是肯定的,那就是SSD。

SSD,英文全称为 Single Shot MultiBox Detector,是一种在项目中非常实用的重要模型。

它的学习目标是:

  • 了解SSD的网络结构。
  • 理解其中“Detector”和“Classifier”组件的作用。
  • 说明SSD的主要优点。

SSD于2016年发表。其核心特点是结合了YOLO的回归思想与Faster R-CNN的Anchor机制,从而在保持较高检测速度的同时,也保证了精度。

因此,SSD兼顾了速度与精度。请注意,这里的SSD并非指固态硬盘,虽然名称相同。


SSD网络结构

了解了SSD的基本概念后,本节我们来看看它的具体网络结构。

对于SSD模型,输入图片有固定要求:必须是300×300像素的大小。如果处理的图片不是此尺寸,则需要进行相应的缩放或裁剪。

图片输入后,会经过一个包含卷积层、全连接层等的深度网络进行处理。最终,SSD在特定数据集上能达到约59 FPS的速度和74.3%的mAP精度,这个表现在当时是相当出色的。

作为对比,之前学习的YOLO模型输入图片尺寸为448×448。


SSD工作流程

明确了输入要求后,我们来梳理SSD的整体工作流程。

当一个300×300×3的图片输入网络后,数据从左向右流动。在网络结构的多个特定层级(图中向下的箭头处),都会进行一项关键操作:Detector & Classify

每一个“Detector & Classify”模块都会输出一系列预测框。所有这些来自不同层级的预测框会被汇集起来,最后通过NMS(非极大值抑制) 算法进行筛选,最终得到图片中物体的预测结果。

由此可见,整个流程的核心就是Detector & Classify模块。我们的重点也将放在理解这个模块上。


Detector & Classify 详解

上一节我们指出了流程中的核心模块,本节中我们来详细剖析“Detector & Classify”。

SSD引入了 Default Box 的概念,这与Faster R-CNN中的Anchor Box机制类似,都是预设一些不同形状和大小的候选框。不同之处在于,SSD会在多个不同尺度的特征图上都生成Default Boxes。

以下是对其结构的说明:

以网络中某个输出为38×38的特征图为例。在该特征图的每一个像素点上,SSD会预测4个默认框(Default Box)。

因此,仅这一层产生的候选框总数就是:38 × 38 × 4 = 5776个。

观察网络结构图可以发现,不同层级的特征图(如19×19, 10×10, 5×5等),其每个像素点对应的Default Box数量是不同的(可能是4个或6个)。

最终,SSD将所有层级的Default Box数量相加,总共会生成8732个候选框。这些框将作为后续分类和位置修正的基础。

那么,这个结构是如何进行训练和预测的呢?“Detector & Classify”模块融合了YOLO的边界框回归思想和Faster R-CNN的Anchor机制

具体来说,该模块包含三个核心组成部分:

  1. Default Boxes:生成的默认候选框。
  2. Localization:4个值,表示预测框相对于Default Box的位置偏移(Δcx, Δcy, Δw, Δh)。
  3. Confidence:21个类别的置信度(20个目标类别 + 1个背景类别)。

三个核心组件解析

我们已经知道了“Detector & Classify”的三个组成部分,现在来逐一深入理解。

我们以网络中一个5×5×256的特征图输出为例进行说明。

1. Default Boxes的生成

首先,通过一个Default Box Generator,为5×5特征图的每个像素点生成若干个(例如6个)默认候选框。这些框是如何产生的呢?

其生成方式与Faster R-CNN类似,也基于滑动窗口和固定公式。每个像素点会预测多个(如3个或6个)不同大小和长宽比的框。

生成过程主要依赖两个参数:

  • Ratio:候选框的长宽比。
  • S_min 和 S_max:分别代表最底层和最顶层特征图用于计算框大小的尺度参数。

生成公式作为了解内容,此处不要求掌握。你只需知道Default Boxes是通过预设的尺度和长宽比,在特征图的每个点上生成的即可。

2. Localization 与 Confidence

对于同一个5×5×256的特征图,SSD会进行两次不同的3×3卷积操作:

  • 一次卷积输出为5×5×12,这12个通道对应了每个Default Box的4个位置偏移量((x, y, w, h)的调整值)。
  • 另一次卷积输出为5×5×63,这63个通道对应了每个Default Box的21个类别的置信度(假设有20个物体类别+背景)。

组件关系总结

综上所述,这三个组件协同工作:

  1. Default Boxes 提供初始的候选区域。
  2. Localization 预测这些区域需要如何微调以更贴合真实物体。
  3. Confidence 判断每个区域属于哪个类别或是否是背景。

网络通过训练学习如何准确地预测偏移量和置信度,从而在推理时,对大量的Default Box进行快速分类和位置修正。


总结

本节课中,我们一起学习了SSD目标检测模型。

  • 我们首先了解了SSD是一种兼顾速度与精度的模型,它融合了YOLO的回归思想和Faster R-CNN的Anchor机制。
  • 然后,我们分析了SSD的网络结构,其输入要求为300×300的图片。
  • 接着,我们梳理了SSD的工作流程,其核心在于多个特征层上的“Detector & Classify”模块。
  • 最后,我们深入剖析了该模块的三个核心组件:Default Boxes(默认框生成)、Localization(位置偏移预测)和Confidence(类别置信度预测)。

理解这些内容,是掌握SSD模型原理和应用的基础。

课程P32:SSD中的定位与置信度 🎯

在本节课中,我们将要学习SSD(Single Shot MultiBox Detector)目标检测算法中的两个核心概念:定位置信度。我们将详细探讨网络如何为每个候选框预测位置偏移和类别概率,并理解这些预测如何用于最终的目标检测。


从候选框到预测

上一节我们介绍了SSD如何生成默认框。本节中我们来看看网络如何利用这些默认框进行预测。

在Fast R-CNN中,默认框用于与真实标注框进行比较和修正,并进行背景或物体的概率预测。在YOLO中,则直接对结果进行回归和损失计算。SSD的做法是:为生成的每个候选框,网络需要输出两个关键信息:定位置信度,以代表每个候选框的预测结果。

定位预测

定位预测的输出是一个形状为 5×5×12 的张量。我们来分解这个结构。

以下是其构成方式:

  • 它可以分为三个部分:5×5×45×5×45×5×4。三者相加即为 5×5×12
  • 这里的“4”代表每个像素点对应的一个候选框的四个位置偏移值(例如中心点坐标和宽高的修正量)。
  • 由于每个像素点对应3个不同尺度的候选框,因此总共有 5×5×3 = 75 个候选框。
  • 每个候选框需要4个值来修正其位置,所以总的位置偏移参数数量为 75 × 4,这正好由 5×5×12 的张量来表示。

网络直接输出的这些值,将用于后续的边界框回归计算。

置信度预测

置信度预测的输出是一个形状为 5×5×21 的张量。

以下是其含义:

  • 每个候选框都会对应一组21个概率值(假设数据集中有20个物体类别,外加1个背景类别)。
  • 总共有75个候选框,因此总的置信度参数数量为 75 × 21
  • 这个张量也可以理解为由卷积得到的 5×5×63 的特征图拆分而成,因为 63 = 3 × 21,对应每个像素点的3个候选框及其21个类别的概率。

这样,我们就得到了每个候选框的位置偏移和属于各个类别的概率。

预测后处理流程

获得定位和置信度信息后,接下来的处理流程与YOLO类似。

以下是处理步骤:

  1. 置信度过滤:根据每个候选框的类别概率,过滤掉置信度低的候选框。
  2. 非极大值抑制:对剩余的候选框应用NMS,去除高度重叠的冗余框。

经过这些步骤,最终保留下来的高质量候选框,就是网络的预测结果。

关键概念辨析

在训练和预测过程中,会涉及几种不同的边界框,理解它们的区别很重要。

以下是核心概念:

  • Ground Truth Boxes:图片中真实物体的标注框。
  • Default Boxes:网络事先设定的、在不同位置和尺度上生成的候选框(类似于Faster R-CNN中的锚框)。它们是计算和匹配的起点。
  • Predicted Boxes:网络对Default Boxes进行位置偏移预测和置信度筛选后,得到的最终预测框。这些框会与Ground Truth Boxes计算损失进行训练,或在推理时作为输出结果。

简而言之,Default Boxes是预设的“模板”,而Predicted Boxes是网络根据“模板”调整和筛选后的“成品”

SSD的多尺度检测优势

现在我们来思考SSD中多个检测层的作用。Faster R-CNN的候选框主要来自最后一个特征图。而SSD的不同之处在于,它在多个不同尺度的特征图上都进行默认框的生成和预测。

以下是这种设计的好处:

  • 兼顾不同大小物体:浅层特征图分辨率高,包含更多细节信息,有利于检测小物体;深层特征图语义信息强,有利于检测大物体。
  • 提高检测精度:通过在不同尺度上进行密集预测,能够覆盖更多可能的物体位置和大小,从而提高检测的召回率和准确性。

这种多尺度预测机制,有效避免了YOLO早期版本中单一网格预测可能遗漏小物体或对物体定位不够精细的问题。


本节课中我们一起学习了SSD算法的核心预测机制。我们明确了网络如何为每个默认框输出定位偏移置信度,理解了从预测到最终输出结果的后处理流程,并辨析了Default Boxes与Predicted Boxes的关键区别。最后,我们探讨了SSD采用多尺度特征图进行预测的优势,这使其能够更有效地检测不同大小的物体。掌握这些概念,是理解SSD算法工作原理的重要基础。

🚀 课程33:SSD训练与测试流程总结

在本节课中,我们将系统性地总结SSD(Single Shot MultiBox Detector)模型的训练与测试流程。我们将深入理解如何准备训练数据、计算损失,以及模型在推理阶段如何工作。


训练流程概述

上一节我们介绍了SSD的网络结构,本节中我们来看看其训练流程。训练的核心在于将模型预测的边界框与真实标注(Ground Truth)进行匹配和比较,从而计算损失并优化模型。

训练流程可以概括为:输入图像,经过网络得到预测框,然后将这些预测框与真实标注进行匹配和标记,最后计算损失并反向传播。

样本标记

要进行训练,就需要为模型生成的大量候选框(例如8732个)分配标签。这个过程称为样本标记。

首先,模型会输出大量预测框。经过置信度筛选后,例如保留约3000个高质量的预测框。接下来,需要为这3000个预测框逐一进行标记,每个框都要对应一个真实标注(GT)的结果。

以下是样本标记的具体规则:

  • 正样本:与任意真实标注框的重叠度(IoU)大于0.5的预测框被标记为正样本。
  • 负样本:其余预测框被标记为负样本。
  • 样本平衡:为了保证训练稳定性,通常会控制正负样本的比例,例如保持1:3的比例。这是一个经验值。

损失计算

在完成样本标记后,就可以计算损失函数来指导模型学习了。SSD的损失函数与YOLO类似,由两部分组成。

总损失 L 的公式可以表示为:
L = (1/N) * (L_conf + α * L_loc)
其中:

  • N 是匹配到的正样本数量。
  • L_conf 是分类置信度损失(Confidence Loss)。
  • L_loc 是定位回归损失(Localization Loss)。
  • α 是用于平衡两项损失的权重参数。

具体来说:

  1. 分类置信度损失 (L_conf):通常使用 Softmax Loss(或交叉熵损失)来计算预测类别与真实类别之间的差异。
  2. 定位回归损失 (L_loc):采用 Smooth L1 Loss 来计算预测框与真实标注框在中心点坐标(cx, cy)和宽高(w, h)上的回归误差。这与Faster R-CNN中使用的回归损失一致。




测试流程概述

了解了训练过程后,我们来看测试(推理)流程。测试流程相对简单直接,其目标是利用训练好的模型对新图像进行预测。

模型训练完成后,其参数是固定的。在测试时,输入一张图像,网络会直接输出大量的预测框及其类别置信度。

测试流程的关键步骤是非极大值抑制(Non-Maximum Suppression, NMS)。NMS会过滤掉那些与得分最高的框重叠度过高的冗余框,最终为每个物体保留一个最准确的预测框和其对应的类别。


SSD算法优势

最后,我们来总结一下SSD算法的特点。从性能对比图中可以看出,SSD在目标检测算法中处于一个非常有利的位置。

SSD的核心优势在于它兼顾了速度与精度。它像YOLO一样是单阶段检测器,速度很快;同时,它通过在多尺度特征图上进行预测,获得了较高的检测准确率。


📝 课程总结

本节课中我们一起学习了SSD模型的完整流程,以下是核心要点总结:

  1. 网络结构回顾:SSD是一个端到端的网络,会在多个不同尺度的特征图上生成候选框,并同步预测每个框的置信度(confidence)定位偏移量(localization)
  2. 训练核心:训练的关键在于样本标记。需要将网络输出的成千上万个预测框与真实标注进行匹配,区分为正负样本,并保持一定的比例(如1:3)。
  3. 损失函数:总损失由分类损失定位回归损失两部分加权构成,分别使用Softmax Loss和Smooth L1 Loss。
  4. 测试流程:训练好的模型可直接进行前向传播,并通过非极大值抑制(NMS) 后处理步骤输出最终的检测结果。
  5. 算法特点:SSD成功地在速度精度之间取得了优秀的平衡,这是其被广泛采用的重要原因。

总而言之,SSD通过单一网络直接输出检测结果,其训练过程围绕着如何为海量默认框分配合适的标签展开,而测试过程则高效简洁。理解了这个流程,就掌握了SSD算法最核心的思想。

课程 P34:Tensorflow SSD 接口介绍 🧠

在本节课中,我们将学习 Tensorflow 框架中 SSD 目标检测算法的接口。我们将了解其核心配置参数、网络结构以及主要功能函数,以便能够快速上手使用。

上一节我们介绍了从 R-CNN 到 YOLO、SSD 等目标检测算法的发展历程。本节中我们来看看 Tensorflow 为 SSD 算法提供的官方接口。

接口文件与网络配置

Tensorflow 的 SSD 接口源码通常包含在项目文件中。其核心在于网络配置,它定义了每一层网络的结构和参数。

以下是网络配置中的关键参数及其含义:

  • image_shape:输入图像的尺寸,例如 300×300
  • num_classes:类别数量,通常为实际类别数加背景类。例如,对于 20 个类别的数据集,此值为 21
  • feature_map_shapes:各层特征图的大小,例如 [38, 19, 10, 5, 3, 1],对应 SSD 网络中的 38×38 到 1×1 的特征层。
  • scales:默认框(default boxes)的尺度范围,对应公式中的 s_mins_max。例如,[0.15, 0.91] 定义了默认框最小和最大的相对尺寸。
  • aspect_ratios:每一层默认框的长宽比列表,例如 [[2, 0.5], [2, 3, 0.5, 0.333], ...]

了解这些参数后,再看接口代码就会清晰很多。它们直接对应了 SSD 论文中描述的网络设计。

核心接口函数

Tensorflow SSD 接口提供了几个关键函数,用于构建和训练网络。

以下是主要的功能函数:

  • ssd_net:此函数定义了整个 SSD 网络结构。它返回构建好的网络模型,其中包含了每一层的具体配置,如卷积核数量(128, 256等)。
  • ssd_anchors:此函数用于生成所有特征层上的默认框(anchors/default boxes)。SSD 通常有 6 个特征层,每层会生成不同数量和尺寸的默认框。
  • ssd_losses:此函数定义了网络的损失函数。它计算预测结果(类别和位置偏移)与真实标注(ground truth)之间的损失。训练时,会从生成的众多默认框中筛选一部分与真实框进行匹配和计算。

这些函数封装了 SSD 算法的核心步骤。在使用时,理解每个参数(如 predictions, localizations)所代表的含义至关重要。

本节课中我们一起学习了 Tensorflow 中 SSD 目标检测接口的核心配置和主要函数。通过理解网络配置参数和几个关键接口,我们能够快速掌握如何使用该接口进行目标检测任务的开发。后续在实际应用时,我们将结合具体代码进行实践。

📚 课程 P35:第一阶段目标检测算法总结

在本节课中,我们将系统性地回顾第一阶段介绍过的几种经典目标检测算法。我们将梳理每个算法的核心思想、关键改进点以及它们之间的演进关系,帮助你构建清晰的知识脉络。

🎯 概述:目标检测问题的初始解决方案

目标检测的核心任务是识别图像中物体的类别并定位其位置。最初的解决思路是“分类加定位”。

  • 单目标场景:直接使用一个全连接层(FC)回归输出物体的边界框坐标。
  • 多目标场景:引入了 滑动窗口 的概念。通过设置 K 个不同尺寸的窗口,在每个位置进行滑动,总共产生 K × M 个待检测区域。OverFeat 模型是这一思路的代表。

🔍 R-CNN:两阶段检测的开端

上一节我们介绍了基础思路,本节中我们来看看首个成功应用深度网络的目标检测算法——R-CNN。它奠定了“候选区域+分类”的两阶段范式。

R-CNN 的测试流程包含以下几个核心步骤:

  1. 候选区域生成:使用选择性搜索(Selective Search)算法从输入图像中提取约2000个候选区域。
  2. 图像变换:将每个候选区域缩放到固定大小(如 227×227)。
  3. 特征提取:使用预训练的卷积网络(如 AlexNet)对每个区域提取特征。最终得到 2000 × 4096 维的特征矩阵。
  4. 类别分类:为每个类别训练一个独立的 SVM 分类器进行二分类,得到一个 2000 × 20 的得分矩阵。
  5. 非极大值抑制(NMS):对每个类别的预测框,应用 NMS 去除冗余的重叠框。
  6. 边界框回归:使用线性回归模型对筛选后的候选框进行位置微调,使其更接近真实标注。

R-CNN 的训练过程是分阶段进行的:

  • 预训练:在大型分类数据集(如 ImageNet)上训练一个卷积网络。
  • 微调:在目标检测数据集上,用候选区域及其类别标签对上述卷积网络进行微调。
  • SVM 训练:用卷积网络提取的特征,为每个类别训练 SVM 分类器。
  • 边界框回归器训练:训练用于修正位置的线性回归模型。

R-CNN 的主要缺点:训练分多个阶段,繁琐且速度慢;特征需要写入磁盘,占用大量存储空间;对每个候选区域独立进行卷积运算,计算存在大量重复;输入图像需要变形,可能影响精度。

⚡ SPP-Net:共享计算与空间金字塔池化

R-CNN 的重复计算问题严重。SPP-Net 的核心改进在于共享卷积计算

其关键流程如下:

  1. 将整张图像输入卷积网络,得到整张图的特征图。
  2. 将 Selective Search 得到的候选区域映射到特征图上的对应区域。
    • 映射公式需要掌握:假设原图上坐标点为 (x, y),经过若干次步长为 S 的卷积/池化后,在特征图上对应的坐标约为 (x/S, y/S)
  3. 每个形状各异的候选区域特征,通过 SPP 层(空间金字塔池化层) 转换为固定长度的特征向量。SPP 层将区域划分为不同尺度的网格(如 4×4, 2×2, 1×1),并对每个网格进行池化,最后将所有结果拼接起来。例如,三种尺度会得到 16+4+1=21 个池化结果。
  4. 固定长度的特征向量送入全连接层进行分类和回归。

SPP-Net 的贡献:通过一次卷积计算整图特征,大幅提速;SPP 层允许输入任意大小的区域,输出固定维度的特征,避免了图像变形。

🚀 Fast R-CNN:迈向端到端训练

SPP-Net 仍需多阶段训练。Fast R-CNN 旨在实现更统一的训练。

其主要改进点如下:

  • ROI Pooling:简化版的 SPP 层。它将每个候选区域特征图划分为固定的 K×M 个网格(如 7×7),然后在每个网格内进行最大池化,输出固定大小的特征。关键优势是 ROI Pooling 可进行反向传播,使得网络能够端到端训练。
  • 多任务损失:用 Softmax 分类器替代多个 SVM 分类器,输出 N+1 个类别(N个物体类 + 1个背景类)。分类损失和边界框回归损失被合并为一个统一的损失函数,同时进行优化。

Fast R-CNN 的缺点:其候选区域生成仍依赖于外部算法(如 Selective Search),未能实现完全端到端,检测速度的瓶颈在于候选区域提取。

🤖 Faster R-CNN:真正的端到端检测器

为了解决候选区域提取的瓶颈,Faster R-CNN 引入了 RPN(区域提议网络),将候选区域生成也融入网络,实现真正意义上的端到端。

其工作流程如下:

  1. 共享特征提取:输入图像经过基础卷积网络,得到共享特征图。
  2. RPN 生成候选框
    • 在共享特征图上滑动一个小网络(如 3×3 卷积)。
    • 在每个滑动窗口中心,预设 K 个不同尺度和长宽比的 锚点(Anchor)。例如,对于 M×N 的特征图,会产生约 M×N×K 个锚点。
    • RPN 输出两部分:① 每个锚点是前景(物体)或背景的概率;② 对每个锚点坐标的修正量。
    • 经过筛选和微调后,输出高质量的候选区域(Proposals)。
  3. Fast R-CNN 检测:将 RPN 产生的候选区域通过 ROI Pooling 映射到共享特征图上,后续流程与 Fast R-CNN 相同(分类 + 精修)。

Faster R-CNN 的意义:精度高,是两阶段检测器的标杆。但速度仍难以满足实时性要求。

⚡ YOLO v1:单阶段检测的先锋

为了追求极致的速度,YOLO(You Only Look Once)提出了全新的单阶段思路。

核心思想:将目标检测视为单一的回归问题。将输入图像划分为 S×S(如 7×7)的网格。每个网格负责预测:

  • B 个边界框(每个框包含中心坐标、宽高和1个置信度)。
  • 1 个条件类别概率(即该网格属于各个类别的概率)。

网络最终输出一个 S × S × (B*5 + C) 的张量。以 YOLO v1 为例:7 × 7 × (2*5 + 20) = 7 × 7 × 30

YOLO 的优点:速度极快,可实现实时检测。
YOLO 的缺点:早期版本(v1)对密集小物体检测效果较差,定位精度相对较低。

🧱 SSD:单阶段检测的精度提升者

SSD(Single Shot MultiBox Detector)结合了 YOLO 的回归思想和 Faster R-CNN 的锚点机制,在速度和精度间取得了更好平衡。

SSD 的核心结构是在多个不同尺度的特征图上进行预测:

  1. 多尺度特征图预测:SSD 利用主干网络(如 VGG)中不同深度的多个特征图进行预测。深层特征图感受野大,适合检测大物体;浅层特征图分辨率高,适合检测小物体。
  2. 默认框(Default Box):在每个特征图的每个单元格上,设置一系列具有不同尺度和长宽比的默认框(类似于锚点)。
  3. 预测模块:对于每个默认框,网络同时预测:
    • 定位偏移4 个值,用于调整默认框的位置。
    • 类别置信度C+1 个值(C个物体类 + 背景)。
  4. 训练时的匹配策略:将真实标注框与这些默认框进行匹配,匹配成功的默认框作为正样本,负责预测该物体的类别和位置。

SSD 的优势:通过多尺度预测,显著提升了对不同大小物体,尤其是小物体的检测能力,在保持高速的同时获得了更高的精度。

📝 总结

本节课中我们一起学习了目标检测算法从 R-CNN 到 SSD 的演进历程:

  • R-CNN 开创了基于深度学习的“候选区域+CNN特征”两阶段范式,但存在计算冗余、训练复杂等问题。
  • SPP-Net 通过共享卷积计算和空间金字塔池化,大幅提升了特征提取效率。
  • Fast R-CNN 引入 ROI Pooling 和多任务损失,实现了端到端训练,但候选区域提取仍是外部模块。
  • Faster R-CNN 创新性地提出 RPN 网络,将候选区域生成纳入网络,完成了两阶段检测器的最终形态。
  • YOLO 另辟蹊径,将检测视为单次回归问题,实现了惊人的实时速度,但早期版本精度有损失。
  • SSD 融合了单阶段的效率和锚点机制的多尺度优势,在多个特征图上进行预测,有效平衡了速度与精度。

理解这些算法的核心思想与演进关系,是掌握现代目标检测技术的重要基础。

课程P36:目标检测数据集介绍 🎯

在本节课中,我们将学习目标检测领域常用的公开数据集,重点了解PASCAL VOC数据集的结构和格式。掌握数据集的组织方式是进行模型训练和评估的基础。

上一节我们介绍了目标检测算法的基本原理,本节中我们来看看如何准备和组织训练数据。

常用数据集概览

以下是两个在目标检测领域广泛使用的公开数据集:

  • PASCAL VOC数据集:这是一个历史悠久且极具代表性的数据集,包含VOC2007和VOC2012等版本。许多经典模型和比赛都基于此数据集进行训练和评估。其官网地址为 http://host.robots.ox.ac.uk/pascal/VOC/。为了方便,也可以从其他镜像地址下载。
  • Open Images Dataset V4:这是一个较新(2018年发布)且规模巨大的数据集,包含约190万张图片和600个物体类别。它由专业标注人员标注,是目前最大的带标注目标检测数据集之一。

在课程演示和后续实践中,我们将主要使用PASCAL VOC 2007数据集,因为其结构清晰,且有丰富的工具和预训练模型支持。

PASCAL VOC 2007数据集详解

PASCAL VOC 2007数据集总共包含9963张图片,标注了20个常见的物体类别,例如:人、鸟、猫、狗、汽车、飞机、自行车、瓶子、椅子、餐桌等。

下载并解压数据集后,你会看到以下目录结构。对于目标检测任务,我们主要关注两个文件夹:

  • JPEGImages/:存放所有的原始图片文件(.jpg格式),共9963张。
  • Annotations/:存放与每张图片对应的XML格式的标注文件。

每个图片文件(例如 000001.jpg)都对应一个同名的XML标注文件(000001.xml)。XML文件记录了该图片中所有被标注物体的详细信息。

标注文件(XML)结构解析

XML文件是数据集的核心,它以一种结构化的方式记录了图片的元数据和物体的标注框。

以下是XML文件中需要关注的关键部分:

  • size:记录了图片的基本信息。
    • width:图片宽度。
    • height:图片高度。
    • depth:图片通道数(通常是3,代表RGB)。
  • object:每个被标记的物体都会有一个object节点。如果一张图中有多个物体,就会有多个object节点。
    • name:物体的类别名称(例如:“person”, “dog”)。
    • bndbox:物体边界框(Bounding Box)的坐标,即我们常说的真实标注(Ground Truth)。它包含四个值:
      • xmin:边界框左上角的x坐标。
      • ymin:边界框左上角的y坐标。
      • xmax:边界框右下角的x坐标。
      • ymax:边界框右下角的y坐标。

例如,对于一张包含一个人和一只狗的图片,其XML文件中会有两个object节点,分别记录“person”和“dog”的类别及其对应的bndbox坐标。这个坐标值 (xmin, ymin, xmax, ymax) 是相对于该图片尺寸的。

核心概念示例:
一个物体的边界框信息在XML中通常如下表示:

<object>
    <name>dog</name>
    <bndbox>
        <xmin>48</xmin>
        <ymin>240</ymin>
        <xmax>195</xmax>
        <ymax>370</ymax>
    </bndbox>
</object>

这表示一个类别为“dog”的物体,其边界框左上角坐标为(48, 240),右下角坐标为(195, 370)。


本节课中我们一起学习了目标检测的常用数据集,并深入剖析了PASCAL VOC数据集的目录结构和核心的XML标注格式。理解数据如何组织与标注,是动手构建和训练目标检测模型的第一步。在接下来的课程中,我们将学习如何读取和处理这些数据。

课程P37:商品数据集标记 🏷️

在本节课中,我们将学习如何为商品检测项目准备和标记自定义数据集。我们将了解数据标记的必要性,学习使用一个名为 labelImg 的图形化标注工具,并亲自动手完成一个商品数据集的标记过程。

为什么需要标记数据集?

上一节我们介绍了目标检测的基本概念,本节中我们来看看数据准备的第一步:标记。

标记数据集是训练目标检测模型的关键前提,主要原因有以下两点:

  1. 模型训练的需要:训练模型需要输入图片以及图片中物体的准确位置和类别信息。这些被标记的物体将作为模型的“目标值”(Ground Truth),用于计算预测边界框(B Box)的损失,例如通过交并比(IOU)来评估预测的准确性。公式可以表示为:损失 = f(预测框, 真实标记框)
  2. 特定场景的数据缺乏:虽然存在像PASCAL VOC、ImageNet这样的公开数据集,但它们通常涵盖通用场景。在实际商业应用中,例如识别特定商品、公司内部物品或特定品牌时,往往缺乏现成的、已标记的数据集。因此,为特定任务准备自定义数据集是必要的。

标记工具:labelImg介绍

明确了标记的必要性后,我们需要一个高效的工具来完成这项工作。本节将介绍一个常用的开源标注工具——labelImg

labelImg 是一个用Python编写、基于Qt图形界面的图像注释工具。它主要将标注结果保存为XML格式,这种格式与PASCAL VOC数据集以及ImageNet比赛使用的格式兼容,但与谷歌的一些数据集格式不同。

以下是关于 labelImg 的关键信息:

  • 官方源码:可以在其GitHub页面找到并安装。
  • 安装说明:官方提供了Windows、Linux (Ubuntu) 和 Mac OS的安装指南。但由于安装过程中可能遇到依赖问题,建议参考课程资料中提供的更详细的安装PDF文档,里面包含了常见问题的解决方案。

成功安装后,你可以在安装目录下找到 labelImg.py 文件,通过运行它来启动标注工具。

动手标记商品数据集

了解了工具后,现在我们来实际动手,为我们的商品检测项目创建数据集。这个过程虽然演示性质,但完整还原了数据标记的核心流程。

第一步:明确标记需求

在开始标记前,必须首先明确我们要检测哪些物体(类别)。我们的演示项目将聚焦于以下8个常见商品类别,以便于理解和操作:

  • 服饰类:衣服 (clothes)、裤子 (pants)、鞋子 (shoes)
  • 科技产品类:手机 (phone)、手表 (watch)、音箱 (speaker)、电脑 (computer)
  • 学习类:书籍 (books)

注意:在实际项目中,类别定义可以更具体(例如,区分不同品牌的衣服)。本教程为简化流程,使用了较宽泛的类别。

第二步:收集与准备图片

根据上述需求,我们已经预先收集好了包含这些商品的图片。在实际工作中,这一步通常涉及从网络爬取或手动下载相关图片。

第三步:使用labelImg进行标记

以下是使用 labelImg 工具进行标注的步骤:

  1. 运行软件:在安装 labelImg 的虚拟环境中,使用命令 python labelImg.py 启动工具。
  2. 界面概览:工具界面主要包括以下功能按钮:
    • Open / Open Dir: 打开单张图片或整个图片目录。
    • Next Image / Prev Image: 在目录中切换图片。
    • Save: 保存当前图片的标注结果,默认生成PASCAL VOC格式的 .xml 文件。
    • Change Save Dir: 更改标注文件的保存目录。
    • Create RectBox: 开始绘制边界框(快捷键 W)。
  3. 开始标注
    • 打开一张商品图片。
    • 点击 Create RectBox 或按 W 键。
    • 在目标物体周围拖动鼠标绘制边界框。
    • 在弹出的对话框中,输入该物体的准确类别名称(必须与第一步定义的名称完全一致,例如 phone)。
    • 重复此过程,标记图片中所有需要识别的物体。
    • 完成一张图片的标注后,按 Ctrl + S 保存。生成的 .xml 文件包含了物体类别、边界框坐标 (xmin, ymin, xmax, ymax) 和图片尺寸等信息。

标注示例代码(模拟逻辑):

# 伪代码,展示标注文件(XML)中的核心结构
<annotation>
    <filename>example.jpg</filename>
    <size>
        <width>800</width>
        <height>600</height>
    </size>
    <object>
        <name>phone</name> # 类别名称
        <bndbox>
            <xmin>100</xmin> # 边界框左上角x坐标
            <ymin>150</ymin> # 边界框左上角y坐标
            <xmax>300</xmax> # 边界框右下角x坐标
            <ymax>400</ymax> # 边界框右下角y坐标
        </bndbox>
    </object>
    <!-- 更多 object 标签... -->
</annotation>

总结与思考

本节课中我们一起学习了目标检测中数据准备的核心环节——数据集标记。

我们首先理解了为什么需要自定义数据集标记,然后认识并安装了实用的图形化标注工具 labelImg,最后通过一个商品检测的示例,完整实践了从明确需求准备图片使用工具进行标注的全过程。

记住,数据标记是一项耗时但至关重要的工作。在实际工业场景中,通常由专业团队或通过数据服务平台完成。

课后思考:如果你想开发一个“潮流品牌识别”系统,识别特定品牌的衣服或鞋款,你会如何规划数据标记工作?思路是:1) 确定要识别的具体品牌列表(需求);2) 收集包含这些品牌的商品图片;3) 使用 labelImg 等工具为图片中的品牌标识或商品打上标签。通过更换和扩充这样的数据集,你就可以训练出识别新类别的模型。

课程 P38:数据集格式转换介绍 📁➡️📦

在本节课中,我们将学习为什么以及如何将原始的图像和标签数据转换为 TensorFlow 专用的 tf.records 文件格式。我们将了解这种格式的优势,并掌握使用 TensorFlow 完成数据集转换的基本流程。

为什么需要格式转换?🤔

上一节我们介绍了如何保存图片和对应的 XML 标签数据。接下来,我们需要进行数据集格式的转换。

我们的目标是了解 tf.records 文件的作用,理解为何要这样处理,并应用 TensorFlow 完成数据集转换。我们将以 VOC2007 数据集为例进行转换。

首先,我们来看一下为何要进行格式转换。下图展示了两种数据存储方式。

左边是原始存储方式:图片和 XML 文件分开存放。XML 文件保存了图片的属性,如像素、长度、宽度、通道数以及图片中的对象信息。这些内容非常多,当训练模型需要读取数据时,编写的业务逻辑会非常复杂。

我们的目的是将数据格式转换的代码与模型使用数据的代码进行解耦。我们希望转换代码更简洁,模型只需负责读取和输出数据,无需编写复杂的转换逻辑。我们可以预先将数据转换好。

转换后的格式会非常方便,整体变得简洁。只需使用一个 tf.train.Example 函数就能将数据封装到一个类中,后续读取和处理都会非常简单。这就是我们想要做的事情。

因此,我们需要将数据逻辑分开处理。

在原始数据集中,标记文件和图片是分开存放在不同文件夹的。图片与标签没有直接的一一对应关系,这给后续的项目处理带来了不便。

我们希望通过转换,使数据集具有统一标准,方便统一使用、复制和移动到其他模块。例如,将格式转换好后提供给他人训练模型,对方无需参考或编写太多逻辑,只需了解数据协议的保存格式即可。

这就是我们进行格式转换的前提。

认识 TFRecords 文件 📄

接下来,我们介绍 tf.records 文件。这是 TensorFlow 提供的一种用于存储数据的格式。

这种格式的特点如下:

以下是 tf.records 文件的核心特点:

  • 数据一体化:它将图像数据和各种标签信息存储在一起。
  • 二进制格式:文件以二进制形式存储,具有快速复制、移动、读取和存储等特点。

我们重点来看“数据一体化”这个特点。在原始项目中,例如 VOC2007 数据集,XML 文件和图片是分开的。读取时,需要将图片与标签文件一一对应起来,过程非常麻烦。而 tf.records 格式可以将图像和标签整合在一起,形成一个数据协议,极大方便了后续处理。

在 TensorFlow 中,保存的文件扩展名可以是 .tfrecords.tfrecord。写入文件内容时,使用 tf.train.Example 将数据封装成 Protocol Buffer 协议。

Protocol Buffer 是一种高效的序列化协议。它的特点是:

  • 体积小:相比 JSON、XML 等格式,其存储体积要小 1/10 到 1/3。
  • 解析速度快:解析速度远快于上述文件格式。

Protocol Buffer 也是谷歌提供的,具有快速、跨平台等优点。在 tf.records 文件中,每一个 Example 对应一张图片及其所有信息。

以上就是我们进行格式转换的原因和 tf.records 文件的简要介绍。

总结 📝

本节课中,我们一起学习了数据集格式转换的必要性。我们了解到,将分散的图片和 XML 标签文件转换为统一的 tf.records 格式,可以实现数据与模型代码的解耦,使数据读取更高效、便捷。tf.records 文件采用二进制存储,并结合了高效的 Protocol Buffer 协议,具有体积小、解析快的优点,为后续的模型训练提供了便利的数据基础。

课程 P39:39.02_格式转换:代码介绍 🛠️

在本节课中,我们将学习如何将 VOC2007 格式的数据集(包含图片和对应的 XML 标注文件)转换为 TensorFlow 的 TFRecord 文件格式。我们将通过一个具体的代码案例,详细讲解转换的步骤和核心逻辑。


概述

我们将动手编写代码,实现 VOC2007 数据集的转换。转换的目标是将原始的图片和 XML 标注文件打包成 TFRecord 文件,以便后续高效地读取和使用。我们的数据集目录结构通常包含 testtrain 两个文件夹,我们将分别对它们进行转换。

转换完成后,我们会得到一系列以 testtrain 命名的 TFRecord 文件,每个文件包含固定数量的样本(例如 200 个)。这样,在训练或测试时,可以选择读取相应的文件。


转换效果预览

在运行转换代码后,我们可以在输出目录中看到生成的 TFRecord 文件。例如,对于 test 数据集,会生成类似 voc2007_test_00001.tfrecordvoc2007_test_00024.tfrecord 的文件。每个文件存储了部分图片及其对应的标注信息。


核心 API 介绍

在开始编写代码之前,我们需要了解一些将要用到的核心 API。

以下是转换过程中将使用的主要工具:

  • 文件操作:使用 Python 的 osglob 模块来检查文件是否存在、创建目录以及获取文件列表。
  • TFRecord 写入:使用 tf.io.TFRecordWriter 来创建和写入 TFRecord 文件。
  • 数据序列化:使用 tf.train.Exampletf.train.Features 来定义和构建要存储的数据协议格式。
  • XML 解析:使用 xml.etree.ElementTree 库来读取和解析 VOC 格式的 XML 标注文件。


转换步骤分析

上一节我们介绍了核心的 API,本节中我们来看看具体的转换逻辑。整个转换过程可以分解为以下几个清晰的步骤:

以下是实现格式转换的核心步骤:

  1. 设定分片大小:首先确定每个 TFRecord 文件包含的样本数量(例如 N=200)。这决定了最终会生成多少个文件。
  2. 读取与配对:循环遍历数据集目录,确保每张图片都能找到其对应的 XML 标注文件,并一一配对。
  3. 序列化与写入:读取图片内容和解析 XML 中的标注信息(如边界框、类别),将它们按照 tf.train.Example 的格式序列化,然后写入当前的 TFRecord 文件。
  4. 文件切换:每写入 N 个样本,就关闭当前的 TFRecord 文件,然后创建并开始写入下一个文件,直到所有样本处理完毕。


代码结构说明

现在,让我们深入到代码的具体实现。我们的代码将组织在 dataset 目录下,结构清晰,便于维护。

以下是项目代码的主要结构:

  • dataset/
    • config.py: 存放配置文件,例如数据集路径、每个 TFRecord 文件的样本数量等参数。
    • dataset_to_tfrecords.py: 这是主要的转换逻辑实现文件,包含了我们上面分析的步骤。
    • utils/: 存放一些公用的辅助函数或组件,例如 XML 解析函数。


核心代码逻辑详解

我们将聚焦于 dataset_to_tfrecords.py 中的 run 函数,它是转换流程的控制器。

1. 路径准备与文件列表获取

首先,代码会检查输入的数据集目录是否存在,并获取所有 XML 标注文件的列表。

import os
import glob

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/bc7191b073671f697e9ed4554b3bb7ab_31.png)

# 假设 dataset_dir 是传入的数据集路径(例如 ‘./VOC2007/ImageSets/Main/test’ 的上级目录)
annotation_dir = os.path.join(dataset_dir, ‘Annotations’)
if not os.path.isdir(annotation_dir):
    raise ValueError(“Annotation directory does not exist.”)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/bc7191b073671f697e9ed4554b3bb7ab_33.png)

# 获取所有 XML 文件路径并排序,确保与图片顺序对应
annotation_files = sorted(glob.glob(os.path.join(annotation_dir, ‘*.xml’)))

2. 循环处理与文件写入

接着,代码进入主循环,遍历排序后的 XML 文件列表。

import tensorflow as tf

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/bc7191b073671f697e9ed4554b3bb7ab_37.png)

samples_per_file = 200  # 每个 TFRecord 文件的样本数
file_index = 0
sample_count = 0
writer = None

for i, annotation_path in enumerate(annotation_files):
    # 每处理 samples_per_file 个样本,就创建新的 TFRecord 文件
    if i % samples_per_file == 0:
        if writer: # 关闭上一个文件
            writer.close()
        # 构建新的输出文件名,例如 ‘voc2007_test_{:05d}.tfrecord’
        output_filename = get_output_filename(output_dir, dataset_name, file_index)
        writer = tf.io.TFRecordWriter(output_filename)
        file_index += 1

    # 核心:处理单个样本(图片+XML),并将其转换为 tf.train.Example
    example = add_to_tfrecord(annotation_path, dataset_dir)
    if example:
        writer.write(example.SerializeToString())
        sample_count += 1

# 循环结束后,关闭最后一个 writer
if writer:
    writer.close()

3. 单个样本处理函数

add_to_tfrecord 函数是转换的核心,它负责读取图片、解析 XML 并构建 tf.train.Example 对象。

以下是 add_to_tfrecord 函数的关键操作:

  1. 解析 XML:从 XML 文件中提取文件名、图片尺寸、以及每个对象的边界框 (xmin, ymin, xmax, ymax) 和类别名称。
  2. 读取图片:根据 XML 中记录的文件名,找到对应的图片文件(通常位于 JPEGImages 目录下),并读取为二进制格式。
  3. 构建 Features:将图片二进制数据、图片尺寸、所有边界框坐标、所有类别标签(转换为整数索引)等,分别封装为 tf.train.Feature
  4. 创建 Example:将所有的 Feature 放入一个 tf.train.Features 字典,最终生成一个 tf.train.Example 对象。

def add_to_tfrecord(annotation_path, dataset_dir):
    # 解析 XML,获取信息
    data = parse_xml(annotation_path)

    # 读取图片
    image_path = os.path.join(dataset_dir, ‘JPEGImages’, data[‘filename’])
    with tf.io.gfile.GFile(image_path, ‘rb’) as fid:
        encoded_image = fid.read()

    # 构建特征字典
    feature_dict = {
        ‘image/encoded’: tf.train.Feature(bytes_list=tf.train.BytesList(value=[encoded_image])),
        ‘image/height’: tf.train.Feature(int64_list=tf.train.Int64List(value=[data[‘height’]])),
        ‘image/width’: tf.train.Feature(int64_list=tf.train.Int64List(value=[data[‘width’]])),
        ‘image/object/bbox/xmin’: tf.train.Feature(float_list=tf.train.FloatList(value=data[‘xmin’])),
        ‘image/object/bbox/ymin’: tf.train.Feature(float_list=tf.train.FloatList(value=data[‘ymin’])),
        # … 类似地添加 ymax, xmax, 和 class_label
    }

    # 创建并返回 Example
    example = tf.train.Example(features=tf.train.Features(feature=feature_dict))
    return example

总结

本节课中我们一起学习了将 VOC2007 数据集转换为 TFRecord 格式的完整流程。我们首先了解了转换的目的和效果,然后介绍了所需的 TensorFlow 和 Python API。接着,我们分析了转换的四个关键步骤,并深入解读了实现这些步骤的核心代码逻辑,包括如何组织代码结构、循环处理文件、以及最关键的单样本序列化函数 add_to_tfrecord 的实现。

通过本教程,你应该能够理解 TFRecord 转换的基本思想,并可以根据这个模式将自己的数据集转换为高效的 TensorFlow 标准格式,为后续的模型训练做好准备。

课程P4:目标检测的定义与技术历史 📚

在本节课中,我们将学习目标检测的基本定义,并了解其技术发展的主要阶段。理解这些基础概念是后续深入学习各类目标检测算法的前提。

目标检测的定义 🎯

上一节我们提到了目标检测的概念,本节中我们来看看其完整的定义。

目标检测的定义为:识别图片中有哪些物体以及物体的位置

这个定义包含两个核心部分:“识别哪些物体”和“物体的位置”。接下来,我们将对这两部分进行详细解释。

识别哪些物体

“物体”指的是图像中存在的一个物体。例如,在下图中,第一张图片中存在一只猫,第二张图片中则存在多个物体,如狗、鸭、猫和水桶。



那么,目标检测需要识别出图像中的所有物体吗?答案是否定的。通常,需要检测哪些物体是人为设定限制的。这取决于具体的业务或应用场景。例如,在商品检测场景中,我们可能只关心电脑、手机等商品,而不会去检测背景中的猫或房屋。因此,目标检测系统通常被设计为只能识别出预先定义好类别的物体。

物体的位置

“物体的位置”指的是物体在图像中的坐标位置。坐标位置主要有两种表示格式。

以下是两种常见的边界框坐标表示方法:

  1. 极坐标(或角点坐标)表示法:使用边界框左上角和右下角的坐标来表示。

    • 公式(x_min, y_min, x_max, y_max)
    • 说明(x_min, y_min) 是边界框左上角的坐标,(x_max, y_max) 是边界框右下角的坐标。通常以图像左上角为原点 (0, 0)

  2. 中心点坐标表示法:使用边界框的中心点坐标以及框的宽度和高度来表示。

    • 公式(x_center, y_center, width, height)
    • 说明(x_center, y_center) 是边界框中心点的坐标,widthheight 分别是边界框的宽和高。

理解这两种坐标表示方式非常重要,它们在数据标注和模型训练中都会被用到。

目标检测的技术发展历史 📈

了解了目标检测的定义后,我们来看看它是如何一步步发展到今天的。目标检测的技术演进大致可以分为三个阶段。

以下是目标检测技术发展的三个主要阶段:

  1. 传统检测方法

    • 这个阶段的方法通常包含多个步骤:首先使用算法(如滑动窗口)生成大量可能包含物体的“候选区域”,然后从这些区域中手工提取特征(如HOG、SIFT),最后使用分类器(如SVM)判断区域内是否有物体以及是什么物体。流程复杂且效率较低。
  2. 基于候选区域与深度学习的融合方法

    • 随着深度学习的兴起,特征提取部分被卷积神经网络(CNN)所取代,大大提升了特征的表征能力。代表性框架有R-CNN系列(R-CNN, Fast R-CNN, Faster R-CNN)。它们仍然需要先产生候选区域,但整体精度得到了显著提升。
  3. 端到端的单阶段检测方法

    • 这类方法摒弃了独立的候选区域生成步骤,直接在网络中一次性预测物体的类别和位置,因此速度非常快。代表性框架有YOLO系列和SSD。它们在速度和精度之间取得了更好的平衡,更适合实时检测场景。

总结 ✨

本节课中,我们一起学习了目标检测的核心内容。

我们首先明确了目标检测的定义:识别图片中有哪些物体以及物体的位置。并对“物体”(由应用场景人为限定)和“位置”(极坐标或中心点坐标表示)进行了详细解读。

接着,我们回顾了目标检测的技术发展历史,将其分为三个主要阶段:传统检测方法基于候选区域与深度学习的融合方法以及端到端的单阶段检测方法。了解这段历史有助于我们理解不同检测框架的设计思路与优缺点。

掌握这些基础知识,将为后续学习具体的检测算法和模型打下坚实的根基。

🗂️ 课程P40:40.03 格式转换:文件读取与存储逻辑

在本节课中,我们将学习如何将图像数据集(包含图片和XML标注文件)转换为TensorFlow的TFRecord格式。我们将逐步构建一个转换脚本,实现读取文件、分批处理和存储的逻辑。


概述

我们将创建一个名为 dataset_to_tfrecord.py 的脚本。其核心功能是遍历指定目录下的图片和标注文件,每处理200个样本,就将它们打包并存储为一个TFRecord文件。这样做有助于高效地管理和读取大规模数据集。


第一步:创建项目结构与文件夹

首先,我们需要建立项目文件夹结构。我们将创建一个名为 Online_class 的目录,并在其中建立 datasets 文件夹来存放我们的代码和数据。

以下是需要创建的目录结构:

  • Online_class/:项目根目录。
  • Online_class/datasets/:存放数据集处理代码。
  • Online_class/images/:存放原始图片和XML标注文件。

接下来,将准备好的 images 文件夹(内含 annotationsJPEGImages 子文件夹)复制到 Online_class 目录下。


第二步:编写转换脚本

datasets 目录下,创建一个新的Python文件 dataset_to_tfrecord.py

首先,导入必要的库。

import tensorflow as tf
import os

第三步:定义主运行函数 run

run 函数是整个转换流程的入口,它接收三个参数:数据集目录、输出目录和数据集名称。

def run(dataset_dir, output_dir, name='data'):
    """
    运行转换代码的主逻辑。
    参数:
        dataset_dir: 原始数据集的根目录。
        output_dir: 存储TFRecord文件的输出目录。
        name: 数据集名称,用于生成输出文件名。
    """

1. 检查数据集目录是否存在

首先,我们需要确认输入的 dataset_dir 是否存在。如果不存在,可以选择创建它或报错。

    # 第一步:判断数据集目录是否存在
    if not tf.gfile.Exists(dataset_dir):
        # 如果目录不存在,则创建它(或可以选择抛出错误)
        tf.gfile.MakeDirs(dataset_dir)

2. 读取文件列表

我们需要获取 annotations 文件夹下所有XML文件的列表。这些文件名(不含扩展名)将对应图片文件名。

    # 第二步:读取某个文件夹下的所有文件名列表
    # 假设annotations文件夹在dataset_dir下
    annotations_path = os.path.join(dataset_dir, 'annotations')
    # 获取所有XML文件名并排序,以保证顺序一致
    file_names = sorted(os.listdir(annotations_path))

3. 循环处理与分批存储

接下来,我们将循环处理文件列表。每处理200个文件,就将它们打包成一个TFRecord文件。

以下是循环和分批存储的核心逻辑:

    # 第三步:循环文件列表,每200个样本存储为一个文件
    i = 0  # 总文件索引
    file_idx = 0  # TFRecord文件序号
    samples_per_file = 200  # 每个TFRecord文件包含的样本数

    while i < len(file_names):
        # 获取当前要写入的TFRecord文件名
        output_filename = get_output_filename(output_dir, name, file_idx)

        # 创建TFRecord写入器
        with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer:
            j = 0  # 当前文件内样本计数
            while j < samples_per_file and i < len(file_names):
                # 打印转换进度
                print(f'正在转换图片: 第 {i+1} / {len(file_names)} 张')

                # 获取单个文件名(例如:'000001.xml')
                single_file = file_names[i]
                # 提取基础名(例如:'000001'),用于匹配图片
                base_name = single_file[:-4]  # 去掉 '.xml' 后缀
                image_file_name = base_name + '.jpg'  # 对应的图片文件名

                # TODO: 在此处调用函数,读取图片和XML内容,并构造一个Example写入文件
                # convert_and_write_example(...)

                i += 1
                j += 1

        # 完成一个文件的写入后,文件序号增加
        file_idx += 1
        print(f'已成功创建TFRecord文件: {output_filename}')

    print(f'数据集“{name}”的所有图片转换完成。')

第四步:辅助函数:生成输出文件名

我们需要一个辅助函数来根据输出目录、数据集名称和文件索引生成标准的TFRecord文件名。

def get_output_filename(output_dir, dataset_name, index):
    """
    获取输出的TFRecord文件完整路径。
    参数:
        output_dir: 输出目录。
        dataset_name: 数据集名称。
        index: 文件序号。
    返回:
        完整的文件路径字符串。
    """
    # 文件名格式示例:`data_train_000.tfrecord`
    filename = f"{dataset_name}_{index:03d}.tfrecord"
    return os.path.join(output_dir, filename)

总结

本节课中,我们一起学习了TFRecord格式转换的第一步:搭建项目结构并编写文件读取与分批存储的核心逻辑。我们创建了 run 函数,它能够检查目录、遍历文件列表,并规划好每200个样本存储为一个TFRecord文件。在下一节中,我们将实现具体的 convert_and_write_example 函数,完成从图片和XML中读取数据并写入TFRecord文件的最后一步。

课程 P41:格式转换:图片数据以及XML读取 🖼️📄

在本节课中,我们将学习如何将图片数据和对应的XML标注文件读取并处理,为后续封装成TFRecord格式做准备。我们将重点关注如何从XML文件中提取关键信息,并对边界框坐标进行归一化处理。

概述

上一节我们介绍了将数据集分割并写入多个TFRecord文件的整体逻辑。本节中,我们来看看如何具体处理每一张图片及其对应的XML文件,提取必要信息,以便封装成一个tf.train.Example

核心逻辑与函数定义

我们的核心任务是为每一张图片执行以下三步操作:

  1. 读取图片的二进制数据及其XML文件内容。
  2. 将提取的信息封装成一个tf.train.Example
  3. 使用TFRecordWriter将序列化后的Example写入文件。

我们首先定义一个函数 add_to_tfrecord 来实现这个流程。

def add_to_tfrecord(output_filename, image_name, tfrecord_writer):
    """
    添加一个图片文件内容以及XML内容写入到TFRecord文件当中。
    参数:
        output_filename: 输出文件路径。
        image_name: 图片编号/名称。
        tfrecord_writer: TFRecord文件写入实例。
    """
    # 1. 读取图片数据及XML内容
    # 2. 将图片封装成Example
    # 3. 写入Example的序列化结果
    return None

第一步:读取图片与XML内容

以下是实现第一步“读取图片数据及XML内容”的详细步骤。我们将这些步骤封装在一个独立的函数 process_image 中。

1. 读取图片二进制数据

使用TensorFlow的 gfile 模块以二进制模式读取图片文件。

import tensorflow as tf
image_filename = f'{dataset_dir}/JPEGImages/{image_name}.jpg'
with tf.gfile.FastGFile(image_filename, 'rb') as f:
    image_data = f.read()

2. 解析XML文件

我们将使用Python的 xml.etree.ElementTree 库来解析XML标注文件,提取图片尺寸、物体类别、边界框等信息。

import xml.etree.ElementTree as ET

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/6aaa25960e8f12f4d7ede28718c8df15_75.png)

xml_filename = f'{dataset_dir}/Annotations/{image_name}.xml'
tree = ET.parse(xml_filename)
root = tree.getroot()

3. 提取关键信息

我们需要从XML树结构中提取以下信息:

  • 图片尺寸:宽度、高度、通道数。
  • 物体信息:每个物体的类别名称、对应的数字编号、难度标志(difficult)、截断标志(truncated)以及边界框坐标。

以下是提取这些信息的代码逻辑:

# 获取图片尺寸
size = root.find('size')
shape = [
    int(size.find('height').text),
    int(size.find('width').text),
    int(size.find('depth').text)
]

# 初始化列表,用于存储所有物体的信息
labels = []          # 物体类别对应的数字编号
labels_text = []     # 物体类别名称(字符串)
difficult = []       # 难度标志列表
truncated = []       # 截断标志列表
bboxes = []          # 边界框坐标列表

# 遍历XML中的所有‘object’节点
for obj in root.findall('object'):
    # 获取类别名称
    label_name = obj.find('name').text
    labels_text.append(label_name)

    # 将类别名称映射为数字编号(此处需要一个预定义的映射字典,例如 VOC_LABELS)
    labels.append(VOC_LABELS[label_name][0])

    # 提取difficult标志,若不存在则默认为0
    isdifficult = obj.find('difficult')
    difficult.append(0 if isdifficult is None else int(isdifficult.text))

    # 提取truncated标志,若不存在则默认为0
    istruncated = obj.find('truncated')
    truncated.append(0 if istruncated is None else int(istruncated.text))

    # 提取边界框坐标 (xmin, ymin, xmax, ymax)
    bbox = obj.find('bndbox')
    # 注意:为了与后续模型计算兼容,我们存储顺序为 (ymin, xmin, ymax, xmax)
    # 并对坐标进行归一化(除以图片高度或宽度),使其值在0~1之间。
    ymin = float(bbox.find('ymin').text) / shape[0]
    xmin = float(bbox.find('xmin').text) / shape[1]
    ymax = float(bbox.find('ymax').text) / shape[0]
    xmax = float(bbox.find('xmax').text) / shape[1]

    bboxes.append([ymin, xmin, ymax, xmax])

核心概念解释:边界框归一化

  • 公式坐标值 / 图片对应维度尺寸
  • 目的:将边界框的绝对坐标转换为相对于图片尺寸的比例值(0到1之间)。这样做的好处是使坐标值与图片的具体分辨率解耦,便于模型进行统一的回归计算,是目标检测任务中的常见预处理步骤。

第二步:封装为TFRecord Example

在获取了所有必要信息(image_data, shape, labels, labels_text, difficult, truncated, bboxes)后,下一步就是将它们按照tf.train.Example要求的BytesListInt64ListFloatList格式进行封装。这部分代码将在下一节详细展开。

总结

本节课中我们一起学习了处理VOC数据集中单张图片及其XML标注文件的核心流程。我们实现了:

  1. 使用 tf.gfile 读取图片二进制数据。
  2. 使用 xml.etree.ElementTree 解析XML文件,并提取了图片尺寸、物体类别、边界框等关键信息。
  3. 理解了边界框坐标归一化的重要性及其计算方法。

这些处理后的数据已经准备好,可以在下一环节被封装成标准的 tf.train.Example 格式,并写入TFRecord文件,从而构建高效、易于读取的数据管道。

课程 P42:42.05_格式转换:example封装与总结 📚

在本节课中,我们将学习如何将读取到的图片和XML标注数据,封装成TensorFlow的tf.train.Example格式,并最终写入TFRecord文件。这是构建高效数据管道的关键一步。


数据读取逻辑回顾 🔍

上一节我们介绍了如何从VOC2007数据集中读取图片和XML文件。本节中,我们来看看如何将这些数据封装起来。

以下是读取逻辑的核心总结:

  • 读取图片:使用 tf.io.gfile.GFile(image_path, 'rb').read() API。
  • 读取XML:使用 ET.parse(xml_path).getroot() 获取根节点,然后通过 root.find('object').find('bndbox').find('xmin').text 等方式提取标注信息。
  • 数据标准化:将边界框坐标进行归一化处理,公式为 x / image_widthy / image_height。目的是为了适应后续算法的运算需求。


封装数据为 Example 📦

读取数据后,下一步是把取出的数据封装成一个tf.train.Example对象。

我们的封装原则是:尽可能存储所有详细信息,方便后续读取和使用。这意味着,不仅存储图片原始数据、高宽和通道数,还要将边界框信息从 [ymin, xmin, ymax, xmax] 的对象列表格式,转换为四个独立的坐标列表。

以下是格式转换的核心逻辑:

# 假设 b_boxes 是一个列表,每个元素是一个对象的 [ymin, xmin, ymax, xmax]
ymin_list, xmin_list, ymax_list, xmax_list = [], [], [], []
for b in b_boxes:
    ymin_list.append(b[0])
    xmin_list.append(b[1])
    ymax_list.append(b[2])
    xmax_list.append(b[3])

转换完成后,我们使用 tf.train.Example 进行封装。关键点在于,存入的每个属性都必须明确指定其TensorFlow数据类型。

以下是封装 Example 的核心API结构:

example = tf.train.Example(features=tf.train.Features(feature={
    'image/height': int64_feature(height),
    'image/width': int64_feature(width),
    'image/filename': bytes_feature(filename.encode('utf8')),
    'image/source_id': bytes_feature(filename.encode('utf8')),
    'image/encoded': bytes_feature(encoded_image_data),
    'image/format': bytes_feature(image_format.encode('utf8')),
    'image/object/bbox/xmin': float_list_feature(xmin_list),
    'image/object/bbox/xmax': float_list_feature(xmax_list),
    'image/object/bbox/ymin': float_list_feature(ymin_list),
    'image/object/bbox/ymax': float_list_feature(ymax_list),
    'image/object/class/text': bytes_list_feature(encoded_label_text_list),
    'image/object/class/label': int64_list_feature(label_list),
}))

其中,int64_feature, float_list_feature, bytes_feature 等是辅助函数,用于将Python数据转换为TensorFlow所需的Feature类型。


写入 TFRecord 文件 💾

将单张图片的数据封装成 Example 后,最后一步就是将其写入TFRecord文件。

写入时需要注意:必须写入序列化后的结果。每一张图片对应写入一次。

以下是写入文件的核心API:

with tf.io.TFRecordWriter(output_path) as writer:
    serialized_example = example.SerializeToString()
    writer.write(serialized_example)

tf.io.TFRecordWriter 用于创建写入器。
example.SerializeToString() 用于将Example对象序列化为二进制字符串。


课程总结 ✨

本节课中我们一起学习了:

  1. 掌握XML文件读取:使用 xml.etree.ElementTree 解析XML并提取关键标注信息。
  2. 掌握Example的构造:理解了 tf.train.Example 的结构,学会了如何将多样化的数据(整型、浮点型列表、字节数据)按照指定格式封装进去。
  3. 完成数据到TFRecord的转换:实现了从原始数据集到高效、可序列化的TFRecord文件的完整转换流程,为后续使用TensorFlow数据管道加载数据奠定了基础。

关键是要记住封装时的原则:信息尽可能详细,关键信息(如图像尺寸、边界框、类别)必须存储,并且要正确指定每个字段的数据类型。

TensorFlow Slim库介绍与TFRecords数据读取教程 🧠

在本节课中,我们将学习如何使用TensorFlow的Slim库来简化模型构建和数据读取流程,特别是如何读取之前存储好的TFRecords文件。


概述

上一节我们介绍了如何将数据存储为TFRecords格式。本节中,我们来看看如何高效地读取这些数据以供模型训练使用。为此,我们将引入TensorFlow Slim库,这是一个旨在简化TensorFlow代码编写的轻量级库。

什么是TensorFlow Slim库?🤔

TensorFlow Slim库是TensorFlow内部定义的一个用于训练和评估复杂模型的轻量级库。它的功能类似于Keras、TFLearn等高级库,核心目的是使TensorFlow代码更加简洁。由于原生TensorFlow的代码步骤较多且接近算法底层,Slim库通过提供高级抽象来简化这一过程。

Slim库包含了许多模块,例如:

  • 数据读取 (slim.data)
  • 网络层定义 (slim.layers)
  • 损失函数 (slim.losses)
  • 评估指标 (slim.metrics)
  • 训练循环 (slim.learning)

导入Slim库非常简单:

import tensorflow.contrib.slim as slim

Slim库读取数据的流程 📖

Slim库读取数据的流程清晰且分为两个主要步骤。以下是这两个步骤的概述:

  1. 准备数据集规范信息:定义数据的结构、解码方式等信息。
  2. 通过Provider读取数据:使用定义好的规范,实际读取并供给数据。

具体来说,在TensorFlow Slim中读取数据只需两步:
第一步,准备 dataset 规范信息;第二步,直接通过 provider 进行读取。

第一步:准备Dataset规范

这一步的核心是封装一个 Dataset 实例,告诉Slim我们想要什么数据以及数据的格式。一个关键的参数是 decoder(解码器)。

为什么需要解码器?
因为数据是以Protocol Buffer协议序列化后存储在TFRecords文件中的。读取时,我们需要一个“反序列化”的过程来解析这个协议,将二进制数据转换回我们可以使用的格式。

解码过程分为两步:

  1. 反序列化原始格式:将数据从Protocol Buffer协议解析回其原始的存储格式(例如 int64, float)。
  2. 解码为高级格式:将原始格式的数据进一步解码,封装成用户可以直接通过预定名称(如 image, label, bbox)访问的高级格式。

对于目标检测任务,Slim特别提供了对边界框(Bounding Box)的支持。需要注意的是,提供给Slim的边界框格式必须是 [ymin, xmin, ymax, xmax]。这与我们之前存储数据时强调的顺序是一致的。

最终,我们会使用 tf.example_decoder 来完成解析。

第二步:通过Provider读取

准备好 Dataset 规范后,就可以使用 slim.dataset_data_provider.DatasetDataProvider 来实际读取数据了。它会根据我们定义的规范,每次返回一个样本的数据(如图片、标签、边界框)。

代码实现示例 💻

以下是实现上述第一步(准备Dataset规范)的关键代码逻辑概述:

我们需要准备相关参数并填入 slim.dataset.Dataset 的构造函数中。主要参数包括:

  • data_sources: TFRecords文件路径。
  • reader: 读取器,例如 tf.TFRecordReader
  • decoder: 配置好的解码器,如 tf.example_decoder
  • num_samples: 数据集的样本总数。
  • num_classes: 数据的类别总数。

以下是一个简化的代码框架:

import tensorflow as tf
import tensorflow.contrib.slim as slim

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/8409ea6aaf3ddf4d1e231ae05270bf2d_29.png)

# 1. 定义解码器
keys_to_features = {
    'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''),
    'image/format': tf.FixedLenFeature((), tf.string, default_value='jpeg'),
    'image/object/bbox/xmin': tf.VarLenFeature(dtype=tf.float32),
    # ... 定义其他特征
}
items_to_handlers = {
    'image': slim.tfexample_decoder.Image('image/encoded', 'image/format'),
    'bbox': slim.tfexample_decoder.BoundingBox(
                ['ymin', 'xmin', 'ymax', 'xmax'], 'image/object/bbox/'),
    # ... 定义其他数据的处理器
}
decoder = slim.tfexample_decoder.TFExampleDecoder(keys_to_features, items_to_handlers)

# 2. 创建Dataset实例
dataset = slim.dataset.Dataset(
    data_sources=['path/to/your.tfrecord'],
    reader=tf.TFRecordReader,
    decoder=decoder,
    num_samples=4952, # 你的样本总数
    num_classes=20,   # 你的类别总数
    items_to_descriptions={'image': 'A color image.', 'bbox': 'A list of bounding boxes.'}
)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/8409ea6aaf3ddf4d1e231ae05270bf2d_31.png)

# 3. 创建Provider并获取数据
provider = slim.dataset_data_provider.DatasetDataProvider(dataset)
[image, bbox, label] = provider.get(['image', 'bbox', 'label'])
# 此时 image, bbox, label 就是可用的Tensor

运行上述代码后,你可以检查获取到的张量形状,例如 image 的形状可能是 (?, ?, 3),表示可变的高度、宽度和3个颜色通道。

总结

本节课中,我们一起学习了:

  1. TensorFlow Slim库的作用:一个用于简化TensorFlow模型开发的高级库。
  2. Slim库读取TFRecords的两步流程:首先准备Dataset数据规范,然后通过Provider读取。
  3. 解码器(Decoder)的核心作用:负责将TFRecords中的序列化数据解析回可用的高级格式,并特别注意了边界框的格式要求。
  4. 基本的代码实现思路:通过定义特征映射、创建Dataset和Provider来完成数据读取。

接下来,你就可以利用定义好的数据供给流程,将数据输入到使用Slim构建的模型中进行训练了。

课程 P44:TFRecord读取 - Dataset准备 🗂️

在本节课中,我们将学习如何为Pascal VOC 2007数据集编写一个规范的Dataset读取函数。我们将使用TensorFlow的slim模块来构建一个标准的数据读取流程,该流程能够从TFRecord文件中解析出图像和标注信息。

概述

上一节我们介绍了TFRecord文件的写入逻辑,本节中我们来看看如何从TFRecord文件中读取数据,并将其封装成一个符合TensorFlow Dataset API规范的Dataset对象。我们将创建一个名为get_dataset的函数来完成这个任务。

创建读取函数

首先,我们创建一个新的Python文件,用于编写Pascal VOC 2007数据集的读取逻辑。

import os
import tensorflow as tf
from tensorflow.contrib import slim

我们定义一个名为get_dataset的函数,它接收数据集目录作为参数,并返回一个Dataset对象。

def get_dataset(dataset_dir):
    """
    获取Pascal VOC 2007数据集。
    Args:
        dataset_dir: 数据集的目录。
    Returns:
        一个符合Dataset API规范的数据集对象。
    """
    # 函数实现将放在这里
    pass

准备Dataset参数

要构建一个Dataset,我们需要准备几个核心参数:数据源路径、读取器(reader)和解码器(decoder)。

1. 构造数据源路径

第一个参数是数据源,即TFRecord文件的路径模式。我们使用通配符来匹配目录下所有相关的文件。

# 构造数据源路径
file_pattern = os.path.join(dataset_dir, 'pascal_2007_*.tfrecord')

2. 准备读取器(Reader)

第二个参数是读取器,它定义了如何读取TFRecord文件。我们使用tf.TFRecordReader

# 准备读取器
reader = tf.TFRecordReader

3. 准备解码器(Decoder)

解码器是最复杂的部分,它负责将序列化的TFRecord数据解析成可用的张量。它分为两部分:keys_to_featuresitems_to_handlers

以下是解码器所需的核心字典定义:

# 定义 keys_to_features, 指定TFRecord中存储的原始数据格式
keys_to_features = {
    'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''),
    'image/format': tf.FixedLenFeature((), tf.string, default_value='jpeg'),
    'image/height': tf.FixedLenFeature([1], tf.int64),
    'image/width': tf.FixedLenFeature([1], tf.int64),
    'image/channels': tf.FixedLenFeature([1], tf.int64),
    'image/shape': tf.FixedLenFeature([3], tf.int64),
    'image/object/bbox/xmin': tf.VarLenFeature(dtype=tf.float32),
    'image/object/bbox/ymin': tf.VarLenFeature(dtype=tf.float32),
    'image/object/bbox/xmax': tf.VarLenFeature(dtype=tf.float32),
    'image/object/bbox/ymax': tf.VarLenFeature(dtype=tf.float32),
    'image/object/bbox/label': tf.VarLenFeature(dtype=tf.int64),
    'image/object/bbox/difficult': tf.VarLenFeature(dtype=tf.int64),
    'image/object/bbox/truncated': tf.VarLenFeature(dtype=tf.int64),
}

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/7376838b6fac37f89f79dbaaedb141ba_20.png)

# 定义 items_to_handlers, 指定如何将解析后的特征映射到最终输出的张量
items_to_handlers = {
    'image': slim.tfexample_decoder.Image('image/encoded', 'image/format'),
    'shape': slim.tfexample_decoder.Tensor('image/shape'),
    'object/bbox': slim.tfexample_decoder.BoundingBox(
            ['ymin', 'xmin', 'ymax', 'xmax'], 'image/object/bbox/'),
    'object/label': slim.tfexample_decoder.Tensor('image/object/bbox/label'),
    'object/difficult': slim.tfexample_decoder.Tensor('image/object/bbox/difficult'),
    'object/truncated': slim.tfexample_decoder.Tensor('image/object/bbox/truncated'),
}

关键点解释:在items_to_handlers中,BoundingBox处理器默认只读取每个列表的第一个元素。这意味着,如果一张图片有多个标注框,这里只会返回第一个框的坐标。在实际应用中,你可能需要根据任务调整这个逻辑。

准备好这两个字典后,我们可以创建解码器:

# 构造解码器
decoder = slim.tfexample_decoder.TFExampleDecoder(keys_to_features, items_to_handlers)

组装Dataset对象

现在,我们已经准备好了所有必需的参数,可以使用slim.dataset.Dataset类来创建最终的Dataset对象。

以下是组装Dataset的完整代码:

def get_dataset(dataset_dir):
    """
    获取Pascal VOC 2007数据集。
    Args:
        dataset_dir: 数据集的目录。
    Returns:
        一个符合Dataset API规范的数据集对象。
    """
    # 1. 构造数据源路径
    file_pattern = os.path.join(dataset_dir, 'pascal_2007_*.tfrecord')

    # 2. 准备读取器
    reader = tf.TFRecordReader

    # 3. 准备解码器
    keys_to_features = { ... } # 如上文所示
    items_to_handlers = { ... } # 如上文所示
    decoder = slim.tfexample_decoder.TFExampleDecoder(keys_to_features, items_to_handlers)

    # 4. 创建并返回Dataset对象
    dataset = slim.dataset.Dataset(
        data_sources=file_pattern,
        reader=reader,
        decoder=decoder,
        num_samples=4952, # 数据集中样本的总数
        items_to_descriptions={
            'image': 'A color image of varying size.',
            'shape': 'Shape of the image',
            'object/bbox': 'A list of bounding boxes.',
            'object/label': 'A list of labels, one for each object.',
            'object/difficult': 'A list of difficulty flags.',
            'object/truncated': 'A list of truncation flags.',
        },
        num_classes=20 # VOC数据集的类别数
    )
    return dataset

核心API总结

本节课中我们一起学习了如何使用TensorFlow Slim来规范地读取TFRecord数据集。以下是涉及的核心API:

  • slim.dataset.Dataset:用于创建标准数据集对象的类。
  • tf.TFRecordReader:用于读取TFRecord文件的读取器类。
  • slim.tfexample_decoder.TFExampleDecoder:用于解析TFExample协议缓冲区的解码器类。

构建Dataset时需要重点准备的三个参数是:

  1. data_sources:数据文件路径模式。
  2. reader:文件读取器。
  3. decoder:数据解码器,其核心是keys_to_featuresitems_to_handlers两个字典的定义。

总结

在本节课中,我们完成了一个从TFRecord文件读取Pascal VOC 2007数据集的完整函数。我们学习了如何构造数据源路径、指定读取器、以及最关键的一步——定义解码器来将序列化的数据还原为图像和标注张量。通过slim.dataset.Dataset类,我们将这些组件封装成一个标准、易用的Dataset对象,为后续的模型训练做好了数据准备。理解这个流程是使用TensorFlow高效处理自定义数据集的基础。

课程 P45:TFRecord 读取 - Provider 读取 🧠

在本节课中,我们将学习如何使用 TensorFlow 的 slim 模块中的 provider 来读取之前准备好的 TFRecord 数据集。我们将重点关注如何从数据规范中获取实际的数据样本。


概述

上一节我们介绍了如何准备数据集的规范信息。本节中,我们来看看如何使用 provider 来实际读取这些数据。provider 是一个数据提供器,它可以根据我们定义的数据集规范,高效地加载和提供数据样本。

数据读取步骤

以下是使用 provider 读取数据的主要步骤:

  1. 导入必要模块:导入数据集模块和 TensorFlow。
  2. 获取数据集规范:调用数据集模块的函数,获取数据集的规范对象。
  3. 创建数据提供器:使用 slim.dataset_data_provider.DatasetDataProvider 创建数据提供器。
  4. 获取数据:通过提供器的 get 方法,按需获取指定字段的数据。

接下来,我们将详细实现每一步。

代码实现

以下是实现数据读取的完整代码逻辑。

首先,我们需要导入必要的模块。

import tensorflow as tf
from datasets import pascalvoc_2007
from tensorflow.contrib import slim

接着,我们定义一个主函数来执行读取操作。

def main():
    # 第一步:获取数据集规范
    dataset = pascalvoc_2007.get_dataset('./images/tfrecords/voc_tfrecords/')

    # 第二步:通过 provider 创建数据提供器
    provider = slim.dataset_data_provider.DatasetDataProvider(
        dataset,
        num_readers=3  # 指定读取线程数
    )

    # 第三步:通过 get 方法获取指定名称的数据
    # 可以按需获取,不一定要获取所有字段
    [image, shape, bbox, label] = provider.get(['image', 'shape', 'object/bbox', 'object/label'])

    # 打印获取到的数据(以 Tensor 形式显示)
    print(image, shape, bbox, label)

参数说明

在创建 DatasetDataProvider 时,有几个关键参数可以配置:

  • dataset:必需参数,即第一步获取的数据集规范对象。
  • num_readers:读取数据时使用的线程数,可以提升读取效率。
  • 其他参数如队列大小、是否打乱数据等均为可选,可以根据需要设置。

在调用 provider.get() 方法时,需要传入一个列表,指定要获取的数据字段名称。这些名称必须与准备数据集规范时定义的键名一致。你可以选择获取全部字段,也可以只获取训练所需的部分字段。

总结

本节课中我们一起学习了使用 provider 读取 TFRecord 数据的完整流程。核心在于两步:首先准备好数据集的规范,然后通过 DatasetDataProvider 创建提供器并利用 get 方法按需获取数据。掌握这个方法,你就能灵活高效地从 TFRecord 文件中加载数据用于模型训练了。

课程P46:第二阶段总结 🧠

在本节课中,我们将对第二阶段——数据集处理的核心内容进行总结。我们将回顾从数据获取、格式转换到最终读取的完整流程,并梳理其中的关键概念与代码实现。

数据集处理概述

上一节我们介绍了数据集处理的完整代码逻辑。本节中,我们来看看整个阶段的要点总结。

数据集处理阶段主要涉及如何获取数据以及需要获取哪些数据。其核心目标是准备用于模型训练的规范化数据。

数据集种类与格式

我们介绍了常见的数据集种类。

以下是两种典型的数据集:

  • VOC数据集:提供XML格式的标注文件以及图片。
  • ImageNet V4数据集:通常提供JSON等多种格式的标注文件,结构更为复杂。

数据标记

标记数据集的目的是确定需要标注的数据类别和内容。这通常由产品、业务或数据标注团队决定,而非开发者。

数据标记后,对于目标检测任务,通常会生成XML标注文件和对应的图片文件

格式转换的目的与原因

我们进行数据集格式转换,主要是为了提供结构更简单、更高效的数据给训练过程或其他用户使用。

我们采用的转换格式是基于 Protobuf协议TFRecords文件。这种格式的特点是速度快、体积小,是跨平台和GRPC传输中常用的协议。

存储为TFRecords文件

在转换格式时,我们选择将数据存储到TFRecords文件中。通常,每个文件会指定一个固定大小,以避免文件过大。

以下是存储过程的关键步骤:

1. 读取数据

  • 读取图片使用:tf.gfile.GFile
  • 读取XML使用:ET 工具

2. 封装与写入
使用 tf.train.Example 协议将数据封装起来。封装的原则是尽可能多地存储与图片相关的信息。封装后,使用 write 方法将序列化的结果写入文件。

3. 关键处理:边界框归一化
在存储过程中,有一个必须注意的操作:将标注的边界框(Bounding Box)坐标进行归一化处理(即除以图片的长和宽)。这样处理后的数据可以直接提供给后续模型使用。

读取TFRecords文件

数据存储后,我们需要从中读取以供训练。读取TFRecords文件的流程分为两步:

1. 准备数据集规范
使用 tf.data.TFRecordDataset API来准备数据集的规范信息。相关的参数和函数都在此API中定义。

2. 获取数据
通过 provider 工具中的 get 方法来获取数据。这个过程包括准备数据集对象和解码(decode)数据。

总结

本节课中,我们一起学习了数据集处理第二阶段的完整总结。我们回顾了:

  1. 常见的数据集种类(如VOC、ImageNet)及其格式。
  2. 数据标记的职责归属。
  3. 将原始数据转换为 TFRecords文件 的目的、原因及具体步骤,包括数据读取、tf.train.Example封装、边界框归一化等关键操作。
  4. 从TFRecords文件中读取数据的两步流程:准备数据集规范和通过provider获取数据。

至此,数据集处理阶段的核心内容已全部梳理完毕。

课程 P47:项目架构设计 🏗️

在本节课中,我们将学习如何为一个AI项目设计完整的实现架构。我们将从整体流程出发,详细讲解从数据处理到模型部署,再到用户交互的每一个环节,并解释为何采用特定的技术方案来实现解耦与高效协作。


项目结构介绍

上一节我们介绍了项目的整体目标,本节中我们来看看项目的具体开发流程与结构设计。

一个清晰的项目结构是成功实现的基础。你应该能够说明项目的开发流程,并应用TensorFlow完成数据与代码模块的编写。

以下是项目架构的整体设计图,它清晰地展示了从数据到用户的全流程:

整个模型流程可分为三层:

  1. 数据采集层:负责数据的标注与存储。
  2. 深度模型训练层:负责模型的训练与导出。
  3. 用户层:负责提供交互界面与业务逻辑。

具体流程如下:

  • 在数据采集层,将标注好的XML文件和图片数据存储后,统一转换为 TFRecords 文件格式。
  • 在深度模型层,读取 TFRecords 文件进行预处理,并将数据送入算法模型,利用多GPU进行训练,最终生成一个训练好的模型。
  • 训练好的模型通过 TensorFlow Serving 进行部署。后台服务通过调用Serving提供的接口,实现Web端的业务逻辑。
  • 最终用户通过前端界面与后台交互,获得模型预测结果。

架构设计详解与解耦思想

了解了整体流程后,我们来深入探讨为何要这样设计,其中的核心在于“解耦”。

如果简单地将训练导出的模型文件直接交给使用者,会遇到诸多问题。模型使用者需要关心模型的版本迭代、文件路径以及内部细节的修改,这增加了使用的复杂度和耦合性。

我们的核心目标是:让模型导出方与模型使用方的业务解耦。使用者无需关心模型如何训练和版本更替,只需专注于使用模型功能。

为实现这一目标,我们引入了 TensorFlow Serving 作为模型部署的中间层。

TensorFlow Serving 的优势

TensorFlow Serving 部署在固定的服务器接口上,其主要优势在于:

  1. 模型热更新:当需要更新模型时,只需将新版模型文件上传至服务器指定位置。TensorFlow Serving 会自动检测并加载新模型,无需重启服务。服务接口始终保持不变。
    • 公式表示服务接口 = 恒定
  2. 提供标准化接口:TensorFlow Serving 会对外提供两种类型的接口供客户端调用:
    • gRPC 接口:基于高效的 Protobuf 协议。
    • RESTful API 接口:基于 HTTP 协议,更易于 Web 集成。

客户端与Web后台的角色

通常,我们会编写一个 TensorFlow Serving 客户端。这个客户端只做两件事:输入数据获取输出结果。其逻辑非常简单。

# 伪代码示例:客户端核心逻辑
def tf_serving_client(input_data):
    # 1. 将数据发送至 TensorFlow Serving 的固定接口
    # 2. 接收并返回预测结果
    prediction_result = call_serving_api(input_data)
    return prediction_result

我们会将这个客户端嵌入到 Web 后台 中。Web 后台负责处理用户请求、业务逻辑,然后调用上述客户端获取模型预测结果,最后将处理好的结果返回给前端用户。

通过这样的设计:

  • 模型使用者(Web后台):只需通过固定客户端调用模型,不关心模型文件与版本。
  • 模型提供者:只需维护和更新 TensorFlow Serving 中的模型文件。
  • 双方通过定义良好的接口(gRPC或RESTful)进行通信,实现了业务上的解耦。

总结

本节课中我们一起学习了AI项目的完整架构设计。我们从项目流程图入手,理解了数据层、模型层和用户层的分工。随后,我们深入探讨了引入 TensorFlow Serving 进行模型部署的核心价值,即实现模型训练与使用的解耦。通过固定接口、热更新机制和专用的Serving客户端,我们构建了一个稳定、可维护且易于协作的项目架构,为后续的具体实现打下了坚实的基础。

课程 P48:训练与测试整体结构设计 🏗️

在本节课中,我们将学习如何为一个目标检测项目设计训练与测试部分的代码架构。我们将重点探讨如何实现模型、数据和预处理模块之间的解耦,以便灵活地组合不同的组件进行实验和开发。


概述

上一节我们介绍了项目的整体架构。本节中,我们来看看训练与测试部分的具体结构设计。我们的核心目标是设计一个灵活、可扩展的代码架构,使得模型训练过程能够轻松适配不同的数据集、预处理方法和模型算法。

整个开发流程主要分为两部分:

  1. 模型的训练与测试。
  2. 模型的部署与应用。

我们关注的重点是第一部分的代码架构设计。部署部分(如TensorFlow Serving、Web客户端)更多是配置和应用,对于模型开发者而言,可以直接使用已准备好的工具。

训练代码架构设计目标

训练和测试都有各自的步骤与流程。我们首先需要设计训练部分的代码架构。

我们现在拥有的核心模块包括:

  • 数据模块:负责提供训练和测试数据。
  • 模型网络模块:包含不同的目标检测算法。
  • 预处理模块:处理图像数据以满足不同模型的输入要求。

我们的设计目标是创建一个流程,使得在训练和测试时能够灵活地使用这三个部分。我们将其概念化为三个“工厂”:

  • Data Factory:数据工厂
  • Process Factory:预处理工厂
  • Model Factory:模型工厂

这意味着,当我想训练一个模型时,可以像在工厂中选择零件一样,自由组合。例如,我可以选择使用PASCAL VOC数据集、Faster R-CNN算法以及相应的预处理流程。这种设计的核心目的是实现解耦合

具体而言,我们希望达到:

  • 模型与数据之间解耦合:更换数据集时,无需修改模型代码。
  • 数据与预处理之间解耦合:数据读取逻辑独立于具体的预处理操作。
  • 预处理与模型之间解耦合:预处理流程可以根据模型需求独立配置。

通过这种设计,当引入一个新的数据集时,我们只需要在Data Factory下增加该数据集的读取模块,即可与现有模型和预处理流程配合使用,无需改动其他部分的代码。这极大地提高了代码的复用性和可维护性。

项目文件结构

为了实现上述架构,我们的代码文件结构安排如下。以下是项目文件夹的核心构成:

project/
├── datasets/          # 数据工厂模块 (Data Factory)
├── preprocessing/     # 预处理工厂模块 (Process Factory)
├── nets/             # 模型工厂模块 (Model Factory)
├── configs/          # 配置文件
└── ...               # 其他辅助文件夹(如训练脚本、工具函数等)

在这三个核心模块中:

  • datasets模块:用于读取和管理不同的数据集(如PASCAL VOC, COCO等)。
  • preprocessing模块:包含各种图像预处理和数据增强方法。
  • nets模块:存放不同的模型网络定义(如SSD, Faster R-CNN, YOLO等)。

其他目录主要用于存放配置、训练主程序、工具脚本等,我们将在后续课程中逐一添加和讲解。

总结

本节课中,我们一起学习了目标检测项目训练与测试部分的代码架构设计。我们明确了将代码划分为数据工厂预处理工厂模型工厂三个核心模块的设计思想,其核心优势在于实现了各组件间的解耦合。这种设计使得组合不同的数据集、模型和预处理流程变得非常灵活,为后续的模型训练、测试和迭代奠定了坚实的基础。下一节,我们将开始着手实现这些工厂模块。

课程 P49:49.01_数据接口:商品格式转换实现 🛠️

在本节课中,我们将学习如何为商品数据集编写数据接口模块,核心任务是实现数据格式的转换,将原始的图片和XML标注文件转换为TensorFlow的TFRecords格式,以便后续高效地读取和训练模型。

概述 📋

我们的目标是修改并完善数据读取模块,使其能够适配商品数据集。具体而言,我们将创建一个数据工厂模式,以支持不同数据集的读取逻辑。本节课将首先完成将商品数据集转换为TFRecords文件的需求。

模块设计 🏗️

上一节我们明确了功能需求,本节中我们来看看模块的目录结构设计。整个数据接口模块的目录安排如下:

  • dataset_factory:数据工厂,负责根据配置获取不同数据集的读取逻辑。
  • dataset_init:保存不同数据集的具体读取逻辑。
  • utils:存放一些公共的工具组件。
  • dataset_config:存放数据读取过程中涉及的各种配置。
  • dataset_to_tfrecords:一个独立的文件,专门负责数据集的转换过程,逻辑较为简单,因此直接放在datasets目录下。

这样的设计使得代码结构清晰,易于维护和扩展。

实现商品数据集转换 🔄

接下来,我们开始实现第一个核心需求:将原始商品数据集转换为TFRecords文件。

首先,我们需要建立项目环境。我们将基于之前数据模块的代码版本进行修改。

以下是具体的操作步骤:

  1. 新建一个项目文件夹,例如 online_class_v2.0
  2. 将之前数据模块版本的相关代码(datasets, utils目录)复制到新文件夹中。
  3. 将商品数据集(包含commodity图片和TFRECORD相关文件)放入新项目下的image目录中。

完成环境搭建后,我们开始修改转换逻辑。关键的修改点在于数据集的类别配置。原始代码使用的是VOC2007数据集的类别,我们需要将其替换为商品数据集的类别。

我们创建一个新的配置文件,例如在 dataset_config 中定义商品数据集的类别列表:

# dataset_config/commodity_config.py
COMMODITY_LABELS = ['手机', '平板电脑', '笔记本电脑', '耳机', ...] # 你的商品类别列表

然后,在转换脚本 dataset_to_tfrecords.py 中,导入并使用这个新的配置。

接下来,指定输入和输出路径。

  • 输入路径:指向 image/commodity 目录下的图片和XML文件。
  • 输出路径:我们新建一个目录来存储生成的TFRecords文件,例如 image/commodity_tfrecord

在转换时,我们需要为生成的数据集文件命名。命名应遵循一定规范,通常包含数据集名称和日期,并区分训练集(train)和测试集(test)。即使当前商品数据集未明确划分,也建议先以 train 命名,便于后续统一读取逻辑。

例如,可以这样命名生成的文件:commodity_2018_train.tfrecord

运行转换脚本后,我们可以在输出目录 image/commodity_tfrecord 中看到生成的 .tfrecord 文件,这标志着第一个需求已成功实现。

总结 📝

本节课中我们一起学习了数据接口模块的初步实现。我们首先设计了模块的目录结构,引入了数据工厂的概念。然后,我们重点完成了将商品数据集从原始格式(图片+XML)转换为TFRecords文件的过程。关键步骤包括:搭建项目环境、修改数据集类别配置、指定输入输出路径以及规范输出文件的命名。

通过本节课的学习,我们已经为商品数据准备好了高效的存储格式,为后续构建能够读取多种数据集的数据工厂打下了坚实的基础。下一节课,我们将在此基础上,实现数据工厂和TFRecords数据集的读取逻辑。

课程P5:目标检测应用场景与开发环境搭建 🎯

在本节课中,我们将学习目标检测技术的实际应用场景,并了解如何搭建后续学习所需的开发环境。目标检测是计算机视觉的核心任务之一,理解其应用有助于我们明确学习方向。

目标检测应用场景

上一节我们介绍了目标检测的基本概念,本节中我们来看看它在现实世界中的具体应用。目标检测的含义是:在图像或视频中定位并识别出感兴趣物体的位置和类别

以下是目标检测在几个主要行业领域的应用介绍。

1. 公共安全领域

在公共安全领域,目标检测技术可用于分析监控视频或图片内容。系统可以检测画面中是否有人出现,一旦识别到人员,便能定位其出现过的具体位置。这为安防监控、轨迹追踪等任务提供了技术支持。

2. 农业领域

在农业领域,目标检测可用于农作物表面的病虫害识别。具体方法是:在农田附近安装摄像头,定时(例如每周)拍摄作物图片。系统通过分析这些图片,实时监测农作物叶片的状况。一旦检测到图片中存在病害区域并做出标记,系统便能发出预警,提示农户农作物可能出现问题,以便及时观察和处理。

3. 医疗影像领域

医疗影像检测是当前的热门应用。其原理与农业应用相似:系统获取医学影像(如X光片、CT扫描图),然后对影像进行识别分析,检测出某个特定部位是否存在异常(例如肿瘤区域)。这能为医生提供更准确的诊断辅助信息。

4. 电子商务领域

目标检测在电商行业有广泛的应用场景。例如,当你在街上看到一个商品或品牌,却不知道其具体名称时,可以拍摄一张照片并上传至购物平台(如淘宝)。平台通过目标检测技术,能够识别出照片中的主体物品。如果模型足够精细,甚至可以识别出具体的品牌。这样,用户就能快速获得想要的商品信息。

以上是行业层面的应用。从检测的具体对象类别来看,目标检测还包括以下场景:

以下是几种常见的目标检测类别场景。

  • 道路检测:检测道路上的行人、车辆等。
  • 动物与商品检测:识别图像中的动物或各类商品。
  • 车牌检测:自动识别车辆牌照信息。
  • 菜品检测:拍摄菜品图片,检测图片中包含哪些菜肴类别,可用于菜品审核或自动点餐。
  • 车型检测:拍摄汽车照片,检测并识别出汽车的品牌或具体型号。

目标检测的应用场景非常广泛。只有深入到具体公司的特定业务中,你才会发现更多创新的应用方式。

开发环境搭建

在开始正式的开发或学习相关知识之前,我们需要先搭建好开发环境。由于搭建过程较为耗时,本教程不进行逐步演示,而是将详细步骤整理在配套文件中,请自行查阅并按照步骤安装。

环境搭建主要分为两部分:虚拟环境安装安装依赖包

1. 虚拟环境安装

虚拟环境是一个环境隔离工具,它允许你为每个Python项目创建独立的运行环境,从而避免不同项目之间的依赖包互相干扰。

安装过程大致如下:首先下载并配置 virtualenvvirtualenvwrapper 工具,然后使用命令新建一个虚拟环境。之后,你便可以在这个隔离的环境中安装项目所需的包。

以下是一个简单的命令示例:

# 创建名为 ml_env 的虚拟环境
mkvirtualenv ml_env
# 进入该虚拟环境
workon ml_env
# 退出虚拟环境
deactivate

具体的安装和配置命令请参考提供的教程文档。

2. 安装依赖包

进入创建好的虚拟环境后,我们需要安装项目所需的第三方库。通常,我们会使用一个名为 requirements.txt 的文件来记录所有依赖包及其版本号。

安装命令如下:

# 确保已进入目标虚拟环境 (例如 ml_env)
pip install -r requirements.txt

执行此命令后,pip 会自动安装 requirements.txt 文件中列出的所有包。

关于TensorFlow版本的说明:上述方式安装的是TensorFlow的CPU版本。如果你需要安装GPU版本以利用显卡进行加速计算(后续的多GPU训练需要),请参考提供的 “GPU版本环境搭建.pdf” 文档,按照其中的步骤进行安装。

3. 操作系统平台建议

我们推荐的开发平台是 Ubuntu 16.04 或更高版本。macOS 系统也可以。不推荐使用 Windows 系统,因为在Windows上可能会遇到各种兼容性和安装问题。如果没有macOS,建议使用Ubuntu环境进行安装。


本节课中我们一起学习了目标检测在公共安全、农业、医疗和电商等多个领域的实际应用,了解了其巨大的实用价值。同时,我们也介绍了为后续实践搭建Python虚拟环境以及安装必要依赖包(包括TensorFlow GPU版本)的总体步骤和注意事项。准备好开发环境是进行后续学习和项目开发的第一步。

课程 P50:数据接口设计 - 读取数据接口与基类定义 🧱

在本节课中,我们将学习如何设计一个通用的数据读取接口。我们将创建一个基类,用于统一不同数据集(如Pascal VOC和COCO)的读取逻辑,从而提高代码的复用性和可维护性。

设计思路与目标

上一节我们讨论了数据处理流程。本节中,我们来看看如何设计一个可扩展的数据读取模块。

我们的核心目标是:通过设计一个基类,让所有不同类型的数据集读取类都能继承它。这样,每个数据集只需实现自己的特定配置,而通用的读取逻辑则由基类提供。

基类设计分析

首先,我们需要分析不同数据集的共同点和差异点。通过观察Pascal VOC数据集的读取代码,我们发现以下信息是必需的:

  • 数据集文件的匹配路径和文件名。
  • 数据集的描述信息,如总样本数、训练集/测试集样本数、类别数量等。

对于COCO数据集,其读取逻辑框架是相似的,主要区别在于上述的具体配置参数(例如,COCO有80个类别,而Pascal VOC有20个)。因此,我们可以将这些可变的配置作为参数传递给基类。

实现数据读取基类

基于以上分析,我们现在开始实现基类。我们将它放在公共工具模块 utils 中。

以下是基类 TFRecordReaderBase 的核心代码框架:

class TFRecordReaderBase(object):
    """
    数据集读取基类
    """
    def __init__(self, params):
        """
        初始化基类
        :param params: 不同数据集的配置参数字典
        """
        self.params = params  # 存储数据集特定配置

    def get_data(self, dataset_dir, mode):
        """
        获取数据规范(TensorFlow Dataset对象)
        :param dataset_dir: 数据集目录路径
        :param mode: 模式,指定是读取训练集('train')还是测试集('test')
        :return: 返回对应的数据规范
        """
        # 基类中暂不实现具体逻辑,由子类重写
        return None

代码解释

  1. __init__ 方法:接收一个 params 参数。这个字典包含了数据集的特定配置(如类别数、样本数等),子类在初始化时会传入自己的配置。
  2. get_data 方法:这是核心接口。它接收两个参数:
    • dataset_dir:数据集所在的根目录。
    • mode:用于指明当前需要读取的是训练集还是测试集,这对于分离训练和评估数据至关重要。
  3. 基类中的 get_data 方法暂时返回 None,具体的读取和解析 TFRecord 文件的逻辑将在继承它的子类中实现。

后续步骤:创建子类

设计好基类后,我们的工作就完成了一半。接下来,针对每一个具体的数据集(如 PascalVOCReaderCOCOReader),我们需要:

  1. 定义该数据集独有的配置参数字典 params
  2. 创建一个类并继承 TFRecordReaderBase
  3. 在该子类中,根据数据集格式,具体实现 get_data 方法,完成从 TFRecord 文件到最终 TensorFlow Dataset 对象的转换。

这样,在使用时,我们只需要实例化对应的子类(例如 reader = PascalVOCReader(voc_params)),然后调用统一的 reader.get_data(dataset_path, ‘train’) 接口即可获得数据,无需关心底层是哪个数据集。

总结

本节课中我们一起学习了数据读取接口的设计。我们通过分析不同数据集的共性,设计了一个名为 TFRecordReaderBase 的基类。这个基类通过参数化配置统一的 get_data 接口,为各种数据集提供了一个可扩展的读取框架。在接下来的课程中,我们将基于此基类,实现具体数据集的读取子类。

课程P51:51.03_数据接口:商品数据读取子类实现 🛒

在本节课中,我们将学习如何为特定的数据集(例如商品数据集)创建数据读取子类。我们将基于之前建立的通用数据读取基类,实现一个专门用于读取商品数据集的子类,并学习如何通过配置文件来管理数据集的特定属性。


概述

上一节我们介绍了数据读取的基类设计。本节中,我们来看看如何为具体的数据集(如商品数据集)实现一个子类。核心在于将数据集特有的属性(如文件匹配模式、样本数量、类别数等)从硬编码中分离出来,通过配置文件进行管理,从而提高代码的复用性和可维护性。

创建数据集初始化文件夹

首先,我们需要一个专门存放不同数据集读取逻辑的目录。按照项目结构,我们在 datasets 目录下创建一个名为 dataset_init 的文件夹。

# 在项目目录中创建文件夹
# datasets/dataset_init/

这个文件夹将用于存放所有特定数据集(如Pascal VOC、商品数据集)的读取类实现。

创建商品数据集读取子类

接下来,我们在 dataset_init 文件夹中创建商品数据集的读取类。我们复制一份已有的数据集类模板(例如Pascal VOC的类),并将其重命名为 commodity_2018.py

这个新文件将用于实现读取我们的商品数据集目录。

导入基类并定义子类

commodity_2018.py 文件中,我们首先需要从基类模块中导入我们之前定义的通用数据读取基类 Dataset

from datasets.dataset import Dataset

然后,我们定义一个继承自 Dataset 基类的子类。

class CommodityTfrecord(Dataset):
    """商品数据集读取类"""
    def __init__(self, param):
        pass

    def get_data(self, dataset_dir, train_or_test):
        pass

__init__ 方法用于初始化类并接收配置参数 paramget_data 方法则是核心,用于根据目录和训练/测试模式获取具体的数据。

实现数据获取逻辑

现在,我们需要将具体的数据集读取逻辑填充到 get_data 方法中。这个逻辑通常包括根据文件模式匹配数据文件、获取样本数量、类别信息等。

关键点在于,这些原本硬编码在方法内的属性(如文件匹配模式 fpattern、样本数 nsamples、类别数 num_classes),现在应该从传入的 param 配置参数中动态获取。

因此,我们的任务转变为:如何设计 param 这个参数,使其能灵活承载不同数据集的属性。

设计数据集配置参数

param 参数应该包含数据集的所有相关属性。我们决定在 dataset_config.py 文件中集中配置这些属性。

我们使用 collections.namedtuple 来定义一个清晰的数据集参数结构。namedtuple 可以创建一个带有字段名的轻量级对象,非常适合用来表示配置。

以下是配置步骤:

  1. collections 导入 namedtuple
  2. 定义一个 namedtuple 来指定数据集参数的字段(即属性名)。
  3. 为商品数据集创建具体的参数实例,并填充对应的值。
# 在 dataset_config.py 中
from collections import namedtuple

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/114e77440eefccefc5f38fd7674e454b_32.png)

# 1. 定义参数结构
DatasetParams = namedtuple('DatasetParams', ['fpattern', 'num_classes', 'split_to_size', 'item_to_description'])

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/114e77440eefccefc5f38fd7674e454b_34.png)

# 2. 为商品数据集创建配置实例
cm_2018 = DatasetParams(
    fpattern='commodity_2018_%s_*.tfrecord',  # 文件匹配模式
    num_classes=8,                            # 类别数量
    split_to_size={'train': 88, 'test': 0},   # 训练集和测试集大小
    item_to_description={'image': '...', 'label': '...'}  # 数据描述字典
)

这样,cm_2018 这个对象就包含了商品数据集的所有必要属性。当创建 CommodityTfrecord 类的实例时,可以将 cm_2018 作为 param 传入。

在子类中使用配置参数

回到 commodity_2018.py 文件中的 get_data 方法,我们现在可以使用 self.param 来访问所有配置好的属性,替换掉原来的硬编码值。

def get_data(self, dataset_dir, train_or_test):
    # 使用配置中的文件匹配模式
    file_pattern = os.path.join(dataset_dir, self.param.fpattern % train_or_test)
    # 使用配置中的数据集划分大小
    num_samples = self.param.split_to_size.get(train_or_test)
    # 使用配置中的类别数
    num_classes = self.param.num_classes
    # 使用配置中的数据描述
    item_to_description = self.param.item_to_description

    # ... 后续的数据读取和处理逻辑

添加异常处理

为了代码的健壮性,我们还需要添加一些基本的异常处理。例如,检查传入的 train_or_test 参数是否合法,以及数据集目录是否存在。

def get_data(self, dataset_dir, train_or_test):
    # 检查模式参数是否合法
    if train_or_test not in ['train', 'test']:
        raise ValueError(f"训练/测试数据集名称指定错误: {train_or_test}")

    # 检查数据集目录是否存在
    if not tf.gfile.Exists(dataset_dir):
        raise ValueError("数据集目录不存在")

    # ... 其余的数据获取逻辑

总结

本节课中,我们一起学习了如何为商品数据集实现一个数据读取子类。我们首先创建了专门存放数据集初始化类的目录,然后建立了继承自通用基类的 CommodityTfrecord 子类。通过将数据集属性(如文件模式、样本数等)抽取到独立的配置文件 dataset_config.py 中,并使用 namedtuple 进行管理,我们大大增强了代码的清晰度和可配置性。最后,我们在子类的 get_data 方法中使用了这些配置参数,并添加了必要的异常处理,从而完成了一个健壮、可复用的数据集读取子类实现。

课程 P52:数据接口 - 数据读取工厂逻辑实现 🏭

在本节课中,我们将学习如何实现一个数据读取工厂。这个工厂的核心作用是提供一个统一的接口,让训练代码能够根据指定的数据集名称,自动调用对应的数据读取逻辑,而无需关心具体的数据集实现细节。

上一节我们完成了商品数据集的读取逻辑改造,并设计了一个基类。本节中,我们来看看如何创建一个工厂来统一管理和调用这些不同的数据集类。

概述:数据工厂的设计思路

数据工厂的核心思想是“路由”或“映射”。我们预先定义一个字典,将数据集名称映射到对应的数据集类。当外部代码需要获取数据时,只需提供数据集名称、训练/测试模式和数据目录,工厂就会根据名称找到对应的类,实例化并调用其数据读取方法。

以下是实现数据工厂的关键步骤:

  1. 导入必要的模块:包括具体的数据集类和配置文件。
  2. 定义数据集映射字典:建立数据集名称到具体数据集类的映射关系。
  3. 实现工厂函数:接收外部参数,根据映射字典调用对应的数据读取逻辑。

第一步:创建工厂文件并导入模块

首先,在项目根目录的 datasets 文件夹下,创建一个名为 dataset_factory.py 的 Python 文件。

在这个文件中,我们需要导入之前定义的商品数据集类以及相关的配置参数。

# 导入具体的数据集读取逻辑类
from datasets.dataset_init.commodity_2018 import CommodityTFRecords
# 导入该数据集的配置参数
from datasets.dataset_config import cm2018

第二步:定义数据集映射字典

接下来,我们定义一个字典来管理所有可用的数据集。字典的键是数据集的名称,值是对应的数据集类。这样,当需要新增数据集时,只需在此字典中添加新的映射即可。

# 定义数据集种类字典,建立名称到类的映射
datasets_map = {
    ‘commodity_2018’: CommodityTFRecords,
    # 未来可以在此添加新的数据集,例如:
    # ‘another_dataset’: AnotherDatasetClass,
}

第三步:实现工厂函数

现在,我们实现核心的工厂函数 get_dataset。这个函数是提供给外部训练代码调用的统一接口。

函数需要接收三个参数:

  • dataset_name: 数据集名称,必须存在于 datasets_map 中。
  • train_or_test: 指定是加载训练集(‘train’)还是测试集(‘test’)。
  • dataset_dir: 数据集所在的根目录路径。

函数的逻辑是:

  1. 检查传入的 dataset_name 是否在映射字典中,如果不存在则报错。
  2. 从字典中获取对应的数据集类。
  3. 实例化该类(传入配置参数 cm2018),并调用其 get_data 方法,将 train_or_testdataset_dir 参数传递进去。
  4. 返回 get_data 方法的结果,即符合规范的数据集对象。
def get_dataset(dataset_name, train_or_test, dataset_dir):
    “””
    获取训练数据:根据指定的数据集名称,返回对应的数据读取对象。
    Args:
        dataset_name: 数据集名称,必须存在于当前数据字典中。
        train_or_test: 指定加载训练集(‘train’)还是测试集(‘test’)。
        dataset_dir: 数据集目录。
    Returns:
        一个符合数据规范(如 tf.data.Dataset)的数据集对象。
    “””
    # 检查数据集名称是否有效
    if dataset_name not in datasets_map:
        raise ValueError(f“你所输入的数据集名称 ‘{dataset_name}’ 不存在。”)

    # 根据名称获取对应的数据集类,并实例化(传入配置),然后调用其数据读取方法
    dataset_class = datasets_map[dataset_name]
    return dataset_class(param=cm2018).get_data(train_or_test, dataset_dir)

总结与回顾

本节课中我们一起学习了数据读取工厂的逻辑与实现。我们主要完成了三件事:

  1. 创建工厂文件:在 datasets 目录下建立了 dataset_factory.py 文件。
  2. 建立映射关系:定义了 datasets_map 字典,将字符串形式的数据集名称关联到具体的 Python 类。
  3. 实现统一接口:编写了 get_dataset 函数。该函数作为对外的唯一接口,接收参数后,通过字典映射找到正确的类,并调用其数据读取方法,最终返回标准化的数据。

通过这个工厂模式,训练代码变得非常简洁和灵活。未来要支持新的数据集,开发者只需:

  1. dataset_init 目录下实现新的数据集类。
  2. 在配置文件中添加其参数。
  3. datasets_map 字典中添加一行映射。

之后,训练时只需更改 dataset_name 参数,即可无缝切换不同的数据集,实现了代码的高内聚和低耦合。

🧠 课程 P53:数据接口设计与模块总结

在本节课中,我们将学习如何设计一个统一的数据接口,并对整个数据模块的封装逻辑进行总结。我们将看到如何通过一个工厂类来调用不同的数据集,以及如何通过继承基类来扩展对新数据集的支持。


🛠️ 代码运行与数据模块接口

上一节我们介绍了数据集的基类设计,本节中我们来看看如何在训练代码中调用我们封装好的数据接口。

我们完成了所有逻辑,并提供了 DatasetFactory 类。在外部训练时,我们调用这个工厂类来实现数据读取。

因此,我们使用之前的逻辑 tf_read_tf_record 来读取数据。我们需要调用方法,所以将原来的代码替换掉。

以下是具体步骤:

  1. datasets 文件导入 DatasetFactory
  2. 导入 DatasetFactory 后,直接调用其下面的方法。
  3. 该方法需要参数:数据集名称、是训练集还是测试集、以及数据集的目录。

具体实现代码如下:

from datasets import DatasetFactory

# 调用工厂方法获取数据集
dataset = DatasetFactory.get_dataset(
    name='commodity',          # 数据集名称,需在工厂中定义
    split='train',             # 指定训练集或测试集
    data_dir='./images/tfrecords/commodity_tfrecords'  # 数据集目录
)

运行此代码。如果提示数据集目录不存在,请检查路径是否正确(例如,检查是否缺少字母,如将 commodity 误写为 comodity)。

修改逻辑结构后,我们同样成功读取了数据,并打印出了 Tensor。

整个数据模块提供给外部的接口,就是这样一个 DatasetFactory.get_dataset 调用。


📊 数据模块总结

现在,我们来总结一下数据模块接口完成的工作。

首先,我们修改了读取图片和 XML 数据的逻辑,主要是简单修改了 work_labels 部分。数据转换的逻辑相对简单。

重点在于数据的读取方式。我们设计了一个基类,为不同数据集的读取逻辑提供继承基础。这个基类包含了数据集的配置参数和获取数据集的方法。

配置参数非常重要,它们在 DataConfig 类中进行定义。每增加一个新的数据集,只需在配置中添加相应的属性即可。

继承基类的具体数据集类,必须实现数据读取逻辑。我们可以将无数个实现不同数据集读取逻辑的代码文件,都放在 datasets/ 目录下的 __init__.py 文件中。

在这些文件中,需要继承基类,定义好配置字典,并实现 get_data 方法。

最后,我们对外只提供一个统一的接口文件,即数据工厂文件 dataset_factory.py

DatasetFactory 可以调用并获取指定的数据集(训练集或测试集)及其目录。这就是我们对数据模块进行的封装。


🗺️ 数据模块设计总览

我们用图表来总结数据模块的相关要点。

数据模块设计总结

设计目的是为了能够调用不同的数据集。因此,我们设计了一个数据集的基类 TFRecordsBase

这个基类包含两个主要部分:

  • 配置属性:数据集的参数。
  • get_dataset 方法:用于获取数据集。

不同的数据集需要继承这个基类,并实现各自的数据读取操作。在读取时,可以选择不同的后端(例如,是读取原始文件还是读取 TFRecord)。

我们对外提供一个工程文件 dataset_factory.py,供训练逻辑使用。这样就实现了训练工程可以灵活调用不同的数据集,达成了我们最初的初衷。

在整个项目结构中,data_factory 负责调用不同的数据集,无论是训练还是测试都可以通过它来调用。


✅ 课程总结

本节课中,我们一起学习了如何构建一个统一的数据接口。我们通过设计一个基类来规范不同数据集的读取方式,并利用工厂模式对外提供简洁的调用接口。这使得我们的代码结构更清晰,扩展新数据集更加方便。核心在于理解基类继承、配置管理和工厂模式的应用。

课程P54:模型接口与工厂模式实现 🧩

在本节课中,我们将学习如何为深度学习项目设置模型接口,并实现一个模型工厂。我们将利用TensorFlow等框架提供的现成模型,通过工厂模式来统一管理和调用不同的网络模型,而无需自己从头实现。


项目目录结构 📁

上一节我们介绍了项目的整体规划,本节中我们来看看如何组织模型相关的代码文件。

以下是项目目录结构的关键部分:

  • nets/:存放所有与网络模型相关的代码。
    • nets_model/:存放具体的网络模型定义文件(例如SSD_VGG300)。
    • utils/:存放该网络模型专用的工具函数(如边界框编解码工具)。
  • utils/(项目根目录下):存放全局通用的基础工具函数(如非极大值抑制NMS),供多个模块调用。

这种结构将专用工具与通用工具分离,使代码更清晰、易于维护。


步骤一:搭建目录与导入源码 🛠️

首先,我们需要创建上述目录结构,并将已有的模型源码和工具代码放入对应位置。

  1. 在项目根目录下创建 nets 文件夹。
  2. nets 文件夹内创建 nets_modelutils 两个子文件夹。
  3. 将现成的SSD_VGG300模型定义文件复制到 nets/nets_model/ 目录下。
  4. 将SSD_VGG300模型专用的工具函数复制到 nets/utils/ 目录下。
  5. nets/nets_model/nets/utils/ 目录中分别创建 __init__.py 文件,使其成为可导入的Python模块。
  6. 在项目根目录下创建 utils 文件夹,并将通用的基础工具函数(如 basic_tools)复制到其中。

完成以上步骤后,模型模块所需的基础文件和工具就准备就绪了。


步骤二:实现模型工厂(Nets Factory)🏭

目录搭建完成后,接下来我们实现核心的模型工厂。工厂模式的作用是根据传入的名称,动态返回对应的网络模型类。

我们在 nets/ 目录下创建一个新文件 nets_factory.py

以下是该文件的核心代码实现:

# nets_factory.py
from nets.nets_model.ssd_vgg300 import SSDNet

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/9139acd7b9e68d9f4864079e4060474e_29.png)

def get_network(network_name):
    """
    根据网络名称获取对应的网络模型类。
    参数:
        network_name (str): 指定的网络模型名称。
    返回:
        class: 对应的网络模型类。
    """
    # 定义可用网络模型的映射字典
    networks = {
        'ssd_vgg300': SSDNet,
        # 未来可以在此添加更多模型,例如:
        # 'yolo_v3': YOLONet,
        # 'faster_rcnn': FasterRCNNNet,
    }
    
    # 根据名称从字典中获取对应的网络类
    network_class = networks.get(network_name)
    
    if network_class is None:
        raise ValueError(f"未知的网络名称: {network_name}。请使用以下之一:{list(networks.keys())}")
    
    return network_class

代码解释

  • 首先从模型定义文件中导入具体的网络类(例如 SSDNet)。
  • 定义一个函数 get_network,它接收一个参数 network_name(网络名称)。
  • 在函数内部,创建一个字典 networks,将字符串名称映射到具体的网络类。
  • 函数通过 network_name 查询字典,并返回对应的网络类。如果名称不存在,则抛出错误提示。

这样,在训练或推理时,只需调用 get_network('ssd_vgg300') 即可获得SSD_VGG300模型的类,之后再进行实例化。


总结 📝

本节课中我们一起学习了如何为深度学习项目构建模型接口。

  1. 我们规划并创建了清晰的目录结构,将模型代码、专用工具和通用工具合理组织。
  2. 我们实现了一个简单的模型工厂(nets_factory.py),它利用字典映射和工厂模式,提供了统一、灵活的模型获取接口。这使得在项目中切换或扩展不同模型变得非常简单,只需在工厂的映射字典中添加新的键值对即可。

通过本节课的内容,我们为后续的训练流程准备好了模型调用的基础设施。在下一节中,我们将开始构建训练流程,并实际使用这个模型工厂。

课程 P55:预处理接口与数据增强 🛠️

在本节课中,我们将要学习深度学习流程中的一个关键模块——预处理。我们将重点介绍预处理的需求、核心目的,并深入探讨数据增强的概念、类型、技术及其重要性。

概述

上一节我们介绍了模型接口的构建。本节中,我们来看看另一个至关重要的模块:预处理。预处理不仅仅是简单的图像变形,它在整个训练流程中扮演着核心角色,主要目的是进行数据增强,以丰富数据集、提升模型特征提取能力和泛化能力。

预处理的需求与目的

预处理模块的核心需求源于图像深度学习的实践。其根本目的是对输入数据进行数据增强

数据增强是一个专门的领域,旨在通过一系列技术手段来丰富图像训练集,从而更好地提取图像特征并泛化模型,防止模型对训练数据过拟合

此外,不同的算法对输入图像的尺寸有严格要求。例如:

  • YOLO 算法要求输入为 448×448
  • SSD 算法要求输入为 300×300

因此,预处理模块的另一个关键任务是将各种尺寸的原始图片调整到算法所要求的固定大小。

综上所述,预处理模块的目的可总结为:

  1. 对数据集进行数据增强
  2. 丰富数据以提取更好特征,防止过拟合
  3. 将图片尺寸调整到算法要求的固定大小。

数据增强详解

那么,数据增强具体是什么,它又是如何帮助模型泛化、防止过拟合的呢?

什么是数据增强?

数据增强是指通过一系列图像变换操作(如剪切、旋转、缩放、平移等)的组合,来增加数据集有效大小的技术

其核心思想是:通过对原始图像进行各种合理的变换,生成新的、多样化的训练样本,从而在不实际收集新数据的情况下,扩充数据集。

例如,一张原始图片经过多种变换后,可以生成20张不同的变体。这相当于将1个样本变成了20个样本输入网络进行训练,极大地增加了数据的多样性。

为什么需要数据增强?

进行数据增强主要有两个原因:

  1. 防止过拟合:这是最主要的原因。当训练数据不足时,模型容易记住训练集中的噪声或不重要的细节(即过拟合),导致在新数据上表现不佳。数据增强通过提供更多样化的数据,迫使模型学习更通用、更本质的特征。
  2. 扩充小数据集:在实际项目中,收集和标注大量数据成本高昂。数据增强是一种低成本、高效率的数据扩充方式。

为了理解数据增强如何防止过拟合,我们来看一个例子:

假设数据集中只有两张汽车图片用于训练分类模型(品牌A和品牌B)。一张品牌A的车头朝左,一张品牌B的车头朝右。神经网络在训练时,可能会将“车头方向”这个非常明显但非本质的特征,作为区分两个品牌的主要依据。

当模型训练完成后,输入一张车头朝右的品牌A汽车图片时,模型很可能错误地将其分类为品牌B。这是因为数据集太小,模型找到了最具代表性(但非本质)的特征。

解决方法有两种:

  • 直接收集更多数据:收集品牌A车头朝右、品牌B车头朝左的图片,打破特征与标签的虚假关联。
  • 进行数据增强:对现有的两张图片进行水平翻转等变换,人工创造出所需的新样本。

数据增强的两种类型

根据增强发生的时机和方式,数据增强可分为两类:

1. 离线增强

  • 定义:在训练开始之前,预先对数据集中的所有图像执行变换操作,并将生成的新图像物理保存到磁盘或数据库中。
  • 特点:从根本上增加了数据集的物理大小。训练时直接读取增强后的数据集。

2. 在线增强

  • 定义:在训练过程中,数据输入模型之前,实时地、随机地对每批(或每张)图像执行变换操作。
  • 特点:原始数据集大小不变。每次迭代时,输入的图像都可能经过不同的随机变换,相当于在“飞行中”动态生成新的训练样本。这种方式更灵活,能产生近乎无限的数据变体。

以下是两种类型的对比总结:

类型 操作时机 数据集大小 特点
离线增强 训练开始前 物理增加 一次性生成,存储开销大,但训练时效率高。
在线增强 数据输入模型前 逻辑增加 动态生成,存储开销小,能提供更多样的样本。

常见的数据增强技术

以下是一些基础且强大的数据增强方法,被广泛应用于各种模型的训练中:

1. 翻转
对图像进行水平或垂直方向的翻转。

# TensorFlow 示例:随机水平翻转
augmented_image = tf.image.random_flip_left_right(original_image)

2. 旋转
将图像旋转一定角度(如90°,180°,270°)。

3. 随机裁剪
从原始图像中随机裁剪出一部分区域,然后将其缩放回原始尺寸。这迫使模型不依赖于物体的完整出现或固定位置。

4. 色彩变换
调整图像的亮度、对比度、饱和度和色调。

5. 平移与缩放
将图像在平面内移动一定像素,或进行一定比例的缩放。

数据增强的效果

数据增强能显著提升模型的训练效果和泛化能力。从下图所示的在MNIST数据集上的训练曲线可以看出:

  • 绿色/粉色线(无增强):代表损失和准确率的变化。
  • 红色/蓝色线(有增强):代表使用了数据增强后的损失和准确率。

可以观察到,使用了数据增强的模型(红/蓝线)能够更快地降低损失、提高准确率,并且最终能达到更好的性能平台。这说明数据增强有效提升了模型的学习效率和泛化能力。

总结

本节课中我们一起学习了预处理与数据增强的核心知识。

我们首先明确了预处理模块的需求和目的:进行数据增强、调整图像尺寸以适配算法,最终目标是提升模型泛化能力、防止过拟合。

接着,我们深入探讨了数据增强。我们了解到,数据增强是通过一系列图像变换来逻辑上扩充数据集的技术。它主要分为离线增强在线增强两种类型。我们还介绍了几种常见的增强技术,如翻转、旋转、裁剪等。

最后,我们通过实例看到了数据增强对模型训练效果的积极影响,它能加速模型收敛并提升最终性能。

理解并合理应用数据增强,是构建鲁棒、高性能深度学习模型的关键一步。在接下来的实践中,我们将学习如何使用TensorFlow等工具具体实现这些预处理和数据增强操作。

课程 P56:56.02_预处理接口:预处理工厂代码 🏗️

在本节课中,我们将学习如何搭建一个数据预处理的工厂模块。这个模块将作为统一的接口,根据不同的模型需求,提供对应的数据增强或预处理方法,从而简化训练和测试流程。

概述

数据增强是提升模型泛化能力的重要手段。然而,我们无需为每个模型手动编写复杂的增强逻辑。TensorFlow 等框架通常会参考 VGG、Inception 等经典论文,将常用的数据增强方法封装成 API。本节课,我们将学习如何利用这些封装好的 API,构建一个灵活的预处理工厂。

预处理 API 简介

上一节我们提到了数据增强的重要性,本节中我们来看看如何具体使用封装好的 API。

在 TensorFlow 的 SSD VGG 预处理代码中,核心的预处理功能位于 ssd_vgg_preprocessing 文件中。该文件提供了两个关键函数,通过 preprocess_image 函数进行调用。

这个函数允许你指定当前是训练模式还是测试模式。区分这两种模式的原因在于:

  • 训练时:目的是通过数据增强来扩充数据集,增加模型的鲁棒性。
  • 测试/验证时:只需将输入图片调整到模型要求的大小即可,无需进行随机变换。因为如果测试时对图像进行了翻转等随机操作,将无法与原始标注对应,也无法正确显示预测结果。

因此,在预处理阶段,我们明确分为训练预处理测试预处理

以下是两种模式的核心区别:

  • 训练过程:会对图像进行随机性变化(数据增强)。
  • 测试过程:仅将图片缩放到指定尺寸,不进行数据增强。

搭建预处理模块

理解了预处理的核心概念后,我们现在开始搭建自己的预处理模块。

首先,我们需要了解预处理模块的目录结构。该结构与我们之前构建的模块类似,旨在为不同模型组织不同的预处理需求。

以下是模块的目录结构:

preprocessing/
├── __init__.py
├── ssd_vgg_preprocessing.py
└── utils/
    └── image_tools.py
  • preprocessing/:主目录,存放所有预处理相关代码。
  • ssd_vgg_preprocessing.py:针对 SSD VGG 模型的具体预处理逻辑。
  • utils/image_tools.py:预处理过程中用到的图像工具函数。

注意:相关的底层 API(如具体的图像变换函数)我们无需自己编写,可以直接使用框架或社区提供的成熟代码。因此,我们可以直接将上述目录和文件结构复制到我们的项目(例如 v3.0 版本)中。

实现预处理工厂

目录搭建完成后,我们最后一步是实现一个预处理工厂。这个工厂的作用是:根据用户提供的模型名称,返回对应的预处理函数。

我们将在 preprocessing_factory.py 文件中编写此工厂。

以下是实现预处理工厂的步骤:

  1. 导入必要的模块:首先导入我们需要的预处理模块。
  2. 定义工厂函数:创建一个函数(例如 get_preprocessing),它接收两个参数:预处理名称 (name) 和是否为训练模式 (is_training)。
  3. 建立名称映射:在函数内部,定义一个字典 (preprocessing_fn_map),将支持的预处理名称映射到对应的模块。
  4. 检查有效性:判断用户传入的 name 是否在支持的映射中。如果不在,则抛出错误提示。
  5. 返回处理函数:如果名称有效,则返回一个闭包函数。这个闭包函数内部会调用具体预处理模块的 preprocess_image 方法,并固定 is_training 参数。这样做的好处是,将具体的参数(如图像、标签、边界框等)延迟到实际训练循环中再传入,使得工厂接口更简洁。

以下是 preprocessing_factory.py 的核心代码框架:

from preprocessing import ssd_vgg_preprocessing

def get_preprocessing(name, is_training=True):
    """
    预处理工厂:获取不同模型的数据增强/预处理方法。
    Args:
        name: 预处理名称,例如 'ssd_vgg_300'。
        is_training: 是否为训练模式。
    Returns:
        一个预处理函数。
    """
    # 支持的预处理方法映射
    preprocessing_fn_map = {
        'ssd_vgg_300': ssd_vgg_preprocessing,
    }

    # 检查名称是否有效
    if name not in preprocessing_fn_map:
        raise ValueError('您提供的预处理名称 "%s" 不在预处理模型库中。请提供正确的模型预处理代码。' % name)

    # 获取对应的预处理模块
    preprocessing_module = preprocessing_fn_map[name]

    # 定义一个闭包函数,延迟参数传入
    def preprocessing_fn(images, labels, bboxes, out_shape, data_format='NHWC'):
        """
        实际的预处理函数。
        Args:
            images: 输入图像。
            labels: 图像标签。
            bboxes: 边界框。
            out_shape: 输出图像形状。
            data_format: 数据格式,默认为 'NHWC'。
        Returns:
            处理后的图像、标签和边界框。
        """
        return preprocessing_module.preprocess_image(
            image=images,
            labels=labels,
            bboxes=bboxes,
            out_shape=out_shape,
            data_format=data_format,
            is_training=is_training  # 使用工厂函数传入的 is_training 参数
        )

    # 返回这个预处理函数
    return preprocessing_fn

通过这种方式,外部代码只需调用 get_preprocessing(‘ssd_vgg_300’, is_training=True) 就能获得一个配置好的预处理函数,然后在训练循环中传入具体的批次数据即可。

总结

本节课中我们一起学习了如何构建一个数据预处理的工厂模块。

我们首先了解了训练与测试阶段预处理的区别。接着,我们搭建了预处理模块的目录结构。最后,我们实现了一个关键的 preprocessing_factory,它作为一个灵活接口,根据模型名称和模式返回对应的预处理函数。这个设计使得数据增强流程模块化、可配置,极大方便了后续的训练代码编写。

课程 P57:预处理工厂代码参数错误调整 🛠️

在本节课中,我们将学习如何调整预处理工厂代码中的参数错误。我们将重点关注如何移除冗余参数、正确传递参数,以及优化代码结构,使其更清晰、更易于维护。

概述

上一节我们介绍了预处理工厂的基本概念。本节中,我们来看看在具体实现时遇到的一个常见问题:参数定义与传递存在错误。我们将通过分析代码片段,逐步修正这些问题。

参数问题分析与修正

在之前的代码中,我们在最外层定义了一个参数 is_training=True。这个参数的意图是:在首次获取预处理函数时,判断当前是否处于训练模式。

然而,在函数内部调用时,我们无需再次指定 is_training 参数。因此,我们需要删除内部调用中的 is_training 参数。

以下是需要修正的代码逻辑示意图:

具体修正步骤如下:

以下是参数调整的具体步骤:

  1. 删除内部调用的 is_training 参数:在函数内部调用预处理方法时,移除显式传递的 is_training 参数,因为它已由外层工厂函数的状态决定。
  2. 确保 extra_train 参数正确传递extra_train 参数应通过工厂函数的外层参数传入,并在内部调用时使用。
  3. 处理 data_format 参数data_format 参数已在工厂函数的参数列表中指定了默认值。因此,在内部调用时,我们无需再重复定义它,而是直接使用传入的值。

修正后的参数传递逻辑可参考下图:

通过以上调整,我们使参数传递路径更加清晰,避免了重复定义和潜在的冲突,让预处理工厂的接口更加简洁和健壮。

总结

本节课中我们一起学习了如何修正预处理工厂代码中的参数错误。我们主要完成了三件事:移除了内部调用的冗余 is_training 参数,确保了 extra_train 参数的正确传递路径,并将 data_format 参数统一到工厂函数参数列表中管理。这些调整使得代码逻辑更清晰,更易于理解和使用。

课程P58:数据、模型与预处理接口参数总结 📚

在本节课中,我们将对之前讲解的数据模块接口、模型接口以及预处理模块接口进行统一梳理和总结。我们将明确每个接口在训练时需要提供的具体参数,以便后续在构建训练流程时,无需再深入每个模块的细节,可以直接调用。


数据模块接口 📊

上一节我们介绍了数据模块的构成,本节中我们来看看其接口的具体调用方式。

文件dataset_factory.py
函数get_dataset
参数
以下是调用 get_dataset 函数时必须提供的参数列表:

  • dataset_name:数据集的名称,例如 coco_2017
  • is_training:一个布尔值,用于指定加载的是训练集还是测试集。
  • dataset_dir:数据集在本地存储的根目录路径。


模型接口 🤖

了解了数据如何加载后,我们接下来看看如何获取模型。

文件nets_factory.py
函数get_network
参数
调用 get_network 函数仅需一个参数:

  • network_name:需要构建的神经网络模型的名称。


预处理接口 ⚙️

模型和数据准备就绪后,数据在输入模型前需要经过预处理。预处理接口较为特殊,它返回一个可调用的处理函数。

文件preprocessing_factory.py
函数get_preprocessing
参数
get_preprocessing 函数本身接收两个参数,并返回一个处理函数:

  • name:预处理策略的名称。
  • is_training:指定是否使用训练阶段的预处理流程。

其返回的 preprocessing_fn 函数则需要更多参数来完成具体的图像变换,以下是该函数所需的参数列表:

  • image
  • labels
  • bboxes
  • out_shape:输出图像的尺寸(例如 [300, 300])。
  • data_format:数据格式(例如 ‘NHWC’‘NCHW’)。
  • is_training:是否处于训练模式。
  • **kwargs:其他可能的关键字参数。

本节课中,我们一起学习了训练流程中三个核心模块的接口调用方法及其参数。我们总结了数据模块的 get_dataset、模型模块的 get_network 以及预处理模块的 get_preprocessing 函数的具体使用方式。有了这些清晰的接口定义作为基础,接下来我们就可以开始构建并深入讲解完整的模型训练过程了。

课程 P59:训练步骤与设备部署介绍 🚀

在本节课中,我们将学习深度学习模型训练的核心流程,并了解如何将训练任务合理地部署到CPU和GPU设备上,以构建一个高效的多GPU训练环境。


多GPU训练的必要性

模型训练是整个项目的关键环节。只有训练出性能良好的模型,才能将其导出并供他人使用。那么,为什么我们需要使用多GPU进行训练呢?

上一节我们介绍了训练的重要性,本节中我们来看看多GPU训练具体在做什么。

我们参考下图来理解多GPU训练中设备的分工:

在训练设备中,通常包含CPU和GPU。CPU也可以用于训练模型和计算输出结果。然而,CPU处理大量计算时非常耗时,训练一个模型可能需要长达一个月的时间,这显然效率过低。

因此,我们需要计算速度更快的设备,这就是图形处理器(GPU)。GPU在并行计算方面比CPU快很多。具体细节我们不做深入探讨。

以下是CPU和GPU在训练中的主要分工:

  • CPU的角色:主要负责变量的保存和参数的统一更新。它像一个中转站或调度中心。我们通常将创建的变量存储在CPU上。
  • GPU的角色:主要负责计算量大的任务,如模型的前向传播、反向传播和梯度计算。GPU相当于执行具体工作的“劳工”。

可以这样理解:CPU管理参数(Parameters),而GPU执行工作(Jobs)。了解CPU和GPU的分工后,我们就知道在训练中应该将哪些任务分配给它们。

在TensorFlow中,设备有默认的命名规则,例如 device:CPU:0device:GPU:0device:GPU:1 等。程序会自动进行标记,我们通常无需关心GPU:0具体对应哪块物理显卡。默认情况下,计算任务会分配到各个可用的GPU上。

这就是多GPU训练的基本概念和分工。


训练步骤与设备部署

我们知道了训练过程中CPU和GPU的职责,接下来需要了解完整的训练步骤,并探讨如何将这些步骤与设备部署相结合。

首先,回顾一下模型训练的基本步骤:

  1. 数据读取
  2. 数据预处理
  3. 网络模型构建
  4. 计算结果与损失
  5. 添加监控:将损失、准确率等需要观察的变量通过 summary 添加到TensorBoard。
  6. 模型训练与保存

那么,如何将这些训练步骤与多GPU环境下的设备部署结合起来呢?下图展示了具体的结合方式:

以下是每个步骤在设备上的具体部署策略:

  • 数据读取与预处理:这些步骤中产生的变量和张量(tensor),默认都存储在CPU的内存中。即使有多个CPU,任务也默认在CPU上完成。
  • 网络模型构建与损失计算:这是定义计算图的过程。在TensorFlow中,我们先定义计算,然后在会话(session)中运行。我们将定义计算图(包括损失计算)的任务指定给GPU执行。这意味着我们需要指定设备来运行这些计算任务。
  • 优化器定义与变量存储:定义优化器(如设置学习率)时,相关的变量同样默认存储在CPU上。
  • 优化计算:真正的优化计算(如根据损失计算梯度)需要在每个GPU设备上运行。我们定义计算操作,并在各个GPU上分别计算损失和梯度。

通过以上方式,我们将训练流程中的各项任务明确地分配到了合适的设备上。

在TensorFlow中,虽然提供了指定设备运算的原生操作,但在复杂的多设备环境下手动管理非常麻烦。因此,我们通常会使用一个专门的工具库来简化部署,例如 tf.contrib.model_pruning 或其他设备部署模块。该库提供了以下关键函数来帮助我们在不同设备上定义计算:

  • variable_device
  • input_device
  • create_comments
  • optimized_column

这些函数用于在指定设备上定义变量、输入和计算操作。


总结 📝

本节课中,我们一起学习了深度学习模型训练的核心流程与多GPU设备部署策略。

我们首先了解了为什么需要多GPU训练,并明确了CPU和GPU在训练中的不同角色:CPU负责变量存储和参数调度,GPU负责繁重的计算任务。

接着,我们梳理了标准的训练步骤,并深入探讨了如何将这些步骤与设备部署相结合。关键点在于,将数据预处理和变量定义放在CPU上,而将模型计算图定义、损失计算和梯度优化等计算密集型任务分配到各个GPU上执行。

最后,我们提到可以使用TensorFlow的相关工具库来简化多设备环境下的部署和管理工作。

请务必理解并掌握文中展示的两张示意图,它们清晰地概括了多GPU训练的分工以及训练步骤与设备部署的对应关系。理解这些概念是构建高效训练管道的基础。


课程P6:目标检测算法原理铺垫 🎯

在本节课中,我们将学习目标检测算法的基础原理,为后续深入理解各类经典模型做好铺垫。我们会明确学习目标检测算法的目的,并梳理需要掌握的核心要点。

为什么要学习算法原理?🤔

在正式讲解具体算法之前,我们首先需要明确两个问题:为什么要学习这些算法,以及在学习过程中需要掌握到什么程度。

以下是学习目标检测算法原理的两个主要原因:

  1. 夯实算法基础:深入理解算法的原理和过程,能让你在技术理解和应用上建立更扎实的基础,相比他人更具优势。
  2. 熟练使用开发接口:在实际项目开发中,我们常常需要调用框架(如TensorFlow)提供的API。如果不了解底层算法,可能连API的参数含义、返回值如何使用都无法理解,更谈不上高效开发。因此,学习原理是为了能够快速上手并熟练使用TensorFlow等框架的API

我们需要掌握到什么程度?📚

那么,学习这些算法是否意味着我们要亲手实现每一个复杂的模型呢?并非如此。在项目实践中,我们的核心目标是快速理解算法原理,并能够迅速应用于实际项目。

因此,我们的学习目标是:清晰地掌握每个算法的识别流程,并理解其核心特点。具体来说,你需要知道每个算法是为了解决什么问题而设计的,以及它采用了哪些关键技术来解决这些问题。

例如,当提到某个算法时,你应该能说出它应用了何种技术,以及该技术是如何运作的。我们的重点在于理解原理,而非从零开始编码实现。

基于以上两点,我们将快速梳理并熟悉一系列经典的目标检测算法,为后续的项目实践打下坚实的理论基础。


本节课中,我们一起学习了学习目标检测算法原理的必要性和应掌握的程度。我们明确了学习目的是为了夯实基础并熟练使用开发工具,同时确定了学习重点是理解算法的流程和关键技术点,而非重复造轮子。接下来,我们将正式进入具体算法的学习。

TensorFlow 模型部署教程 P60:Model Deploy 库介绍 🚀

在本节课中,我们将学习 TensorFlow 中用于简化多设备(如多GPU)训练的 model_deploy 库。我们将了解其核心概念、主要组件以及基本使用方法。

概述

model_deploy 库位于 TensorFlow 的 slim 模块下,旨在简化在单台计算机的多个 GPU 或 CPU 上进行模型训练的操作。它通过封装设备管理和模型复制逻辑,使得多设备训练比手动管理设备更加简单。

核心概念介绍

上一节我们介绍了库的基本定位,本节中我们来看看其核心术语。

model_deploy 中,理解以下术语至关重要:

  • Replica:指代一台独立的机器。在单机多设备场景下,通常只有一个 Replica。
  • Clone:这是最关键的概念。它指的是复制到每个计算设备(如每个 GPU)上的模型副本。在多 GPU 训练中,每个 GPU 都会获得一个完整的模型克隆,用于独立进行前向和反向传播计算。梯度最终会汇总以更新主模型参数。
  • Parameter ServerWorker Server:在分布式多机训练场景中,Parameter Server 专门负责存储和更新模型参数变量,而 Worker Server 上的设备则负责进行计算。对于单机多 GPU 训练,我们主要关注 Clone 的概念。

Model Deploy 库结构

了解了核心概念后,我们来看看这个库提供了哪些主要组件。

model_deploy 主要包含一个配置类和一些辅助函数:

  1. DeploymentConfig:用于初始化和配置训练部署的环境。它指定了变量、输入数据和优化器应放置在哪个设备上。
  2. create_clones 函数:负责为每个计算设备创建模型克隆。
  3. optimize_clones 函数:负责为每个模型克隆定义优化操作(如前向传播、损失计算、梯度计算等)。

DeploymentConfig 配置详解

以下是 DeploymentConfig 类中一些重要参数的介绍:

  • num_clones:每个 Replica(机器)上模型克隆的数量,通常等于可用的 GPU 数量。
  • clone_on_cpu:布尔值,指示是否在 CPU 上创建克隆(用于没有 GPU 或调试的情况)。
  • replica_id:当前机器的 ID,默认为 0(第一台机器)。
  • num_replicas:可用的机器总数。
  • num_ps_tasks:Parameter Server 的数量。
  • worker_job_nameps_job_name:分别为 Worker 和 Parameter Server 任务指定的名称。

对于常见的单机多 GPU 训练,我们主要配置 num_clones(GPU数量)等前面几个参数即可。

设备指定方法

DeploymentConfig 还提供了指定设备的方法:

  • variable_device():指定全局变量存放的设备,默认为 CPU:0
  • input_device():指定输入数据变量的设备,默认为 CPU:0
  • optimizer_device():指定优化器相关张量的设备,默认为 CPU:0
  • clone_device(clone_index):根据克隆索引返回对应的设备名称(如 GPU:0, GPU:1)。

例如,clone_device(0) 返回第一个克隆的设备名。

基本使用流程

根据库源码提供的范例,使用 model_deploy 进行多 GPU 训练的基本流程如下:

  1. 创建 DeploymentConfig 对象,配置克隆数量等参数。
  2. 使用 create_clones 函数,传入配置和模型构建函数,为每个设备创建模型克隆。
  3. 使用 optimize_clones 函数,传入克隆列表和优化器,为每个克隆定义训练操作。
  4. 在训练循环中,将数据分发到各个克隆设备上执行定义好的操作。

总结

本节课中我们一起学习了 TensorFlow slim.model_deploy 库。我们了解了其用于简化多设备训练的目的,掌握了 Clone 这一核心概念,认识了 DeploymentConfig 配置类以及 create_clonesoptimize_clones 两个关键函数。通过该库,我们可以更高效地组织和管理在多个 GPU 上的模型训练任务。

课程P61:训练运行结果显示与初始配置确定 🚀

在本节课中,我们将学习如何实现SSD模型的训练过程。主要内容包括:查看训练代码的运行结果与流程,理解关键命令行参数,以及为编写训练代码进行必要的初始配置确定。


训练运行结果与流程展示

上一节我们介绍了训练的整体思路,本节中我们来看看具体的代码运行过程。

首先,训练代码在项目的根目录下通过命令行参数启动。

以下是运行训练的核心命令示例:

python train_ssd_network.py --pretrained_model_path=... --fine_tuning_checkpoint_path=... --dataset_dir=...

运行该命令后,控制台会打印训练过程信息。这些信息包括网络参数(如SSD模型中的长宽比)和训练数据文件的路径。

程序会首先尝试从微调检查点目录(fine_tuning)读取参数。如果该目录为空,则从预训练模型(pretrained)中读取参数。随后,训练过程开始,模型会按步骤进行迭代。

训练过程中,控制台会打印每一步的全局步数(global step)和每秒步数等信息。Summary信息也会在特定步数(例如第5步)被保存。

由于训练耗时较长(尤其在仅使用CPU的环境下),我们可以使用 Ctrl+C 中断训练。

训练中断或完成后,生成的模型文件会保存在指定目录。在项目根目录的 ckpt 文件夹下:

  • pretrained/:存放预训练模型文件。
  • fine_tuning/:存放本次训练(微调)过程中保存的模型检查点,所有更新的参数都保存在这里。


训练逻辑梳理与项目结构

了解了运行流程后,我们接下来梳理训练的逻辑步骤,并查看项目代码结构。

完整的训练逻辑可以梳理为以下几个步骤:

  1. 设备与全局配置:配置训练环境(如GPU数量),并定义记录训练进度的全局步数。
  2. 获取数据队列:读取并准备图片数据输入队列。
  3. 构建计算图:将数据输入网络进行计算,定义损失函数,并将模型复制到每个GPU设备上。同时,添加变量观察器到TensorBoard。
  4. 定义学习率与优化器:设置动态学习率策略和模型优化器。
  5. 执行训练操作:利用优化器计算梯度,更新模型参数以最小化损失,并计算平均损失。该步骤会返回训练操作(train_op)和汇总操作(summary_op)。
  6. 启动训练:最后,使用 tf.train.Supervisor 或类似API配置会话(CONFIG),并调用 slim.learning.train 启动训练循环。

这个流程与我们之前讨论的通用训练流程是一致的。

接下来,我们查看项目代码结构。我们从 3.0 版本复制一份到 4.0 版本作为基础。关键的训练逻辑代码文件如下:

  • train_ssd_network.py:训练主脚本,包含参数配置和训练主循环。
  • utils/train_tools.py:存放训练所需的公共工具函数,如形状变换、多GPU模型复制、学习率配置等。


训练初始配置确定

在开始编写训练代码之前,我们必须先确定一些核心的初始配置。这些配置通常分为两类:路径参数和训练超参数。

以下是需要确定的关键配置项列表:

  • 预训练模型路径 (pretrained_model_path):用于微调的基础模型存放位置。
  • 微调输出路径 (fine_tuning_checkpoint_path):训练过程中保存的检查点文件输出目录。
  • 数据集目录 (dataset_dir):训练数据(TFRecords格式)的存放位置。
  • 批次大小 (batch_size):每次迭代训练所使用的样本数量。
  • 权重衰减系数 (weight_decay):用于防止模型过拟合的L2正则化惩罚项系数。
  • 初始学习率 (learning_rate) 与 最终学习率 (end_learning_rate):定义学习率衰减的起止值。
  • 优化器选择 (optimizer):如 AdamMomentum 等。
  • 模型名称 (model_name):所使用SSD模型的变体名称(如 ssd_300)。

这些参数的值并非随意设定,而是参考了社区经验(例如谷歌在原始论文中使用的数值)和常见实践。我们将根据这些配置创建或确认相应的文件夹结构。

例如,在项目根目录下,我们需要建立 ckpt 文件夹,并在其下创建 fine_tuningpretrained 两个子目录,分别用于存放微调输出和预训练模型。


本节课中我们一起学习了SSD模型训练的完整运行流程,梳理了从数据输入到模型更新的代码逻辑步骤,并明确了在编写训练代码前必须确定的各项初始配置(包括路径和超参数)。这些准备工作是成功实现模型训练的基础。

课程P62:设备配置与全局步数定义 🛠️

在本节课中,我们将学习如何为分布式训练配置计算设备,并定义用于记录训练进度的全局步数。这是构建一个完整训练流程的第一步。

概述

我们将按照一个清晰的步骤来编写代码。每个步骤的内容都比较多,因此我们将逐一进行。本节课程对应第一个步骤:配置部署参数和定义全局步数。

第一步:导入必要的库

首先,我们需要将所有必需的库导入到代码中。

import tensorflow as tf

接下来,我们从预定义的模型接口中导入参数。这些参数,包括模型文件夹的名称,都必须明确指定。

# 假设从某个模块导入预定义的参数
from config import model_params

第二步:定义主函数结构

我们将定义一个主函数。在TensorFlow中,通常使用 tf.app.run() 来启动程序,它会调用我们定义的 main 函数。

def main(_):
    # 主函数逻辑将在这里编写
    pass

if __name__ == '__main__':
    tf.app.run(main)

main 函数中,我们将编写主要的训练逻辑。一开始,我们可以进行一些基础配置,例如检查文件夹是否存在或设置日志打印级别。

def main(_):
    # 基础配置:设置日志级别等
    tf.logging.set_verbosity(tf.logging.INFO)
    # 检查数据目录等逻辑...

第三步:配置命令行参数

我们需要配置一些通过命令行传入的参数,例如数据集目录。

以下是数据集相关的命令行参数设置:

# 定义数据集目录参数
tf.app.flags.DEFINE_string(
    'dataset_dir',
    '',
    '训练数据集目录'
)

同样地,我们也需要配置设备相关的参数。

以下是设备相关的命令行参数配置:

# 定义可用GPU设备数量的参数
tf.app.flags.DEFINE_integer(
    'num_gpus',
    1,
    '可用设备的GPU数量'
)

# 定义是否仅在CPU上运行的参数
tf.app.flags.DEFINE_boolean(
    'clone_on_cpu',
    False,
    '是否只在CPU上运行'
)

我们可以通过 tf.app.flags.FLAGS 来获取这些在命令行中设置的值。

第四步:配置部署参数 (Deployment Config)

在默认的计算图中编写训练逻辑。第一步是配置 DeploymentConfig

这个配置用于定义集群属性或计算资源的相关情况,例如有多少台设备、是否仅在CPU上运行、主设备的ID等。

# 配置 DeploymentConfig
deploy_config = model_deploy.DeploymentConfig(
    num_clones=FLAGS.num_gpus,
    clone_on_cpu=FLAGS.clone_on_cpu,
    replica_id=0,
    num_replicas=1,
    num_ps_tasks=1
)

配置完成后,我们返回这个 deploy_config 对象,它将在后续步骤中使用。

第五步:定义全局步数

在网络训练中,通常需要定义一个全局步长变量来记录训练的总步数。这类变量参数通常会被放置在指定的设备上。

在TensorFlow中,我们可以使用 tf.device() 来指定变量创建的设备。我们将使用 deploy_config 中定义的变量放置策略。

# 在指定设备上创建全局步数变量
with tf.device(deploy_config.variables_device()):
    global_step = tf.train.create_global_step()

这样,我们就创建了一个名为 global_step 的变量,用于追踪训练的全局进度。

总结

本节课中,我们一起完成了训练流程的第一步编写:

  1. 我们导入了必要的库和参数。
  2. 我们定义了程序的主函数结构。
  3. 我们配置了数据集和设备相关的命令行参数。
  4. 我们使用 DeploymentConfig 配置了分布式训练的环境。
  5. 我们在指定的设备上创建了用于记录训练进度的全局步数变量。

这一步为后续构建完整的训练循环奠定了基础。下一节,我们将在此基础上继续编写数据输入和模型构建的代码。

课程 P63:63.05_训练:图片数据读取与处理逻辑介绍 🖼️➡️🔢

在本节课中,我们将要学习SSD目标检测模型训练流程中的第二步:如何读取图片数据并进行必要的预处理。我们将重点分析从数据集中获取哪些信息,以及为什么不能直接将原始数据送入网络训练。

概述

上一节我们介绍了训练流程的整体步骤。本节中,我们来看看第二步的具体任务:获取图片队列数据以及处理样本标记。核心目标是理解我们需要处理哪些数据,以及为何要进行这些处理。

数据来源与内容

首先,我们需要明确数据从哪里来以及包含什么内容。我们的数据集模块接口是通过 dataset_factory.get_dataset 获取的,它返回一个符合 DATASET 规范的对象,其中包含了图片的相关内容。

回顾我们之前定义的数据集(以commodity2018为例),读取出来的数据包含以下几项:

  • Image
  • image_shape
  • b_box
  • label

在实际训练中,image_shapelabel 可能不会被直接使用。因此,我们主要获取和处理的是以下三项:

  1. Image(图片)
  2. b_box(边界框)
  3. label(标签)

数据处理需求分析

获取数据后,我们不能直接将其输入网络。以下是对两类核心数据的处理需求分析。

图片(Image)处理

图片本身需要经过一系列变换,主要包括:

  • 形状/大小变换:将图片调整到网络所需的固定尺寸。
  • 数据增强:通过随机翻转、色彩抖动等方式增加数据多样性,提升模型泛化能力。

边界框与标签(b_box & label)处理

这是本步骤的关键。我们不能直接将原始的 b_boxlabel 送入网络训练。原因在于SSD网络的训练机制。

SSD网络会为每个特征图位置生成一系列默认的锚点框(default anchors),总数可能多达8752个。而一张图片中的真实目标框(Ground Truth, GT)数量通常很少(例如只有3个)。

这就产生了一个问题:如何用少量的GT去计算大量锚点框的损失?

解决方案是进行 “样本标记” 。这个过程的目标是:为每一个用于训练的锚点框分配一个对应的目标值(或标记其为背景)

具体流程如下:

  1. 计算每个锚点框与所有GT框的IOU(交并比)。
  2. 根据IOU阈值,将锚点框标记为正样本(包含物体)或负样本(背景)。
  3. 对于正样本锚点框,将其与最匹配的GT框进行关联。这样,每个参与训练的锚点框都有了明确的回归目标(对于正样本)或类别目标(对于负样本是背景类)。

通过这种方式,我们将数量不匹配的原始GT框,转换成了与训练锚点框数量一致的目标张量,从而可以逐一对位地计算损失函数。

用公式表示核心匹配原则之一:
正样本锚点框的偏移量目标 = GT框坐标 - 锚点框坐标

本节总结

本节课中我们一起学习了训练流程第二步的核心逻辑:

  1. 我们从数据集中主要获取 图片(Image)、边界框(b_box)和标签(label) 三种数据。
  2. 图片需要经过尺寸调整和数据增强等预处理。
  3. 更关键的是对 边界框和标签 的处理。由于SSD网络会产生大量锚点框,必须通过样本标记过程,为每个训练用的锚点框分配一个对应的回归目标或背景标签,使其数量与网络输出对齐,才能进行有效的损失计算。

下一节,我们将深入代码,具体实现这些数据的读取与处理逻辑。

课程P64:64.06_训练:数据模块与网络模型获取结果 🧩

在本节课中,我们将学习如何从数据模块和网络模型中获取训练所需的关键信息。我们将分步获取数据规范、网络计算的锚框以及预处理函数,为后续的训练步骤做好准备。


上一节我们分析了获取图片数据的需求。根据这些需求,我们需要从不同的模块中提取信息。

以下是获取每个模块结果的具体步骤:

  1. 通过数据工程,取出规范信息:我们将从数据工厂中获取图片的规范信息。
  2. 获取网络计算的锚框结果:我们将从网络模型中获取计算出的默认锚框,用于后续的正负样本标记。
  3. 获取预处理函数:我们将从预处理工厂中获取数据增强和预处理函数。


第一步:获取数据规范信息

首先,我们需要从数据工厂获取数据集的规范信息。我们使用 get_dataset 这个API。

# 从命令行参数中获取数据集配置
dataset_name = flags.dataset_name  # 例如:'commodity_2018'
train_or_test = flags.train_or_test  # 例如:'train'
dataset_dir = flags.dataset_dir

# 调用数据工厂获取数据规范
dataset = dataset_factory.get_dataset(dataset_name, train_or_test, dataset_dir)

这里的 dataset 是一个数据规范对象,它描述了如何读取和处理数据,而不是数据本身。


第二步:获取网络计算的锚框

接下来,我们需要从网络模型中获取计算出的锚框。这需要我们先获取网络类,然后初始化网络并调用其锚框生成函数。

# 1. 从网络工厂获取网络类
model_name = flags.model_name  # 例如:'SSD_VGG'
ssd_class = nets_factory.get_network(model_name)

# 2. 获取网络的默认参数
ssd_params = ssd_class.default_params

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/4debdb016aa3f74e69e6d4d4e76e5346_24.png)

# 3. 使用默认参数初始化网络实例
ssd_net = ssd_class(ssd_params)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/4debdb016aa3f74e69e6d4d4e76e5346_26.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/4debdb016aa3f74e69e6d4d4e76e5346_28.png)

# 4. 从网络参数中获取输入图片的形状
ssd_shape = ssd_net.params.image_shape  # 例如:(300, 300)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/4debdb016aa3f74e69e6d4d4e76e5346_30.png)

# 5. 调用网络的锚框生成函数,获取所有默认候选框
ssd_anchors = ssd_net.anchors(ssd_shape)

ssd_anchors 就是SSD网络为不同特征图层计算出的所有默认锚框,它们将用于与真实边界框进行匹配和标记。


第三步:获取预处理函数

最后,我们需要获取预处理函数。这部分逻辑通常也封装在工厂中,根据数据集名称和模式(训练/测试)返回相应的预处理管道。

# 获取预处理函数(此处为示意,具体API名称可能不同)
preprocess_fn = preprocess_factory.get_preprocess(dataset_name, train_or_test)

preprocess_fn 是一个函数,它接收原始图片和标注,并返回经过缩放、裁剪、颜色抖动等增强处理后的数据。


步骤总结

在本节中,我们完成了训练准备工作的第一步:从各个工厂模块获取核心组件。

  1. 我们从 dataset_factory 获取了数据规范 (dataset)。
  2. 我们从 nets_factory 获取了网络类,并进一步获取了网络计算出的锚框 (ssd_anchors)。
  3. 我们从 preprocess_factory 获取了预处理函数 (preprocess_fn)。

这些组件是构建数据流水线和执行训练的基础。下一节,我们将学习如何利用这些组件,实际地读取图片数据、应用预处理,并为锚框标记正负样本。

课程P65:网络参数修改、数据获取与预处理 🧠

在本节课中,我们将学习如何修改SSD网络的输出类别参数,并使用数据提供器(provider)从数据集中获取数据,最后对获取的数据进行预处理,为模型训练做好准备。


1. 修改网络参数 🔧

上一节我们获取了网络参数,但有一个关键参数尚未修改。现在,我们来看看如何修改它。

在SSD类的网络源码定义中,有一个参数叫做 network.nb_classes。这个参数定义了网络最终输出的类别数量。我们需要根据数据集的实际情况来修改它。

以下是修改步骤:

  1. 获取默认参数 default_params
  2. 使用 replace 方法,将 nb_classes 参数的值修改为我们的目标类别数。
  3. 对于Pascal VOC数据集,类别数为 20(20个物体类别)加上 1(背景类),因此总数为 21

代码示例:

# 修改网络输出类别数
default_params = default_params.replace(nb_classes=21)

完成此步骤后,网络参数的计算就正确了。


2. 获取数据预处理函数 📥

网络参数准备好后,下一步是处理数据。首先,我们需要获取数据预处理函数。

数据预处理函数存放在 processing_factory 模块中。我们通过调用 get_preprocessing 函数来获取它。

以下是获取预处理函数的步骤:

  1. 第一个参数是预处理名称,它通常与模型名称相关,例如 ‘ssd_vgg’
  2. 第二个参数 is_training 用于指定是否为训练模式。在本训练过程中,我们将其设为 True

代码示例:

# 获取数据预处理函数
image_preprocessing_fn = preprocessing_factory.get_preprocessing(
    model_name, # 例如 ‘ssd_vgg’
    is_training=True
)

这样,我们就得到了一个名为 image_preprocessing_fn 的函数,它将用于后续的数据预处理。


3. 打印网络参数 📋

在继续之前,我们可以先打印出当前的网络配置,以便确认参数设置是否正确。

我们可以使用 train_tools 模块中的 print_configuration 函数来完成这个操作。

代码示例:

# 打印网络相关参数
print_configuration(model_params)

4. 使用Provider获取并预处理数据 🚀

现在,我们进入核心的数据处理环节。数据规范信息已经准备好,但数据本身还没有被加载。我们需要使用 slim.dataset_data_provider 来获取数据,并进行预处理。

以下是完整的四个步骤,我们将逐一讲解:

4.1 指定计算设备

首先,我们需要指定在哪个设备上执行数据获取操作。通常,数据加载和预处理放在CPU上进行。

代码示例:

with tf.device(deploy_config.input_device):
    # 在此设备下执行数据操作

4.2 创建数据提供器(Provider)

在指定的设备作用域内,我们创建一个命名空间来组织数据提供相关的操作。然后,实例化 DatasetDataProvider 来读取数据。

关键参数包括:

  • dataset: 数据集规范信息。
  • num_readers: 读取数据的线程数,例如 4
  • common_queue_capacity: 队列容量。
  • common_queue_min: 队列最小容量。
  • shuffle: 是否打乱数据顺序。

代码示例:

with tf.name_scope(‘data_provider’):
    provider = slim.dataset_data_provider.DatasetDataProvider(
        dataset,
        num_readers=4,
        common_queue_capacity=20 * batch_size,
        common_queue_min=10 * batch_size,
        shuffle=True
    )

4.3 从Provider获取原始数据

Provider实例化后,我们调用其 get 方法来获取具体的张量(tensors)。

我们需要获取以下四项内容:

  1. image: 原始图像数据。
  2. shape: 图像的原始尺寸。
  3. glabels: 真实标签(ground truth labels)。
  4. gbboxes: 真实边界框(ground truth bounding boxes)。

代码示例:

[image, shape, glabels, gbboxes] = provider.get([‘image’, ‘shape’, ‘object/label’, ‘object/bbox’])

4.4 对数据进行预处理

最后,我们使用之前获取的预处理函数 image_preprocessing_fn 来处理原始数据。

预处理函数需要传入以下参数:

  • image: 原始图像。
  • labels: 真实标签 (glabels)。
  • bboxes: 真实边界框 (gbboxes)。
  • out_shape: 模型期望的输入尺寸,例如 [300, 300](SSD300)。
  • data_format: 数据格式,例如 ‘NHWC’(批次数,高度,宽度,通道数)。

代码示例:

# 对图像、标签和边界框进行预处理
processed_image, processed_glabels, processed_gbboxes = image_preprocessing_fn(
    image,
    labels=glabels,
    bboxes=gbboxes,
    out_shape=[300, 300],
    data_format=‘NHWC’
)

处理完成后,processed_image 的形状将变为 [None, 300, 300, 3],符合SSD网络的输入要求。标签和边界框也完成了相应的转换(如归一化、数据增强等)。


总结 📝

本节课中,我们一起学习了训练准备阶段的关键步骤:

  1. 修改网络参数:调整了SSD网络的输出类别数,使其与数据集匹配。
  2. 获取预处理函数:从工厂中获取了用于训练的数据预处理函数。
  3. 打印网络配置:确认了当前的参数设置。
  4. 获取并预处理数据:通过指定设备、创建数据提供器、读取原始数据并应用预处理函数,最终得到了可以直接输入SSD网络进行训练的标准化数据批次。

通过以上步骤,我们为模型的正式训练做好了充分的数据准备。下一节,我们将开始构建完整的训练流程。

课程 P66:66.08_训练:NHWC与NCHW格式介绍 🧠

在本节课中,我们将要学习深度学习中两种常见的数据格式:NHWC与NCHW。理解它们的区别对于正确设置模型输入和处理图像数据至关重要。

概述

在深度学习中,尤其是在处理图像数据时,我们需要将图片组织成多维数组(张量)进行运算。NHWC和NCHW是描述这个多维数组维度顺序的两种主要格式。本节将详细介绍它们的含义、区别以及应用场景。

NHWC与NCHW的区别

那么,NHWC和NCHW有什么区别呢?

在设置图片数据时,有两种格式可以选择。一种是NHWC,这意味着你的数据格式维度顺序为:批处理大小(Batch Size)、图片高度(Height)、图片宽度(Width)和通道数(Channels)。另一种是NCHW,它将通道数(Channels)放在第二个维度,即顺序为:批处理大小(Batch Size)、通道数(Channels)、图片高度(Height)和图片宽度(Width)。

这两种格式的核心区别在于维度的排列顺序不同。

默认格式与排列方式

TensorFlow默认使用的是NHWC格式。

这两种格式的区别在于数据的排列方式。如果是NHWC格式,数据可能按一种方式排列;如果是NCHW格式,则按另一种方式排列。这意味着我们可以通过不同的维度顺序来访问我们的数据。

通过例子理解

我们通过一个具体的例子来理解这两种格式。

假设我们有代表红(R)、绿(G)、蓝(B)三个通道的像素值,例如:

  • 红色通道值:1, 2, 3, 4
  • 绿色通道值:5, 6, 7, 8
  • 蓝色通道值:9, 10, 11, 12

以下是两种格式下数据组织的关键区别:

  • 对于NCHW格式,通道(C)在第二个维度(忽略批处理维度N后,它是第一个维度)。这意味着RGB三个颜色通道的数据被分成三组。你可以在三维数组的第一个维度上分别找到R、G、B三个完整的通道数据。
  • 对于NHWC格式(或简化为HWC格式),通道(C)在最后一个维度。你可以在三维数组的第三个维度上找到每个像素点的RGB颜色值。

三维数组维度解析

为了更好地理解,我们来看一个三维数组的表示。

假设我们有一个代表NCHW格式(忽略N)的三维数组。在这个三维数组中:

  • 第一维度:代表通道(Channels)。在这个维度上,你可以找到R、G、B三个独立的通道数据块。
  • 第二维度:代表图片的高度(Height)。
  • 第三维度:代表图片的宽度(Width)。

其结构可以用一个三维数组表示,其中第一维有三个元素,分别对应R、G、B通道。

而对于NHWC格式(忽略N),在一个三维数组中:

  • 第一维度:代表图片的高度(Height)。
  • 第二维度:代表图片的宽度(Width)。
  • 第三维度:代表通道(Channels)。在这个维度的每个位置上,你可以找到一个像素点的(R, G, B)值。

其结构可以看作一个二维像素网格(H x W),其中每个网格点是一个包含三个元素(R, G, B)的向量。

数据处理中的应用

因此,在数据处理时,TensorFlow默认会使用其原生的NHWC格式(即高度、宽度、通道的顺序)来读取和排列像素,并进行后续的预处理和模型训练。理解这一点有助于确保数据输入格式与框架及模型期望的格式保持一致。

总结

本节课中,我们一起学习了NHWC和NCHW这两种重要的数据格式。

  • NHWC:维度顺序为(批大小, 高, 宽, 通道)。TensorFlow的默认格式。
  • NCHW:维度顺序为(批大小, 通道, 高, 宽)。在某些硬件(如NVIDIA GPU cuDNN)和框架(如PyTorch)上计算效率可能更高。

核心区别在于通道(Channels)维度的位置不同,这影响了数据在内存中的存储方式和访问模式。在实际工作中,需要根据所使用的深度学习框架和硬件优化需求来选择合适的格式。

课程 P67:对锚框进行正负样本标记 🏷️

在本节课中,我们将学习如何为SSD目标检测网络中的锚框(Anchor Boxes)进行正负样本标记。这是将真实标注(Ground Truth)转换为网络训练所需目标值的关键步骤。

上一节我们介绍了数据预处理,获取了图像数据和真实边界框(gt box)。本节中我们来看看如何利用这些真实标注,为网络生成的所有锚框分配类别和位置目标。

锚框标记的目的

输入到网络中进行训练的数据,除了图像本身,还需要对应的目标标签。我们的任务是对网络生成的锚框进行标记,从而得到用于计算损失函数的目标值。在SSD网络中,这个标记过程通过一个特定的编码函数完成。

编码函数:ssd_net.bboxes_encode

该函数利用真实标签(gt_labels)和真实边界框(gt_boxes),与网络生成的所有锚框进行一一对应的计算。核心是计算每个锚框与所有真实框之间的交并比(IoU),并根据IoU值为每个锚框分配一个对应的目标。

以下是该函数的核心作用:

  • 输入:真实类别标签、真实边界框、锚框。
  • 处理:计算锚框与真实框的IoU,根据阈值(如0.5)判断正负样本。
  • 输出:编码后的目标值,其格式与网络预测值的格式保持一致。

编码函数的输出

SSD网络的预测输出包含两部分:预测的类别概率和预测的边界框偏移量。因此,编码函数返回的目标值也必须与之对应。

具体来说,函数返回三个值:

  1. 目标类别 (gt_labels): 每个锚框对应的真实物体类别编号。
  2. 目标位置 (gt_localizations): 每个锚框对应的真实边界框的位置编码(通常是相对于锚框中心的偏移量)。
  3. 目标分数 (gt_scores): 标记每个锚框是正样本还是负样本(例如,1表示正样本,0表示负样本)。

假设网络生成了8732个锚框,经过编码后,我们就得到了8732个标记好的目标值,它们将与网络的8732个预测值进行一一对应的损失计算。

总结

本节课中我们一起学习了SSD目标检测中锚框标记的核心过程。我们了解到,通过bboxes_encode函数,可以将少量的真实标注信息“分配”给大量的锚框,生成网络训练所需的目标类别、目标位置和正负样本标签。这个过程是连接数据标注与模型训练的关键桥梁,确保了模型能够学习到如何从锚框回归到真实物体的位置和类别。

课程P68:68.10_训练:批处理获取与数据形状变换 🧠

在本节课中,我们将学习如何对处理好的图片数据和标签进行批处理,以及如何将嵌套的列表数据转换为适合TensorFlow批处理函数要求的单层列表格式。

上一节我们介绍了如何读取和处理图片信息及其对应的正负样本标签。本节中,我们来看看如何将这些单个样本组织成批次,以便于高效训练。

批处理的需求与函数

在训练神经网络时,我们通常需要同时处理多个样本,而不是单个样本。TensorFlow提供了一个名为 tf.train.batch 的函数来实现批处理。

以下是 tf.train.batch 函数的核心参数:

  • tensor_list:一个由 Tensor 对象组成的列表。
  • batch_size:每个批次中包含的样本数量。
  • num_threads:用于入队的线程数。
  • capacity:队列的最大容量。

数据形状问题与转换

我们首先需要确定哪些数据需要进行批处理。这包括图片数据(image)、类别标签(gclasses)、位置坐标(glocalization)和置信度分数(gscores)。

然而,直接将这些数据传入 tf.train.batch 会遇到问题。通过打印 gclassesglocalization 的形状,我们发现它们本身是包含多个 Tensor 的嵌套列表(对应网络的六个预测层),而不是一个由单个 Tensor 组成的简单列表。

代码示例:问题数据形状

# 打印查看数据结构
print(gclasses)   # 输出可能为: [<tf.Tensor...>, <tf.Tensor...>, ...] (一个列表)
print(glocalization) # 输出类似,也是一个列表
# 但 tf.train.batch 需要的是 tensor_list = [tensor1, tensor2, tensor3, ...]

这不符合 tf.train.batchtensor_list 参数的要求。因此,我们需要将这种嵌套的列表结构转换成一个扁平的、由单个 Tensor 组成的列表。

实施数据转换与批处理

为了解决上述问题,我们使用 train_tools.reshape_list 工具函数。这个函数的作用正是将嵌套的列表转换成一个单层的列表。

代码示例:实施转换与批处理

# 1. 将需要批处理的数据组合成一个列表
data_to_batch = [image, gclasses, glocalization, gscores]

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/f5feffa018a44db75867a5659a9bfe4a_25.png)

# 2. 使用 reshape_list 函数转换嵌套结构
flattened_list = train_tools.reshape_list(data_to_batch)

# 3. 将转换后的单层列表送入 tf.train.batch 函数
batch_tensors = tf.train.batch(
    tensor_list=flattened_list,
    batch_size=FLAGS.batch_size,
    num_threads=4,
    capacity=5 * FLAGS.batch_size
)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/f5feffa018a44db75867a5659a9bfe4a_27.png)

# 打印批处理后的结果以验证
print(batch_tensors)

运行上述代码后,打印 batch_tensors 的结果显示,它现在是一个包含19个 Tensor 的列表(1个image + 6个gclasses + 6个glocalization + 6个gscores),并且每个 Tensor 的第一维(batch 维度)都变成了我们设定的批次大小(例如32)。这表明批处理已成功完成,数据形状符合后续训练步骤的要求。

本节课中我们一起学习了批处理的重要性,识别了数据形状不匹配的问题,并掌握了使用 tf.train.batchreshape_list 工具将数据处理成适合训练批次的方法。这是构建高效训练流程的关键一步。

🚀 课程 P69:69.11_训练:批处理与队列设置

在本节课中,我们将学习如何对预处理后的数据进行批处理,并将其放入队列中,以便高效地供给多个设备进行模型训练。


上一节我们介绍了单个样本的数据预处理流程。本节中我们来看看如何将多个样本组织成批次,并利用队列机制进行管理。

批处理之后,数据从单个样本转变为多个样本的集合。接下来,当模型读取并获取这些数据以计算损失和进行前向传播时,需要一个队列机制。SLAM框架默认提供了这种队列机制,用于存放批处理数据。

以下是队列设置的核心函数 prefetch 的用法:

bench_queue = prefetch(tensor_list, capacity=0)
  • tensor_list:需要放入队列的张量列表。注意,传入的必须是一个由张量组成的列表。
  • capacity:队列的容量大小。

我们之前打印的批处理结果 R 已经是一个由张量组成的列表形式,因此可以直接传入 prefetch 函数。

现在,我们将批处理数据放入队列:

# 放入队列:批处理数据
bench_queue = prefetch(tensor_list=R, capacity=0)

参数 capacity 定义了队列的大小。队列的目的是为了满足不同设备的数据需求。例如,如果有五个训练设备,每个设备都需要一个批次的数据(即一个 R),那么总共就需要五个 R,相当于五组批处理样本。

因此,capacity 通常应设置得大一些,至少等于设备数量。设备数量可以从部署配置中获取:

# 从部署配置中获取设备数量,并以此设置队列容量
num_devices = deployment_config.NCS
bench_queue = prefetch(tensor_list=R, capacity=num_devices)

这样,bench_queue 中就会准备好足够多组(例如五组)的批处理数据。


当我们完成数据放入队列的操作后,就可以从中取出数据供模型使用了。至此,我们第一阶段的核心目标——获取网络对接数据以及处理样本的GT标记——已经全部完成。

最后,我们来总结一下整个第二步的关键步骤:

  1. 提取与规范数据:从各个模块中提取出原始数据、标注信息等。
  2. 指定设备与获取数据:在指定的CPU设备上,通过 provider 获取单张图片的信息。
  3. 数据预处理:对获取的图片进行一系列处理操作。
  4. 生成训练目标:利用 SSDNetBBoxesEncoder 工具,根据处理后的 imagelabelgt_bbox 生成模型训练所需的目标值标记,区分正负样本。
  5. 批处理与队列设置:将多个单样本数据组合成批次,并放入队列中,为多设备并行训练做好准备。

以上就是我们在数据准备第二步中需要完成的所有工作。


本节课中我们一起学习了如何将预处理后的数据组织成批,并通过设置队列来高效管理这些批次数据,为后续的多设备模型训练做好了准备。

课程P7:目标检测任务描述 🎯

在本节课中,我们将学习目标检测任务的基本概念。我们将了解目标检测算法的分类、常见的评估指标,并探讨一种实现目标定位的简单方法。

算法分类 🔍

上一节我们介绍了课程目标,本节中我们来看看目标检测算法的分类。目标检测算法主要分为两大类。

以下是两种主要算法类别及其特点:

  • 两步走的目标检测:这类算法首先进行区域推荐,然后对推荐的区域进行分类。
  • 端到端的目标检测:这类算法通过一个网络直接从输入图像得到物体的类别和位置,一步到位。

为了更直观地理解,我们可以看下面的对比图。两步走的方法(如R-CNN系列)先找出图像中可能包含物体的区域,再对这些区域进行分类。而端到端的方法(如YOLO、SSD)则直接将整张图输入网络,一次性输出所有检测到的物体及其位置。

任务回顾与定义 📝

上一节我们介绍了算法的分类,本节中我们来看看目标检测任务的具体定义。首先,让我们回顾一下基础的图像分类任务。

在图像分类中,我们使用卷积神经网络(CNN)。输入一张图片,经过卷积、激活、池化等层,最后通过全连接层输出。输出向量的长度等于类别数量,每个值代表该类别的概率。训练时,我们使用Softmax函数计算概率,并通过交叉熵损失函数进行优化。

常见的分类模型有AlexNet、VGG、GoogleNet、ResNet等。随着模型发展,识别准确率不断提升,例如ResNet在ImageNet数据集上的Top-5错误率达到了3.57%,超过了人类的识别水平。

现在,我们引入目标检测。它比分类更复杂,需要同时完成两件事:

  1. 分类:识别图像中的物体是什么。
  2. 定位:找出物体在图像中的具体位置。

这里有一个重要的概念区分:

  • 分类+定位:通常指图像中只有一个主要物体时,同时识别其类别和位置的任务。
  • 目标检测:通常指图像中有多个物体时,识别所有物体类别和位置的任务。

评估指标 📊

上一节我们明确了任务,本节中我们来看看如何评估模型的好坏。对于分类任务,我们通常使用准确率(Accuracy)来评估。

对于定位任务,我们需要评估预测的物体框(Bounding Box)与真实框的接近程度。这里涉及两个关键术语:

  • Ground Truth Bounding Box (gt_bbox):数据集中人为标记的物体真实位置框。
  • Predicted Bounding Box (pred_bbox):模型预测出的物体位置框。

衡量这两个框重叠程度的指标叫做交并比(Intersection over Union, IoU)。其计算公式为:

IoU = (预测框与真实框的交集面积) / (预测框与真实框的并集面积)

IoU值越高,说明预测框与真实框重叠得越好,定位越准确。当两个框完全重合时,IoU值为1。

一种简单的实现思路 💡

上一节我们介绍了评估指标,本节中我们来看看如何用简单的方法实现“分类+定位”。我们的目标是让网络同时输出类别和位置。

解决思路是扩展CNN网络的输出。假设我们要识别10个类别,网络原本会输出10个类别的概率值。我们可以增加一个全连接层,让它额外输出4个值,代表物体的位置(例如中心点坐标x, y和宽高w, h)。

因此,网络的最终输出包含两部分:

  1. 分类输出:10个类别的概率(使用Softmax)。
  2. 定位输出:4个坐标值(一个回归问题)。

在训练时,我们需要计算两个损失:

  • 分类损失:使用交叉熵损失函数,衡量预测类别与真实类别的差异。
  • 定位损失:使用均方误差(MSE)损失函数,衡量预测坐标与真实坐标的差异。

总的损失是这两个损失的加权和,通过反向传播同时优化网络参数。对于坐标值,通常需要进行归一化处理(例如除以图像宽高),以便于模型训练。

总结 🏁

本节课中我们一起学习了目标检测的基础知识。我们了解了目标检测算法的两大分类(两步走 vs. 端到端),明确了分类+定位与目标检测的任务区别。我们学习了定位任务的核心评估指标IoU,并探讨了一种通过修改网络输出层来实现单物体分类与定位的简单思路。这为后续学习更复杂的目标检测算法奠定了基础。

课程 P70:多GPU训练模型复制与优化器配置 🚀

在本节课中,我们将学习如何将定义好的网络模型和损失计算过程复制到多个GPU设备上,并配置学习率与优化器,为后续的分布式训练做好准备。

概述

上一节我们完成了数据批处理队列的构建。本节我们将回到训练主流程,执行以下核心步骤:

  1. 将网络模型、损失计算等操作复制到多个GPU设备。
  2. 配置学习率衰减策略和优化器。

第三步:模型复制、损失计算与变量观察 🔄

数据准备就绪后,下一步是将数据输入网络进行计算,得到预测结果并定义损失函数。这些操作需要被复制到多个计算设备(Client)上,并为每个设备添加TensorBoard观察点。

以下是实现这一步骤的关键操作:

  • 使用函数train_tools.deploy_loss_summary
  • 函数作用:该函数将网络配置、数据队列和网络模型作为输入,自动完成模型复制、损失计算和摘要(summary)添加。
  • 输入参数
    • cluster_config:设备集群配置,指定复制份数。
    • batch_queue:批处理数据队列。
    • net:需要进行计算的网络模型。
    • summaries:需要通过TensorBoard观察的变量集合。
    • batch_shapes:指定队列中每个数据元素的张量形状。
  • 返回参数
    • first_clone_ops:第一个设备上的所有操作集合。
    • first_clone_scope:第一个设备的名称。
    • clones:所有设备的输出、名称及其所在设备信息的集合。

函数工作原理

假设我们有三个GPU设备。该函数执行以下流程:

  1. 定义网络前向传播、计算预测结果和损失的操作。
  2. 将这些完整的计算图复制到三个GPU设备上,每个设备都拥有独立的计算过程。
  3. 默认选择第一个设备(clone_0)作为主要观察对象,将其上的变量和损失值添加到TensorBoard摘要中。

代码实现

我们将在训练流程的第三步调用此函数。

# 第三步:复制模型到不同GPU设备,计算损失,并添加变量观察
# 获取需要观察的摘要集合
summaries = set(tf.get_collection(tf.GraphKeys.SUMMARIES))

# 定义批处理队列中数据的形状
# 假设数据包含图像和若干标注张量
batch_shapes = [1, ] + [3 * ssd_anchors.num_anchors]  # 示例形状,需根据实际网络结构调整

# 调用函数进行模型部署
first_clone_ops, first_clone_scope, clones = train_tools.deploy_loss_summary(
    cluster_config=deploy_config,
    batch_queue=batch_queue,
    net=ssd_net,
    summaries=summaries,
    batch_shapes=batch_shapes
)

通过以上代码,我们完成了模型在多设备上的复制,并为监控训练过程准备好了观察点。


第四步:定义学习率与优化器 ⚙️

损失计算图在多设备上部署完成后,接下来需要定义学习率衰减策略和优化器,以便更新网络参数。

关键概念与函数

  1. 学习率配置:使用 train_tools.configure_learning_rate 函数。它根据配置参数、总样本数和全局步数,生成一个随时间衰减的学习率张量。
  2. 优化器配置:使用 train_tools.configure_optimizer 函数。它接收配置参数和学习率张量,返回一个优化器对象。
  3. 运行设备:学习率和优化器的定义通常在CPU设备上进行,而梯度计算和参数更新则分布在各个GPU上。

添加必要参数

在配置之前,需要在命令行参数中添加相关配置项,例如学习率类型、初始学习率、终止学习率、优化器类型以及权重衰减系数等。

# 在flags定义中添加训练相关参数
flags.DEFINE_string('learning_rate_decay_type', 'exponential', '学习率衰减类型')
flags.DEFINE_float('learning_rate', 0.01, '初始学习率')
flags.DEFINE_float('end_learning_rate', 0.0001, '终止学习率')
flags.DEFINE_string('optimizer', 'momentum', '优化器类型')
flags.DEFINE_float('weight_decay', 0.0005, '权重衰减系数,用于防止过拟合')

代码实现

我们将在指定的优化器设备(通常是CPU)上定义学习率和优化器。

# 第四步:定义学习率与优化器
with tf.device(deploy_config.optimizer_device()):
    # 配置学习率
    learning_rate = train_tools.configure_learning_rate(
        flags.FLAGS,
        dataset.num_samples,  # 数据集中总样本数
        global_step
    )
    # 将学习率添加到摘要,便于在TensorBoard中观察
    summaries.add(tf.summary.scalar('learning_rate', learning_rate))

    # 配置优化器
    optimizer = train_tools.configure_optimizer(flags.FLAGS, learning_rate)

这段代码完成了学习率策略的定义和优化器的创建,并将学习率的变化情况添加到了监控摘要中。


总结

本节课中,我们一起学习了多GPU训练流程中的两个关键环节:

  1. 模型复制与观察:我们使用 deploy_loss_summary 函数,将完整的网络计算图(包括前向传播和损失计算)复制到多个GPU设备,并设置了TensorBoard监控点,便于观察第一个设备上的训练变量。
  2. 学习率与优化器配置:我们在CPU设备上定义了随着训练步数衰减的学习率策略,并创建了优化器对象,为下一步的多设备梯度计算和参数同步做好了准备。

至此,我们已经构建了分布式训练的计算框架。下一节,我们将在此基础上实现梯度的计算、汇总与参数的更新操作。

课程 P71:分布式训练 - 总损失计算、梯度平均与训练配置 🚀

在本节课中,我们将学习分布式训练流程中的两个核心步骤:计算所有设备的总损失与平均梯度,以及配置最终的训练会话。我们将了解如何整合优化器、损失和梯度信息,并设置训练循环的参数,以启动多GPU设备的协同训练。


第五步:计算总损失与变量平均梯度 🔢

上一节我们配置了基础的学习率和优化器。本节中,我们来看看如何为分布式环境计算用于优化的关键指标:所有设备的总损失和每个可训练变量的平均梯度。

以下是实现此步骤的核心函数调用:

train_op, summary_scalar_op = train_tools.get_a_train_op(
    optimizer=optimizer,
    summaries=summaries,
    clones=clones,
    global_step=global_step,
    update_ops=update_ops,
    variables_to_train=variables_to_train
)
  • train_op: 这是训练操作(Operation),它封装了计算梯度并应用更新(如 optimizer.apply_gradients)的逻辑。
  • summary_scalar_op: 这是用于记录标量摘要(如损失值)的操作,便于在TensorBoard等工具中可视化。

该函数内部主要完成以下工作:

  1. 聚合所有计算设备(GPU)上的损失值,计算总损失
  2. 收集所有设备上对同一变量计算出的梯度,并计算其平均值。梯度平均是数据并行分布式训练的标准做法,公式可表示为:
    平均梯度 = (设备1梯度 + 设备2梯度 + ... + 设备N梯度) / N
  3. 使用优化器(如Adam、SGD)和这些平均梯度来更新模型变量。
  4. 返回用于执行训练和记录摘要的OP。

第六步:配置训练会话并启动训练 ⚙️

在获得了训练操作 train_op 后,我们需要配置训练会话并启动实际的训练循环。这一步涉及设置训练步数、模型保存、日志记录等参数。

以下是启动训练的核心代码结构:

slim.learning.train(
    train_op=train_op,
    logdir=train_model_dir,
    master=FLAGS.master,
    is_chief=FLAGS.task == 0,
    init_fn=init_fn,
    summary_op=summary_scalar_op,
    number_of_steps=FLAGS.number_of_steps,
    log_every_n_steps=FLAGS.log_every_n_steps,
    save_summaries_secs=FLAGS.save_summaries_secs,
    save_interval_secs=FLAGS.save_interval_secs,
    saver=saver,
    session_config=config
)

配置会话参数 (Session Config)

在创建 slim.learning.train 所需的 config 时,我们通常进行如下设置,以控制训练过程的行为:

config = tf.ConfigProto(
    log_device_placement=False,  # 是否打印每个操作所在的设备,通常设为False以避免输出过于冗长
    allow_soft_placement=True,   # 允许当指定设备不存在时,自动分配到其他可用设备
    gpu_options=tf.GPUOptions(per_process_gpu_memory_fraction=0.8)  # 限制每个GPU进程的内存使用率为80%,防止内存耗尽
)

配置模型保存器 (Saver)

saver 负责保存和恢复模型的检查点(checkpoint)。可以配置其保留最新模型的数量和保存策略。

saver = tf.train.Saver(
    max_to_keep=5,               # 最多保留5个最新的检查点文件
    keep_checkpoint_every_n_hours=0.5,  # 每0.5小时额外保留一个检查点
    pad_step_number=False        # 是否用0填充步数编号
)

初始化函数 (Init Function)

init_fn 是一个关键参数,它定义了模型参数的初始化逻辑。其核心作用是处理预训练模型加载微调模型恢复的优先级。

以下是 init_fn 的典型逻辑:

  1. 检查 finetuning_dir(微调输出目录)下是否存在已保存的模型。如果存在,则从中恢复,因为这是基于当前数据集训练得到的最新、最相关的参数。
  2. 如果不存在,则从 pretrain_dir(预训练模型目录)加载基础权重。这通常是在大规模通用数据集(如ImageNet)上预训练的模型。
  3. 通过 train_tools.get_init_fn() 函数可以实现这一逻辑。


总结 📝

本节课中我们一起学习了分布式训练流程的最后两个环节:

  1. 计算总损失与平均梯度:我们使用 train_tools.get_a_train_op 函数聚合所有GPU设备的损失,并计算每个可训练变量的梯度平均值,为优化器提供更新依据。
  2. 配置并启动训练:我们详细介绍了 slim.learning.train 函数的各项参数,包括会话配置(控制设备与内存)、模型保存器设置以及至关重要的参数初始化函数 init_fninit_fn 确保了训练能从正确的检查点(优先微调模型,其次预训练模型)开始,使训练过程能够高效、稳定地进行。

至此,一个完整的分布式模型训练配置流程就构建完成了。

课程 P72:SSD模型训练流程总结 🚀

在本节课中,我们将学习并总结SSD(Single Shot MultiBox Detector)模型的完整训练流程。我们将从代码运行开始,逐步讲解训练过程中的关键步骤,包括参数设置、数据读取、损失计算、优化器定义以及使用TensorBoard进行可视化监控,最后对整个训练流程进行梳理和总结。


运行训练代码

上一节我们介绍了训练代码的编写,本节中我们来看看如何运行它并开始训练过程。

我们直接运行之前编写的训练代码。代码中涉及的参数与之前定义的参数完全一致,包括预训练模型路径(CKPT)和微调模型的保存路径。

以下是运行命令示例:

python train_ssd_network.py --ckpt_path=./pretrained_model --save_dir=./finetuned_model

注意,我们的训练脚本位于 train_ssd_network 目录下。在终端中粘贴并执行上述命令。

在运行前,需要关闭代码中用于调试的Tensor打印语句,以避免输出混乱。找到代码中变换形状或打印张量的位置,将其注释或关闭。

运行成功后,控制台会显示读取了数据文件,并提示每批次(batch)加载的数据量。例如,GD训练可能设置为每批次2个数据。批次大小可以根据你的硬件和数据集情况随意指定,只要总训练步数和数据集足够即可。

训练过程现在已经开始。由于模型计算量庞大,训练可能非常耗时,使用GPU训练也可能需要一至数天时间。因此,在课堂演示中,我们使用现成模型进行训练,其效果可能不是最优的。在实际应用中,通常会使用已经训练了较长时间(例如两到三天)的模型以获得更好性能。

训练时,可以观察控制台输出的每秒步数(steps per second)。由于计算复杂,这个数值通常会非常小。


监控训练过程

当训练开始后,模型文件会保存到指定的 CKPT 目录下的 finetuning 文件夹中。同时,会生成一个用于TensorBoard可视化的 summary 目录。

为了监控训练过程,我们可以新建一个终端,启动TensorBoard服务来观察训练指标。

以下是操作步骤:

  1. 首先,激活你的Python虚拟环境,因为TensorBoard依赖该环境。
  2. 切换到你的工作空间和项目目录。
  3. 进入保存summary日志的目录(通常是 CKPT/finetuning 下的某个文件夹)。
  4. 运行TensorBoard命令。

示例命令如下:

source activate your_env_name
cd /workspace/detection_ml/online/v4
tensorboard --logdir=.

运行后,TensorBoard会启动一个本地服务。我们可以在浏览器中打开提供的地址(通常是 http://localhost:6006)来观察训练进程。

在TensorBoard中,我们可以查看以下内容:

  • Scalars(标量):这里记录了全局训练步数(global_step)、损失值(loss)以及学习率(learning_rate)的变化曲线。这是我们最关心的部分,用于判断模型是否在有效学习。
  • Images(图像):这里展示了训练过程中图像经过数据增强(如翻转、颜色变化)后的效果,以及模型预测的边界框与真实标注的对比。
  • Graphs(计算图):这里展示了模型的计算图结构。由于SSD模型结构复杂,此图会非常庞大,通常不需要深入分析。
  • Histograms(直方图):这里展示了模型各层参数(权重、偏置等)的分布变化情况。

目前,由于我们仅训练了很少的步数(例如19步),损失曲线可能呈现不稳定的状态,甚至暂时升高。随着训练步数增加,损失曲线通常会逐渐下降并趋于平缓。在演示中,我们暂停了训练,因此无法看到完整的收敛曲线。在实际训练中,你可以通过TensorBoard持续观察损失变化,以判断训练是否正常。

观察完毕后,可以关闭TensorBoard服务。


训练流程总结 🧠

整个训练程序虽然代码复杂,但我们可以按照清晰的数据流和操作流程来理解。

以下是训练流程的核心步骤总结:

  1. 初始化设置

    • 定义训练设备(如GPU或CPU)。
    • 定义全局步数(global_step)变量,用于记录训练进度。
  2. 数据准备

    • 读取数据集。
    • 对数据进行预处理和增强(如缩放、翻转)。
    • 对先验框(anchor boxes)进行编码,标记正负样本。

  1. 前向传播与损失计算

    • 定义SSD网络结构。
    • 将数据输入网络,得到预测结果。
    • 计算损失(loss),包括定位损失(localization loss)和置信度损失(confidence loss)。
  2. 监控与可视化

    • 添加TensorBoard的summary操作,用于监控损失、学习率等标量,以及图像和参数直方图。
  3. 优化器配置

    • 定义学习率及其变化策略(如指数衰减)。
    • 定义优化器(如Adam或SGD),用于更新网络参数。
  4. 梯度计算与参数更新(分布式训练关键)

    • 在每个GPU设备上独立计算损失和梯度。
    • 将所有GPU计算出的梯度汇总到CPU。
    • 由CPU统一执行优化器步骤,更新所有设备上的模型参数。
  5. 模型保存

    • 定期将训练好的模型参数保存到指定路径(如.ckpt文件),以便后续评估或继续训练。


本节课中我们一起学习了SSD模型的完整训练流程。我们从运行训练脚本开始,了解了如何设置参数并启动训练。接着,我们学习了如何使用TensorBoard工具来可视化监控训练过程中的关键指标,如损失曲线和学习率。最后,我们系统性地总结了训练代码的七个核心步骤:从初始化、数据准备、前向计算损失,到配置优化器、计算梯度、更新参数以及保存模型。理解这个流程对于掌握任何深度学习模型的训练都至关重要。

课程 P73:73.01_测试流程介绍与代码实现 🧪

在本节课中,我们将学习如何对训练好的目标检测模型进行测试。我们将详细介绍测试流程的每一步,包括数据准备、模型加载、预测以及结果的后处理与可视化,并最终在 Jupyter Notebook 中实现完整的测试代码。


测试流程概述 📋

上一节我们完成了模型的训练,本节中我们来看看如何对训练好的模型进行测试。测试流程与训练流程有相似之处,但也有其独特步骤。

测试流程的目标是:完成一个简单的测试,了解如何加载已学习的模型、如何产生预测结果,以及如何处理这些预测结果。

以下是测试流程的核心步骤:

  1. 准备数据:输入一张图片。
  2. 图片预处理:将图片调整至模型所需的输入尺寸。
  3. 加载模型:加载训练好的权重。
  4. 模型预测:将处理后的图片输入模型,得到预测输出。
  5. 后处理:对模型的原始输出进行筛选和调整,得到最终的检测框。
  6. 结果可视化:使用工具库将带有检测框的图片显示出来。

与训练流程的主要区别在于:

  • 预处理:测试时只需进行尺寸缩放 (resize),无需数据增强。
  • 后处理:测试时必须进行,包括通过置信度 (score) 筛选、非极大值抑制 (NMS) 以及将边界框 (bbox) 坐标缩放回原图尺寸。训练时不需要此步骤。


文件结构准备 📁

为了组织代码,我们需要创建特定的文件结构。我们将在项目根目录下创建一个名为 test 的目录。

以下是测试所需的文件结构:

test/
├── ssd_notebook.ipynb          # 测试流程的主代码文件
└── visualization.py            # 用于结果可视化的辅助代码

我们选择在 Jupyter Notebook 中编写测试流程,因为它便于我们逐步执行、输入图片、查看中间结果,并直接利用 matplotlib 等库可视化最终图片。

因此,我们需要:

  1. 创建 test 文件夹。
  2. 将提供的 visualization.py 文件复制到该文件夹下。这个文件包含一个针对商品数据集的显示函数。
  3. test 目录下启动 Jupyter Notebook 并创建新的 notebook 文件,例如命名为 predict_image.ipynb


代码实现:逐步构建测试流程 💻

现在,我们进入 Jupyter Notebook 环境,开始一步步编写测试代码。

第一步:导入必要的模块

首先,我们需要导入所有必需的 Python 模块。为了能调用项目其他目录下的处理模块,我们需要将上级目录添加到系统路径中。

import sys
sys.path.append('../')  # 添加上级目录到路径,以便调用项目模块

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/c29eafed08b5e9ee1345407ca2fb2599_45.png)

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from preprocessing import preprocessing_factory
from nets import nets_factory
from notebooks.test import visualization  # 导入可视化工具

代码解释

  • sys.path.append('../'):确保 Python 能在项目根目录下查找和导入我们自定义的模块(如 preprocessing, nets)。
  • 导入 tensorflow, numpy, matplotlib 用于张量操作、数值计算和绘图。
  • preprocessingnets 模块导入工厂类,用于获取预处理函数和网络模型。
  • 导入我们准备好的可视化模块。

第二步:定义数据输入占位符

在 TensorFlow 中,我们常使用 placeholderfeed_dict 的方式在运行会话时输入数据。

# 定义输入图片数据的占位符
# 形状为 [None, None, None, 3],表示批次数、高、宽、通道数均可变
image_input = tf.placeholder(tf.uint8, shape=(None, None, None, 3))

代码解释

  • tf.placeholder 定义了一个数据占位符 image_input
  • 类型为 tf.uint8,对应图片的像素值范围。
  • 形状 (None, None, None, 3) 表示:
    • 第一个 None:批处理大小(可输入单张或多张图片)。
    • 第二、三个 None:图片的高度和宽度(可接受任意尺寸的图片)。
    • 3:RGB 三个颜色通道。

第三步:图片预处理

接下来,我们需要对输入的图片进行预处理,使其符合模型输入要求(例如,缩放到 300x300 像素)。

# 获取预处理函数工厂
preprocessing_fn = preprocessing_factory.get_preprocessing(
    'ssd_vgg_300',  # 模型名称
    is_training=False  # 测试阶段,非训练
)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/c29eafed08b5e9ee1345407ca2fb2599_57.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/c29eafed08b5e9ee1345407ca2fb2599_59.png)

# 定义网络输入形状
net_shape = (300, 300)  # SSD300 模型的输入尺寸

# 应用预处理函数
# 对于测试,没有标签和边界框,所以传入 None
image_pre, _, _, bbox_img = preprocessing_fn(
    image_input,
    None,  # labels
    None,  # bboxes
    out_shape=net_shape,
    data_format='NHWC'  # 数据格式:批次数、高、宽、通道
)

# 卷积神经网络通常需要4维输入 [batch, height, width, channels]
# 因此需要扩展维度
image_4d = tf.expand_dims(image_pre, axis=0)

代码解释

  1. 通过 preprocessing_factory.get_preprocessing 获取针对 ssd_vgg_300 模型在测试模式下的预处理函数 preprocessing_fn
  2. 调用该函数处理 image_input。由于是测试,没有真实标签和边界框,所以 labelsbboxes 参数传入 None
  3. 参数 out_shape=net_shape 指定输出图片尺寸为 300x300。
  4. 函数返回处理后的图片 image_pre 和一个用于后续后处理的参考值 bbox_img
  5. 使用 tf.expand_dims(image_pre, axis=0) 将图片从 [height, width, channels] 扩展为 [1, height, width, channels],添加了批次维度。

第四步:加载模型并进行预测

现在,我们需要加载 SSD 网络模型,并将预处理后的图片输入模型以得到预测结果。

# 获取网络工厂
network_fn = nets_factory.get_network_fn(
    'ssd_vgg_300',  # 模型名称
    num_classes=8 + 1,  # 商品数据集类别数 (8类商品 + 1个背景类)
    is_training=False  # 测试模式
)

# 在 Jupyter Notebook 中运行时,需要处理变量重用问题
# 第一次运行为创建新变量,后续运行需重用已创建的变量
reuse = True if 'ssd_net' in locals() else False

# 使用 slim.arg_scope 设置网络层的公共参数(如数据格式)
with tf.contrib.slim.arg_scope(network_fn.arg_scope(data_format='NHWC')):
    # 将图片输入网络,得到预测结果
    predictions, localizations, _, _ = network_fn(
        image_4d,
        is_training=False,
        reuse=reuse
    )

代码解释

  1. 通过 nets_factory.get_network_fn 获取 SSD 网络构造函数 network_fn。关键参数 num_classes 必须设置为商品数据集的类别数(8类商品 + 1个背景类 = 9)。
  2. reuse 逻辑:在 Jupyter Notebook 中,如果重复运行定义网络的单元格,TensorFlow 会报错,因为变量已存在。此逻辑判断如果 'ssd_net' 已存在,则设置 reuse=True 以重用变量。
  3. tf.contrib.slim.arg_scope 用于为网络内部的许多层函数统一设置默认参数,这里统一指定数据格式为 'NHWC'
  4. 调用 network_fn,输入预处理后的图片 image_4d,并指定 is_training=Falsereuse 参数。函数返回的 predictions 是类别预测置信度,localizations 是预测的边界框偏移量。

第五步:后处理与结果可视化

模型输出的 predictionslocalizations 是原始数据,包含了大量默认框(anchor)的预测信息。我们需要通过后处理来筛选出最终的、有意义的检测结果。

后处理通常包括:

  • 置信度筛选:过滤掉置信度低于阈值的预测。
  • 非极大值抑制 (NMS):去除对同一物体的重复检测框。
  • 坐标转换与缩放:将预测的边界框坐标从网络输出格式转换并缩放到原始输入图片的尺寸。

这部分代码逻辑相对复杂,会用到 SSD 算法中定义的解码和筛选函数。为了课程清晰,我们在此概述步骤,具体函数调用请参考项目提供的 visualization.py 或后续教程。

# 注意:此处为逻辑示意,具体实现需调用项目中的后处理函数
# 1. 解码:将 localizations 和 anchor 信息结合,得到图像上的绝对坐标。
# 2. 筛选:根据 predictions 的置信度进行阈值筛选。
# 3. NMS:对筛选后的框进行非极大值抑制。
# 4. 缩放:将框的坐标从网络输入尺寸(300x300)缩放到原始图片尺寸。

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/c29eafed08b5e9ee1345407ca2fb2599_77.png)

# 假设我们有一个后处理函数 `postprocess`
final_boxes, final_scores, final_labels = postprocess(
    predictions, localizations, bbox_img, original_image_shape
)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/c29eafed08b5e9ee1345407ca2fb2599_79.png)

# 使用 visualization.py 中的函数显示结果
visualization.plt_bboxes(original_image, final_boxes, final_labels, final_scores)

流程总结

  1. 开启一个 TensorFlow 会话 (tf.Session)。
  2. 在会话中,运行之前定义好的计算图。需要向 image_input 这个占位符 feed 一张真实的图片数据(例如,通过 cv2.imread 读取)。
  3. 运行的计算节点应包括:预处理后的图片 image_4d、网络预测 predictionslocalizations
  4. 将运行得到的原始预测结果,送入后处理流程,得到最终的边界框、置信度和类别标签。
  5. 最后,调用可视化函数,将原始图片和绘制好的检测框一起显示出来。

总结 🎯

本节课中,我们一起学习了目标检测模型的完整测试流程。

我们首先概述了测试的步骤,明确了其与训练流程的异同。接着,我们准备了测试环境的文件结构。最后,我们在 Jupyter Notebook 中逐步实现了测试代码,涵盖了数据输入、预处理、模型加载与预测等核心环节。

通过本课的学习,你应该能够理解如何将训练好的模型应用于新图片,并得到可视化的检测结果。虽然后处理的具体代码未完全展开,但你已经掌握了测试流程的主干和关键概念。在后续实践中,你可以进一步完善后处理部分,并尝试用自己训练的模型检测不同的图片。

课程P74:74.02_测试:图片输入与结果标记 🖼️➡️📦

在本节课中,我们将学习如何加载已训练好的模型,输入一张图片,运行模型进行预测,并对预测结果(如物体位置框)进行筛选和可视化标记。我们将一步步完成从模型加载到结果展示的整个测试流程。


创建会话与加载模型

上一节我们完成了模型的训练与保存,本节中我们来看看如何加载模型并进行测试。首先,我们需要创建一个TensorFlow会话并加载之前保存的模型文件。

以下是创建交互式会话并加载模型的关键步骤:

  1. 创建交互式会话:使用 tf.InteractiveSession 并配置相关参数,这便于在Notebook等交互式环境中使用。
  2. 初始化全局变量:运行 tf.global_variables_initializer() 来初始化计算图中的所有变量。
  3. 创建Saver并恢复模型:创建一个 tf.train.Saver 对象,并使用 saver.restore() 方法从指定的检查点文件(.ckpt)中恢复模型参数。
# 示例代码
import tensorflow as tf

# 1. 创建交互式会话
sess = tf.InteractiveSession(config=tf.ConfigProto(log_device_placement=False))

# 2. 初始化全局变量
sess.run(tf.global_variables_initializer())

# 3. 创建Saver并加载模型
saver = tf.train.Saver()
model_path = ‘../ckpt/finetuning/model.ckpt-0‘ # 请替换为你的模型路径
saver.restore(sess, model_path)

注意:模型的效果取决于训练时长。仅训练几分钟的模型可能无法准确识别物体,因此在实际应用中应使用充分训练后的模型。


准备输入图片并运行模型

模型加载完毕后,下一步是准备输入数据。我们需要读取一张图片,并将其处理成模型可接受的格式(例如,调整尺寸、转换为数组、扩充维度等)。

以下是处理输入图片并运行模型进行预测的步骤:

  1. 读取并转换图片:使用图像处理库(如PIL)打开图片,并将其转换为RGB格式的数组。
  2. 运行会话获取预测结果:通过 sess.run() 方法,将处理后的图片数据传入模型的输入占位符,运行计算图,获取模型的输出张量。这些输出通常包括预测类别概率、边界框位置等信息。
# 示例代码
from PIL import Image
import numpy as np

# 1. 读取并转换图片
image_path = ‘../images/commodity/example.jpg‘ # 请替换为你的图片路径
img = Image.open(image_path).convert(‘RGB‘)
img_array = np.array(img) # 将图片转换为numpy数组

# 2. 运行模型预测
# 假设 ‘image_input‘ 是模型的输入占位符,‘predictions‘ 和 ‘localizations‘ 是输出张量
feed_dict = {image_input: img_array}
predictions, localizations, bbox_image = sess.run([predictions_tensor, localizations_tensor, bbox_image_tensor], feed_dict=feed_dict)

筛选与处理预测结果

模型直接输出的原始结果通常包含大量预测框,我们需要对其进行筛选,只保留可能性高且位置准确的框。这个过程主要涉及分数阈值筛选和非极大值抑制。

以下是使用工具函数进行结果后处理的步骤:

  1. 获取锚框(Anchors):从网络定义中获取预定义的锚框(Default Boxes),用于后续的解码和筛选。
  2. 调用后处理方法:使用提供的工具函数(如 np_methods 中的方法)对原始预测结果进行解码、阈值筛选、排序和非极大值抑制(NMS)操作。这些步骤是目标检测中的标准后处理流程。
  3. 关键参数
    • select_threshold:分数阈值,低于此值的预测框将被直接过滤掉。
    • nms_threshold:NMS操作的阈值,用于合并重叠度过高的框。调整这些参数可以控制筛选的严格程度。
# 示例代码(概念性展示)
# 假设 ssd_anchors 是获取的锚框,np_methods 是后处理模块
from utils import np_methods

# 1. 获取锚框
ssd_anchors = ssd_net.anchors(net_shape) # net_shape 是网络输入形状

# 2. 进行后处理筛选
# 该函数内部完成了解码、阈值筛选、排序和NMS
rscores, rbboxes = np_methods.ssd_bboxes_select(
    predictions, localizations, ssd_anchors,
    select_threshold=0.5, nms_threshold=0.45,
    num_classes=9, decode=True)


可视化标记最终结果

最后,我们将筛选出的边界框和类别标签在原图上进行可视化标记,直观地查看模型的检测效果。

以下是使用可视化工具绘制结果框的步骤:

  1. 准备原图数据:确保用于绘制的图片是原始的numpy数组格式。
  2. 调用绘图函数:使用可视化工具(如 utils.visualization 模块中的函数),传入原图、预测的类别、分数和边界框坐标,函数会自动在图片上绘制标记。
# 示例代码
from utils import visualization

# 1. 确保图片是numpy数组
image_to_draw = np.array(img) # 使用原图

# 2. 可视化结果
visualization.plt_bboxes(image_to_draw, predicted_classes, rscores, rbboxes)

运行上述代码后,程序会显示一张带有检测框和类别标签的图片。如果模型训练充分,你将能看到准确的物体检测结果。


课程总结

本节课中我们一起学习了完整的模型测试流程:

  1. 创建会话与加载模型:我们使用 tf.InteractiveSessiontf.train.Saver 来恢复已保存的模型参数。
  2. 准备与输入数据:我们学习了如何读取图片,将其转换为合适的格式,并通过 sess.run() 输入模型。
  3. 结果后处理:我们了解了目标检测后处理的核心步骤,包括使用分数阈值筛选非极大值抑制(NMS) 来过滤冗余的预测框。这些步骤通常由封装好的工具函数(如 np_methods)完成。
  4. 结果可视化:最后,我们使用绘图工具将筛选后的边界框和类别标签绘制在原图上,直观评估模型性能。

记住,测试结果的准确性高度依赖于所加载模型的训练质量。一个训练充分的模型是获得良好检测效果的前提。通过本教程,你已经掌握了从加载模型到可视化结果的完整测试链路。

课程 P75:75.01 微信小程序与模型部署流程关系介绍 🚀

在本节课中,我们将要学习如何将训练好的AI模型部署到线上,并与微信小程序进行对接。我们将重点了解整个线上部署的流程和所使用的技术架构。

概述

首先,我们来介绍整个逻辑流程。我们的学习目标是了解线上部署的流程以及部署架构中使用的技术。

部署架构图解析

为了清晰地描述整个架构流程,我们参考下面这张图。

接下来,我们将对这张图进行详细解析。

模型文件与部署

我们之前通过训练代码得到的是模型文件,通常保存为 CKPT 格式。然而,这个文件并不能直接用于线上部署,我们需要单独进行部署工作。

TensorFlow Serving 服务器

在架构图中,中间部分是 TensorFlow Serving Server。它的作用是将我们导出的模型部署在线上服务器上。

部署完成后,这个服务器将提供模型服务。这里的关键目的是实现模型生产者与模型使用者的解耦合。

模型生产者只需导出模型,并将其上传到服务器。然后,使用 TensorFlow Serving 开启服务。该服务会通过两种接口提供访问:gRPCREST

客户端与 Web 服务器

作为模型使用者(例如本地电脑或手机),我们需要一个客户端程序来请求这个服务。这个客户端被称为 TensorFlow Serving Client

客户端通过 gRPC 协议与服务器通信,获取模型的输入和输出结果。通常,我们会将这个客户端程序托管在一个 Web 服务器 上。

Web 服务器负责对外提供网络接口。例如,微信小程序可以通过网络首先访问 Web 服务器,Web 服务器再将请求转交给客户端程序,客户端程序最终通过 TensorFlow Serving 获取结果并返回。

在整个通信过程中,如果使用 gRPC,通常会遵循 Protocol Buffers 协议。

核心流程总结

以上就是小程序与模型部署之间的整体关系。我们可以将整个过程梳理为以下四个步骤:

以下是实现对接的四个核心步骤:

  1. 导出模型:模型生产者将训练好的模型导出为可部署的格式。
  2. 开启服务:使用 TensorFlow Serving 将导出的模型部署在服务器上并开启服务。
  3. 客户端访问:编写客户端程序,通过 gRPC 协议访问 TensorFlow Serving 服务。
  4. Web 托管:将客户端程序托管在 Web 服务器上,对外提供 API 接口供小程序调用。

总结

本节课中,我们一起学习了微信小程序与 AI 模型对接的完整部署流程。我们了解了从模型导出、使用 TensorFlow Serving 部署服务,到通过客户端和 Web 服务器搭建桥梁,最终让小程序能够调用模型的核心步骤。理解这个架构是进行实际项目开发的基础。

课程 P76:TensorFlow Serving 本地演示与逻辑介绍 🚀

在本节课中,我们将学习如何使用 TensorFlow Serving 在生产环境中部署机器学习模型。我们将通过一个完整的本地演示,了解从模型导出、服务启动到客户端调用的全流程。

概述

TensorFlow Serving 是一个专为生产环境设计的高性能服务系统。它的核心目标是实现模型生产者与使用者之间的解耦,使得模型部署和调用过程标准化、高效化。

本地演示流程

上一节我们概述了 TensorFlow Serving 的作用,本节中我们来看看一个完整的本地部署与调用演示。

以下是演示的核心步骤:

  1. 导出模型:首先,我们需要一个训练好的模型,并将其导出为 TensorFlow Serving 能够识别的格式。
  2. 启动服务:使用 Docker 在本地启动 TensorFlow Serving 服务,并加载我们导出的模型。
  3. 客户端调用:编写客户端代码,向运行中的服务发送请求,并接收模型的预测结果。

第一步:导出模型

我们使用 tf.saved_model.builder.SavedModelBuilder 模块来导出模型。导出的模型结构应尽可能简洁,其职责是接收符合要求的输入数据,并直接输出预测结果。

核心设计原则:模型服务本身不处理复杂的业务逻辑(如数据预处理)。这些工作应在客户端或调用链的其他环节完成。

第二步:启动服务

我们使用 Docker 命令来启动服务。命令的关键是指定模型在本地文件系统中的绝对路径。

启动命令示例:

docker run -p 8501:8501 \
  --mount type=bind,source=/path/to/your/model,target=/models/commodity \
  -e MODEL_NAME=commodity -t tensorflow/serving

此命令将本地路径 /path/to/your/model 下的模型挂载到容器内的 /models/commodity,并以 commodity 为模型名启动服务。

第三步:客户端调用

服务启动后,我们可以通过客户端代码向其发起预测请求。客户端代码会读取一张图片,将其转换为模型所需的输入格式,发送给服务端,并解析返回的预测结果。

TensorFlow Serving 架构介绍

了解了完整流程后,我们深入看一下 TensorFlow Serving 的架构逻辑。

它的工作流程清晰地体现在其架构图中:

  1. 模型训练与导出:开发者使用数据训练模型,并通过 SavedModel 格式导出。
  2. 服务部署:将导出的模型部署到 TensorFlow Serving 服务器上。
  3. 客户端请求:客户端应用程序向服务器发送预测请求(提交数据)。
  4. 结果返回:服务器运行模型进行计算,并将预测结果返回给客户端。

这个过程实现了模型与应用的分离,便于模型的独立更新和管理。

模型导出代码结构

现在,我们具体看看如何用代码实现模型导出。

以下是导出模型的关键代码结构:

  1. 定义模型输入输出:明确模型的输入张量(如 placeholder)和输出张量。这决定了客户端需要提供什么格式的数据。
    • 输入:模型算法所要求的张量(例如,归一化后的图像张量 [None, height, width, channels])。
    • 输出:模型的预测结果张量。

  1. 构建 SavedModel:使用 SavedModelBuilder 来构建并保存模型。
    • 定义模型导出的目标路径。
    • 创建一个 signature_def(签名定义),它是一份“协议”,明确指定了服务的输入、输出以及调用方法(例如 predict)。
    • 将包含计算图和变量信息的模型,连同定义好的签名,保存到指定目录。

通过这两步,我们就得到了一个可以被 TensorFlow Serving 加载并提供服务的模型文件。

总结

本节课中我们一起学习了 TensorFlow Serving 的核心概念与本地部署流程。我们了解到 TensorFlow Serving 是一个用于生产环境模型服务的系统,并通过演示掌握了导出模型使用Docker启动服务以及编写客户端进行调用的三个关键步骤。记住,良好的模型服务设计应保持模型本身的纯粹性,将业务处理逻辑置于服务之外。

课程 P77:模型导出 - 模型输入输出定义 🚀

在本节课中,我们将学习如何为训练好的SSD模型定义清晰的输入和输出,这是将模型导出以供后续部署或服务的关键第一步。我们将创建一个独立的脚本,明确指定模型接收的数据格式和返回的预测结果。


1. 创建模型导出脚本

上一节我们介绍了模型导出的基本概念,本节中我们来看看如何具体实现。首先,我们需要创建一个新的Python文件来编写导出逻辑。

我们在项目目录的 test 文件夹下新建一个文件,命名为 export_serving_model.py

以下是创建文件后需要导入的基础库和模块:

import os
import sys
import tensorflow as tf

为了能够正确导入项目内的网络结构,我们需要将项目根目录添加到系统路径中。

sys.path.append('..')
from nets import ssd_vgg_300

2. 定义主函数与模型图

接下来,我们定义程序的主入口。我们将使用 tf.app.run() 来运行我们的脚本。

def main(_):
    # 模型定义代码将写在这里
    pass

if __name__ == '__main__':
    tf.app.run()

3. 明确模型输入

模型输入必须是经过预处理后、可以直接输入网络的数据格式。对于SSD VGG 300模型,输入是固定尺寸的RGB图像。

我们使用TensorFlow的占位符来定义输入。注意,这里定义的是模型期望的输入,而不是原始的、未处理的图片。

    # 定义输入占位符,数据类型为float32,形状为 [height, width, channels]
    image_input = tf.placeholder(dtype=tf.float32, shape=[300, 300, 3], name='image_input')

由于网络通常需要批处理数据,我们需要将三维的张量扩展为四维,增加一个批处理维度。

    # 扩展维度,从 [H, W, C] 变为 [1, H, W, C]
    image_4d = tf.expand_dims(image_input, axis=0)

4. 构建网络并获取输出

现在,我们将处理后的图像输入到SSD网络中,并获取网络的预测输出。

以下是构建网络并执行前向传播的步骤:

    # 1. 获取默认参数并替换数据格式
    ssd_params = ssd_vgg_300.SSDNet.default_params._replace(data_format='NHWC')
    
    # 2. 实例化SSD网络模型
    ssd_net = ssd_vgg_300.SSDNet(ssd_params)
    
    # 3. 将图像输入网络,设置 is_training=False 以使用推理模式
    predictions, localisations, _, _ = ssd_net.net(image_4d, is_training=False)

至此,我们已经完成了模型计算图的定义:输入是 image_input,核心输出是 predictions(类别预测)和 localisations(位置预测)。

5. 加载训练好的模型参数

定义好计算图后,我们需要将训练好的权重参数加载到图中,这样模型才具有预测能力。

以下是加载模型权重的标准流程:

    # 开启一个TensorFlow会话
    with tf.Session() as sess:
        # 初始化所有变量
        sess.run(tf.global_variables_initializer())
        
        # 创建Saver对象用于加载模型
        saver = tf.train.Saver()
        
        # 指定训练好的模型检查点文件路径
        ckpt_file_path = '../checkpoints/fine_tuning/model.ckpt-0'
        
        # 从检查点文件恢复模型权重
        saver.restore(sess, ckpt_file_path)
        
        print("模型权重加载完成。")
        # 后续可以在这里进行模型保存(如SavedModel格式)或测试


本节课中我们一起学习了如何为SSD目标检测模型定义清晰的输入和输出。我们创建了一个独立的脚本,使用占位符明确了模型期望的输入张量格式,并通过网络前向传播得到了预测输出。最后,我们初始化会话并加载了训练好的模型参数,为下一步将模型导出为可服务的格式做好了准备。整个过程的核心是确保模型接口的简洁和明确,便于后续的部署与集成。

课程 P78:模型导出:SavedModel 导出模型 🚀

在本节课中,我们将学习如何使用 TensorFlow 的 SavedModelBuilder 将训练好的模型导出为 SavedModel 格式。这种格式是用于部署和提供服务的标准格式。

概述

SavedModel 是 TensorFlow 用于保存和加载模型的通用格式。它包含了一个完整的 TensorFlow 程序,包括权重和计算图。导出过程的核心是使用 tf.saved_model.builder.SavedModelBuilder 来构建模型,并定义其输入输出的签名(Signature),以便于后续的部署和调用。

导出步骤详解

上一节我们介绍了模型训练与保存,本节中我们来看看如何将模型导出为 SavedModel 格式。

第一步:定义导出路径

首先,需要指定模型导出的目标路径。该路径通常由基础路径、模型名称和版本号组成。

export_path = “./test/model/commodity/1”
  • 基础路径:模型文件存储的根目录。
  • 模型名称:用于标识模型的字符串,例如 commodity
  • 版本号:一个数字,用于管理模型的不同版本。TensorFlow Serving 会自动选择版本号最大的模型进行服务。

定义好路径后,可以打印提示信息:

print(“正在导出模型到路径:%s” % export_path)

第二步:创建 SavedModelBuilder

接下来,我们创建一个 SavedModelBuilder 对象。这个构建器(Builder)负责定义模型的导出格式和内容。

builder = tf.saved_model.builder.SavedModelBuilder(export_path)

第三步:构建并添加模型签名(Signature)

这是导出过程的核心。我们需要定义一个签名,明确地告诉调用者模型的输入和输出是什么。签名是一个协议,它将具体的计算图输入输出张量(Tensor)映射为有意义的名称。

以下是构建签名的关键步骤:

  1. 获取模型输入输出张量:首先,你需要从你的计算图中获取代表模型输入和输出的张量。假设你的模型输入是一个名为 input_tensor 的张量,输出是多个张量,例如 predictionslocations

    # 假设这是你的模型输入占位符或张量
    input_tensor = ...
    # 假设这些是你的模型输出张量
    output_predictions = ...
    output_locations = ...
    
  2. 构建签名定义映射:使用 tf.saved_model.signature_def_utils.build_signature_def 函数来创建签名。该函数接收一个字典来定义输入和输出。

    # 定义输入部分:给输入张量起一个对客户端友好的别名,例如 ‘images’
    inputs = {‘images’: tf.saved_model.utils.build_tensor_info(input_tensor)}
    
    # 定义输出部分:同样为每个输出张量起别名。注意,填入的参数必须是单个张量,不能是列表。
    # 如果模型有多个输出,需要分别指定。
    outputs = {
        ‘prediction’: tf.saved_model.utils.build_tensor_info(output_predictions),
        ‘location’: tf.saved_model.utils.build_tensor_info(output_locations)
    }
    
    # 构建签名定义
    prediction_signature = tf.saved_model.signature_def_utils.build_signature_def(
        inputs=inputs,
        outputs=outputs,
        method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME
    )
    
    • build_tensor_info: 将 TensorFlow 张量转换为 TensorInfo 协议缓冲区,这是签名的一部分。
    • method_name: 指定方法名,对于预测任务,通常使用 PREDICT_METHOD_NAME

  1. 将签名添加到构建器:将创建好的签名以字典形式添加到构建器中。字典的键是签名的名称(例如 ‘detected_model’),值是上一步创建的签名定义。

    signature_def_map = {
        tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: prediction_signature
    }
    

    这里使用了默认的服务签名键 DEFAULT_SERVING_SIGNATURE_DEF_KEY,客户端调用时会默认使用这个签名。

第四步:保存模型

最后,调用构建器的 save 方法,并传入当前的 TensorFlow 会话(Session)以及定义好的签名映射,将模型保存到指定路径。

builder.save(sess=session, signature_def_map=signature_def_map)
print(“Serving 模型结构导出结束。”)

保存成功后,在导出路径下会生成 saved_model.pb 文件和 variables 文件夹。

  • saved_model.pb:包含模型的图结构、元数据和签名定义。
  • variables/:包含模型权重(变量)的序列化文件。

完整流程总结

本节课中我们一起学习了将 TensorFlow 模型导出为 SavedModel 格式的完整流程。我们来总结一下关键步骤:

  1. 模型定义与加载:确保你拥有定义好的计算图,并且已经将训练好的权重(CKPT 文件)加载到会话中。
  2. 准备导出路径:构造一个包含模型名称和版本号的导出路径。
  3. 创建构建器:实例化 tf.saved_model.builder.SavedModelBuilder
  4. 定义签名:这是最关键的一步。你需要:
    • 明确模型的输入张量和输出张量。
    • 使用 build_tensor_info 包装它们。
    • 使用 build_signature_def 创建输入输出映射,并指定方法名。
    • 将签名组织成字典,通常包含默认服务签名。
  5. 执行保存:调用构建器的 save 方法,传入当前会话和签名字典,完成模型导出。

通过以上步骤,你就得到了一个标准的 SavedModel,可以轻松地部署到 TensorFlow Serving 或其他支持该格式的服务环境中,供客户端远程调用。

课程P79:开启TensorFlow Serving模型服务 🚀

在本节课中,我们将学习如何将训练好的TensorFlow模型部署为一个在线服务。具体来说,我们将使用TensorFlow Serving工具,通过Docker容器来启动一个模型服务,并了解其提供的两种API接口。


模型服务概述

上一节我们完成了模型的导出,接下来需要将模型部署为可访问的服务。这要求我们安装并运行TensorFlow Serving。虽然模型可以上传到远程服务器,但本节课我们将在本地进行测试和部署。

服务类型与端口

TensorFlow Serving主要提供两种服务接口:

  • gRPC服务:一种高性能的远程过程调用协议。
  • REST API服务:基于HTTP协议的表述性状态转移接口。

启动服务时,可以为这两种接口指定不同的端口。通常的做法是同时开启两种服务,以便根据需求选择调用方式。

以下是启动服务时需要指定的关键端口:

  • gRPC服务端口:8500
  • REST API服务端口:8501

启动服务命令详解

我们将使用Docker来运行TensorFlow Serving,这是最便捷的部署方式。在运行前,请确保已安装Docker并拉取了TensorFlow Serving的镜像。

启动服务的核心命令结构如下:

docker run -p 8500:8500 -p 8501:8501 \
  --mount type=bind,source=/path/to/your/model/,target=/models/your_model_name \
  -e MODEL_NAME=your_model_name \
  -t tensorflow/serving

以下是命令中关键参数的解释:

  • -p 8500:8500 -p 8501:8501:将容器内的8500和8501端口映射到主机,从而开放gRPC和REST服务。
  • --mount type=bind,source=...,target=...:将本地的模型目录挂载到Docker容器内部。
    • source:指向你本地保存已导出模型的完整目录路径
    • target:容器内部的固定挂载路径,格式为 /models/你的模型名。这里的 models 是一个固定关键字,不代表具体文件。
  • -e MODEL_NAME=...:设置环境变量,指定模型的名字。这个名字需要与target路径中的模型名以及导出模型时设置的文件夹名保持一致。
  • -t tensorflow/serving:指定要运行的Docker镜像。

重要提示target参数中的模型名(例如your_model_name)必须与导出模型所在文件夹的名称(例如saved_model目录的父目录名)以及环境变量MODEL_NAME的值三者一致,服务才能正确找到并加载模型。

实际操作演示

假设我们的模型导出在本地路径 /home/user/online_class_view5.0/commodity 下,并且模型文件夹名就是 commodity

  1. 停止可能存在的旧服务(如果之前已运行):

    # 查看正在运行的容器
    docker ps
    # 停止对应的容器
    docker stop <容器ID>
    
  2. 启动TensorFlow Serving服务
    将上述命令中的路径和模型名替换为实际值。

    docker run -p 8500:8500 -p 8501:8501 \
      --mount type=bind,source=/home/user/online_class_view5.0/commodity,target=/models/commodity \
      -e MODEL_NAME=commodity \
      -t tensorflow/serving
    
  3. 验证服务
    命令成功运行后,控制台会输出日志。你可以使用 docker ps 命令查看容器是否正在运行。服务启动后,便可通过 localhost:8501(REST)或 localhost:8500(gRPC)来访问模型并进行预测。


课程总结

本节课中,我们一起学习了如何使用TensorFlow Serving部署模型服务。关键步骤包括:理解gRPC和REST两种服务接口,掌握通过Docker命令启动服务的方法,并详细解析了命令中源路径(source)、目标路径(target)和模型名(MODEL_NAME)等关键参数的配置。成功启动服务后,你的模型就从一个静态文件变成了一个可通过网络调用的在线API。

课程P8:8.01_Overfeat模型 🧠

在本节课中,我们将学习目标检测任务中,当图像包含多个目标时,如何解决定位和分类的难题。我们将重点介绍一种基础且重要的思想——滑动窗口,以及它在Overfeat模型中的应用。

多目标检测的挑战

上一节我们介绍了单目标检测的解决方案,即通过增加一个全连接层来输出目标的位置坐标。然而,当一张图片中存在多个目标时,这种方法便不再适用。

核心问题在于:网络输出的目标数量不确定。如果图片中有N个目标,网络就需要输出N个位置坐标,以及每个目标属于各个类别的概率。这使得我们之前提出的“分类+回归”方案变得不可行。

滑动窗口的解决思路

为了解决多目标检测的问题,我们引入一种新的思路。在详细讲解R-CNN算法之前,我们先来看一个关键概念:滑动窗口。

滑动窗口的核心思想是:既然不确定图片中有多少目标,我们就用不同大小和形状的窗口,像扫描一样遍历整张图片。

以下是滑动窗口的具体操作步骤:

  1. 首先,我们预先定义K种不同大小或长宽比的窗口。
  2. 对于每一种窗口,我们让它从图片的左上角开始,按照设定的步长(例如每次向右移动10个像素)从左到右、从上到下地滑动。
  3. 每滑动一次,就截取出窗口当前覆盖的图片区域,作为一个“子图片”。
  4. 假设每种窗口滑动后得到M个子图片,那么总共就会得到 K × M 个子图片。

这样,我们就把一张包含多个目标的复杂图片,转化成了许多个可能只包含单个目标的子图片。对于每一个子图片,我们就可以应用之前学过的单目标“分类+回归”方法进行处理了。

Overfeat模型详解

基于滑动窗口思想的一个经典模型就是Overfeat模型。它的工作流程清晰地体现了这一思路。

以下是Overfeat模型的关键步骤:

  1. 生成候选区域:使用预先定义的K种滑动窗口,在输入图像上滑动,生成大量(K × M个)候选子图片区域。
  2. 特征提取:将每一个候选子图片输入到一个卷积神经网络(CNN)中,进行特征提取。
  3. 分类与回归:网络最后连接两个分支:
    • 分类分支:输出该子图片属于各个类别的概率。
    • 回归分支:输出该子图片内目标位置的微调坐标([Δx, Δy, Δw, Δh])。

模型的训练

那么,如何训练这样的模型呢?关键在于准备训练数据。

训练数据需要为每张原始图片准备若干个子图片,并为每个子图片标注以下信息:

  • 类别标签:如果子图片中包含目标,则标记为具体的类别(如“人”、“车”);如果不包含目标,则标记为背景类(如0)。
  • 位置标签:对于包含目标的子图片,需要标注其真实的位置坐标 [x, y, w, h]

在训练时,模型将子图片输入网络,分别计算分类损失(如交叉熵损失)和回归损失(如Smooth L1损失),并通过反向传播同时优化这两个任务。

Overfeat模型的优缺点

虽然滑动窗口思想非常直观,但Overfeat模型也存在明显的缺点,这主要是由其“暴力穷举”的特性导致的。

优点

  • 思路简单直接,将复杂的多目标检测问题转化为熟悉的单目标分类回归问题。

缺点

  1. 计算成本高:需要处理海量的子图片(K × M),每个子图片都要独立通过CNN进行前向传播,导致计算非常耗时。
  2. 窗口效率低:预先定义的滑动窗口大小和长宽比是固定的,可能无法完美匹配图像中千变万化的物体形状和尺寸,产生大量无效的候选区域。
  3. 精度受限:滑动步长和窗口尺寸的选择会影响检测精度,步长大可能漏检,步长小则计算量剧增。

总结

本节课中我们一起学习了Overfeat模型及其核心的滑动窗口思想。我们了解到,为了解决图像中多目标检测的难题,可以通过定义多种窗口来遍历图像,生成大量候选子区域,再对每个区域进行分类和位置回归。尽管这种方法计算量大且效率不高,但它为后续更高效的目标检测算法(如R-CNN系列)奠定了重要的基础。理解滑动窗口,是理解现代目标检测算法演进的关键第一步。

TensorFlow Serving 客户端逻辑教程 🚀

概述

在本节课中,我们将学习如何构建一个TensorFlow Serving的客户端。这个客户端将被一个Web服务器包裹,负责接收用户请求,与TensorFlow Serving服务器通信,并将结果返回给用户。我们将重点介绍客户端的逻辑设计、环境配置以及核心代码结构。


客户端架构设计 🏗️

上一节我们介绍了TensorFlow Serving的基本概念,本节中我们来看看客户端的具体架构。

客户端会被我们的Web服务器包裹或托管。外部用户的请求首先到达Web服务器,然后被传递到客户端。客户端随后请求TensorFlow Serving服务器,服务器处理请求后将结果返回。

以下是完整的请求路径:

  1. 用户请求发送到Web服务器。
  2. Web服务器将请求转发给TensorFlow Serving客户端。
  3. 客户端请求TensorFlow Serving服务器。
  4. 服务器将结果返回给客户端。
  5. 客户端将结果返回给Web服务器。
  6. Web服务器最终将结果返回给用户。


接口选择 ⚙️

TensorFlow Serving客户端通常提供两种接口形式。我们会选择gRPC配合Protocol Buffers的接口,主要考虑是其性能非常出色。

由于这种接口面向用户提供,在具体场景中需要权衡。如果不是特别追求卓越的性能,也可以使用REST接口的方式去请求。


环境配置与Docker 🐳

我们将Web服务器程序和TensorFlow Serving客户端程序所需的环境整合在一起。

这个整合后的环境已经放在本地的Dockerfile中。你可以通过Docker直接构建指定的环境,或者将其拉取为容器。

以下是环境配置的核心步骤:

  1. 构建Docker镜像:在包含Dockerfilerequirements.txt文件的目录下执行构建命令。
    docker build -t tf-serving-web .
    
  2. 查看镜像:构建完成后,使用以下命令查看已创建的镜像。
    docker images
    
    你可以看到名为tf-serving-web的容器镜像,以及之前可能已经拉取好的tensorflow/serving镜像。

环境配置至关重要。如果环境有问题,后续的请求发送和处理都会出现错误。


项目结构 📁

我们的Web项目主要包裹着TensorFlow Serving客户端。在项目结构中,有一个名为web_code的目录。

以下是关键文件说明:

  • statictemplates目录:存放Web相关的静态文件和模板,与核心客户端逻辑无关。
  • main.py:Web服务器的主程序文件,负责路由和请求调度。
  • TensorFlow Serving客户端核心文件:我们主要使用以下三个文件来构建客户端逻辑:
    1. prediction.py:向服务器发送请求并处理响应格式的核心文件。
    2. preprocess.py:封装了数据预处理逻辑的文件。
    3. postprocess.py:封装了结果后处理(如标签标记)逻辑的文件。

prediction.py会调用preprocess.pypostprocess.py来完成整个流程。


核心业务流程 🔄

现在,让我们梳理一下用户、Web后台和Serving客户端之间的完整业务流程。

  1. 用户发起请求:用户提供图片,发送到Web后台。
  2. Web后台中转:Web后台不进行具体的TensorFlow相关处理,仅作为中转站。它将接收到的图片数据流直接传递给TensorFlow Serving客户端。
  3. 客户端处理与请求:Serving客户端负责所有核心业务逻辑,包括:
    • 数据预处理
    • 请求格式封装
    • 向TensorFlow Serving服务器发送请求
    • 接收服务器响应
    • 对结果进行解析和后处理(如标记标签)
  4. 结果返回:客户端将处理好的结果返回给Web后台。
  5. 响应最终用户:Web后台将结果返回给用户。

整个设计确保了业务逻辑的清晰分离:Web后台只处理HTTP请求和响应,所有与TensorFlow Serving相关的操作都封装在独立的客户端模块中。


Web服务代码简析 💻

Web服务的相关代码(主要是main.py)基于Flask框架编写。它的职责非常明确:

  • 提供默认的页面接口。
  • 提供一个用于预测的路由函数(例如/predict)。
  • 在该路由函数中,它获取用户上传的图片数据,然后调用TensorFlow Serving客户端(即prediction.py中的函数)进行预测。
  • 最后,它将客户端返回的结果进行适当编码(如JSON格式),并返回给用户。

请注意:这部分Flask框架的Web代码我们不需要深入掌握或修改,只需理解其作为“桥梁”的作用即可。我们的开发重点在于prediction.pypreprocess.pypostprocess.py这三个构成Serving客户端的文件。


总结 📝

本节课中我们一起学习了TensorFlow Serving客户端的完整逻辑。

我们首先了解了客户端在Web服务器包裹下的架构设计。接着,探讨了gRPC和REST两种接口的选择考量。然后,我们通过Docker配置了整合Web和客户端依赖的统一环境。之后,解析了项目文件结构,明确了核心业务文件。最后,我们详细梳理了从用户请求到最终响应的数据流,并强调了Web后台与TensorFlow Serving客户端之间职责分离的设计原则。

通过本课,你应该对如何构建一个与TensorFlow Serving交互的客户端应用有了清晰的认识。

TensorFlow Serving 客户端开发教程 🚀

课程概述

在本节课中,我们将学习如何构建一个TensorFlow Serving的客户端。这个客户端的主要功能是接收用户输入的图片数据,将其转换为模型所需的格式,发送预测请求,并对返回的结果进行处理和标记。


客户端流程分析

上一节我们介绍了课程的整体目标,本节中我们来看看客户端的具体工作流程。

以下是客户端处理数据的核心步骤:

  1. 获取Web服务的用户数据流:客户端首先从Web服务接收用户上传的图片数据流。
  2. 转换数据格式:接收到的图片通常是二进制格式,需要将其转换为程序能够处理的数组格式。
  3. 图片预处理:将转换后的图片数据调整成服务端模型所要求的输入格式(例如,调整尺寸、归一化等)。
  4. 发送请求:将处理好的数据封装成请求,发送给TensorFlow Serving服务。
  5. 获取结果:接收服务端返回的预测结果。
  6. 结果处理与标记:对预测结果进行解析,并将其转换为人类可读的标签或信息。

这个流程的核心是与Web服务进行数据交互,客户端本身不进行复杂的业务逻辑处理。


代码结构与实现

理解了流程之后,我们开始进行相关代码的编写。首先,我们来看一下完整的代码结构。

我们将在一个名为 web_code 的目录中组织代码。其中,prediction.py 是核心文件,它包含了图片转换、预处理、请求服务和结果处理的全部逻辑。这个模块最终会提供一个函数供Web客户端调用。

其他文件(如 main.py)是辅助性的Web框架组件(例如Flask)。

现在,我们开始实现 prediction.py

第一步:导入依赖包

首先,需要导入所有必要的Python包。这包括处理Web请求、TensorFlow Serving交互以及图片处理的库。

import numpy as np
from PIL import Image
import io
import tensorflow as tf
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2_grpc
import grpc

第二步:定义核心预测函数

我们将定义一个名为 make_prediction 的函数。这个函数是客户端的主要接口。

def make_prediction(image_data):
    """
    对输入的图片数据进行预测。
    参数:
        image_data: 二进制格式的图片数据流。
    返回:
        处理后的预测结果标签。
    """
    # 函数主体将在后续步骤中实现
    pass

该函数接收一个二进制图片数据流作为输入,并返回预测结果的标签。

第三步:图片格式转换与预处理

make_prediction 函数中,第一步是对输入的 image_data 进行格式转换和预处理。

首先,调用 convert_image_to_array 函数将二进制数据转换为NumPy数组。

def convert_image_to_array(image_data):
    """
    将二进制图片数据转换为NumPy数组。
    """
    image = Image.open(io.BytesIO(image_data)).convert('RGB')
    image_array = np.array(image)
    return image_array

make_prediction 函数中应用:

    # 1. 对image进行格式转换以及预处理操作
    image_array = convert_image_to_array(image_data)

接着,需要将数组格式的图片处理成模型要求的输入格式(例如,调整大小为 300x300 像素并进行归一化)。这个逻辑封装在 image_preprocessing 函数中。

def image_preprocessing(image_array):
    """
    对图片数组进行预处理(调整大小、归一化等)。
    此函数复用了模型训练时的预处理逻辑。
    """
    # 这里是一个示例预处理流程
    # 1. 调整图片尺寸
    # 2. 归一化像素值
    # 具体代码取决于你的模型要求
    processed_image = ... # 预处理操作
    return processed_image

make_prediction 函数中继续:

    processed_image = image_preprocessing(image_array)

至此,我们完成了数据准备阶段的工作。下一节我们将实现如何向TensorFlow Serving服务发送请求并获取结果。


课程总结

本节课中我们一起学习了TensorFlow Serving客户端的基本开发流程。我们首先分析了客户端从接收数据到返回结果的关键步骤。然后,我们开始动手实现,完成了项目结构的搭建、依赖包的导入以及核心预测函数 make_prediction 的框架。最后,我们重点实现了客户端的第一步操作:将用户上传的二进制图片数据转换为NumPy数组,并进行模型所需的预处理。

在接下来的课程中,我们将继续完善这个函数,实现与TensorFlow Serving服务的gRPC通信,并完成对预测结果的后处理与标记。

课程P82:gRPC与TensorFlow Serving API介绍 🚀

在本节课中,我们将学习如何编写客户端代码,通过gRPC协议向部署好的TensorFlow Serving模型服务器发起请求并获取预测结果。我们将了解整个请求流程以及所需的核心API。


建立连接与核心API

上一节我们介绍了模型服务的基本概念,本节中我们来看看如何与服务器建立通信。要获取模型服务器的数据,需要使用两个关键库:grpctensorflow_serving.apis

建立连接并发送请求遵循一个清晰的步骤流程。

以下是完整的请求步骤:

  1. 使用gRPC建立到服务器的网络连接通道。
  2. 创建一个符合服务要求的请求协议,并封装好数据。
  3. 通过已建立的通道发送请求,并获取服务器返回的结果。
  4. 解析返回的结果,提取出我们需要的预测数据。

接下来,我们详细介绍其中用到的几个核心API。


gRPC连接通道

首先,我们需要建立与服务器的连接。由于使用gRPC进行通信,因此必须使用 grpc.insecure_channel 函数。该函数需要指定服务器的IP地址和端口号。

channel = grpc.insecure_channel('localhost:8500')

本地测试通常使用 localhost:8500,若为远程服务器,则需替换为对应的IP和端口。此函数返回一个 channel 对象,它将用于后续的通信。

TensorFlow Serving API

建立gRPC通道后,我们需要使用TensorFlow Serving提供的API来封装数据和发送请求。主要涉及两个模块:

  1. prediction_service_pb2_grpc:此模块用于在已建立的gRPC通道上创建具体的服务访问通道(Stub)。它提供了一个可以向模型发送请求的管道。
    stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
    
    通过这个 stub,我们可以发送请求并获取结果。结果中包含了模型导出时定义的签名映射(例如 input_tensoroutput_tensor),因此获取输出时需要对应 output_tensor 的名称。

  1. predict_pb2:此模块主要用于构造请求对象。它会创建一个 PredictRequest 实例。
    request = predict_pb2.PredictRequest()
    
    构造请求时,需要设置几个关键信息:
    • 请求的模型名称。
    • 模型导出时设置的签名名称(例如 serving_default 或自定义的 detection_model)。
    • 用户输入的数据,需要封装到与签名中 input_tensor 名称对应的字段中(例如 images)。

请求构造完成后,通过 stub 通道发送出去即可获得结果。


流程总结与代码实践

由于涉及的API名称较多,我们先对整个客户端流程进行总结。

客户端获取结果的完整过程可分为以下几个步骤:

  1. 数据准备:获取用户输入数据,并进行必要的格式转换和预处理。
  2. 建立连接:使用gRPC创建到模型服务器的连接通道。
  3. 创建服务通道:使用TensorFlow Serving API在gRPC连接上创建可发送请求的服务存根(Stub)。
  4. 构造请求:封装一个请求,其中必须包含:模型名称、模型签名名称以及预处理后的输入数据。
  5. 发送请求并获取响应:通过服务存根发送请求,并接收服务器返回的响应。
  6. 解析结果:从响应中解析出最终的预测输出。

我们将依据以上步骤来编写客户端代码。


本节课中我们一起学习了如何利用gRPC和TensorFlow Serving API构建模型预测的客户端。核心在于理解“建立连接-创建存根-构造请求-发送解析”这一流程,并掌握 grpc.insecure_channelprediction_service_pb2_grpc.PredictionServiceStubpredict_pb2.PredictRequest 这几个关键API的用法。

课程 P83:客户端建立连接与获取结果 🚀

在本节课中,我们将学习如何编写一个 gRPC 客户端,用于向 TensorFlow Serving 服务器发送请求并获取模型预测结果。我们将按照清晰的步骤,从建立连接到处理响应,逐一讲解。


建立 gRPC 连接

上一节我们介绍了服务端部署,本节中我们来看看如何从客户端发起请求。首先,我们需要与服务器建立 gRPC 连接。

我们使用 grpc.insecure_channel 来创建一个通道。需要传入服务器的 IP 地址和端口号。

import grpc

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/2d3083dfb65aab22875a2ba547f5a986_4.png)

# 本机测试使用的 IP 和端口
server_ip = '192.168.1.100'  # 请替换为你的服务器 IP
server_port = '8500'  # gRPC 端口
server_address = f'{server_ip}:{server_port}'

# 建立 insecure channel
with grpc.insecure_channel(server_address) as channel:
    # 后续操作在此代码块内进行

创建预测服务存根

连接建立后,我们需要创建一个能够与服务器上特定预测服务通信的“存根”。

以下是创建预测服务存根的步骤:

from tensorflow_serving.apis import prediction_service_pb2_grpc

# 在已有的 channel 上下文中,创建 PredictionServiceStub
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

这个 stub 对象将用于后续发送预测请求。


封装预测请求

现在,我们需要构造一个具体的预测请求。这包括指定模型名称、签名以及输入数据。

以下是封装请求的具体步骤:

  1. 创建请求对象:使用 predict_pb2.PredictRequest()
  2. 设置模型名称:指定要调用的已部署模型的名称。
  3. 设置签名名称:指定模型内用于处理请求的签名。
  4. 填充输入数据:将预处理好的数据放入请求的指定输入中。
from tensorflow_serving.apis import predict_pb2
from tensorflow import make_tensor_proto

# 1. 创建请求对象
request = predict_pb2.PredictRequest()

# 2. 设置模型名称 (需与导出模型时设置的名称一致)
request.model_spec.name = 'commodity'

# 3. 设置签名名称 (需与模型签名定义一致)
request.model_spec.signature_name = 'serving_default'

# 4. 填充输入数据
# 假设 `image_data` 是已经预处理好的图像数据(例如 shape 为 [1, 300, 300, 3] 的数组)
# 使用 make_tensor_proto 将数据转换为 TensorProto 格式,并放入请求的指定输入中
request.inputs['input_tensor'].CopyFrom(make_tensor_proto(image_data))

请注意,'input_tensor' 这个键名必须与模型签名中定义的输入名称完全一致。


发送请求并获取结果

请求封装完成后,即可通过存根发送给服务器并等待响应。

我们使用存根的 Predict 方法来发送请求。

# 发送请求并获取结果
result = stub.Predict(request, timeout=10.0)  # 设置超时时间

返回的 result 对象包含了模型的预测输出。


处理与解析结果

最后一步是从响应结果中提取我们需要的数据。

结果中的输出也通过键名来访问,该键名需与模型签名中定义的输出名称一致。

# 从结果中提取输出数据
# 假设模型输出名为 'output_tensor'
output_data = result.outputs['output_tensor']

# output_data 是一个 TensorProto 对象,可以将其转换为 numpy 数组进行处理
import tensorflow as tf
prediction_array = tf.make_ndarray(output_data)

# 现在可以使用 prediction_array 进行后续分析或展示
print(prediction_array)

总结

本节课中我们一起学习了构建一个完整 TensorFlow Serving gRPC 客户端的流程。

我们首先建立 gRPC 连接,然后创建预测服务存根,接着封装包含模型名、签名和数据的预测请求,之后发送请求并获取响应,最后解析响应结果得到模型的预测数据。掌握这些步骤,你就能成功地从客户端调用远程的机器学习模型服务了。

🧠 P84:客户端结果解析教程

在本节课中,我们将学习如何解析从TensorFlow Serving模型服务端返回的预测结果。我们将重点讲解如何从返回的复杂数据结构中提取出我们需要的预测值和定位信息,并将其转换为可操作的Tensor格式。

上一节我们介绍了如何向模型服务端发送请求并获取原始结果。本节中,我们来看看如何对这些结果进行解析和处理。

结果解析原理

解析结果的关键在于理解模型导出时的结构。模型返回的result对象包含两个主要部分:predictlocalization。我们需要按照正确的名称将它们提取出来。

解析步骤详解

以下是解析结果的具体步骤:

  1. 提取原始输出
    通过result.outputs方法,根据指定的输出名称获取数据。注意,此时获取的是TensorInfo格式,而非直接的Tensor

    prediction_outputs = result.outputs['predict']
    localization_outputs = result.outputs['localization']
    
  2. 转换为Tensor格式
    使用tf.convert_to_tensor方法将TensorInfo转换为真正的Tensor,以便进行后续的数值操作。

    import tensorflow as tf
    predict_tensor = tf.convert_to_tensor(prediction_outputs)
    local_tensor = tf.convert_to_tensor(localization_outputs)
    
  3. 处理多个输出
    在我们的例子中,predictlocalization各自包含6个部分。我们需要为每个部分单独进行转换。

    # 解析 prediction 的六个部分
    predict_list = []
    for i in range(6):
        tensor_info = result.outputs[f'predict{i}']
        tensor = tf.convert_to_tensor(tensor_info)
        predict_list.append(tensor)
    
    # 解析 localization 的六个部分
    local_list = []
    for i in range(6):
        tensor_info = result.outputs[f'local{i}']
        tensor = tf.convert_to_tensor(tensor_info)
        local_list.append(tensor)
    

完整测试示例

为了验证解析过程,我们需要输入一张图片进行测试。

以下是加载图片并调用模型服务的完整代码示例:

import tensorflow as tf

# 1. 定义模型服务地址
server_url = 'http://localhost:8501/v1/models/your_model:predict'

# 2. 加载测试图片
image_path = '../web_code/images/commodity/example.jpg'  # 请替换为你的图片路径
with open(image_path, 'rb') as f:
    image_data = f.read()

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/bad8cb6e16cdbfe46a5973582bf98030_3.png)

# 3. 构建请求数据
request_data = {
    'instances': [{'input_image': image_data}]
}

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/goods-detect-tangyudi/img/bad8cb6e16cdbfe46a5973582bf98030_5.png)

# 4. 发送请求(假设使用requests库)
import requests
response = requests.post(server_url, json=request_data)
result = response.json()

# 5. 解析结果
predictions = []
localizations = []

for i in range(6):
    # 解析预测值
    pred_tensor_info = result['outputs'][f'predict{i}']
    pred_tensor = tf.convert_to_tensor(pred_tensor_info)
    predictions.append(pred_tensor)

    # 解析定位信息
    loc_tensor_info = result['outputs'][f'local{i}']
    loc_tensor = tf.convert_to_tensor(loc_tensor_info)
    localizations.append(loc_tensor)

# 6. 打印解析后的结果结构
print(f"Predictions 列表长度: {len(predictions)}")
print(f"Localizations 列表长度: {len(localizations)}")

运行上述代码后,控制台将成功打印出请求的响应数据。你会看到predictionslocalizations各自都是一个包含6个Tensor的列表,这标志着我们成功完成了结果的解析。

本节课中我们一起学习了如何从TensorFlow Serving的响应中解析出结构化的预测结果。核心在于通过正确的输出名称提取数据,并使用tf.convert_to_tensor进行格式转换。掌握这一步骤是后续进行结果可视化和业务应用的基础。

课程 P85:85.06_Client - 结果标记与返回 🖼️➡️🌐

在本节课中,我们将学习如何将模型预测的Tensor结果转换为可视化的标记图片,并通过Web服务器返回给用户。核心在于理解TensorFlow会话的运行机制以及如何将处理后的图片数据封装为可传输的格式。


上一节我们介绍了如何获取模型的预测结果,它是一个由Tensor组成的列表。本节中我们来看看如何将这个结果标记在原图片上并返回。

获取的结果是一个Tensor列表。如果要将这个结果返回给用户,我们需要将预测的标记(如边界框和类别)绘制在原始图片上。这相当于将预测结果以图示的方式标记在原图片当中。

因此,tag_picture函数的作用就是接收原始图片和预测结果,生成一张带标记的新图片。

tag_picture函数需要输入图片数据和预测结果。但预测结果目前还是一个Tensor对象,不能直接用于绘图运算。

这里涉及TensorFlow中一个常见的问题:需要从Tensor对象中获取具体的数值结果。为此,我们需要在TensorFlow会话中运行这个Tensor。

以下是具体的操作步骤:

  1. 创建一个TensorFlow会话。
  2. 在会话中运行获取预测结果的函数,从而得到具体的数值(如边界框坐标P和类别标签L)。
  3. 将这些数值结果传递给tag_picture函数进行绘图。
with tf.Session() as sess:
    P, L = sess.run([localization, classification])  # 获取具体的预测值

tag_picture函数内部封装了绘图逻辑。它会利用postprocess_image等方法处理预测数据,计算边界框,并使用PILmatplotlib等库将标记绘制在图片上。

处理完成后,函数会将标记好的图片转换为字节流格式(例如使用BytesIO),方便通过网络传输。

所以,在配置函数中,我们需要:

  • 传入原始图片的数组格式image_array),因为内部需要进行数组运算。
  • 传入从会话中获取的具体预测值PL
  • 接收tag_picture返回的图片字节流结果。
# 假设 original_image_array 是原始图片的NumPy数组格式
image_byte_stream = tag_picture(original_image_array, P, L)

最后,这个图片字节流(image_byte_stream)可以直接传递给Web服务器框架(例如Flask的send_file或返回Response),最终返回给前端用户。


本节课中我们一起学习了如何将模型输出的Tensor预测结果转换为可视化的图片。关键步骤包括:在TensorFlow会话中获取Tensor的具体值、调用绘图函数生成带标记的图片、以及将图片转换为字节流格式以便通过Web接口返回。这样就完成了从模型预测到用户可视化的完整流程。

🚀 课程 P86:服务器部署与小程序测试

在本节课中,我们将学习如何将训练好的模型部署到服务器上,并了解如何通过小程序或Web服务来访问模型接口。我们将重点关注服务器部署所需的文件、服务开启流程以及一个简单的测试示例。


上一节我们介绍了模型训练与导出,本节中我们来看看如何将模型部署到服务器并开启服务。

服务器部署概述

服务器部署通常用于线上环境。在服务器上,我们需要安装并运行两个核心服务:TensorFlow ServingFlask Web 服务。

  • TensorFlow Serving:负责加载和运行导出的模型,提供模型推理的底层接口。
  • Flask Web 服务:作为客户端与 TensorFlow Serving 之间的桥梁,提供一个易于调用的 HTTP API 接口。

服务器部署所需文件

以下是部署到服务器所需的文件和目录结构:

  1. 模型文件:通过 tf.saved_model.save 导出的 SavedModel 格式模型。
  2. Docker 配置文件:用于在服务器上构建包含 TensorFlow Serving 和 Flask 环境的 Docker 镜像。
  3. Web 应用程序代码:包含 Flask 后端服务逻辑的代码文件。

开启 TensorFlow Serving 服务

首先,我们需要在服务器上启动 TensorFlow Serving 服务来加载模型。

  1. 将导出的模型文件夹上传到服务器的指定目录,例如 commodity
  2. 使用 Docker 命令启动 TensorFlow Serving 容器。该命令会指定端口、模型名称和模型路径。
docker run -p 8501:8501 --name=tf_serving_commodity \
  -v /path/to/your/model/commodity:/models/commodity \
  -e MODEL_NAME=commodity tensorflow/serving

  1. 服务启动后,可以使用 docker ps 命令查看运行中的容器,确认 TensorFlow Serving 服务已成功启动。

开启 Flask Web 服务

TensorFlow Serving 提供了 gRPC/HTTP 接口,但通常我们会通过一个更易用的 Web 服务来封装它。

  1. 确保服务器上有构建好的 Docker 镜像,该镜像包含了 Flask 环境和与 TensorFlow Serving 交互的客户端代码。
  2. 进入存放 Web 代码的目录(例如 web_code)。
  3. 使用 Docker 命令启动 Flask Web 服务容器,并指定应用程序的入口点。
docker run -p 5000:5000 --name=tf_serving_web \
  -v /path/to/your/web_code:/app web_code_image \
  python main.py
  1. 再次使用 docker ps 命令,现在应该能看到两个运行中的容器:tf_serving_commoditytf_serving_web

服务测试与小程序接入

Web 服务启动后,会提供一个简单的 HTTP 接口。我们可以通过浏览器访问该服务的 IP 和端口(例如 http://服务器IP:5000),通常会看到一个简单的文件上传界面,用于测试模型。

如果希望通过小程序调用模型,流程如下:

  1. 在微信开发者工具中创建或导入小程序项目。
  2. 在小程序代码中,编写网络请求代码,调用我们部署的 Flask Web 服务提供的 API 接口。
  3. 将图片或其他数据发送到该接口,并接收模型返回的预测结果。

核心调用逻辑(伪代码表示):

// 小程序端发起请求
wx.uploadFile({
  url: 'https://your-server-ip:5000/predict',
  filePath: tempFilePath, // 用户选择的图片
  name: 'image',
  success (res) {
    // 处理服务器返回的JSON格式预测结果
    console.log(res.data)
  }
})

本节课中我们一起学习了服务器部署的核心步骤。我们了解到,部署主要涉及开启两个服务:TensorFlow Serving 用于托管模型,以及 Flask Web 服务 用于提供客户端访问接口。需要上传到服务器的关键文件包括模型文件Web应用程序代码。整个过程通过 Docker 容器化部署,简单且易于管理。最后,任何客户端(如网页或小程序)都可以通过调用 Web 服务提供的 API 来使用我们部署的模型。

课程 P87:项目实现总结 🎯

在本节课中,我们将对整个目标检测项目进行回顾与总结。我们将梳理从数据集准备、模型训练到最终部署的完整流程,并总结其中涉及的核心技术点。


项目技术点总结

上一节我们完成了整个项目的流程,本节中我们来看看项目实现过程中用到了哪些关键技术。

以下是项目实现所涉及的主要技术点:

  • 训练框架:使用 TensorFlow 及其子库 Slim 进行模型训练。
  • 训练流程:使用 tf.estimator 等工具构建训练流程,并指定设备(如多GPU)进行分布式训练。
  • 可视化与测试:使用 Matplotlib 进行结果可视化与绘图。
  • 模型部署:使用 TensorFlow Serving 来部署训练好的模型。
  • 模型文件:部署时需要提供符合 Serving 要求的模型文件。
  • 客户端编写:编写客户端程序来请求 TensorFlow Serving 服务以获取预测结果。
  • 服务集成:将客户端逻辑内嵌到 Web 服务 中进行托管。
  • 通信协议:客户端与 Serving 之间使用 gRPC 协议进行高性能通信,其性能优于 REST API。

训练流程总结

了解了关键技术点后,我们再来详细回顾一下模型训练的完整步骤。

以下是训练流程的主要步骤:

  1. 设备配置与初始化:指定训练设备(如CPU/GPU),并进行全局变量初始化。通常使用 tf.device 来指定,初始操作一般在 CPU 上完成。
  2. 数据获取与预处理:读取数据集,进行数据预处理(如归一化、增强)以及正负样本标记。此步骤也通常在 CPU 上执行。
  3. 模型定义与复制:将模型计算图复制到每个 GPU 设备上,定义网络前向传播、损失函数并计算损失。
  4. 优化器指定:在 CPU 上指定学习率策略和优化器(如 Adam、SGD)。
  5. 梯度计算与汇总:计算每个模型参数的梯度,并汇总所有设备上的总梯度。
  6. 模型训练:使用优化器根据总梯度和损失更新模型参数,通常借助 tf.trainslim.learning 中的训练循环完成。

测试与模型导出流程

训练完成后,我们需要对模型进行测试并将其导出以供部署。本节中我们来看看测试和导出的关键步骤。

以下是测试与模型导出的流程:

  • 测试流程
    1. 准备测试数据并格式化。
    2. 对数据进行与训练时一致的预处理。
    3. 将数据输入模型,获取网络输出(预测结果)。
    4. 对输出进行后处理,包括应用非极大值抑制(NMS)等操作。
    5. 使用 Matplotlib 将带有预测标记的结果图像显示出来。

  • 模型导出流程
    导出用于部署的模型应保持最简单的逻辑,仅包含网络的核心计算。
    1. 明确定义模型的输入和输出张量。
    2. 使用 tf.saved_model.builder.SavedModelBuilder 来构建 SavedModel。
    3. 在构建器中指定模型保存路径,并定义签名(包含输入和输出)。
    4. 执行导出,生成 SavedModel 格式的模型文件。


服务部署与客户端编写

模型导出后,下一步就是启动服务并编写客户端进行调用。本节中我们来看看如何完成部署的最后环节。

以下是服务部署与客户端编写的要点:

  1. 开启服务:使用 TensorFlow Serving 加载导出的模型文件,开启 gRPC 服务。
  2. 客户端编写
    • 建立与 Serving 服务的连接通道。
    • 实现完整的预测逻辑:接收用户输入(如图片),进行预处理和格式转换,发送请求,接收预测结果,并对结果进行后处理和标记。
    • 主要使用 tensorflow_serving.apis 中的 PredictionServiceStubpredict_pb2 等 API 进行通信。
  3. Web服务集成:将编写好的客户端逻辑集成到 Web 后端框架(如 Flask、Django)中,通过 Web 服务对外提供 API 接口。


完整项目流程回顾

最后,让我们串联起整个项目的生命周期,形成一个清晰的脉络。

整个项目的实现遵循以下完整流程:
数据准备 -> 模型与预处理准备 -> 模型训练 -> 模型导出 -> 客户端编写 -> Web服务集成


本节课中我们一起学习了目标检测项目的完整实现总结。我们从技术栈、训练流程、测试导出、服务部署等多个维度进行了梳理,旨在帮助你系统地理解一个机器学习项目从开发到上线的全貌。掌握这个流程,对于你未来实现其他AI项目将大有裨益。

课程P9:9.02_RCNN:步骤流程介绍 🎯

在本节课中,我们将要学习目标检测领域一个里程碑式的算法——RCNN。我们将详细介绍其核心思想、完整的工作流程以及每一步的具体操作,帮助你理解它是如何解决多目标检测问题的。


上一节我们介绍了OverFeat模型及其滑动窗口的检测思路。本节中,我们来看看一个更高效、影响深远的算法——RCNN。

RCNN于2014年在CVPR会议上被提出。它摒弃了OverFeat中暴力滑窗的方法,转而使用“候选区域”策略,并首次将深度神经网络成功应用于目标检测任务。这奠定了后续众多检测模型的基础。

以下是RCNN的完整结构图,我们先对其有一个整体的认识。

如图所示,RCNN的流程主要分为四个阶段:

  1. 输入一张图片,生成约2000个候选区域。
  2. 将每个候选区域调整到固定尺寸,并送入CNN网络提取特征向量。
  3. 将每个特征向量输入一组SVM分类器,判断其属于哪个类别。
  4. 使用非极大值抑制剔除重叠框,并通过回归网络精细调整边界框位置。

接下来,我们将逐一拆解这些步骤。


第一步:生成候选区域 (Region Proposal)

RCNN的第一步是找出图片中所有可能包含目标的候选区域。这与OverFeat生成候选框的思路类似,但方法更高效。常用的算法如Selective Search会生成约2000个候选框,而非暴力遍历所有位置。

核心操作region_proposals = selective_search(image)
此步骤的输出是一个包含约2000个候选框坐标的列表。


第二步:调整尺寸与特征提取

由于CNN网络(如AlexNet)需要固定尺寸的输入,我们必须将形状各异的候选区域统一缩放(例如到227x227像素)。接着,每个调整后的区域被送入一个预训练好的CNN网络(如AlexNet)中,从最后的全连接层提取出固定长度的特征向量。

以下是该步骤的简要描述:

  • 统一尺寸:将所有候选区域缩放至固定大小。
  • 特征提取:将每个区域输入CNN,得到一个高维特征向量。

对于2000个候选区域,我们将得到2000个特征向量。


第三步:SVM分类

上一步我们得到了2000个特征向量,这一步我们将利用它们进行目标分类。

RCNN为每一个待检测的类别(例如VOC数据集的20个类别)都训练了一个独立的SVM二分类器。每个特征向量会依次通过这20个SVM,从而判断其是否属于某个类别以及相应的置信度得分。

结果:得到一个 2000 x 20 维的得分矩阵,矩阵中的每个值代表对应候选框属于某个类别的得分。


第四步:非极大值抑制与边界框回归

经过SVM分类后,我们得到了大量带有类别得分的候选框,其中很多框指向同一个物体且相互重叠。

首先,我们需要使用非极大值抑制来剔除冗余框。NMS会为每个类别保留得分最高且与其他高得分框重叠度(IoU)较低的框。

核心概念非极大值抑制 (NMS),用于去除冗余检测框。

最后,为了更精确地定位物体,RCNN使用一个边界框回归器对保留下来的候选框进行微调,使其更紧密地贴合真实物体的边界。

核心操作

  1. selected_boxes = NMS(all_boxes, scores)
  2. refined_boxes = BBox_Regressor(selected_boxes)

本节课中我们一起学习了RCNN目标检测算法的完整流程。我们从生成候选区域开始,经历了统一尺寸、CNN特征提取、SVM分类,最后通过非极大值抑制和边界框回归得到精确的检测结果。RCNN的核心贡献在于引入了“候选区域+CNN特征提取”的两阶段范式,极大地提升了检测精度,成为深度学习目标检测的开山之作。

posted @ 2026-02-05 08:54  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报