CNN經典之LeNet網絡+PyTorch復現

一、前情說明:

寫在前面的話

本系列博客在於彙總CSDN的精華帖,類似自用筆記,不做學習交流,方便以後的複習回顧,博文中的引用都註明出處,並點贊收藏原博主。

本博客大致分爲兩部分,第一部是轉載於其他平臺的關於LeNet的講解,第二部分是自己對網絡的復現,包括:數據集加載和預處理,網絡構建,模型測試等

二、CNN經典模型:LeNet

在這裏插入圖片描述
近幾年來,卷積神經網絡(Convolutional Neural Networks,簡稱CNN)在圖像識別中取得了非常成功的應用,成爲深度學習的一大亮點。CNN發展至今,已經有很多變種,其中有幾個經典模型在CNN發展歷程中有着里程碑的意義,它們分別是:LeNet、Alexnet、Googlenet、VGG、DRL等,接下來將分期進行逐一介紹。
在之前的文章中,已經介紹了卷積神經網絡(CNN)的技術原理,細節部分就不再重複了,有興趣的同學再打開鏈接看看(卷積神經網絡詳解),在此簡單回顧一下CNN的幾個特點:局部感知、參數共享、池化。

1、局部感知

人類對外界的認知一般是從局部到全局、從片面到全面,類似的,在機器識別圖像時也沒有必要把整張圖像按像素全部都連接到神經網絡中,在圖像中也是局部周邊的像素聯繫比較緊密,而距離較遠的像素則相關性較弱,因此可以採用局部連接的模式(將圖像分塊連接,這樣能大大減少模型的參數),如下圖所示:
在這裏插入圖片描述
2、參數(權值)共享

每張自然圖像(人物、山水、建築等)都有其固有特性,也就是說,圖像其中一部分的統計特性與其它部分是接近的。這也意味着這一部分學習的特徵也能用在另一部分上,能使用同樣的學習特徵。因此,在局部連接中隱藏層的每一個神經元連接的局部圖像的權值參數(例如5×5),將這些權值參數共享給其它剩下的神經元使用,那麼此時不管隱藏層有多少個神經元,需要訓練的參數就是這個局部圖像的權限參數(例如5×5),也就是卷積核的大小,這樣大大減少了訓練參數。如下圖
在這裏插入圖片描述
3、池化

隨着模型網絡不斷加深,卷積核越來越多,要訓練的參數還是很多,而且直接拿卷積核提取的特徵直接訓練也容易出現過擬合的現象。回想一下,之所以對圖像使用卷積提取特徵是因爲圖像具有一種“靜態性”的屬性,因此,一個很自然的想法就是對不同位置區域提取出有代表性的特徵(進行聚合統計,例如最大值、平均值等),這種聚合的操作就叫做池化,池化的過程通常也被稱爲特徵映射的過程(特徵降維),如下圖:
在這裏插入圖片描述
回顧了卷積神經網絡(CNN)上面的三個特點後,下面來介紹一下CNN的經典模型:手寫字體識別模型LeNet5。
LeNet5誕生於1994年,是最早的卷積神經網絡之一, 由Yann LeCun完成,推動了深度學習領域的發展。在那時候,沒有GPU幫助訓練模型,甚至CPU的速度也很慢,因此,LeNet5通過巧妙的設計,利用卷積、參數共享、池化等操作提取特徵,避免了大量的計算成本,最後再使用全連接神經網絡進行分類識別,這個網絡也是最近大量神經網絡架構的起點,給這個領域帶來了許多靈感。
LeNet5的網絡結構示意圖如下所示:
在這裏插入圖片描述
LeNet5由7層CNN(不包含輸入層)組成,上圖中輸入的原始圖像大小是32×32像素,卷積層用Ci表示,子採樣層(pooling,池化)用Si表示,全連接層用Fi表示。下面逐層介紹其作用和示意圖上方的數字含義。

1、C1層(卷積層):6@28×28

該層使用了6個卷積核,每個卷積核的大小爲5×5,這樣就得到了6個feature map(特徵圖)。

(1)特徵圖大小

