Skip to main content

TensorFlow Custom Layers

TensorFlow's modular architecture allows you to build neural networks by stacking layers. While the framework provides many standard layers like Dense, Conv2D, and LSTM, there are situations where you might need custom functionality. This is where custom layers come in handy.

Introduction to Custom Layers

Custom layers in TensorFlow allow you to define your own operations that can be integrated seamlessly into a neural network. You might need custom layers for:

  1. Implementing novel research ideas
  2. Creating specialized operations not available in standard layers
  3. Optimizing specific parts of your network for performance
  4. Encapsulating repeated patterns of layers into reusable components

In this tutorial, we'll learn how to create and use custom layers in TensorFlow.

Building Custom Layers: The Basics

To create a custom layer in TensorFlow, you need to subclass tf.keras.layers.Layer and implement at least two methods:

  1. __init__: Initialize your layer's attributes
  2. call: Define the computation performed by the layer

Here's a simple example of a custom layer:

python
import tensorflow as tf

class MySimpleLayer(tf.keras.layers.Layer):
def __init__(self, units=32, **kwargs):
super(MySimpleLayer, self).__init__(**kwargs)
self.units = units

def build(self, input_shape):
# Create trainable weights
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='zeros',
trainable=True,
)

def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b

This simple layer is similar to a Dense layer. Let's see how to use it:

python
# Create a model with our custom layer
model = tf.keras.Sequential([
MySimpleLayer(64),
tf.keras.layers.Activation('relu'),
MySimpleLayer(10),
tf.keras.layers.Activation('softmax')
])

# Compile the model
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])

# Print model summary
model.build(input_shape=(None, 28*28))
model.summary()

Output:

Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
my_simple_layer (MySimpleLay (None, 64) 50240
_________________________________________________________________
activation (Activation) (None, 64) 0
_________________________________________________________________
my_simple_layer_1 (MySimpleL (None, 10) 650
_________________________________________________________________
activation_1 (Activation) (None, 10) 0
=================================================================
Total params: 50,890
Trainable params: 50,890
Non-trainable params: 0
_________________________________________________________________

Managing Layer Weights and State

The build method is where you define the weights of your layer. It is called once when the layer receives its first input, which allows you to create weights with shapes that depend on the input shape.

Using add_weight

The add_weight method is used to create trainable weights for your layer:

python
self.w = self.add_weight(
name='weights',
shape=(input_shape[-1], self.units),
initializer='random_normal',
trainable=True,
regularizer=tf.keras.regularizers.l2(0.01) # Optional regularization
)

Implementing Custom Behavior

The real power of custom layers is the ability to implement arbitrary computations. Let's create a more interesting custom layer that applies a learnable polynomial transformation:

python
class PolynomialLayer(tf.keras.layers.Layer):
def __init__(self, degree=2, **kwargs):
super(PolynomialLayer, self).__init__(**kwargs)
self.degree = degree

def build(self, input_shape):
# Create coefficients for each polynomial term
self.coefficients = []
for i in range(self.degree + 1):
coef = self.add_weight(
name=f'coef_{i}',
shape=(1,),
initializer='random_normal',
trainable=True
)
self.coefficients.append(coef)

def call(self, inputs):
result = 0
for i, coef in enumerate(self.coefficients):
result += coef * tf.pow(inputs, i)
return result

Let's see this in action:

python
# Create a simple dataset
import numpy as np

# Generate some data: y = 2x^2 + 3x + 1 + noise
x = np.linspace(-1, 1, 1000).reshape(-1, 1).astype(np.float32)
y = 2 * np.power(x, 2) + 3 * x + 1 + 0.1 * np.random.randn(*x.shape).astype(np.float32)

# Create and train the model
poly_model = tf.keras.Sequential([
PolynomialLayer(degree=2)
])

poly_model.compile(optimizer='adam', loss='mse')
history = poly_model.fit(x, y, epochs=200, verbose=0)

# Print learned coefficients
for i, coef in enumerate(poly_model.layers[0].coefficients):
print(f"Term x^{i}: {coef.numpy()[0]:.4f}")

Output:

Term x^0: 0.9983
Term x^1: 3.0216
Term x^2: 1.9878

Layers with Training and Inference Behavior

Sometimes you want your layer to behave differently during training and inference. The call method accepts a training argument that you can use for this purpose:

python
class DropConnectDense(tf.keras.layers.Layer):
def __init__(self, units, drop_rate=0.5, **kwargs):
super(DropConnectDense, self).__init__(**kwargs)
self.units = units
self.drop_rate = drop_rate

def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer='glorot_uniform',
trainable=True,
name='weights'
)
self.b = self.add_weight(
shape=(self.units,),
initializer='zeros',
trainable=True,
name='bias'
)

def call(self, inputs, training=None):
if training:
# Apply DropConnect during training
w_masked = self.w * tf.cast(
tf.random.uniform(self.w.shape) > self.drop_rate,
dtype=tf.float32
) / (1 - self.drop_rate)
return tf.matmul(inputs, w_masked) + self.b
else:
# Normal operation during inference
return tf.matmul(inputs, self.w) + self.b

Adding Custom Layers to Models

You can use custom layers in three different ways:

1. In Sequential Models

python
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
MySimpleLayer(128),
tf.keras.layers.Activation('relu'),
DropConnectDense(10, drop_rate=0.3),
tf.keras.layers.Activation('softmax')
])

