0%

Python 深度学习脚手架 ModelZoo

想必大家都或多或少听过 TensorFlow 的大名,这是 Google 开源的一个深度学习框架,里面的模型和 API 可以说基本是一应俱全,但 TensorFlow 其实有很多让人吐槽的地方,比如 TensorFlow 早期是只支持静态图的,你要调试和查看变量的值的话就得一个个变量运行查看它的结果,这是极其不友好的,而 PyTorch、Chainer 等框架就天生支持动态图,可以直接进行调试输出,非常方便。另外 TensorFlow 的 API 各个版本之间经常会出现不兼容的情况,比如 1.4 升到 1.7,里面有相当一部分 API 都被改了,里面有的是 API 名,有的直接改参数名,有的还给你改参数的顺序,如果要做版本兼容升级,非常痛苦。还有就是用 TensorFlow 写个模型,其实相对还是比较繁琐的,需要定义模型图,配置 Loss Function,配置 Optimizer,配置模型保存位置,配置 Tensor Summary 等等,其实并没有那么简洁。 然而为啥这么多人用 TensorFlow?因为它是 Google 家的,社区庞大,还有一个原因就是 API 你别看比较杂,但是确实比较全,contrib 模块里面你几乎能找到你想要的所有实现,而且更新确实快,一些前沿论文现在基本都已经在新版本里面实现了,所以,它的确是有它自己的优势。 然后再说说 Keras,这应该是除了 TensorFlow 之外,用的第二广泛的框架了,如果你用过 TensorFlow,再用上 Keras,你会发现用 Keras 搭模型实在是太方便了,而且如果你仔细研究下它的 API 设计,你会发现真的封装的非常科学,我感觉如果要搭建一个简易版的模型,Keras 起码得节省一半时间吧。 一个好消息是 TensorFlow 现在已经把 Keras 包进来了,也就是说如果你装了 TensorFlow,那就能同时拥有 TensorFlow 和 Keras 两个框架,哈哈,所以你最后还是装个 TensorFlow 就够了。 还有另一个好消息,刚才我不是吐槽了 TensorFlow 的静态图嘛?这的确是个麻烦的东西,不过现在的 TensorFlow 不一样了,它支持了 Eager 模式,也就是支持了动态图,有了它,我们可以就像写 Numpy 操作一样来搭建模型了,要看某个变量的值,很简单,直接 print 就 OK 了,不需要再去调用各种 run 方法了,可以直接抛弃 Session 这些繁琐的东西,所以基本上和 PyTorch 是一个套路的了,而且这个 Eager 模式在后续的 TensorFlow 2.0 版本将成为主打模式。简而言之,TensorFlow 比之前好用多了! 好,以上说了这么多,我今天的要说的正题是什么呢?嗯,就是我基于 TensorFlow Eager 模式和 Keras 写了一个深度学习的框架。说框架也不能说框架,更准确地说应该叫脚手架,项目名字叫做 ModelZoo,中文名字可以理解成模型动物园。 有了这个脚手架,我们可以更加方便地实现一个深度学习模型,进一步提升模型开发的效率。 另外,既然是 ModelZoo,模型必不可少,我也打算以后把一些常用的模型来基于这个脚手架的架构实现出来,开源供大家使用。

动机

有人说,你这不是闲的蛋疼吗?人家 Keras 已经封装得很好了,你还写个啥子哦?嗯,没错,它的确是封装得很好了,但是我觉得某些地方是可以写得更精炼的。比如说,Keras 里面在模型训练的时候可以自定义 Callback,比如可以实现 Tensor Summary 的记录,可以保存 Checkpoint,可以配置 Early Stop 等等,但基本上,你写一个模型就要配一次吧,即使没几行代码,但这些很多情况都是需要配置的,所以何必每个项目都要再去写一次呢?所以,这时候就可以把一些公共的部分抽离出来,做成默认的配置,省去不必要的麻烦。 另外,我在使用过程中发现 Keras 的某些类并没有提供我想要的某些功能,所以很多情况下我需要重写某个功能,然后自己做封装,这其实也是一个可抽离出来的组件。 另外还有一个比较重要的一点就是,Keras 里面默认也不支持 Eager 模式,而 TensorFlow 新的版本恰恰又有了这一点,所以二者的兼并必然是一个绝佳的组合。 所以我写这个框架的目的是什么呢?

  • 第一,模型存在很多默认配置且可复用的地方,可以将默认的一些配置在框架中进行定义,这样我们只需要关注模型本身就好了。
  • 第二,TensorFlow 的 Eager 模式便于 TensorFlow 的调试,Keras 的高层封装 API 便于快速搭建模型,取二者之精华。
  • 第三,现在你可以看到要搜一个模型,会有各种花式的实现,有的用的这个框架,有的用的那个框架,而且参数、数据输入输出方式五花八门,实在是让人头大,定义这个脚手架可以稍微提供一些规范化的编写模式。
  • 第四,框架名称叫做 ModelZoo,但我的理想也并不仅仅于实现一个简单的脚手架,我的愿景是把当前业界流行的模型都用这个框架实现出来,格式规范,API 统一,开源之后分享给所有人用,给他人提供便利。