每個卷積核(5×5)與原始的輸入圖像(32×32)進行卷積,這樣得到的feature map(特徵圖)大小爲**(32-5+1)×(32-5+1)= 28×28**
卷積過程如下圖所示:
在這裏插入圖片描述
卷積核與輸入圖像按卷積核大小逐個區域進行匹配計算,匹配後原始輸入圖像的尺寸將變小,因爲邊緣部分卷積核無法越出界,只能匹配一次,如上圖,匹配計算後的尺寸變爲Cr×Cc=(Ir-Kr+1)×(Ic-Kc+1),其中Cr、Cc,Ir、Ic,Kr、Kc分別表示卷積後結果圖像、輸入圖像、卷積核的行列大小。

(2)參數個數

由於參數(權值)共享的原因,對於同個卷積核每個神經元均使用相同的參數,因此,參數個數爲(5×5+1)×6= 156(如果都是獨立參數,應該是28×28×(5×5×1)×6),其中5×5爲卷積核參數,1爲偏置參數bias(這個可以翻到神經元激活那部分,Y=WX+b,相當於加了一個截距),

(3)連接數

卷積後的圖像大小爲28×28,因此每個特徵圖有28×28個神經元,每個卷積核參數爲(5×5+1)×6,因此,該層的連接數爲(5×5+1)×6×28×28=122304

2、S2層(下采樣層,也稱池化層):6@14×14

(1)特徵圖大小

這一層主要是做池化或者特徵映射(特徵降維),池化單元爲2×2,因此,6個特徵圖的大小經池化後即變爲14×14。回顧本文剛開始講到的池化操作,池化單元之間沒有重疊,在池化區域內進行聚合統計後得到新的特徵值,因此經2×2池化後,每兩行兩列重新算出一個特徵值出來,相當於圖像大小減半,因此卷積後的28×28圖像經2×2池化後就變爲14×14。
這一層的計算過程是:2×2 單元裏的值相加,然後再乘以訓練參數w,再加上一個偏置參數b(每一個特徵圖共享相同的w和b),然後取sigmoid值(S函數:0-1區間),作爲對應的該單元的值。卷積操作與池化的示意圖如下:
在這裏插入圖片描述(2)參數個數

S2層由於每個特徵圖都共享相同的w和b這兩個參數,因此需要2×6=12個參數(爲什麼是2×6???博主也找了很多資料,後面瞭解到池化層的採樣方式爲:2*2區域的4個值相加,乘以一個可訓練參數,再加上一個偏置參數,結果通過Sigmoid非線性化個人感覺也應該和LeNet採用的最大池化有關,也就是2*2的區域中找到是最大特徵值,也就是卷積核的作用是找數,而不是參與運算,有不同意見的大佬可以交流指點🙏)

3、C3層(卷積層):16@10×10

C3層有16個卷積核,卷積模板大小爲5×5。

(1)特徵圖大小

與C1層的分析類似,C3層的特徵圖大小爲(14-5+1)×(14-5+1)= 10×10

(2)參數個數

需要注意的是,C3與S2並不是全連接而是部分連接,有些是C3連接到S2三層、有些四層、甚至達到6層,通過這種方式提取更多特徵,連接的規則如下表所示:
在這裏插入圖片描述
例如第一列表示C3層的第0個特徵圖(feature map)只跟S2層的第0、1和2這三個feature maps相連接,計算過程爲:用3個卷積模板分別與S2層的3個feature maps進行卷積,然後將卷積的結果相加求和,再加上一個偏置,再取sigmoid得出卷積後對應的feature map了。其它列也是類似(有些是3個卷積模板,有些是4個,有些是6個)。因此,C3層的參數數目爲(5×5×3+1)×6 +(5×5×4+1)×9 +5×5×6+1 = 1516

(3)連接數

卷積後的特徵圖大小爲10×10,參數數量爲1516,因此連接數爲1516×10×10= 151600

4、S4(下采樣層,也稱池化層):16@5×5

(1)特徵圖大小

與S2的分析類似,池化單元大小爲2×2,因此,該層與C3一樣共有16個特徵圖,每個特徵圖的大小爲5×5。

(2)參數數量

與S2的計算類似,所需要參數個數爲16×2 = 32

(3)連接數

連接數爲(2×2+1)×5×5×16 = 2000

5、C5層(卷積層):120

(1)特徵圖大小

該層有120個卷積核,每個卷積核的大小仍爲5×5,因此有120個特徵圖。由於S4層的大小爲5×5,而該層的卷積核大小也是5×5,因此特徵圖大小爲(5-5+1)×(5-5+1)= 1×1。這樣該層就剛好變成了全連接,這只是巧合,如果原始輸入的圖像比較大,則該層就不是全連接了。

