1. 程式人生 > >機器學習 -- >深度學習-- >卷積神經網路(CNN)

機器學習 -- >深度學習-- >卷積神經網路(CNN)

上一篇我們詳細講解了人工神經網路以及DNN的原理。CNN主要應用在影象處理方面。這一講我們將詳細講解卷積神經網路CNN的原理以及在深度學習框架pytorch上的實現。

在講CNN之前我們需要了解這麼幾個問題?
①人工神經網路能用到計算機視覺上嗎?
答:能
②那麼為什麼還需要卷積神經網路?
答:上一講我們講到人工神經網路中,每一層都是全連線,就是相鄰的層之間,神經元必須兩兩之間彼此連線,而影象資料維度很高,這就導致神經網路中神經元個數很多,那麼引數w的個數就會很多,神經網路的目的就是學習到引數w,這麼多的w會很耗費計算機的運算資源,而且很容易產生過擬合。所以有時候引數越多並不一定是好事。
③卷積神經網路和人工神經網路的差異在哪?
答:上一講我們說了人工神經網路的結果基本上是全連線,就是每兩層之間都是wx+b然後在啟用函式做個非線性變化。在卷積神經網路中,我們會多出一個卷積(convolution)層。

卷積神經網路的層級結構:
保持了層級網路結構。
不同層次有不同形式(運算)與功能
這裡寫圖片描述

主要是一下層次:
資料輸入層/ Input layer
卷積計算層/ Conv layer
Relu啟用層/ Relu layer
池化層/ Pooling layer
全連線層/ FC layer
下面詳細講下每一層內部原理

資料輸入層:需要對資料進行預處理,常見有三種資料處理方式
1)去均值:把輸入資料各個維度都中心化到0
2)歸一化:幅度歸一化到同一的範圍
3)PCA:用PCA降維

卷積計算層:這一層極大降低了人工神經網路中的引數個數
這裡寫圖片描述

上圖左邊是一個3個通道((RGB)的32*32的影象,假設我們取一個3*3的滑動視窗,就是每個滑動視窗中有9個畫素點,我們可以做這樣的比喻:右圖有五個小孩子(神經元),每個孩子有一組不同的價值觀(一組權重) ,每個孩子都以上面滑動視窗每次滑動2個步長來看這張影象,每個滑動視窗內都能根據他的權重得出這個滑動視窗的值,並且每次在滑動時,每個孩子的權重保持不變,這樣這個五個孩子就把這張圖片卷積成五張不同的圖片資訊。


以上的說明中涉及到幾個概念,我們得詳細的解釋下:
深度/depth:就是上面說的孩子個數(有幾個不同引數組數,然後卷積成幾個不同的影象資訊)
步長/stride:滑動視窗在滑動過程中,每次滑動幾個步長。
填充值/zero-padding:在滑動過程中不一定能正好從左邊滑到右邊,滑到右邊時,很有可能滑到外面,這樣我們可以在外層再新增一圈0,使得正好滑到右邊。
這裡寫圖片描述
上面這張圖很好的說明整個卷積過程,滑動串列埠大小取3*3,9個畫素點,並且在原始的影象的進行了zero-padding,一組孩子的權重矩陣看影象做左上方的一個滑動視窗,計算出這個滑動視窗結果為-8,.

引數共享機制:假設每個神經元連線資料視窗的權重是固定的。


固定每個神經元連線權重,可以看做模板:每個神經元只關注一個特徵。
需要估算的權重個數大大減少:AlexNet 1億=>3.5w(而上圖需要學習的引數個數為9個,前後層的通道都為1)
一組固定的權重和不同的視窗做內積:卷積

上圖的卷積操作只是二維的,那麼對於多層的feature_map 來說,該如何操作?
這裡寫圖片描述
我們假設輸入的shape[batch_size,w_inp,w_inp,channels_inp], 卷積核的shape[w,h,channels_inp,channels_out],則引數的個數為channels_inpwhchannels_out,也即是在做卷積對映時,同一個feature_map 在輸入的不同channel上,引數是不一樣的,這點比較重要。

啟用層:將卷積層輸出的結果做非線性對映。
啟用函式通常有這些:Sigmoid,Tanh(雙曲正切) ,ReLu,Leaky Relu,ELU,Maxout
這裡寫圖片描述

啟用層(實際經驗):
1)CNN儘量不用sigmoid函式!因為典型CNN神經網路都是一層一層疊加,這樣在反向更新引數時容易產生梯度消失現象!雙曲正切同樣有這個問題。
2)首先試試Relu,Relu函式在資料小於0時,梯度為0直接將資料過濾掉,只有在資料大於0時才能通過且由直線斜率可以看出可以學習的很快。
3)但是有時候,在用Relu時,資料全小於0,這樣就行不通了,這個時候可以用Leaky Relu或Maxout
4)某些情況下tanh效果不錯,但是很少。