所以,ModelZoo 诞生了!

开发过程

开发的时候,我自己首先先实现了一些基本的模型,使用的是 TensorFlow Eager 和 Keras,然后试着抽离出来一些公共部分,将其封装成基础类,同时把模型独有的实现放开,供子类复写。然后在使用过程中自己还封装和改写过一些工具类,这部分也集成进来。另外就是把一些配置都规范化,将一些常用参数配置成默认参数,同时放开重写开关,在外部可以重定义。 秉承着上面的思想,我大约是在 10 月 6 日 那天完成了框架的搭建,然后在后续的几天基于这个框架实现了几个基础模型,最终打磨成了现在的样子。

框架介绍

GitHub 地址:https://github.com/ModelZoo/ModelZoo 框架我已经发布到 PyPi,直接使用 pip 安装即可,目前支持 Python3,Python 2 尚未做测试,安装方式:

1
pip3 install model-zoo

其实我是很震惊,这个名字居然没有被注册!GitHub 和 PyPi 都没有!不过现在已经被我注册了。 OK,接下来让我们看看用了它能怎样快速搭建一个模型吧! 我们就以基本的线性回归模型为例来说明吧,这里有一组数据,是波士顿房价预测数据,输入是影响房价的各个因素,输出是房价本身,具体的数据集可以搜 Boston housing price regression dataset 了解一下。 总之,我们只需要知道这是一个回归模型就好了,输入 x 是一堆 Feature,输出 y 是一个数值,房价。好,那么我们就开始定义模型吧,模型的定义我们继承 ModelZoo 里面的 BaseModel 就好了,实现 model.py 如下:

1
2
3
4
5
6
7
8
9
10
11
from model_zoo.model import BaseModel
import tensorflow as tf

class BostonHousingModel(BaseModel):
def __init__(self, config):
super(BostonHousingModel, self).__init__(config)
self.dense = tf.keras.layers.Dense(1)

def call(self, inputs, training=None, mask=None):
o = self.dense(inputs)
return o

好了,这就定义完了!有人会说,你的 Loss Function 呢?你的 Optimizer 呢?你的 Checkpoint 保存呢?你的 Tensor Summary 呢?不需要!因为我已经把这些配置封装到 BaseModel 了,有默认的 Loss Function、Optimizer、Checkpoint、Early Stop、Tensor Summary,这里只需要关注模型本身即可。 有人说,要是想自定义 Loss Function 咋办呢?自定义 Optimizer 咋办呢?很简单,只需要复写一些基本的配置或复写某个方法就好了。 如改写 Optimizer,只需要重写 optimizer 方法即可:

1
2
def optimizer(self):
return tf.train.AdamOptimizer(0.001)

好,定义了模型之后怎么办?那当然是拿数据训练了,又要写数据加载,数据标准化,数据切分等等操作了吧,写到什么方法里?定义成什么样比较科学?现在,我们只需要实现一个 Trainer 就好了,然后复写 prepare_data 方法就好了,实现 train.py 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tensorflow as tf
from model_zoo.trainer import BaseTrainer
from model_zoo.preprocess import standardize

tf.flags.DEFINE_integer('epochs', 100, 'Max epochs')
tf.flags.DEFINE_string('model_class', 'BostonHousingModel', 'Model class name')

class Trainer(BaseTrainer):

