Tensorflow深度学习算法整理

原创
2021/09/20 05:21
阅读数 4.7K

神经网络

神经元-逻辑回归模型

神经元——最小的神经网络

神经元的基本图示,这里x1、x2、x3.....代表特征数据的输入,最后一个1是一个特殊的特征,表示x^0项,对应的就是一个截距b。它们经过加权W•x,这里W,x都是向量,再经过一个函数h(W•x)就得到了一个输出。整个公式如下

而这个f()函数,我们一般使用逻辑回归的函数(关于逻辑回归的内容请参考机器学习算法整理(三) )

而这单个神经元就相当于是一个逻辑回归的分类。输出的结果就是一个[0,1]之间的概率数字。那么这里就是一个二分类的问题,比如说这个猫、狗二分类的图。

我们将t=(这里w、x都为向量)代入函数,就有

那么我们的第一张图就变成了这个样子

这里我们可以看到,上面式子的分母就是上面两个节点的和,而分子分别为上面的两个不同节点,而归一化指的就是这两个节点的和

这是一个二分类的问题,同样,我们也可以采用这种方式来进行多分类。

这是一个多输出神经元,softmax多分类逻辑回归模型,这里的是不同的向量系数,归一化也是将所有节点相加的和。则它们每一种分类的概率也就为

神经元多输出

在神经元多输出的时候,W从向量扩展成矩阵。W•x就变成了一个向量(矩阵点乘向量为向量)。这种情况下,我们可以得到一个多分类的神经元-逻辑回归模型。

神经元的Tensorflow实现

首先我们下载一个图像库的数据文件,下载地址http://www.cs.toronto.edu/~kriz/cifar.html

这个图像库中有60000张32*32(单位像素)的图像,有10个分类,如下所示

现在我们来看一下它里面的信息

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import pickle
import numpy as np
import os

if __name__ == "__main__":

    print(os.listdir(INPUT_PATH))
    with open(os.path.join(INPUT_PATH, "data_batch_1"), 'rb') as f:
        data = pickle.load(f, encoding='bytes')
        print(type(data))
        print(data.keys())
        print(data[b'data'].shape)

运行结果

['data_batch_1', 'readme.html', 'batches.meta', 'data_batch_2', 'data_batch_5', 'test_batch', 'data_batch_4', 'data_batch_3']
<class 'dict'>
dict_keys([b'batch_label', b'labels', b'data', b'filenames'])
(10000, 3072)

这里可以看出data中有10000张图片,每个图片有3072个维度,这个维度是将图片展开,将3通道(rgb)合并在一起,32*32=1072*3=3072。

现在我们随便找一张图片打印出来

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import pickle
import numpy as np
import os
import matplotlib.pyplot as plt

if __name__ == "__main__":

    # print(os.listdir(INPUT_PATH))
    with open(os.path.join(INPUT_PATH, "data_batch_1"), 'rb') as f:
        data = pickle.load(f, encoding='bytes')
        # print(type(data))
        # print(data.keys())
        # print(data[b'data'].shape)
        # 随便找一张图片
        image_arr = data[b'data'][99]
        # 将三通道拆开
        image_arr = image_arr.reshape(3, 32, 32)
        # 通道交换
        image_arr = image_arr.transpose(1, 2, 0)
        plt.imshow(image_arr)
        plt.show()

运行结果

这里我们可以看到它是一个汽车的图片。这里我们调整一下通道交换的顺序。

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import pickle
import numpy as np
import os
import matplotlib.pyplot as plt

if __name__ == "__main__":

    # print(os.listdir(INPUT_PATH))
    with open(os.path.join(INPUT_PATH, "data_batch_1"), 'rb') as f:
        data = pickle.load(f, encoding='bytes')
        # print(type(data))
        # print(data.keys())
        # print(data[b'data'].shape)
        # 随便找一张图片
        image_arr = data[b'data'][99]
        # 将三通道拆开
        image_arr = image_arr.reshape(3, 32, 32)
        # 通道交换
        image_arr = image_arr.transpose(2, 1, 0)
        plt.imshow(image_arr)
        plt.show()

运行结果

这里我们可以看到图像旋转了90度。现在我们就用tensorflow来写一个

单神经元模型

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                for item, label in zip(data, labels):
                    # 只获取0,1两个分类的图像
                    if label in [0, 1]:
                        all_data.append(item)
                        all_labels.append(label)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 定义系数
    W = tf.get_variable('W', shape=[X.get_shape()[-1], 1], initializer=tf.random_normal_initializer(0, 1))
    # 定义截距
    b = tf.get_variable('b', shape=[1], initializer=tf.constant_initializer(0))
    y_ = tf.matmul(X, W) + b
    p_sigmoid = tf.nn.sigmoid(y_)
    y = tf.reshape(y, (-1, 1))
    y = tf.cast(y, tf.float32)
    # 损失函数
    loss = tf.reduce_mean(tf.square(y - p_sigmoid))
    # 预测值
    predict = tf.cast(p_sigmoid > 0.5, tf.float32)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 100000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果(部分截取)

[Train] step: 97000, loss: 0.21120569109916687, acc: 0.800000011920929
[Train] step: 97500, loss: 0.05000118166208267, acc: 0.949999988079071
[Train] step: 98000, loss: 0.20015530288219452, acc: 0.800000011920929
[Train] step: 98500, loss: 0.15044231712818146, acc: 0.8500000238418579
[Train] step: 99000, loss: 0.05000009387731552, acc: 0.949999988079071
[Train] step: 99500, loss: 0.29996025562286377, acc: 0.699999988079071
[Train] step: 100000, loss: 0.10000014305114746, acc: 0.8999999761581421
(2000, 3072)
(2000,)
[Test] step: 100000, acc: 0.8190000057220459

神经网络模型(多神经元模型)

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 定义系数,10个神经元
    W = tf.get_variable('W', shape=[X.get_shape()[-1], 10], initializer=tf.random_normal_initializer(0, 1))
    # 定义截距,10个神经元
    b = tf.get_variable('b', shape=[10], initializer=tf.constant_initializer(0))
    y_ = tf.matmul(X, W) + b
    # # 多分类归一化
    # p_softmax = tf.nn.softmax(y_)
    # # 使用独热编码将一个标签变成一个标签向量
    # y_one_hot = tf.one_hot(y, 10, dtype=tf.float32)
    # # 平方差损失函数
    # loss = tf.reduce_mean(tf.square(y_one_hot - p_softmax))
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果

(50000, 3072)
(50000,)
(10000, 3072)
(10000,)
[Train] step: 500, loss: 11.185017585754395, acc: 0.10750000178813934
[Train] step: 1000, loss: 14.086614608764648, acc: 0.13500000536441803
[Train] step: 1500, loss: 8.61455249786377, acc: 0.0949999988079071
[Train] step: 2000, loss: 11.293926239013672, acc: 0.0949999988079071
[Train] step: 2500, loss: 11.915858268737793, acc: 0.10249999910593033
[Train] step: 3000, loss: 8.637430191040039, acc: 0.11500000208616257
[Train] step: 3500, loss: 7.245659828186035, acc: 0.12250000238418579
[Train] step: 4000, loss: 9.391130447387695, acc: 0.10249999910593033
[Train] step: 4500, loss: 7.8521013259887695, acc: 0.07750000059604645
[Train] step: 5000, loss: 5.67455530166626, acc: 0.11999999731779099
(10000, 3072)
(10000,)
[Test] step: 5000, acc: 0.11159998923540115
[Train] step: 5500, loss: 10.998319625854492, acc: 0.09000000357627869
[Train] step: 6000, loss: 9.57147216796875, acc: 0.07750000059604645
[Train] step: 6500, loss: 9.204813957214355, acc: 0.10249999910593033
[Train] step: 7000, loss: 5.323137283325195, acc: 0.10750000178813934
[Train] step: 7500, loss: 5.950613498687744, acc: 0.1274999976158142
[Train] step: 8000, loss: 8.578786849975586, acc: 0.11249999701976776
[Train] step: 8500, loss: 4.945796012878418, acc: 0.11999999731779099
[Train] step: 9000, loss: 6.142848014831543, acc: 0.10999999940395355
[Train] step: 9500, loss: 6.239230155944824, acc: 0.14000000059604645
[Train] step: 10000, loss: 5.829006195068359, acc: 0.10249999910593033
(10000, 3072)
(10000,)
[Test] step: 10000, acc: 0.11072499305009842

这里我们可以看到它的预测准确率是很低的,这里只是做演示用。以上代码都是tensorflow1的代码。

卷积神经网络

神经网络进阶

  1. 正向传播:为了给一个数据,从数据中求解它的预测值。
  2. 反向传播:为了求解参数,是梯度下降算法在神经网络上的一个具体的计算过程。

这是具有一个隐含层的神经网络,它的计算公式为

这是一个展开式,其实只需要计算一个矩阵•X就可以了。在layer l2层中,不同的神经元对应着不同的系数W。

神经网络正向计算

现在我们来看一下多层神经网络

多层神经网络其实只是将中间层的神经元作为输入层,将结果传递到下一层,其计算方式跟之前的单层神经网络是一样的,每一层都是通过构建损失函数,通过梯度下降法使损失函数最小化来得到一组模型概率值。再将这组模型概率值作为训练数据集传递到下一层,求下一层的新的模型系数和概率值。

神经网络反向传播

我们知道,在最后一层到最终输出层,是通过梯度下降法来求系数的矩阵,对于每一个h2开头的神经元,它的输入就是损失函数里面的x。再通过对W求导数和梯度下降算法来获得对下一层的输出概率值。而x来自于h1开头的神经元的概率值输出。那么通过将上一层的通过损失函数求出来的结果概率值代入下一层的损失函数的x,就形成了复合函数,通过对复合函数求导链式法则,这就构成了一个反向求各层的模型系数的过程。

如果我们有一个神经元,它的输入是x和y,输出是z。我们可以计算出来z对于x的偏导和z对于y的偏导。假设我们已经求到了损失函数对这个神经元输出的导数,即L对z的偏导,那么我们可以求到损失函数L对于x和y的偏导,那么损失函数L对x的偏导即为。那么损失函数L对y的偏导也是一样。

神经网络训练优化

对于这样的神经网络有一些问题,比如每次都在整个数据集上计算损失函数Loss和梯度,这对于数据集比较小的时候是没问题的,但是对于大数据量来说会导致

  1. 计算量大
  2. 可能内存承载不住

梯度方向确定的时候,仍然是每次都走一个步长。这就造成了耗时,太慢。这样就可以使用随机梯度下降。它是每次只使用一个样本,但是只使用一个样本并不能反映整个数据集的梯度的方向,会导致它整个数据集的收敛速度会比较慢。有关随机梯度下降法可以参考机器学习算法整理 。我们可以使用Mini-Batch梯度下降,每次使用小部分数据进行训练。它是随机采样出来的,因为是随机的,它能反映整个数据集的梯度方向,又不会有计算量大,内存承载不住的缺点。一般在神经网络训练被使用。但是Mini-Batch梯度下降法也存在震荡的问题,不过每批次的Size越大,这个问题越不明显。

任何梯度下降法存在局部极值和saddle point的问题。对于损失函数,它不一定是凸函数。

对于saddle point的问题,我们知道梯度下降法的公式为,如果偏导数为0的话,无论学习率α多大,参数θ都得不到更新。

这个不管是哪种梯度下降法,它都会停在这里。解决上面这些问题,我们需要使用动量梯度下降

对于一般的梯度下降法来说是这个样子的

这里compute_gradient(x)就是计算梯度,然后迭代计算x减去学习率乘以梯度。但对于动量梯度下降是这个样子的

对比两种梯度下降的不同,动量梯度下降每次迭代减去的是学习率乘以,而不是梯度。是上一步的乘以ρ加上梯度。是之前的梯度的均值,是一个积累值,用这个积累值加上梯度就得到了一个新的积累值,用这个新的积累值去更新我们的参数,就得到了t+1步的参数。

梯度是有方向的,两个向量相加不仅体现在大小上,还体现在方向上。上图中红色的梯度向量,绿色的是积累值向量,那么紫色的就是更新系数向量的方向。

动量梯度下降

  1. 开始训练时,积累动量,加速训练。
  2. 局部极值附近震荡时,梯度为0,由于动量,跳出陷阱。
  3. 梯度改变方向的时候,动量缓解动荡。

如图所示,在局部最优解中,由于之前积累的动量,仍然可以使得参数进行更新,更新到新的下降坡度的时候就可以继续进行训练了。同理,在鞍点上也是一样,由于有积累的动量,所以仍然可以使得参数进行更新,并继续训练。而对于震荡的情况,由于方向的叠加,从而减少动荡,沿着蓝色曲线的方向下降。

 这里依然还是MNIST的手写识别数据图片,总共有7W张图片,其中训练数据集为6W张,测试数据集有1W张。每张图片的像素为28*28。

由于这是灰度图片,只有一个通道

每个像素的灰度值是从0到255,0代表纯白,255代表纯黑。

所以该图片的特征维度就为28*28*1=784,如果我们要进行数字识别,那么这里就是一个10分类的问题,跟之前一样,我们需要构建一个10个神经元的神经网络来进行分类。我们先来用代码看一下数据的情况

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

if __name__ == "__main__":

    (X, y), _ = datasets.mnist.load_data()
    # 查看训练数据集样式
    print(X.shape)
    # 查看标签样式
    print(y.shape)

运行结果

(60000, 28, 28)
(60000,)

这里我们可以看到训练数据集有6W张图片。

