Pytorch從零開始實戰——DenseNet算法實戰
本系列來源于365天深度學習訓練營
原作者K同學
文章目錄
- Pytorch從零開始實戰——DenseNet算法實戰
- 環境準備
- 數據集
- 模型選擇
- 開始訓練
- 可視化
- 總結
環境準備
本文基于Jupyter notebook,使用Python3.8,Pytorch2.0.1+cu118,torchvision0.15.2,需讀者自行配置好環境且有一些深度學習理論基礎。本次實驗的目的是理解并使用DenseNet模型,本次實驗由于參數較大,建議使用GPU進行計算。
第一步,導入常用包
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn.functional as F
import random
from time import time
import numpy as np
import pandas as pd
import datetime
import gc
import os
import copy
import warnings
os.environ['KMP_DUPLICATE_LIB_OK']='True' # 用于避免jupyter環境突然關閉
torch.backends.cudnn.benchmark=True # 用于加速GPU運算的代碼
設置隨機數種子
torch.manual_seed(428)
torch.cuda.manual_seed(428)
torch.cuda.manual_seed_all(428)
random.seed(428)
np.random.seed(428)
檢查設備對象
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device, torch.cuda.device_count() # # (device(type='cuda'), 2)
數據集
本次實驗將探索在醫學領域使用深度學習,準確識別和分類乳腺癌亞型是一項重要的臨床任務,利用深度學習方法識別可以有效節省時間并減少錯誤。數據集是由多張以 40 倍掃描的乳腺癌 (BCa) 標本的完整載玻片圖像組成。
使用pathlib查看類別,本次類別只有0,1兩種類別分別代表不患癌和患癌
import pathlib
data_dir = './data/ill/'
data_dir = pathlib.Path(data_dir) # 轉成pathlib.Path對象
data_paths = list(data_dir.glob('*'))
classNames = [str(path).split("/")[2] for path in data_paths]
classNames # ['0', '1']
使用transforms對數據集進行統一處理,并且根據文件夾名映射對應標簽
all_transforms = transforms.Compose([transforms.Resize([224, 224]),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 標準化
])total_data = datasets.ImageFolder("./data/ill/", transform=all_transforms)
total_data.class_to_idx # {'0': 0, '1': 1}
隨機查看5張圖片
def plotsample(data):fig, axs = plt.subplots(1, 5, figsize=(10, 10)) #建立子圖for i in range(5):num = random.randint(0, len(data) - 1) #首先選取隨機數,隨機選取五次#抽取數據中對應的圖像對象,make_grid函數可將任意格式的圖像的通道數升為3,而不改變圖像原始的數據#而展示圖像用的imshow函數最常見的輸入格式也是3通道npimg = torchvision.utils.make_grid(data[num][0]).numpy()nplabel = data[num][1] #提取標簽 #將圖像由(3, weight, height)轉化為(weight, height, 3),并放入imshow函數中讀取axs[i].imshow(np.transpose(npimg, (1, 2, 0))) axs[i].set_title(nplabel) #給每個子圖加上標簽axs[i].axis("off") #消除每個子圖的坐標軸plotsample(total_data)
根據8比2劃分數據集和測試集,并且利用DataLoader劃分批次和隨機打亂
train_size = int(0.8 * len(total_data))
test_size = len(total_data) - train_size
train_ds, test_ds = torch.utils.data.random_split(total_data, [train_size, test_size])batch_size = 32
train_dl = torch.utils.data.DataLoader(train_ds,batch_size=batch_size,shuffle=True,)
test_dl = torch.utils.data.DataLoader(test_ds,batch_size=batch_size,shuffle=True,)len(train_dl.dataset), len(test_dl.dataset) # (10722, 2681)
模型選擇
本次實驗使用DenseNet模型,DenseNet的設計核心思想是通過密集連接來增強神經網絡的信息流動,促進梯度的傳播,以及提高參數的共享和重復使用。采用跨通道concat的形式來連接,會連接前面所有層作為輸入,這里的連接不是ResNet那樣的相加,而在channel維度的疊加。
核心公式為:
DenseNet中的基本組成單元是DenseBlock,它由多個密集連接的DenseLayer組成。每個DenseLayer都接收所有前面的DenseLayer特征作為輸入,將其連接到自己的輸出上,并傳遞給后續的層。如圖所示,這是一個基本的DenseBlock模塊。
整體網絡架構圖如下所示,借用K同學的圖片
為了控制模型的復雜度并減少特征圖的大小,DenseNet引入了Transition Block。過渡塊包括批歸一化、ReLU激活和 1x1 卷積,以減小特征圖的通道數,并通過池化操作降低空間維度。
首先對DenseLayer類定義,本次實驗使用add_module函數,默認是用于向類中添加一個子模塊,第一個參數為模塊名,第二個參數為模塊實例,其實相當于加到父類的nn.Sequential里面,所以調用的時候使用super().forward(x),這段的核心是將輸入 x 與新特征 t 進行通道維度上的連接,完成密集連接。
class DenseLayer(nn.Sequential):def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):super().__init__()self.add_module("norm1", nn.BatchNorm2d(num_input_features))self.add_module("relu1", nn.ReLU(inplace=True))self.add_module("conv1", nn.Conv2d(num_input_features, bn_size * growth_rate, kernel_size=1, stride=1, bias=False))self.add_module("norm2", nn.BatchNorm2d(bn_size * growth_rate))self.add_module("relu2", nn.ReLU(inplace=True))self.add_module("conv2", nn.Conv2d(bn_size*growth_rate, growth_rate, kernel_size=3, stride=1, padding=1, bias=False))self.drop_rate = drop_ratedef forward(self, x):t = super().forward(x)if self.drop_rate > 0:t = F.dropout(t, p=self.drop_rate, training=self.training)return torch.cat([x, t], 1)
下面是DenseBlock的實現,通過循環創建了多個DenseLayer。其中的 num_input_features + i * growth_rate 用于指定輸入通道的數量,確保每個DenseLayer的輸入通道數逐漸增加。將新創建的DenseLayer添加為 DenseBlock 的子模塊。循環結束后,DenseBlock 就包含了多個DenseLayer,每個DenseLayer都具有逐漸增加的輸入通道數量。
class DenseBlock(nn.Sequential):def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate):super().__init__()for i in range(num_layers):layer = DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size, drop_rate)self.add_module("denselayer%d" % (i + 1), layer)
下面是Transition,實現過渡的功能,是在塊之間降低通道數量和空間維度。
class Transition(nn.Sequential):def __init__(self, num_input_feature, num_output_features):super().__init__()self.add_module("norm", nn.BatchNorm2d(num_input_feature))self.add_module("relu", nn.ReLU(inplace=True))self.add_module("conv", nn.Conv2d(num_input_feature, num_output_features, kernel_size=1, stride=1, bias=False))self.add_module("pool", nn.AvgPool2d(2, stride=2))
模型實現,self.features 是一個包含多個層的序列,包括初始卷積塊、多個DenseBlock和Transition,以及最后的全局平均池化和分類器。遍歷 block_config 中的配置,創建DenseBlock和Transition。參數初始化部分使用了 Kaiming 初始化和常數初始化。
其中,OrderedDict是Python中的一種有序字典數據結構,它保留了元素添加的順序。在神經網絡中,我們可以使用OrderedDict來指定模型的層次結構。
from collections import OrderedDict
class DenseNet(nn.Module):def __init__(self, growth_rate=32, block_config=(6, 12, 24, 16), num_init_features=64,bn_size=4, compression_rate=0.5, drop_rate=0, num_classes=1000):super().__init__()self.features = nn.Sequential(OrderedDict([("conv0", nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),("norm0", nn.BatchNorm2d(num_init_features)),("relu0", nn.ReLU(inplace=True)),("pool0", nn.MaxPool2d(3, stride=2, padding=1))]))num_features = num_init_featuresfor i, num_layers in enumerate(block_config):block = DenseBlock(num_layers, num_features, bn_size, growth_rate, drop_rate)self.features.add_module("denseblock%d" % (i + 1), block)num_features += num_layers * growth_rateif i != len(block_config) - 1:transition = Transition(num_features, int(num_features * compression_rate))self.features.add_module("transition%d" % (i + 1), transition)num_features = int(num_features * compression_rate)self.features.add_module("norm5", nn.BatchNorm2d(num_features))self.features.add_module("relu5", nn.ReLU(inplace=True))self.classifier = nn.Linear(num_features, num_classes)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight)elif isinstance(m, nn.BatchNorm2d):nn.init.constant_(m.bias, 0)nn.init.constant_(m.weight, 1)elif isinstance(m, nn.Linear):nn.init.constant_(m.bias, 0)def forward(self, x):features = self.features(x)out = F.avg_pool2d(features, 7, stride=1).view(features.size(0), -1)out = self.classifier(out)return out
使用summary查看網絡
開始訓練
定義訓練函數
def train(dataloader, model, loss_fn, opt):size = len(dataloader.dataset)num_batches = len(dataloader)train_acc, train_loss = 0, 0for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model(X)loss = loss_fn(pred, y)opt.zero_grad()loss.backward()opt.step()train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()train_loss += loss.item()train_acc /= sizetrain_loss /= num_batchesreturn train_acc, train_loss
定義測試函數
def test(dataloader, model, loss_fn):size = len(dataloader.dataset)num_batches = len(dataloader)test_acc, test_loss = 0, 0with torch.no_grad():for X, y in dataloader:X, y = X.to(device), y.to(device)pred = model(X)loss = loss_fn(pred, y)test_acc += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss += loss.item()test_acc /= sizetest_loss /= num_batchesreturn test_acc, test_loss
定義學習率、損失函數、優化算法
loss_fn = nn.CrossEntropyLoss()
learn_rate = 0.0001
opt = torch.optim.Adam(model.parameters(), lr=learn_rate)
開始訓練,epoch設置為20
import time
epochs = 20
train_loss = []
train_acc = []
test_loss = []
test_acc = []T1 = time.time()best_acc = 0
best_model = 0for epoch in range(epochs):model.train()epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)model.eval() # 確保模型不會進行訓練操作epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)if epoch_test_acc > best_acc:best_acc = epoch_test_accbest_model = copy.deepcopy(model)train_acc.append(epoch_train_acc)train_loss.append(epoch_train_loss)test_acc.append(epoch_test_acc)test_loss.append(epoch_test_loss)print("epoch:%d, train_acc:%.1f%%, train_loss:%.3f, test_acc:%.1f%%, test_loss:%.3f"% (epoch + 1, epoch_train_acc * 100, epoch_train_loss, epoch_test_acc * 100, epoch_test_loss))T2 = time.time()
print('程序運行時間:%s秒' % (T2 - T1))PATH = './best_model.pth' # 保存的參數文件名
if best_model is not None:torch.save(best_model.state_dict(), PATH)print('保存最佳模型')
print("Done")
效果還是不錯的
可視化
可視化訓練過程與測試過程
import warnings
warnings.filterwarnings("ignore") #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用來正常顯示中文標簽
plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負號
plt.rcParams['figure.dpi'] = 100 #分辨率epochs_range = range(epochs)plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
總結
本次實驗學習到了一個更激進的密集連接機制,每個層都會包含前面層所有的輸入,而且與ResNet不同,層與層之間使用疊加的方式進行連接,來增強神經網絡的信息流動,促進梯度的傳播,以及提高參數的共享和重復使用,使得模型表現出不錯的效果。