def prepare_data(self):
from tensorflow.python.keras.datasets import boston_housing
(x_train, y_train), (x_eval, y_eval) = boston_housing.load_data()
x_train, x_eval = standardize(x_train, x_eval)
train_data, eval_data = (x_train, y_train), (x_eval, y_eval)
return train_data, eval_data

if __name__ == '__main__':
Trainer().run()

好了,完事了,模型现在已经全部搭建完成!在这里只需要实现 prepare_data 方法,返回训练集和验证集即可,其他的什么都不需要! 数据标准化在哪做的?这里我也封装好了方法。 运行在哪运行的?这里我也做好了封装。 模型保存在哪里做的?同样做好了封装。 Batch 切分怎么做的?这里也做好了封装。 我们只需要按照格式,返回这两组数据就好了,其他的什么都不用管! 那同样的,模型保存位置,模型名称,Batch Size 多大,怎么设置?还是简单改下配置就好了。 如要修改模型保存位置,只需要复写一个 Flag 就好了:

1
tf.flags.DEFINE_string('checkpoint_dir', 'checkpoints', help='Data source dir')

好了,现在模型可以训练了!直接运行上面的代码就好了:

1
python3 train.py

结果是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Epoch 1/100
1/13 [=>............................] - ETA: 0s - loss: 816.1798
13/13 [==============================] - 0s 4ms/step - loss: 457.9925 - val_loss: 343.2489

Epoch 2/100
1/13 [=>............................] - ETA: 0s - loss: 361.5632
13/13 [==============================] - 0s 3ms/step - loss: 274.7090 - val_loss: 206.7015
Epoch 00002: saving model to checkpoints/model.ckpt

Epoch 3/100
1/13 [=>............................] - ETA: 0s - loss: 163.5308
13/13 [==============================] - 0s 3ms/step - loss: 172.4033 - val_loss: 128.0830

Epoch 4/100
1/13 [=>............................] - ETA: 0s - loss: 115.4743
13/13 [==============================] - 0s 3ms/step - loss: 112.6434 - val_loss: 85.0848
Epoch 00004: saving model to checkpoints/model.ckpt

Epoch 5/100
1/13 [=>............................] - ETA: 0s - loss: 149.8252
13/13 [==============================] - 0s 3ms/step - loss: 77.0281 - val_loss: 57.9716
....

Epoch 42/100
7/13 [===============>..............] - ETA: 0s - loss: 20.5911
13/13 [==============================] - 0s 8ms/step - loss: 22.4666 - val_loss: 23.7161
Epoch 00042: saving model to checkpoints/model.ckpt

可以看到模型每次运行都会实时输出训练集和验证集的 Loss 的变化,另外还会自动保存模型,自动进行 Early Stop,自动保存 Tensor Summary。 可以看到这里运行了 42 个 Epoch 就完了,为什么?因为 Early Stop 的存在,当验证集经过了一定的 Epoch 一直不见下降,就直接停了,继续训练下去也没什么意义了。Early Stop 哪里配置的?框架也封装好了。 然后我们还可以看到当前目录下还生成了 events 和 checkpoints 文件夹,这一个是 TensorFlow Summary,供 TensorBoard 看的,另一个是保存的模型文件。 现在可以打开 TensorBoard 看看有什么情况,运行命令:

1
2
cd events
tensorboard --logdir=.

可以看到训练和验证的 Loss 都被记录下来,并化成了图表展示。而这些东西我们配置过吗?没有,因为框架封装好了。 好,现在模型有了,我们要拿来做预测咋做呢?又得构建一边图,又得重新加载模型,又得准备数据,又得切分数据等等,还是麻烦,并没有,这里只需要这么定义就好了,定义 infer.py 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from model_zoo.inferer import BaseInferer
from model_zoo.preprocess import standardize
import tensorflow as tf

tf.flags.DEFINE_string('checkpoint_name', 'model.ckpt-20', help='Model name')

class Inferer(BaseInferer):

def prepare_data(self):
from tensorflow.python.keras.datasets import boston_housing
(x_train, y_train), (x_test, y_test) = boston_housing.load_data()
_, x_test = standardize(x_train, x_test)
return x_test

if __name__ == '__main__':
result = Inferer().run()
print(result)