import tensorflow as tf
from tensorflow.keras import datasets, layers, optimizers
import os

if __name__ == "__main__":

    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    (X, y), _ = datasets.mnist.load_data()
    # 将训练数据集(numpy格式)转化为tensorflow自有格式,可以用于GPU加速
    X = tf.convert_to_tensor(X, dtype=tf.float32) / 255
    y = tf.convert_to_tensor(y, dtype=tf.int32)
    y = tf.one_hot(y, 10, dtype=tf.float32)
    # 查看训练数据集样式
    print(X.shape)
    # 查看标签样式
    print(y.shape)
    # 将数据集转化为tensorflow的格式,方便GUP加速,也可以用来批量获取数据
    db = tf.data.Dataset.from_tensor_slices((X, y))
    db = db.batch(200)
    # 设置一个层的堆叠模型
    model = tf.keras.Sequential([
        # 设置三个全连接层,由于传入的数据维度为784,第一层传给第二层输出的维度为512
        # 第二层传给第三层的输出维度为256,由于是10分类,最后的输出维度为10
        # relu为非线性因子,在每一层的输出上再经过一个激活函数relu函数,rulu函数可以
        # 类比于σ函数,效果比σ函数要好
        layers.Dense(512, activation='relu'),
        layers.Dense(256, activation='relu'),
        layers.Dense(10)
    ])
    # 梯度下降优化器
    optimizer = optimizers.SGD(learning_rate=0.001)

    def train_epoch(epoch):
        for step, (X, y) in enumerate(db):
            with tf.GradientTape() as tape:
                # 对每一张图片打平成一维数组
                X = tf.reshape(X, (-1, 28 * 28))
                # 对每一张图片进行预测
                predict = model(X)
                # 损失函数
                loss = tf.reduce_sum(tf.square(predict - y)) / X.shape[0]
            # 求梯度
            grads = tape.gradient(loss, model.trainable_variables)
            # 开始进行梯度下降
            optimizer.apply_gradients(zip(grads, model.trainable_variables))
            if step % 100 == 0:
                print(epoch, step, loss.numpy())

    def train():
        for epoch in range(30):
            train_epoch(epoch)

    train()

这里是tensorflow2的代码,运行结果

(60000, 28, 28)
(60000, 10)
0 0 2.2444384
0 100 0.93035024
0 200 0.7698274
1 0 0.6627595
1 100 0.6615924
1 200 0.5804173
2 0 0.54505724
2 100 0.5841151
2 200 0.5089795
3 0 0.49001732
3 100 0.53978395
3 200 0.46623895
4 0 0.4543409
4 100 0.50788426
4 200 0.43594733
5 0 0.42823532
5 100 0.48310557
5 200 0.41248694
6 0 0.40765586
6 100 0.46295124
6 200 0.39331773
7 0 0.39071265
7 100 0.44583237
7 200 0.37724006
8 0 0.37629554
8 100 0.43092072
8 200 0.3635077
9 0 0.36369827
9 100 0.4176999
9 200 0.35145202
10 0 0.35254088
10 100 0.40608546
10 200 0.34083095
11 0 0.34255296
11 100 0.3957585
11 200 0.33142632
12 0 0.33357754
12 100 0.38654548
12 200 0.32290307
13 0 0.3254689
13 100 0.37816343
13 200 0.31531122
14 0 0.31801963
14 100 0.37048012
14 200 0.3084434
15 0 0.3111582
15 100 0.36332893
15 200 0.30210865
16 0 0.30486727
16 100 0.3567211
16 200 0.29624426
17 0 0.2989938
17 100 0.35063168
17 200 0.29082808
18 0 0.2935084
18 100 0.344897
18 200 0.2858345
19 0 0.28844598
19 100 0.3394714
19 200 0.28116322
20 0 0.28367782
20 100 0.3344191
20 200 0.2767598
21 0 0.27922782
21 100 0.32969108
21 200 0.2726376
22 0 0.27502617
22 100 0.32515225
22 200 0.2687631
23 0 0.27106187
23 100 0.32085288
23 200 0.26511666
24 0 0.2673382
24 100 0.3167893
24 200 0.26162907
25 0 0.26378632
25 100 0.31295282
25 200 0.25830314
26 0 0.26037133
26 100 0.30934215
26 200 0.2551478
27 0 0.25710094
27 100 0.30588272
27 200 0.25217777
28 0 0.25397494
28 100 0.30258736
28 200 0.24935097
29 0 0.2509634
29 100 0.29942432
29 200 0.24664043

这里我们可以看到它的损失函数值是在不断下降的。

卷积神经网络

神经网络遇到的问题。

  • 参数过多

在神经网络中,多个神经元可以组成一层,这一层的神经元需要和之前的层次的输出进行一个全连接,就是说下一层的神经元需要和上一层的所有的输出相连接。在输入的时候,就代表着一个神经元就需要将输入数据的每一个分量都连接到自己的神经元上,作为输入,然后去得到一个输出。如果输入是一个图像的话,那么图像中的每一个像素都可以看成是图像的一个特征值。对于一个1000*1000的图像来说,就会有一百万个像素特征值,如果我们将图像输入到普通的神经网络中去,在第一层,比如说我们使用了一百万个神经元,那么我们需要的全连接参数为1000*1000*10^6=10^12,仅仅是对于第一层来说,我们就有了10^12个参数。一百亿个参数对于我们的内存和计算量来说是非常大的。

参数过多会带来容易过拟合,需要更多训练数据。关于过拟合的内容可以参考机器学习算法整理(二) 中的介绍。造成泛化能力非常差。参数过多,收敛到较差的局部极值。如果数据量太少,那么稍微调整一下参数,可能就会使得在数据集中的某一部分的损失函数的值变的比较小。解决这些问题的方法就是——卷积神经网络

对于一张图像数据来说,某个像素点跟它周围的像素点的关系比较大,而和它离的比较远的像素点的关系会比较小。比如右边这个图中,爱因斯坦的眼睛的像素的值和其周围的像素点的值关系非常大,它们共同的代表的是爱因斯坦的眼睛。但是这个像素点的值与离它比较远的头发像素点的值关联性并不大,因为头发上的点是代表着爱因斯坦的头发的一些特征。所以说对于一个图像数据来说,它具有非常强的区域性。基于图像这样的一个性质,我们可以做一个筛检,我们将全连接变成局部连接,从而去降低它的参数量。在左图中,它是一个普通的神经网络,它的每一个神经元都需要和输入图像中的所有的像素点去进行连接。从而需要有10^12个参数。而对于右图来说,当我们采用了局部连接之后,我们的参数量就会大大减少。我们其中的每个神经元都是和图像中的10*10的区域去做连接,参数量就是一亿个参数(10*10*10^6=10^8),相对于左图的全连接的这种设置来说,我们将参数数目减少了一百倍。当然我们还可以调整区域的大小,来使得参数可以变的更小。这里是解决问题的第一个方面——局部连接

参数共享图像特征与位置无关。对于很多张相同人物的图像来说,每一张图像中这个人所处的位置可能会不一样。上面右图中,爱因斯坦右眼的特征,是需要由红色神经元去提取的,当他的人像往右偏移了之后,那么他右眼的特征就到了更右边的地方,那么这个时候红色神经元就失效了。为了解决这个问题,我们就要用到参数共享,就是我们强制每一个神经元和图像的局部连接都使用同样的参数。这样一来,全连接的参数数量会变的非常小。依然以1000*1000的图像为例,下一层神经元的数量为10^6,局部连接范围为10*10,由于所有神经元都使用相同的参数,那么此时全连接的参数也就为10*10=10^2。参数共享的物理意义为我们使用相同的参数对图像进行扫描,无论扫描到图像的哪个局部区域,我们都让它产生一个个神经元,这样就会形成一个新的图像,不管原图像中的特征在哪个区域,由于我们的共享参数就是学习该特征得出来的,那么我们都可以捕捉到该特征并激活捕捉到该特征的神经元

我们在对普通的神经网络做了局部连接和参数共享之后,我们就得到了一个卷积操作,卷积操作的参数叫做卷积核。卷积核就是局部连接中学习到的各个参数,这些参数都是在所有位置上共享的。

这里我们可以看到输入图像是一个5*5的划分,卷积核是3*3的,当我们使用卷积核对该图像进行扫描的时候,第一个进入扫描的范围就是

,通过内积运算然后将结果放入输出的第一行第一列的问号?的位置,这相当于输出中这个位置的神经元和输入图像中的这个区域做了一次全连接。同理,输出中第一行第二列的问号?就相当于这个位置的神经元与输入图像中的的区域做了一次全连接。那么这样的操作就叫做卷积操作。这里我们可以看到输入图像是5*5的,卷积核是3*3的,输出是3*3的,那么我们就有一个关系是5 3 3。现在假如卷积核是4*4的,那么首先被扫描到的区域就为,那么我们可以看到卷积核只能在图像中滑两次,所以输出就变成了2*2,那么它们的关系就变成了5 4 2。从而我们可以得出输入图像、卷积核、输出图像的size的关系就是:输出size = 输入size - 卷积核size + 1

卷积核和输入图像的计算和全连接是一样的,我们以卷积核和扫描区域为例,它们的计算结果就为点乘result = 1*1+1*3+1*7+1*11+1*13=35,此时的输出就为。同理卷积核和扫描区域的点乘为result = 1*2+1*4+1*8+1*12+1*14=40,此时的输出就为

卷积中的另外一个参数——步长,我们令步长为2

步长的意思就是控制卷积核在图像上滑动的过程中,每一次滑动在图像上间隔的位置是多少。在上图中,步长为2的情况下,卷积核向右滑动后,就是和做点乘,结果为result = 1*3+1*5+1*9+1*13+1*15=45。结果就如输出所示。

当我们的卷积核在输入图像中滑过,输出图像相对于输入图像来说是要变小的。这个对于我们在中间的计算来说就会比较复杂,因为我们可能会有多个卷积层,然后每个卷积层图像都变小一点,当这个图像本来就比较小的时候,那么我们多加几个卷积层,这个图像的size就变成1了,这是很不直观的。而对于一个比较大的图像来说经过一个卷积层,本来它的size是整数,变成一个非整数,对于我们来说也是很难去计算的。那么有没有办法使得输入和输出的size是一样的呢?有一个方法叫做padding,使输出size不变

padding就是在输入图像的数据周围再加n层0,那么它的输出就和输入图像的size是一样的。这个n要根据卷积核的大小而定,n是padding的size。我们知道输出size = 输入size - 卷积核size + 1,这就相当于要解一个方程:输入size=(输入size+2n)-卷积层size+1,要解的是n。以上图为例,5=(5+2n)-3+1,得出n=1,如果卷积核的size为5的话,则有5=(5+2n)-5+1,得到n=2。

上面的内容都是使用卷积去处理单通道的情况,处理多通道(比如RGB三通道)的情况如下所示

图片的最左边代表一张多通道的图片,它的大小是32*32,3通道可以理解成厚度为3。对于卷积核来说,我们也把它变成多通道的,每一个通道上的卷积参数是不共享的,在做计算的过程中,每个通道的卷积参数去和相应通道上的图像的输入去做内积,然后将这三个通道同样位置上得到的结果相加来作为输出神经元的值,所以这里输出图像是单通道的,厚度为1。

那么我如何去产生一个多通道的输出神经元的图呢?

我们只需要多加几个卷积核就可以了,当然这些卷积核的参数是不共享的。多加卷积核的物理含义为:单个卷积核是用来提取某一种图像区域特征的,当图像的某个区域有这个特征的时候,那么单个卷积核捕捉到的输出神经元的值比较大;当图像某区域中没有这个特征的时候,那么输出神经元的值就会比较小。当我们加了多个卷积核的时候,我们就可以在图像中提取多种特征,这就是多卷积核的物理含义

  • 我们来看一个示例,卷积层,输入三通道,输出192通道,卷积核大小是3*3,问该卷积层有多少参数?

首先,输出192通道一定有192个卷积核,由于多卷积核之间的参数是不共享的。输入是3通道,说明卷积核也是3通道的,所以有3*3*3*192=5184个参数。

激活函数

我们之前在讲神经元的时候,由于是逻辑回归,需要使用一个σ函数来将线性值(内积)转化成概率值,这个σ函数就是激活函数。其实除了σ函数还有很多其他的函数可以作为激活函数。

在卷积神经网络中比较常用的是ReLU这个激活函数,因为ReLU计算起来非常高效,它的计算式就是当x<0的时候输出就是0,当x>0的时候,输出就是x。之前我们在手写数据集的神经网络代码中就是使用这种激活函数。

这里所有的激活函数都是单调递增函数,代表输入越大,输出就会越大。而且它们都是非线性函数。之前我们在说普通神经网络的时候,它的网络都是层次的,高一级的层次和低一级的层次是用全连接来联系在一起的。这个全连接就相当于是一个矩阵W,如果我们没有使用非线性的激活函数的时候,那么多个神经网络的层次相当于是每个层次之间都是矩阵操作,矩阵的操作是具有合并性的(可以参考线性代数整理 中矩阵的乘法),比如说3个矩阵相乘会等于一个大矩阵。如果我们不使用非线性函数而是使用线性函数的时候,那么后果就是即便是很深层次的神经网络也只相当于是一个单层的神经网络,但是加了非线性的激活函数之后,这个情况就不会存在了。这是我们使用激活函数的含义。