(2)參數個數

與前面的分析類似,本層的參數數目爲120×(5×5×16+1) = 48120

(3)連接數

由於該層的特徵圖大小剛好爲1×1,因此連接數爲48120×1×1=48120

6、F6層(全連接層):84

(1)特徵圖大小

F6層有84個單元,之所以選這個數字的原因是來自於輸出層的設計,對應於一個7×12的比特圖,如下圖所示,-1表示白色,1表示黑色,這樣每個符號的比特圖的黑白色就對應於一個編碼。
在這裏插入圖片描述
該層有84個特徵圖,特徵圖大小與C5一樣都是1×1,與C5層全連接。

(2)參數個數

由於是全連接,參數數量爲(120+1)×84=10164。跟經典神經網絡一樣,F6層計算輸入向量和權重向量之間的點積,再加上一個偏置,然後將其傳遞給sigmoid函數得出結果。
(3)連接數

由於是全連接,連接數與參數數量一樣,也是10164。

7、OUTPUT層(輸出層):10

Output層也是全連接層,共有10個節點,分別代表數字0到9。如果第i個節點的值爲0,則表示網絡識別的結果是數字i。

(1)特徵圖大小

該層採用徑向基函數(RBF)的網絡連接方式,假設x是上一層的輸入,y是RBF的輸出,則RBF輸出的計算方式是:

上式中的Wij的值由i的比特圖編碼確定,i從0到9,j取值從0到7×12-1。RBF輸出的值越接近於0,表示當前網絡輸入的識別結果與字符i越接近。

(2)參數個數

由於是全連接,參數個數爲84×10=840

(3)連接數

由於是全連接,連接數與參數個數一樣,也是840

通過以上介紹,已經瞭解了LeNet各層網絡的結構、特徵圖大小、參數數量、連接數量等信息,下圖是識別數字3的過程,可對照上面介紹各個層的功能進行一一回顧:

在這裏插入圖片描述
轉載於:https://my.oschina.net/u/876354/blog/1632862

三、LeNet實現Cifer分類

LeNet圖像分類器步驟如下:

CIFAR10簡介
加載和預處理數據集
定義LeNet神經網絡
定義損失函數
在訓練數據上訓練並測試網絡

Cifer數據集

CIFAR-10數據集由10類32*32的彩色圖片組成, 一共包含60000張圖片,每一類包含6000張圖片。
下圖顯示的是數據集的類, 以及每一類中隨機挑選的10張圖片:
在這裏插入圖片描述
圖來自:https://blog.csdn.net/qq_37766812/article/details/105532468

加載和預處理數據集

import torchvision.transforms as transforms
from torch.autograd import Variable
import torch
import torchvision as tv
from torch import nn, optim

################################數據加載和預處理#########################
transform = transforms.Compose([
    transforms.ToTensor(), # 轉化爲Tensor
    transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5)), # 歸一化處理
])

# 定義了我們的訓練集,名字就叫trainset,至於後面這一堆,其實就是一個類:
# torchvision.datasets.CIFAR10( )也是封裝好了的,就在前面系列提到的torchvision.datasets
# 加載已經下載好的數據集
trainset = tv.datasets.CIFAR10(
    root = 'E:/pytorch/動手學深度學習/',
    train = True,
    download = False,
    transform = transform
)

trainloader = torch.utils.data.DataLoader(
    trainset,
    batch_size = 16,
    shuffle = True,
    num_workers = 2
)
#測試集
testset = tv.datasets.CIFAR10(
    'E:/pytorch/動手學深度學習/',
    train = False,
    download = False,
    transform = transform
)

testloader = torch.utils.data.DataLoader(
    trainset,
    batch_size = 16,
    shuffle = True,
    num_workers = 2
)

# 類別信息也是需要我們給定的
classes = ('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck')

代碼的具體作用和註解在之前系列已說明,詳見https://blog.csdn.net/weixin_45829462/article/details/106568309

結果如下:

Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data\cifar-10-python.tar.gz


100%|██████████████████████████████████████████████████████████████▉| 170434560/170498071 [05:20<00:00, 1334098.03it/s]

Files already downloaded and verified
Files already downloaded and verified

