天天看點

卷積神經網絡(CNN)詳解

章節

  • Filter
  • 池化
  • Demo
  • 冷知識
  • 參考
卷積神經網絡(CNN)詳解

CNN 一共分為輸入,卷積,池化,拉直,softmax,輸出

卷積由互關運算(用Filter完成)和激活函數

CNN常用于圖像識别,在深度學習中我們不可能直接将圖檔輸入進去,向量是機器學習的通行證,我們将圖檔轉換為像素矩陣再送進去,對于黑白的圖檔,每一個點隻有一個像素值,若為彩色的,每一個點會有三個像素值(RGB)

互關運算其實就是做矩陣點乘運算,用下面的Toy Example說明:其實就是用kernel(filter)來與像素矩陣局部做乘積,如下圖,output的第一個陰影值其實是input和kernel的陰影部分進行矩陣乘法所得

卷積神經網絡(CNN)詳解

接下來引入一個參數(Stride),代表我們每一次濾波器在像素矩陣上移動的步幅,步幅共分為水準步幅和垂直步幅,下圖為水準步幅為2,垂直步幅為3的設定

卷積神經網絡(CNN)詳解

是以filter就不斷滑過圖檔,所到之處做點積,那麼,做完點積之後的shape是多少呢?假設input shape是32 * 32,stride 為1,filter shape 為4 * 4,那麼結束後的shape為29 * 29,計算公式是((input shape - filter shape) / stride ) + 1,記住在深度學習中務必要掌握每一層的輸入輸出。

那麼,假如stride改為3,那麼((32 - 4) / 3) + 1 不是整數,是以這樣的設定是錯誤的,那麼,我們可以通過padding的方式填充input shape,用0去填充,這裡padding設為1,如下圖,填充意味着輸入的寬和高都會進行增加2 * 1,那麼接下來的out shape 就是 ((32 + 2 * 1 - 4)/3) + 1,即為11 * 11

卷積神經網絡(CNN)詳解

接下來引入通道(channel),或為深度(depth)的介紹,一張彩色照片的深度為3,每一個像素點由3個值組成,我們的filter的輸入通道或者說是深度應該和輸入的一緻,舉例來說,一張照片32 * 32 * 3,filter可以設定為3 * 3 * 3,我們剛開始了解了一維的互關運算,三維無非就是filter拿出每一層和輸入的每一層做運算,最後再組成一個深度為3的輸出,這裡stride設定為1,padding也為1,是以輸出的shape為30 * 30 * 3。

卷積的時候是用多個filter完成的,一般經過卷積之後的output shape 的輸入通道(深度)為filter的數量,下圖為輸入深度為2的操作,會發現一個filter的輸出最終會相加,将它的深度壓為1,而不是一開始的輸入通道。這是一個filter,多個filter最後放在一起,最後的深度就是filter的數量了。

卷積神經網絡(CNN)詳解

Q & A:

1.卷積的意義是什麼呢?

其實如果用圖檔處理上的專業術語,被叫做銳化,卷積其實強調某些特征,然後将特征強化後提取出來,不同的卷積核關注圖檔上不同的特征,比如有的更關注邊緣而有的更關注中心地帶等等,如下圖:

卷積神經網絡(CNN)詳解

當完成幾個卷積層後(卷積 + 激活函數 + 池化):

卷積神經網絡(CNN)詳解

可以看出,一開始提取一些比較基礎簡單的特征,比如邊角,後面會越來越關注某個局部比如頭部甚至是整體

2.如何使得不同的卷積核關注不同的地方?

設定filter矩陣的值,比如input shape是4 * 4的,filter是2 * 2,filter是以一個一個小區域為機關,如果說我們想要關注每一個小區域的左上角,那麼将filter矩陣的第一個值設為1,其他全為0即可

總結來說,就是通過不斷改變filter矩陣的值來關注不同的細節,提取不同的特征

3.filter矩陣裡的權重參數是怎麼來的?

首先會初始化權重參數,然後通過梯度下降不斷降低loss來獲得最好的權重參數

4.常見參數的預設設定有哪些?

一般filter的數量(output channels)通常可以設定為2的指數次,如32,64,128,512,這裡提供一組比較穩定的搭配(具體還得看任務而定),F(kernel_size/filter_size)= 3,stride = 1,padding = 1;F = 5,stride = 1,Padding = 2;F = 1,S = 1,P = 0

5.參數數量?

