繼續在我的系列文章中探讨如何构建曾在過去1-2十年的间革新電腦視覺領域的經典卷積神經網絡,我們接下來自行使用PyTorch建造一个非常深的卷積神經網絡VGG。你可以在我的個人檔案中看到這個系列文章的前一篇文章,主要是LeNet5和AlexNet。
與此同時,我們將探讨VGG的結構和背後理念以及當時的表現結果。然後,我們將研究我們的數據集CIFAR100,並使用記憶效率高的代碼將其載入我們的程式中。接下來,我們將使用PyTorch從頭開始實現VGG16(數字指的是層數,主要有兩個版本:VGG16和VGG19),然後在我們的測試集中的數據上訓練它,並對其進行評估,以查看它在未見过的數據上的表現如何。
建立在AlexNet的工作之上,VGG着眼的另一種卷積神經網絡(CNNs)的重要方面是深度。它由Simonyan和Zisserman開發。它通常由16個卷積層組成,但也可以擴展到19層(因此有两个版本,VGG-16和VGG-19)。所有的卷積層都由3×3過濾器組成。您可以在官方論文這裡

了解更多關於網絡的詳細信息。來源
在建立模型之前,任何機器學習项目中最重要的其中之一就是載入、分析與預處理數據集。在本文中,我們將使用CIFAR-100數據集。這個數據集就像CIFAR-10一樣,只是它有100個類別,每個類別含有600張圖像。每個類別有500張訓練圖像和100張測試圖像。CIFAR-100中的100個類別被分為20個超大類別。每張圖像都伴隨著一個“細”標籤(它所属的類別)和一個“粗”標籤(它所属的超類別)。我們將在此使用“細”標籤。以下是CIFAR-100的類別列表:

CIFAR-100數據集的類別列表
我們將主要使用torch
(用於建立模型和訓練)、torchvision
(用於數據載入/處理,含有用於计算机视觉中處理這些數據集的數據集和方法)和numpy
(用於數值計算)。我們還將定義一個變量device
,使程序能夠在有可用GPU時使用
torchvision
是一個庫,提供輕鬆存取大量電腦視覺數據集,以及容易且直觀的方式來前處理這些數據集
- 我們定義了一個函數
data_loader
,它根據參數返回訓練/驗證數據或測試數據
- 我們首先定義了一個變量
normalize
,它包含了數據集中每個通道(紅、綠、藍)的平均值和標準差。這些可以手動計算,但也可以在網路上找到。這在 transform
變量中使用,我們將數據縮放到正確的大小,然後將其轉為張量並進行標準化
- 如果
test
參數為真,我們只是載入數據集的測試部分,並使用數據载入器(稍後解釋)返回它
- 如果
test
為假(這是預設行為),我們則載入數據集的訓練部分,並隨機將其分為訓練和驗證集(9:1)
- 最後,我們使用數據载入器。對於像 CIFAR100 這樣的小數據集,這可能不會影響性能,但在大數據集情況下,這可能會严重影响性能,且通常被認為是一種良好實踐。數據载入器讓我們可以批量迭代數據,而且在迭代過程中載入數據,而不是在開始時將所有數據一次載入內存中。
def data_loader(data_dir,
batch_size,
random_seed=42,
valid_size=0.1,
shuffle=True,
test=False):
normalize = transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2023, 0.1994, 0.2010],
)
transform = transforms.Compose([
transforms.Resize((227,227)),
transforms.ToTensor(),
normalize,
])
if test:
dataset = datasets.CIFAR100(
root=data_dir, train=False,
download=True, transform=transform,
)
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=batch_size, shuffle=shuffle
)
return data_loader
train_dataset = datasets.CIFAR100(
root=data_dir, train=True,
download=True, transform=transform,
)
valid_dataset = datasets.CIFAR10(
root=data_dir, train=True,
download=True, transform=transform,
)
num_train = len(train_dataset)
indices = list(range(num_train))
split = int(np.floor(valid_size * num_train))
if shuffle:
np.random.seed(random_seed)
np.random.shuffle(indices)
train_idx, valid_idx = indices[split:], indices[:split]
train_sampler = SubsetRandomSampler(train_idx)
valid_sampler = SubsetRandomSampler(valid_idx)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, sampler=train_sampler)
valid_loader = torch.utils.data.DataLoader(
valid_dataset, batch_size=batch_size, sampler=valid_sampler)
return (train_loader, valid_loader)
train_loader, valid_loader = data_loader(data_dir='./data',
batch_size=64)
test_loader = data_loader(data_dir='./data',
batch_size=64,
test=True)
要从頭開始建立模型,我們首先需要了解在 torch
中模型的定義如何工作,以及我們將在此使用的不同類型的層:
- 每個自訂模型都需要從
nn.Module
類繼承,因為它提供了一些基本功能,幫助模型進行訓練。
- 其次,我們需要做兩件主要的事情。首先,在
__init__
函數內定義我們模型的不同層,以及這些層在 forward
函數內對於輸入数据將執行的順序
現在讓我們定義我們在此使用的各種類型的層:
nn.Conv2d
:這些是卷積層,它接受輸入和輸出通道的數量作為參數,以及過濾器的核大小。它還接受任何步長或填充,如果你想要應用這些功能
nn.BatchNorm2d
:此函數將批次正規化應用於卷積層的輸出
nn.ReLU
:此為應用於網絡中各種輸出的激活函數。
nn.MaxPool2d
: 此函式將應用最大池化至給定核大小之輸出
nn.Dropout
: 此函式用於在給定機率下將輸出進行dropout
nn.Linear
: 此為基本上是一個全連接層
nn.Sequential
: technically 這不是一種層,但它有助於結合屬於同一步的不同操作
倚靠此知識,我們現在可以建立我們的VGG16模型,使用论文中的架構:
class VGG16(nn.Module):
def __init__(self, num_classes=10):
super(VGG16, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
self.layer2 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU())
self.layer4 = nn.Sequential(
nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer5 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU())
self.layer6 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU())
self.layer7 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer8 = nn.Sequential(
nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer9 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer10 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer11 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer12 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer13 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(7*7*512, 4096),
nn.ReLU())
self.fc1 = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU())
self.fc2= nn.Sequential(
nn.Linear(4096, num_classes))
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
out = self.layer6(out)
out = self.layer7(out)
out = self.layer8(out)
out = self.layer9(out)
out = self.layer10(out)
out = self.layer11(out)
out = self.layer12(out)
out = self.layer13(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
out = self.fc1(out)
out = self.fc2(out)
return out
從頭開始的VGG16
任何機器或深度學習项目中重要的部分是優化超参数。在這裡,我們不會experiment 不同的值那些,但我們將必須在hand 之前定義它們。這些包括定義迭代的次數、批次大小、學習率、損失函數以及優化器
設置超参数
我們現在準備訓練我們的模型。我們先看一下在 torch
中如何訓練我們的模型,然後查看代碼:
- 對於每個時代,我們遍历我們
train_loader
中的圖像和標籤,並將這些圖像和標籤移到可用的GPU上。這個過程是自動的
- 我們使用我們的模型來在標籤上進行預測(
model(images)
),然後使用我們的損失函數(criterion(outputs, labels)
)計算預測值和真實標籤之間的損失
- 然後我們使用這個損失進行反向傳播(
loss.backward
)並更新權重(optimizer.step()
)。但請記住,在每次更新之前將梯度置為零。這是用optimizer.zero_grad()
完成的
- 此外,在每個時代的尾声,我們使用我們的驗證集來計算模型的準確性。在這個情況下,我們不需要梯度,因此我們使用
with torch.no_grad()
以加快評估
現在,我們將所有這些內容結合到下面的代碼中:
訓練
我們可以從上面代碼的輸出看到如下結果,這表明模型的實際學習情況,因為損失隨著每個時代的減少:

訓練損失
為測試,我們使用與驗證完全相同的代碼,但與test_loader
一起:
測試
使用上述代碼並對模型進行20個週期的訓練,我們能夠在測試集中的準確率达到75%。
現在讓我們總結本文中所做的事情:
- 我們首先了解VGG-16模型的結構以及不同類型的層
- 接下來,我們使用
torchvision
載入和預處理CIFAR100數據集
- 然後,我們使用
PyTorch
從頭開始建造我們的VGG-16模型,並了解torch
中不同類型的層
- 最後,我們的模型在CIFAR100數據集上進行了訓練和測試,並且模型在測試數據集上表現出色,準確率达到75%
通過本文,您獲得了一個很好的介紹和實戰學習,但如果您延伸這個並看看您還能做什麼,您將學習到更多:
- 您可以嘗試使用不同的數據集。一個這樣的數據集是CIFAR10或ImageNet數據集的子集。
- 您可以试验区別性的超参数,並查看對模型最佳的組合。
- 最後,您可以嘗試從數據集中添加或移除層,以查看它們對模型性能的影響。更棒的是,嘗試建造這個模型的VGG-19版本。