定義LeNet神經網絡

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv = nn.Sequential(
            # 對卷積層進行Sequential封裝
            nn.Conv2d(3, 6, 5),
            # 卷積層“3”表示輸入圖片爲單通道,“6”表示輸出通道數,‘5’表示卷積核爲5*5
            nn.Sigmoid(),
            # 使用Sigmoid激活
            nn.MaxPool2d(2, 2),
            # 使用最大池化層,卷積核大小爲2*2,步長爲2


            nn.Conv2d(6, 16, 5),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2)
        )
        self.fc = nn.Sequential(
            # 全連接層,y=Wx+b,in_channels=16*5*5
            nn.Linear(16*5*5, 120),
            nn.Sigmoid(),

            nn.Linear(120, 84),
            nn.Sigmoid(),

            nn.Linear(84,10)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output
net = LeNet()
print(net)

結果如下:

LeNet(
  (conv): Sequential(
    (0): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
    (1): Sigmoid()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): Sigmoid()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=400, out_features=120, bias=True)
    (1): Sigmoid()
    (2): Linear(in_features=120, out_features=84, bias=True)
    (3): Sigmoid()
    (4): Linear(in_features=84, out_features=10, bias=True)
  )
)

定義損失函數和優化器

###############定義損失函數和優化器(loss和optimiter)#################
criterion = nn.CrossEntropyLoss() # 交叉熵損失函數
# 訓練算法使⽤用SGD算法
optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)

Pytorch學習之Cifer實戰四·激活函數+損失函數+優化器詳解可參見此文章:
https://blog.csdn.net/weixin_45829462/article/details/106595624

訓練和評估網絡:

for epoch in range(100):
    # 指定訓練的批次
    start = time.time()
    correct = 0.0
    # 預測正確的圖片數
    total = 0.0
    # 總共參與測試的圖片數,也初始化爲0
    running_loss = 0.0
    # 定義loss,並初始爲0,方便後面計算後輸出
    for i, data in enumerate(trainloader, 0):
        # enumerate()用於可迭代\可遍歷的數據對象組合爲一個索引序列,
        # 同時列出數據和數據下標.上面代碼的0表示從索引從0開始,假如爲1的話,那索引就從1開始。
        inputs, labels = data
        # data是從enumerate返回的data,包含數據和標籤信息,分別賦值給inputs和labels
        inputs, labels = Variable(inputs), Variable(labels)
        # 之所以要使用Variable,是因爲這個函數包含了對Tensor的所有操作,包括自動求導和實現反向傳播等
        optimizer.zero_grad()
        # 梯度清零

        # 前向傳播+反向傳播 forword + backward
        outputs = net(inputs)
        # 把數據輸進網絡alexnet,這個alexnet()在第二部分我們已經定義了
        loss = criterion(outputs, labels)
        # 計算損失值,criterion是損失函數包裝器,我們在第四節也講過,
        # criterion(outputs, labels)指的是卷積操作後outputs和labels之間的差異值
        # 記住這個outputs輸出的並非tensor,而是ship、airplane這類的便籤信息
        loss.backward()
        # loss進行反向傳播
        # 更新參數
        optimizer.step()

        # running_loss += loss.data[0]
        running_loss += loss.item()


    with torch.no_grad():
        for data in testloader:
            # 循環每一個batch,之前我們定義了每個batch_size=16
            images, labels = data
            if isinstance(net, torch.nn.Module):
                # 判斷網絡是否運行中
                net.eval()
                # 是的話暫停網絡訓練
                outputs = net(Variable(images))
                # 輸入網絡進行測試
                # images其實是tensor矩陣,我們在第二部分輸出過
                _, predicted = torch.max(outputs.data, 1)
                correct += (predicted == labels)
                # 更新正確分類的圖片的數量,(predicted ==labels)是一個bool類型,
                # (predicted ==labels).sum()將所有預測正確的值相加,得到的仍是tensor類別的int值
                total += labels.size
                # 更新測試圖片的數量,labels.size(0)=1,等價於total += 1
                net.train()

    print('epoch %d, loss %.4f, acc %.3f,  time % .1f sec'
          % (epoch, running_loss / 12000, correct / total, time.time() - start))
print('Finished Training')
# 每迭代一次計算一次loss,然後清零

結果如下:

在這裏插入圖片描述

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