Tensorflow2 ResNet实现猫狗二分类

  • 1 前言
  • 2 ResNet架构简介
  • 3 Tensorflow实现ResNet


1 前言

卷积神经网络(CNN)在计算机视觉领域扮演着重要的角色,而ResNet(Residual Network)的诞生则是CNN历史上重要的里程碑。理论上说网络的层数越深,提取的特征越好,检测结果越好,但实验证明网络层数较深的却不如网络层数较浅的模型,这是因为网络层数越深,越容易出现梯度消失和梯度爆炸,而ResNet很好的解决了这一问题。

2 ResNet架构简介

ResNet的思想就是在普通的卷积过程中加了一个恒等映射x。这个恒等映射保证了模型相对于较浅的网络效果不会差

理解恒等映射x:

G(x) = F(x) + x

我们假设上一层(输出为x)已经达到最优效果,如果再简单的往后添加层数,则模型的效果便会降低,但如果我们让后面的层加上前一层的输出x,即让模型记住上一层的输出,极限状态下只需让F(x)=0,模型输出G(x)仍是最优状态x,可保证模型效果不会差于较浅层。

二分类神经网络 python 二分类神经网络架构_卷积神经网络

3 Tensorflow实现ResNet

二分类神经网络 python 二分类神经网络架构_二分类神经网络 python_02


注意事项:

1 ResNet包括两个残差块,左边的两次卷积的步长均为1,右边的第一次卷积步长为2,第二次卷积步长为1,用于压缩长和宽。

2 ResNet的本质在于上一层的输出x和x经过卷积、标准化等进一步提取特征后的结果的对应元素之和作为新的输出(好像很绕),要求两个相加的特征输出的长宽和维度必须完全一致,在卷积的过程中采用padding的模式保证长宽不变,另外需要自己检查两者的维度是否一致。比如说 x 是 (32, 224, 224, 64),则F(x)也必须是(32, 224, 224, 64),当然可以通过广播的机制相加,不过要自己检查能否广播到同一维度。

代码实现:

1 创建图片数据输入管道

数据集使用的是小型猫狗数据集仅3000张,创建图片数据输入管道时使用了图片增强(随机翻转和gamma变换)。

图片数据集网盘地址:https://pan.baidu.com/s/1qdg1fEBHWWpetYGDjIqTjA

提取码:oe0j

import tensorflow as tf
import numpy as np
import glob
import cv2

all_image_path = glob.glob('./dc_2000/*/*/*.jpg')
all_image_label = [int(path.split('\\')[2] == 'cat') for path in all_image_path]
BATCH_SIZE = 50

def generate_iter_data(all_image_path, all_image_label):
    all_data = tf.data.Dataset.from_tensor_slices((all_image_path, all_image_label))
    all_data = all_data.shuffle(len(all_image_path)).batch(BATCH_SIZE).repeat().prefetch(tf.data.experimental.AUTOTUNE)
    for batch_path, batch_label in all_data:
        batch_label = batch_label.numpy()
        batch_image_data = []
        for path in batch_path.numpy():
            path = path.decode()
            image = cv2.imread(path, cv2.IMREAD_COLOR)
            image = cv2.resize(image, (224, 224))
            
            a = np.random.randint(-1, 2)
            flip_img = cv2.flip(image, a)
            gamma = np.random.choice([0.4, 0.5, 0.6, 0.7])
            gamma_img = np.power(flip_img / 255., gamma)
            
            batch_image_data.append(gamma_img)
        batch_image_data_ = np.stack(batch_image_data, axis=0)
        yield batch_image_data_, batch_label.reshape(-1, 1)

2 自己搭建小型ResNet
自定义层搭建残差块,strides=2时用于改变维度和压缩长宽,模型参数量为160w。

class Residual_Block(tf.keras.layers.Layer):
    def __init__(self, filters, strides=1):
        super(Residual_Block, self).__init__()
        self.conv1 = tf.keras.layers.Conv2D(filters=filters,
                                            kernel_size=(3, 3),
                                            strides=strides,
                                            padding='same')
        self.bn1 = tf.keras.layers.BatchNormalization()
        self.relu1 = tf.keras.layers.Activation(tf.nn.relu)
        self.conv2 = tf.keras.layers.Conv2D(filters=filters,
                                            kernel_size=(3, 3),
                                            strides=1,
                                            padding='same')
        self.bn2 = tf.keras.layers.BatchNormalization()
        if strides!= 1: #这个既可以降维又可以改变通道数
            self.downSamples = tf.keras.Sequential()
            self.downSamples.add(tf.keras.layers.Conv2D(filters=filters, 
                                                        kernel_size=(1, 1),
                                                        strides=strides))
            self.downSamples.add(tf.keras.layers.BatchNormalization())
        else:
            self.downSamples = lambda x:x
    
    def call(self, inputs, training=None, **kwargs):
        residual = self.downSamples(inputs)
        x = self.conv1(inputs)
        x = self.bn1(x)
        x = self.relu1(x)
        x = self.conv2(x)
        x = self.bn2(x)
        return tf.nn.relu(tf.keras.layers.add([x, residual])) #传进来的必须是一个列表 不能是元组

def mini_resnet():
    inputs = tf.keras.layers.Input(shape=(224, 224, 3))
    x = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), strides=2, padding='same')(inputs)
    x = tf.keras.layers.BatchNormalization()(x)
    x = tf.keras.layers.Activation(tf.nn.relu)(x)
    
    x = Residual_Block(filters=32)(x)
    x = Residual_Block(filters=64, strides=2)(x)
    x = Residual_Block(filters=64)(x)
    x = Residual_Block(filters=128, strides=2)(x)
    x = Residual_Block(filters=128)(x)
    x = Residual_Block(filters=256, strides=2)(x)
    
    x = tf.keras.layers.GlobalAveragePooling2D()(x)
    x = tf.keras.layers.Dense(units=10, activation='relu')(x)
    x = tf.keras.layers.Dense(units=1, activation='sigmoid')(x)
    
    model = tf.keras.models.Model(inputs, x)
    return model

3 迁移学习ResNet50
采用在imagenet上训练的权重,去掉全连接层,并冻结ResNet50,训练速度快且准备率高。不过参数量多达两千多万,这个数据集对于ResNet50来说完全是大材小用。

def resnet50():
    ResNet50 = tf.keras.applications.ResNet50(include_top=False, 
                                              weights='imagenet', 
                                              input_shape=(224, 224, 3))
    ResNet50.trainable = False
    model = tf.keras.Sequential()
    model.add(ResNet50)
    model.add(tf.keras.layers.GlobalAveragePooling2D())
    model.add(tf.keras.layers.Dense(10, activation='relu'))
    model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
    return model