iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
AI & Data

從0開始:傳統圖像處理到深度學習模型系列 第 13

Day 13 - 卷積神經網路(一) CNN 入門

  • 分享至 

  • xImage
  •  

ANN 的缺點

雖然 ANN 能夠自動學習特徵,但是他在處理影像時會把二維的圖片攤平成一維的向量,這會造成空間結構資訊的喪失。ANN 沒辦法理解相鄰或上下左右這類的關係,這也引出了電腦視覺最關鍵的模型:卷積神經網路 (Convolutional Neural Network, CNN)。

CNN 的核心思想

CNN 的設計靈感來自於動物的視覺皮層,視覺皮層中的神經元是分層級的,每個神經元只對視野中的一小塊局部區域 (receptive field) 做出反應。一些神經元可能對橫向邊緣敏感,另一些則對縱向或斜向邊緣敏感。更高層級的神經元,則會整合這些來自淺層神經元的訊號,來識別更複雜的形狀,如眼睛或鼻子。

CNN 有著三個核心思想

  1. 局部感受域 (local receptive fields):與 ANN 中每個神經元都連接到前一層所有神經元不同,CNN 中的神經元(卷積層中)只連接到前一層輸入的一個局部小區域。這意味著每個神經元只專注於分析影像的一小部分,例如一個 3×3 或 5×5 的區域。

  2. 權重共享 (shared weights):在一個卷積層中,我們用同一個小小的濾波器或卷積核,去掃描整張輸入影像。這個濾波器就像一個特徵偵測器,例如一個被訓練來偵測「鳥嘴」的濾波器,它在掃描到影像左上角時,和掃描到右下角時,使用的是完全相同的權重。這帶來了兩個好處

    a. 參數數量大幅減少:無論影像有多大,我們需要學習的只是一個小小的濾波器的權重,而不是像 ANN 那樣為每個像素位置都學習一套獨立的權重。

    b. 平移不變性 (translation invariance):因為同一個濾波器被應用於影像的所有位置,所以無論「鳥嘴」出現在圖片的哪個位置,這個濾波器都有可能被觸發。模型學會了識別一個特徵,而不管這個特徵在哪裡。

  3. 池化 (pooling):在卷積層之後,通常會接一個池化層。它的作用是對特徵圖進行「降採樣」(down-sampling),例如將一個 2×2 的區域,用其最大值 (max pooling) 或平均值 (average pooling) 來代替。這不僅能進一步減少數據量和計算量,還能為模型帶來一定程度的形變不變性,讓模型對物體的輕微位移不那麼敏感。

CNN 的組成

一個典型的 CNN 由以下幾種層構成

  1. 卷積層 (convolutional layer):核心中的核心。這一層包含多個卷積核(濾波器)。每個卷積核都會滑過整張輸入圖片(或前一層的輸出特徵圖),進行卷積運算,並生成一張新的「特徵圖 (feature map)」。每一張特徵圖,都代表了原始影像在某種特定特徵(如橫向邊緣、特定顏色、特定紋理)上的響應。

  2. 激勵函數:通常緊跟在卷積層之後。與 ANN 一樣,它為網路引入非線性,使其能夠學習更複雜的模式。最常用的激勵函數是 ReLU。

  3. 池化層 (pooling layer):用於降採樣,最常用的是 max pooling。

  4. 全連接層 (fully connected layer):在經過多輪「卷積 → 激勵 → 池化」的特徵提取後,我們會將最後得到的特徵圖攤平成一個一維向量,然後送入一個或多個全連接層(這部分就和 ANN 一樣)。全連接層的作用,是將前面學到的高級特徵進行整合,並最終做出分類決策。

  5. Softmax 層:通常是分類 CNN 的最後一層,它會將全連接層的輸出,轉換成每個類別的機率分佈。

CNN 實作

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# --- 1. 設定超參數與設備 ---
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"將使用設備: {device}")

num_classes = 10
learning_rate = 0.001
batch_size = 64
num_epochs = 5

# --- 2. 載入並預處理 MNIST 數據集 ---
# CNN 的輸入是 2D 圖片 (加上通道),所以不再需要攤平
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)

train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)

# --- 3. 定義 CNN 模型 ---
class CNN(nn.Module):
    def __init__(self, num_classes=10):
        super(CNN, self).__init__()
        # 卷積層 1: 
        # in_channels=1 (灰階圖), out_channels=16 (使用16個濾波器)
        # kernel_size=5 (5x5的卷積核), stride=1 (步長為1), padding=2 (填充2圈)
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2)
        # 批次標準化層
        self.bn1 = nn.BatchNorm2d(16)
        self.relu = nn.ReLU()
        # 最大池化層: 2x2的窗口,步長為2
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # 卷積層 2:
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2)
        self.bn2 = nn.BatchNorm2d(32)
        
        # 全連接層:
        # 圖片經過兩次 2x2 池化後,尺寸從 28x28 -> 14x14 -> 7x7
        # 所以攤平後的維度是 32(通道數) * 7 * 7
        self.fc = nn.Linear(32 * 7 * 7, num_classes)

    def forward(self, x):
        # 輸入 x 的尺寸: [batch_size, 1, 28, 28]
        out = self.pool(self.relu(self.bn1(self.conv1(x)))) # -> [batch_size, 16, 14, 14]
        out = self.pool(self.relu(self.bn2(self.conv2(out)))) # -> [batch_size, 32, 7, 7]
        
        # 攤平特徵圖,用於輸入全連接層
        out = out.reshape(-1, 32 * 7 * 7) # -> [batch_size, 32*7*7]
        
        out = self.fc(out) # -> [batch_size, 10]
        return out

# 建立模型實例並移至指定設備
model = CNN(num_classes).to(device)

# --- 4. 定義損失函數與最佳化器 ---
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# --- 5. 訓練模型 ---
print("開始訓練 CNN...")
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        # 將數據移至設備 (注意:這次 images 不需要 reshape)
        images = images.to(device) # -> [batch_size, 1, 28, 28]
        labels = labels.to(device)
        
        # 前向傳播
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # 反向傳播與最佳化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if (i+1) % 100 == 0:
            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')
print("訓練完成!")

# --- 6. 評估模型 ---
model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    print(f'CNN 模型在 10000 張測試圖片上的準確率: {100 * correct / total:.2f} %')

結果

CNN 模型在 10000 張測試圖片上的準確率: 99.13 %

上一篇
Day 12 - 深度學習初探
下一篇
Day 14 – 卷積神經網路(二) AlexNet 與 VGG
系列文
從0開始:傳統圖像處理到深度學習模型23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言