这里只需要继承 BaseInferer,实现 prepare_data 方法就好了,返回的就是 test 数据集的 x 部分,其他的还是什么都不用干! 另外这里额外定义了一个 Flag,就是 checkpoint_name,这个是必不可少的,毕竟要用哪个 Checkpoint 需要指定一下。 这里我们还是那数据集中的数据当测试数据,来看下它的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[ 9.637125 ]
[21.368305 ]
[20.898445 ]
[33.832504 ]
[25.756516 ]
[21.264557 ]
[29.069794 ]
[24.968184 ]
...
[36.027283 ]
[39.06852 ]
[25.728745 ]
[41.62165 ]
[34.340042 ]
[24.821484 ]]

就这样,预测房价结果就计算出来了,这个和输入的 x 内容都是一一对应的。 那有人又说了,我如果想拿到模型中的某个变量结果怎么办?还是很简单,因为有了 Eager 模式,直接输出就好。我要自定义预测函数怎么办?也很简单,复写 infer 方法就好了。 好,到现在为止,我们通过几十行代码就完成了这些内容:

  • 数据加载和预处理
  • 模型图的搭建
  • Optimizer 的配置
  • 运行结果的保存
  • Early Stop 的配置
  • Checkpoint 的保存
  • Summary 的生成
  • 预测流程的实现

总而言之,用了这个框架可以省去很多不必要的麻烦,同时相对来说比较规范,另外灵活可扩展。 以上就是 ModelZoo 的一些简单介绍。

愿景

现在这个框架刚开发出来几天,肯定存在很多不成熟的地方,另外文档也还没有来得及写,不过我肯定是准备长期优化和维护下去的。另外既然取名叫做 ModelZoo,我后面也会把一些常用的深度学习模型基于该框架实现出来并发布,包括 NLP、CV 等各大领域,同时在实现过程中,也会发现框架本身的一些问题,并不断迭代优化。 比如基于该框架实现的人脸情绪识别的项目:https://github.com/ModelZoo/EmotionRecognition 其识别准确率还是可以的,比如输入这些图片: 模型便可以输出对应的情绪类型和情绪分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Image Path: test1.png
Predict Result: Happy
Emotion Distribution: {'Angry': 0.0, 'Disgust': 0.0, 'Fear': 0.0, 'Happy': 1.0, 'Sad': 0.0, 'Surprise': 0.0, 'Neutral': 0.0}
====================
Image Path: test2.png
Predict Result: Happy
Emotion Distribution: {'Angry': 0.0, 'Disgust': 0.0, 'Fear': 0.0, 'Happy': 0.998, 'Sad': 0.0, 'Surprise': 0.0, 'Neutral': 0.002}
====================
Image Path: test3.png
Predict Result: Surprise
Emotion Distribution: {'Angry': 0.0, 'Disgust': 0.0, 'Fear': 0.0, 'Happy': 0.0, 'Sad': 0.0, 'Surprise': 1.0, 'Neutral': 0.0}
====================
Image Path: test4.png
Predict Result: Angry
Emotion Distribution: {'Angry': 1.0, 'Disgust': 0.0, 'Fear': 0.0, 'Happy': 0.0, 'Sad': 0.0, 'Surprise': 0.0, 'Neutral': 0.0}
====================
Image Path: test5.png
Predict Result: Fear
Emotion Distribution: {'Angry': 0.04, 'Disgust': 0.002, 'Fear': 0.544, 'Happy': 0.03, 'Sad': 0.036, 'Surprise': 0.31, 'Neutral': 0.039}
====================
Image Path: test6.png
Predict Result: Sad
Emotion Distribution: {'Angry': 0.005, 'Disgust': 0.0, 'Fear': 0.027, 'Happy': 0.002, 'Sad': 0.956, 'Surprise': 0.0, 'Neutral': 0.009}

如果大家对这个框架感兴趣,或者也想加入实现一些有趣的模型的话,可以在框架主页提 Issue 留言,我非常欢迎你的加入!另外如果大家感觉框架有不足的地方,也非常欢迎提 Issue 或发 PR,非常非常感谢! 最后,如果你喜欢的话,还望能赠予它一个 Star,这样我也更有动力去维护下去。 项目的 GitHub 地址:https://github.com/ModelZoo/ModelZoo。 谢谢!