2. In Functional API

python
inputs = tf.keras.Input(shape=(28, 28))
x = tf.keras.layers.Flatten()(inputs)
x = MySimpleLayer(128)(x)
x = tf.keras.layers.Activation('relu')(x)
outputs = DropConnectDense(10)(x)
outputs = tf.keras.layers.Activation('softmax')(outputs)

model = tf.keras.Model(inputs=inputs, outputs=outputs)

3. In Model Subclassing

python
class MyCustomModel(tf.keras.Model):
def __init__(self):
super(MyCustomModel, self).__init__()
self.flatten = tf.keras.layers.Flatten()
self.my_layer = MySimpleLayer(128)
self.activation = tf.keras.layers.Activation('relu')
self.drop_connect = DropConnectDense(10)
self.output_activation = tf.keras.layers.Activation('softmax')

def call(self, inputs, training=None):
x = self.flatten(inputs)
x = self.my_layer(x)
x = self.activation(x)
x = self.drop_connect(x, training=training)
return self.output_activation(x)

# Create and compile the model
model = MyCustomModel()

Saving and Loading Models with Custom Layers

When you save a model with custom layers, you need to provide the custom layers when loading:

python
# Save the model
model.save('my_model_with_custom_layers.h5')

# Load the model with custom layers
custom_objects = {
'MySimpleLayer': MySimpleLayer,
'DropConnectDense': DropConnectDense
}

loaded_model = tf.keras.models.load_model(
'my_model_with_custom_layers.h5',
custom_objects=custom_objects
)

Practical Example: Creating an Attention Layer

Let's implement a simple attention mechanism as a custom layer, which is commonly used in advanced deep learning models:

python
class AttentionLayer(tf.keras.layers.Layer):
def __init__(self, **kwargs):
super(AttentionLayer, self).__init__(**kwargs)

def build(self, input_shape):
# Input shape is expected to be [batch_size, time_steps, features]
self.W = self.add_weight(
name="attention_weight",
shape=(input_shape[-1], 1),
initializer="random_normal",
trainable=True
)
self.b = self.add_weight(
name="attention_bias",
shape=(input_shape[1], 1),
initializer="zeros",
trainable=True
)
super(AttentionLayer, self).build(input_shape)

def call(self, inputs):
# inputs shape: [batch_size, time_steps, features]

# Linear projection for obtaining attention logits
e = tf.keras.backend.tanh(tf.keras.backend.dot(inputs, self.W) + self.b)

# Get attention weights (softmax over time dimension)
a = tf.keras.backend.softmax(e, axis=1)

# Apply attention weights to input sequence
output = inputs * a

# Sum over time dimension to get context vector
return tf.keras.backend.sum(output, axis=1)

Example usage in a text classification model:

python
# Example LSTM model with attention for text classification
max_features = 20000
max_length = 100

model = tf.keras.Sequential([
tf.keras.layers.Embedding(max_features, 128, input_length=max_length),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True)),
AttentionLayer(),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])

model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)

model.summary()

Best Practices for Custom Layers

  1. Handle Shapes Correctly: Always use the build method to create weights that depend on input shapes.

  2. Document Your Layer: Add docstrings explaining what your layer does, its parameters, and expected input/output shapes.

  3. Implement get_config: If your layer has configurable parameters, implement get_config for serialization:

python
def get_config(self):
config = super().get_config()
config.update({
"units": self.units,
"drop_rate": self.drop_rate
})
return config
  1. Make Use of TensorFlow Ops: When possible, use TensorFlow's built-in operations for efficiency.

  2. Test Thoroughly: Ensure your layer works in both training and inference modes.

Summary

Custom layers in TensorFlow provide flexibility to implement specialized operations tailored to your needs. By subclassing tf.keras.layers.Layer, you can create reusable components with trainable weights that fit seamlessly into the TensorFlow ecosystem.

In this tutorial, we've covered:

  • Creating basic custom layers
  • Managing weights with the build method
  • Implementing custom computations in the call method
  • Creating layers with different training/inference behaviors
  • Integrating custom layers in various model building approaches
  • Saving and loading models with custom layers
  • A practical attention layer implementation

Additional Resources

  1. TensorFlow Official Guide on Custom Layers
  2. Keras API Documentation
  3. TensorFlow GitHub Repository

Exercises

  1. Create a custom layer that applies a different activation function to each unit in the output.
  2. Implement a custom layer that performs a weighted sum of the input with learnable weights.
  3. Create a layer that implements the Gaussian Error Linear Unit (GELU) activation function.
  4. Build a custom layer that implements local response normalization.
  5. Create a custom attention mechanism that uses multi-head attention instead of the single-head version we implemented.

Happy coding with TensorFlow custom layers!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)