Read on Omnivore
Read Original

Highlights&&Note

因此,整个demo的核心实际上是VisualizationDemo

模型处理输入得到输出predictions=self.predictor(image)predictions就是模型(刚刚的self.predictor)输出的结果。阅读机器学习、深度学习代码最重要的就是追踪这类模型处理数据的代码,因为这类代码是理解整体计算模型的关键。

predictor.py9-12行的import部分,我们可以学习到很多架构深度学习项目的规范、设计方法,在不同的文件夹中,我们往往会通过功能将不同的模块分开包装。例如在predictor.py中体现的:

  • .data:处理数据相关的类和方法
  • .engine:对训练、预测逻辑的整体包装,类似于对整体Pipeline的定义,常见于大型项目
  • .utils:应该是utilities的简写,一般用来放置常用的工具模块,例如在这里体现出来的可视化部分

总之,对于越大型的项目来说,合理的分区、包装就越有必要,因为这可以从软件工程角度节省大量用来理解、开发、查错(Debug)的成本。在自己的很多小项目中,合理地使用类似的方法也能有效地提升项目质量。

训练代码是tools/train_net.py

if __name__ == '__main__'的部分(这里是代码运行的接口),可以看到detectron2的结构是利用launch运行了main函数中的内容。如果我们不关心分布式训练的部分(即在distributed的作用域的部分),那么main函数的逻辑相当简单:得到模型和运行的参数(在参数cfg中)。利用定义好的类Trainer,通过传入cfg参数可以定义出模型,后面的部分均通过Trainer里面的方法都可以实现,例如train()顾名思义就是做训练的,test()就是测试的,build_model()就是创建模型的。
很有意思的是,过去的detectron2没有封装invoke_main.不知道为什么要封装这么个东西

Trainer的定义中我们发现它是一个继承自engine.DefaultTrainer的子类,而我们通过上面对main函数的分析发现Trainer的主要功能其实都来自于DefaultTrainer
trainer看engine.DefaultTrianer就行

查看文件名字和每个文件上面__all__的部分可以大致猜测出它们之间相互引用的关系和每个文件主要负责的部分

defaults.py包含了我们在train_net.py中见到的DefaultTrainer,也大多是在import别人

launch.py中的launch顾名思义是让算法开始运行的代码,我们浏览一下它最主要的函数launch,根据它的参数num_machinesmachine_rank可得知它是负责分布式训练的代码,又有参数中main_func,可以知道launch不涉及detectron2的实际功能
分布式训练,不用管

我们清晰了engine部分的层次关系。具体而言,我们按照如下的顺序阅读代码

  • train_loop.py
  • hook.py
  • defaults.py

HookBaseHook的基类,其中实现了方法before_stepbefore_trainafter_stepafter_train,其主要的作用是在真正做训练之前,做好每一步的准备工作。针对不同的Trainer可以使用不同的Hook。Hook翻译过来叫做“钩子”,所以我们可以形象地理解成Hook像在训练首尾的两个钩子一样挂着负责训练的Trainer

TrainerBase中定义了多个Hook,并且在Trainerbefore_stepafter_step等函数中可以看到需要执行每一个Hook在训练之前的准备动作HookBase.before_step、训练之后的收尾动作HookBase.after_step。具体的训练过程非常正常,就是按照iteration的数量运行before_steprun_stepafter_step三个函数。
在 Python 中,hook通常是指在特定时刻自动执行的函数。

SimpleTrainer中作者实现了一种最基本的训练神经网络的流程,它是作为上一段中TrainerBase的子类出现的。它最主要的工作就是将TrainerBase中没有实现的run_step方法实现。事实上,在SimpleTrainer中实现的过程也是最通用的训练过程:

  • iter(dataloader)读取数据
  • loss_dict = self.model(data)计算每个batch的loss
  • self.optimizer.zero_gradlosses.backward()self.optimizer.step()实现训练的过程
  • 通过统一的结构_write_metrics记录、打印计算的指标。
  • 继承HookBase定义在训练前的准备工作、训练后的收尾工作要怎么做
  • 继承SimpleTrainer或者TrainerBase定义自己的训练逻辑
    继承自HookBase,负责before_step、after_step
    继承自SimpleTrainer,负责run_step

hook.py的主要内容就是针对深度学习训练、测试过程中的不同需求,定义了很多个不同的Hook,用来处理训练之前、之后需要准备、收尾的工作。

