打造Fashion-MNIST CNN,PyTorch風格
- 2019 年 10 月 28 日
- 筆記
編輯 | 代碼醫生團隊
關於技術框架,一個有趣的事情是,從一開始,似乎總是被各種選擇。但是隨着時間的推移,比賽將演變為只剩下兩個強有力的競爭者。例如「 PC vs Mac」,「 iOS vs Android」,「 React.js vs Vue.js」等。現在,在機器學習中擁有「 PyTorch vs TensorFlow」。
由Google支持的TensorFlow無疑是這裡的領先者。它於2015年作為開放源代碼的機器學習框架發佈,迅速獲得了廣泛的關注和認可,尤其是在生產準備和部署至關重要的行業中。PyTorch於2017年在Facebook上推出的很晚,但由於其動態的計算圖和“ pythonic ''風格而很快贏得了從業者和研究人員的廣泛喜愛。

圖片來自漸變
The Gradient的最新研究表明,PyTorch在研究人員方面做得很好,而TensorFlow在行業界佔主導地位:
在2019年,機器學習框架之戰還有兩個主要競爭者:PyTorch和TensorFlow。我的分析表明,研究人員正在放棄TensorFlow並大量湧向PyTorch。同時,在行業中,Tensorflow當前是首選平台,但長期以來可能並非如此。— 漸變
PyTorch 1.3的最新版本引入了PyTorch Mobile,量化和其他功能,它們都在正確的方向上縮小了差距。如果對神經網絡基礎有所了解,但想嘗試使用PyTorch作為其他樣式,請繼續閱讀。將嘗試說明如何使用PyTorch從頭開始為Fashion-MNIST數據集構建卷積神經網絡分類器。如果沒有強大的本地環境,則可以在Google Colab和Tensor Board上使用此處的代碼。事不宜遲開始吧。可以在下面找到Google Colab Notebook和GitHub鏈接:
Co Google Colab筆記本
https://colab.research.google.com/drive/1YWzAjpAnLI23irBQtLvDTYT1A94uCloM
GitHub上
https://github.com/wayofnumbers/SideProjects/blob/master/PyTorch_Tutorial_Basic_v1.ipynb
Import
首先,導入必要的模塊。
# import standard PyTorch modules import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.tensorboard import SummaryWriter # TensorBoard support # import torchvision module to handle image manipulation import torchvision import torchvision.transforms as transforms # calculate train time, writing train data to files etc. import time import pandas as pd import json from IPython.display import clear_output torch.set_printoptions(linewidth=120) torch.set_grad_enabled(True) # On by default, leave it here for clarity
PyTorch模塊非常簡單。
Torch
torch是包含Tensor計算所需的所有內容的主要模塊。可以單獨使用Tensor計算來構建功能齊全的神經網絡,但這不是本文的目的。將利用更強大和便捷torch.nn,torch.optim而torchvision類快速構建CNN。
torch.nn和torch.nn.functional

Alphacolor在Unsplash上拍攝的照片
該torch.nn模塊提供了許多類和函數來構建神經網絡。可以將其視為神經網絡的基本構建塊:模型,各種層,激活函數,參數類等。它可以像將一些LEGO集放在一起一樣構建模型。
Torch優化
torch.optim 提供了SGD,ADAM等所有優化程序,因此無需從頭開始編寫。
Torch視覺
torchvision包含許多用於計算機視覺的流行數據集,模型架構和常見圖像轉換。我們從中獲取Fashion MNIST數據集,並使用其變換。
SummaryWriter(張量板)
SummaryWriter使PyTorch可以為Tensor Board生成報告。將使用Tensor Board查看訓練數據,比較結果並獲得直覺。Tensor Board曾經是TensorFlow相對於PyTorch的最大優勢,但是現在從v1.2開始,PyTorch正式支持它。
也引進了一些其他實用模塊,如time,json,pandas,等。
數據集
torchvision已經具有Fashion MNIST數據集。如果不熟悉Fashion MNIST數據集:
Fashion-MNIST是Zalando文章圖像的數據集-包含60,000個示例的訓練集和10,000個示例的測試集。每個示例都是一個28×28灰度圖像,與來自10個類別的標籤相關聯。我們打算Fashion-MNIST直接替代原始MNIST數據集,以對機器學習算法進行基準測試。它具有相同的圖像大小以及訓練和測試分割的結構。— 來自Github
https://github.com/zalandoresearch/fashion-mnist