池化層/Pooling layer
夾在連續的卷積層中間
壓縮資料和引數數量,減小過擬合
這裡寫圖片描述
通常有兩種pooling方式
Max Pooling:取視窗內最大值作為pooling的結果值
average Pooling:取視窗內平均值作為Pooling的結果值
這裡寫圖片描述

全連線層
兩層之間所有神經元都有權重連線
通常全連線層在卷積神經網路尾部

綜上我們可以知道一般CNN的結構依次是:
Input–>[[Conv->Relu]*N->Pool?]*M–>[FC->Relu]*k–>FC
*N或者*M,*K:表示有連續數個這樣的結構
?:表示可能有可能無這個結構

CNN訓練演算法:
1)同一般的機器學習演算法類似,定義loassFunction,衡量和實際結果之間的差距。
2)找到最小化損失函式的w和b,CNN中用的是SGD
3)SGD需要計算W和b的偏導
4)BP演算法用來計算偏導(鏈式求導法)

下面說說CNN的優缺點:
優點:1)共享卷積核,對處理高維資料無壓力。
2)無需手動選取特徵,訓練好權重,既得特徵
3)深層次的網路抽取影象資訊豐富,表達效果好。
缺點:1)需要調參,需要大量樣本,訓練最好用GPU
2)物理意義不明確

幾個經典的CNN模型
詳細說說LeNet和resNet模型:
LeNet神經網路:
這裡寫圖片描述

這裡寫圖片描述
我們知道神經網路越深越難訓練,因為越深梯度消失的越嚴重。但是在resNet神經網路中,設計一種辦法解決這個問題,使得神經網路的層數更深。
這裡寫圖片描述

在正向傳播時每一層得出的結果為上一層累積結果加上前幾層的輸入資料x,因為是求和,這樣在反向更新引數時,會有兩條路徑更新引數,減少了梯度消失的現象。

下面在pytorch上定義一個類似leNet神經網路

#coding=utf-8
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable

class Net(nn.Module):
    #定義Net的初始化函式,這個函式定義了該神經網路的基本結構
    def __init__(self):
        super(Net, self).__init__() #複製並使用Net的父類的初始化方法,即先執行nn.Module的初始化函式
        self.conv1 = nn.Conv2d(1, 6, 5) # 定義conv1函式的是影象卷積函式:輸入為影象(1個頻道,即灰度圖),輸出為 6張特徵圖, 卷積核為5x5正方形
        self.conv2 = nn.Conv2d(6, 16, 5)# 定義conv2函式的是影象卷積函式:輸入為6張特徵圖,輸出為16張特徵圖, 卷積核為5x5正方形
        self.fc1 = nn.Linear(16*5*5, 120) # 定義fc1(fullconnect)全連線函式1為線性函式:y = Wx + b,並將16*5*5個節點連線到120個節點上。
        self.fc2 = nn.Linear(120, 84)#定義fc2(fullconnect)全連線函式2為線性函式:y = Wx + b,並將120個節點連線到84個節點上。
        self.fc3 = nn.Linear(84, 10)#定義fc3(fullconnect)全連線函式3為線性函式:y = Wx + b,並將84個節點連線到10個節點上。

    #定義該神經網路的向前傳播函式,該函式必須定義,一旦定義成功,向後傳播函式也會自動生成(autograd)
    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) #輸入x經過卷積conv1之後,經過啟用函式ReLU,使用2x2的視窗進行最大池化Max pooling,然後更新到x。
        x = F.max_pool2d(F.relu(self.conv2(x)), 2) #輸入x經過卷積conv2之後,經過啟用函式ReLU,使用2x2的視窗進行最大池化Max pooling,然後更新到x。
        x = x.view(-1, self.num_flat_features(x)) #view函式將張量x變形成一維的向量形式,總特徵數並不改變,為接下來的全連線作準備。
        x = F.relu(self.fc1(x)) #輸入x經過全連線1,再經過ReLU啟用函式,然後更新x
        x = F.relu(self.fc2(x)) #輸入x經過全連線2,再經過ReLU啟用函式,然後更新x
        x = self.fc3(x) #輸入x經過全連線3,然後更新x
        return x

    #使用num_flat_features函式計算張量x的總特徵量(把每個數字都看出是一個特徵,即特徵總量),比如x是4*2*2的張量,那麼它的特徵總量就是16。
    def num_flat_features(self, x):
        size = x.size()[1:] # 這裡為什麼要使用[1:],是因為pytorch只接受批輸入,也就是說一次性輸入好幾張圖片,那麼輸入資料張量的維度自然上升到了4維。【1:】讓我們把注意力放在後3維上面
        num_features = 1
        for s in size:
            num_features *= s
        return num_features


