本笔记参照TensorFlow官方教程,主要是对‘Writing custom layers and models with Keras’教程内容翻译和内容结构编排,原文链接:Writing custom layers and models with Keras
目录
创建环境(setup)
一、层类
1.1 层封装一个状态(权重)和一些计算
1.2 最佳实践:延迟创建权重,直到知道输入的形状
1.3 层是递归可组合的
1.4 层递归地收集在前向传递过程中产生的代价
1.5 我们可以选择性地将层序列化
1.6 在call方法中给与训练参数特权
二、创建模型
2.1 模型类
2.2 整合所有:一个端到端示例
2.3超越面向对象开发: Functional API
创建环境(Setup)
from __future__ import absolute_import, division, print_function, unicode_literals
try:
# %tensorflow_version only exists in Colab.
%tensorflow_version 2.x
except Exception:
pass
import tensorflow as tf
tf.keras.backend.clear_session() # For easy reset of notebook state.
一、层类
1.1 层封装一个状态(权重)和一些计算
我们主要处理得数据结构就是:‘层’(layer)。一个层同时将‘状态’(层的权重)和从输入到输出的转换(调用,层的前向传递)封装了一起。下面是一个紧密相连的‘densely-connected’层,它的状态有:变量w和b。
from tensorflow.keras import layers
class Linear(layers.Layer):
def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()
w_init = tf.random_normal_initializer()
self.w = tf.Variable(initial_value=w_init(shape=(input_dim, units),
dtype='float32'),
trainable=True)
b_init = tf.zeros_initializer()
self.b = tf.Variable(initial_value=b_init(shape=(units,),
dtype='float32'),
trainable=True)
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b
x = tf.ones((2, 2))
linear_layer = Linear(4, 2)
y = linear_layer(x)
print(y)
注意,权值w和b被设置为层属性后,层会自动跟踪:
assert linear_layer.weights == [linear_layer.w, linear_layer.b]
我们也可以使用‘add_weight’方法快速地给层加权重:
class Linear(layers.Layer):
def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()
self.w = self.add_weight(shape=(input_dim, units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(units,),
initializer='zeros',
trainable=True)
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b
x = tf.ones((2, 2))
linear_layer = Linear(4, 2)
y = linear_layer(x)
print(y)
1.1.2层也可以拥有不可训练的权重
除了可训练的权重,我们也可以添加不可训练的权重到一个层中。当你在训练层的时候,这样的权重值在反向传播过程中是不被考虑的。
下面是展示如何添加和使用不可训练的权重实例:
class ComputeSum(layers.Layer):
def __init__(self, input_dim):
super(ComputeSum, self).__init__()
self.total = tf.Variable(initial_value=tf.zeros((input_dim,)),
trainable=False)
def call(self, inputs):
self.total.assign_add(tf.reduce_sum(inputs, axis=0))
return self.total
x = tf.ones((2, 2))
my_sum = ComputeSum(2)
y = my_sum(x)
print(y.numpy())
y = my_sum(x)
print(y.numpy())
它属于‘layer.weights’,但它归属于不可训练权重:
print('weights:', len(my_sum.weights))
print('non-trainable weights:', len(my_sum.non_trainable_weights))
# It's not included in the trainable weights:
print('trainable_weights:', my_sum.trainable_weights)
1.2 最佳实践:延迟创建权重,直到知道输入的形状
在上面的逻辑回归实例里,我们的线性层使用了‘input_dim’参数,这个参数在‘init’中是用来计算权重w和b的形状的:
class Linear(layers.Layer):
def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()
self.w = self.add_weight(shape=(input_dim, units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(units,),
initializer='zeros',
trainable=True)
在许多场景中,我们也许不能提前知道输入的大小, 并且在实例化层之后的一段时间内,希望在知道该值时惰性地创建权重。在Keras API,建议在层的‘build(inputs_shape)’方法中创建层权重,像这样:
class Linear(layers.Layer):
def __init__(self, units=32):
super(Linear, self).__init__()
self.units = units
def build(self, input_shape):
self.w = self.add_weight(shape=(input_shape[-1], self.units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(self.units,),
initializer='random_normal',
trainable=True)
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b
定制的层的_call__方法将在第一次调用时自动运行build。现在有一个方便有好使的层使用了:
linear_layer = Linear(32) # At instantiation, we don't know on what inputs this is going to get called
y = linear_layer(x) # The layer's weights are created dynamically the first time the layer is called
1.3 层是递归可组合的
如果将一个层实例赋值为另一个层的属性,则外层将开始跟踪内层的权重。我们建议在_init__方法中创建这样的子层(因为子层通常有一个构建方法,它们将在构建外层时构建)。
# Let's assume we are reusing the Linear class
# with a `build` method that we defined above.
class MLPBlock(layers.Layer):
def __init__(self):
super(MLPBlock, self).__init__()
self.linear_1 = Linear(32)
self.linear_2 = Linear(32)
self.linear_3 = Linear(1)
def call(self, inputs):
x = self.linear_1(inputs)
x = tf.nn.relu(x)
x = self.linear_2(x)
x = tf.nn.relu(x)
return self.linear_3(x)
mlp = MLPBlock()
y = mlp(tf.ones(shape=(3, 64))) # The first call to the `mlp` will create the weights
print('weights:', len(mlp.weights))
print('trainable weights:', len(mlp.trainable_weights))
1.4 层递归地收集在前向传递过程中产生的代价
在编写一个层的‘调用’(call)方法时,我们可以创建代价张量,并稍后在编写训练循环时使用这些张量。这可以通过调用self.add_loss(value)来实现:
# A layer that creates an activity regularization loss
class ActivityRegularizationLayer(layers.Layer):
def __init__(self, rate=1e-2):
super(ActivityRegularizationLayer, self).__init__()
self.rate = rate
def call(self, inputs):
self.add_loss(self.rate * tf.reduce_sum(inputs))
return inputs
这些代价(包括任何内层所造成的损失)可以通过layer.losses来恢复。此属性将在每个到顶层的‘call’调用开始时重置,因此该layer.losses总是包含在最后一次前向传递时创建的代价值。
class OuterLayer(layers.Layer):
def __init__(self):
super(OuterLayer, self).__init__()
self.activity_reg = ActivityRegularizationLayer(1e-2)
def call(self, inputs):
return self.activity_reg(inputs)
layer = OuterLayer()
assert len(layer.losses) == 0 # No losses yet since the layer has never been called
_ = layer(tf.zeros(1, 1))
assert len(layer.losses) == 1 # We created one loss value
# `layer.losses` gets reset at the start of each __call__
_ = layer(tf.zeros(1, 1))
assert len(layer.losses) == 1 # This is the loss created during the call above
另外,代价属性也包含由任何内层权重创建的正则化代价:
class OuterLayer(layers.Layer):
def __init__(self):
super(OuterLayer, self).__init__()
self.dense = layers.Dense(32, kernel_regularizer=tf.keras.regularizers.l2(1e-3))
def call(self, inputs):
return self.dense(inputs)
layer = OuterLayer()
_ = layer(tf.zeros((1, 1)))
# This is `1e-3 * sum(layer.dense.kernel ** 2)`,
# created by the `kernel_regularizer` above.
print(layer.losses)
在创建训练循环时,这些代价是要考虑在内的,像这样:
# Instantiate an optimizer.
optimizer = tf.keras.optimizers.SGD(learning_rate=1e-3)
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
# Iterate over the batches of a dataset.
for x_batch_train, y_batch_train in train_dataset:
with tf.GradientTape() as tape:
logits = layer(x_batch_train) # Logits for this minibatch
# Loss value for this minibatch
loss_value = loss_fn(y_batch_train, logits)
# Add extra losses created during this forward pass:
loss_value += sum(model.losses)
grads = tape.gradient(loss_value, model.trainable_weights)
optimizer.apply_gradients(zip(grads, model.trainable_weights))
1.5 我们可以选择性地将层序列化
如果想把定制的层序列化为功能模型的一部分,我们可以选择性地执行‘get_config’方法:
class Linear(layers.Layer):
def __init__(self, units=32):
super(Linear, self).__init__()
self.units = units
def build(self, input_shape):
self.w = self.add_weight(shape=(input_shape[-1], self.units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(self.units,),
initializer='random_normal',
trainable=True)
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b
def get_config(self):
return {'units': self.units}
# Now you can recreate the layer from its config:
layer = Linear(64)
config = layer.get_config()
print(config)
new_layer = Linear.from_config(config)
注意,基层类的_init__方法接受一些关键字参数,特别是‘name’和’dtype’。在_init__中将这些参数传递给父类并将它们包含在层配置中是一个很好的做法:
class Linear(layers.Layer):
def __init__(self, units=32, **kwargs):
super(Linear, self).__init__(**kwargs)
self.units = units
def build(self, input_shape):
self.w = self.add_weight(shape=(input_shape[-1], self.units),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(self.units,),
initializer='random_normal',
trainable=True)
def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b
def get_config(self):
config = super(Linear, self).get_config()
config.update({'units': self.units})
return config
layer = Linear(64)
config = layer.get_config()
print(config)
new_layer = Linear.from_config(config)
如果在从层的配置反序列化层时需要更大的灵活性,我们可以重写from_config类方法。这是‘from_config’的基本实现:
def from_config(cls, config):
return cls(**config)
1.6 在call方法中给与训练参数特权
在一些层中,特别是‘BatchNormalization’层和‘Dropout’层,在训练和推断过程中,有不同的行为。对于这样的层,标准的做法是在调用方法中公开一个训练(布尔)参数。
通过在call中暴露这个参数,您可以使内置的训练和评估循环(例如fit)在训练和推断中正确使用该层。
class CustomDropout(layers.Layer):
def __init__(self, rate, **kwargs):
super(CustomDropout, self).__init__(**kwargs)
self.rate = rate
def call(self, inputs, training=None):
if training:
return tf.nn.dropout(inputs, rate=self.rate)
return inputs
二、创建模型
2.1模型类
通常,我们可以使用Layer类来定义内部计算快,并使用Model类来定义外部模型—我们将训练的对象。
例如,在一个ResNet50模型中,您将有几个ResNet块的子类化层,以及一个包含整个ResNet50网络的模型。
Model类和Layer有同样的API,下面是它们之间的不同:
(1)它公开了内置的训练、评估和预测循环(model.fit()、model.evaluate()、model.predict())。
(2)它通过‘model.layers’属性,公开其内层列表
(3)它公开保存和序列化API
实际上,“层”类对应于我们在文献中提到的“层”(如“卷积层”或“递归层”)或“块”(如“ResNet块”或“Inception块”)。
同时,“Model”类对应于文献中所称的“Model”(如“deep learning Model”)或“network”(如“deep neural network”)。
例如,我们可以使用上面的mini-resnet示例,并使用它来构建一个模型,我们可以使用fit()进行训练,还可以使用save_weights进行保存:
class ResNet(tf.keras.Model):
def __init__(self):
super(ResNet, self).__init__()
self.block_1 = ResNetBlock()
self.block_2 = ResNetBlock()
self.global_pool = layers.GlobalAveragePooling2D()
self.classifier = Dense(num_classes)
def call(self, inputs):
x = self.block_1(inputs)
x = self.block_2(x)
x = self.global_pool(x)
return self.classifier(x)
resnet = ResNet()
dataset = ...
resnet.fit(dataset, epochs=10)
resnet.save_weights(filepath)
2.2 整合所有:一个端到端示例
下面是我们已经学的知识:
(1)层封装了状态(在‘init’或者‘build’中创建)和一些运算(在‘call’)
(2)层可以递归嵌套以创建新的、更大的计算快。
(3)层可以创建并追踪代价(典型地如:regularization losses)
(4)外部容器,你想要训练的东西,是一个模型。模型就像一个层,但是添加了培训和序列化实用程序。
我们把上面所说的东西整合进一个端到端的例子中:执行一个VariationalAutoEncoder(VAE)。我们将在MNIST数字集中训练它。
我们的VAE将会是模型的一个子类,它被构建为一个嵌套的层的组合,而这些层又构成了这个子类。它将以正则化损失(KL散度)为特征。
class 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 = inputs
batch = 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) * epsilon
class 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, z
class 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(tf.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_dim
self.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
original_dim = 784
vae = VariationalAutoEncoder(original_dim, 64, 32)
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
mse_loss_fn = tf.keras.losses.MeanSquaredError()
loss_metric = tf.keras.metrics.Mean()
(x_train, _), _ = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype('float32') / 255
train_dataset = tf.data.Dataset.from_tensor_slices(x_train)
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
# Iterate over epochs.
for epoch in range(3):
print('Start of epoch %d' % (epoch,))
# Iterate over the batches of the dataset.
for step, x_batch_train in enumerate(train_dataset):
with tf.GradientTape() as tape:
reconstructed = vae(x_batch_train)
# Compute reconstruction loss
loss = mse_loss_fn(x_batch_train, reconstructed)
loss += sum(vae.losses) # Add KLD regularization loss
grads = tape.gradient(loss, vae.trainable_weights)
optimizer.apply_gradients(zip(grads, vae.trainable_weights))
loss_metric(loss)
if step % 100 == 0:
print('step %s: mean loss = %s' % (step, loss_metric.result()))
注意,因为VAE是子类化模型,所以它具有内置的训练循环。所以你也可以这样训练它:
vae = VariationalAutoEncoder(784, 64, 32)
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())
vae.fit(x_train, x_train, epochs=3, batch_size=64)
2.3超越面向对象开发: Functional API
上面这个例子是不是太过于面向对象的开发了?我们还可以使用Functional API构建模型。重要的是,选择一种或另一种样式并不会阻止您利用以另一种样式编写的组件:您总是可以混合使用。
例如,下面的‘Functional API’示例重用了我们在上面的示例中定义的采样层。
original_dim = 784
intermediate_dim = 64
latent_dim = 32
# Define encoder model.
original_inputs = tf.keras.Input(shape=(original_dim,), name='encoder_input')
x = layers.Dense(intermediate_dim, activation='relu')(original_inputs)
z_mean = layers.Dense(latent_dim, name='z_mean')(x)
z_log_var = layers.Dense(latent_dim, name='z_log_var')(x)
z = Sampling()((z_mean, z_log_var))
encoder = tf.keras.Model(inputs=original_inputs, outputs=z, name='encoder')
# Define decoder model.
latent_inputs = tf.keras.Input(shape=(latent_dim,), name='z_sampling')
x = layers.Dense(intermediate_dim, activation='relu')(latent_inputs)
outputs = layers.Dense(original_dim, activation='sigmoid')(x)
decoder = tf.keras.Model(inputs=latent_inputs, outputs=outputs, name='decoder')
# Define VAE model.
outputs = decoder(z)
vae = tf.keras.Model(inputs=original_inputs, outputs=outputs, name='vae')
# 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)
vae.add_loss(kl_loss)
# Train.
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
vae.compile(optimizer, loss=tf.keras.losses.MeanSquaredError())
vae.fit(x_train, x_train, epochs=3, batch_size=64)
本文详细介绍如何使用Keras API定制神经网络层和模型,包括层的创建、权重管理、模型构建及序列化等核心概念。并通过Variational AutoEncoder(VAE)实例,展示了层和模型的递归组合与训练过程。
1万+

被折叠的 条评论
为什么被折叠?