舉例來說,filter的shape為5 * 5 * 3 ,一共6個,stride設定為1,padding設為2,卷積層為(32 * 32 * 6),注意卷積層這裡是代表最後的輸出shape,輸入shape為 32 * 32 * 3,那麼所需要的參數數量為 6 * (5 * 5 * 3 + 1),裡面 +1 的原因是原因是做完點積運算之後會加偏置(bias),當然這個參數是可以設定為沒有的

6.1 x 1 卷積的意義是什麼?

filter的shape為1 x 1,stride = 1,padding = 0,假如input為32 * 32 * 3,那麼output shape = (32 - 1) / 1 + 1 = 32,換言之,它并沒有改變原來的shape,但是filter的數量可以決定輸出通道,是以,1 x 1的卷積目的是改變輸出通道。可以對輸出通道進行升維或者降維,降維之後乘上的參數數量會減少,訓練會更快,記憶體占用會更少。升維或降維的技術在ResNet中同樣運用到啦(右圖):

卷積神經網絡(CNN)詳解

另外,其實1 x 1的卷積不過是實作多通道之間的線性疊加,如果你還記得上面多通道的意思,1 x 1 卷積改變卷積核的數量,無非就是使得不同的feature map進行線性疊加而已(feature map指的是最後輸出的每一層疊加出來的),因為通道的數量可以随時改變,1 x 1卷積也可以有跨通道資訊交流的内涵。

卷積好之後會用RELU進行激活,當然,這并不會改變原來的shape,這樣可以增加模型的非線性相容性,如果模型是線性的,很容易出問題,如XOR問題,接下來進行池化操作(Pooling),常見的是MaxPooling(最大池化),它基本上長得跟filter一樣,隻不過功能是選出區域内的最大值。假如我們的shape是4 * 4 ,池化矩陣的shape是2 * 2,那麼池化後的shape是2 * 2(4 / 2)

那麼,池化的意義是什麼?池化又可以被成為向下取樣(DownSample),經過池化之後shape會減小不少,如果說卷積的意義是提取出特征,那麼,池化的意義是在這些特征中取出最有代表性的特征,這樣可以降低像素的重複性,使得後續的卷積更有意義,同時可以降低shape,使得計算更為友善

當然,也還有平均池化(AveragePooling),這樣做試圖包含區域内的所有的特征,那麼,如果圖檔相鄰色素重複很多,那麼最大池化是不錯的,如果說一張圖檔很多不同的特征需要關注,那麼可以考慮平均池化

補充一下,可以給上述池操作加一個Global,這就意味着全局,而不是一個一個的小區域

!!!我的PyTorch完整Demo在:

https://colab.research.google.com/drive/1XMlSmiZ4FjHohptX-GSHsT_CFs4EoE6f?usp=sharing

進行卷積池化這樣一組操作多次之後再全部拉直送入全連接配接網絡,最後輸出10個值,然後優化它們與真實标簽的交叉熵損失,接下來用PyTorch和TensorFlow實操一下

首先先搭建一個簡單的PyTorch網絡,這裡采用Sequential容器寫法,當然也可以按照普遍的self.conv1 = ...,按照Sequential寫法更加簡潔明了,後面前向傳播函數也沒有采取x = ...不斷更新x,而是直接放進layer,周遊每一層即可,簡潔幹淨

# 導入庫
import torch
from torch import nn
import torchvision
from torchvision import datasets,transforms
import torch.nn.functional as F
import matplotlib.pyplot as plt
      
class Net(nn.Module):
   def __init__(self):
      super().__init__()
      self.layer = nn.Sequential(
                   nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3),nn.ReLU(),
                   nn.MaxPool2d(kernel_size=2),
                   nn.Conv2d(32,64,2),nn.ReLU(),
                   nn.MaxPool2d(2,2),
                   nn.Flatten(),
                   nn.Linear(64 * 6 * 6,10),nn.Softmax(),
                   )

   def forward(self,x):
      x = self.layer(x)
      return x
      

PyTorch中輸入必須為(1,1,28,28),這裡比tensorflow多了一個1,原因是Torch中有一個group參數,預設為1,是以可以不設定,如果為N,就會把輸入分為N個小部分,每一個部分進行卷積,最後再将結果拼接起來

搭建好網絡之後,建議先檢驗一下網絡和優化器參數

