目前在开发一个 AI 相关业务的框架。假设用户需要一个 DataLoader
用于实现从硬盘读取数据,其中每一组数据中包含一张图像数据和一份标签数据。
由于无法预知用户所需的图像和标签格式,因此在基类中我将 load_image
和 load_label
定义为抽象方法:
from abc import ABC
class BaseLoader(ABC):
def __init__(self, image_paths, label_paths)
self.image_paths = image_paths
self.label_paths = label_paths
@abstractmethod
def load_image(self, index):
pass
@abstractmethod
def load_label(self, index):
pass
用户需要在派生类中自己去定义具体的图像读取方式,比如用户自己写了一个 JpegLoader
类用来读取 jpeg 图像,另一个 TiffLoader
类用来读取 tiff 图像:
@IMAGE_LOADERS.register('jpeg')
class JpegLoader(BaseLoader):
def load_image(self, index):
# load jpeg image
@IMAGE_LOADERS.register('tiff')
class TiffLoader(BaseLoader):
def load_image(self, index):
# load tiff image
类似地,也有不同的派生类用来定义标签数据的读取方式:
@LABEL_LOADERS.register('json')
class JsonLoader(BaseLoader):
def load_label(self, index):
# load json label
@LABEL_LOADERS.register('yaml')
class YamlLoader(BaseLoader):
def load_label(self, index):
# load yaml label
在运行阶段,用户通过配置文件从 IMAGE_LOADERS
和 LABEL_LOADERS
注册器中分别选取要调用的派生类,并临时通过菱形继承的方式生成一个新的派生类:
def create_data_loader(image_type, label_type):
image_loader_cls = IMAGE_LOADERS[image_type]
label_loader_cls = LABEL_LOADERS[label_type]
class RuntimeLoader(image_loader_cls, label_loader_cls):
pass
return RuntimeLoader()
以上是我针对这种需求想到的一个设计方案。我的问题是:
1 、当需要解耦一个类中的不同功能组件时,让用户自己去定义派生类(比如上面例子中的 JpegLoader
和 JsonLoader
),并由菱形继承的方式再去生成一个新的派生类,这是不是一种合理的设计模式?我总感觉怪怪的,因为一般的对外接口都是提供一个抽象基类,让用户继承自这个基类去定义自己的子类,没见过用这种菱形继承的方式;
2 、最终用来实例化的类是 RuntimeLoader
,并不是用户自己去定义的派生类中的任何一个,而且整个 create_data_loader
函数对用户也是封闭的,这会不会让用户觉得很迷惑?比如在 debug 的时候会发现真正的 dataloader 是一个 RuntimeLoader
对象,而自己分明没有开发过这么一个类。
第一次开发比较大的一个工程,希望多家多多指点~
1
hsfzxjy 2021-11-21 17:57:25 +08:00 2
有个概念是 Composition over Inheritance Principle ,可以看看这篇文章 https://hynek.me/articles/python-subclassing-redux/ 。
我认为一个理想的做法是 ImageLoader 和 LabelLoader 作为单独两支类,不要继承自 BaseLoader 。然后 create_data_loader 里根据 image_type, label_type 构建相应类的实例,RuntimeLoader.__init__ 则接收一个 ImageLoader 和 LabelLoader 的实例,并在相应的方法中调用。 |
2
shm7 2021-11-21 18:09:07 +08:00
我想问下,假如读取图像的后缀不同,还没法读了?
读图像 /label 外,不应该再有个 Transform 的办法?不去看看 TorchVision 的实现? 我是搞不明白这么简单的读图像和 label ,为啥还能搞出几层继承?这设计在代码复杂度大大提升的同时,连基本功能都完不成。 我建议 lz 从使用方角度考虑怎么使用方便,再来设计吧。不懂怎么设计,怎么简单怎么来,不会错很多。搞不懂到处加东西,是很恶心的。 |
3
shm7 2021-11-21 18:10:09 +08:00
我觉得这里不需要做二次开发,Torch 的 Dataloader/Dataset 已经帮你做好了。
|
4
jaredyam 2021-11-21 19:31:21 +08:00
近年来似乎每个公司的算法部门都在想怎么做一个自己的框架,但似乎都没能逃出 PyTorch 和商汤 mmcv 。怎么说,就你讨论的数据加载和预处理相关模块,以上模块都是以一个数据集作为一个实现单元,而你的想法是以文件类型,感觉这种以类型为捆绑的描述方法有点奇怪?
|
5
capbone OP |
6
capbone OP @hsfzxjy 拜读了一下,收获不小。
你说的这个方案我也想过,不过类似上面那个例子中提到的情况,`ImageLoader` 需要获取到 `BaseLoader` 中的一些属性(比如 `image_paths`),如果不使用继承而是使用组合,那么就需要在实例化 `ImageLoader` 的时候把 `image_paths` 作为参数之一?这样似乎又使得整个系统的复杂度变高了? |
7
2i2Re2PLMaDnghL 2021-11-22 11:08:11 +08:00 1
@capbone 第一,显式传递复杂度反而较低,虽然看上去写了更多代码。
Explicit is better than implicit 第二,菱形继承时运用隐式传递会产生一个重大的问题,就是不写 super() 那句的话「不产生报错但行为不正常」 这个是从 C 开始就花了大力气避免的事情。 反之,如果是显式传递,一旦不写立马报错,很容易排查问题。 除非你要在两个 loader 间 cross reference ,否则不要菱形继承。 就算对于资源要 cross ref ,有时你也可以分离 resource provider 和 loader |
8
ekidona 2021-11-22 11:29:31 +08:00 via iPhone
熟读各类框架并 maintain 了一两套内部的发表一下看法:mm 系列的接口设计基本上就是反面教材,mmcv 对 ddp 的额外封装更是 trash, 不要学习它。 相比之下 D2 ,fvcore 那一套 FAIR 实现就漂亮很多
|
9
capbone OP @2i2Re2PLMaDnghL 再请教一下,如果资源要 cross reference ,是建议把这些资源直接作为子类 __init__ 的参数吗?比如:
``` class JpegLoader(BaseLoader): def load_image(self, arg1, arg2, arg3, ...): pass ``` 这样? |
10
2i2Re2PLMaDnghL 2021-11-22 12:24:24 +08:00
@capbone 视具体的侵入程度,除了你所指的这种,另一种就是分层,用一个类似字典的对象来存储所有的资源(资源供应商、资源管理器、资源注册表、资源池、you name it ),然后把整个资源对象传给 loader ,让 loader 按需摘录资源处理。
|