使用 Pytorch 實現卷積神經網絡

【導讀】圖像識別是深度學習取得重要成功的領域,特別是卷積神經網絡在圖像識別和圖像分類中取得了超過人類的好成績。本文詳細介紹了卷積神經網絡(CNN)的基本結構,對卷積神經網絡中的重要部分進行詳細講解,如卷積、非線性函數 ReLU、Max-Pooling、全連接等。另外,本文通過對 CIFAR-10 的 10 類圖像分類來加深讀者對 CNN 的理解和 Pytorch 的使用,列舉了如何使用 Pytorch 收集和加載數據集、設計神經網絡、進行網絡訓練、調參和準確度量。總的來講,這篇文章偏重概念理解和動手實現,相信對您的入門會有幫助。

使用 Pytorch 實現卷積神經網絡

卷積神經網絡在許多計算機視覺任務中取得了令人震驚的突破,幾乎是是開發人員和數據科學家必備技能之一。

本教程將介紹卷積神經網絡(CNN)的基本結構,解釋它的工作原理,並使用 pytorch 實一步步實現一個簡單的 CNN 網絡。 

什麼是卷積神經網絡?

CNN 算是計算機視覺的一個子領域,它的應用對象是視覺內容——圖像。人們經常將 CNN 稱爲一種算法,但它實際上是多種不同算法的一種組合。CNN 與普通神經網絡的主要區別在於預處理。它有兩個要點:

CNN 中的預處理旨在將輸入圖像轉換爲一組神經網絡能更方便理解的特徵。它看起來很複雜,但只要你記住上述兩個要點,就不會迷茫。

卷積

CNN 的名字來源於 Convolution(卷積),它是提取提取圖像特徵的第一步。卷積可以看成是對圖像濾波。我們傳遞一個小濾波器,通常稱爲 kernel,並輸出濾波後的圖像。

由於圖像只是一串像素值,實際上這意味着我們的輸入圖像的一部分與濾波器相乘。可調節的參數有:

卷積的輸出稱爲 “卷積特徵” 或“特徵圖”。得到的特徵可以看作是輸入圖像的優化表示。實踐表明,卷積與後面的兩個步驟(ReLU,池化)相結合可以大大提高圖像分類的準確性。

在 Pytorch 中,卷積操作用 torch.nn.Conv2d()函數實現。

ReLU

由於神經網絡的前向傳播本質上是一個線性函數(只是通過權重乘以輸入並添加一個偏置項),CNN 通常添加非線性函數來幫助神經網絡理解底層數據。

在 CNN 中,最受歡迎的非線性函數式 ReLU。ReLU 表示整流線性單位,它只是將所有負值變成 0. 即 output = Max(0,input)。

還有其他函數可以用來添加非線性,如 tanh 或 softmax。但在 CNN 中,ReLU 是最常用的。

在 Pytorch 中,ReLU 操作用 torch.nn.relu() 函數實現。

Max-Pooling

使用 CNN 提取特徵的最後一步是 pooling,名如其實:我們將一個區域裏面的最大值作爲該區域的代表。這可以減少傳入神經網絡的特徵。我們可以將它形象地表示爲:

Max-Pooling 也有一些可調整的參數,如步長和填充。還有別的池化方式,如 sum-pooling 和 average-pooling。

在 Pytorch 中,Macpooling 操作用 torch.nn.MaxPool2d() 函數實現.

全連接層

在上述預處理步驟之後,得到的特徵(可能和一開始的完全不一樣)被傳遞到傳統的神經網絡中。在這類我們使用包含一個隱藏層和一個輸出層的兩層神經網絡。

這部分和其他網絡相同,不是我們討論的重點。CNNs 的關鍵之處是提取特徵。好的特徵決定了模型達到的上限,而最終的分類器只決定我們有多接近這個上限。卷積,ReLU 和 maxpooling 從圖像中提取了有效的特徵。

在 Pytorch 中,全連接操作用 torch.nn.Linear()函數實現。

