卷积神经网络

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers,Sequential,losses,optimizers,datasets

1.卷积层的实现

在 TensorFlow 中,既可以通过自定义权值的底层实现方式搭建神经网络,也可以直接调用现成的卷积层类的高层方式快速搭建复杂网络。我们主要以 2D 卷积为例。

1.1 自定义权值

在 TensorFlow 中,通过 tf.nn.conv2d 函数可以方便地实现 2D 卷积运算。tf.nn.conv2d基于输入 X: [b,ℎ,𝑤,𝑐𝑖𝑛] 和卷积核 W:[𝑘,𝑘,𝑐𝑖𝑛,𝑐𝑜𝑢𝑡] 进行卷积运算,得到输出O:[b,ℎ′,𝑤′, 𝑐𝑜𝑢𝑡],其中𝑐𝑖𝑛表示输入通道数,𝑐𝑜𝑢𝑡表示卷积核的数量,也是输出特征图的通道数。

tf.nn.conv2d?
x = tf.random.normal([2,5,3,3]) # 2组数据,高宽为5,通道为3
w = tf.random.normal([3,3,3,4]) # 创建4个3*3,3个通道的卷积核
out = tf.nn.conv2d(x,w,strides = 1,padding = [[0,0],[0,0],[0,0],[0,0]])
out.shape
TensorShape([2, 3, 1, 4])

其中 padding 参数的设置格式为:

padding=[[0,0],[上,下],[左,右],[0,0]]

例如,上下左右各 padding 一个单位,则 padding=[[0,0],[1,1],[1,1],[0,0]],实现如下:

out = tf.nn.conv2d(x,w,strides = 1,padding = [[0,0],[1,1],[1,1],[0,0]])
out.shape
TensorShape([2, 5, 3, 4])

特别地,通过设置参数padding='SAME',strides=1 可以直接得到输入、输出同大小的卷积层,其中 padding 的具体数量由 TensorFlow 自动计算并完成填充操作:

out = tf.nn.conv2d(x,w,strides=1,padding='SAME')
out.shape
TensorShape([2, 5, 3, 4])

当𝑠 > 时,设置 padding='SAME' 将使得输出高、宽将成 1/𝑠 倍地减少:

# 高宽先 padding 成可以整除 3 的最小整数 6,然后 6 按 3 倍减少,得到 2x2
out = tf.nn.conv2d(x,w,strides=3,padding='SAME')
out.shape

TensorShape([2, 2, 1, 4])

卷积神经网络层与全连接层一样,可以设置网络带偏置向量。tf.nn.conv2d 函数是没有实现偏置向量计算的,添加偏置只需要手动累加偏置张量即可:

# 根据[cout]格式创建偏置向量
b = tf.zeros([4])
# 在卷积输出上叠加偏置向量,它会自动 broadcasting 为[b,h',w',cout]
out = out + b

1.2 卷积层类

通过卷积层类 layers.Conv2D 可以不需要手动定义卷积核 W 和偏置 b 张量,直接调用类实例即可完成卷积层的前向计算,实现更加高层和快捷。

在新建卷积层类时,只需要指定卷积核数量参数 filters,卷积核大小 kernel_size,步长strides,填充 padding 等即可,如下创建了 4 个 3x3 大小的卷积核的卷积层,步长为 1, padding 方案为’SAME’:

layer = layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')

实际上,如果卷积核高宽不等,步长行列方向不等,此时需要将 kernel_size 参数设计为(𝑘ℎ,𝑘𝑤),strides 参数设计为(𝑠ℎ,𝑠𝑤)。如下创建 4 个 3x4 大小的卷积核,竖直方向移动步长𝑠ℎ = 2,水平方向移动步长𝑠𝑤 = 1:

layer = layers.Conv2D(4,kernel_size=(3,4),strides=(2,1),padding='SAME')

创建完成后,通过调用实例(的__call__方法)即可完成前向计算:

# 创建卷积层类
layer = layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')
out = layer(x) # 前向计算
out.shape