net = Net()
net

# 以下程式碼是為了看一下我們需要訓練的引數的數量
print net
params = list(net.parameters())

k=0
for i in params:
    l =1
    print "該層的結構:"+str(list(i.size()))
    for j in i.size():
        l *= j
    print "引數和:"+str(l)
    k = k+l

print "總引數和:"+ str(k)

下面是在pytorch上訓練一個類似Lenet神經網路

#coding=utf-8
import torch
import torchvision
import torchvision.transforms as transforms

#ToTensor是指把PIL.Image(RGB) 或者numpy.ndarray(H x W x C) 從0到255的值對映到0到1的範圍內,並轉化成Tensor格式。
# torchvision輸出的是PILImage,值的範圍是[0, 1].
# 我們將其轉化為tensor資料,並歸一化為[-1, 1]。
#Normalize(mean,std)是通過下面公式實現資料歸一化
####channel=(channel-mean)/std
transform=transforms.Compose([transforms.ToTensor(),
                              transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                              ])

#訓練集,將相對目錄./data下的cifar-10-batches-py資料夾中的全部資料(50000張圖片作為訓練資料)載入到記憶體中,若download為True時,會自動從網上下載資料並解壓
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)

#將訓練集的50000張圖片劃分成12500份,每份4張圖,用於mini-batch輸入。shffule=True在表示不同批次的資料遍歷時,打亂順序。num_workers=2表示使用兩個子程序來載入資料
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=False, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# print len(trainset)
# print len(trainloader)
#
#
#
# #下面是程式碼只是為了給小夥伴們顯示一個圖片例子,讓大家有個直覺感受。
# # functions to show an image
# import matplotlib.pyplot as plt
# import numpy as np
# #matplotlib inline
# def imshow(img):
#     img = img / 2 + 0.5 # unnormalize
#     npimg = img.numpy()
#     plt.imshow(np.transpose(npimg, (1,2,0)))
#     plt.show()
#
#
#
#
# # show some random training images
# dataiter = iter(trainloader)
# images, labels = dataiter.next()
#
# # print images
# imshow(torchvision.utils.make_grid(images))
# # print labels
# print(' '.join('%5s'%classes[labels[j]] for j in range(4)))

from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        self.conv1=nn.Conv2d(3,6,5)
        self.pool=nn.MaxPool2d(2,2)
        self.conv2=nn.Conv2d(6,16,5)
        self.fc1=nn.Linear(16*5*5,120)
        self.fc2=nn.Linear(120,84)
        self.fc3=nn.Linear(84,10)

    def forward(self,x):
        x=self.pool(F.relu(self.conv1(x)))
        x=self.pool(F.relu(self.conv2(x)))
        x=x.view(-1,16*5*5)
        x=F.relu(self.fc1(x))
        x=F.relu(self.fc2(x))
        x=self.fc3(x)
        return x

net=Net()
net.cuda()
import torch.optim as optim
criterion=nn.CrossEntropyLoss()
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

for epoch in range(10):
    running_loss=0.0
    for i,data in enumerate(trainloader,0):
        inputs,labels=data
        inputs,labels=Variable(inputs.cuda()),Variable(labels.cuda())
        optimizer.zero_grad()
        outputs=net(inputs)
        loss=criterion(outputs,labels)
        loss.backward()
        optimizer.step()

        running_loss+=loss.data[0]
        if i%2000==1999:
            print '[%d,%5d] loss:%.3f' %(epoch + 1,i+1,running_loss/2000)
            running_loss=0.0
print 'Finished Training'
correct = 0
total = 0
for data in testloader:
    images, labels = data
    inputs,labels=Variable(images.cuda()),Variable(labels.cuda())
    outputs = net(inputs)
    #print outputs.data
    _, predicted = torch.max(outputs.data, 1)  #outputs.data是一個4x10張量,將每一行的最大的那一列的值和序號各自組成一個一維張量返回,第一個是值的張量,第二個是序號的張量。
    total += labels.size(0)
    predicted=Variable(predicted.cuda())
    correct += (predicted == labels).sum()   #兩個一維張量逐行對比,相同的行記為1,不同的行記為0,再利用sum(),求總和,得到相同的個數。

#print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
print 'Accuracy of the network on the 10000 test images:', (100 * correct / total)