Pytorch 入門

Pytorch 是用於設計深度神經網絡的以 Python 框架之一,和 TensorFlow 和 Keras 等流行的框架類似。

在爲你的項目選擇合適的框架時,需要考慮以下幾點:

可以肯定地說,Pytorch 和其他框架相比更容易理解,並且它在數據科學領域很火。

收集和加載數據

大多數機器學習項目,不少代碼是用來收集、清洗、準備數據大部分都用於收集。用 Pytorch 實現 CNN 也是一樣。

Pytorch 附帶 torchvision 軟件包,可以輕鬆下載和使用數據集。在這裏,我們將使用 CIFAR-10 數據集。CIFAR-10 包含 10 個不同類別的圖像。

首先導入必要的包,如 Pytorch 和用於數值計算的 numpy。

我們還需要設置一個標準的隨機種子以獲得可重現的結果。

首先用 Pytorch 下載數據集。

#The compose function allows for multiple transforms
#transforms.ToTensor() converts our PILImage to a tensor of 
shape (C x H x W) in the range [0,1]
#transforms.Normalize(mean,std) normalizes a tensor to 
a (mean, std) for (R, G, B)
transform = transforms.Compose([transforms.ToTensor(), 
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

train_set = torchvision.datasets.CIFAR10(root='./cifardata', 
train=True, download=True, transform=transform)

test_set = torchvision.datasets.CIFAR10(root='./cifardata', 
train=False, download=True, transform=transform)

然後,指定標籤:

classes = ('plane', 'car','bird', 'cat',
          'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

最後,定義採樣器。在訓練模型時,它們會將訓練樣例分解爲訓練,測試和交叉驗證集。

from torch.utils.data.sampler import SubsetRandomSampler

#Training
n_training_samples =20000
train_sampler =SubsetRandomSampler(np.arange(n_training_samples, 
dtype=np.int64))

#Validation
n_val_samples =5000
val_sampler =SubsetRandomSampler(np.arange(n_training_samples, 
n_training_samples +n_val_samples, dtype=np.int64))

#Test
n_test_samples =5000
test_sampler =SubsetRandomSampler(np.arange(n_test_samples, 
dtype=np.int64))

用 Pytorch 設計神經網絡

用 Pytorch 實現上述步驟很容易,在 CNN 中使用 4 個主要函數:

  1. torch.nn.Conv2d(in_channels,out_channels,kernel_size,stride,padding)- 卷積

  2. torch.nn.relu(x) - ReLU torch.nn.

  3. MaxPool2d(kernel_size,stride,padding) - Max Pooling

  4. torch.nn.Linear(in_features,out_features) - 全連接(學習權值乘以輸入)

我們將創建繼承 torch.nn.Module 類的 SimpleCNN 類。

from torch.autograd import Variable
importtorch.nn.functional as F

classSimpleCNN(torch.nn.Module):
    
    #Our batch shape for input x is (3, 32, 32)
    
    def__init__(self):
        super(SimpleCNN,self).__init__()
        
        #Input channels = 3, output channels = 18
        self.conv1=torch.nn.Conv2d(3, 18, kernel_size=3, stride=1, 
padding=1)
        self.pool=torch.nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        
        #4608 input features, 64 output features (seesizing flow below)
        self.fc1=torch.nn.Linear(18*16*16, 64)
        
        #64 input features, 10 output features for our10 defined classes
        self.fc2=torch.nn.Linear(64, 10)
        
    defforward(self, x):
        
        #Computes the activation of the firstconvolution
        #Size changes from (3, 32, 32) to (18, 32, 32)
        x=F.relu(self.conv1(x))
        
        #Size changes from (18, 32, 32) to (18, 16, 16)
        x=self.pool(x)
        
        #Reshape data to input to the input layer ofthe neural net
        #Size changes from (18, 16, 16) to (1, 4608)
        #Recall that the -1 infers this dimension fromthe other 
       given dimension
        x=x.view(-1, 18*16*16)
        
        #Computes the activation of the first fullyconnected layer
        #Size changes from (1, 4608) to (1, 64)
        x=F.relu(self.fc1(x))
        
        #Computes the second fully connected layer(activation 
applied later)
        #Size changes from (1, 64) to (1, 10)
        x=self.fc2(x)
        return(x)

讓我來解釋一下這段代碼。我們在 SimpleCNN 類中定義了一個函數:forward。forward() 函數 CNN 的前向傳播,包括我們上面提到的預處理步驟。

手動定義神經網絡的麻煩之處在於,我們需要爲每一層指定輸入和輸出的大小。一般來說,輸入集合中任何維度的輸出大小都可以定義爲:

def outputSize(in_size, kernel_size, stride,padding):

output =int((in_size - kernel_size +2*(padding))/stride) +1

return(output)

比如,在 max pooling 層中,輸入維度爲(18,32,32) - 將公式應用於最後的兩個維度(第一維度或特徵映射的數量在池化操作期間保持不變),我們得到的輸出大小爲 (18,16,16)。

用 Pytorch 訓練神經網絡

在爲 CNN 定義了類別之後,就可以開始訓練網絡。這是神經網絡變得有趣的地方。如果您正在使用更多基本機器學習算法,則通常只需幾行代碼即可獲得有意義的輸出結果。例如,在 sklearn Python 包中實現支持向量機就如下一樣簡單:

#Import the support vectormachine module from the sklearn framework
fromsklearn import svm

#Label x and y variables from our dataset
x = ourData.features
y = ourData.labels

#Initialize our algorithm
classifier =svm.SVC()

#Fit model to our data
classifier.fit(x,y)

然而,使用 Pytorch(和 TensorFlow)實現神經網絡,需要更多代碼。基本流程是一個訓練循環:每次我們通過循環(被稱爲 “epoch”)時,我們計算網絡上的前向傳播並實施反向傳播來調整權重。我們還會記錄一些其他測量值,比如損失和時間,來分析網絡的優劣。

首先,使用上面創建的採樣器來定義我們的數據加載器。

#DataLoader takes in adataset and a sampler for loading 
(num_workers deals with system level memory) 
defget_train_loader(batch_size):
   train_loader = torch.utils.data.DataLoader(train_set, 
batch_size=batch_size, sampler=train_sampler, num_workers=2)
    return(train_loader)

#Test and validation loadershave constant batch sizes, 
so we can define them directly
test_loader =torch.utils.data.DataLoader(test_set, 
batch_size=4, sampler=test_sampler, num_workers=2)
val_loader =torch.utils.data.DataLoader(train_set, 
batch_size=128, sampler=val_sampler, num_workers=2)

我們還將定義我們的損失函數和優化器,CNN 將使用它來調整權重。我們將使用交叉熵損失(對數損失)作爲損失函數,它會強烈懲罰錯誤的答案。優化器選擇流行的 Adam 算法。

import torch.optim as optim

defcreateLossAndOptimizer(net, learning_rate=0.001):
    
    #Loss function
    loss =torch.nn.CrossEntropyLoss()
    
    #Optimizer
   optimizer = optim.Adam(net.parameters(), lr=learning_rate)
    
    return(loss,optimizer)

最後,我們將定義一個函數來使用簡單的 for 循環來訓練我們的 CNN。

import time

deftrainNet(net, batch_size, n_epochs, learning_rate):
    
    #Print all of the hyperparameters of thetraining iteration:
    print("===== HYPERPARAMETERS =====")
    print("batch_size=",batch_size)
    print("epochs=",n_epochs)
    print("learning_rate=",learning_rate)
    print("="*30)
    
    #Get training data
   train_loader = get_train_loader(batch_size)
   n_batches =len(train_loader)
    
    #Create our loss and optimizer functions
    loss,optimizer = createLossAndOptimizer(net, learning_rate)
    
    #Time for printing
   training_start_time = time.time()
    
    #Loop for n_epochs
    forepoch inrange(n_epochs):
        
       running_loss =0.0
       print_every = n_batches //10
       start_time = time.time()
       total_train_loss =0
        
        fori, data inenumerate(train_loader, 0):
           
           #Get inputs
           inputs, labels = data
           
           #Wrap them in a Variableobject
           inputs, labels = Variable(inputs), Variable(labels)
           
           #Set the parameter gradientsto zero
           optimizer.zero_grad()
           
           #Forward pass, backward pass,optimize
           outputs = net(inputs)
           loss_size = loss(outputs, labels)
           loss_size.backward()
           optimizer.step()
           
           #Print statistics
           running_loss += loss_size.data[0]
           total_train_loss += loss_size.data[0]
           
           #Print every 10th batch of anepoch
           if (i +1) % (print_every +1) ==0:
               print("Epoch {},{:d}% \t train_loss: {:.2f} took: 
 {:.2f}s".format(epoch+1, int(100* (i+1) /n_batches), 
running_loss / print_every, time.time() - start_time))
               #Reset running loss and time
               running_loss =0.0
               start_time = time.time()
           
        #Atthe end of the epoch, do a pass on the validation set
       total_val_loss =0
        forinputs, labels in val_loader:
           
           #Wrap tensors in Variables
           inputs, labels = Variable(inputs), Variable(labels)
            
           #Forward pass
           val_outputs = net(inputs)
           val_loss_size = loss(val_outputs, labels)
           total_val_loss += val_loss_size.data[0]
           
        print("Validation loss = {:.2f}".
        format(total_val_loss/len(val_loader)))
        
    print("Training finished, took {:.2f}s".
    format(time.time() - training_start_time))

在每個訓練階段,我們將數據以預先定義的批量傳遞給模型。訓練時,使用剛定義的 SimpleCNN 提取特徵,並在幾輪之後打印模型在驗證集上的評估結果。

真正訓練模型的代碼其實只有如下兩行:

CNN = SimpleCNN()
trainNet(CNN, batch_size=32, n_epochs=5, learning_rate=0.001)

就是這樣!你成功地用 Pytorch 實現了 CNN。

更進一步

準確度量

我們的訓練循環打印出 CNN 的兩個準確度量度:訓練損失(每 10 輪打印一次)和驗證集誤差(每輪打印一次)。當定義 CNN 損失和優化函數時,我們使用了 torch.nn.CrossEntropyLoss()函數。

交叉熵損失(也稱爲對數損失)輸出介於 0 和 1 之間的概率值,隨着預測標籤與實際標籤的分離概率的增加而增加。

對於機器學習,會使用精度,召回率和混淆矩陣等其他準確度度量。每個項目都有不同的目標,因此應該針對這些目標量身定製度量標準。

網絡架構

在 CIFAR-10 數據集中,訓練數據集上得到約 60%的準確度。這比隨機猜測要好得多,但它離現有最好的結果還很遙遠。

我們的模型與精度達到 80%以上的模型之間的主要差異之一是層數。我們的網絡有一個卷積層,一個池層和一個全連接層,一個輸出層。而 VGG-16 架構利用 16 個以上的層次,並在 ImageNet 2014 挑戰賽中獲得高分。

爲了在我們的 CNN 中添加更多的層數,我們可以在初始化 SimpleCNN 類實例的過程中創建新的方法(儘管此時我們可能想要將類名更改爲 LessSimpleCNN)。

例如,我們可以嘗試:

self.conv2= torch.nn.Conv2d(3,18,kernel_size = 3,stride = 1,
padding = 1)
self.pool2 = torch.nn.MaxPool2d(kernel_size = 2,stride = 2,
padding = 0)

調整超參數

除了改變我們使用的輸入和激活函數之外,卷積和 maxpooling 還有更多可以調整的超參數。如 kernel_size,stride 和 padding 可以提取更多的信息特徵,並得到更高的準確性(如果不是過擬合)。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3_IJgNgyqlrzKyD0tV-89Q