Hook的实现也都和自己的具体功能有关,会涉及到一些细节,在后面需要考虑这些细节的时候我们自然会涉及到它们。但是这些Hook的实现都遵循着统一的设计逻辑——回顾HookBase,它包含四种方法before_stepafter_stepbefore_trainafter_train,只要我们想要的Hook需要在其中某一个部分做工作,那么只需要定义一个函数的实现即可。
统一的设计原则

在深度学习的任务中,如何清晰简洁地定义每一步训练之前、之后的上下文和额外操作是一件非常重要但是麻烦的事情。在detectron2hook设计中,它通过简单统一的一个接口就将这一切统一到了一起,是一个非常有创意、管用的设计。

DefaultTrainer继承自SimpleTrainer,它在之前的基础上增加了一些常见的特性,使得DefaultTrainer几乎是一个完整的能用来训练神经网络的框架了。

它主要增加了如下的几个部分:

  • 通过参数构建模型(model)、加载训练数据(DataLoader)、优化器(Optimizer)、学习率的变化规则(Scheduler)。
  • 创建常见的Hook(在上一部分我们已经分析了Hook是什么,它们可以处理每次训练前后的准备、收尾工作)。
  • 加载、存储网络中间参数,也就是CheckPoint功能。
  • 训练完全继承自SimpleTrainer,可见super().train()

DefaultTrainerSimpleTrainer不同的部分在于它构建模型、数据集等的部分,可以看到DefaultTrainer都是调用已有的build_xxx函数实现的。其次看创建Hook的部分,在__init__里面它是通过self.register_hooks(self.build_hooks())实现的。在self.build_hooks()里面它构建了在hook.py中已经实现的三种最常见的Hook,包括计算时间的、调整学习率的和计算BatchNorm的统计量的。

Content

我自己阅读detectron2主要是出于下面两点原因:

  • 最近一年的经历越发让我意识到工程能力——特别是在复杂的需求下可以做好的系统设计,在每个模块能写出清晰简洁代码的软件工程能力的重要性。所以我选择把detectron2作为一个绝好的学习范本。
  • Object Detection(物体检测)是计算机视觉中非常基础重要的一个任务,所以我有必要通过学习detectron2的代码弄清楚这个任务一些重要的实现细节。

但是当我写作阅读笔记的时候,我产生了新的动机,因为写出来到知乎这个平台上的文字和写给我自己的文字终究是不同的。我的新动机就是:

  • 帮助一位深度学习初学者学会如何阅读一个开源深度学习项目一个Pytorch项目
  • 帮助自己和任意的一位初学者思考项目中一些独特的设计在软件工程上究竟有怎样的好处,这样来帮助自己写出更好的项目。

不过在真正阅读这篇文章之前,还是需要具备一些基本知识的:

如果你已经做好了一切准备,就让我们一起踏上这段探索的旅程吧哈哈哈!

  1. 庞子奇:Detectron2 代码学习 1 — 整体结构 (本篇)

2. 庞子奇:Detectron2代码学习2 — 检测模型实现

3.庞子奇:Detectron2代码学习3 — 数据加载

4. (计划中) 模型实现细节与其它设计

1. 整体结构

1.1 Demo

想要高效地阅读代码需要摸准它的主体结构,而找到代码的主体结构就得从它的使用方式看起。所以我们首先关注Detectron2的demo部分。在demo中主要包括两个文件,其中demo.py是代码的主要运行部分。demo.py的运行过程依靠predictor.py中的VisualizationDemo,其主要功能是提供了在输入图片/视频上运行Detectron2模型的接口,例如run_on_image在图片上运行,run_on_video在视频上运行。

1
2
3
4
1. 根据参数和config文件创建模型、输入
2. 利用VisualizationDemo的run_on_image、run_on_video接口处理输入图片/视频
3. 利用opencv-python的专用函数,记录或者显示detectron处理后的图片/视频

==因此,整个====demo====的核心实际上是====VisualizationDemo====。==

VisualizationDemo的逻辑也很清楚,我们首先关注__init__部分,这里是整个类的初始化部分,在这里self.predictor顾名思义:它是根据输入图片得到预测结果的模块。在self.run_on_image中展示了用model在给定图片(image参数)上做预测的逻辑。

  • ==模型处理输入得到输出====predictions=====self.predictor(image)====,====predictions====就是模型(刚刚的====self====.predictor====)输出的结果。阅读机器学习、深度学习代码最重要的就是追踪这类====模型处理数据====的代码,因为这类代码是理解整体====计算模型====的关键。==
  • visualizer中作者可以根据不同的输出模式(比如panoptic_segsem_seg)对图片做不同方式的可视化,其中的细节我们暂时先不追究。
  • run_on_videoAsyncPredictordetectron2针对视频和多线程的情况进行了实现,因为我们的重点在于学习整个detectron2的写法,所以暂时不对这些细节进行讨论,感兴趣/对这类情况有需要可以自行学习。

