写在前面

尽管 tf.keras 提供了很多的常用网络层类,但深度学习可以使用的网络层远远不止这些。科研工作者一般是自行实现了较为新颖的网络层,经过大量实验验证有效后,深度学习框架才会跟进,内置对这些网络层的支持。因此掌握自定义网络层、网络的实现非常重要。


写在中间

1. 初始化方法

对于自定义的网络层,我们至少需要实现初始化__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  

2. 自定义网络

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数据集来实战

3. cifar10实战

注意:由于我们的图像的尺寸很小,图案本身就不清楚,加之网络简单,所以训练出的模型准确率不高是正常现象。

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)


写在最后

08-15 07:30