TF2中的custom layer&model&training
在上Coursera上关于Tensorflow的高级用法课程时,老师简略介绍了custom layer和custom model的用法,但后来看到其实课程覆盖的内容比较简单,除了介绍了__init__和call两个可override的function外没有介绍其他的。偶然看到一篇博客详细介绍了在tensorflow中如何使用sub classing来搭建模型,写的非常好,这里贴上链接
我们知道在tensorflow中有三种搭建模型的方式: 1) sequential API
也就是想创建一个Sequential实例,然后通过add的方式把一个layer加到模型中去,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# declare input shape
seq_model = tf.keras.Sequential()
seq_model.add(tf.keras.Input(shape=imput_dim))
# Block 1
seq_model.add(tf.keras.layers.Conv2D(32, 3, strides=2, activation="relu"))
seq_model.add(tf.keras.layers.MaxPooling2D(3))
seq_model.add(tf.keras.layers.BatchNormalization())
# Block 2
seq_model.add(tf.keras.layers.Conv2D(64, 3, activation="relu"))
seq_model.add(tf.keras.layers.BatchNormalization())
seq_model.add(tf.keras.layers.Dropout(0.3))
# Now that we apply global max pooling.
seq_model.add(tf.keras.layers.GlobalMaxPooling2D())
# Finally, we add a classification layer.
seq_model.add(tf.keras.layers.Dense(output_dim))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# declare input shape
input = tf.keras.Input(shape=(imput_dim))
# Block 1
x = tf.keras.layers.Conv2D(32, 3, strides=2, activation="relu")(input)
x = tf.keras.layers.MaxPooling2D(3)(x)
x = tf.keras.layers.BatchNormalization()(x)
# Block 2
x = tf.keras.layers.Conv2D(64, 3, activation="relu")(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Dropout(0.3)(x)
# Now that we apply global max pooling.
gap = tf.keras.layers.GlobalMaxPooling2D()(x)
# Finally, we add a classification layer.
output = tf.keras.layers.Dense(output_dim)(gap)
# bind all
func_model = tf.keras.Model(input, output)tf.keras.Model()
来将inputs和outputs接起来。
- Model sub-classing API 第三种方式是现在用的最多的方式。
之前我没理解layer和model两种调用方式的区别,我觉得就是一系列运算,我们把输入输进来,return
output结果的一个过程。但如果一个类它是Layer的子类,它比model的子类多了一个功能,它有state属性,也就是我们熟悉的weights。比如Dense
layer,我们知道它做了线性运算+激活函数,其中的weights就是我们assign给每一个feature的权重,但其实我们并不只是想要这一类别的运算,比如下面的:
上面的代码将inputs平方之后和a做乘积,之后再加上inputs和b的乘积,最终返回的是和。这样的运算是tf.keras.layer中没有的。这个时候我们自己customize layer就很方便。还有一个很方便的地方在于很多模型其实是按模块来的,模块内部的layer很类似。这个时候我们就可以把这些模型内的layer包起来变成一个layer的子类(Module),再定义完这些module之后我们使用Model把这些module再包起来,这就是我们最终的model。这时候我们就可以看到Model和Layer子类的区别了,虽然两者都可以实现输入进来之后实现一系列运算返回运算结果,但后者可以实现更灵活的运算,而前者往往是在把每一个模块定义好之后最终定义我们训练模型的类。 > In general, we use the Layer class to define the inner computation blocks and will use the Model class to define the outer model, practically the object that we will train. ---粘贴自博客
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
28
29
30
31
32class SimpleQuadratic(Layer):
def __init__(self, units=32, activation=None):
'''Initializes the class and sets up the internal variables'''
# YOUR CODE HERE
super(SimpleQuadratic, self).__init__()
self.units = units
self.activation = tf.keras.activations.get(activation)
def build(self, input_shape):
'''Create the state of the layer (weights)'''
# a and b should be initialized with random normal, c (or the bias) with zeros.
# remember to set these as trainable.
# YOUR CODE HERE
a_init = tf.random_normal_initializer()
b_init = tf.random_normal_initializer()
c_init = tf.zeros_initializer()
self.a = tf.Variable(name = "kernel", initial_value = a_init(shape= (input_shape[-1], self.units),
dtype= "float32"), trainable = True)
self.b = tf.Variable(name = "kernel", initial_value = b_init(shape= (input_shape[-1], self.units),
dtype= "float32"), trainable = True)
self.c = tf.Variable(name = "bias", initial_value = c_init(shape= (self.units,),
dtype= "float32"), trainable = True)
def call(self, inputs):
'''Defines the computation from inputs to outputs'''
# YOUR CODE HERE
result = tf.matmul(tf.math.square(inputs), self.a) + tf.matmul(inputs, self.b) + self.c
return self.activation(result)
You can treat any model as if it were a layer by invoking it on an
Input
or on the output of another layer. By calling a model you aren't just reusing the architecture of the model, you're also reusing its weights
同样值得注意的是,model的子类也可以像layer那样使用functional API来调用,比如:
1 | encoder_input = keras.Input(shape=(28, 28, 1), name="original_img") |
我们以sub-classing的方式定义的model是没有办法调用summary来看模型架构的,作者也给出了解决方案:github comments
方法就是在Model的子类中添加build_graph方法: 1
2
3def build_graph(self, raw_shape):
x = tf.keras.layers.Input(shape=raw_shape)
return Model(inputs=[x], outputs=self.call(x))1
2
3
4
5
6
7
8cm.build_graph(raw_input).summary()
# 不仅如此还能使用tf.keras.utils.plot_model来生成png
tf.keras.utils.plot_model(
model.build_graph(raw_input), # here is the trick (for now)
to_file='model.png', dpi=96, # saving
show_shapes=True, show_layer_names=True, # show shapes and layer name
expand_nested=False # will show nested block
)
作者同样推荐了一篇博客讲tensorflow中保存模型的各种方式:博客地址.非常推荐阅读
总结一下就是:
- 对于Functional API创建的模型,最好的保存模型和导入模型的方式是:
1 | model.save('path_to_my_model.h5') |
以上方式会将模型的架构,weights以及训练过程中的设定(也就是model.compile())的内容全部保存。
- 对于sub class创建的模型,推荐的方式是用save_weights
1 | model.save_weights('path_to_my_weights', save_format='tf') |
如果想要加载weights,必须要知道原来用sub class建立模型的code。不仅如此,还需要用原来的code先build起模型,让模型知道输入tensor的shape以及dtype,如果没有build这一步程序将会报错。
1 | new_model = MiniInception() |
tf.function
在我们定义custum training 过程中时我们经常会用到这个装饰器@tf.function
1 |
|
先看如果一个函数不加这个装饰器会如何:
1 | def f(x): |
输出为:
1 | Traced with 2 |
加上装饰器:
1 |
|
输出为:
1 | Traced with 2 |
可以看到第二种加了装饰器的方式,即便是循环了5遍,我们仍然只有一行打印了2.
如果我们在上面的代码中print之前加上一行:
1 |
|
程序的输出就变成了:
1 | Traced with 2 |
可以看到tf.print
就可以正常按loop
运行。注意一点:
被tf.function
装饰的函数只能包含operations
而不能定义variable
比如tf.Variable()