尽管 tf.keras
提供了很多的常用网络层类,但深度学习可以使用的网络层远远不止这些。科研工作者一般是自行实现了较为新颖的网络层,经过大量实验验证有效后,深度学习框架才会跟进,内置对这些网络层的支持。因此掌握自定义网络层、网络的实现非常重要。
对于自定义的网络层,我们至少需要实现初始化__init__
方法和前向传播逻辑call
方法。我们以具体的自定义网络层为例,假设需要一个没有偏置向量的全连接层,即 bias
为0,同时固定激活函数为 ReLU
函数。我们通过实现这个“特别的”网络层类来阐述如何实现自定义网络层。
首先创建类,并继承自 Layer 基类。
inp_dim
和输出特征的长度outp_dim
,并通过 self.add_variable(name, shape)
创建 shape
大小,名字为 name
的张量,并设置为需要优化。class MyDense(layers.Layer):
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 添加权重,用于线性变换
self.kernel = self.add_weight('w', [inp_dim, outp_dim])
# 添加偏置项
self.bias = self.add_weight('b', [outp_dim])
完成自定义类的初始化工作后,我们来设计自定义类的前向运算逻辑,对于这个例子,只需要完成 = @矩阵运算,并通过固定的 ReLU 激活函数即可;
自定义类的前向运算逻辑实现在 call(inputs, training=None)
函数中,其中 inputs
代表输入,由用户在调用时传入;training
参数用于指定模型的状态:training
为 True 时执行训练模式,training
为 False 时执行测试模式,默认参数为 None
,即测试模式。
def call(self, inputs, training=None):
# 实现自定义类的前向计算逻辑
# X@W
out = inputs @ self.kernel
# 执行激活函数运算
out = tf.nn.relu(out)
return out
Sequential
容器适合于数据按序从第一层传播到第二层,再从第二层传播到第三层,以此规律传播的网络模型。对于复杂的网络结构,例如第三层的输入不仅是第二层的输出,还有第一层的输出,此时使用自定义网络更加灵活。下面我们来创建自定义网络类
首先创建类,并继承自 Model
基类:
接着在类里创建对应的网络层对象。
然后实现自定义网络的前向运算逻辑。
class MyModel(tf.keras.Model):
def __init__(self):
super(MyModel, self).__init__()
# 添加自定义的全连接层作为网络的组成部分
self.fc1 = MyDense(28 * 28, 256) # 输入维度为28*28,输出维度为256
self.fc2 = MyDense(256, 128) # 输入维度为256,输出维度为128
self.fc3 = MyDense(128, 64) # 输入维度为128,输出维度为64
self.fc4 = MyDense(64, 32) # 输入维度为64,输出维度为32
self.fc5 = MyDense(32, 10) # 输入维度为32,输出维度为10
def call(self, inputs, training=None):
"""
前向传播函数
Args:
inputs (tf.Tensor): 输入张量
training (bool, 可选): 是否处于训练模式
Returns:
tf.Tensor: 输出张量
"""
x = self.fc1(inputs) # 经过第一层全连接层
x = tf.nn.relu(x) # ReLU激活函数处理
x = self.fc2(x) # 经过第二层全连接层
x = tf.nn.relu(x)
x = self.fc3(x) # 经过第三层全连接层
x = tf.nn.relu(x)
x = self.fc4(x) # 经过第四层全连接层
x = tf.nn.relu(x)
x = self.fc5(x) # 经过第五层全连接层,无激活函数处理,直接输出结果
return x
只学理论可不行,我们还要学会实际应用,接下来我们就使用cifar10
数据集来实战
CIFAR-10
是一个包含60000张32x32 RGB彩色图片的数据集,被用于物体识别。数据集包含10个类别的图片,每个类别有6000张。这些类别包括飞机、汽车、鸟类、猫、鹿、狗、蛙类、马、船和卡车。其中,50000张图片用于训练集,10000张图片用于测试集。与MNIST数据集相比,CIFAR-10
数据集的图像具有更高的色彩分辨率和更复杂的物体形状,因此对物体识别的挑战更大。
注意:由于我们的图像的尺寸很小,图案本身就不清楚,加之网络简单,所以训练出的模型准确率不高是正常现象。
import tensorflow as tf
from tensorflow.keras import datasets, layers, optimizers, Sequential, metrics
# 图像预处理函数
def preprocess(x, y):
# 一张一张的传入图像数据
# 将图片数值范围调整到[-1, 1]
x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1.
# 标签数据
y = tf.squeeze(y) # 删除大小为 1 的维度
y = tf.cast(y, dtype=tf.int32) # 将标签转换为int32类型
y = tf.one_hot(y, depth=10) # 将标签转为one-hot编码
return x, y
# 加载CIFAR10数据集,分为训练数据和测试数据
(x, y), (x_test, y_test) = datasets.cifar10.load_data()
# 打印数据集信息
print('datasets:', x.shape, y.shape, x_test.shape, y_test.shape)
print('图片像素范围:', x.min(), x.max())
# 处理训练集
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.map(preprocess).shuffle(10000).batch(128)
# 处理测试集
test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).batch(128)
# 取一个batch并打印shape
sample = next(iter(train_db))
print('batch:', sample[0].shape, sample[1].shape)
# 自定义全连接层
class MyDense(layers.Layer):
# 初始化函数
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# 添加一个kernel变量,作为自定义层的权重矩阵
# inp_dim为输入特征维度,outp_dim为输出特征维度
self.kernel = self.add_weight('w', [inp_dim, outp_dim])
# 前向计算函数
def call(self, inputs, training=None):
# inputs为输入张量
# self.kernel为自定义层的权重矩阵
# 使用矩阵乘法计算线性变换
x = inputs @ self.kernel
# 返回计算结果
return x
# 自定义模型类,继承自tf.keras.Model
class MyNetwork(tf.keras.Model):
# 初始化函数
def __init__(self):
super(MyNetwork, self).__init__()
# 定义全连接层,层间节点数逐步减小
self.fc1 = MyDense(32 * 32 * 3, 256)
self.fc2 = MyDense(256, 128)
self.fc3 = MyDense(128, 64)
self.fc4 = MyDense(64, 32)
self.fc5 = MyDense(32, 10)
# 前向计算函数
def call(self, inputs, training=None):
# 将输入reshape为一维向量
x = tf.reshape(inputs, [-1, 32 * 32 * 3])
# 通过自定义的全连接层计算
x = self.fc1(x)
x = tf.nn.relu(x)
x = self.fc2(x)
x = tf.nn.relu(x)
x = self.fc3(x)
x = tf.nn.relu(x)
x = self.fc4(x)
x = tf.nn.relu(x)
x = self.fc5(x)
# 返回最终结果
return x
# 构建网络
network = MyNetwork()
network.compile(optimizer=optimizers.Adam(learning_rate=1e-3),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
# 训练
network.fit(train_db, epochs=15, validation_data=test_db, validation_freq=1)
# 评估
print('测试集评估...')
network.evaluate(test_db)
# 保存权重
network.save_weights('ckpt/weights.ckpt')
print('保存权重...')
# 恢复权重
network = MyNetwork()
network.compile(optimizer=optimizers.Adam(lr=1e-3),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
network.load_weights('ckpt/weights.ckpt')
print('读取权重,重新评估...')
network.evaluate(test_db)
点赞,你的认可是我创作的动力!
⭐收藏,你的青睐是我努力的方向!
✏️评论,你的意见是我进步的财富!