TensorShape([2, 5, 3, 4])

在类 Conv2D 中,保存了卷积核张量 W 和偏置 b,可以通过类成员 trainable_variables直接返回 W,b 的列表:

layer.trainable_variables

2.LeNet-5 实战

下图是 LeNet-5 的网络结构图,它接受 32x32 大小的数字、字符图片,经过第一个卷积层得到 [b,28,28,6]形状的张量,经过一个向下采样层,张量尺寸缩小到 [b,14,14,6],经过第二个卷积层,得到[b,10,10,16]形状的张量,同样经过下采样层,张量尺寸缩小到[b,5,5,16],在进入全连接层之前,先将张量打成[b,400]的张量,送入输出节点数分别为 120,84 10 的 3 个全连接层,得到[b,10]的张量。

在这里插入图片描述

其中下采样层我们用最大池化层来实现
在这里插入图片描述

我们基于 MNIST 手写数字图片数据集训练 LeNet-5 网络,并测试其最终准确度。首先通过 Sequential 容器创建 LeNet-5:

def preprocess(x, y):
    # [0~255] => [-1~1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1.
    y = tf.cast(y, dtype=tf.int32)
    return x,y


batchsz = 32
# [50k, 32, 32, 3], [10k, 1]
(x, y), (x_val, y_val) = datasets.mnist.load_data()
y = tf.squeeze(y)
y_val = tf.squeeze(y_val)
y = tf.one_hot(y, depth=10) # [50k, 10]
y_val = tf.one_hot(y_val, depth=10) # [10k, 10]
print('datasets:', x.shape, y.shape, x_val.shape, y_val.shape, x.min(), x.max())


train_db = tf.data.Dataset.from_tensor_slices((x,y))
train_db = train_db.map(preprocess).shuffle(10000).batch(batchsz)
db_test = tf.data.Dataset.from_tensor_slices((x_val, y_val))
db_test = db_test.map(preprocess).batch(batchsz)

datasets: (60000, 28, 28) (60000, 10) (10000, 28, 28) (10000, 10) 0 255
from tensorflow.keras import Sequential
network = Sequential([ # 网络容器
    layers.Conv2D(6,kernel_size=3,strides=1), # 第一个卷积层, 6 个 3x3 卷积核
    layers.MaxPooling2D(pool_size=2,strides=2), # 高宽各减半的池化层
    layers.ReLU(), # 激活函数
    layers.Conv2D(16,kernel_size=3,strides=1), # 第二个卷积层, 16 个 3x3 卷积核
    layers.MaxPooling2D(pool_size=2,strides=2), # 高宽各减半的池化层
    layers.ReLU(), # 激活函数
    layers.Flatten(), # 打平层,方便全连接层处理
    layers.Dense(120, activation='relu'), # 全连接层,120 个节点
    layers.Dense(84, activation='relu'), # 全连接层,84 节点
    layers.Dense(10) # 全连接层,10 个节点
])
# build 一次网络模型,给输入 X 的形状,其中 4 为随意给的 batchsz
network.build(input_shape=(4, 28, 28, 1))
network.summary()

在这里插入图片描述

可以看到,卷积层的参数量非常少,主要的参数量集中在全连接层。

我们新建交叉熵损失函数类(没错,损失函数也能使用类方式)用于处理分类任务,通过设定 from_logits=True 标志位将 softmax 激活函数实现在损失函数中,不需要手动添加损失函数,提升数值计算稳定性:

# 导入误差计算,优化器模块
from tensorflow.keras import losses, optimizers
# 创建损失函数的类,在实际计算时直接调用类实例即可
criteon = losses.CategoricalCrossentropy(from_logits=True)

训练模型

optimizer = optimizers.Adam()