==在====predictor====.py====9-12行的====import====部分,我们可以学习到很多架构深度学习项目的规范、设计方法,在不同的文件夹中,我们往往会通过功能将不同的模块分开包装。例如在====predictor====.py====中体现的:==

  • ==.====data====:处理数据相关的类和方法==
  • ==.engine====:对训练、预测逻辑的整体包装,类似于对整体Pipeline的定义,常见于大型项目==
  • ==.utils====:应该是====utilities====的简写,一般用来放置常用的工具模块,例如在这里体现出来的可视化部分==

==总之,对于越大型的项目来说,合理的分区、包装就越有必要,因为这可以从软件工程角度节省大量用来理解、开发、查错(Debug)的成本。在自己的很多小项目中,合理地使用类似的方法也能有效地提升项目质量。==

1.2 训练概览 train_net.py

demo中我们看到了在图片上做infer是怎样的结构,那么训练要怎么做呢?通过阅读说明文档可以发现==训练代码是====tools/train_net.====py==,所以我们下一步就是分析train_net.py是如何实现的。

train_net.py在文件开始的部分import了一堆东西,因为我们是自顶向下(Top-Down)地对项目进行阅读和理解,所以可以暂时不管这部分的代码具体在做什么。在整个文件中,我们直接跳转到==if== ==__name__== ==== '====__main__===='====的部分(这里是代码运行的接口),可以看到====detectro====n2====的结构是利用====launch====运行了====main====函数中的内容。如果我们不关心分布式训练的部分(即在distributed的作用域的部分),那么====main====函数的逻辑相当简单:得到模型和运行的参数(在参数cfg中)。利用定义好的类====Trainer====,通过传入====cfg====参数可以定义出模型,后面的部分均通过====Trainer====里面的方法都可以实现,例如====train====()====顾名思义就是做训练的,====test====()====就是测试的,====build_model====()====就是创建模型的。==

==在====Trainer====的定义中我们发现它是一个继承自====engine====.DefaultTrainer====的子类,而我们通过上面对====main====函数的分析发现====Trainer====的主要功能其实都来自于====DefaultTrainer==,因此我们的主要任务就是在engine中弄清楚负责训练的模块是如何构建与运行的。

1.3 Engine

engine中,detectron2定义了训练、测试的Pipeline。在demo中我们也看到了来自enginepredictor能够在给定的图片上用模型做预测。对于一个机器学习/深度学习的项目来说,他必须要完成、也是主要完成的事情,就是针对给定的模型进行训练与测试。因此,按照自顶向下的原则认识detectron2就需要首先研究engine中是怎样实现的。

engine中有多个py文件,==查看文件名字和每个文件上面====__all__====的部分可以大致猜测出它们之间相互引用的关系和每个文件主要负责的部分==。==defaults====.py====包含了我们在====train_net====.py====中见到的====DefaultTrainer====,也大多是在import别人==,==launch====.py====中的====launch====顾名思义是让算法开始运行的代码,我们浏览一下它最主要的函数====launch====,根据它的参数====num_machines====、====machine_rank====可得知它是负责分布式训练的代码,又有参数中====main_func====,可以知道====launch====不涉及====detectro====n2====的实际功能==,所以我们跳过launch.py。进一步考察,我们发现hook.py引用了train_loop.py中的内容,因此==我们清晰了====engine====部分的层次关系。具体而言,我们按照如下的顺序阅读代码==

  • ==train_loop====.py==
  • ==hook====.py==
  • ==defaults====.py==

1.3.1 train_loop.py

train_loop中作者实现了从简单到复杂的三个类,分别在不同的层次上抽象了对训练过程的需要和实现,包括HookBaseTrainBaseSimpleTrainer。通过名字上的Base和Simple可以这些只是实际被使用的类的基类,但是我们还是要首先针对这三个类进行分析。