Fashion-MNIST數據集— 來自GitHub
# Use standard FashionMNIST dataset train_set = torchvision.datasets.FashionMNIST( root = './data/FashionMNIST', train = True, download = True, transform = transforms.Compose([ transforms.ToTensor() ]) )
這不需要太多解釋。指定了根目錄來存儲數據集,獲取訓練數據,允許將其下載(如果本地計算機上不存在的話),然後應用transforms.ToTensor將圖像轉換為Tensor,以便可以在網絡中直接使用它。數據集存儲在dataset名為train_set.
網絡
在PyTorch中建立實際的神經網絡既有趣又容易。假設對卷積神經網絡的工作原理有一些基本概念。如果沒有,可以參考Deeplizard的以下視頻:
Fashion MNIST的尺寸僅為28×28像素,因此實際上不需要非常複雜的網絡。可以像這樣構建一

CNN拓撲
有兩個卷積層,每個都有5×5內核。在每個卷積層之後,都有一個最大步距為2的最大合併層。這能夠從圖像中提取必要的特徵。然後,將張量展平並放入密集層中,通過多層感知器(MLP)來完成10類分類的任務。
現在已經了解了網絡的結構,看看如何使用PyTorch來構建它:
# Build the neural network, expand on top of nn.Module class Network(nn.Module): def __init__(self): super().__init__() # define layers self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5) self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5) self.fc1 = nn.Linear(in_features=12*4*4, out_features=120) self.fc2 = nn.Linear(in_features=120, out_features=60) self.out = nn.Linear(in_features=60, out_features=10) # define forward function def forward(self, t): # conv 1 t = self.conv1(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # conv 2 t = self.conv2(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # fc1 t = t.reshape(-1, 12*4*4) t = self.fc1(t) t = F.relu(t) # fc2 t = self.fc2(t) t = F.relu(t) # output t = self.out(t) # don't need softmax here since we'll use cross-entropy as activation. return t
首先,PyTorch中的所有網絡類都在基類上擴展nn.Module。它包含了所有基礎知識:權重,偏差,正向方法,以及一些實用程序屬性和方法,例如.parameters()以及.zero_grad()將使用的方法。
網絡結構在__init__dunder函數中定義。
def __init__(self): super().__init__() # define layers self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5) self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5) self.fc1 = nn.Linear(in_features=12*4*4, out_features=120) self.fc2 = nn.Linear(in_features=120, out_features=60) self.out = nn.Linear(in_features=60, out_features=10)
nn.Conv2d並且nn.Linear是內限定兩個標準PyTorch層torch.nn模塊。這些是不言而喻的。需要注意的一件事是,僅在此處定義了實際的圖層。激活和最大池操作包含在下面說明的正向功能中。
# define forward function def forward(self, t): # conv 1 t = self.conv1(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # conv 2 t = self.conv2(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) # fc1 t = t.reshape(-1, 12*4*4) t = self.fc1(t) t = F.relu(t) # fc2 t = self.fc2(t) t = F.relu(t) # output t = self.out(t) # don't need softmax here since we'll use cross-entropy as activation. return t
一旦定義了層,就可以使用層本身來計算每個層的前向結果,再加上激活函數(ReLu)和最大池操作,可以輕鬆地編寫上述網絡的前向函數。請注意,在fc1(完全連接層1)上,使用了PyTorch的張量操作t.reshape來拉平張量,以便隨後可以將其傳遞到密集層。另外,沒有在輸出層添加softmax激活函數,因為PyTorch的CrossEntropy函數將解決這個問題。
超參數
可以精選一組超參數和做一些實驗和他們在一起。在這個例子中,想通過引入一些結構來做更多的事情。將構建一個系統來生成不同的超參數組合,並使用它們進行訓練「運行」。每個「運行」使用一組超參數組合。將每次運行的訓練數據/結果導出到Tensor Board,以便可以直接比較並查看哪個超參數集表現最佳。
將所有超參數存儲在OrderedDict中:
# put all hyper params into a OrderedDict, easily expandable params = OrderedDict( lr = [.01, .001], batch_size = [100, 1000], shuffle = [True, False] ) epochs = 3
lr:學習率。想為模型嘗試0.01和0.001。
batch_size:批次大小以加快訓練過程。將使用100和1000。
shuffle:隨機切換,是否在訓練之前對批次進行隨機混合。
一旦參數關閉。使用兩個幫助程序類:RunBuilder和RunManager管理超參數和訓練過程。
運行構建器
該類的主要目的RunBuilder是提供一個靜態方法get_runs。它以OrderedDict(所有超參數都存儲在其中)為參數,並生成一個命名元組Run,每個的元素run表示超參數的一種可能組合。此命名的元組稍後由訓練循環使用。該代碼很容易理解。
# import modules to build RunBuilder and RunManager helper classes from collections import OrderedDict from collections import namedtuple from itertools import product # Read in the hyper-parameters and return a Run namedtuple containing all the # combinations of hyper-parameters class RunBuilder(): @staticmethod def get_runs(params): Run = namedtuple('Run', params.keys()) runs = [] for v in product(*params.values()): runs.append(Run(*v)) return runs
運行管理器
本RunManager 課程有四個主要目的。
- 計算並記錄每個時期和運行的持續時間。
- 計算每個時期和跑步的訓練損失和準確性。
- 記錄每個時期的訓練數據(例如,損失,準確性,權重,梯度,計算圖等)並運行,然後將其導出到Tensor Board中進行進一步分析。
- 保存所有訓練結果csv,json以備將來參考或提取API。
如您所見,它可以幫助處理物流,這對於成功訓練模型也很重要。看一下代碼。它有點長,所以請忍受:
# Helper class, help track loss, accuracy, epoch time, run time, # hyper-parameters etc. Also record to TensorBoard and write into csv, json class RunManager(): def __init__(self): # tracking every epoch count, loss, accuracy, time self.epoch_count = 0 self.epoch_loss = 0 self.epoch_num_correct = 0 self.epoch_start_time = None # tracking every run count, run data, hyper-params used, time self.run_params = None self.run_count = 0 self.run_data = [] self.run_start_time = None # record model, loader and TensorBoard self.network = None self.loader = None self.tb = None # record the count, hyper-param, model, loader of each run # record sample images and network graph to TensorBoard def begin_run(self, run, network, loader): self.run_start_time = time.time() self.run_params = run self.run_count += 1 self.network = network self.loader = loader self.tb = SummaryWriter(comment=f'-{run}') images, labels = next(iter(self.loader)) grid = torchvision.utils.make_grid(images) self.tb.add_image('images', grid) self.tb.add_graph(self.network, images) # when run ends, close TensorBoard, zero epoch count def end_run(self): self.tb.close() self.epoch_count = 0 # zero epoch count, loss, accuracy, def begin_epoch(self): self.epoch_start_time = time.time() self.epoch_count += 1 self.epoch_loss = 0 self.epoch_num_correct = 0 # def end_epoch(self): # calculate epoch duration and run duration(accumulate) epoch_duration = time.time() - self.epoch_start_time run_duration = time.time() - self.run_start_time # record epoch loss and accuracy loss = self.epoch_loss / len(self.loader.dataset) accuracy = self.epoch_num_correct / len(self.loader.dataset) # Record epoch loss and accuracy to TensorBoard self.tb.add_scalar('Loss', loss, self.epoch_count) self.tb.add_scalar('Accuracy', accuracy, self.epoch_count) # Record params to TensorBoard for name, param in self.network.named_parameters(): self.tb.add_histogram(name, param, self.epoch_count) self.tb.add_histogram(f'{name}.grad', param.grad, self.epoch_count) # Write into 'results' (OrderedDict) for all run related data results = OrderedDict() results["run"] = self.run_count results["epoch"] = self.epoch_count results["loss"] = loss results["accuracy"] = accuracy results["epoch duration"] = epoch_duration results["run duration"] = run_duration # Record hyper-params into 'results' for k,v in self.run_params._asdict().items(): results[k] = v self.run_data.append(results) df = pd.DataFrame.from_dict(self.run_data, orient = 'columns') # display epoch information and show progress clear_output(wait=True) display(df) # accumulate loss of batch into entire epoch loss def track_loss(self, loss): # multiply batch size so variety of batch sizes can be compared self.epoch_loss += loss.item() * self.loader.batch_size # accumulate number of corrects of batch into entire epoch num_correct def track_num_correct(self, preds, labels): self.epoch_num_correct += self._get_num_correct(preds, labels) @torch.no_grad() def _get_num_correct(self, preds, labels): return preds.argmax(dim=1).eq(labels).sum().item() # save end results of all runs into csv, json for further analysis def save(self, fileName): pd.DataFrame.from_dict( self.run_data, orient = 'columns', ).to_csv(f'{fileName}.csv') with open(f'{fileName}.json', 'w', encoding='utf-8') as f: json.dump(self.run_data, f, ensure_ascii=False, indent=4)
__init__:初始化必要的屬性,例如計數,損失,正確預測的數量,開始時間等。
begin_run:記錄運行的開始時間,以便在運行結束時可以計算出運行的持續時間。創建一個SummaryWriter對象以存儲我們想要在運行期間導出到Tensor Board中的所有內容。將網絡圖和樣本圖像寫入SummaryWriter對象。
end_run:運行完成後,關閉SummaryWriter對象,並將紀元計數重置為0(為下一次運行做好準備)。
begin_epoch:記錄紀元開始時間,以便紀元結束時可以計算紀元持續時間。重置epoch_loss並epoch_num_correct。
end_epoch:大多數情況下都會發生此功能。當一個紀元結束時,將計算該紀元持續時間和運行持續時間(直到該紀元,除非最終的運行紀元,否則不是最終的運行持續時間)。將計算該時期的總損失和準確性,然後將記錄的損失,準確性,權重/偏差,梯度導出到Tensor Board中。為了便於在Jupyter Notebook中進行跟蹤,還創建了一個OrderedDict對象results,並將所有運行數據(損耗,準確性,運行計數,時期計數,運行持續時間,時期持續時間,所有超參數)放入其中。然後,將使用Pandas讀取它並以整潔的表格格式顯示它。
track_loss,track_num_correct,_get_num_correct:這些是實用功能以累積損耗,每批所以曆元損失和準確性可以在以後計算的正確預測的數目。
save:保存所有運行數據(名單results OrderedDict所有實驗對象)到csv和json作進一步的分析或API訪問的格式。
這RunManager堂課有很多內容。恭喜到此為止!最困難的部分已經在身後。
訓練
準備做一些訓練!在RunBuilder 和RunManager的幫助下,訓練過程變得輕而易舉:
m = RunManager() # get all runs from params using RunBuilder class for run in RunBuilder.get_runs(params): # if params changes, following line of code should reflect the changes too network = Network() loader = torch.utils.data.DataLoader(train_set, batch_size = run.batch_size) optimizer = optim.Adam(network.parameters(), lr=run.lr) m.begin_run(run, network, loader) for epoch in range(epochs): m.begin_epoch() for batch in loader: images = batch[0] labels = batch[1] preds = network(images) loss = F.cross_entropy(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() m.track_loss(loss) m.track_num_correct(preds, labels) m.end_epoch() m.end_run() # when all runs are done, save results to files m.save('results')
首先,用於RunBuilder創建超參數的迭代器,然後循環遍歷每種超參數組合以進行訓練:
for run in RunBuilder.get_runs(params):
然後,network從Network上面定義的類創建對象。network = Network()。該network物體支撐着我們需要訓練的所有重量/偏向。
還需要創建一個DataLoader 對象。這是一個保存訓練/驗證/測試數據集的PyTorch類,它將迭代該數據集,並以與batch_size指定數量相同的批次提供訓練數據。
loader = torch.utils.data.DataLoader(train_set, batch_size = run.batch_size)
之後,將使用torch.optim類創建優化器。該optim課程將網絡參數和學習率作為輸入,將幫助逐步完成訓練過程並更新梯度等。在這裡,將使用Adam作為優化算法。
optimizer = optim.Adam(network.parameters(), lr=run.lr)
現在已經創建了網絡,準備了數據加載器並選擇了優化器。開始訓練吧!
將循環遍歷所有想要訓練的紀元(此處為3),因此將所有內容包裝在「紀元」循環中。還使用班級的begin_run方法RunManager來開始跟蹤跑步訓練數據。
m.begin_run(run, network, loader) for epoch in range(epochs):
對於每個時期,將遍歷每批圖像以進行訓練。
m.begin_epoch() for batch in loader: images = batch[0] labels = batch[1] preds = network(images) loss = F.cross_entropy(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() m.track_loss(loss) m.track_num_correct(preds, labels)
上面的代碼是進行實際訓練的地方。從批處理中讀取圖像和標籤,使用network類進行正向傳播(還記得forward上面的方法嗎?)並獲得預測。通過預測,可以使用cross_entropy函數計算該批次的損失。一旦計算出損失,就用重置梯度(否則PyTorch將積累不想要的梯度).zero_grad(),執行一種反向傳播使用loss.backward()方法來計算權重/偏差的所有梯度。然後,使用上面定義的優化程序來更新權重/偏差。現在,針對當前批次更新了網絡,將計算損失和正確預測的數量,並使用類的track_loss和track_num_correct方法進行累積/跟蹤RunManager。
完成所有操作後,將使用將結果保存到文件中m.save('results')。

張量板

圖片來自Tensorboard.org
Tensor Board是一個TensorFlow可視化工具,現在也PyTorch支持。已經採取了將所有內容導出到'./runs'文件夾的工作,Tensor Board將在其中查找要使用的記錄。現在需要做的只是啟動張量板並檢查。由於在Google Colab上運行此模型,因此將使用一種稱為的服務ngrok來代理和訪問在Colab虛擬機上運行的Tensor Board。ngrok 首先安裝:
!wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip !unzip ngrok-stable-linux-amd64.zip
然後,指定要從中運行Tensor Board的文件夾並啟動Tensor Board Web界面(./runs為默認值):
LOG_DIR = './runs' get_ipython().system_raw( 'tensorboard --logdir {} --host 0.0.0.0 --port 6006 &' .format(LOG_DIR) )
啟動ngrok代理:
get_ipython().system_raw('./ngrok http 6006 &')
生成一個URL,以便可以從Jupyter Notebook中訪問Tensor Board:
! curl -s http://localhost:4040/api/tunnels | python3 -c "import sys, json; print(json.load(sys.stdin)['tunnels'][0]['public_url'])"
如下所示,TensorBoard是一個非常方便的可視化工具,可深入了解訓練,並可以極大地幫助調整超參數。可以輕鬆地找出哪個超參數comp表現最佳,然後使用它來進行真正的訓練。



結論
如您所見,PyTorch作為一種機器學習框架是靈活,強大和富於表現力的。只需編寫Python代碼。由於本文的主要重點是展示如何使用PyTorch構建卷積神經網絡並以結構化方式對其進行訓練,因此我並未完成整個訓練時期,並且準確性也不是最佳的。可以自己嘗試一下,看看模型的性能如何。