Link
Highlights&&Note
因此,整个
demo
的核心实际上是VisualizationDemo
。模型处理输入得到输出
predictions=self.predictor(image)
,predictions
就是模型(刚刚的self.predictor
)输出的结果。阅读机器学习、深度学习代码最重要的就是追踪这类模型处理数据的代码,因为这类代码是理解整体计算模型的关键。在
predictor.py
9-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_machines
、machine_rank
可得知它是负责分布式训练的代码,又有参数中main_func
,可以知道launch
不涉及detectron2
的实际功能
分布式训练,不用管我们清晰了
engine
部分的层次关系。具体而言,我们按照如下的顺序阅读代码
train_loop.py
hook.py
defaults.py
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
三个函数。
在 Python 中,hook通常是指在特定时刻自动执行的函数。在
SimpleTrainer
中作者实现了一种最基本的训练神经网络的流程,它是作为上一段中TrainerBase
的子类出现的。它最主要的工作就是将TrainerBase
中没有实现的run_step
方法实现。事实上,在SimpleTrainer
中实现的过程也是最通用的训练过程:
iter(dataloader)
读取数据loss_dict = self.model(data)
计算每个batch的lossself.optimizer.zero_grad
、losses.backward()
、self.optimizer.step()
实现训练的过程- 通过统一的结构
_write_metrics
记录、打印计算的指标。
- 继承
HookBase
定义在训练前的准备工作、训练后的收尾工作要怎么做- 继承
SimpleTrainer
或者TrainerBase
定义自己的训练逻辑
继承自HookBase,负责before_step、after_step
继承自SimpleTrainer,负责run_step
hook.py
的主要内容就是针对深度学习训练、测试过程中的不同需求,定义了很多个不同的Hook
,用来处理训练之前、之后需要准备、收尾的工作。
Hook
的实现也都和自己的具体功能有关,会涉及到一些细节,在后面需要考虑这些细节的时候我们自然会涉及到它们。但是这些Hook
的实现都遵循着统一的设计逻辑——回顾HookBase
,它包含四种方法before_step
、after_step
、before_train
、after_train
,只要我们想要的Hook
需要在其中某一个部分做工作,那么只需要定义一个函数的实现即可。
统一的设计原则在深度学习的任务中,如何清晰简洁地定义每一步训练之前、之后的上下文和额外操作是一件非常重要但是麻烦的事情。在
detectron2
的hook
设计中,它通过简单统一的一个接口就将这一切统一到了一起,是一个非常有创意、管用的设计。
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的统计量的。
Content
我自己阅读detectron2
主要是出于下面两点原因:
- 最近一年的经历越发让我意识到工程能力——特别是在复杂的需求下可以做好的系统设计,在每个模块能写出清晰简洁代码的软件工程能力的重要性。所以我选择把
detectron2
作为一个绝好的学习范本。 - Object Detection(物体检测)是计算机视觉中非常基础重要的一个任务,所以我有必要通过学习
detectron2
的代码弄清楚这个任务一些重要的实现细节。
但是当我写作阅读笔记的时候,我产生了新的动机,因为写出来到知乎这个平台上的文字和写给我自己的文字终究是不同的。我的新动机就是:
- 帮助一位深度学习初学者学会如何阅读一个开源深度学习项目、一个Pytorch项目。
- 帮助自己和任意的一位初学者思考项目中一些独特的设计在软件工程上究竟有怎样的好处,这样来帮助自己写出更好的项目。
不过在真正阅读这篇文章之前,还是需要具备一些基本知识的:
- Python的基本使用和Pytorch的基础用法。
- 神经网络训练的基本技巧,或者看过相关博客文章了解相关概念。
- Object Detection的基本算法RCNN、Fast-RCNN、Faster-RCNN,或者看过相关文章,了解相关概念。
如果你已经做好了一切准备,就让我们一起踏上这段探索的旅程吧哈哈哈!
2. 庞子奇:Detectron2代码学习2 — 检测模型实现
4. (计划中) 模型实现细节与其它设计
1. 整体结构
1.1 Demo
想要高效地阅读代码需要摸准它的主体结构,而找到代码的主体结构就得从它的使用方式看起。所以我们首先关注Detectron2的demo
部分。在demo
中主要包括两个文件,其中demo.py
是代码的主要运行部分。demo.py
的运行过程依靠predictor.py
中的VisualizationDemo
,其主要功能是提供了在输入图片/视频上运行Detectron2
模型的接口,例如run_on_image
在图片上运行,run_on_video
在视频上运行。
1 | 1. 根据参数和config文件创建模型、输入 |
==因此,整个====demo==
==的核心实际上是====VisualizationDemo==
==。==
VisualizationDemo
的逻辑也很清楚,我们首先关注__init__
部分,这里是整个类的初始化部分,在这里self.predictor
顾名思义:它是根据输入图片得到预测结果的模块。在self.run_on_image
中展示了用model
在给定图片(image
参数)上做预测的逻辑。
- ==模型处理输入得到输出==
==predictions=====self.predictor(image)==
==,====predictions==
==就是模型(刚刚的====self====.predictor==
==)输出的结果。阅读机器学习、深度学习代码最重要的就是追踪这类====模型处理数据====的代码,因为这类代码是理解整体====计算模型====的关键。== - 在
visualizer
中作者可以根据不同的输出模式(比如panoptic_seg
、sem_seg
)对图片做不同方式的可视化,其中的细节我们暂时先不追究。 - 在
run_on_video
和AsyncPredictor
中detectron2
针对视频和多线程的情况进行了实现,因为我们的重点在于学习整个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
中我们也看到了来自engine
的predictor
能够在给定的图片上用模型做预测。对于一个机器学习/深度学习的项目来说,他必须要完成、也是主要完成的事情,就是针对给定的模型进行训练与测试。因此,按照自顶向下的原则认识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
中作者实现了从简单到复杂的三个类,分别在不同的层次上抽象了对训练过程的需要和实现,包括HookBase
、TrainBase
、SimpleTrainer
。通过名字上的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==
==记录、打印计算的指标。==
综上,我们了解了detectron2
中engine
的框架,和我们使用时需要注意的层次:
- ==继承==
==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
的整体架构与逻辑,特别是它如何为训练过程的组件和函数构建了统一的接口Trainer
、Hook
。但是我们在这个过程中也没有深究一些细节,比如说如何构建数据集、如何构建模型、如何构建优化器等等,在后面我们将对它们进行详细分析。