这三个模块的注释都非常详尽,阅读之后可以对模块的构成、作用有初步的了解,在这里仅做简要的解释。==HookBase====是====Hook====的基类,其中实现了方法====before_step====、====before_train====和====after_step====、====after_train====,其主要的作用是在真正做训练之前,做好每一步的准备工作。针对不同的====Trainer====可以使用不同的====Hook====。Hook翻译过来叫做“钩子”,所以我们可以形象地理解成Hook像在训练首尾的两个钩子一样挂着负责训练的====Trainer====。==

==在====TrainerBase====中定义了多个====Hook====,并且在====Trainer====的====before_step====、====after_step====等函数中可以看到需要执行每一个====Hook====在训练之前的准备动作====HookBase====.====before_step====、训练之后的收尾动作====HookBase====.====after_step====。具体的训练过程非常正常,就是按照iteration的数量运行====before_step====、====run_step====、====after_step====三个函数。==

==在====SimpleTrainer====中作者实现了一种最基本的训练神经网络的流程,它是作为上一段中====TrainerBase====的子类出现的。它最主要的工作就是将====TrainerBase====中没有实现的====run_step====方法实现。事实上,在====SimpleTrainer====中实现的过程也是最通用的训练过程:==

  • ==iter====(dataloader)====读取数据==
  • ==loss_dict== === self.model(====data====)====计算每个batch的loss==
  • ==self====.optimizer====.zero_grad====、====losses====.backward====()====、====self====.optimizer====.step====()====实现训练的过程==
  • ==通过统一的结构====_write_metrics====记录、打印计算的指标。==

综上,我们了解了detectron2engine的框架,和我们使用时需要注意的层次:

  • ==继承====HookBase====定义在训练前的准备工作、训练后的收尾工作要怎么做==
  • ==继承====SimpleTrainer====或者====TrainerBase====定义自己的训练逻辑==

1.3.2 hook.py

==hook====.py====的主要内容就是针对深度学习训练、测试过程中的不同需求,定义了很多个不同的====Hook====,用来处理训练之前、之后需要准备、收尾的工作。==其中包括了计算时间的IterationTimer、按一定周期输出结果的PeriodicWriter、调整学习率的LRScheduler等。

其它的==Hook====的实现也都和自己的具体功能有关,会涉及到一些细节,在后面需要考虑这些细节的时候我们自然会涉及到它们。但是这些====Hook====的实现都遵循着统一的设计逻辑——回顾====HookBase====,它包含四种方法====before_step====、====after_step====、====before_train====、====after_train====,只要我们想要的====Hook====需要在其中某一个部分做工作,那么只需要定义一个函数的实现即可。==

==在深度学习的任务中,如何清晰简洁地定义每一步训练之前、之后的上下文和额外操作是一件非常重要但是麻烦的事情。在====detectro====n2====的====hook====设计中,它通过简单统一的一个接口就将这一切统一到了一起,是一个非常有创意、管用的设计。==

1.3.3 defaults.py

有了train_loop.py中的基本理解,我们就可以深入到defaults.py了,在这里定义了在train_net.py中使用的基本模块DefaultTrainer

==DefaultTrainer====继承自====SimpleTrainer====,它在之前的基础上增加了一些常见的特性,使得====DefaultTrainer====几乎是一个完整的能用来训练神经网络的框架了。==它主要增加了如下的几个部分:

  • ==通过参数构建模型(model)、加载训练数据(DataLoader)、优化器(Optimizer)、学习率的变化规则(Scheduler)。==
  • ==创建常见的Hook(在上一部分我们已经分析了Hook是什么,它们可以处理每次训练前后的准备、收尾工作)。==
  • ==加载、存储网络中间参数,也就是CheckPoint功能。==
  • ==训练完全继承自====SimpleTrainer====,可见====super====()====.train()==

==DefaultTrainer====和====SimpleTrainer====不同的部分在于它构建模型、数据集等的部分,可以看到====DefaultTrainer====都是调用已有的====build_xxx====函数实现的。其次看创建Hook的部分,在====__init__====里面它是通过====self.register====_hooks(====self====.====build_hooks====()====)====实现的。在====self====.build_hooks()====里面它构建了在====hook====.py====中已经实现的三种最常见的Hook,包括计算时间的、调整学习率的和计算BatchNorm的统计量的。==

1.4 小结

综上,我们已经清晰了detectron2的整体架构与逻辑,特别是它如何为训练过程的组件和函数构建了统一的接口TrainerHook。但是我们在这个过程中也没有深究一些细节,比如说如何构建数据集、如何构建模型、如何构建优化器等等,在后面我们将对它们进行详细分析。