# 利用PyTorch-Lightning最佳化訓練流程 [TOC] <a href="https://colab.research.google.com/github/davidho27941/ML_tutorial_notebook/blob/main/PyTorch_Lightning.ipynb" target="_parent\"> <img src="https://colab.research.google.com/assets/colab-badge.svg\" alt="Open In Colab\"> </a> ## 簡介 在先前的章節中,我們瞭解了如何利用`PyTorch`建立資料流、模型以及進行訓練的流程。我們可以體會在在`PyTorch`中建立訓練所需要的所有元素是相對複雜的(與`TensorFlow`相比)。然而,我們可以透過`PyTorch-Lightning`這個模組所提供的各種功能來補足`PyTorch`所缺少的部份,並得到超越`TensorFlow`的彈性以及可塑性。在這個章節,我們將會初步介紹`PyTorch-Lightning`所能帶來的便利性,以及與過往使用原生`PyTorch`之間的差異。 ## 行前複習 在開始了解如何利用`PyTorch-Lightning`函式庫來最佳化我們的訓練流程之前,我們先來複習如何在利用原生`PyTorch`函式庫來建構訓練的流程。 > 也可瀏覽先前的[章節](https://hackmd.io/@davidho9713/HyfHBzcxY/%2FbGSXeP5iRpa8m0XwyircIw)來複習。 ```python= # Import libraries import torchvision import torch import numpy as np import pandas as pd # Transform # Step 1: Convert to Tensor # Step 2: Normalize with mean = 0.5 and std= 0.5 transform = torchvision.transforms.Compose( [torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.5,), (0.5,)),] ) # Obtain MNIST training dataset. # Transform is a custom functon define by user, which allow user to apply the transformation when loading dataset. mnist_train = torchvision.datasets.MNIST(root='MNIST', download=True, train=True, transform=transform) # Obtain MNIST test dataset mnist_test = torchvision.datasets.MNIST(root='MNIST', download=True, train=False, transform=transform) # Build train data loader trainLoader = torch.utils.data.DataLoader(mnist_train, batch_size=64, shuffle=True, pin_memory=True, num_workers=4) # Build test data loader testLoader = torch.utils.data.DataLoader(mnist_test, batch_size=64, shuffle=False, pin_memory=True, num_workers=4) device = 'cuda:0' if torch.cuda.is_available() else 'cpu' model = torch.nn.Sequential( torch.nn.Linear(in_features=784, out_features=128), torch.nn.ReLU(), torch.nn.Linear(in_features=128, out_features=64), torch.nn.ReLU(), torch.nn.Linear(in_features=64, out_features=10), torch.nn.LogSoftmax(dim=1) ).to(device) EPOCHS = 10 LR = 1e-2 OPTIMIZER = 'adam' criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=LR) for epoch in range(EPOCHS): # 定義變數用以儲存單次訓練週期中的loss以及acc值 running_loss = list() running_acc = 0.0 for times, data in enumerate(trainLoader): # 宣告模型訓練狀態 model.train() # 取得資料,並將其傳遞至相應裝置 inputs, labels = data[0].to(device), data[1].to(device) # 將影像維度改為第一維度長度為inputs.shape[0],第二維度為所剩維度展平的長度 inputs = inputs.view(inputs.shape[0], -1) # 將既存梯度歸零 optimizer.zero_grad() # 將資料導入模型,並取得模型輸出 outputs = model(inputs) # 將模型輸出轉為整數 predicted = torch.max(outputs.data, 1)[1] # 利用損失函數計算loss loss = criterion(outputs, labels) # 進行反向傳播 loss.backward() # 更新參數 optimizer.step() # 紀錄週期內的損失值 running_loss.append(loss.item()) # 計算週期內正確預測的數量 running_acc += (labels==predicted).sum().item() # 計算週期為單位的loss以及acc _epoch_loss = torch.tensor(running_loss).mean() _epoch_acc = running_acc/(len(trainLoader)*64) # 輸出資訊 print(f"Epoch : {epoch+1}, Epoch loss: {_epoch_loss:.4f}, Epoch Acc: {_epoch_acc:.2f}") print('Training Finished.') ``` 想必對於大部分的人來說,這種需要對每一個步驟都進行設定的作法,相較下是比較令人感到麻煩的。在`TensorFlow`中都不需要親力親為的部份,在`PyTorch`中卻需要一項一項的處理,會花費掉開發者許多寶貴的時間。這時候就是`PyTorch-Lightning`函式庫大顯身手的好時機了。 ## 關於利用`PyTorch-Lightning`能帶給我們的好處 1. 不在需要處理變數與硬體之間的關係(可以把`.to()`丟掉了)。 2. 因為大多數的工程部份被抽象化了,所以程式碼變得更加容易閱讀。 3. 更容易重現結果。 4. 因為少了棘手的工程部份,所以減少了失誤率。 5. 因為保有`PyTorch`的原生功能,所以能在最大化彈性的情景下刪除大部分樣版程式。 6. 可與大部分機器學習工具整合。 7. 具有比`PyTorch`更好的運行效率。 > 節錄自`PyTorch-Lightning`的[GitHub頁面](https://github.com/PyTorchLightning/pytorch-lightning) ## 開始使用`PyTorch-Lightning` 在瞭解了`PyTorch-Lightning`能帶給我們怎樣的好處之後,我們可以開始來動手試試看將劑有訓練流程改寫為基於`PyTorch-Lightning`的形式。為此,首先我們需要安裝`PyTorch-Lightning`函式庫。 ### 安裝PyTorch-Lightning函式庫 因為`PyTorch-Lightning`有發布在`Pypi`上,所以我們可以利用`pip install pytorch-lightning`來安裝`PyTorch-Lightning`函式庫。 ``` pip3 install --quiet pytorch-lightning ``` ### 利用`PyTorch-Lightning`建立訓練流程 利用`PyTorch-Lightning`建立訓練流程的過程很簡單,簡單來說就是「將必要的部份補齊」就好了。`PyTorch-Lightning`提供了各種已經設定好的類別,使用者只需要透過子類化(Sub-Classing)的方式繼承`PyTorch-Lightning`所建立好的類別,並將必要的部份補上/改寫即可。接下來就讓我們來看看如何實作。 #### 匯入必要函式庫並設定部份參數 ```python= import os import torch from pytorch_lightning import LightningModule, Trainer from torch import nn from torch.nn import functional as F from torch.utils.data import DataLoader, random_split from torchmetrics import Accuracy from torchvision import transforms from torchvision.datasets import MNIST import pytorch_lightning as pl import torchmetrics !rm -rf lightning_logs/ AVAIL_GPUS = min(1, torch.cuda.device_count()) BATCH_SIZE = 256 if AVAIL_GPUS else 64 ``` #### 建立基於Lightning模組的機器學習模型物件 基於`PyTorch-Lightning`的功能,我們可以將過往的好幾個步驟整合成一個物件,並透過`pl.Trainer`來完成對不同裝置的整合以及支援。 在過往的流程中,我們必須要依照以下流程來建立整個模型以及資料管線: 1. 建立DataLoader以及相關ETL管線。 2. 建立模型。 3. 設定超參數、優化器損失函數等等必要的部份。 4. 利用for迴圈開始訓練並進行訓練、反向傳播、計算損失值等等操作。 在導入了`PyTorch-Lightning`之後,以上繁瑣的的部份都不再需要。你只需要在繼承了`pl.LightningModule`的物件中設定以下部份: 1. 設定模型以及向前傳播的流程。(在物件中建立`training_step`以及`validatation_step`方法) 2. 設定資料該如何被導入。 (於`trainer.fit()`中導入或是在物件中建立`prepare_data`或`xxxx_dataloader`方法) 3. 設定優化器。 (在物件中建立`configure_optimizers`方法) 就可以了。在準備好物件之後,只需要透過`pl.Trainer`以及`trainer.fit()`函數的協助,就能透無痛的將你的訓練流程佈署在GPU、TPU以及其他可協助加速運算的硬體設備上進行訓練以及推論了。 以下是一個利用繼承了`pl.LightningModule`的物件來建立的MNIST模型物件。其包含了以下幾個元素: * `__init__`:整個物件的基本設定。 * `forward`:模型如何向前傳播。 * `training_step`:定義訓練的運算流程。 * `configure_optimizers`:定義所使用的優化器。 在此之上,若有需要也可以自行添加`validation_step`來定義驗證的運算流程。 我們在`training_step`中利用`self.log`來對準確率以及損失值進行了紀錄,但我們在這裡不會進行介紹,有興趣的人可以先前往瀏覽相關[文件](https://pytorch-lightning.readthedocs.io/en/stable/extensions/logging.html#)來了解詳情。 ```python= class MNISTModel(pl.LightningModule): def __init__(self): super().__init__() self.l1 = torch.nn.Linear(28 * 28, 10) self.accuracy = torchmetrics.Accuracy() def forward(self, x): return torch.relu(self.l1(x.view(x.size(0), -1))) def training_step(self, batch, batch_nb): x, y = batch out = self(x) self.accuracy(out, y) loss = F.cross_entropy(self(x), y) self.log("train_loss", loss, on_epoch=True) self.log("train_acc", self.accuracy, on_epoch=True) return loss def configure_optimizers(self): return torch.optim.Adam(self.parameters(), lr=0.02) ``` 在設定好模型物件後,我們就可以開始設定`pl.Trainer()`並利用它來進行訓練了。 #### 進行訓練 在進行訓練之前,我們必須先建立一個`pl.Trainer`物件,並告知我們所希望使用的硬體加速設備、要跑幾個週期進行訓練,以及其他的設定。在建立`pl.Trainer`物件後,我們可以利用`trainer.fit()`搭配模型以及相關參數來開始我們的訓練。 ```python= dataset = MNIST(os.getcwd(), train=True, download=True, transform=transforms.ToTensor()) train, val = random_split(dataset, [55000, 5000]) mnist_model = MNISTModel() trainer = pl.Trainer(max_epochs=10, gpus=AVAIL_GPUS, progress_bar_refresh_rate=20, ) trainer.fit(mnist_model, DataLoader(train, num_workers=4, pin_memory=True, batch_size=BATCH_SIZE), DataLoader(val, num_workers=4, pin_memory=True, batch_size=BATCH_SIZE)) ``` #### 利用TensorBoard了解訓練的成果 因為我們上方有利用`pl.LightningModule`的`self.log`功能對準確率以及損失值進行紀錄,所以我們可以透過`TensorBoard`來了解經由這樣的訓練流程設計,我們是否能成功對的對模型進行訓練。 ```python= %load_ext tensorboard %tensorboard --logdir lightning_logs/ ``` ![](https://i.imgur.com/0KYx6pp.png) 我們可以發現,`PyTorch-Lightning`很順利的依照我們所設定的流程進行了訓練。 #### 小結 藉由`PyTorch-Lightning`的幫助,我們省下了許多不必要且重複的的工程部份,以及節省了很大篇幅的程式編寫,並且很大的程度提昇了整個程式碼的可讀性。下方的組圖中,左方是舊有的訓練流程,而又方是藉由`PyTorch-Lightning`來編寫的核心部份。 <center class="half"> <img src="https://i.imgur.com/0CuLKoh.png" width="300"><img src="https://i.imgur.com/NIE0QXT.png" width="300"> </center> 我們省去了`to()`、`backward`以及`step`等不必要的部份,並獲得了專心在設計模型以及調整架構的時間以及機會,並且也無須在安排變數應該在哪一個裝置上費心思。這些就是藉由導入`PyTorch-Lightning`所帶來的好處。 ## `PyTorch-Lightning`機制介紹 在前面的部份,我們介紹了如何利用`PyTorch-Lightning`來建立訓練流程。接下來我們就來詳細了解`PyTorch-Lightning`是如何運作,又是如何以上方的機制來簡化流程的建立的。 ### 設計核心 `PyTorch-Lightning`的設核心為:以基於`PyTorch`的架構為基礎,建構一個具有高度彈性以及可擴充性的函式庫。`PyTorch-Lightning`所提供的所有主要函式,都是基於`PyTorch`原本就有的功能。以`LightningModule`為例,`LightningModule`是基於`nn.Module`來建構的,也就是在原生`PyTorch`中我們透過模型子類化建立模型時,必須繼承的對象,所以過往所有的模型設計可以無縫接軌直接改寫成使用`LightningModule`來建立。 ### 基於「Hook」,但不僅止於「Hook」 `PyTorch-Lightning`的架構之所有能有極高的彈性,就在於其設計上均以「Hook」的方式來將各個功能擴充到主要的功能之中,讓使用者不管在建立模型、建立數據管線、建立驗證以及測試邏輯、甚至是設計預測函式,都能夠被包裝在一個主要的物件之中,不需要在建立額外的物件。若要舉例的話,就類似於`tf.keras.Callback`所提供的`on_epoch_end`以及`on_epoch_start`等在自定義回測函數時,所用到的各個部份。使用者只需要將繼承對象原有的函式進行覆寫,就能夠隨心所欲的自訂一所想要的功能。 ### LightningModule ― 一即為全,全即唯一 `LightningModule`是在`PyTorch-Lightning`的架構中,最核心的一部分。無論是建立模型、設定超參數、建立數據管線、設定優化器以及處理訓練狀態等,只要是能想到的功能都能往繼承了`LightningModule`的物件裡面塞,最後在外面套一個`pl.Trainer()`結束這一回合。可以說是要多優雅就有多優雅,也不需要管變數要放在哪一個硬體上,更不用管資料要怎要在分佈式系統上處理,基本上交給`LightningModule`加上`pl.Trainer()`的組合就沒問題。 怎麼做到的?因為所有在`PyTorch-Lightning`中的Hook以及延伸功能都與`LightningModule`有掛勾,所以才能提供這樣極具彈性的功能。無論是訓練及測試用的`xxxx_step`也好,處理數據的`xxxx_dataloader`也罷。都與`LightningModule`緊緊相連。使用者只需要將必要的部份改寫成自己的設計,剩下的都可以交由`PyTorch-Lightning`所預先設計好的架構來處理。 對`PyTorch-Lightning`所提供的Hook有興趣的話,可以參考他們的[網站](https://pytorch-lightning.readthedocs.io/en/stable/_modules/pytorch_lightning/core/hooks.html),這裡就不一一介紹了。 ## 結語 在本章節中,我們介紹了如何利用`PyTorch-Lightning`所帶來的強大功能來最佳化我們的各項流程,也介紹了`PyTorch-Lightning`的核心設計理念以及架構的精隨所在。接下來我們會介紹如何利用`PyTorch-Lightning`的功能來一步一步讓整個流程更加的有彈性以及組織。 ###### tags: `Machine Learning` `Notebook` `技術隨筆` `機器學習` `Python` `PyTorch` `PyTorch-Lightning`