经过上面的描述,我们来看一下卷积神经网络的参数

  1. P=边距(padding)
  2. S=步长(Stride)
  3. 输出size=(n-p)/s+1
  4. 参数数目=kw * kh * Ci * Co
    1. Ci:输入通道数
    2. Co:输出通道数
    3. Kw,Kh:卷积核长宽

池化

最大值池化

在上图中,我们可以看到,卷积核的size为2*2,步长为2,但是该卷积核中是没有参数的。当卷积核在扫描输入图像的时候,第一个找到的为,此时就会找到该图像区域中最大的值7作为输出。由于步长为2,卷积核向右滑动,找到的就是,此时就会找到该图像区域中的最大值9作为输出。同理卷积核向下滑动,找到的两个最大值就为17和19。对于多出来的边界最右边的列或者最下面的行

或者

要么丢弃,要么做padding操作。

平均值池化

这里的操作跟最大值池化是一样的,只不过输出的不是最大值,而是平均值,比如(1+2+6+7)/4=4,所以输出值为4;(3+4+8+9)/4=6,第二个输出值为6,以此类推。

池化的性质

  1. 常使用不重叠、不补零。
  2. 没有用于求导的参数。
  3. 池化层参数为步长和池化核大小。
  4. 用于减少图像尺寸,从而减少计算量。
  5. 一定程度解决平移鲁棒。这里的意思是说当图像的某一个区域的位置进行了一点偏移,但是在经过池化后,它这个偏移就被抹除了,得到的效果可能就跟该区域未偏移的激活值相同。
  6. 损失了空间位置精度。

池化是一个计算量和精度之间的一个交换

全连接层

卷积神经网络的全连接层和普通神经网络的全连接层是一样的

将上一层输出展开并连接到每一个神经元上。但是在卷积神经网络中,之前的卷积层的输入和输出都是二维多通道的图像的形式,所以它的神经元都是这样去排列的,而不是以一个向量去排列的,在和卷积层的输出去做全连接的时候,需要将卷积层的输出展开,成为一个一维的向量,然后再连到下一层的所有神经元上。这样卷积层的输出和下一层的神经元的连接就叫全连接层。当然在全连接层之后还可以加更多的全连接层,但是就不能够再加卷积层了,因为已经把输出给变成一维的了,没有二维的信息了。在卷积神经网络上,一旦加了全连接层,后面就再也不能加卷积层和池化层了。全连接层即普通神经网络的层。相比于卷积层,全连接层参数数目较大。参数数目=Ci * Co,Ci/Co为输入输出通道数目。

卷积神经网络结构

卷积神经网络=卷积层+池化层+全连接层

在上图中,最左边是一个二通道的96*96的图像,经过一个5*5的卷积核之后,得到了一个92*92的8通道的图像。由于原始图像没有做padding,所以输出的图像的size会变小。再经过一个4*4的池化核,得到一个23*23的8通道图像,再经过一个6*6的卷积核,得到一个18*18的24通道的图像,再通过一个3*3的池化核,得到一个6*6的24通道的图像,最后与全连接层连接,得到最终输出结果。

全卷积神经网络=卷积层+池化层

由于全连接层的输出是一维的,不是二维的,所以全连接层是没办法去生成图像的。我们把全连接层给去掉之后,我们就可以使得卷积神经网络以图像的形式去输出结果,比如说可以应用到图像分割的问题上。在上图中,只用了卷积层和池化层,输出和输入是同等大小的图像,每个图像上的值就是一个分类信息,比如说图中最右边图像就是把狗和猫都分别识别了出来。在图中我们可以看出,虽然原始图像经过了size不同大小的卷积核,它的尺寸应该变小了,但是最终却依然得到了一个跟原始图像一样大小的图像,这里可以用到反卷积层,使用的卷积核的步长为小数,就能够使得比较小的二维的特征图变成一个大的特征图,后面会介绍相关的内容。

卷积神经网络代码实现

还是以之前的彩色图像数据为例,我们修改一点代码,替换掉定义系数,截距和线性输出

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 定义一个10个神经元的全连接层
    y_ = tf.layers.dense(X, 10)
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

这样我们就飞快的定义出了一个全连接层。这里只是一个单层的普通的神经网络,现在我们来多添加几个全连接层。

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 定义一个100个神经元的全连接层,激活函数为relu
    hidden1 = tf.layers.dense(X, 100, activation=tf.nn.relu)
    hidden2 = tf.layers.dense(hidden1, 100, activation=tf.nn.relu)
    hidden3 = tf.layers.dense(hidden2, 50, activation=tf.nn.relu)
    # 定义一个10个神经元的全连接层,作为最终输出层
    y_ = tf.layers.dense(hidden3, 10)
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果

(50000, 3072)
(50000,)
(10000, 3072)
(10000,)
[Train] step: 500, loss: 1.598971962928772, acc: 0.10750000178813934
[Train] step: 1000, loss: 2.117562770843506, acc: 0.11999999731779099
[Train] step: 1500, loss: 1.1181511878967285, acc: 0.1599999964237213
[Train] step: 2000, loss: 1.5395044088363647, acc: 0.13249999284744263
[Train] step: 2500, loss: 1.6425586938858032, acc: 0.10750000178813934
[Train] step: 3000, loss: 1.617976188659668, acc: 0.10499999672174454
[Train] step: 3500, loss: 1.9643516540527344, acc: 0.10249999910593033
[Train] step: 4000, loss: 1.3068873882293701, acc: 0.125
[Train] step: 4500, loss: 2.042571783065796, acc: 0.07500000298023224
[Train] step: 5000, loss: 1.7530193328857422, acc: 0.07500000298023224
(10000, 3072)
(10000,)
[Test] step: 5000, acc: 0.12125001102685928
[Train] step: 5500, loss: 1.165002703666687, acc: 0.11999999731779099
[Train] step: 6000, loss: 1.8682725429534912, acc: 0.0925000011920929
[Train] step: 6500, loss: 1.4488182067871094, acc: 0.1550000011920929
[Train] step: 7000, loss: 1.4004666805267334, acc: 0.11500000208616257
[Train] step: 7500, loss: 1.6362285614013672, acc: 0.11249999701976776
[Train] step: 8000, loss: 1.1082794666290283, acc: 0.11749999970197678
[Train] step: 8500, loss: 1.555980920791626, acc: 0.0949999988079071
[Train] step: 9000, loss: 1.2064460515975952, acc: 0.0925000011920929
[Train] step: 9500, loss: 0.9272489547729492, acc: 0.11249999701976776
[Train] step: 10000, loss: 1.347654104232788, acc: 0.13500000536441803
(10000, 3072)
(10000,)
[Test] step: 10000, acc: 0.1211249828338623