# 如果GPU沒有就會調到CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 
model = Net().to(device)
print(model.parameters)
# 訓練時還需要優化器(Optimizer)
optimizer = torch.optim.Adam(model.parameters())
print(optimizer)      
卷積神經網絡(CNN)詳解
i = torch.tensor([
    [1,2,3],
    [4,5,6]
])
# 輸出最大的值和它的索引
print(i.max(1,keepdim=True))
# torch.return_types.max(values=tensor([[3],[6]]), indices=tensor([[2],[2]]))
# 一般隻要索引的話:
print(i.max(1,keepdim=True))[1]
# tensor([[2],
#         [2]])
      
a = torch.tensor([1,2,3,4])
b = torch.tensor([[1],
                  [-1],
                  [-2],
                  2])
# 将a轉換為與b形狀相同
a.view_as(b)
print(a)
# tensor([[1],
#        [2],
#        [3],
#        [4]])

# 相對于numpy的equal函數,判斷tensor裡每一個值是否相等
# 輸出為True 或者 False
print(b.eq(a.view_as(b)))

# tensor([[ True],
#        [False],
#        [False],
#        [False]])

# 求和用來判斷損失和準确率
# True --> 1,False --> 0
print(b.eq(a.view_as(b)).sum())

# tensor(1)

# 最後将PyTorch的tensor轉換為Python中标準值
print(b.eq(a.view_as(b)).sum().item())
# 1      
# 下載下傳訓練和測試資料集
# transforms函數可以對下載下傳的資料做一些預處理
# Compose 指的是将多個transforms操作組合在一起
# ToTensor 是将[0,255] 範圍 轉換為[0,1]
# 灰階圖檔(channel=1),是以每一個括号内隻有一個值,前者代表mean,後者std(标準差)
# 彩色圖檔(channel=3),是以每一個括号内有三個值,如
# transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,),(0.5,))
])

data_train = datasets.MNIST(root="填自己的主路徑",
                            transform=transform,
                            train=True,
                            download=True)

data_test = datasets.MNIST(root="填自己的主路徑",
                           transform=transform,
                           train=False)      
# 加載資料集
# Load Data
train_loader = torch.utils.data.DataLoader(dataset=data_train,
                                                batch_size=64,
                                                shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=data_test,
                                               batch_size=64,
                                               shuffle=True)      

每一次新的batch中都需要梯度清零,否則的話梯度就會跨batch

def train(model,device,train_loader,optimizer,epoch):
    model.train()
    for batch_idx,(data,target) in enumerate(train_loader):
        data,target = data.to(device),target.to(device)
        optimizer.zero_grad() # 梯度清零
        output = model(data)
        loss = F.nll_loss(output,target) # negative likelihood loss
        loss.backward() # 誤差反向傳播
        optimizer.step() # 參數更新
        if (batch_idx + 1) % 200 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item())) # .item()轉換為python值
    return loss.item()      

因為測試的時候不需要更新參數,是以with torch.no_grad()

# 定義測試函數
def test(model, device, test_loader):
    model.eval()
    test_loss,correct = 0 , 0
    with torch.no_grad(): # 不track梯度
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction = 'sum') # 将一批的損失相加
            pred = output.max(1, keepdim = True)[1] # 找到機率最大的下标
            correct += pred.eq(target.view_as(pred)).sum().item() # equals
    test_loss /= len(test_loader.dataset)
    acc = correct / len(test_loader.dataset)
    print("\nTest set: Average loss: {:.4f}, Accuracy: {} ({:.0f}%) \n".format(
        test_loss, acc ,
        100.* correct / len(test_loader.dataset)
            ))

    return acc      
卷積神經網絡(CNN)詳解
train_ = []
test_acc = []
for epoch in range(1,EPOCHS+1):
    train_loss = train(model,DEVICE,train_loader,optimizer,epoch)
    acc = test(model,DEVICE,test_loader)
    train_.append(train_loss)
    test_acc.append(acc)

visualize(train_,[i for i in range(20)],"loss")
visualize(test_acc,[i for i in range(20)],"accuracy")
      
卷積神經網絡(CNN)詳解

接下來使用tensorflow-gpu 1.14.0再實操一下

from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import matplotlib as mpl
import sys 

# solve could not create cudnn handle: CUDNN_STATUS_INTERNAL_ERROR
config = tf.compat.v1.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.compat.v1.InteractiveSession(config=config)

# 不同庫版本,使用此代碼塊檢視
print(sys.version_info)
for module in mpl,np,tf,keras:
  print(module.__name__,module.__version__)

'''
sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)
matplotlib 3.3.4
numpy 1.16.0
tensorflow 1.14.0
tensorflow.python.keras.api._v1.keras 2.2.4-tf
'''
# If you get numpy futurewarning,then try numpy 1.16.0

