一文入門卷積神經網絡

爲什麼要用CNN來處理圖片

卷積神經網絡近年來在計算機視覺中大放異彩,是什麼特點讓卷積神經網絡在圖像處理方面大火呢?
首先我們來看一張圖片:
在這裏插入圖片描述
這是一張普通的表示全連接神經網絡的結構圖。大家可以看到全連接神經網絡中,每一個節點都與前一層所有的節點想連接。鑑於圖像數據和其他數據的區別,我們做個簡單的計算即可發現全連接神經網絡處理圖像的弊端。
比如一個像素很低3232的圖片數據,那麼它的輸入既有1024個節點,那麼神經網絡的第二層的第一個節點則需要1024個權重w來和前一層進行連接,同理第二層的第二個節點也需要1024個權重w和第一層所有節點連接…… 並且此處第一層的權重和第二層的權重之間是不同的,是沒有聯繫的。所以可想而知,這個神經網絡的數據量和計算量將是十分龐大的。並且大家都知道,現在隨着對圖片清晰度要求的逐步提高,圖片早已不可能是3232像素的圖片了,那對全連接層處理圖片來說將是災難。

卷積神經網絡的不同之處在下圖可以直觀感受到:
在這裏插入圖片描述
卷積神經網絡的優點既體現在此處,如果將全連接神經每個節點的處理比作一次掃描的話。那麼全連接層是一次掃描全部節點,且每個節點的觀察方式還不一樣,卷積層則是一次掃描鄰近的部分節點,節點之間的觀察方式是一樣的。
這裏我強調了“鄰近的”這三個字,是因爲對於圖像數據其實還有一個特點,就是相鄰的像素點之間的關係比較大,距離較遠的像素點之間的關係可能就比較小,所有研究這相鄰的像素點之間的關係的意義其實更大。

下面這張圖片很形象的表達的卷積神經網絡的優勢:
卷積神經網絡

卷積操作簡述

其實最早卷積操作出現在在計算機視覺(CV)當中。那是爲了得到不同效果的圖片設計了不同的卷積操作,
比如爲了得到銳化後的圖片
Sharpen
爲了得到模糊化的圖片
Blur
爲了得到邊緣化的圖片:
Edge Detect
在此處的應用中,卷積核中的權重是一開始就已經固定好了,並且有着屬於自己的特殊含義,因爲不同卷積核得到了不同的結果。而在卷積神經網絡中,我們的目的就是爲了經過不斷訓練得到卷積核中的權重w,訓練的過程是基於SGD(Stochastic Gradient Descent,隨機梯度下降)得到的,並且權重w也沒有特定的目的。
而是爲了不斷提取特徵,卷積核就相當於與一種觀察方式。比如從底層像素級的概念,逐步到模塊級別的概念等

low level Feature–> Mid level Feature–>High Level Feature

相乘再累加?

我們都知道卷積操作並不神祕,就是卷積核中的對應元素與圖片上的對應元素進行相乘再累加,那爲什麼要相乘再累加呢,這裏就要說到信號學上面的一個概念,信號學上將兩個函數之間的操作定義爲卷積。下面的公式就定義一個卷積操作,
在這裏插入圖片描述
在連續的函數裏面,卷積體現在函數的積分上,例如這個公式中對τ積分。這一點在離散上就體現爲相乘再累加,同時對於這個公式來說,t不一樣,積分得到結果不一樣,改變t就相當於在卷積中移動卷積核重新相乘再累加。
卷積
信號學中關於卷積的介紹和推導遠不止這麼簡單,這只是我目前粗淺的理解。

卷積層中重要的操作與概念

在實現卷積中,有一些我們不得不瞭解操作和概念,當我們瞭解了這些之後,才能更好的去實戰。

兩個重要操作 Padding & Stride

Padding&Stride
經過卷積層的處理之後得到的輸出的shape要比輸入的shape要小一點,爲了使輸入和輸出保持一致,,就需要Padding操作。從圖中也可以看出,所謂的padding就是將輸入的shape變大一點。
而Stride操作代表了卷積核移動的步長,比如stride = 1就代表一次移動一格。
通過合理的調節Padding 和 stride 就可以使得輸出的shape滿足自己的需求。

