前言:由于Tensorflow2.0整合了Keras,因此在Tensorflow 2.0中可以直接通过使用tf.keras下的API建立Sequential API model,Function API model 和subclass model
由于Tensorflow 目前还在完善中,本文目前仅关注Tensorflow2.3以前的版本。
先写结论:个人推荐使用Functional API建立模型,无论是不是新学习Tensorflow和Keras,直接上手Functional API可以少走不少弯路,尽管一开始上手可能会有很多地方不理解,只能多学多看源代码了。
====================================================================
Sequential API
根据官方文档,编写方式如下:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# Define Sequential model with 3 layers
model = keras.Sequential([layers.Dense(2, activation="relu", name="layer1"),layers.Dense(3, activation="relu", name="layer2"),layers.Dense(4, name="layer3"),]
)
# Call model on a test input
x = tf.ones((3, 3))
y = model(x)
第1 层的输出节点数设计为2,第2 层设计为3,输出层节点数设计为4,激活函数为relu。直接调用这个模型对象model(x)就可以返回模型最后一层的输出?。
可以看出Sequential最大的优点就是简单,快速,易理解,适合简单的模型,所有的层封装在一起调用model()
就可以输出结果,适合新学习Tensorflow和Keras的人,但是缺点也很明显,它无法自定义输入和输出模式,这样一来如果是多输入或者多输出模式,Sequential API就不适合了。这时候就需要使用Functional API来解决这个问题。
====================================================================
Functional API
根据官方文档,编写方式如下:
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layersencoder_input = keras.Input(shape=(28, 28, 1), name="img")
x = layers.Conv2D(16, 3, activation="relu")(encoder_input)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.Conv2D(16, 3, activation="relu")(x)
encoder_output = layers.GlobalMaxPooling2D()(x)encoder = keras.Model(encoder_input, encoder_output, name="encoder")
encoder.summary()
可以看出,Functional API最开始使用了keras.Input
来定义输入,之后的每一层layers可以当作一个函数,然后以输入的数据作为函数的输入,例如:
''' layers是keras的函数,对由多个输入平面组成的输入信号进行二维卷积 encoder_input是上一层keras.Input的输出 layers.Conv2D作为函数被调用,encoder_input是传入的参数 '''
x = layers.Conv2D(16, 3, activation="relu")(encoder_input)
样例代码中keras.Model()
定义了输入encoder_input
和输出encoder_output
,所以它可以适配多输入和多输出模式,缺点是变换层结构之后需要重新定义每一层的输入和输出,相比Sequential API,Functional API的结构显然比较麻烦,但是它灵活性更强,可以适配更多的模型。也有人说Sequential API是Functional API的特例,这话真是太形象了。
Sequential API和Functional API的共同优点是,它们可以轻松保存,克隆,和共享模型,它们的模型结构也很清晰并且容易分析,框架可以推断形状和检查类型,可以更早发现错误。由于它们都是静态图,因此调用也非常简单,但是缺点也是因为它们是静态图,因此在涉及循环、变化的形状、条件分支和其他动态行为时就容易使代码变得难以阅读。Subclass Model的设计初衷可能就是出于希望可以解决这个问题。
====================================================================
Subclass Model
根据官方文档,编写样例如下:
from tensorflow.keras import layersclass Sampling(layers.Layer):"""Uses (z_mean, z_log_var) to sample z, the vector encoding a digit."""def call(self, inputs):z_mean, z_log_var = inputsbatch = tf.shape(z_mean)[0]dim = tf.shape(z_mean)[1]epsilon = tf.keras.backend.random_normal(shape=(batch, dim))return z_mean + tf.exp(0.5 * z_log_var) * epsilonclass Encoder(layers.Layer):"""Maps MNIST digits to a triplet (z_mean, z_log_var, z)."""def __init__(self, latent_dim=32, intermediate_dim=64, name="encoder", **kwargs):super(Encoder, self).__init__(name=name, **kwargs)self.dense_proj = layers.Dense(intermediate_dim, activation="relu")self.dense_mean = layers.Dense(latent_dim)self.dense_log_var = layers.Dense(latent_dim)self.sampling = Sampling()def call(self, inputs):x = self.dense_proj(inputs)z_mean = self.dense_mean(x)z_log_var = self.dense_log_var(x)z = self.sampling((z_mean, z_log_var))return z_mean, z_log_var, zclass Decoder(layers.Layer):"""Converts z, the encoded digit vector, back into a readable digit."""def __init__(self, original_dim, intermediate_dim=64, name="decoder", **kwargs):super(Decoder, self).__init__(name=name, **kwargs)self.dense_proj = layers.Dense(intermediate_dim, activation="relu")self.dense_output = layers.Dense(original_dim, activation="sigmoid")def call(self, inputs):x = self.dense_proj(inputs)return self.dense_output(x)class VariationalAutoEncoder(keras.Model):"""Combines the encoder and decoder into an end-to-end model for training."""def __init__(self,original_dim,intermediate_dim=64,latent_dim=32,name="autoencoder",**kwargs):super(VariationalAutoEncoder, self).__init__(name=name, **kwargs)self.original_dim = original_dimself.encoder = Encoder(latent_dim=latent_dim, intermediate_dim=intermediate_dim)self.decoder = Decoder(original_dim, intermediate_dim=intermediate_dim)def call(self, inputs):z_mean, z_log_var, z = self.encoder(inputs)reconstructed = self.decoder(z)# Add KL divergence regularization loss.kl_loss = -0.5 * tf.reduce_mean(z_log_var - tf.square(z_mean) - tf.exp(z_log_var) + 1)self.add_loss(kl_loss)return reconstructed
Subclass model的具体编写模式可以去看官方文档,这里就不展开了。Subclass Model的优点如前文所述,灵活,代码结构清晰,但是缺点也很明显,由于模型架构隐藏在call()方法中,Keras无法对其进行检查,它无法被保存或克隆,调用summary()方法时只能得到一个图层列表而无法显示层连接信息,更容易出错。
由于它无法被保存或者克隆,这在部署模型的时候是致命的,虽然可以通过@tf.function
的signature
参数进行输入数据的定义并且保存为PB模式,但是我在调试过程中遇到了很多问题也找了很多资料,实在是非常麻烦,因此不推荐使用Subclass Model。
推荐阅读:《Hands-On Machine Learning with Scikit-Learn, Keras & Tensorflow》- Aurélien Géron
注:由于该书最新版是基于2.0,而Tensorflow2目前已经更新至2.4,因此很多部分还是需要自己实战了才知道。该书比较适合那些对python,tensorflow和keras有一定理解的朋友。