上一節,我們已經學會了基於PyTorch深度學習框架高效,快捷的搭建一個神經網絡,並對模型進行訓練和對參數進行優化的方法,接下來讓我們牛刀小試,基於PyTorch框架使用神經網絡來解決一個關於手寫數字識別的計算機視覺問題,評價我們搭建的模型的標准是它是否能准確的對手寫數字圖片進行識別。
其具體的過程是:先使用已經提供的訓練數據對搭建好的神經網絡模型進行訓練並完成參數優化,然后使用優化好的模型對測試數據進行預測,對比預測值和真實值之間的損失值,同時計算出結果預測的准確率。在將要搭建的模型中會使用到卷積神經網絡模型,下面讓我們開始學習吧。
知識儲備——深度學習中的常見概念
1:MNIST數據集的了解
MNIST數據集是一個已經被“嚼爛”了的數據集,很多教程都會對他“下手”,幾乎成為了一個典范。
具體詳情請參考 Yann LeCun's MNIST page 或 Chris Olah's visualizations of MNIST.
下載下來的文件如下:


可從該頁面獲得的MNIST手寫數字數據庫具有60,000個示例的訓練集和10,000個示例的測試集。它是NIST提供的更大集合的子集。數字已經過尺寸標准化,並以固定尺寸的圖像為中心。
對於那些希望在實際數據上嘗試學習技術和模式識別方法,同時在預處理和格式化方面花費最少的人來說,它是一個很好的數據庫。
2:梯度遞減算法
在對決策函數進行優化的時候,通常是針對一個誤差的度量,比如誤差的平方,以求得一系列參數,從而最小化這個誤差度量的值來進行的,而目前一般采用的計算方法是梯度遞減法(Gradient Descent Method )。這是一個非常形象的名字,好比一個游客要從某個不知名的高山上盡快,安全的下到谷底,這時候需要借助指南針來引導方向。對於這個游客,他需要在南北和東西兩個軸向上進行選擇,以保證下山的路在當前環境下即使最快的,又是最安全的。我們可以將南北和東西兩個軸向想象成目標函數里面的兩個維度或者自變量。那么這個游客怎么獲取這個最優的路徑呢?
在山頂的時候,游客因為不能完全看到通往谷底的情況,所以很可能隨機選擇一條路線。這個選擇很多時候很關鍵。一般山頂是一塊平地,有多個可以選擇下山的可能點。如果真正下山的路線是在某個地方,而游客選擇了另外一個地方,則很有可能最終到不了真正的谷底,可能到達半山腰或者山腳下的某一個地方,但是離真正的谷底差距可能不小。這就是優化問題中的由於初始化參數不佳導致只能獲取局部最優解的情況,下圖就形象的展示了這樣的情況。

在優化算法中初始值的影響
梯度遞減法是一種短視的方法,好比游客在下山的時候遇到非常濃的大霧,只能看見腳下一小塊的地方,游客就把一個傾角計放在地上,看哪個方向最陡,然后朝着最陡的方向往下滑一段距離,下滑的距離通常根據游客對當前地形的審視度勢來決定,停下來,再審視周邊哪個方向現在是最陡的。繼續重復上面的傾角計算並往下滑的動作,這跟優化中常用的最陡下降法很類似,可以看做最陡下降法的一個特例。在最陡下降法中,參數的更新使用如下公式:
![]()

但是在使用梯度遞減法求解神經網絡模型時,通常使用的是隨機梯度遞減法(Stochastic Gradient Descent),其公式如下:

這個算法有如下幾點變化:
首先,在計算時不是通覽所有的數據后再執行優化計算,而是對於每個觀測值或
者每組觀測值執行梯度遞減的優化計算。原來的那種算法因此被叫作批量(Batch)
或者離線(Offline)算法,而現在這種算法則被稱為遞增(Incremental)或者
在線(Online)算法,因為參數估計值隨着觀測組的更新而更新。
其次,這個步進值通常從一開始就固定為一個較小的值。
最后,通過上述公式可以看出,參數更新部分不僅取決於一階偏微分的大小,還
包含了一個動量項 這個動量項的效果是將過去的累計更新項的一部分加入到當前參
數的更新項中,即把過去每一步的更新做一個指數遞減的加權求和,可以看作對過
往的更新值的記憶,越遠的記憶影響越小,越近的記憶影響越大。這有助於算法的
穩定性。如果步進值極小,而動量項里的控制變量α接近於數值1,那么在線算法就
近似於離線算法。
下圖對梯度遞減算法進行了形象的展示:

雖然現在最常見的算法是基於一階偏微分的梯度遞減法,但是跟其他幾種以前常用的基於二階偏微分的優化算法進行比較還是比較有趣的,有助於讀者更好地理解這些算法。
基於二階偏微分的算法通常統稱為牛頓法,因為使用了比一階偏微分更多的信息,可以看作游客在下山的過程中霧小了點,能直接看到周邊的地勢。假定整座山是一個平滑的凸形狀,游客就可以一路下滑到谷底,不用中途停下來。當然,這個谷底也不能保證是最低的,有可能也是某個半山腰的窪地,因為還是有霧,游客無法徹底看清整個地勢。
對一般的牛頓法的一種改進叫作增穩牛頓法(Stabilized Newton Method)。這種方法相當於游客帶了一個高度計,因此他在滑下去以后可以查看結果,如果發現地勢反而增高了,那么游客退回到原來的地方,重新跳一小步,從而保證每次下滑都能到達更低的點。對這種方法的進一步改進叫作嶺增穩牛頓法(Ridge Stabilized NewtonMethod)。這種方法在上一種方法的基礎上,游客不僅可以退回到原來的地方,而且重新下滑時還可以選擇跳的方向,以保證有更多的機會使得下滑都離谷底更進一步。
對於深度學習模型中的函數,我們看到在每一層的節點都是一個激活函數套着一個組合函數的形式(參見圖3.5),即常見的復合函數形式,那么在參數更新部分就需要用到微積分里面的鏈式法則(Chain Rule)來計算復合函數的導數

如果假設損失函數使用均方差,同時采用logistic的sigmoid激活函數,而組合函數是求和函數,則采用鏈式法則求解參數的更新可以寫作:

將上述公式帶入前面提到的梯度遞減算法的參數更新步驟中,就可以得到新的參數估計。更新偏置項b采用幾乎一樣的公式,只是這個時候x=1,因此上面公式中最后的x就消掉了。
3:后向傳播算法
上一節對梯度遞減算法進行了介紹,如果這個神經網絡只有一層,那么反復運用這個算法到損失函數,依照上面公式更新參數直到收斂就好了。如果神經網絡模型是一個深度模型,在輸入層和輸出層之間包含很多隱含層的話,就需要一個高效率的算法來盡量減少計算量。后向傳播算法(Backpropagation)就是一種為了快速估計深度神經網絡中的權重值而設計的算法。
設定f0,···,fN代表1,···,N層的決策函數,其中0對應於輸入層,而N對應於輸出層。如果已知各層的權重值和偏置項估計值,那么可以采用下面的遞歸算法快速求得在當前參數值下的損失函數大小:

為了更新參數值,即權重值和偏置項的估值,后向傳播算法先正向計算組合函數和其他相關數值,再反向從輸出層N求解損失函數開始,按照梯度遞減算法逐次往輸入層回算參數的更新量。
在深度學習模型所需的計算中會大量使用鏈式法則,這就會使很多計算結果得到重復使用,后向傳播算法將這些中間結果保存下來可以極大地減少計算量,提高模型擬合速度。因為在每一層都使用同樣的函數:f:R→R,在這些層中,有:f(1)=f(w),f(2)=f(f(1)),f(3)=f(f(2)),其中上標代表對應的網絡層,要計算
可以通過鏈式法則得到:

可以看到,只需計算f(w)一次,保存在變量f(1)中,就可以在以后的計算中使用多次,層數越多,效果越明顯。相反,如果不是反向求解參數更新量,而是在正向傳播那一步求解參數更新量,那么每一步中的f(w)都要重新求解,計算量大增。可以說,后向傳播算法是神經網絡模型普及的基礎之一。
實戰手寫數字識別
手寫數字識別是一個比較簡單的任務,數字只可能是0-9中的一個,這是個10分類的問題。
MNIST手寫數字識別項目因為數據量小,識別任務簡單而成為圖像識別入門的第一課,MNIST手寫數字識別項目有如下特點:
(1) 識別難度低,即使把圖片展開為一維數據,且只使用全連接層也能獲得超過98%的識別准確度。
(2)計算量小,不需要GPU加速也可以快速訓練完成。
(3)數據容易得到,教程容易得到。
超參數的確定
我們首先需要確定網絡的層數和每層的節點數。關於第一個問題,實際上並沒有什么理論化的方法,大家都是根據經驗來拍,如果沒有經驗的話就隨便拍一個。然后,你可以多試幾個值,訓練不同層數的神經網絡,看看哪個效果最好就用哪個。嗯,現在你可能明白為什么說深度學習是個手藝活了,有些手藝很讓人無語,而有些手藝還是很有技術含量的。
不過,有些基本道理我們還是明白的,我們知道網絡層數越多越好,也知道層數越多訓練難度越大。對於全連接網絡,隱藏層最好不要超過三層。那么,我們可以先試試僅有一個隱藏層的神經網絡效果怎么樣。畢竟模型小的話,訓練起來也快些(剛開始玩模型的時候,都希望快點看到結果)。
輸入層節點數是確定的。因為MNIST數據集每個訓練數據是28*28的圖片,共784個像素,因此,輸入層節點數應該是784,每個像素對應一個輸入節點。
輸出層節點數也是確定的。因為是10分類,我們可以用10個節點,每個節點對應一個分類。輸出層10個節點中,輸出最大值的那個節點對應的分類,就是模型的預測結果。
隱藏層節點數量是不好確定的,從1到100萬都可以。下面有幾個經驗公式:

因此,我們可以先根據上面的公式設置一個隱藏層節點數。如果有時間,我們可以設置不同的節點數,分別訓練,看看哪個效果最好就用哪個。我們先拍一個,設隱藏層節點數為300吧。
對於3層784*300*10的全連接網絡,總共有300*(784+1)+10*(300+1)=238510個參數!神經網絡之所以強大,是它提供了一種非常簡單的方法去實現大量的參數。目前百億參數、千億樣本的超大規模神經網絡也是有的。因為MNIST只有6萬個訓練樣本,參數太多了很容易過擬合,效果反而不好。
模型的訓練和評估
MNIST數據集包含10000個測試樣本。我們先用60000個訓練樣本訓練我們的網絡,然后再用測試樣本對網絡進行測試,計算識別錯誤率:

我們每訓練10輪,評估一次准確率。當准確率開始下降時(出現了過擬合)終止訓練。
1.1 導入相關包
import torch # torchvision包的主要功能是實現數據的處理,導入和預覽等 import torchvision from torchvision import datasets from torchvision import transforms from torch.autograd import Variable
首先,導入必要的包。對這個手寫數字識別問題的解決只用到了torchvision中的部分功能,所以這里通過 from torchvision import方法導入其中的兩個子包 datasets和transforms,我們將會用到這兩個包。
1.2 獲取手寫數字的訓練集和測試集
我們就要想辦法獲取手寫數字的訓練集和測試集。使用torchvision.datasets可以輕易實現對這些數據集的訓練集和測試集的下
載,只需要使用 torchvision.datasets再加上需要下載的數據集的名稱就可以了,比如在這個問題中我們要用到手寫數字數據集,它的名稱是MNIST,那么實現下載的代碼就是torchvision.datasets.MNIST。其他常用的數據集如COCO、ImageNet、CIFCAR等都可以通過這個方法快速下載和載入。實現數據集下載的代碼如下:
# 首先獲取手寫數字的訓練集和測試集
# root 用於指定數據集在下載之后的存放路徑
# transform 用於指定導入數據集需要對數據進行那種變化操作
# train是指定在數據集下載完成后需要載入那部分數據,
# 如果設置為True 則說明載入的是該數據集的訓練集部分
# 如果設置為FALSE 則說明載入的是該數據集的測試集部分
data_train = datasets.MNIST(root="./data/",
transform = transform,
train = True,
download = True)
data_test = datasets.MNIST(root="./data/",
transform = transform,
train = False)
其中,root用於指定數據集在下載之后的存放路徑,這里存放在根目錄下的data文件夾中;transform用於指定導入數據集時需要對數據進行哪種變換操作,在后面會介紹詳細的變換操作類型,注意,要提前定義這些變換操作;train用於指定在數據集下載完成后需要載入哪部分數據,如果設置為True,則說明載入的是該數據集的訓練集部分;如果設置為False,則說明載入的是該數據集的測試集部分。
1.3 數據預覽和數據裝載
在數據下載完成后並且載入后,我們還需要對數據進行裝載。我們可以將數據的載入理解為對圖片的處理,在處理完成后,我們就需要將這些圖片打包好送給我們的模型進行訓練了,而裝載就是這個打包的過程。在裝載時通過batch_size的值來確認每個包的大小,通過shuffle的值來確認是否在裝載的過程中打亂圖片的順序。裝載的代碼如下:
#數據預覽和數據裝載
# 下面對數據進行裝載,我們可以將數據的載入理解為對圖片的處理,
# 在處理完成后,我們就需要將這些圖片打包好送給我們的模型進行訓練 了 而裝載就是這個打包的過程
# dataset 參數用於指定我們載入的數據集名稱
# batch_size參數設置了每個包中的圖片數據個數
# 在裝載的過程會將數據隨機打亂順序並進打包
data_loader_train = torch.utils.data.DataLoader(dataset =data_train,
batch_size = 64,
shuffle = True)
data_loader_test = torch.utils.data.DataLoader(dataset =data_test,
batch_size = 64,
shuffle = True)
對數據的裝載使用的是是torch.utils.data.DataLoader類,類中的dataset參數用於指定我們載入的數據集名稱,batch_size參數設置了每個包中的圖片數據個數,代碼中的值是64,所以在每個包中會包含64張圖片。將shuffle參數設置為True,在裝載的過程會將數據隨機打亂順序並進行打包。
在裝載完成后,我們可以選取其中一個批次的數據進行預覽,進行數據預覽的代碼如下:
# 在裝載完成后,我們可以選取其中一個批次的數據進行預覽
images,labels=next(iter(data_loader_train))
img = torchvision.utils.make_grid(images)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std +mean
print(labels)
# print([labels[i] for i in range(64)])
# 由於matplotlab中的展示圖片無法顯示,所以現在使用OpenCV中顯示圖片
# plt.imshow(img)
cv2.imshow('win',img)
key_pressed=cv2.waitKey(0)
在以上代碼中使用了iter和next來獲取取一個批次的圖片數據和其對應的圖片標簽,然后使用torchvision.utils中的make_grid類方法將一個批次的圖片構造成網格模式。需要傳遞給torchvision.utils.make_grid的參數就是一個批次的裝載數據,每個批次的裝載數據都是4維的,維度的構成從前往后分別為batch_size、channel、height和weight,分別對應一個批次中的數據個數、每張圖片的色彩通道數、每張圖片的高度和寬度。在通過torchvision.utils.make_grid之后,圖片的維度變成了(channel,height,weight),這個批次的圖片全部被整合到了一起,所以在這個維度中對應的值也和之前不一樣了,但是色彩通道數保持不變。
若我們想使用Matplotlib將數據顯示成正常的圖片形式,則使用的數據首先必須是數組,其次這個數組的維度必須是
(height,weight,channel),即色彩通道數在最后面。所以我們要通過numpy和transpose完成原始數據類型的轉換和數據維度的交換,這樣才能夠使用Matplotlib繪制出正確的圖像。
在完成數據預覽的代碼中,我們先打印輸出了這個批次中的數據的全部標簽,然后才對這個批次中的所有圖片數據進行顯示,代碼如下
tensor([7, 1, 8, 3, 2, 3, 6, 7, 3, 9, 7, 3, 0, 0, 7, 5, 0, 0, 2, 5, 4, 4, 2, 7,
6, 9, 8, 1, 8, 3, 7, 1, 8, 9, 6, 5, 3, 2, 0, 0, 4, 6, 1, 9, 2, 2, 2, 6,
9, 8, 7, 2, 2, 6, 8, 2, 5, 8, 6, 1, 9, 9, 9, 8])
效果如下,可以看到輸出的首先是64張圖片對應的標簽,然后是64張圖片的預覽結果:

1.4 模型搭建和參數優化
在順利完成數據裝載后,我們就可以開始編寫卷積神經網絡模型的搭建和參數優化的代碼了,因為我們想要搭建一個包含了卷積層,激活函數,池化層,全連接層的卷積神經網絡來解決這個問題,所以模型在結構上會和之前簡單的神經網絡有所區別,當然,各個部分的功能實現依然是通過torch.nn中的類來完成的,比如卷積層使用torch.nn.Conv2d類方法來搭建,激活層使用torch.nn.ReLU類來搭建;池化層使用torch.nn.MaxPool2d類方法來搭建;全連接層使用torch.nn.Linear類方法來搭建。
卷積神經網絡CNN的結構一般包含這幾層:
輸入層:用於數據的輸入
卷積層:使用卷積核進行特征提取和特征映射
激勵層:由於卷積也是一種線性運算,因此需要增加非線性映射
池化層:進行下采樣,對特征圖稀疏處理,減少特征信息的損失
輸出層:用於輸出結果
實現卷積神經網絡模型搭建的代碼如下:
#模型搭建和參數優化
# 在順利完成數據裝載后,我們可以開始編寫卷積神經網絡模型的搭建和參數優化的代碼
#卷積層使用torch.nn.Conv2d類來搭建
# 激活層使用torch.nn.ReLU 類方法來搭建
# 池化層使用torch.nn.MaxPool2d類方法來搭建
# 全連接層使用 torch.nn.Linear 類方法來搭建
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(1,64,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(stride=2,kernel_size=2))
self.dense = torch.nn.Sequential(
torch.nn.Linear(14*14*128,1024),
torch.nn.ReLU(),
torch.nn.Dropout(p = 0.5),
torch.nn.Linear(1024,10)
)
def forward(self, x):
x = self.conv1(x)
x = x.view(-1,14*14*128)
x = self.dense(x)
return x
因為這個問題並不復雜,所以我們選擇搭建一個在結構層次上有所簡化的卷積神經網絡模型,在結構上使用了兩個卷積層:一個最大池化層和兩個全連接層。
最后說一下代碼中前向傳播forward函數中的內容,首先經過self.conv1進行卷積處理,然后進行x.view(-1,14*14*128),對參數實現扁平化,因為之后緊接着的就是全連接層,所以如果不進行扁平化,則全連接層的實際輸出的參數維度和其定義輸入的維度將不匹配,程序將會報錯,最后通過self.dense定義的全連接層進行最后的分類。
在編寫完搭建卷積神經網絡模型的代碼后,我們就可以開始對模型進行訓練和對參數進行優化了。首先,定義在訓練之前使用哪種損失函數和優化函數:
# 在編寫完搭建卷積神經網絡模型的代碼后,我們可以對模型進行訓練和參數進行優化了 # 首先 定義在訓練之前使用哪種損失函數和優化函數 # 下面定義了計算損失值的損失函數使用的是交叉熵 # 優化函數使用的額是Adam自適應優化算法 model = Model() cost = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters())
在以上的代碼中定義了計算損失值的損失函數使用的是交叉熵,也確定了優化函數使用的是Adam自適應優化算法,需要優化的參數在Model中生成的全部參數,因為沒有定義學習效率的值,所以使用默認值,然后通過打印輸出的方式查看搭建好的模型的完整結構們只需要print(model)就OK。輸出的結果如下:
Model(
(conv1): Sequential(
(0): Conv2d(1, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU()
(2): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU()
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(dense): Sequential(
(0): Linear(in_features=25088, out_features=1024, bias=True)
(1): ReLU()
(2): Dropout(p=0.5)
(3): Linear(in_features=1024, out_features=10, bias=True)
)
)
最后,卷積神經網絡模型進行模型訓練和參數優化的代碼如下:
# 卷積神經網絡模型進行模型訓練和參數優化的代碼
n_epochs = 5
for epoch in range(n_epochs):
running_loss = 0.0
running_corrrect = 0
print("Epoch {}/{}".format(epoch, n_epochs))
print("-"*10)
for data in data_loader_train:
X_train , y_train = data
X_train,y_train = Variable(X_train),Variable(y_train)
outputs = model(X_train)
_,pred = torch.max(outputs.data,1)
optimizer.zero_grad()
loss = cost(outputs,y_train)
loss.backward()
optimizer.step()
running_loss += loss.data[0]
running_corrrect += torch.sum(pred == y_train.data)
testing_correct = 0
for data in data_loader_train:
X_test,y_test = data
X_test,y_test = Variable(X_test),Variable(y_test)
outputs = model(X_test)
_,pred = torch.max(X_test)
testing_correct += torch.sum(pred == y_test.data)
print("Loss is {:.4f}, Train Accuracy is:{:.4f}%,Test Accuracy is:{:.4f}"
.format(running_loss/len(data_train),100*running_corrrect/len(data_train),
100*testing_correct/len(data_test)))
總的訓練次數為5次,訓練中的大部分代碼和之前的相比較沒有大的改動,增加的內容都在原來的基礎上加入了更多的打印輸出,其目的是更好的顯示模型訓練過程中的細節,同時,在每輪訓練完成后,會使用測試集驗證模型的泛化能力並計算准確率。在模型訓練過程中打印輸出的結果如下:
(未實現原文結果!!!)
Epoch 0/5 ---------- Process finished with exit code -1073741795 (0xC000001D)
(這是原文作者實現的結果)

Epoch 0/5 ---------- ---------- train ok Loss is 0.0022, Train Accuracy is:95.0000%,Test Accuracy is:591.0000 Epoch 1/5 ---------- train ok Loss is 0.0007, Train Accuracy is:98.0000%,Test Accuracy is:594.0000 Epoch 2/5 ---------- train ok Loss is 0.0005, Train Accuracy is:98.0000%,Test Accuracy is:596.0000 Epoch 3/5 ---------- train ok Loss is 0.0004, Train Accuracy is:99.0000%,Test Accuracy is:595.0000 Epoch 4/5 ---------- train ok Loss is 0.0003, Train Accuracy is:99.0000%,Test Accuracy is:596.0000 運行時間為:-9805.16249585
可以看到,結果表現的非常不錯,訓練集達到的最高准確率為99.73%,而測試集達到的最高准確率為98.96%,如果我們使用功能更強大的卷積神經網絡模型,則會取得比現在更好的結果。為了驗證我們訓練的模型是不是真的已如結果顯示的一樣准確,則最好的方法就是隨機選取一部分測試集中的圖片,用訓練好的模型進行預測,看看和真實值有多大的偏差,並對結果進行可視化。
本人對代碼進行修改,實現的結果如下(windows7實現):
1 Epoch [1/5], Iter [100/600] Loss: 0.1948 Epoch [1/5], Iter [200/600] Loss: 0.1884 Epoch [1/5], Iter [300/600] Loss: 0.1267 Epoch [1/5], Iter [400/600] Loss: 0.2222 Epoch [1/5], Iter [500/600] Loss: 0.0289 Epoch [1/5], Iter [600/600] Loss: 0.0657 Epoch [2/5], Iter [100/600] Loss: 0.0138 Epoch [2/5], Iter [200/600] Loss: 0.1130 Epoch [2/5], Iter [300/600] Loss: 0.0487 Epoch [2/5], Iter [400/600] Loss: 0.0206 Epoch [2/5], Iter [500/600] Loss: 0.0284 Epoch [2/5], Iter [600/600] Loss: 0.0299 Epoch [3/5], Iter [100/600] Loss: 0.0279 Epoch [3/5], Iter [200/600] Loss: 0.0766 Epoch [3/5], Iter [300/600] Loss: 0.0158 Epoch [3/5], Iter [400/600] Loss: 0.0143 Epoch [3/5], Iter [500/600] Loss: 0.0180 Epoch [3/5], Iter [600/600] Loss: 0.0042 Epoch [4/5], Iter [100/600] Loss: 0.0117 Epoch [4/5], Iter [200/600] Loss: 0.0151 Epoch [4/5], Iter [300/600] Loss: 0.0034 Epoch [4/5], Iter [400/600] Loss: 0.0144 Epoch [4/5], Iter [500/600] Loss: 0.0482 Epoch [4/5], Iter [600/600] Loss: 0.0550 Epoch [5/5], Iter [100/600] Loss: 0.0393 Epoch [5/5], Iter [200/600] Loss: 0.0028 Epoch [5/5], Iter [300/600] Loss: 0.0368 Epoch [5/5], Iter [400/600] Loss: 0.0250 Epoch [5/5], Iter [500/600] Loss: 0.1075 Epoch [5/5], Iter [600/600] Loss: 0.0663 Process finished with exit code 0
1.5 完整代碼及解析
(原文代碼,本人未實現)
#_*_coding:utf-8_*_
import matplotlib.pyplot as plt
import numpy as np
import cv2
import time
import torch
# torchvision包的主要功能是實現數據的處理,導入和預覽等
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.autograd import Variable
start_time = time.time()
# 對數據進行載入及有相應變換,將Compose看成一種容器,他能對多種數據變換進行組合
# 傳入的參數是一個列表,列表中的元素就是對載入的數據進行的各種變換操作
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])])
# 首先獲取手寫數字的訓練集和測試集
# root 用於指定數據集在下載之后的存放路徑
# transform 用於指定導入數據集需要對數據進行那種變化操作
# train是指定在數據集下載完成后需要載入那部分數據,
# 如果設置為True 則說明載入的是該數據集的訓練集部分
# 如果設置為FALSE 則說明載入的是該數據集的測試集部分
data_train = datasets.MNIST(root="./data/",
transform = transform,
train = True,
download = True)
data_test = datasets.MNIST(root="./data/",
transform = transform,
train = False)
#數據預覽和數據裝載
# 下面對數據進行裝載,我們可以將數據的載入理解為對圖片的處理,
# 在處理完成后,我們就需要將這些圖片打包好送給我們的模型進行訓練 了 而裝載就是這個打包的過程
# dataset 參數用於指定我們載入的數據集名稱
# batch_size參數設置了每個包中的圖片數據個數
# 在裝載的過程會將數據隨機打亂順序並進打包
data_loader_train = torch.utils.data.DataLoader(dataset =data_train,
batch_size = 64,
shuffle = True)
data_loader_test = torch.utils.data.DataLoader(dataset =data_test,
batch_size = 64,
shuffle = True)
# 在裝載完成后,我們可以選取其中一個批次的數據進行預覽
images,labels = next(iter(data_loader_train))
img = torchvision.utils.make_grid(images)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std +mean
# print(labels)
print([labels[i] for i in range(64)])
# 由於matplotlab中的展示圖片無法顯示,所以現在使用OpenCV中顯示圖片
# plt.imshow(img)
# cv2.imshow('win',img)
# key_pressed=cv2.waitKey(0)
#模型搭建和參數優化
# 在順利完成數據裝載后,我們可以開始編寫卷積神經網絡模型的搭建和參數優化的代碼
#卷積層使用torch.nn.Conv2d類來搭建
# 激活層使用torch.nn.ReLU 類方法來搭建
# 池化層使用torch.nn.MaxPool2d類方法來搭建
# 全連接層使用 torch.nn.Linear 類方法來搭建
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(1,64,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(stride=2,kernel_size=2))
self.dense = torch.nn.Sequential(
torch.nn.Linear(14*14*128,1024),
torch.nn.ReLU(),
torch.nn.Dropout(p = 0.5),
torch.nn.Linear(1024,10)
)
# 我們通過繼承torch.nn.Modeule來構造網絡,因為手寫數字
# 識別比較簡單,我們只是用了兩個卷積層,一個最大池化層,兩個全連接層。
# 在向前傳播過程中進行x.view(-1, 14 * 14 * 128)
# 對參數實現扁平化。最后通過自己self.dense定義的全連接層進行最后的分類
def forward(self, x):
x = self.conv1(x)
x = x.view(-1,14*14*128)
x = self.dense(x)
return x
# 在編寫完搭建卷積神經網絡模型的代碼后,我們可以對模型進行訓練和參數進行優化了
# 首先 定義在訓練之前使用哪種損失函數和優化函數
# 下面定義了計算損失值的損失函數使用的是交叉熵
# 優化函數使用的額是Adam自適應優化算法
model = Model()
# 將所有的模型參數移動到GPU上
if torch.cuda.is_available():
model.cuda()
cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
# print(model)
# 卷積神經網絡模型進行模型訓練和參數優化的代碼
n_epochs = 5
for epoch in range(n_epochs):
running_loss = 0.0
running_correct = 0
print("Epoch {}/{}".format(epoch, n_epochs))
print("-"*10)
for data in data_loader_train:
X_train , y_train = data
# 有GPU加下面這行,沒有不用加
# X_train, y_train = X_train.cuda(), y_train.cuda()
X_train , y_train = Variable(X_train),Variable(y_train)
# print(y_train)
outputs = model(X_train)
# print(outputs)
_,pred = torch.max(outputs.data,1)
optimizer.zero_grad()
loss = cost(outputs,y_train)
loss.backward()
optimizer.step()
# running_loss += loss.data[0]
running_loss += loss.item()
running_correct += torch.sum(pred == y_train.data)
# print("ok")
# print("**************%s"%running_corrrect)
print("train ok ")
testing_correct = 0
for data in data_loader_test:
X_test,y_test = data
# 有GPU加下面這行,沒有不用加
# X_test, y_test = X_test.cuda(), y_test.cuda()
X_test,y_test = Variable(X_test),Variable(y_test)
outputs = model(X_test)
_, pred = torch.max(outputs,1)
testing_correct += torch.sum(pred == y_test.data)
# print(testing_correct)
print( "Loss is :{:.4f},Train Accuracy is:{:.4f}%,Test Accuracy is:{:.4f}".format(
running_loss / len(data_train),100 * running_correct / len(data_train),
100 * testing_correct / len(data_test)))
stop_time = time.time()
print("time is %s" %(stop_time-start_time))
(本人對原文代碼進行修改,然后實現的是這個代碼)
import torch
import torch.nn as nn
import torchvision.datasets as normal_datasets
import torchvision.transforms as transforms
from torch.autograd import Variable
num_epochs = 5
batch_size = 100
learning_rate = 0.001
# 將數據處理成Variable, 如果有GPU, 可以轉成cuda形式
def get_variable(x):
x = Variable(x)
return x.cuda() if torch.cuda.is_available() else x
# 從torchvision.datasets中加載一些常用數據集
train_dataset = normal_datasets.MNIST(
root='./mnist/', # 數據集保存路徑
train=True, # 是否作為訓練集
transform=transforms.ToTensor(), # 數據如何處理, 可以自己自定義
download=True) # 路徑下沒有的話, 可以下載
# 見數據加載器和batch
test_dataset = normal_datasets.MNIST(root='./mnist/',
train=False,
transform=transforms.ToTensor())
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False)
# 兩層卷積
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 使用序列工具快速構建
self.conv1 = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, padding=2),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.MaxPool2d(2))
self.conv2 = nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, padding=2),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2))
self.fc = nn.Linear(7 * 7 * 32, 10)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
out = out.view(out.size(0), -1) # reshape
out = self.fc(out)
return out
cnn = CNN()
if torch.cuda.is_available():
cnn = cnn.cuda()
# 選擇損失函數和優化方法
loss_func = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(cnn.parameters(), lr=learning_rate)
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
images = get_variable(images)
labels = get_variable(labels)
outputs = cnn(images)
loss = loss_func(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i + 1) % 100 == 0:
print('Epoch [%d/%d], Iter [%d/%d] Loss: %.4f'
% (epoch + 1, num_epochs, i + 1, len(train_dataset) // batch_size, loss.item()))
# Save the Trained Model
torch.save(cnn.state_dict(), 'cnn.pkl')
1.6 驗證模型
為了驗證我們訓練的模型是不是真的已如結果顯示的一樣准確,則最好的方法就是隨機選取一部分測試集中的圖片,用訓練好的模型進行預測,看看和真實值有多大的偏差,並對結果進行可視化,測試過程的代碼如下:
import matplotlib.pyplot as plt
import cv2
import torch
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.autograd import Variable
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])])
data_test = datasets.MNIST(root="./data/",
transform = transform,
train = False)
data_loader_test = torch.utils.data.DataLoader(dataset =data_test,
batch_size = 4,
shuffle = True)
#模型搭建和參數優化
# 在順利完成數據裝載后,我們可以開始編寫卷積神經網絡模型的搭建和參數優化的代碼
#卷積層使用torch.nn.Conv2d類來搭建
# 激活層使用torch.nn.ReLU 類方法來搭建
# 池化層使用torch.nn.MaxPool2d類方法來搭建
# 全連接層使用 torch.nn.Linear 類方法來搭建
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.conv1 = torch.nn.Sequential(
torch.nn.Conv2d(1,64,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(stride=2,kernel_size=2))
self.dense = torch.nn.Sequential(
torch.nn.Linear(14*14*128,1024),
torch.nn.ReLU(),
torch.nn.Dropout(p = 0.5),
torch.nn.Linear(1024,10)
)
# 我們通過繼承torch.nn.Modeule來構造網絡,因為手寫數字
# 識別比較簡單,我們只是用了兩個卷積層,一個最大池化層,兩個全連接層。
# 在向前傳播過程中進行x.view(-1, 14 * 14 * 128)
# 對參數實現扁平化。最后通過自己self.dense定義的全連接層進行最后的分類
def forward(self, x):
x = self.conv1(x)
x = x.view(-1,14*14*128)
x = self.dense(x)
return x
# 在編寫完搭建卷積神經網絡模型的代碼后,我們可以對模型進行訓練和參數進行優化了
# 首先 定義在訓練之前使用哪種損失函數和優化函數
# 下面定義了計算損失值的損失函數使用的是交叉熵
# 優化函數使用的額是Adam自適應優化算法
model = Model()
# 將所有的模型參數移動到GPU上
if torch.cuda.is_available():
model.cuda()
cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
# print(model)
# 卷積神經網絡模型進行模型訓練和參數優化的代碼
n_epochs = 5
X_test,y_test = next(iter(data_loader_test))
inputs = Variable(X_test)
pred = model(inputs)
_,pred = torch.max(pred,1)
print("Predict Label is:",(i for i in pred))
print("Real Label is :",[i for i in y_test])
img = torchvision.utils.make_grid(X_test)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std +mean
cv2.imshow('win',img)
key_pressed=cv2.waitKey(0)
用於測試的數據標簽結果輸出的結果如下:
Predict Label is: <generator object <genexpr> at 0x0000000015419C00> Real Label is : [tensor(7), tensor(3), tensor(7), tensor(3)]
在輸出結果中,第1個結果是我們訓練好的模型的預測值,第2個結果是這4個測試數據的真實值。對測試數據進行可視化,如圖所示:

可以看到,在圖中可視化的這部分測試集圖片,模型的預測結果和真實的結果是完全一致的。當然,如果想選取更多的測試集進行可視化,則只需將batch_size的值設置得更大。