现在我们来实现卷积神经网络。

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 将一维数组转化成多通道的二维图像32*32
    X_image = tf.reshape(X, [-1, 3, 32, 32])
    # 交换通道
    X_image = tf.transpose(X_image, perm=[0, 2, 3, 1])
    # 定义一个卷积层,输出size为32,卷积核大小为3*3,进行padding操作,使得输入、输出图像大小相等
    conv1 = tf.layers.conv2d(X_image, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv1')
    # 定义一个池化层,池化核大小为2*2,步长为2,输出图像为16*16
    pooling1 = tf.layers.max_pooling2d(conv1, (2, 2), (2, 2), name='pool1')
    conv2 = tf.layers.conv2d(pooling1, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv2')
    # 输出图像为8*8
    pooling2 = tf.layers.max_pooling2d(conv2, (2, 2), (2, 2), name='pool2')
    conv3 = tf.layers.conv2d(pooling2, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv3')
    # 输出图像为4*4
    pooling3 = tf.layers.max_pooling2d(conv3, (2, 2), (2, 2), name='pool3')
    # 将池化层pooling3的输出图像打平成一维数组
    flatten = tf.layers.flatten(pooling3)
    # 定义一个10个神经元的全连接层
    y_ = tf.layers.dense(flatten, 10)
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果

(50000, 3072)
(50000,)
(10000, 3072)
(10000,)
[Train] step: 500, loss: 1.5410069227218628, acc: 0.11999999731779099
[Train] step: 1000, loss: 1.2453359365463257, acc: 0.12250000238418579
[Train] step: 1500, loss: 1.0332647562026978, acc: 0.12999999523162842
[Train] step: 2000, loss: 1.1799488067626953, acc: 0.11249999701976776
[Train] step: 2500, loss: 1.2227897644042969, acc: 0.11249999701976776
[Train] step: 3000, loss: 1.0810329914093018, acc: 0.13750000298023224
[Train] step: 3500, loss: 0.5868452787399292, acc: 0.1525000035762787
[Train] step: 4000, loss: 1.0799229145050049, acc: 0.12999999523162842
[Train] step: 4500, loss: 0.5203913450241089, acc: 0.10499999672174454
[Train] step: 5000, loss: 1.506365180015564, acc: 0.13500000536441803
(10000, 3072)
(10000,)
[Test] step: 5000, acc: 0.13107499480247498
[Train] step: 5500, loss: 1.118972897529602, acc: 0.14749999344348907
[Train] step: 6000, loss: 0.991036593914032, acc: 0.17249999940395355
[Train] step: 6500, loss: 1.1500517129898071, acc: 0.12250000238418579
[Train] step: 7000, loss: 1.4104559421539307, acc: 0.10249999910593033
[Train] step: 7500, loss: 1.0728533267974854, acc: 0.125
[Train] step: 8000, loss: 1.0774002075195312, acc: 0.09749999642372131
[Train] step: 8500, loss: 1.3520853519439697, acc: 0.12250000238418579
[Train] step: 9000, loss: 0.6292940378189087, acc: 0.1525000035762787
[Train] step: 9500, loss: 0.7110069394111633, acc: 0.14249999821186066
[Train] step: 10000, loss: 0.8863431811332703, acc: 0.10999999940395355
(10000, 3072)
(10000,)
[Test] step: 10000, acc: 0.13352499902248383

以上都是基于tensorflow1的,现在我们来看一下tensorflow2的代码

我们这里的卷积神经网络结构如下所示

我们先来看一下这个卷积神经网络大概的样子

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

if __name__ == "__main__":

    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    tf.random.set_seed(2345)
    # 定义一个卷积神经网络结构
    conv_layers = [
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
    ]

    def main():
        # 设置一个层的堆叠模型
        conv_net = Sequential(conv_layers)
        conv_net.build(input_shape=[None, 32, 32, 3])
        # 造一个基于正态分布的大小为32*32,3通道的4张图像数据
        x = tf.random.normal([4, 32, 32, 3])
        # 使用层的堆叠模型进行输出,由于我们这里没有加入全连接层,所以得到的依然是一个二维图像
        out = conv_net(x)
        print(out)

    main()

运行结果

tf.Tensor(
[[[[0.02058488 0.0086482  0.         ... 0.00441001 0.
    0.01776086]]]


 [[[0.0208177  0.00782351 0.         ... 0.00454724 0.
    0.01972192]]]


 [[[0.0218306  0.00832988 0.         ... 0.00516474 0.
    0.01913012]]]


 [[[0.02113035 0.00810606 0.         ... 0.00471996 0.
    0.01912492]]]], shape=(4, 1, 1, 512), dtype=float32)

通过结果,我们可以看到,它最终的输出是一个1*1的512个通道的图像

现在我们来导入数据集,来看一下数据集的情况

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import matplotlib.pyplot as plt
import os

if __name__ == "__main__":

    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    tf.random.set_seed(2345)
    # 定义一个卷积神经网络结构
    conv_layers = [
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
    ]

    def main():
        # 设置一个层的堆叠模型
        conv_net = Sequential(conv_layers)
        conv_net.build(input_shape=[None, 32, 32, 3])
        # # 造一个基于正态分布的大小为32*32,3通道的4张图像数据
        # x = tf.random.normal([4, 32, 32, 3])
        # # 使用层的堆叠模型进行输出,由于我们这里没有加入全连接层,所以得到的依然是一个二维图像
        # out = conv_net(x)
        # print(out)
        # 定义全连接层的堆叠模型
        fc_net = Sequential([
            layers.Dense(256, activation=tf.nn.relu),
            layers.Dense(128, activation=tf.nn.relu),
            layers.Dense(100)
        ])
        # 该全连接层的输入为一个长度为512的一维数组
        fc_net.build(input_shape=[None, 512])

    def preprocess(X, y):
        # 预处理
        X = tf.cast(X, dtype=tf.float32) / 255
        y = tf.cast(y, dtype=tf.int32)
        return X, y

    (X, y), (X_test, y_test) = datasets.cifar100.load_data()
    print(X.shape, y.shape, X_test.shape, y_test.shape)
    plt.imshow(X[2])
    plt.show()

    # main()

运行结果

(50000, 32, 32, 3) (50000, 1) (10000, 32, 32, 3) (10000, 1)

这个图像是一个随便挑出来的32*32的图像。而整体上该数据集的训练数据集有5W张图片,32*32,3通道的彩色图片;测试数据集有1W张图片。

和之前一样,我们要将数据集进行tensorflow的格式转换,以便于gpu的加速。再进行分批。

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import matplotlib.pyplot as plt
import os

if __name__ == "__main__":

    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    tf.random.set_seed(2345)
    # 定义一个卷积神经网络结构
    conv_layers = [
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
    ]

    def main():
        # 设置一个层的堆叠模型
        conv_net = Sequential(conv_layers)
        conv_net.build(input_shape=[None, 32, 32, 3])
        # # 造一个基于正态分布的大小为32*32,3通道的4张图像数据
        # x = tf.random.normal([4, 32, 32, 3])
        # # 使用层的堆叠模型进行输出,由于我们这里没有加入全连接层,所以得到的依然是一个二维图像
        # out = conv_net(x)
        # print(out)
        # 定义全连接层的堆叠模型
        fc_net = Sequential([
            layers.Dense(256, activation=tf.nn.relu),
            layers.Dense(128, activation=tf.nn.relu),
            layers.Dense(100)
        ])
        # 该全连接层的输入为一个长度为512的一维数组
        fc_net.build(input_shape=[None, 512])

    def preprocess(X, y):
        # 预处理
        # 将数据集置于[0,1]之间
        X = tf.cast(X, dtype=tf.float32) / 255
        y = tf.cast(y, dtype=tf.int32)
        return X, y

    (X, y), (X_test, y_test) = datasets.cifar100.load_data()
    # 将标签数据集挤压掉一个维度
    y = tf.squeeze(y, axis=1)
    y_test = tf.squeeze(y_test, axis=1)
    print(X.shape, y.shape, X_test.shape, y_test.shape)
    # plt.imshow(X[2])
    # plt.show()
    train_db = tf.data.Dataset.from_tensor_slices((X, y))
    # 将训练数据集打散,再进行预处理后进行分批次获取
    train_db = train_db.shuffle(1000).map(preprocess).batch(64)
    test_db = tf.data.Dataset.from_tensor_slices((X_test, y_test))
    test_db = test_db.map(preprocess).batch(64)
    # 查看分批后的一批的形状和最大最小值
    sample = next(iter(train_db))
    print(sample[0].shape, sample[1].shape, tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))
    # main()

运行结果

(50000, 32, 32, 3) (50000,) (10000, 32, 32, 3) (10000,)
(64, 32, 32, 3) (64,) tf.Tensor(0.0, shape=(), dtype=float32) tf.Tensor(1.0, shape=(), dtype=float32)

现在我们来对图像数据集进行卷积神经网络的处理。

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import matplotlib.pyplot as plt
import os

if __name__ == "__main__":

    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    tf.random.set_seed(2345)
    # 定义一个卷积神经网络结构
    conv_layers = [
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(64, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(128, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(256, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.Conv2D(512, kernel_size=[3, 3], padding='same', activation=tf.nn.relu),
        layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
    ]

    def main():
        # 设置一个层的堆叠模型
        conv_net = Sequential(conv_layers)
        conv_net.build(input_shape=[None, 32, 32, 3])
        # # 造一个基于正态分布的大小为32*32,3通道的4张图像数据
        # x = tf.random.normal([4, 32, 32, 3])
        # # 使用层的堆叠模型进行输出,由于我们这里没有加入全连接层,所以得到的依然是一个二维图像
        # out = conv_net(x)
        # print(out)
        # 定义全连接层的堆叠模型
        fc_net = Sequential([
            layers.Dense(256, activation=tf.nn.relu),
            layers.Dense(128, activation=tf.nn.relu),
            layers.Dense(100)
        ])
        # 该全连接层的输入为一个长度为512的一维数组
        fc_net.build(input_shape=[None, 512])
        # 创建一个梯度下降优化器
        optimizer = optimizers.Adam(lr=1e-4)
        # 拼接卷积层和全连接层的参数
        variables = conv_net.trainable_variables + fc_net.trainable_variables
        for epoch in range(50):
            for step, (X, y) in enumerate(train_db):
                with tf.GradientTape() as tape:
                    # 通过所有的卷积层和池化层操作
                    out = conv_net(X)
                    # 将输出的二维图像打平为一维数组
                    out = tf.reshape(out, [-1, 512])
                    # 通过所有的全连接层操作
                    logits = fc_net(out)
                    # 对标签进行独热编码,由于是100分类,所以分成100
                    y_onehot = tf.one_hot(y, depth=100)
                    # 交叉熵损失函数
                    loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
                    loss = tf.reduce_mean(loss)
                # 获取梯度,对所有卷积层和全连接层的参数求偏导
                grads = tape.gradient(loss, variables)
                # 开始梯度下降
                optimizer.apply_gradients(zip(grads, variables))
                if step % 100 == 0:
                    print(epoch, step, float(loss))
            total_num = 0
            total_correct = 0
            for X, y in test_db:
                out = conv_net(X)
                out = tf.reshape(out, [-1, 512])
                logits = fc_net(out)
                # 多分类归一化
                prob = tf.nn.softmax(logits, axis=1)
                # 预测值
                predict = tf.argmax(prob, axis=1)
                predict = tf.cast(predict, dtype=tf.int32)
                correct = tf.cast(tf.equal(predict, y), dtype=tf.int32)
                correct = tf.reduce_sum(correct)
                total_num += X.shape[0]
                total_correct += int(correct)
            acc = total_correct / total_num
            print(epoch, acc)

    def preprocess(X, y):
        # 预处理
        # 将数据集置于[0,1]之间
        X = tf.cast(X, dtype=tf.float32) / 255
        y = tf.cast(y, dtype=tf.int32)
        return X, y

    # 获取一个100分类的图像数据集
    (X, y), (X_test, y_test) = datasets.cifar100.load_data()
    # 将标签数据集挤压掉一个维度
    y = tf.squeeze(y, axis=1)
    y_test = tf.squeeze(y_test, axis=1)
    print(X.shape, y.shape, X_test.shape, y_test.shape)
    # plt.imshow(X[2])
    # plt.show()
    train_db = tf.data.Dataset.from_tensor_slices((X, y))
    # 将训练数据集打散,再进行预处理后进行分批次获取
    train_db = train_db.shuffle(1000).map(preprocess).batch(64)
    test_db = tf.data.Dataset.from_tensor_slices((X_test, y_test))
    test_db = test_db.map(preprocess).batch(64)
    # 查看分批后的一批的形状和最大最小值
    sample = next(iter(train_db))
    print(sample[0].shape, sample[1].shape, tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))
    main()

运行结果

(50000, 32, 32, 3) (50000,) (10000, 32, 32, 3) (10000,)
(64, 32, 32, 3) (64,) tf.Tensor(0.0, shape=(), dtype=float32) tf.Tensor(1.0, shape=(), dtype=float32)
0 0 4.605831146240234
0 100 4.589689254760742
0 200 4.536626815795898
0 300 4.313758850097656
0 400 4.252532958984375
0 500 4.163078308105469
0 600 4.198701858520508
0 700 3.924816846847534
0 0.0697
1 0 4.181185722351074
1 100 3.933149814605713
1 200 3.7610483169555664
1 300 3.9917283058166504
1 400 3.5505595207214355
1 500 3.8909597396850586
1 600 3.6412038803100586
1 700 3.8353421688079834
1 0.1357
2 0 3.911957263946533
2 100 3.73714017868042
2 200 3.702810764312744
2 300 3.4621024131774902
2 400 3.506136417388916
2 500 3.587125778198242
2 600 3.44793701171875
2 700 3.461962938308716
2 0.1902
3 0 3.4771053791046143
3 100 3.2606635093688965
3 200 3.597566843032837
3 300 3.384145498275757
3 400 2.733502149581909
3 500 3.188058853149414
3 600 2.796154022216797
3 700 3.377180814743042
3 0.2323
4 0 3.133169174194336
4 100 2.835268497467041
4 200 3.519522190093994
4 300 3.010409355163574
4 400 3.0466957092285156
4 500 2.896507740020752
4 600 2.957728147506714
4 700 3.647308826446533
4 0.264
5 0 3.1861443519592285
5 100 2.899226188659668
5 200 2.657050371170044
5 300 2.5587069988250732
5 400 2.807854413986206
5 500 2.9691715240478516
5 600 2.948747158050537
5 700 2.571744441986084
5 0.2982
6 0 2.677814483642578
6 100 2.9436862468719482
6 200 2.9091458320617676
6 300 2.7937870025634766
6 400 2.5567421913146973
6 500 2.7617549896240234
6 600 2.8203275203704834
6 700 2.473966121673584
6 0.3287
7 0 2.626572370529175
7 100 2.2559022903442383
7 200 2.691819429397583
7 300 2.7180891036987305
7 400 2.050813674926758
7 500 2.207024335861206
7 600 2.1584086418151855
7 700 2.4163193702697754
7 0.3467
8 0 2.113696575164795
8 100 2.374277114868164
8 200 2.6424672603607178
8 300 2.0887229442596436
8 400 2.511427879333496
8 500 2.2378270626068115
8 600 2.2883739471435547
8 700 2.54970121383667
8 0.3659
9 0 2.6589956283569336
9 100 2.1897788047790527
9 200 2.3607382774353027
9 300 2.361562728881836
9 400 2.082178831100464
9 500 2.26580548286438
9 600 2.3972692489624023
9 700 1.8203392028808594
9 0.3712
10 0 1.988417148590088
10 100 2.148609161376953
10 200 2.2174034118652344
10 300 1.8767625093460083
10 400 1.6459825038909912
10 500 1.9446841478347778
10 600 1.8217933177947998
10 700 1.6534565687179565
10 0.3913
11 0 2.0026211738586426
11 100 1.8009371757507324
11 200 1.6902337074279785
11 300 1.7578232288360596
11 400 1.4481439590454102
11 500 1.951409101486206
11 600 2.2183847427368164
11 700 2.0311813354492188
11 0.4063
12 0 1.783891201019287
12 100 1.373616337776184
12 200 1.793033480644226
12 300 1.6040012836456299
12 400 1.6418792009353638
12 500 1.7574044466018677
12 600 1.3021467924118042
12 700 1.465400218963623
12 0.4168
13 0 1.8337361812591553
13 100 1.4551146030426025
13 200 1.5964735746383667
13 300 1.6845424175262451
13 400 1.538775086402893
13 500 1.3571796417236328
13 600 1.5174219608306885
13 700 1.504241704940796
13 0.4081
14 0 1.386425495147705
14 100 1.1267224550247192
14 200 1.4012194871902466
14 300 1.4799937009811401
14 400 1.3404619693756104
14 500 1.3455839157104492
14 600 1.4546904563903809
14 700 0.8698689937591553
14 0.4136
15 0 1.3489749431610107
15 100 1.160531759262085
15 200 1.131421685218811
15 300 1.4906198978424072
15 400 1.0041879415512085
15 500 1.1338030099868774
15 600 1.2709101438522339
15 700 1.1831451654434204
15 0.413
16 0 1.2627732753753662
16 100 1.122431755065918
16 200 1.1559514999389648
16 300 0.7461320161819458
16 400 0.7262576818466187
16 500 0.8563841581344604
16 600 0.9288478493690491
16 700 0.9878270626068115
16 0.4151
17 0 0.9544808864593506
17 100 0.772314190864563
17 200 0.7349984645843506
17 300 0.7722299098968506
17 400 0.7409048080444336
17 500 0.9896746873855591
17 600 0.5164972543716431
17 700 0.7082763910293579
17 0.4101
18 0 0.8230305910110474
18 100 0.5578747987747192
18 200 0.6586290001869202
18 300 0.4347994923591614
18 400 0.7593863010406494
18 500 0.34424859285354614
18 600 0.7150816917419434
18 700 0.84696364402771
18 0.4068
19 0 0.8911774158477783
19 100 0.47401052713394165
19 200 0.46469882130622864
19 300 0.42585989832878113
19 400 0.3037422299385071
19 500 0.7571595907211304
19 600 0.4282373785972595
19 700 0.3077845573425293
19 0.412
20 0 0.47909843921661377
20 100 0.33346062898635864
20 200 0.48404020071029663
20 300 0.2714119255542755
20 400 0.4841940701007843
20 500 0.3308350443840027
20 600 0.2655009925365448
20 700 0.2771804928779602
20 0.4046
21 0 0.324692964553833
21 100 0.3309849500656128
21 200 0.2568356394767761
21 300 0.2714660167694092
21 400 0.07225202023983002
21 500 0.20433107018470764
21 600 0.18614551424980164
21 700 0.2796657979488373
21 0.4033
22 0 0.3774525225162506
22 100 0.18599148094654083
22 200 0.3296045660972595
22 300 0.30598852038383484
22 400 0.30166077613830566
22 500 0.1734868884086609
22 600 0.0761394202709198
22 700 0.3384521007537842
22 0.4012
23 0 0.33016541600227356
23 100 0.1943494975566864
23 200 0.1787973940372467
23 300 0.36919230222702026
23 400 0.2110413759946823
23 500 0.3920859098434448
23 600 0.1626299023628235
23 700 0.20021706819534302
23 0.4081
24 0 0.29184406995773315
24 100 0.09991900622844696
24 200 0.184937983751297
24 300 0.2038504183292389
24 400 0.1427721381187439
24 500 0.18976283073425293
24 600 0.10614551603794098
24 700 0.2454446405172348
24 0.4091
25 0 0.13421182334423065
25 100 0.13370943069458008
25 200 0.2758972942829132
25 300 0.06339503079652786
25 400 0.16928648948669434
25 500 0.1679631471633911
25 600 0.18717710673809052
25 700 0.17920580506324768
25 0.409
26 0 0.229348823428154
26 100 0.12948466837406158
26 200 0.15009045600891113
26 300 0.03244897723197937
26 400 0.13071602582931519
26 500 0.18456685543060303
26 600 0.2743353247642517
26 700 0.20379316806793213
26 0.4096
27 0 0.10926089435815811
27 100 0.1679360270500183
27 200 0.11868173629045486
27 300 0.11359536647796631
27 400 0.22055433690547943
27 500 0.15916958451271057
27 600 0.4154285788536072
27 700 0.08481328189373016
27 0.4038
28 0 0.04566130042076111
28 100 0.0865478366613388
28 200 0.24377693235874176
28 300 0.1275908499956131
28 400 0.06748542189598083
28 500 0.12357091903686523
28 600 0.09865625202655792
28 700 0.2000463455915451
28 0.4071
29 0 0.2220441997051239
29 100 0.11607027798891068
29 200 0.046200063079595566
29 300 0.15237504243850708
29 400 0.09139750897884369
29 500 0.16604240238666534
29 600 0.10398057848215103
29 700 0.2262987345457077
29 0.4113
30 0 0.03569779172539711
30 100 0.0728602185845375
30 200 0.40212303400039673
30 300 0.14296859502792358
30 400 0.18192380666732788
30 500 0.07451804727315903
30 600 0.18027660250663757
30 700 0.18559089303016663
30 0.4082
31 0 0.2411424219608307
31 100 0.19804883003234863
31 200 0.028898371383547783
31 300 0.0938447043299675
31 400 0.19860418140888214
31 500 0.05129503458738327
31 600 0.06362833082675934
31 700 0.08428147435188293
31 0.4133
32 0 0.03293028101325035
32 100 0.14080160856246948
32 200 0.031101545318961143
32 300 0.12151184678077698
32 400 0.09270815551280975
32 500 0.18508528172969818
32 600 0.1691283881664276
32 700 0.07396792620420456
32 0.408
33 0 0.07547740638256073
33 100 0.08547955006361008
33 200 0.09924746304750443
33 300 0.18034891784191132
33 400 0.10348965227603912
33 500 0.12805971503257751
33 600 0.1726735532283783
33 700 0.15238720178604126
33 0.402
34 0 0.2098470777273178
34 100 0.14905662834644318
34 200 0.09521455317735672
34 300 0.1619969606399536
34 400 0.12564197182655334
34 500 0.10553520917892456
34 600 0.31648850440979004
34 700 0.05865616723895073
34 0.4104
35 0 0.2687649428844452
35 100 0.20638248324394226
35 200 0.1438908576965332
35 300 0.10306224972009659
35 400 0.02375633828341961
35 500 0.11924442648887634
35 600 0.037159163504838943
35 700 0.15299615263938904
35 0.3964
36 0 0.21671585738658905
36 100 0.05531296133995056
36 200 0.18589413166046143
36 300 0.14120224118232727
36 400 0.1211746335029602
36 500 0.13588906824588776
36 600 0.050172239542007446
36 700 0.030262641608715057
36 0.404
37 0 0.283321350812912
37 100 0.10213848203420639
37 200 0.10902011394500732
37 300 0.07458092272281647
37 400 0.08158361166715622
37 500 0.1437099277973175
37 600 0.13590548932552338
37 700 0.03593549504876137
37 0.4112
38 0 0.10626142472028732
38 100 0.12806737422943115
38 200 0.05799797549843788
38 300 0.05324167385697365
38 400 0.03643240034580231
38 500 0.08759482204914093
38 600 0.06121838092803955
38 700 0.021787459030747414
38 0.4166
39 0 0.20895548164844513
39 100 0.02377612702548504
39 200 0.017479076981544495
39 300 0.05781538411974907
39 400 0.0428706556558609
39 500 0.17528679966926575
39 600 0.19193512201309204
39 700 0.1171540841460228
39 0.4204
40 0 0.026156442239880562
40 100 0.1113971620798111
40 200 0.039948299527168274
40 300 0.0612960159778595
40 400 0.022514544427394867
40 500 0.054041143506765366
40 600 0.01733088307082653
40 700 0.06446746736764908
40 0.4131
41 0 0.07690320909023285
41 100 0.06831232458353043
41 200 0.058896102011203766
41 300 0.05141609162092209
41 400 0.09460064768791199
41 500 0.061292581260204315
41 600 0.0735091045498848
41 700 0.14570656418800354
41 0.4206
42 0 0.056741420179605484
42 100 0.1331595778465271
42 200 0.035598624497652054
42 300 0.18623973429203033
42 400 0.023088140413165092
42 500 0.02422635443508625
42 600 0.18194451928138733
42 700 0.14204511046409607
42 0.4119
43 0 0.026710068807005882
43 100 0.046530622988939285
43 200 0.16910172998905182
43 300 0.06983809173107147
43 400 0.03517763689160347
43 500 0.026091743260622025
43 600 0.20179958641529083
43 700 0.06709711253643036
43 0.4222
44 0 0.04029643535614014
44 100 0.04577355831861496
44 200 0.020225508138537407
44 300 0.09216542541980743
44 400 0.0352892130613327
44 500 0.04647892341017723
44 600 0.021014541387557983
44 700 0.14567427337169647
44 0.4082
45 0 0.13222073018550873
45 100 0.1136949211359024
45 200 0.1268986463546753
45 300 0.06129436939954758
45 400 0.0680326372385025
45 500 0.03143832087516785
45 600 0.01591929793357849
45 700 0.1411474049091339
45 0.4145
46 0 0.23368796706199646
46 100 0.027059141546487808
46 200 0.08579476177692413
46 300 0.04996348172426224
46 400 0.033637095242738724
46 500 0.08706655353307724
46 600 0.09377028048038483
46 700 0.01384596899151802
46 0.4109
47 0 0.06969841569662094
47 100 0.048556700348854065
47 200 0.054020728915929794
47 300 0.036685507744550705
47 400 0.04709421098232269
47 500 0.04992792382836342
47 600 0.006808187812566757
47 700 0.06895837932825089
47 0.4147
48 0 0.04567830637097359
48 100 0.019322432577610016
48 200 0.06474582850933075
48 300 0.063883475959301
48 400 0.18779249489307404
48 500 0.009765483438968658
48 600 0.02241288311779499
48 700 0.05832831561565399
48 0.417
49 0 0.08714817464351654
49 100 0.10505669564008713
49 200 0.12268838286399841
49 300 0.0668354406952858
49 400 0.040120989084243774
49 500 0.08351605385541916
49 600 0.040638454258441925
49 700 0.008790128864347935
49 0.4182

卷积神经网络进阶

自卷积神经网络出现以来,我们有了各种各样的卷积神经网络的结构。这些卷积神经网络的模型是不断演变和进化的。演变的模型大致包括AlexNet、VGG、ResNet、Inception、MobileNet

不同的网络结构解决的问题不同,并没有一个最优的卷积神经网络结构。不同的网络结构使用的技巧不同,这里所谓技巧指的是不同的网络结构,它们中使用的很多优化的子手段可以被借鉴用在其他的网络模型上。不同的网络结构应用的场景不同,比如说我们将卷积神经网络应用在服务器端和手机端是不一样的,在手机端,我们需要将卷积神经网络优之再忧,而在服务器端则无需这种限制。

模型的进化

  1. 更深更宽——AlexNet到VGGNet,在卷积神经网络刚出现的时候,是以AlexNet打头阵。
  2. 不同的模型结构——VGG到InceptionNet/RestNet,当卷积神经网络不断拓深后,并不能带来模型精度上的提升,于是有了另外一种方向。
  3. 优势组合——Inception+Res=InceptionResNet,对于不同的方向,我们还可以将它们的优势组合在一起。
  4. 自我学习——NASNet,再之后我们会将一些强化学习的知识应用到卷积神经网络结构中。在前3步中,卷积神经网络都是由人手工设计的,而在自我学习的这一步中,开始使用一种自学习的方法,使得模型可以自己学出一个新的卷积神经网络结构。
  5. 实用——MobileNet,从服务器端到手机端变动之后,就有了MobileNet.

AlexNet

AlexNet是2012年出的一款模型,它在2010年的比赛中,在1000类的分类任务上能够达到15.3%的错误率。相对于第二名实用传统机器学习的方法26.2%的错误率,它提高了11%。

首先原始输入的图像是一个3通道的,224*224的图像,然后下面的部分经过一个卷积层加池化层,然后经过第二个卷积层加池化层,然后经过第三和第四个卷积层,然后经过第五个卷积层加池化层,再经过两个全连接层到最后的输出1000个分类。同时在上面的部分有一个分割,这个分割代表有两个GPU,上面的部分有一个卷积层加池化层,它的卷积核是11*11的,下面的部分也是一样,这两个卷积层都是跟原始输入的图像做局部连接,它们的输出都是48通道。到第二个卷积层的时候,第一个卷积层的输出,上面的部分是给上面的第二个卷积层的,下面的部分是给下面的第二个卷积层的,但是到了第三个卷积层的时候就不一样了,它们的上下两个卷积层不仅给第三个上下卷积层传输数据而且还有一个交叉,上层的卷积层的输出会传给下面的卷积层,下层的卷积层的输出会传给上面的卷积层。到了第四个卷积层的时候,它们又恢复了各自传输,不再交叉。到了第五个卷积层的时候也是一样,最后输出给全连接层。这个卷积神经网络结构设计的特点是有其历史原因的,因为当时的GPU还没有那么强大,无法装下那么大的图像数据,所以才使用了2个GPU来配合装载。而且2个GPU也可以使整个训练更快,可以并行处理。

我们来看一些细节

  1. 原始图像输入:224*224,3通道。
  2. 第一层卷积:11*11的卷积核 ,上下两个卷积层的输出共有96个通道,也就是共有96个卷积核。卷积核的步长为4.
  3. 第二层卷积:5*5的卷积核,共256个。
  4. 第二层最大池化:2*2的池化核

......

  1. 第一层全连接层:上下共4096个神经元
  2. 第二层全连接层:上下共4096个神经元
  3. Softmax输出:1000,概率值

现在我们来计算一下第一个卷积层的输出。它的输出是224*224,卷积核11*11,步长为4,根据公式:输出size=(输入size-卷积核size+padding size)/步长+1,则有(224-11+padding size)/4+1,由于213/4是除不尽的,所以我们给padding size设置为3,这样213+3=216,216/4=54,54+1=55,所以输出为55*55。这个过程也是我们在设计卷积神经网路编码前要去计算的。参数数目为3*(11*11)*96=34848。

  • AlexNet是首个使用Relu激活函数的。

上图是神经网络的训练过程图,横坐标是训练的迭代次数,纵坐标是错误率。如果是使用σ激活函数的话(图中的虚线曲线部分),它的下降过程非常缓慢,计算量大;如果是Relu的话(图中实线曲线部分),它的下降速度非常快。从这个图中,我们可以看到σ激活函数比Relu激活函数所花的时间大概是6倍。

  • 2-GPU并行结构
  • 1、2、5卷积层后跟随最大池化层(max-pooling层)
  • 两个全连接层上使用了dropout技术

上图中左边的图中的每一个神经元都和上一层的所有神经元去连接。而dropout是在某个神经元上做输入的时候,我们随机的把上一层的神经元的输出值加一个mask(面具),这个面具它使得随机的把其中的一些值给变成0,变成0之后就相当于这些神经元对下一层的神经元没有贡献,因为0*任何参数为0。这个是dropout的基本计算方法,随机的把上一层的神经元给去掉一半。在每一次的训练迭代的时候,训练过程中,头一次去计算反向传播梯度的时候,可能某个神经元是被去掉了,但是在第二次的时候,可能又保留下来了,而去掉的是另外一些神经元,所以每一次使用的神经元都是随机的。

为什么要把dropout应用在全连接层上?

全连接层参数占全部参数数目的大部分,容易过拟合。当参数过多,而样本过少的情况下,会使得参数记住训练集中的所有样本,从而在训练集中拟合的非常好,达到100%的准确率,但是在测试集上,它可能表现的没有那么好,只有20%或30%,这就是过拟合的情况。它的泛化能力不是那么好。对于卷积神经网络来说,既然它的参数绝大部分集中在全连接层上,我们有理由认为如果这个网络过拟合了,那么问题一定出在全连接层上的参数太多了。

为什么dropout能解决这个问题呢,其中有几种主流的解释:

  1. 组合解释:每次dropout都相当于训练了一个子网络,最后的结果相当于很多子网络组合。
  2. 动机解释:消除了神经单元之间的依赖,增强泛化能力。
  3. 数据解释:对于dropout后的结果,总能找到一个样本与其对应。相当于数据增强。
  • 数据增强,图片随机采样

比如一张图片来说,每一张图片的像素是各不相同的,我们先把该图片给转化为256*256,然后再在这个256*256的图像上去随机的采样224*224,能采到什么是什么。这相当于我本来输入了一张图像,但是可能会产生很多种输入图像的方式,这种方式就相当于对于一张图像,我们从很多种角度去观察它,对它进行分类,从而使得我们的网络模型会更准。

  • Dropout=0.5,表示随机干掉的神经元是50%,有50%的神经元会被保留下来。
  • Batch size=128,每次批量采集的最小样本数量为128
  • SGD momentum=0.9,动量梯度下降,之前积攒的动量和现在梯度之间,算平均的时候,之前积攒的动量的系数就是0.9.
  • Learning rate=0.01,梯度下降法的学习率,过一定次数降低为1/10,这个学习率是一个变化的值,比如训练一百万次之后,就会把学习率变成0.001,再训练一百万次之后变成0.0001.
  • 7个CNN做ensemble:18.2%->15.4%,类似于集成学习(关于集成学习的内容可以参考机器学习算法整理(四) ),我们训练多个模型,这多个模型得到的结果之后去投票,这种组合方法可以使得错误率从18.2%降低到15.4%。

VGGNet

VGGNet的全称是VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECONGNITION,翻译过来就是——用于大规模图像识别的甚深卷积网络。它在2014年在物体检测上获得第一名,在分类问题上获得第二名。

  • VGGNet是一个更深的网络,AlexNet有5个卷积层,3个全连接层,所以它是一个8层的神经网络结构(在这里,池化层我们通常不把它算成是一层)。
  • 在VGGNet中多使用3*3的卷积核
    • 2个3*3的卷积层可以看成一层5*5的卷积层    
    • 3个3*3的卷积层可以看成一层7*7的卷积层

从上图可以看到,如果是一个5*5的卷积核,它可以看到下面的图像区域的所有内容。这里我们所说的2个3*3的卷积层不是指2个3*3的卷积核,而是分成2层的卷积层。最上面的卷积层只有1个卷积核,它可以看到中间一层的9个神经元。而中间一层的卷积层有9个卷积核,而这9个卷积核的任意一个卷积核都可以看到下面的3*3的区域,把这9个加起来就可以看到整个5*5的区域,这个就是2个3*3的卷积层可以看成一层5*5的卷积层的意义。这种可以看到多少区域的概念叫做视野域

2层比1层更多一次非线性变换,非线性变换能够使得整个模型的拟合能力变好。所以说2个3*3的卷积层反而会比一个5*5的卷积层能学到更多的东西。

参数降低28%。如果是一个5*5的卷积核的话,这个卷积层的参数数目为5*5*输入通道数*输出通道数。如果是2个3*3的卷积层,我们假设输入通道数和输出通道数是一样的,比如说都是a,那么图像经过两个卷积层,它的参数为3*3*2*a*a,而5*5的参数为25a^2,2个3*3的参数为18a^2,(25-18)/25=28%。这里需要注意的是虽然中间的卷积层有9个卷积核,但是它们的参数是一样的。

  • 多使用1*1的卷积层可以看作是非线性变换

之前我们说过,当一个5*5的3通道卷积核对一个3通道的32*32的图像进行处理时,会得到一个32-5+1=28size的单通道的图像输出。但如果卷积核的size为1的话,我们就会得到一个32-1+1=32的单通道的图像输出。这相当于一个全连接层的事情是一样的,只不过这个全连接层是纵向的,图像没有被打平成一维数组,是在不同通道间去做的,最后也是由激活函数来做转化,这就是1*1的卷积核来做非线性变换的来源。

我们也可以通过这种方式对通道进行一个降维的操作,比如说我们输入的图像是1000通道的,我们可以使用200个1*1的卷积核,将输出变成200个通道的图像。1*1的卷积核可以在加多个这种操作的情况下,也不损失信息。

  • 每经过一个池化层之后,通道数目翻倍

我们每经过一个池化层之后,相当于某些信息要丢失,为了提取更多的特征,从而使得信息丢失的没有那么多。

  • 从11层增至19层

上图中的A是一个11层的神经网络结构,它会经过一个卷积核是3的64核的卷积层,再经过一个池化层,它的通道数目会扩大2倍变成128,再经过一个卷积核是3的128核的卷积层,再经过一个池化层,通道数目变成256,再经过2个256的卷积层后再经过一个池化层,通道数目变成512,再经过2个512的卷积层和1个池化层,这里通道数目将不会改变,依然是512,这是因为考虑到计算的性能问题,再通过2个512核的卷积层和1个池化层然后跟两个4096的全连接层相连。最后输出为1000的全连接层。这里跟之前的AlexNet没有太大区别。

然后我们看一下A-LRN网络,它的意思就是说局部归一化,比如说某一层输出的是32个通道,然后对于这32个通道依次排开,然后对于某一个通道和周边的5层通道去做归一化(把均值生成0,把方差生成1,归一化到0,1之间)。

再来看一下B网络,就是在模块上加层,比如在第一模块上加了一个64核的3*3的卷积层。

再来看一下C网络,C网络就是在B网络的基础上,加了1*1的卷积层进行通道间的非线形变换。

D网络就是将C网络的1*1的卷积核都改成3*3的卷积核,E网络就是增加更多的卷积层。这就是VGGNet从A到E网络的不同配置。

虽然从A到E,看起来变的越来越复杂,但是它们总体参数数目基本保持不变。因为它主要的参数数目集中在全连接层,而全连接层是不变的,对于卷积层来说,它们的参数数目跟全连接层不在一个量级上。

训练技巧

  • 先训练浅层网络如A,再去训练深层网络。我们用A训练出来的模型可以直接用于B初始化的模型。
  • 多尺度输入
    • 不同对尺度训练多个分类器,然后做ensemble
    • 随机使用不同的尺度缩放然后输入进分类器进行训练

我们在讲AlexNet的时候,说的是会从256*256的图像上去抠224*224的图像。它首先会把图先缩放到256*256的格式。在VGGNet中,我们加强了一个方法,我们会使得对于任意一个图像不仅仅是缩放到256*256,我们还缩放到384和512上,在这两个格式上再去提取224*224的图,相当于是我们从更多的角度去看看这个图像是什么。VGGNet对于不同的尺度是训练了不一样的分类器,不一样的分类器再去做组合。

ResNet

VGGNet将网络加深到一定的层次,但是将网络加深到一定的层次之后,它就不能再继续加深了。因为继续加深也不能够提升效果。ResNet解决了这个问题,可以使网络可以继续加深,最深可以加深到1000多层。ResNet是2015年的分类比赛的冠军。

加深层次的问题

  • VGGNet模型深度达到某个程度后继续加深会导致训练集准确率下降。

这是一个训练集误差图,我们知道VGGNet是19层的,现在我们给它加深到20层,如图中的绿线所示,它还是可以得到一个比较低的错误率,但是如果我们把加深层次继续加深到56层,如图中红线所示,我们会发现它虽然也能在降低,但是降到最后,反而没有20层的好。这个是在实践中得到的结果,如果在19层后继续加深,反而不能够得到更好的效果。

  • 模型深度达到某个程度后继续加深会导致训练集准确率下降。

这个是模型准确率随着模型深度的一种变化。它会随着网络深度加深而慢慢上升,达到一个阈值后,开始慢慢下降。

加深层次的问题解决

  • 假设:深层网络更难优化而非深层网络学不到东西。
    • 深层网络至少可以和浅层网络持平。
    • 我们加一些恒等层y=x,虽然增加了深度,但误差不会增加。

  • Identity部分是恒等变换。首先,这里加了一种恒等变换的子结构,就是说仍然把图像数据经过一些卷积层和池化层之类的层次得到的结果F(x),再加上一个恒等变换x。在这里如果深度学习想学到东西,我至少可以使得F(x)是0,从而把这个深度给忽略掉,相当于这些卷积层和池化层就没有了,所以这样的一个网络结构至少是可以和浅层的网络结构是可以持平的。但是它的层次会更深,比如F(x)确实学到了一些东西,它就可以继续增强这个效果。
  • 这里这个F(x)是残差学习

ResNet叫做残差网络。它也有很多的变种,ResNet-34与ResNet-101使用的子结构

以及更多的ResNet的层次

无论是哪一种层次的ResNet网络,首先来了一个224*224的图像会经过一个7*7的64通道,步长为2的卷积核,使得输出size变成一半112*112。然后再经过一个3*3的步长为2的池化层,又把图像减少为原来的1/2,变为56*56。再去经过各种残差子结构(例如或者),乘以2代表有经过多少个这样的子结构。我们可以看到越深的神经网络,它里面的子结构就会越多。在经过所有的残差子结构之后,就没有中间的全连接层了,直接经过平均池化层之后,就到了1000个最终的全连接输出,从最终输出之后就可以直接用softmax去计算概率了。由于没有全连接层,它的参数数目比起之前的VGGNet和AlexNet的参数数目要少很多。所以它可以把这些参数数目给提升到更多的卷积层上,可以加更多的卷积层。在这里参数数目可以一定程度上去反映这个模型的容量,参数数目越多,这个模型就能够学到更多的东西。对于一个更深的卷积神经网络来说,它的卷积层已经有了很多的参数,为了使得这个模型的容量是一定的,这里就强调了卷积层,而弱化了全连接层。所以就把全连接层给去掉了。这个残差网络结构的分析。

模型结构

  • 先用一个普通的卷积层,步长stride为2.
  • 再经过一个3*3的最大池化层。
  • 再经过残差子结构
  • 没有中间的全连接层,直接到输出
  • 残差结构使得网络需要学习的知识变少,容易学习

为什么这样的残差结构能够有效果呢?AlexNet、VGGNet它们都是图像输入到卷积层,再输入到池化层,再输入到卷积层,到最后输入到全连接层,这样的结构如果每一层需要分类的更准的话都需要在每一层去学习到这个图像的所有信息。也就是说每一层都需要保持图像的所有信息。但是对于一个残差网络来说,因为它有残差的存在,残差其实就可以认为是学习的图像的信息,因为完整图像信息可以直接传递过来,所以说它在卷积层上去学到的那些东西就会比较少,它只是学到了一个更特殊的表达而已,而全图的信息它可以保证是可以由残差的另一个恒等式而保存下来,所以说残差结构使得网络要学习的知识变少,它就比较容易的去学习。

  • 残差结构使得每一层的数据分布接近,容易学习

由于残差结构是使得F(x)+x=y,如果在深层次的时候,如果它没有效的话,我们是希望F(x)=0,但如果F(x)≠0,也就相当于是它会比x相加起来得到的数据分布肯定更靠近x一些。在这样的情况下,我们就使得每一层数据分布变的比较相近。数据分布比较相近会使得整个网络变的比较容易学习,比如说在AlexNet或者VGGNet在低层图像输入的时候,图像数据分布的值都在0~255之间,它是一个固定的区域,经过了很多个卷积层和池化层之后,在高层次上,它这个批次数据得到了一个区间可能会是0~512,但对于另外一个批次数据来说,它得到的区间可能会是512~1024,这两个区间是完全不一样的,使得这个模型需要去兼容各种区间的数据。对于残差网络来说,这个模型在一定程度上会更接近它输入模型的数据分布,比如说初始输入的数据是0~512,在经过了几个卷积层和池化层之后它可能还是0~512再加上一个值,如3~515这样一个值。所以它们的数据分布会比较的接近。更加接近就使得这个神经网络在卷积层上就没有那么混淆(confuse)。

DenseNet

ResNet的思路大致是以下这个样子

它每一层都有一个短接层,如果我们把这个思路更加拓展一下就意味着中间的每一层都有机会跟最开始的一层有机会接触。

就变成了这个样子

这就意味着中间的任何一层都有机会跟之前的所有层都有机会接触。这种就叫做DenseNet。因为连接会变的非常非常密集。它不是一个元素级相加而是一个concat相加。这样会使得后面的通道越来越大,如果使用DenseNet,就得要设计的比较好,使得后面的通道不至于太大。

InceptionNet

除了ResNet可以提升网络的深度以外还有另外一个方向就是InceptionNet,InceptionNet不只是一种神经网络,它有多种版本都是Inception V1、V2、V3和V4。它们都是由google研发的。google更加关注于工程的优化——同样的参数量更加的有效率。

深层网络遇到的问题

  • 更深的网络容易过拟合
  • 更深的网络有更大的计算量
    • 稀疏网络虽然减少了参数但没有减少计算量

稀疏网络比较类似于dropout,在卷积层和全连接层存在着很多的参数,这么多参数总有些参数是极度接近于0的,或者是某些参数没有太大的效果,在这个时候稀疏网络就可以把这些参数给成0,从而我们就可以不用分这些参数了,达到了压缩模型大小的目的。但是这种压缩是不能减少计算量的。因为计算的时候为了追求计算的效果依然是采用密集计算的方式,而不是用稀疏矩阵计算的方式。如果使用稀疏矩阵的计算方式会比密集计算的方式更低效。

V1结构

分组卷积

InceptionNet V1把这些卷积核分成一个组。这个组还可以去扩展

每个组还可以去加一些层,而加的这些层它们相互的计算就没有交叉了。这就像AlexNet在某些层上相互计算也是不相互交叉的。不相互交叉就能够降低计算量。

Inception优势

  • 一层上同时使用多种卷积核,看到各种层级的特征(feature),相当于视野域是从小到大的。
  • 不同组之间的特征不交叉计算,减少了计算量

卷积计算量

一个卷积层的计算量:((Kw*Kh)*Ci)*((Ow*Oh)*Co),这里Kw为卷积核宽度,Kh为卷积核高度,Ci为输入通道数,Ow为输出宽度,Oh为输出高度,Co为输出通道数。

首先我们来看单个卷积核在单通道图像上的计算量,这个卷积核是一个3*3的卷积核,这里就是(3*3*1)*(3*3*1)=81

Inception与普通3*3卷积相比

  • 假设输入通道是3,输出通道400,参数比较
    • 普通3*3:    3*3*3*400=10.8k
    • Inception:    3*1*1*100+3*3*3*100+3*5*5*100=10.5k

假设输出是10*10

  • 计算量比较
    • 普通3*3:    3*3*3*10*10*400=1.08M
    • Inception:    (3*1*1*100+3*3*3*100+3*5*5*100)*100=1.05M

这里无论是参数比较还是计算量比较,Inception都略低于普通卷积,Inception还是经过调优可以更低于普通卷积。优化方式可以如下所示

比如输入是一个100通道的图像,经过1*1的卷积去做非线形变换,把它变成25通道的或者是20通道的,这样3*3和5*5的输入就变小了,从而它们的参数数目就变少了,计算量也变少了。

现在我们来看看如何来构建一个Inception V1。首先是经过两个普通的卷积层和池化层,然后到了Inception层,这里有2个Inception的结构,再经过一个池化层,然后有5个Inception结构,经过一个池化层,然后有2个Inception结构,再经过一个平均池化层,再经过一个dropout然后再映射到1000的维度上全连接层,最后计算softmax得到概率值。和ResNet比较类似,这里依然是弱化了全连接层。在inception(3a)中包含了64个1*1的卷积核,96个3*3的步长比较大的卷积核,128个普通的3*3的卷积核,16个步长比较大的5*5的卷积核,32个普通的5*5的卷积核和32个池化核。

V2结构

  • 引入3*3视野域同等卷积替换

使用3*3替换5*5的可以替换参数数目。

 V3结构

  • 3*3不是最小卷积,3*3=1*3和3*1,使参数降低33%。

V4结构

引入skip connection

这个skip connection其实跟ResNet是一个东西,就是残差连接。这是一种强强联合的组合。

MobileNet

MobileNet也是由google开发的。它能够保证在精度损失在可控的范围之内,然后可以大幅度降低参数的数目和计算量。

模型结构

  • 引入深度可分离卷积

图中左图是一个普通的3*3的卷积再经过一个批归一化,再经过一个激活函数。右图中,是先经过一个3*3的深度可分离卷积层,再经过一个批归一化,再经过一个激活函数,再经过一个普通的1*1的卷积层,再经过一个批归一化,再经过激活函数。我们用右边的结构去替换掉左边的结构就能够去构建一个深度可分离的卷积神经网络。

在InceptionNet中有一个分组卷积的结构

这里假设输入、输出通道都是300,经过一组1*1的卷积层去做分组,每一组的通道数目就变成了原来的1/3,即100。再用3*3的卷积核在这1/3的通道上去做处理,然后再把得到的结果去拼接到一起。我们看一下这样的一个分组卷积能降低多少参数。

  1. 如果是普通的3*3卷积:    3*3*300*300=81W
  2. 如果是分组卷积:    3*3*100*100*3=27W

所以参数降低了1/3。

对于深度可分离卷积,它分到极致

首先数据输入经过一个1*1的卷积层之后会得到一个多通道的输出,而中间的每一个3*3的卷积核只读其中的一个通道,最后再完成拼接。

  • 普通卷积计算量((Kw*Kh)*Ci)*((Ow*Oh)*Co)
  • 深度可分离卷积计算量
    • 深度可分离:    Kw*Kh*Co*Ow*Oh    输入通道Ci为1,所以在这里可以不显示
    • 1*1卷积:    Ci*Co*Ow*Oh。    由于1*1的卷积核长宽都是1,所以这里可以不显示
  • 优化比例

比如说输入通道为100,卷积核大小为3*3,则优化比例为1/100+1/9,这么大幅度的降低参数,一定会带来精度的损失,但通过实验得到精度的损失是比较有限的,可能只在10%的范畴之内。

模型结构优劣对比

  • 效果分析

这里不同的结构的分类准确率如上图所示,随着模型的进化AlexNet的准确率只有40%,而Inception V4的分类准确率可以达到80%。

  • 效果层数分析

这里是错误率,即越低越好。我们可以看到AlexNet的时候为8层,在VGG的时候是19层,GoogleNet(即InceptionNet V1)为22层,到ResNet的时候有152层。

  • 性价比

这张图就是可以用于选模型的一张图,对比了模型的各种指标。这里横坐标为计算量,纵坐标为模型精度。圆圈的大小就是model size,就是模型参数量的大小。在这里就计算量而言,Inception V3是一个比较好的拐点,Inception V4即比它大,计算量又比它多。

VGGNet代码(tensorflow1版)

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()
    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 将一维数组转化成多通道的二维图像32*32
    X_image = tf.reshape(X, [-1, 3, 32, 32])
    # 交换通道
    X_image = tf.transpose(X_image, perm=[0, 2, 3, 1])
    # 定义一个卷积层,输出size为32,卷积核大小为3*3,进行padding操作,使得输入、输出图像大小相等
    conv1_1 = tf.layers.conv2d(X_image, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv1_1')
    conv1_2 = tf.layers.conv2d(conv1_1, 32, (3, 3), padding='same',
                               activation=tf.nn.relu, name='conv1_2')
    # 定义一个池化层,池化核大小为2*2,步长为2,输出图像为16*16
    pooling1 = tf.layers.max_pooling2d(conv1_2, (2, 2), (2, 2), name='pool1')
    conv2_1 = tf.layers.conv2d(pooling1, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv2_1')
    conv2_2 = tf.layers.conv2d(conv2_1, 32, (3, 3), padding='same',
                               activation=tf.nn.relu, name='conv2_2')
    # 输出图像为8*8
    pooling2 = tf.layers.max_pooling2d(conv2_2, (2, 2), (2, 2), name='pool2')
    conv3_1 = tf.layers.conv2d(pooling2, 32, (3, 3), padding='same',
                             activation=tf.nn.relu, name='conv3_1')
    conv3_2 = tf.layers.conv2d(conv3_1, 32, (3, 3), padding='same',
                               activation=tf.nn.relu, name='conv3_2')
    # 输出图像为4*4
    pooling3 = tf.layers.max_pooling2d(conv3_2, (2, 2), (2, 2), name='pool3')
    # 将池化层pooling3的输出图像打平成一维数组
    flatten = tf.layers.flatten(pooling3)
    # 定义一个10个神经元的全连接层
    y_ = tf.layers.dense(flatten, 10)
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果

(50000, 3072)
(50000,)
(10000, 3072)
(10000,)
[Train] step: 500, loss: 1.5734227895736694, acc: 0.13249999284744263
[Train] step: 1000, loss: 1.9170109033584595, acc: 0.08500000089406967
[Train] step: 1500, loss: 1.2270904779434204, acc: 0.0949999988079071
[Train] step: 2000, loss: 1.2478458881378174, acc: 0.125
[Train] step: 2500, loss: 1.2079541683197021, acc: 0.11249999701976776
[Train] step: 3000, loss: 0.928557276725769, acc: 0.15000000596046448
[Train] step: 3500, loss: 1.3090790510177612, acc: 0.13750000298023224
[Train] step: 4000, loss: 1.0952129364013672, acc: 0.11749999970197678
[Train] step: 4500, loss: 0.7466774582862854, acc: 0.13750000298023224
[Train] step: 5000, loss: 0.7525835037231445, acc: 0.11999999731779099
(10000, 3072)
(10000,)
[Test] step: 5000, acc: 0.13044999539852142
[Train] step: 5500, loss: 0.821816623210907, acc: 0.17499999701976776
[Train] step: 6000, loss: 1.009394645690918, acc: 0.11999999731779099
[Train] step: 6500, loss: 0.5836264491081238, acc: 0.13500000536441803
[Train] step: 7000, loss: 0.9741325378417969, acc: 0.10499999672174454
[Train] step: 7500, loss: 0.9230200052261353, acc: 0.14249999821186066
[Train] step: 8000, loss: 0.2339707911014557, acc: 0.1574999988079071
[Train] step: 8500, loss: 0.6255263686180115, acc: 0.12999999523162842
[Train] step: 9000, loss: 0.9320176839828491, acc: 0.11249999701976776
[Train] step: 9500, loss: 0.8363860845565796, acc: 0.14749999344348907
[Train] step: 10000, loss: 0.7538067698478699, acc: 0.15000000596046448
(10000, 3072)
(10000,)
[Test] step: 10000, acc: 0.13545000553131104

这里需要说明的是迭代次数远没有达到分类准确率上限的次数,这里只是一个演示。

ResNet代码(tensorflow1版)

INPUT_PATH = "/Users/admin/Downloads/cifar-10-batches-py/"
import tensorflow.compat.v1 as tf
import pickle
import numpy as np
import os

if __name__ == "__main__":

    def load_data(filename):
        # 读取数据文件
        with open(filename, 'rb') as f:
            data = pickle.load(f, encoding='bytes')
        return data[b'data'], data[b'labels']

    class CifarData:
        def __init__(self, filenames, need_shuffle):
            all_data = []
            all_labels = []
            for filename in filenames:
                data, labels = load_data(filename)
                # 多分类
                all_data.append(data)
                all_labels.append(labels)
            self._data = np.vstack(all_data)
            # 对图像数据进行缩放,使之在[-1,1]之间
            self._data = self._data / 127.5 - 1
            self._labels = np.hstack(all_labels)
            print(self._data.shape)
            print(self._labels.shape)
            # 获取数据样本数
            self._num_examples = self._data.shape[0]
            # 是否需要打散
            self._need_shuffle = need_shuffle
            self._indicator = 0
            if self._need_shuffle:
                self._shuffle_data()

        def _shuffle_data(self):
            # 打散
            p = np.random.permutation(self._num_examples)
            self._data = self._data[p]
            self._labels = self._labels[p]

        def next_batch(self, batch_size):
            '''
            获取下一个批次的数据
            :param batch_size: 下一个批次的数量
            :return:
            '''
            end_indicator = self._indicator + batch_size
            if end_indicator > self._num_examples:
                if self._need_shuffle:
                    self._shuffle_data()
                    self._indicator = 0
                    end_indicator = batch_size
                else:
                    raise Exception("数据集已经遍历完")
            if end_indicator > self._num_examples:
                raise Exception("batch size大于全部数据集")
            batch_data = self._data[self._indicator: end_indicator]
            batch_lables = self._labels[self._indicator: end_indicator]
            self._indicator = end_indicator
            return batch_data, batch_lables.reshape(-1, 1)

    train_filenames = [os.path.join(INPUT_PATH, 'data_batch_%d' % i) for i in range(1, 6)]
    test_filenames = [os.path.join(INPUT_PATH, 'test_batch')]
    train_data = CifarData(train_filenames, True)
    test_data = CifarData(test_filenames, False)

    tf.disable_eager_execution()

    def residual_block(X, output_channel):
        '''
        残差连接块
        :param X: 输入
        :param output_channel: 输出通道数
        :return:
        '''
        # 输入通道数
        input_channel = X.get_shape().as_list()[-1]
        # 如果输出的通道数翻倍,使用池化降采样,否则不适用池化降采样
        # 降采样的意思是缩小图像
        if input_channel * 2 == output_channel:
            increase_dim = True
            strides = (2, 2)
        elif input_channel == output_channel:
            increase_dim = False
            strides = (1, 1)
        else:
            raise Exception("输入通道数无法匹配输出通道数")
        # 处理图像特征学习分支
        # 定义一个卷积层,输出size为output_channel
        # 卷积核大小为3 * 3,进行padding操作,使得输入、输出图像大小相等
        conv1 = tf.layers.conv2d(X, output_channel, (3, 3),
                                 strides=strides, padding='same',
                                 activation=tf.nn.relu, name='conv1')
        conv2 = tf.layers.conv2d(conv1, output_channel, (3, 3),
                                 strides=(1, 1), padding='same',
                                 activation=tf.nn.relu, name='conv2')
        # 处理恒等变换分支
        if increase_dim:
            # 如果需要降采样,通过平均池化层将输入图像的尺寸变为一半
            pooled_x = tf.layers.average_pooling2d(X, (2, 2), (2, 2),
                                                   padding='valid')
            # 将通道数翻倍(上下各补充一半的输入通道数的0,即补充了一个输入通道数的0)
            padded_x = tf.pad(pooled_x, [[0, 0], [0, 0], [0, 0], [input_channel // 2, input_channel // 2]])
        else:
            # 如果输出的通道数=输入通道数,则不做降采样,直接获取原输入图像
            padded_x = X
        # 将特征学习分支和恒等变换分支合并
        output_x = conv2 + padded_x
        return output_x

    def res_net(X, num_residual_blocks,
                num_filter_base, class_num):
        '''
        残差网络
        :param X: 输入
        :param num_residual_blocks: 所有残差连接块的各阶段数量列表
        :param num_filter_base: 残差块基础输出size
        :param class_num: 类别数
        :return:
        '''
        # 获取残差连接块的总阶段数
        num_subsampling = len(num_residual_blocks)
        layers = []
        # 获取输入图像的通道数和size
        input_size = X.get_shape().as_list()[1:]
        with tf.variable_scope('conv0'):
            # 定义一个卷积层,输出size为num_filter_base,这里为32,
            # 卷积核大小为3 * 3,进行padding操作,使得输入、输出图像大小相等
            conv0 = tf.layers.conv2d(X, num_filter_base, (3, 3),
                                     strides=(1, 1), padding='same',
                                     activation=tf.nn.relu,
                                     name='conv0')
            layers.append(conv0)
        # 遍历残差网络的每一个阶段
        for sample_id in range(num_subsampling):
            # 遍历每个阶段的每一个残差子模块
            for i in range(num_residual_blocks[sample_id]):
                with tf.variable_scope('conv%d_%d' % (sample_id, i)):
                    # 获取残差连接块(特征学习分支和恒等变换分支合并后的图像信息)
                    # num_filter_base * 2 ** sample_id表示通道翻倍
                    conv = residual_block(layers[-1], num_filter_base * 2**sample_id)
                    layers.append(conv)
        # 预测最后输出的神经元图的大小和实际的神经元大小是否相等
        multiplier = 2**(num_subsampling - 1)
        assert layers[-1].get_shape().as_list()[1:] \
            == [input_size[0] / multiplier, input_size[1] / multiplier, num_filter_base * multiplier]
        with tf.variable_scope('fc'):
            # 对残差网络的输出图像的size(宽度和高度)上求平均
            global_pool = tf.reduce_mean(layers[-1], [1, 2])
            # 定义一个class_num个神经元的全连接层
            logits = tf.layers.dense(global_pool, class_num)
            layers.append(logits)
        return layers[-1]

    # 搭建一个data的tensorflow图,样本数量不确定,维度为3072
    X = tf.placeholder(tf.float32, shape=(None, 3072))
    # 搭建一个标签的tensorflow图
    y = tf.placeholder(tf.int64, shape=(None))
    # 将一维数组转化成多通道的二维图像32*32
    X_image = tf.reshape(X, [-1, 3, 32, 32])
    # 交换通道
    X_image = tf.transpose(X_image, perm=[0, 2, 3, 1])
    # 通过残差网络获取概率值
    y_ = res_net(X_image, [2, 3, 2], 32, 10)
    # 交叉熵损失函数,它可以完成多分类归一化,独热编码的全部过程
    loss = tf.losses.sparse_softmax_cross_entropy(labels=y, logits=y_)
    # 预测值
    predict = tf.argmax(y_, 1)
    # 预测正确的值
    correct_predict = tf.cast(tf.equal(predict, y), tf.float32)
    # 准确率
    accuracy = tf.reduce_mean(correct_predict)
    # 梯度下降法
    with tf.name_scope('train_op'):
        train_op = tf.train.AdamOptimizer(1e-3).minimize(loss)
    # 全局参数初始化
    init = tf.global_variables_initializer()
    batch_size = 20
    train_steps = 10000
    test_steps = 100
    with tf.Session() as sess:
        sess.run(init)
        for i in range(train_steps):
            batch_data, batch_labels = train_data.next_batch(batch_size)
            # 使用梯度下降法来求损失函数的最小值,和预测准确率的值
            loss_val, acc_val, _ = sess.run([loss, accuracy, train_op],
                                            feed_dict={X: batch_data, y: batch_labels})
            if (i + 1) % 500 == 0:
                print(f"[Train] step: {i + 1}, loss: {loss_val}, acc: {acc_val}")
            if (i + 1) % 5000 == 0:
                test_data = CifarData(test_filenames, False)
                all_test_acc_val = []
                for j in range(test_steps):
                    test_batch_data, test_batch_labels = test_data.next_batch(batch_size)
                    test_acc_val = sess.run([accuracy], feed_dict={X: test_batch_data, y: test_batch_labels})
                    all_test_acc_val.append(test_acc_val)
                test_acc = np.mean(all_test_acc_val)
                print(f"[Test] step: {i + 1}, acc: {test_acc}")

运行结果

(50000, 3072)
(50000,)
(10000, 3072)
(10000,)
[Train] step: 500, loss: 2.185913562774658, acc: 0.13249999284744263
[Train] step: 1000, loss: 1.2987085580825806, acc: 0.14000000059604645
[Train] step: 1500, loss: 1.7132829427719116, acc: 0.10750000178813934
[Train] step: 2000, loss: 1.3021440505981445, acc: 0.11749999970197678
[Train] step: 2500, loss: 1.4075722694396973, acc: 0.13249999284744263
[Train] step: 3000, loss: 1.2595329284667969, acc: 0.125
[Train] step: 3500, loss: 1.1323442459106445, acc: 0.11999999731779099
[Train] step: 4000, loss: 1.3741838932037354, acc: 0.11749999970197678
[Train] step: 4500, loss: 0.953040599822998, acc: 0.11749999970197678
[Train] step: 5000, loss: 0.6787084341049194, acc: 0.11749999970197678
(10000, 3072)
(10000,)
[Test] step: 5000, acc: 0.13015002012252808
[Train] step: 5500, loss: 0.8726496696472168, acc: 0.10999999940395355
[Train] step: 6000, loss: 0.8844865560531616, acc: 0.14000000059604645
[Train] step: 6500, loss: 0.6240081787109375, acc: 0.1274999976158142
[Train] step: 7000, loss: 1.035969853401184, acc: 0.16500000655651093
[Train] step: 7500, loss: 0.7985122799873352, acc: 0.11999999731779099
[Train] step: 8000, loss: 0.6917668581008911, acc: 0.1550000011920929
[Train] step: 8500, loss: 0.5434420108795166, acc: 0.1574999988079071
[Train] step: 9000, loss: 0.7845785021781921, acc: 0.10750000178813934
[Train] step: 9500, loss: 0.548119068145752, acc: 0.12999999523162842
[Train] step: 10000, loss: 0.7236884236335754, acc: 0.11249999701976776
(10000, 3072)
(10000,)
[Test] step: 10000, acc: 0.13349999487400055

ResNet代码(tensorflow2版)

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

if __name__ == "__main__":

    class BasicBlock(layers.Layer):
        # 残差连接块

        def __init__(self, filter_num, stride=1):
            '''
            :param filter_num: 残差块基础输出size
            :param stride: 卷积核步长
            '''
            super(BasicBlock, self).__init__()
            self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride,
                                       padding='same')
            self.bn1 = layers.BatchNormalization()
            self.relu = layers.Activation('relu')
            self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1,
                                       padding='same')
            self.bn2 = layers.BatchNormalization()
            # 是否需要降采样
            if stride != 1:
                self.downsample = Sequential()
                self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
            else:
                self.downsample = lambda x: x

        def call(self, inputs, training=None):
            out = self.conv1(inputs)
            out = self.bn1(out)
            out = self.relu(out)
            out = self.conv2(out)
            out = self.bn2(out)
            identity = self.downsample(inputs)
            output = layers.add([out, identity])
            output = tf.nn.relu(output)
            return output

    class ResNet(keras.Model):
        # 残差网络

        def __init__(self, layer_dims, number_classes=100):
            '''
            :param layer_dims: 所有残差连接块的各阶段数量列表
            :param number_classes: 类别数
            '''
            super(ResNet, self).__init__()
            self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=1),
                                    layers.BatchNormalization(),
                                    layers.Activation('relu'),
                                    layers.MaxPool2D(pool_size=(2, 2), strides=1,
                                                     padding='same')])
            self.layer1 = self.build_resblock(64, layer_dims[0])
            self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
            self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
            self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
            self.avgpool = layers.GlobalAveragePooling2D()
            self.fc = layers.Dense(number_classes)

        def call(self, inputs, training=None):
            X = self.stem(inputs)
            X = self.layer1(X)
            X = self.layer2(X)
            X = self.layer3(X)
            X = self.layer4(X)
            X = self.avgpool(X)
            X = self.fc(X)
            return X

        def build_resblock(self, filter_num, blocks, stride=1):
            '''
            构建残差连接块
            :param filter_num: 残差块基础输出size
            :param blocks: 每个阶段的子模块数
            :param stride: 卷积核步长
            :return:
            '''
            res_blocks = Sequential()
            res_blocks.add(BasicBlock(filter_num, stride))
            for _ in range(1, blocks):
                res_blocks.add(BasicBlock(filter_num, stride=1))
            return res_blocks

    def resnet18():
        return ResNet([2, 2, 2, 2])

    def resnet34():
        return ResNet([3, 4, 6, 3])


    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    tf.random.set_seed(2345)

    def main():
        # 获取一个resnet18的层堆叠模型
        model = resnet18()
        model.build(input_shape=(None, 32, 32, 3))
        # 创建一个梯度下降优化器
        optimizer = optimizers.Adam(learning_rate=1e-3)
        for epoch in range(50):
            for step, (X, y) in enumerate(train_db):
                with tf.GradientTape() as tape:
                    # 通过所有的残差网络操作
                    logits = model(X)
                    # 对标签进行独热编码,由于是100分类,所以分成100
                    y_onehot = tf.one_hot(y, depth=100)
                    # 交叉熵损失函数
                    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, step, float(loss))
            total_num = 0
            total_correct = 0
            for X, y in test_db:
                logits = model(X)
                # 多分类归一化
                prob = tf.nn.softmax(logits, axis=1)
                # 预测值
                predict = tf.argmax(prob, axis=1)
                predict = tf.cast(predict, dtype=tf.int32)
                correct = tf.cast(tf.equal(predict, y), dtype=tf.int32)
                correct = tf.reduce_sum(correct)
                total_num += X.shape[0]
                total_correct += int(correct)
            acc = total_correct / total_num
            print(epoch, acc)


    def preprocess(X, y):
        # 预处理
        # 将数据集置于[0,1]之间
        X = tf.cast(X, dtype=tf.float32) / 255
        y = tf.cast(y, dtype=tf.int32)
        return X, y


    # 获取一个100分类的图像数据集
    (X, y), (X_test, y_test) = datasets.cifar100.load_data()
    # 将标签数据集挤压掉一个维度
    y = tf.squeeze(y, axis=1)
    y_test = tf.squeeze(y_test, axis=1)
    print(X.shape, y.shape, X_test.shape, y_test.shape)
    # plt.imshow(X[2])
    # plt.show()
    train_db = tf.data.Dataset.from_tensor_slices((X, y))
    # 将训练数据集打散,再进行预处理后进行分批次获取
    train_db = train_db.shuffle(1000).map(preprocess).batch(256)
    test_db = tf.data.Dataset.from_tensor_slices((X_test, y_test))
    test_db = test_db.map(preprocess).batch(256)
    # 查看分批后的一批的形状和最大最小值
    sample = next(iter(train_db))
    print(sample[0].shape, sample[1].shape, tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))
    main()

运行结果

(50000, 32, 32, 3) (50000,) (10000, 32, 32, 3) (10000,)
(256, 32, 32, 3) (256,) tf.Tensor(0.0, shape=(), dtype=float32) tf.Tensor(1.0, shape=(), dtype=float32)
0 0 4.6072773933410645
0 100 4.583498001098633
0 0.02
1 0 4.563678741455078
1 100 4.188776016235352
1 0.0888
2 0 4.001941204071045
2 100 3.85906982421875
2 0.1392
3 0 3.638822555541992
3 100 3.376675605773926
3 0.1819
4 0 3.482811212539673
4 100 3.2017664909362793
4 0.2226
5 0 3.3416965007781982
5 100 2.872835636138916
5 0.2575
6 0 2.97902512550354
6 100 2.818711757659912
6 0.2958
7 0 2.6150143146514893
7 100 2.269937038421631
7 0.304
8 0 2.399383783340454
8 100 2.3401083946228027
8 0.3222
9 0 2.304474353790283
9 100 1.915872573852539
9 0.3141
10 0 2.126451253890991
10 100 1.5459128618240356
10 0.3178
11 0 1.8112839460372925
11 100 1.3028935194015503
11 0.3097
12 0 1.4375911951065063
12 100 1.2522438764572144
12 0.2964
13 0 1.3238859176635742
13 100 1.0940250158309937
13 0.2938
14 0 1.300029993057251
14 100 0.7527588605880737
14 0.2812
15 0 0.8890683650970459
15 100 0.645304262638092
15 0.2876
16 0 0.8162912726402283
16 100 0.46089187264442444
16 0.2855
17 0 0.535907506942749
17 100 0.412101686000824
17 0.2878
18 0 0.6782030463218689
18 100 0.3451211154460907
18 0.291
19 0 0.4079127907752991
19 100 0.3429681956768036
19 0.2812
20 0 0.4620097279548645
20 100 0.30339062213897705
20 0.2901
21 0 0.36842578649520874
21 100 0.18700706958770752
21 0.2928
22 0 0.3440532088279724
22 100 0.3221585154533386
22 0.296
23 0 0.1867597997188568
23 100 0.16435979306697845
23 0.2907
24 0 0.2601481080055237
24 100 0.24263298511505127
24 0.297
25 0 0.14730435609817505
25 100 0.22660230100154877
25 0.2912
26 0 0.31283998489379883
26 100 0.19935232400894165
26 0.2872
27 0 0.09959240257740021
27 100 0.12533828616142273
27 0.2873
28 0 0.13528719544410706
28 100 0.1601458340883255
28 0.2901
29 0 0.08132912218570709
29 100 0.13631102442741394
29 0.2981
30 0 0.2016129195690155
30 100 0.11876983940601349
30 0.2952
31 0 0.16555550694465637
31 100 0.06668324768543243
31 0.2913
32 0 0.17212697863578796
32 100 0.06371282041072845
32 0.3024
33 0 0.08938313275575638
33 100 0.12386852502822876
33 0.2956
34 0 0.14696258306503296
34 100 0.12041454017162323
34 0.2936
35 0 0.1451103240251541
35 100 0.09373585879802704
35 0.2934
36 0 0.2344374805688858
36 100 0.12745310366153717
36 0.3015
37 0 0.08963020145893097
37 100 0.14435136318206787
37 0.2937
38 0 0.0810844823718071
38 100 0.10973253846168518
38 0.2913
39 0 0.09135954082012177
39 100 0.07489153742790222
39 0.2934
40 0 0.061260394752025604
40 100 0.054804835468530655
40 0.291
41 0 0.16718783974647522
41 100 0.0777578130364418
41 0.2947
42 0 0.06593611091375351
42 100 0.03256486356258392
42 0.2938
43 0 0.08015348762273788
43 100 0.09424982964992523
43 0.2957
44 0 0.07563602924346924
44 100 0.09831291437149048
44 0.2993
45 0 0.11798415333032608
45 100 0.0758892148733139
45 0.2978
46 0 0.08733449131250381
46 100 0.042607150971889496
46 0.2924
47 0 0.10244087874889374
47 100 0.10055334120988846
47 0.2935
48 0 0.11981760710477829
48 100 0.08102770149707794
48 0.2929
49 0 0.07564711570739746
49 100 0.0723133534193039
49 0.3

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部