Keras Has Been With Me
Looking back at the two or three years since I entered the field of machine learning, Keras has always been by my side. If I hadn’t encountered such an easy-to-use framework when I first fell into this pit, which allowed me to quickly implement my ideas, I am not sure if I would have had the perseverance to stick with it. After all, back then, the world belonged to Theano, Pylearn, Caffe, and Torch, which even today remain as incomprehensible as hieroglyphics to me.
Later, to broaden my horizons, I also spent some time learning TensorFlow and wrote several programs in pure TensorFlow. However, no matter what, I still couldn’t let go of Keras. As my understanding of Keras deepened, especially after spending some time studying the Keras source code, I found that Keras is not as “inflexible” as people often complain. In fact, the ingenious encapsulation of Keras allows us to easily implement many complex functions. I increasingly feel that Keras is like a very exquisite work of art, fully reflecting the profound creative skills of the Keras developers.
This article introduces some content regarding custom models in Keras. Relatively speaking, this belongs to the advanced content of Keras; friends who have just started should temporarily ignore it.
Customizing Layers
Here I will introduce custom layers in Keras and some application techniques. Through these, we can see the ingenuity of Keras layers.
Basic Definition Method
In Keras, the simplest way to customize a layer is through the
Lambda layer:
from keras.layers import *
from keras import backend as K
x_in = Input(shape=(10,))
x = Lambda(lambda x: x+2)(x_in) # Add 2 to the input
Sometimes, we want to distinguish between the training phase and the
testing phase. For example, adding some noise to the input during the
training phase and removing it during the testing phase. This can be
achieved using K.in_train_phase, for example:
def add_noise_in_train(x):
x_ = x + K.random_normal(shape=K.shape(x)) # Add standard Gaussian noise
return K.in_train_phase(x_, x)
x_in = Input(shape=(10,))
x = Lambda(add_noise_in_train)(x_in) # Add Gaussian noise during training, remove during testing
Of course, the Lambda layer is only suitable for
situations where no additional trainable parameters are needed. If the
functionality you want to implement requires adding new parameters to
the model, you must use a custom Layer. This is actually
not complicated; it just takes a few more lines of code than a
Lambda layer. The official documentation explains it very
clearly: https://keras.io/layers/writing-your-own-keras-layers/
Here is the example from that page:
class MyLayer(Layer):
def __init__(self, output_dim, **kwargs):
self.output_dim = output_dim # Can define custom attributes for easy calling
super(MyLayer, self).__init__(**kwargs) # Required
def build(self, input_shape):
# Add trainable weights
self.kernel = self.add_weight(name='kernel',
shape=(input_shape[1], self.output_dim),
initializer='uniform',
trainable=True)
def call(self, x):
# Define functionality, equivalent to the function in a Lambda layer
return K.dot(x, self.kernel)
def compute_output_shape(self, input_shape):
# Calculate output shape. If input and output shapes are the same, this can be omitted, but it's better to include it.
return (input_shape[0], self.output_dim)
Layers with Multiple Outputs
Almost all layers we encounter daily are single-output, including all
built-in layers in Keras, which take one or more inputs and return a
single output result. So, can Keras define a layer with multiple
outputs? The answer is yes, but you must clearly define the
output_shape. For example, the following layer simply
splits the input into two halves and returns both simultaneously.
class SplitVector(Layer):
def __init__(self, **kwargs):
super(SplitVector, self).__init__(**kwargs)
def call(self, inputs):
# Slice the tensor along the second dimension, returning a list
in_dim = K.int_shape(inputs)[-1]
return [inputs[:, :in_dim//2], inputs[:, in_dim//2:]]
def compute_output_shape(self, input_shape):
# output_shape must also be a corresponding list
in_dim = input_shape[-1]
return [(None, in_dim//2), (None, in_dim-in_dim//2)]
x1, x2 = SplitVector()(x_in) # Usage
Combining Layers with Loss
Readers with experience from the article “Customizing Complex Loss
Functions in Keras” know that the basic definition of a loss in
Keras is a function with inputs y_true and
y_pred. However, in more complex cases, it is not just a
function of predicted and target values; it can also be combined with
weights for complex calculations.
Here, we take Center Loss as an example again to introduce a writing method based on custom layers.
class Dense_with_Center_loss(Layer):
def __init__(self, output_dim, **kwargs):
self.output_dim = output_dim
super(Dense_with_Center_loss, self).__init__(**kwargs)
def build(self, input_shape):
# Add trainable parameters
self.kernel = self.add_weight(name='kernel',
shape=(input_shape[1], self.output_dim),
initializer='glorot_normal',
trainable=True)
self.bias = self.add_weight(name='bias',
shape=(self.output_dim,),
initializer='zeros',
trainable=True)
self.centers = self.add_weight(name='centers',
shape=(self.output_dim, input_shape[1]),
initializer='glorot_normal',
trainable=True)
def call(self, inputs):
# For center loss, the return result is still consistent with Dense
# So it's just ordinary matrix multiplication plus bias
self.inputs = inputs
return K.dot(inputs, self.kernel) + self.bias
def compute_output_shape(self, input_shape):
return (input_shape[0], self.output_dim)
def loss(self, y_true, y_pred, lamb=0.5):
# Define the complete loss
y_true = K.cast(y_true, 'int32') # Ensure y_true dtype is int32
crossentropy = K.sparse_categorical_crossentropy(y_true, y_pred, from_logits=True)
centers = K.gather(self.centers, y_true[:, 0]) # Retrieve sample centers
center_loss = K.sum(K.square(centers - self.inputs), axis=1) # Calculate center loss
return crossentropy + lamb * center_loss
f_size = 2
x_in = Input(shape=(784,))
f = Dense(f_size)(x_in)
dense_center = Dense_with_Center_loss(10)
output = dense_center(f)
model = Model(x_in, output)
model.compile(loss=dense_center.loss,
optimizer='adam',
metrics=['sparse_categorical_accuracy'])
# Here y_train is the integer ID of the category, no need to convert to one-hot
model.fit(x_train, y_train, epochs=10)
Fancy Callbacks
In addition to modifying the model, we might also do many things during the training process, such as calculating validation set metrics after each epoch, saving the best model, reducing the learning rate after a certain number of epochs, or modifying regularization parameters, etc. All of these can be implemented through callbacks.
Official Callbacks page: https://keras.io/callbacks/
Saving the Best Model
In Keras, the easiest way to keep the best model based on validation
set metrics is through the built-in ModelCheckpoint, for
example:
checkpoint = ModelCheckpoint(filepath='./best_model.weights',
monitor='val_acc',
verbose=1,
save_best_only=True)
model.fit(x_train,
y_train,
epochs=10,
validation_data=(x_test, y_test),
callbacks=[checkpoint])
However, while this method is simple, it has a significant drawback:
the metrics inside are determined by the metrics in
compile. In Keras, defining a custom metric requires
writing it as a tensor operation. That is to say, if the metric you
expect cannot be written as a tensor operation (such as BLEU and other
metrics), then it cannot be written as a metric function, and this
solution cannot be used.
Thus, a universal solution emerges: write your own callback and calculate whatever you want. For example:
from keras.callbacks import Callback
def evaluate(): # Evaluation function
pred = model.predict(x_test)
return np.mean(pred.argmax(axis=1) == y_test) # Calculate anything you want
# Define Callback to calculate validation acc and save the best model
class Evaluate(Callback):
def __init__(self):
self.accs = []
self.highest = 0.
def on_epoch_end(self, epoch, logs=None):
acc = evaluate()
self.accs.append(acc)
if acc >= self.highest: # Save best model weights
self.highest = acc
model.save_weights('best_model.weights')
# Run anything you want
print('acc: %s, highest: %s' % (acc, self.highest))
evaluator = Evaluate()
model.fit(x_train,
y_train,
epochs=10,
callbacks=[evaluator])
Modifying Hyperparameters
During the training process, it is also possible to fine-tune
hyperparameters. For example, a common requirement is to adjust the
learning rate based on the epoch. This can be easily achieved through
LearningRateScheduler, which is also one of the
callbacks.
from keras.callbacks import LearningRateScheduler
def lr_schedule(epoch):
# Return different learning rates based on the epoch
if epoch < 50:
lr = 1e-2
elif epoch < 80:
lr = 1e-3
else:
lr = 1e-4
return lr
lr_scheduler = LearningRateScheduler(lr_schedule)
model.fit(x_train,
y_train,
epochs=10,
callbacks=[evaluator, lr_scheduler])
What about other hyperparameters? For example, the lamb
in the previous center loss, or similar regularization terms. In this
case, we need to set lamb as a Variable and
then customize a callback to dynamically assign values. For example, a
loss I once defined:
def mycrossentropy(y_true, y_pred, e=0.1):
loss1 = K.categorical_crossentropy(y_true, y_pred)
loss2 = K.categorical_crossentropy(K.ones_like(y_pred)/nb_classes, y_pred)
return (1-e)*loss1 + e*loss2
To dynamically change the parameter e, it can be
modified to:
e = K.variable(0.1)
def mycrossentropy(y_true, y_pred):
loss1 = K.categorical_crossentropy(y_true, y_pred)
loss2 = K.categorical_crossentropy(K.ones_like(y_pred)/nb_classes, y_pred)
return (1-e)*loss1 + e*loss2
model.compile(loss=mycrossentropy,
optimizer='adam')
class callback4e(Callback):
def __init__(self, e):
self.e = e
def on_epoch_end(self, epoch, logs={}):
if epoch >= 100: # Set to 0.01 after 100 epochs
K.set_value(self.e, 0.01)
model.fit(x_train,
y_train,
epochs=10,
callbacks=[callback4e(e)])
Note that the Callback class supports six execution
functions at different stages: on_epoch_begin,
on_epoch_end, on_batch_begin,
on_batch_end, on_train_begin, and
on_train_end. Each function executes at a different stage
(easily judged by the name), and they can be combined to implement very
complex functions. For example, warmup refers to
setting a default learning rate but not using it at the very beginning
of training. Instead, in the first few epochs, the learning rate is
slowly increased from zero to the default learning rate. This process
can be understood as adjusting the model for better initialization.
Reference code:
class Evaluate(Callback):
def __init__(self):
self.num_passed_batchs = 0
self.warmup_epochs = 10
def on_batch_begin(self, batch, logs=None):
# params are parameters automatically passed to the Callback by the model
if self.params['steps'] == None:
self.steps_per_epoch = np.ceil(1. * self.params['samples'] / self.params['batch_size'])
else:
self.steps_per_epoch = self.params['steps']
if self.num_passed_batchs < self.steps_per_epoch * self.warmup_epochs:
# In the first 10 epochs, the learning rate increases linearly from zero to 0.001
K.set_value(self.model.optimizer.lr,
0.001 * (self.num_passed_batchs + 1) / self.steps_per_epoch / self.warmup_epochs)
self.num_passed_batchs += 1
Infinite Possibilities of Keras
Keras has many other noteworthy techniques, such as directly using
model.add_loss to flexibly add losses, nested model calls,
acting purely as a simple high-level API for TensorFlow, and so on. I
won’t list them all here. Readers with questions or interest are welcome
to leave a message for discussion.
It is generally believed that highly encapsulated libraries like
Keras lack flexibility, but this is actually not the case. You should
know that Keras does not simply call existing high-level functions in
TensorFlow or Theano; instead, it only encapsulates some basic functions
through the backend and then rewrites everything (various
layers, optimizers, etc.) using its own backend! It is precisely because
of this that it can support switching between different backends.
Being able to achieve this level of detail, the flexibility of Keras is beyond doubt. However, this flexibility is difficult to reflect in help documents and ordinary cases. Often, you have to read the source code to feel that the Keras way of writing is already impeccable. I feel that implementing complex models with Keras is both a challenge and a kind of artistic creation. When you succeed, you will be intoxicated by the work of art you have created.
Original Address: https://kexue.fm/archives/5765
For more details on reposting, please refer to: Scientific Space FAQ