文章目录
- 前言
- ResNet原理
- Basic Block
- Res Block
- CiFar100与ResNet18实战
- ResNet18框架
- ResNet18.py代码
- 主文件run.py代码
- CiFar100与ResNet34代码
- 步长不为1,padding为same时
- Python中的__init__()和__call__()函数
- __ init __()
- __ call __()
前言
神经网络的层数越堆越多,并不意味着效果越来越好
当模型加深以后,网络变得越来越难训练,这主要是由于梯度弥散和梯度爆炸
现象造成的。在较深层数的神经网络中,梯度信息由网络的末层逐层传向网络的首层时,传递的过程中会出现梯度接近于0或梯度值非常大的现象。
网络层数越深,这种现象可能会越严重。
那么怎么解决深层神经网络的梯度弥散和梯度爆炸现象呢?一个很自然的想法是,既然浅层神经网络不容易出现这些梯度现象,那么可以尝试给深层神经网络添加一种回退到浅层神经网络的机制。当深层神经网络可以轻松地回退到浅层神经网络时,深层神经网络可以获得与浅层神经网络相当的模型性能,而不至于更糟糕。
通过在输入和输出之间添加一条直接连接的Skip Connection可以让神经网络具有回退的能力。
以 VGG13 深度神经网络为例,假设观察到 VGG13 模型出现梯度弥散现象,而 10 层的网络模型并没有观测到梯度弥散现象,那么可以考虑在最后的两个卷积层添加 Skip Connection,如图 10.62 中所示。通过这种方式,网络模型可以自动选择是否经由这两个卷积层完成特征变换,还是直接跳过这两个卷积层而选择 Skip Connection,亦或结合两个卷积层和 Skip Connection 的输出。
ResNet原理
ResNet通过在卷积层的输入和输出之间添加Skip Connection实现层数回退机制,如下图10.63所示,输入𝒙通过两个卷积层,得到特征变换后的输出F(𝒙),与输入𝒙进行对应元素的相加运算,得到最终输出H(𝒙): H(𝒙) = 𝒙 + F(𝒙)
。H(𝒙)叫作残差模块(Residual Block,简称 ResBlock)
。由于被 Skip Connection 包围的卷积神经网络需要学习映射F(𝒙) = H(𝒙) − 𝒙,故称为残差网络。
为了能够满足输入𝒙与卷积层的输出F(𝒙)能够相加运算,需要输入𝒙的 shape 与F(𝒙)的 shape 完全一致。当出现 shape 不一致时,一般通过在 Skip Connection 上添加额外的卷积运算环节将输入𝒙变换到与F(𝒙)相同的 shape
,如图 10.63 中identity(𝒙)函数所示,其中 identity(𝒙)以 1 × 1 的卷积运算居多,主要用于调整输入的通道数。
Basic Block
Res Block
Res Block 由多个Basic Block构成
CiFar100与ResNet18实战
数据传入ResNet18框架后,到所有Sequential()里逛一圈出来
ResNet18框架
最下面是fc100:
ResNet18.py代码
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential
# 继承自layers.Layer里面不能有容器
class BasicBlock(layers.Layer):
# 残差模块
def __init__(self, filter_num, stride=1):
super(BasicBlock, self).__init__()
# 第一个卷积单元
self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')
self.bn1 = layers.BatchNormalization() # BN层只修改channel通道
self.relu = layers.Activation('relu')
# 第二个卷积单元
self.conv2 = layers.Conv2D(filter_num, (3,3), strides=1, padding='same')
self.bn2 = layers.BatchNormalization() # BN层只修改channel通道
if stride != 1: # 用于修改输入的维度,使其与BasicBlock的输出维度相同,方便做加法
#self.downsample = Sequential()
#self.downsample.add(layers.Conv2D(filter_num, (3,3), strides=stride, padding='same'))
#self.downsample.add(layers.BatchNormalization())
self.downsample = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
else:
self.downsample = lambda x:x
# 前向传播, 让输入x进去BasicBlock逛一圈出来得到F(x)
# inputs的shape是[b, h, w, c];
# 将training设置为None,表示不进行梯度更新;
def call(self, inputs, training=None):
# [b, h, w, c], 通过第一个卷积单元
out = self.conv1(inputs) # 等于self.conv1.call(inputs)
out = self.bn1(out) # 等于self.bn1.call(out)
out = self.relu(out) # 等于self.relu.call(out)
# 通过第二个卷积单元
out = self.conv2(out) # 等于self.bn1.call(out)
output = self.bn2(out) # 等于self.bn2.call(out), output相当于F(x)
# 通过identity模型
identify = self.downsample(inputs) # 修改inputs的维度,和output的维度相同
# 2条路径输出直接相加
output = layers.add([output, identify])
output = tf.nn.relu(output) # 激活函数
return output
# BasicBlock 并不是处理的最终单元, 多个BasicBlock组成的ResBlock才是大的模块
# 通用的 ResNet 实现类
# 继承自keras.Model里面可以有容器
class ResNet(keras.Model):
# layers_dims=[2,2,2,2],这个"列表"里面每个元素代表每次调用"函数build_resblock"时,添加几个BasicBlock;
# [2,2,2,2]说明有4块ResBlock,每块里面有2个BasicBlock
# num_classes是标签数量,这里使用"CIFAR100数据集"就有100个标签
def __init__(self, layers_dims, num_classes=100):
super(ResNet, self).__init__()
# 预处理单元
self.pre = Sequential([
layers.Conv2D(64, (3, 3), strides=(1,1), padding='same'), # 卷积层是把高h/宽w转变到通道c
layers.BatchNormalization(), # BN层只动通道c,不动高h/宽w
layers.Activation('relu'),
layers.MaxPool2D(pool_size=(2,2), strides=(1,1), padding='same')# 池化层也改变高h/宽w,不动通道c
])
# 堆叠 4 个 Block,每个 block 包含了多个 BasicBlock,设置步长不一样
self.layer1 = self.build_resblock(64, layers_dims[0])
self.layer2 = self.build_resblock(128, layers_dims[1], stride=2)
self.layer3 = self.build_resblock(256, layers_dims[2], stride=2)
self.layer4 = self.build_resblock(512, layers_dims[3], stride=2)
# 经过上面四层以后, 假设目前的输出维度为 output:[b, h, w, 512]
# 接着,通过 Pooling 层将高宽降低为 1x1,维度变为: [b, 1, 1, 512]
self.avgpool = layers.GlobalAveragePooling2D()
self.fc = layers.Dense(num_classes) # 经过全连接层变为[b, 100]
# 只想测试一下当前模型,不需要反向求导,赋值training=None,则只会给出当前模型的预测结果;
# 如果输入的数据是用来训练的,需要进行梯度更新,则赋值training=True
def call(self, inputs, training=None):
x = self.pre(inputs) # [b, 32, 32, 64]
# 一次通过4个模块
x = self.layer1(x) # [b, 32, 32, 64]
x = self.layer2(x) # [b, 16, 16, 128]
x = self.layer3(x) # [b, 8, 8, 256]
x = self.layer4(x) # [b, 4, 4, 512]
# 通过池化层
x = self.avgpool(x) # [b, 1, 1, 512]
# 通过全连接层
out = self.fc(x) # 此时的输出应该为 [b, 100]
return out
def build_resblock(self, filter_num, blocks, stride=1):
# 通过build_resblock函数构建了一个Sequential()容器;
# 这个Sequential()容器里根据你指定的块数blocks,在容器里添加了若干的basicblock,每个basicblock在内部有5层;
# 假设参数blocks=3,那么就是一共有3个basicblock,每个basicblock里面有5层
res_blocks = Sequential()
# 只有第一个BasicBlock的步长可能不为1,实现下采样
res_blocks.add(BasicBlock(filter_num, stride))
for _ in range(1, blocks): # 其他BasicBlock步长都为1
res_blocks.add(BasicBlock(filter_num, stride=1))
return res_blocks
def resNet18():
# 通过调整模型内部BasicBlock的数量和配置实现不同的ResNet
return ResNet([2,2,2,2], 100)
def resNet34():
# 通过调整模型内部BasicBlock的数量和配置实现不同的ResNet
return ResNet([3,4,6,3], 100)
主文件run.py代码
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os
from ResNet18 import resNet18
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# Step1 数据准备
(x,y), (x_test, y_test) = datasets.cifar100.load_data()
y = tf.squeeze(y)
y_test = tf.squeeze(y_test)
print('x, y', x.shape, y.shape)
print('x_test, y_test', x_test.shape, y_test.shape)
def preprocessing(input_x, input_y):
input_x = tf.cast(input_x, dtype=tf.float32) / 255.
input_y = tf.cast(input_y, dtype=tf.int32)
return input_x, input_y
train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocessing).batch(128)
train_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
train_test = train_test.map(preprocessing).batch(128)
sample = next(iter(train_db))
print('train_sample:', sample[0].shape)
print('train_sample[1]', sample[1].shape)
print('train_sample的长度', len(sample))
sample = next(iter(train_test))
print('test_sample:', sample[0].shape)
print('test_sample[1]', sample[1].shape)
print('testsample的长度', len(sample))
# Step2 Model
# Step3 Train
def train():
model = resNet18()
model.build(input_shape=(None, 32, 32, 3))
model.summary()
optimizer = optimizers.Adam(learning_rate=1e-4)
for epoch in range(40):
for step, (x,y) in enumerate(train_db):
with tf.GradientTape() as tape:
logits = model(x)
y_onehot = tf.one_hot(y, depth=100)
# 计算loss
loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
loss = tf.reduce_mean(loss)
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
if step % 100 == 0:
print('epoch', epoch, 'step:', step)
total_number = 0
total_correct = 0
for x_test, y_test in train_test:
logits = model(x_test)
probility = tf.nn.softmax(logits, axis=1)
pred = tf.argmax(probility, axis=1)
pred = tf.cast(pred, dtype=tf.int32)
correct = tf.cast(tf.equal(pred, y_test), dtype=tf.int32)
correct = tf.reduce_sum(correct)
total_correct += int(correct)
total_number += x_test.shape[0]
acc = total_correct / total_number
print('total_correct:', total_correct,'total_number:', total_number)
print('acc:', acc)
train()
CiFar100与ResNet34代码
链接: https://pan.baidu.com/s/13YNLG57mEOO1QjgfivEjvQ?pwd=pb4c 提取码: pb4c
ResNet34框架图:(注:最后全连接层输出应是100)
步长不为1,padding为same时
当步长s>1时,设置padding=‘same’,将使得输出高、宽成1/s倍减少。高宽先padding成可以整除s的最小整数,再除以步长:
例1:
例2:
例3:
例4:
例5:
Python中的__init__()和__call__()函数
在Python的class中有一些函数往往具有特殊的意义。__init__()
和__call__()
就是class很有用的两类特殊的函数
__ init __()
__init__()方法的作用是初始化一个类的实例
__ call __()
call方法使得Python中类的实例(对象)可以被当做函数对待。 也就是说,我们可以将它们作为输入传递到其他的函数/方法中并调用他们,正如我们调用一个正常的函数那样。而类中__call__()函数的意义正在于此。为了将一个类实例当做函数调用,我们需要在类中实现__call__()方法
。也就是我们要在类中实现如下方法:def call(self, *args)。这个方法接受一定数量的变量作为输入。假设myCar是类的一个实例。那么调用myCar.__call__(1,2)等同于调用myCar(1,2)。这个实例本身在这里相当于一个函数。
总结
那么,__ init __() 和 __ call __()的区别如下:
__ init __()的作用是初始化某个类的一个实例;
__ call __()的作用是使实例能够像函数一样被调用。