# 记录预测正确的数量,总样本数量
correct, total = 0,0
# 构建梯度记录环境
for i in range(30):
    for x, y in train_db:
        with tf.GradientTape() as tape:
            # 插入通道维度,=>[b,28,28,1]
            x = tf.expand_dims(x,axis=3) # 前向计算,获得 10 类别的概率分布,[b, 784] => [b, 10]
            out = network(x)
            # 计算交叉熵损失函数,标量
            loss = criteon(y, out)
            grads = tape.gradient(loss,network.trainable_variables)
            optimizer.apply_gradients(zip(grads,network.trainable_variables))

对模型进行测试

# 记录预测正确的数量,总样本数量
correct, total = 0,0
for x,y in db_test: # 遍历所有训练集样本
    # 插入通道维度,=>[b,28,28,1]

    x = tf.expand_dims(x,axis=3) # 前向计算,获得 10 类别的预测分布,[b, 784] => [b, 10]
    out = network(x)
    # 真实的流程时先经过 softmax,再 argmax
    # 但是由于 softmax 不改变元素的大小相对关系,故省去
    pred = tf.argmax(out, axis=-1) 
    y = tf.cast(y, tf.int64)
    y = tf.argmax(y,axis = -1)
    # 统计预测正确数量
    correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y),tf.float32)))
    # 统计预测样本总数
    total += x.shape[0] # 计算准确率
print('test acc:', correct/total)

test acc: 0.9855

2.BatchNorm 层

对数据进行标准化处理:

在这里插入图片描述

BN 层统计每个通道上面所有数据的𝜇r,𝜎r2,因此𝜇r,𝜎𝐵r是每个通道上所有其他维度的均值和方差。

除了在通道上计算均值和方差,还有下面的几种方式:

❑Layer Norm:统计每个样本的所有特征的均值和方差

❑ Instance Norm:统计每个样本的每个通道上特征的均值和方差

❑ Group Norm:将 c 通道分成若干组,统计每个样本的通道组内的特征均值和方差

在这里插入图片描述

2.1 BN 层实现

在 TensorFlow 中,通过 layers.BatchNormalization() 类可以非常方便地实现 BN 层:

通过设置training 标志位来区分训练模式还是测试模式。以 LeNet-5 的网络模型为例,在卷积层后添加 BN 层:

network = Sequential([ # 网络容器
    layers.Conv2D(6,kernel_size=3,strides=1),
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),
    
    layers.Conv2D(16,kernel_size=3,strides=1),
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2,strides=2),
    layers.ReLU(),
    layers.Flatten(),
    
    layers.Dense(120, activation='relu'),
    # 此处也可以插入 BN 层
    layers.Dense(84, activation='relu'),
    # 此处也可以插入 BN 层
    layers.Dense(10)
])

在训练阶段,需要设置网络的参数 training=True 以区分 BN 层是训练还是测试模型:

with tf.GradientTape() as tape:
    # 插入通道维度
    x = tf.expand_dims(x,axis=3) # 前向计算,设置计算模式,[b, 784] => [b, 10]
    out = network(x, training=True)

在测试阶段,需要设置 training=False,避免 BN 层采用错误的行为:

for x,y in db_test: # 遍历测试集
    # 插入通道维度
    x = tf.expand_dims(x,axis=3) # 前向计算,测试模式
    out = network(x, training=False)
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers, optimizers


# 2 images with 4x4 size, 3 channels
# we explicitly enforce the mean and stddev to N(1, 0.5)
x = tf.random.normal([2,4,4,3], mean=1.,stddev=0.5)

net = layers.BatchNormalization(axis=-1, center=True, scale=True,
                                trainable=True)

out = net(x)
print('forward in test mode:', net.variables)


out = net(x, training=True)
print('forward in train mode(1 step):', net.variables)

for i in range(100):
    out = net(x, training=True)
print('forward in train mode(100 steps):', net.variables)


optimizer = optimizers.SGD(lr=1e-2)
for i in range(10):
    with tf.GradientTape() as tape:
        out = net(x, training=True)
        loss = tf.reduce_mean(tf.pow(out,2)) - 1

    grads = tape.gradient(loss, net.trainable_variables)
    optimizer.apply_gradients(zip(grads, net.trainable_variables))
print('backward(10 steps):', net.variables)
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