我已發布在:如何使用 pytorch 創建一個神經網絡 SapientialM.Github.io
構建神經網絡
1 導入所需包
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
2 檢查GPU是否可用
device = ("cuda"if torch.cuda.is_available()else "mps"if torch.backends.mps.is_available()else "cpu"
)
print(f"Using {device} device")
print(torch.__version__)
print(torch.cuda.is_available())
print(torch.version.cuda)
Using cuda device
2.2.2+cu121
True
12.1
如果發現GPU不可用,可能是因為torch版本問題(比如我),應該去下載GPU版本。
- 在CUDA官網找到合適版本的cuda,一般是根據系統平臺和顯卡版本來選擇所需CUDA
- 查看安裝完成是否
# 版本,看CUDA的版本,比如我的是cuda_11.2
nvcc --version
# 驅動,看Driver Version
nvidia-smi
- 去PyTorch官網找到合適版本的PyTorch,一般是根據開發環境來選擇,然后復制所給的Commond去shell下安裝即可
# 比如我的命令就是
pip install torch==2.2.2 torchvision==0.17.2 torchaudio==2.2.2 --index-url https://download.pytorch.org/whl/cu121
3 定義我們的神經網絡
pytorch里面一切自定義操作基本上都是繼承nn.Module
類來實現的,你可以先不去深入了解這個類,但是要知道,我們一般都是通過繼承和重構nn.Module
來定義我們的神經網絡。我們一般重構__init__
和forward
這兩個方法。根據PyTorch官網的說法:__init__
初始化神經網絡層;forward
層之間的數據操作,也是整個網絡的核心。__init__
只會定義層,而forward
負責將層連接起來。實際上類的初始化參數一般是一些固有屬性
,我們可以將一些帶有訓練參數的層放在__init__
,而沒有訓練參數的層是可以加入到forward
里面的,或者說我們將沒有訓練參數的層看作是層之間的數據操作。
當然直接這么說,肯定不是很清晰,我們來看一個官網給的例子:
class NeuralNetwork(nn.Module):def __init__(self):super().__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),)def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits
model = NeuralNetwork().to(device) # 將網絡移入device,并打印結構
print(model)
NeuralNetwork((flatten): Flatten(start_dim=1, end_dim=-1)(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True))
)
我們用torchviz可以將神經網絡進行一個簡單的可視化,當然很多參數可選,這里不一一列舉
pip install torchviz
from torchviz import make_dot
X = torch.rand(1, 28, 28, device=device) # 需要對神經網絡進行數據輸入,才是一個完整的網絡
y = model(X)
output = make_dot(y.mean(), params=dict(model.named_parameters())) # 開始繪制
output.format = "png"
output.directory = "."
output.render("torchviz", view=True) # 會在相對路徑下保存一個可視化后的圖片并打開
'torchviz.png'
看起來可能會比較復雜,也看不懂,我們得需要學會看懂神經網絡的結構才能看懂結構圖。當然,還有諸如draw_convnet、NNSVG、netron等可視化工具會更加優秀。
4 神經網絡模型層
想要構建一個神經網絡并進行訓練和預測,我們需要去認識神經網絡的構成。假定你已經了解過感知機、人工神經網絡的基本概念,那么現在就是來了解一下神經網絡的模型層。
我們直接分解一下官網所給出的這個模型,這是一個簡單的前饋神經網絡(Feedforward Neural Network),我們先不去了解它的作用,一點點的分解它,看看它最終實現了什么。
我們先根據官網所說的,取一個大小為 28x28 的 3 張圖像的樣本小批量作為輸入(我們一般將數據的第一個維度看作批量維度并保留):
input_image = torch.rand(3,28,28)
print(input_image.size())
torch.Size([3, 28, 28])
4.1 nn.Flatten
雖然是PyTorch的nn.Flatten
,但是Flatten層是神經網絡中常見的組成部分。在神經網絡的訓練和預測過程中,輸入數據通常需要經過一系列的處理和轉換。在這個過程中,Flatten層能夠將多維的輸入數據轉化為一維的線性形式,以便于神經網絡的進一步處理。模型中的nn.Flatten
,將我們所輸入的2D 28*28 圖像轉換為一個包含 784 個像素值的連續數組,也就是和它表面的意思一樣展平這個高維數組。
(
nn.Flatten()
默認參數是start_dim=1
和end_dim=-1
,如果你想展平所有維度,可以通過設置start_dim=0
來實現)
flatten = nn.Flatten()
flat_image = flatten(input_image) # 將輸入的圖像展平
print(flat_image.size())
torch.Size([3, 784])
在卷積神經網絡(CNN)中,Flatten
層可以將卷積層提取到的特征圖展平,便于進一步的特征處理或分類,也便于輸入到全連接層(全連接層通常需要一維的輸入,后面會講到)。在構建復雜網絡時,Flatten
層可以幫助不同類型的層之間進行連接。總的來說,Flatten
層起到了橋梁的作用,使得卷積神經網絡的層次結構更加靈活和易于設計,并且確保了從卷積層到全連接層的數據傳遞順暢,維持了網絡的整體性能和效率。
4.2 nn.Linear
nn.Linear
應該耳熟能詳,我們稱之為線性層(Linear Layer),也可以稱為全連接層(Fully Connected Layer)或密集層(Dense Layer)。線性層是一個使用其存儲的權重和偏差對輸入應用線性變換的模塊,也就是對輸入數據進行線性變換。線性層對數據的處理方式基本上可以表示為:
y = W x + b y = Wx + b y=Wx+b
,其中 W 是權重矩陣,b 是偏置。向量都是可學習的參數。在神經網絡的訓練和預測過程中,Linear
層的作用是將輸入數據通過一組權重進行線性變換,然后添加一個偏置項。簡單來說,它能夠將輸入特征映射到輸出特征,從而實現對數據的線性組合和轉換。如下圖是一個單隱藏層的多層感知機(Multilayer Perceptron),一般稱為MLP,隱藏層和輸出層均是由線性層和激活函數組成:
# 定義一個線性層,將28*28維度的向量轉換為20維度的向量
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
torch.Size([3, 20])
在這個例子中,in_features=28*28表示輸入特征的維度,out_features=20表示輸出特征的維度。nn.Linear層會自動初始化權重和偏置,并在訓練過程中通過反向傳播算法進行調整。簡單理解就是,該線性層的輸入是784維,而輸出是20維。
4.3 nn.ReLU
ReLU函數,全稱Rectified Linear Unit,是人工神經網絡中常用的一種激活函數.
講到這里,我們就講講常見的激活函數及其作用。
Sigmoid 激活函數
數學表達式:
σ ( x ) = 1 1 + e ? x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+e?x1?
作用:
- 將輸入映射到 (0, 1) 之間。
- 常用于輸出層,尤其是在二分類問題中,輸出概率值。
優點:
- 輸出范圍在 (0, 1) 之間,可以解釋為概率。
- 平滑梯度,有助于梯度下降。
缺點:
- 容易導致梯度消失問題。
- 輸出不是零中心的,會影響網絡的訓練效率。
import numpy as np
import matplotlib.pyplot as pltdef sigmoid(x):return 1 / (1 + np.exp(-x))x = np.linspace(-10, 10, 100)
y = sigmoid(x)plt.plot(x, y)
plt.title("Sigmoid Activation Function")
plt.xlabel("x")
plt.ylabel("Sigmoid(x)")
plt.grid()
plt.show()
Tanh 激活函數
數學表達式:
tanh ? ( x ) = e x ? e ? x e x + e ? x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+e?xex?e?x?
作用:
- 將輸入映射到 (-1, 1) 之間。
- 常用于隱藏層,提供零中心的輸出,有助于訓練。
優點:
- 輸出是零中心的,梯度消失問題較輕。
缺點:
- 仍然存在梯度消失問題。
import numpy as np
import matplotlib.pyplot as pltdef tanh(x):return np.tanh(x)x = np.linspace(-10, 10, 100)
y = tanh(x)plt.plot(x, y)
plt.title("Tanh Activation Function")
plt.xlabel("x")
plt.ylabel("Tanh(x)")
plt.grid()
plt.show()
ReLU (Rectified Linear Unit) 激活函數
數學表達式:
ReLU ( x ) = max ? ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x)
作用:
- 將輸入小于0的部分設為0,大于0的部分保持不變。
- 常用于隱藏層,特別是深度神經網絡。
優點:
- 計算簡單,收斂速度快。
- 減少梯度消失問題。
缺點:
- 輸出不是零中心的。
- 輸入小于0時梯度為零,可能導致“神經元死亡”問題。
import numpy as np
import matplotlib.pyplot as pltdef relu(x):return np.maximum(0, x)x = np.linspace(-10, 10, 100)
y = relu(x)plt.plot(x, y)
plt.title("ReLU Activation Function")
plt.xlabel("x")
plt.ylabel("ReLU(x)")
plt.grid()
plt.show()
Leaky ReLU 激活函數
數學表達式:
Leaky?ReLU ( x ) = { x if? x > 0 α x if? x ≤ 0 \text{Leaky ReLU}(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha x & \text{if } x \leq 0 \end{cases} Leaky?ReLU(x)={xαx?if?x>0if?x≤0?
其中 (\alpha) 通常是一個很小的常數,如 0.01。
作用:
- 解決 ReLU 的“神經元死亡”問題。
優點:
- 輸入小于0時仍有較小梯度,避免神經元死亡。
缺點:
- 計算稍復雜。
import numpy as np
import matplotlib.pyplot as pltdef leaky_relu(x, alpha=0.01):return np.where(x > 0, x, alpha * x)x = np.linspace(-10, 10, 100)
y = leaky_relu(x)plt.plot(x, y)
plt.title("Leaky ReLU Activation Function")
plt.xlabel("x")
plt.ylabel("Leaky ReLU(x)")
plt.grid()
plt.show()
Softmax 激活函數
數學表達式:
Softmax ( x i ) = e x i ∑ j e x j \text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} Softmax(xi?)=∑j?exj?exi??
作用:
- 將輸入向量轉換為概率分布,總和為1。
- 常用于多分類問題的輸出層。
優點:
- 輸出可以解釋為概率,便于分類。
缺點:
- 計算相對復雜,容易導致數值不穩定。
import numpy as np
import matplotlib.pyplot as pltdef softmax(x):e_x = np.exp(x - np.max(x))return e_x / e_x.sum(axis=0)x = np.linspace(-10, 10, 100)
y = softmax(x)plt.plot(x, y)
plt.title("Softmax Activation Function")
plt.xlabel("x")
plt.ylabel("Softmax(x)")
plt.grid()
plt.show()
4.4 nn.Sequential
nn.Sequential
是 PyTorch 提供的一個容器模塊,它按順序包含其他子模塊,便于構建和管理簡單的神經網絡結構。通過 nn.Sequential,可以方便地將一系列層(如線性層、激活函數、卷積層等)按順序堆疊在一起,從而簡化模型定義和前向傳播的代碼。簡而言之就是一個包裹的順序容器。
5 理解我們的神經網絡
看完這些,我們再來理解這個官網給的例子:
class NeuralNetwork(nn.Module):# 重構 __init__,定義“固有屬性”def __init__(self):# 這一步操作是調用父類 nn.Module 的構造函數,確保繼承自 nn.Module 的特性正確初始化super().__init__()# 定義一個展開層 flattenself.flatten = nn.Flatten()# 定義一個線性容器,可以簡化在forward中的調用self.linear_relu_stack = nn.Sequential(# 容器內包含一個三層網絡# 這里的512、10都是研究者根據具體任務和數據集進行調試和優化得到的結果# 熟悉的調參nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),)# 重構forward,定義前向傳播路徑def forward(self, x):# 在這里定義各個層輸入輸出的順序,即層在網絡里的位置關系x = self.flatten(x)logits = self.linear_relu_stack(x)return logits
model = NeuralNetwork().to(device) # 將網絡移入device,并打印結構
print(model)
NeuralNetwork((flatten): Flatten(start_dim=1, end_dim=-1)(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True))
)
6 使用我們的網絡
主要步驟如下:
- 定義模型
- 數據載入
- 損失函數和優化
- 訓練和評估
- 預測與可視化
先導入所需包:
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import numpy as np
import matplotlib.pyplot as plt
6.1 定義模型
# 定義設備,如果有GPU則使用GPU,否則使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 定義神經網絡模型
class NeuralNetwork(nn.Module):def __init__(self):super().__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),)def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits# 創建神經網絡模型實例,并移動到設備上
model = NeuralNetwork().to(device)
print(model)
NeuralNetwork((flatten): Flatten(start_dim=1, end_dim=-1)(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True))
)
6.2 數據載入
我們這次用的模型用于簡單圖像分類問題,所以可以使用MNIST數據集,導入用的是PyTorch的datasets。
# 加載MNIST數據集并進行預處理
transform = transforms.Compose([# 對圖片的常用操作,將圖像數據轉換為形狀為 (C, H, W) 的張量transforms.ToTensor(),# 因為數據集是灰度圖像,所以只有單值標準化transforms.Normalize((0.5,), (0.5,))
])# 加載MNIST數據集,并劃分訓練集和測試集(這里會下載下來)
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform)# 定義批量大小和數據加載器
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
6.3 損失函數和優化
損失函數選取交叉熵損失函數(Cross Entropy Loss),它是一種常用的損失函數,能夠有效地衡量預測類別和真實類別之間的差異。它能夠處理模型輸出的logits,并且在計算過程中會自動應用Softmax操作,從而簡化代碼。
優化器選取隨機梯度下降法(Stochastic Gradient Descent, SGD),它是一種簡單而有效的優化方法,特別適用于大規模數據集和模型。結合Momentum算法,SGD優化器可以加速收斂并減小震蕩,從而在一定程度上提高訓練效率和模型性能。
# 定義損失函數
criterion = nn.CrossEntropyLoss()
# 定義優化器
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
6.4 訓練和評估
# 訓練模型
# 訓練的目的是通過多個訓練周期和批次的數據,不斷調整模型參數以最小化損失函數,從而提高模型的性能。
def train(model, train_loader, optimizer, criterion, epochs=5):# 某些層(如Dropout和BatchNorm)在訓練和評估模式下的行為不同,所以需要顯式地設置模型為訓練模式。model.train()# 開始訓練循環for epoch in range(epochs):# 初始化損失running_loss = 0.0# 遍歷訓練數據加載器中的每個批次for batch_idx, (data, target) in enumerate(train_loader):# 將數據移動到設備上data, target = data.to(device), target.to(device)# 梯度清零optimizer.zero_grad()# 前向傳播,將數據輸入模型進行預測output = model(data)# 計算損失loss = criterion(output, target)# 將損失反向傳播loss.backward()# 使用優化器更新參數optimizer.step() # 累計損失running_loss += loss.item()if batch_idx % 100 == 99: # 每100個批次打印一次訓練狀態print(f'Epoch [{epoch+1}/{epochs}], Step [{batch_idx+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')running_loss = 0.0# 測試模型
def test(model, test_loader):# 進入評估模式,原因與train同理model.eval()# 累計計數correct = 0total = 0# 測試過程不會進行優化,所以no_grad禁用梯度計算可以加快測試速度with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)# 前向傳播,將數據輸入模型進行預測outputs = model(data)# 獲取預測結果_, predicted = torch.max(outputs.data, 1)total += target.size(0)correct += (predicted == target).sum().item()accuracy = 100 * correct / totalprint(f'Test Accuracy: {accuracy:.2f}%')
6.5 預測與可視化
# 可視化預測結果
def visualize_predictions(model, test_loader, num_images=10):# 這里和上面定義的test相似,主要是在執行過程中添加了可視化代碼和限制了測試數量model.eval()images_so_far = 0plt.figure(figsize=(12, 8))with torch.no_grad():for i, (inputs, labels) in enumerate(test_loader):inputs = inputs.to(device)labels = labels.to(device)# 獲取每張圖的預測結果,并將數據繪制出來進行比對outputs = model(inputs)_, preds = torch.max(outputs, 1)for j in range(inputs.size(0)):images_so_far += 1ax = plt.subplot(num_images // 5, 5, images_so_far)ax.axis('off')ax.set_title(f'Predicted: {preds[j]} (Label: {labels[j]})')# imshow用于在繪圖窗口中顯示圖像ax.imshow(inputs.cpu().data[j].numpy().squeeze(), cmap='gray')if images_so_far == num_images:model.train()returnmodel.train()# 執行訓練
train(model, train_loader, optimizer, criterion)# 執行測試
test(model, test_loader)# 可視化預測結果
visualize_predictions(model, test_loader, num_images=10)
plt.suptitle('Model Predictions')
plt.tight_layout()
plt.show()
NeuralNetwork((flatten): Flatten(start_dim=1, end_dim=-1)(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True))
)
Epoch [1/5], Step [100/938], Loss: 1.1812
Epoch [1/5], Step [200/938], Loss: 0.4243
Epoch [1/5], Step [300/938], Loss: 0.3437
Epoch [1/5], Step [400/938], Loss: 0.3305
Epoch [1/5], Step [500/938], Loss: 0.2820
Epoch [1/5], Step [600/938], Loss: 0.2634
Epoch [1/5], Step [700/938], Loss: 0.2482
Epoch [1/5], Step [800/938], Loss: 0.2131
Epoch [1/5], Step [900/938], Loss: 0.2161
Epoch [2/5], Step [100/938], Loss: 0.1853
Epoch [2/5], Step [200/938], Loss: 0.1658
Epoch [2/5], Step [300/938], Loss: 0.1766
Epoch [2/5], Step [400/938], Loss: 0.1507
Epoch [2/5], Step [500/938], Loss: 0.1606
Epoch [2/5], Step [600/938], Loss: 0.1347
Epoch [2/5], Step [700/938], Loss: 0.1407
Epoch [2/5], Step [800/938], Loss: 0.1371
Epoch [2/5], Step [900/938], Loss: 0.1283
Epoch [3/5], Step [100/938], Loss: 0.1027
Epoch [3/5], Step [200/938], Loss: 0.1169
Epoch [3/5], Step [300/938], Loss: 0.1150
Epoch [3/5], Step [400/938], Loss: 0.1077
Epoch [3/5], Step [500/938], Loss: 0.0986
Epoch [3/5], Step [600/938], Loss: 0.1139
Epoch [3/5], Step [700/938], Loss: 0.1110
Epoch [3/5], Step [800/938], Loss: 0.0986
Epoch [3/5], Step [900/938], Loss: 0.0927
Epoch [4/5], Step [100/938], Loss: 0.0908
Epoch [4/5], Step [200/938], Loss: 0.0834
Epoch [4/5], Step [300/938], Loss: 0.0957
Epoch [4/5], Step [400/938], Loss: 0.0742
Epoch [4/5], Step [500/938], Loss: 0.0873
Epoch [4/5], Step [600/938], Loss: 0.0786
Epoch [4/5], Step [700/938], Loss: 0.0901
Epoch [4/5], Step [800/938], Loss: 0.0828
Epoch [4/5], Step [900/938], Loss: 0.0810
Epoch [5/5], Step [100/938], Loss: 0.0682
Epoch [5/5], Step [200/938], Loss: 0.0729
Epoch [5/5], Step [300/938], Loss: 0.0601
Epoch [5/5], Step [400/938], Loss: 0.0684
Epoch [5/5], Step [500/938], Loss: 0.0755
Epoch [5/5], Step [600/938], Loss: 0.0706
Epoch [5/5], Step [700/938], Loss: 0.0733
Epoch [5/5], Step [800/938], Loss: 0.0579
Epoch [5/5], Step [900/938], Loss: 0.0621
Test Accuracy: 97.45%
神經網絡(尤其是深度神經網絡)的一個非常吸引人的特點就是:它們具有很強的通用性,可以通過不同的數據集進行訓練,以解決各種不同的任務。我們可以將該模型使用另外的數據集進行訓練和測試,仍然有不低的準確率。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import numpy as np
import matplotlib.pyplot as plt# 定義設備,如果有GPU則使用GPU,否則使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 定義神經網絡模型
class NeuralNetwork(nn.Module):def __init__(self):super().__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10),)def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits# 創建神經網絡模型實例,并移動到設備上
model = NeuralNetwork().to(device)
print(model)# 加載FashionMNIST數據集并進行預處理
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,))
])train_dataset = datasets.FashionMNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = datasets.FashionMNIST(root='./data', train=False, transform=transform)# 定義批量大小和數據加載器
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)# 定義損失函數和優化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)# 訓練模型
def train(model, train_loader, optimizer, criterion, epochs=5):model.train()for epoch in range(epochs):running_loss = 0.0for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)optimizer.zero_grad()output = model(data)loss = criterion(output, target)loss.backward()optimizer.step()running_loss += loss.item()if batch_idx % 100 == 99: # 每100個批次打印一次訓練狀態print(f'Epoch [{epoch+1}/{epochs}], Step [{batch_idx+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')running_loss = 0.0# 測試模型
def test(model, test_loader):model.eval()correct = 0total = 0with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)outputs = model(data)_, predicted = torch.max(outputs.data, 1)total += target.size(0)correct += (predicted == target).sum().item()accuracy = 100 * correct / totalprint(f'Test Accuracy: {accuracy:.2f}%')# 可視化預測結果
def visualize_predictions(model, test_loader, num_images=10):model.eval()images_so_far = 0plt.figure(figsize=(12, 8))with torch.no_grad():for i, (inputs, labels) in enumerate(test_loader):inputs = inputs.to(device)labels = labels.to(device)outputs = model(inputs)_, preds = torch.max(outputs, 1)for j in range(inputs.size(0)):images_so_far += 1ax = plt.subplot(num_images // 5, 5, images_so_far)ax.axis('off')ax.set_title(f'Predicted: {preds[j]} (Label: {labels[j]})')ax.imshow(inputs.cpu().data[j].numpy().squeeze(), cmap='gray')if images_so_far == num_images:model.train()returnmodel.train()# 執行訓練
train(model, train_loader, optimizer, criterion)# 執行測試
test(model, test_loader)# 可視化預測結果
visualize_predictions(model, test_loader, num_images=10)
plt.suptitle('Model Predictions')
plt.tight_layout()
plt.show()
NeuralNetwork((flatten): Flatten(start_dim=1, end_dim=-1)(linear_relu_stack): Sequential((0): Linear(in_features=784, out_features=512, bias=True)(1): ReLU()(2): Linear(in_features=512, out_features=512, bias=True)(3): ReLU()(4): Linear(in_features=512, out_features=10, bias=True))
)
Epoch [1/5], Step [100/938], Loss: 1.1111
Epoch [1/5], Step [200/938], Loss: 0.6047
Epoch [1/5], Step [300/938], Loss: 0.5097
Epoch [1/5], Step [400/938], Loss: 0.4919
Epoch [1/5], Step [500/938], Loss: 0.4808
Epoch [1/5], Step [600/938], Loss: 0.4519
Epoch [1/5], Step [700/938], Loss: 0.4558
Epoch [1/5], Step [800/938], Loss: 0.4473
Epoch [1/5], Step [900/938], Loss: 0.4138
Epoch [2/5], Step [100/938], Loss: 0.3960
Epoch [2/5], Step [200/938], Loss: 0.3889
Epoch [2/5], Step [300/938], Loss: 0.4075
Epoch [2/5], Step [400/938], Loss: 0.3719
Epoch [2/5], Step [500/938], Loss: 0.3819
Epoch [2/5], Step [600/938], Loss: 0.3858
Epoch [2/5], Step [700/938], Loss: 0.3838
Epoch [2/5], Step [800/938], Loss: 0.3564
Epoch [2/5], Step [900/938], Loss: 0.3616
Epoch [3/5], Step [100/938], Loss: 0.3488
Epoch [3/5], Step [200/938], Loss: 0.3507
Epoch [3/5], Step [300/938], Loss: 0.3522
Epoch [3/5], Step [400/938], Loss: 0.3363
Epoch [3/5], Step [500/938], Loss: 0.3375
Epoch [3/5], Step [600/938], Loss: 0.3445
Epoch [3/5], Step [700/938], Loss: 0.3378
Epoch [3/5], Step [800/938], Loss: 0.3208
Epoch [3/5], Step [900/938], Loss: 0.3163
Epoch [4/5], Step [100/938], Loss: 0.3189
Epoch [4/5], Step [200/938], Loss: 0.3005
Epoch [4/5], Step [300/938], Loss: 0.3071
Epoch [4/5], Step [400/938], Loss: 0.3240
Epoch [4/5], Step [500/938], Loss: 0.3147
Epoch [4/5], Step [600/938], Loss: 0.2946
Epoch [4/5], Step [700/938], Loss: 0.3150
Epoch [4/5], Step [800/938], Loss: 0.3024
Epoch [4/5], Step [900/938], Loss: 0.3152
Epoch [5/5], Step [100/938], Loss: 0.2723
Epoch [5/5], Step [200/938], Loss: 0.2969
Epoch [5/5], Step [300/938], Loss: 0.2963
Epoch [5/5], Step [400/938], Loss: 0.2835
Epoch [5/5], Step [500/938], Loss: 0.2910
Epoch [5/5], Step [600/938], Loss: 0.2990
Epoch [5/5], Step [700/938], Loss: 0.2990
Epoch [5/5], Step [800/938], Loss: 0.3039
Epoch [5/5], Step [900/938], Loss: 0.3005
Test Accuracy: 87.60%
7 優化與調參
顯然,同樣的模型對于不同的數據集的適配程度是不一樣的。對于MNIST數據集準確率可以達到97%,但對于FashionMNIST只能達到86%。所以我們可以來探索一下為什么會有這樣的偏差,以及如何優化該模型才能讓FashionMNIST也可以達到90%以上的準確率。
7.1 可能的問題
- 數據集的復雜性
- MNIST 數據集:包含手寫數字的灰度圖像(0-9),這些圖像相對簡單,特征明顯,模式較少。
- FashionMNIST 數據集:包含服裝物品的灰度圖像(例如 T 恤、褲子、鞋子等),這些圖像的特征更加復雜,類別之間的差異較小。
- 模型的復雜性
- 我們使用的是一個簡單的全連接神經網絡,它可能足以在 MNIST 數據集上達到高準確率,但在處理更復雜的 FashionMNIST 數據集時會表現不佳。
- 超參數調整:
- 我們的模型可能需要在不同的數據集上進行不同的超參數調整。例如,學習率、批量大小、正則化參數等可能需要重新調整以適應 FashionMNIST 的復雜性。
- 數據預處理:
- 數據的預處理步驟(如標準化、歸一化、數據增強等)對不同的數據集可能有不同的效果。我們可能需要針對 FashionMNIST 數據集嘗試不同的預處理方法。
7.2 解決方案
7.2.1 增加神經網絡層數和神經元容量
通過增加模型的容量,模型能夠學習更多的特征。如下,我們添加兩個線性層進一步提高模型對復雜特征的處理能力。
class NeuralNetwork_v1(nn.Module):def __init__(self):super().__init__()self.flatten = nn.Flatten()self.linear_relu_stack = nn.Sequential(nn.Linear(28*28, 1024),nn.ReLU(),nn.Linear(1024, 1024),nn.ReLU(),nn.Linear(1024, 512),nn.ReLU(),nn.Linear(512, 512),nn.ReLU(),nn.Linear(512, 10))def forward(self, x):x = self.flatten(x)logits = self.linear_relu_stack(x)return logits
可能因為線性層在處理FashionMNIST數據集時,難以處理和學習更多的特征,在添加了線性層后預測準確率沒有明顯的提高,仍然是87%左右。所以需要嘗試其他的方法。
7.2.2 使用卷積神經網絡(CNN)
FashionMNIST 數據集涉及到服裝和配件的圖像分類,每個圖像都是單通道的灰度圖像,分辨率為 28x28 像素。盡管 FashionMNIST 數據集相對于真實世界的圖像數據集如 CIFAR-10 或 ImageNet 來說較為簡單,但仍然涉及到一定程度的空間特征。且如紋理圖案、形狀輪廓等復雜特征,線性層更加難以處理和識別。所以在局部相關性和空間結構處理占優的情況下,使用卷積神經網絡(CNN)來處理是更優的選擇。
import torch
import torch.nn as nn
import torch.nn.functional as Fclass ConvNeuralNetwork(nn.Module):def __init__(self):super(ConvNeuralNetwork, self).__init__()# 卷積層定義self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)# 扁平化層self.flatten = nn.Flatten()# 全連接層self.fc1 = nn.Linear(128*3*3, 512)self.fc2 = nn.Linear(512, 512)self.fc3 = nn.Linear(512, 10)# Dropout 正則化層,用于隨機丟棄一定比例的神經元,防止過擬合self.dropout = nn.Dropout(0.4)# 批量歸一化層,對每個卷積層的輸出進行歸一化,有助于加速收斂和提高模型泛化能力self.batch_norm1 = nn.BatchNorm2d(32)self.batch_norm2 = nn.BatchNorm2d(64)self.batch_norm3 = nn.BatchNorm2d(128)# 批量歸一化層,對全連接層的輸出進行歸一化self.batch_norm_fc1 = nn.BatchNorm1d(512)self.batch_norm_fc2 = nn.BatchNorm1d(512)def forward(self, x):# 卷積層和激活函數# 依次進行卷積、ReLU 激活函數、批量歸一化和最大池化操作x = self.conv1(x)x = F.relu(self.batch_norm1(x))x = F.max_pool2d(x, 2)x = self.conv2(x)x = F.relu(self.batch_norm2(x))x = F.max_pool2d(x, 2)x = self.conv3(x)x = F.relu(self.batch_norm3(x))x = F.max_pool2d(x, 2)# 進行全連接和正則化x = self.flatten(x)x = self.fc1(x)x = F.relu(self.batch_norm_fc1(x))x = self.dropout(x)x = self.fc2(x)x = F.relu(self.batch_norm_fc2(x))x = self.dropout(x)# 輸出層logits = self.fc3(x)return logits# 創建CNN實例,并移動到設備上
model_v1 = ConvNeuralNetwork().to(device)# 定義損失函數和優化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model_v1.parameters(), lr=0.01, momentum=0.9)# train_loader、test_loader 在之前已初始化# 訓練模型
def train_v1(model, train_loader, optimizer, criterion, epochs=5):model.train()for epoch in range(epochs):running_loss = 0.0for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)optimizer.zero_grad()output = model(data)loss = criterion(output, target)loss.backward()optimizer.step()running_loss += loss.item()if batch_idx % 100 == 99: # 每100個批次打印一次訓練狀態print(f'Epoch [{epoch+1}/{epochs}], Step [{batch_idx+1}/{len(train_loader)}], Loss: {running_loss/100:.4f}')running_loss = 0.0# 獲取類別名稱
class_names_v1 = train_dataset.classes# 設置中文字體
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用SimHei字體
plt.rcParams['axes.unicode_minus'] = False # 解決負號'-'顯示為方塊的問題def visualize_predictions_v1(model, dataloader, class_names):model.eval()num_classes = len(class_names)confusion_matrix = np.zeros((num_classes, num_classes), dtype=np.int32)with torch.no_grad():for inputs, labels in dataloader:inputs, labels = inputs.to(device), labels.to(device) # 將數據移動到設備上outputs = model(inputs)_, preds = torch.max(outputs, 1)for t, p in zip(labels.view(-1), preds.view(-1)):confusion_matrix[t.long(), p.long()] += 1# 歸一化混淆矩陣confusion_matrix = confusion_matrix.astype('float') / confusion_matrix.sum(axis=1)[:, np.newaxis]# 繪圖fig, ax = plt.subplots(figsize=(10, 8))im = ax.imshow(confusion_matrix, interpolation='nearest', cmap=plt.cm.Blues)ax.figure.colorbar(im, ax=ax)ax.set(xticks=np.arange(confusion_matrix.shape[1]),yticks=np.arange(confusion_matrix.shape[0]),xticklabels=class_names, yticklabels=class_names,title='歸一化混淆矩陣',ylabel='真實標簽',xlabel='預測標簽')# 旋轉標簽并設置對齊plt.setp(ax.get_xticklabels(), rotation=45, ha="right",rotation_mode="anchor")# 遍歷數據維度并創建文本注釋fmt = '.2f'thresh = confusion_matrix.max() / 2.for i in range(confusion_matrix.shape[0]):for j in range(confusion_matrix.shape[1]):ax.text(j, i, format(confusion_matrix[i, j], fmt),ha="center", va="center",color="white" if confusion_matrix[i, j] > thresh else "black")fig.tight_layout()plt.show()# 執行訓練
train_v1(model_v1, train_loader, optimizer, criterion)# 執行測試
test(model_v1, test_loader)# 可視化預測結果
visualize_predictions_v1(model_v1, test_loader, class_names_v1)
plt.suptitle('Model Predictions_v1')
plt.tight_layout()
plt.show()
Epoch [1/5], Step [100/938], Loss: 0.7875
Epoch [1/5], Step [200/938], Loss: 0.4787
Epoch [1/5], Step [300/938], Loss: 0.4455
Epoch [1/5], Step [400/938], Loss: 0.3960
Epoch [1/5], Step [500/938], Loss: 0.3657
Epoch [1/5], Step [600/938], Loss: 0.3557
Epoch [1/5], Step [700/938], Loss: 0.3363
Epoch [1/5], Step [800/938], Loss: 0.3495
Epoch [1/5], Step [900/938], Loss: 0.3140
Epoch [2/5], Step [100/938], Loss: 0.2842
Epoch [2/5], Step [200/938], Loss: 0.3065
Epoch [2/5], Step [300/938], Loss: 0.2671
Epoch [2/5], Step [400/938], Loss: 0.2750
Epoch [2/5], Step [500/938], Loss: 0.2874
Epoch [2/5], Step [600/938], Loss: 0.2722
Epoch [2/5], Step [700/938], Loss: 0.2639
Epoch [2/5], Step [800/938], Loss: 0.2840
Epoch [2/5], Step [900/938], Loss: 0.2630
Epoch [3/5], Step [100/938], Loss: 0.2359
Epoch [3/5], Step [200/938], Loss: 0.2461
Epoch [3/5], Step [300/938], Loss: 0.2350
Epoch [3/5], Step [400/938], Loss: 0.2337
Epoch [3/5], Step [500/938], Loss: 0.2453
Epoch [3/5], Step [600/938], Loss: 0.2247
Epoch [3/5], Step [700/938], Loss: 0.2354
Epoch [3/5], Step [800/938], Loss: 0.2351
Epoch [3/5], Step [900/938], Loss: 0.2333
Epoch [4/5], Step [100/938], Loss: 0.2045
Epoch [4/5], Step [200/938], Loss: 0.2206
Epoch [4/5], Step [300/938], Loss: 0.2161
Epoch [4/5], Step [400/938], Loss: 0.2125
Epoch [4/5], Step [500/938], Loss: 0.2003
Epoch [4/5], Step [600/938], Loss: 0.2060
Epoch [4/5], Step [700/938], Loss: 0.1919
Epoch [4/5], Step [800/938], Loss: 0.2012
Epoch [4/5], Step [900/938], Loss: 0.2138
Epoch [5/5], Step [100/938], Loss: 0.1789
Epoch [5/5], Step [200/938], Loss: 0.1724
Epoch [5/5], Step [300/938], Loss: 0.1737
Epoch [5/5], Step [400/938], Loss: 0.1883
Epoch [5/5], Step [500/938], Loss: 0.1921
Epoch [5/5], Step [600/938], Loss: 0.1982
Epoch [5/5], Step [700/938], Loss: 0.2055
Epoch [5/5], Step [800/938], Loss: 0.1865
Epoch [5/5], Step [900/938], Loss: 0.1930
Test Accuracy: 91.53%
<Figure size 640x480 with 0 Axes>
7.2.3 調整超參數
超參數是模型訓練過程中需要預先設定的參數,學習率、批次大小和迭代次數等都稱之為超參數。通過調整這些超參數,我們可以提高模型的性能和準確性。
- 學習率是最重要的超參數之一。我們可以嘗試不同的學習率,觀察其對模型性能的影響:
learning_rates = [0.1, 0.01, 0.001]for lr in learning_rates:optimizer = optim.SGD(model.parameters(), lr=lr)# 訓練模型并記錄性能
- 當然我們還可以通過學習率調度器在訓練過程中動態調整學習率
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)for epoch in range(num_epochs):# 訓練模型scheduler.step()
- 批量大小會影響訓練的穩定性和速度:
batch_sizes = [32, 64, 128]for batch_size in batch_sizes:train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)# 訓練模型并記錄性能
- 不同的優化器可能會對模型的收斂速度和最終性能產生影響。我們可以嘗試不同的優化器,如SGD和Adam:
optimizers = {'SGD': optim.SGD(model.parameters(), lr=0.01),'Adam': optim.Adam(model.parameters(), lr=0.01)
}
參考
- PyTorch-構建神經網絡
- PyTorch中文文檔
- d2L-多層感知機