iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
AI & Data

AI白話文運動系列之「A!給我那張Image!」系列 第 10

Pytorch實戰(三)--建立與訓練模型--以簡單CNN模型為例

  • 分享至 

  • xImage
  •  

前言

昨天我們介紹完捲積這個在影像處理當中很重要也很好用的工具,今天我們會沿著這個節奏往下介紹由它所建構出來的捲積神經網路(Convolutional Neural Network),最後也會有個簡單的實戰,讓大家感受一下這樣的模型是怎麼運作的,以及在設計的時候有哪些需要留意的事情。

先備知識

  1. Python(至少對Python語法不陌生)
  2. 物件導向(至少需要知道class, function等概念)

看完今天的內容你可能會知道......

  1. 甚麼是捲積神經網路?
  2. 捲積神經網路有甚麼優勢?
  3. 如何利用Pytorch建立自己的捲積神經網路

一、捲積神經網路(Convolutional Neural Network)

  • 昨天討論的內容比較偏向理論或概念,所以在正式開始實戰之前,我們藉由介紹捲積神經網路這個架構,來深入了解一下要如何把捲積這個概念應用在AI模型中。

    1. 概述

    • 捲積神經網路(Convolutional Neural Network,簡稱CNN)泛指將使用捲積層架構的AI模型,在昨天的討論中我們可以知道,捲積運算的目的在於提取出影像的「(局部)特徵」,這個特徵的樣式是由捲積運算的矩陣,也就是「核(Kernel)」所決定的。更早之前,AI還沒興起的時候,捲積這個工具就已經被廣泛應用在各種影像任務中了,不過那個時候,大多是由人為設計這些「核」,即人為判斷哪些特徵是重要的、關鍵的,然而,正如我們昨天最後留下的懸念一樣,要如何確定我們設計的這些特徵是電腦所需要的?如果電腦需要非常多的特徵才能認識一張圖片該怎麼辦?這時候就輪到CNN出場了,既然人為設計的有這麼多不方便以及需要解決的問題的話,何不乾脆讓電腦自己學著設計這些核呢?這就是整個CNN架構的核心概念。
    • 在MLP這篇(https://ithelp.ithome.com.tw/articles/10322151)裡面我們有聊到,神經網路(或是你也可以想成是之前一直提到的AI模型)的起源是要模仿人腦神經元的運作方式。在網路/模型中的每個節點(Node)都代表一個神經元,他們就像真正的神經元一樣各司其職,對於輸入的訊號產生不同的反應,這個過程有沒有聽起來很熟悉的感覺?用另外一個角度來看,這些神經元是不是也是在找尋某種「特徵」或「模式」,所以對於同樣的訊號,不同的神經元才會產生不同的反應,因為他們都在找這個訊號當中有沒有他們感興趣或是他們想要的的東西,這個動作就是神經元在提取特徵。
    • 綜上所述,CNN架構是專門設計出來處理影像問題的AI模型架構,藉由讓電腦自己學習如何設計每個「核」,便可讓模型認識不同的影像。因為CNN架構簡單實用的優勢,讓他在很長一段時間佔據影像任務中的霸主寶座,除此之外,大家經常會將CNN架構歸納出三大特性:局部連接、權重共享與下取樣,我們在下面會一一介紹這些特性的由來。

    2. 捲積層(Convolution layer)

    • 如果是在MLP模型中的話,我們可以把每個節點(Node)對應成不同的特徵,然而在CNN模型中,則是一個核(Kernel)對應一個特徵,畢竟如果今天考慮的是一張圖片的話,「特徵」用一個點一個點來表示似乎不太合理,應該是某種顏色、紋理、形狀等等的,會比較適合。所以這就衍伸出CNN架構的第一個優勢:參數量下降,請注意,這個是指相較於要達到類似效果的MLP架構來說的,下面我們用個例子來看一下為甚麼會這樣:
    • 今天如果我要確認一張圖中是否有「斜線」這個特徵,在MLP的世界中,我們會利用足夠多的節點負責記錄「斜線」這樣的特徵,再來我們會將一張圖片打散排列(如下圖所示)https://ithelp.ithome.com.tw/upload/images/20230925/20163299kzd9mxoBn3.png 最後就是像之前我們做的一樣,將中間所有節點彼此相連去計算那條線的權重多少,如果權重越高,代表那個輸入節點跟目標節點之間的關係越強,「有多少條線,就代表有多少權重需要學習」https://ithelp.ithome.com.tw/upload/images/20230925/201632996x7weMoJ1x.png 右邊是我們之前討論過的MLP架構,跟左邊我們在做的事情是一樣的,只是畫法上有些差異。
    • 如果今天是使用CNN的話就不需要這麼多橘黃色的節點了,為甚麼?在討論捲積運算的時候我們有提到,捲積會利用「滑動」的方式,不斷去計算「核」與其所覆蓋區域的相似性,正是因為這個滑動,所以我只需要使用同一個「核」就可以看完整張圖片,並且找出哪裡有我想要的特徵,如下圖所示:https://ithelp.ithome.com.tw/upload/images/20230925/20163299Q9dnXVdLxk.png 與MLP最大的差異就在這邊,因為在CNN中,我們需要學習的就是「核」要怎麼設計,所以這個「核裡面有多少元素就代表我們就多少參數需要學習」,以上圖為例就是5*5,跟MLP相比是不是少了很多!
    • 所以為甚麼CNN具有這兩種特性:局部連接、權重共享,就是因為捲積核只與輸入圖片的特定區域進行運算,不向MLP需要一次看完整張圖片,而且因為一個核可以藉由滑動的方式看完整張圖片,所以整張圖片在同一個特徵上其實是使用同樣的權重的(注意,CNN中的權重指的是核裡面的元素),和MLKP不同區域要使用不同的權重不同(注意,MLP中的權重是指每個節點之間的線的重要性/加權係數)。

    3. 池化層(Pooling layer)

    • 如果我們從始至終都使用同樣的圖片大小會發生甚麼事?假設今天有兩張貓的圖片,一張拍起來貓比較大隻,另外一隻因為比較遠的關係,看起來比較小隻,如果一個訓練好的CNN模型遇到這種情況只能辨識出一張圖片是貓,這是因為我們的「核」已經被固定下來了,這個核看到的區域、紋理、顏色等等的資訊都被固定下來了,所以今天圖片稍微縮放或是裁剪的話就會認不出來。為了避免這樣的問題出現,在訓練CNN模型的時候我們就應該讓模型能夠考慮到不同大小的圖片。
    • 具體的做法就是通過「池化層」進行圖片的下取樣(Downsampling),也就是讓圖片經過池化層之後能夠變小,這樣一來,就算核的大小固定,它也能夠擷取到不同尺度的特徵。池化的示意圖如下:https://ithelp.ithome.com.tw/upload/images/20230925/20163299502jjtEnDy.png 池化層設計的重點在如何從左邊變到右邊,也就是「要如何挑選元素」,以及「經過挑選之後的圖片大小」,這兩個關鍵我們會在實作的時候看到具體的運作方式。
    • 目前有許多不同的池化方式,在Pytorch的網站(https://pytorch.org/docs/stable/nn.html#pooling-layers)中也有Poolng這個分類,感興趣的人可以點進去看看有哪些常用的池化方式。

二、Pytorch中的CNN

  • https://ithelp.ithome.com.tw/articles/10323073 這篇的內容中,我們有提到Pytorch替我們建立好許多「層(Layer)」的架構,讓我們可以直接拿出來使用,所以下面我們會先介紹CNN中最重要的兩種「層」,也就是捲積層(Convolution layer)與池化層(Pooling layer)在Pytorch中的使用方式,最後會將今天的所有內容整合在一起,建立一個簡單的CNN模型。

    1. 捲積層(Convolution layer)

    • 參考資料:1D捲積https://pytorch.org/docs/stable/generated/torch.nn.Conv1d.html ,2D捲積https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html
    • 因為在模型中,每一層的輸入與輸出大小是統一的(可以想像,如果不統一的話,每輸入一張不同大小的圖片,要學習的「核」就會不一樣大),所以在設計每一層的時候「輸入與輸出大小」需要事先考慮過,並且這邊要牢記一個觀念:前一層的輸出是下一層的輸入,聽起來很像廢話,可是這取決於我們層跟層之間是如何連接的,跟我們建立每個層的先後順序無關。
    • 在上述的兩個網站中皆有提供輸入與輸出大小的計算公式,在設計模型的時候最好可以試著計算一下,才不容易出錯,當然還有另外一種方便的做法:跟著別人走,因為很多經典、熱門的模型都有公開,所以可以直接參考他們都使用哪一種輸入圖片大小,每一層之間的大小變化又是如何,算是給剛入門的人很好的參照依據。
    • 這邊有個簡單的範例,可以自己嘗試更改nn.Conv2d裡面不同的參數設定,看看輸出跟輸入大小之間的關係如何變化:
    import torch
    import torch.nn as nn
    
    # Create a simple input tensor (batch size, channels, height, width)
    input_tensor = torch.randn(1, 1, 28, 28)  
    # Example input with 1 channel (grayscale) and 28x28 resolution
    
    # Define a 2D convolution layer
    conv_layer = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=1, padding=1)
    
    # Forward pass through the convolution layer
    output = conv_layer(input_tensor)
    
    # Print the output shape
    print("Output shape:", output.shape)
    
    
    • 捲積層裡面的每個參數都可以拿出來好好討論一波,不過對於初學者來說,只需要會調整下圖前面五個參數就夠了,最後兩個大家比較陌生,這邊簡單說明一下,捲積的運算方式是「核的滑動」,所以stride 指的就是每一滑動的間隔,而padding 則是控制輸出圖片的大小,因為正常經過捲機運算之後得到的圖片會小於原先的輸入圖片,所以我們可以在一開始就先把輸入圖片外圍填充一圈別的元素,讓最後輸出的圖片可以大一點。https://ithelp.ithome.com.tw/upload/images/20230925/20163299uKzKpAYWu5.png

    2. 池化層(Pooling layer)

    import torch
    import torch.nn.functional as F
    
    # Create a sample 2D tensor
    input_tensor = torch.tensor([[1, 2, 3, 4],
                                 [5, 6, 7, 8],
                                 [9, 10, 11, 12],
                                 [13, 14, 15, 16]], dtype=torch.float32)
    
    # Max Pooling Example
    max_pooling_result = F.max_pool2d(input_tensor.view(1, 1, 4, 4), kernel_size=2, stride=2)
    print("Max Pooling Result:")
    print(max_pooling_result)
    
    # Average Pooling Example
    avg_pooling_result = F.avg_pool2d(input_tensor.view(1, 1, 4, 4), kernel_size=2, stride=2)
    print("\nAverage Pooling Result:")
    print(avg_pooling_result)
    
    
    • 各位看到這邊是不是有點困惑,為甚麼池化層也有「核」?這是因為在實作上,我們會使用跟捲積類似的概念,通過某個核的滑動來判定現在要處理哪個區域,這個區域有多大,這樣會比較方便,可以把這邊的核簡單的想成是鎖定區域,就是你在玩遊戲的時候,透過槍上面的瞄準鏡看到的區域,也就是被鎖定的目前要處理的區域。
    • 池化層跟捲積層有許多類似的參數可以調整,像是「核的大小」、「滑動的間隔」以及是否需要在外部填充東西等等的,基本上跟前面我們在捲積層那邊提到的類似。

    3. 簡單CNN模型實戰

    import torch
    import torch.nn as nn
    import torch.optim as optim
    import torchvision
    import torchvision.transforms as transforms
    
    # Define the CNN model
    class SimpleCNN(nn.Module):
        def __init__(self):
            super(SimpleCNN, self).__init__()
            self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
            self.relu = nn.ReLU()
            self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
            self.fc1 = nn.Linear(32 * 14 * 14, 128)
            self.fc2 = nn.Linear(128, 10)
    
        def forward(self, x):
            x = self.conv1(x)
            x = self.relu(x)
            x = self.maxpool(x)
            x = x.view(x.size(0), -1)  # Flatten the tensor
            x = self.fc1(x)
            x = self.relu(x)
            x = self.fc2(x)
            return x
    
    # Hyperparameters
    batch_size = 64
    learning_rate = 0.001
    num_epochs = 10
    
    # Load MNIST dataset and apply transformations
    transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
    
    train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
    train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
    
    # Initialize the model
    model = SimpleCNN()
    
    # Loss and optimizer
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    
    # Training loop
    total_step = len(train_loader)
    for epoch in range(num_epochs):
        for i, (images, labels) in enumerate(train_loader):
            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)
    
            # Backward pass and optimize
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
    
            if (i + 1) % 100 == 0:
                print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{total_step}], Loss: {loss.item():.4f}')
    

三、總結

  • 今天的內容可以說是非常的充實,我們介紹了捲積神經網路這個非常知名的架構,也談了它具有哪些特點、優勢,最後在實作的部分,我們也介紹了在Pytorch中跟CNN相關的兩個重要的層,並且整合在一起建構一個簡單的CNN模型。剛入門的同學建議可以多看幾次,同時也可以複習一下之前的內容,這樣學習的效果比較好!

上一篇
內捲給我捲起來--AI中的捲積運算
下一篇
唧唧復唧唧--AI中的機率統計之生成模型(Generative model)與判別模型(Discriminative model)
系列文
AI白話文運動系列之「A!給我那張Image!」30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言