打造Fashion-MNIST CNN,PyTorch風格

  • 2019 年 10 月 28 日
  • 筆記

作者 | Lee 來源 | Medium

編輯 | 代碼醫生團隊

關於技術框架,一個有趣的事情是,從一開始,似乎總是被各種選擇。但是隨着時間的推移,比賽將演變為只剩下兩個強有力的競爭者。例如「 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 課程有四個主要目的。

  1. 計算並記錄每個時期和運行的持續時間。
  2. 計算每個時期和跑步的訓練損失和準確性。
  3. 記錄每個時期的訓練數據(例如,損失,準確性,權重,梯度,計算圖等)並運行,然後將其導出到Tensor Board中進行進一步分析。
  4. 保存所有訓練結果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構建卷積神經網絡並以結構化方式對其進行訓練,因此我並未完成整個訓練時期,並且準確性也不是最佳的。可以自己嘗試一下,看看模型的性能如何。