兩個重要概念 池化 & 採樣

圖片在經過卷積層的處理得到特徵圖(feature map)之後,爲了進一步得到圖像的高階特徵,這時候就會用到池化層。

平均池化:傾向於保留突出背景特徵
最大池化:傾向於保留突出紋理特徵

池化層所用到的技術就是採樣。採樣我的簡單理解就是對一個feature map放大或者縮小,放大就叫做上採樣,縮小就叫做下采樣。在這個過程中採取的方式不同,所着重保留的圖像的特徵也不同。

採樣
下采樣 Polling
上採樣 upsample
Max Pooling 選出最大值
Avg Pooling 求平均值
nearest
bilinear

下圖可以很直觀的感受兩種下采樣的區別:
Pooling

理解這個過程的Gradient

作爲Deep Learning中最核心的部分梯度下降Gradient理解是十分重要的。
Gradient
上面的公式就反映了卷積過程的梯度問題是可以解決的,我們理解這個過程就行,tensorflow提供了封裝好的梯度下降的工具,我們直接用就行。

CNN實戰

上面介紹了這麼多,終於要開始實戰了。
在寫代碼之前,有必要有一個意識,就是對如何搭建網絡要有一個大致的瞭解,換句話說也就是網絡的結構應該是怎樣的,大致可以分爲四步:

準備數據
搭建網絡
訓練網絡
測試網絡

接下來用CIFAR100數據集來實戰CNN,首先我們則必須瞭解一下這個數據集。

CIFAR數據集由60000個32x32彩色圖像組成,共有100個類,每個類包含600個圖像。每類各有500個訓練圖像和100個測試圖像。CIFAR-100中的100個類被分成20個超類。每個圖像都帶有一個“精細”標籤(它所屬的類)和一個“粗糙”標籤(它所屬的超類)
下面是截取的一部分類別列表
CIFAR100

我們繼續照着之前 準備數據–>搭建網絡–>訓練網絡–>測試網絡 四步來編寫代碼

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os
# 不輸出通知信息和警告信息
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

tf.random.set_seed(2345)

# 改變數據的格式
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 和 y_test 的維度爲( ,1) ( ,1) tf.squeeze 變換維度
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)


train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(128)

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:', sample[0].shape, sample[1].shape,
      tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))

#第二步:搭建網絡的結構
conv_layers = [# 5 units of conv + max pooling
    # unit 1
    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'),
    #

    # unit 2
    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'),

    # unit 3
    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'),

    # unit 4
    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'),

    # unit 5
    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():
    # 整個網絡分爲兩個部分,卷積層和全連接層
    # 卷積層, 效果:[b, 32, 32, 3] => [b, 1, 1, 512]
    conv_net = Sequential(conv_layers)

    #全連接層的網絡搭建, 效果:[b, 512] => [100]
    fc_net = Sequential([
        layers.Dense(256, activation=tf.nn.relu),
        layers.Dense(128, activation=tf.nn.relu),
        layers.Dense(100, activation=None),
    ])

    conv_net.build(input_shape=[None, 32, 32, 3])
    fc_net.build(input_shape=[None, 512])
    optimizer = optimizers.Adam(lr=1e-4)

# 第三步,訓練網絡
    # [1, 2] + [3, 4] => [1, 2, 3, 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:
                # [b, 32, 32, 3] => [b, 1, 1, 512]
                out = conv_net(x)
                # flatten, => [b, 512]
                out = tf.reshape(out, [-1, 512])
                # [b, 512] => [b, 100]
                logits = fc_net(out)
                # [b] => [b, 100]
                y_onehot = tf.one_hot(y, depth=100)
                # compute loss
                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, 'loss:', 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)
            pred = tf.argmax(prob, axis=1)
            pred = tf.cast(pred, dtype=tf.int32)

            correct = tf.cast(tf.equal(pred, 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:', acc)


if __name__ == '__main__':
    main()

可參考下圖來理解上面卷積神經網絡的架構。
網絡結構

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章