# load train and test
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255

# 1 Byte = 8 Bits,2^8 -1 = 255。[0,255]代表圖上的像素,同時除以一個常數進行歸一化。1 就代表全部塗黑。0 就代表沒塗

# Make sure images have shape (28, 28, 1)
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)

# CNN 的輸入方式必須得帶上channel,這裡擴充一下次元

# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)

# y 屬于 [0,9]代表手寫數字的标簽,這裡将它轉換為0-1表示,可以類比one-hot,舉個例子,如果是2

# [[0,0,1,0,0,0,0,0,0,0]……]

model = keras.Sequential(
    [
        keras.Input(shape=(28, 28, 1)),
        keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation="relu"),
        keras.layers.MaxPooling2D(pool_size=(2, 2)),
        keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation="relu"),
        keras.layers.MaxPooling2D(pool_size=(2, 2)),
        keras.layers.Flatten(),
        keras.layers.Dense(units=10, activation="softmax"),
    ]
)

# 注意,Conv2D裡面有激活函數不代表在卷積和池化的時候進行。而是在DNN裡進行,最後拉直後直接接softmax就行


# kernel_size 代表濾波器的大小,pool_size 代表池化的濾波器的大小

model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])

model.summary()

history = model.fit(x_train, y_train, batch_size=128, epochs=15, validation_split=0.1) #10層交叉檢驗
score = model.evaluate(x_test, y_test)
print("Test loss:", score[0])
print("Test accuracy:", score[1])

# Test loss: 0.03664601594209671
# Test accuracy: 0.989300012588501

# visualize accuracy and loss
def plot_(history,label):
    plt.plot(history.history[label])
    plt.plot(history.history["val_" + label])
    plt.title("model " + label)
    plt.ylabel(label)
    plt.xlabel("epoch")
    plt.legend(["train","test"],loc = "upper left")
    plt.show()

plot_(history,"acc")
plot_(history,"loss")      

在機器學習中畫精确度和loss的圖很有必要,這樣可以發現自己的代碼中是否存在問題,并且将這個問題可視化

卷積神經網絡(CNN)詳解
卷積神經網絡(CNN)詳解

We don't minimize total loss to find the best function.

我們采取将資料打亂并分組成一個一個的mini-batch,每個資料所含的資料個數也是可調的。關于epoch

卷積神經網絡(CNN)詳解

将一個mini-batch中的loss全部加起來,就更新一次參數。一個epoch就等于将所有的mini-batch都周遊一遍,并且經過一個就更新一次參數。

如果epoch設為20,就将上述過程重複20遍

這裡再細談一下batch 和 epoch

卷積神經網絡(CNN)詳解

由圖可知,當batch數目越多,分的越開,每一個epoch的速度理所應當就會**上升**的,當batch_size = 1的時候,1 epoch 就更新參數50000次 和 batch_size = 10的時候,1 epoch就更新5000次,那麼如果更新次數相等的話,batch_size = 1會花**166s**;batch_size = 10每個epoch會花**17s**,總的時間就是**17 * 10 = 170s**。其實batch_size = 1不就是[SGD](../optimization/GD.md)。随機化很不穩定,相對而言,batch_size = 10,收斂的會更穩定,時間和等于1的差不多。那麼何樂而不為呢?

肯定有人要問了?随機速度快可以了解,看一眼就更新一次參數

卷積神經網絡(CNN)詳解

為什麼batch_size = 10速度和它差不多呢?按照上面來想,應該是一個mini-batch結束再來下一個,這樣慢慢進行下去,其實沒理由啊。

接下來以batch_size = 2來介紹一下

卷積神經網絡(CNN)詳解

學過線性代數應該明白,可以将同次元的向量拼成矩陣,來進行矩陣運算,這樣每一個mini-batch都在同一時間計算出來,即為平行運算

所有平行運算GPU都能進行加速。

那麼,好奇的是到底計算機看到了什麼?是一個一個的數字嗎?

卷積神經網絡(CNN)詳解

其實這件事情很反直覺,原以為計算機是看一張一張的圖檔,可是這個很難看出是單個數字而是數字集,那麼我們試試看最大化像素

卷積神經網絡(CNN)詳解

其實左下角的6其實蠻像的耶。

https://zh-v2.d2l.ai/ https://demo.leemeng.tw/ http://cs231n.stanford.edu/ https://www.youtube.com/watch?v=FrKWiRv254g&list=PLJV_el3uVTsPy9oCRY30oBPNLCo89yu49&index=19

繼續閱讀