Coder Social home page Coder Social logo

ithome2022's People

Contributors

srarod avatar

Stargazers

 avatar

Watchers

 avatar

ithome2022's Issues

[Day23] Learning Rate Warm Up

Day23 Learning Rate Warmup

前言

本日將介紹另一個Learning Rate的技巧,也就是Warmup,並整合到Lightning-module當中。

什麼是Warmup?

Warmup的本質,實際上就是一種Learning Rate的使用策略。最早是在Deep Residual Learning for Image Recognition內有討論到「過大的Learning Rate似乎不易於收斂」這樣的可能性,後來才在Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour中作為一個Learning Rate策略廣為人知。

主要是為了避免初期Learning Rate過大,導致收斂的狀態不好(不收斂、或是收斂到Local Optimal之類的)。warmup主要採取的策略就是在訓練初期時,使用比較小的learning rate,在指定的epoch之後才恢復成原先的learning rate這樣的一個策略。也就是所謂訓練前要先暖身(warm-up)這樣的一個概念。

在論文中主要分成,下列兩種:
$$
\text{- constant warmup : } \alpha_t =
\begin{cases}
\alpha/n,& \text{if } t < n\
\alpha, & t \geq n
\end{cases}
$$

$$ \text{- gradual warmup : } \alpha_t = \begin{cases} t\alpha/n,& \text{if } t < n\\ \alpha, & t \geq n \end{cases}, \\ \alpha_t : \text{learning rate at } t\text{-th step}, n: \text{a specified warmup number of steps} $$

精神上都差不多,主要就是gradual是屬於一個緩步上升的版本,逐漸暖升。下圖則是論文中的實驗結果,主要是展示在不同的batch size底下,有warmup相對會比較穩定收斂:

Lightning-Module內如何實作

那麼在Lightning-Module內該如何實作這個warm-up的機制呢?實際上有很多種方法都可行,最簡單的方法就是設置一個learning rate scheduler去達成這個功能,例如依上面的式子直接設置一個lr_scheduler.LambdaLR就也可以達成。不過依照epoch、step的單位變化就稍微要調整一下,而且若要跟其他Scheduler一併使用也會讓lambda function的定義上更為複雜。

因此另外一個實作方法則是,利用lightning-module內的optimization_step(參考文件)。首先在超參數內新增一個:

train:
  optimizer: 
    name: 'Adam'
    learning_rate: 0.001
    warmup_epochs: 5

這邊參照論文中一樣設定為5個epoch,沒有太多額外的理由。

接著定義具有warm-up功能的optimization_step

# learning rate warm-up
def optimizer_step(
    self,
    epoch,
    batch_idx,
    optimizer,
    optimizer_idx,
    optimizer_closure,
    on_tpu=False,
    using_native_amp=False,
    using_lbfgs=False,):
    
    # update params
    optimizer.step(closure=optimizer_closure)

    # skip the first epochs        
    if self.CONFIG['train']['optimizer']['warmup_epochs'] > 0:
        if (self.trainer.global_step < self.CONFIG['train']['optimizer']['warmup_epochs'] * self.trainer.num_training_batches) :
            lr_scale = min(1.0, float(self.trainer.global_step + 1) / float(self.CONFIG['train']['optimizer']['warmup_epochs'] * self.trainer.num_training_batches))
            for pg in optimizer.param_groups:
                pg["lr"] = lr_scale * self.trainer.lr_scheduler_configs[0].scheduler._get_closed_form_lr()[0]

其中主要就是利用先找到每個epoch內batch的數量self.trainer.num_training_batches接著就能計算出該step應當的變化量,在乘上根據初始learning rate以及scheduler當下本該有的learning rateself.trainer.lr_scheduler_configs[0].scheduler._get_closed_form_lr()[0],就能scale出warm-up所需要設定的量了!

實作結果

接著我們進實作面,改好的檔案可以參考model.py,之後在同個commit之下便可以執行train.py。一樣參考wandb的結果圖,可以看到Learning的變化囉!

那一樣的有使用pretrained的情況可以看一下Loss與AUC的表現:

是不是相對來說有warm-up的版本平穩了許多呢?而且即使過度訓練,AUC的表顯距離上個峰值的差距也來的比較小了一些!

本日小節

  • 介紹Warm-up的機制
  • 與其他的Scheduler一同實做Warm-up

# [Day16] 資料預處理機制與優化 - 軟體篇 - 土法煉鋼

[Day16] 資料預處理機制與優化 - 軟體篇 - 土法煉鋼

前言

一樣承襲前幾日的主題,繼續討論資料前處理優化的方式。同樣是Cached到記憶體內,今天會利用比較土法煉鋼的方式來進一步優化效能。

此篇始於意外

實際上有跑前一篇利用Monai來進行Cached的朋友應該有發現,每次的訓練前,都需要花費一段時間進行Cached。於是就會發生「80M的東西要讀取好幾分鐘」這樣十分不合理的事情。於是我本身在實驗的時候,對於前一篇所提到的「monai.data.CacheDataset與直接把mednist的.npz讀進來的效率差不多」這件事情開始懷疑,經過測試以後發現果然還有優化的空間,因此意外誕生了這一篇。

直接使用.npz

直接讀取.npz其實會發現,不過就是幾秒鐘的事情,而且已經直接把三個切分全部讀進來了。

>>> npz_files = np.load('data/chestmnist.npz')
>>> for key in npz_files.files:
>>>     print(key, f'shape {npz_files[key].shape}')
train_images shape (78468, 28, 28)
val_images shape (11219, 28, 28)
test_images shape (22433, 28, 28)
train_labels shape (78468, 14)
val_labels shape (11219, 14)
test_labels shape (22433, 14)

上一篇情況中,會比較久,主要是因為需要把10多萬張png分別進行讀取,再整成檔案花費了許多時間。而.npz的狀況,則是因為是單一個連續的檔案,因此可以只進行一次的I/O直接全部讀取進來。

所以若是原始比較大型的檔案,如果前處理以後的影像不大(例如把X光壓成224x224),再壓成像.npz這樣的array實際上應該是可行的。

轉成dataloader

為了讓先前dict-based的Transforms以及Dataloader可以延續使用,首先我們要把資料點從整個大矩陣切個出來,像是train set就是把78468x28x28 切成 78,468個28x28,可以利用np.split直接進行快速的分割:

for key in npz_files.files:
    sliced_data[key] = np.split(npz_files[key], len(npz_files[key]))
  • 注意一點是,在這裡千萬不要使用python內建的List Comprehension來對array進行slice進行分割(e.g. [npz_files[key][i] for i in range(len(npz_files[key]))] 很慢,可以體驗看看...),會受到python慢的原罪影響,要跑非常久。矩陣運算的任務可以的話一定要交給優化過效能的numpy來執行。

接著把slice後的array製程dict就完成了!

datasets = {
    split : [{ 
        'img' : sliced_data[f'{split_mapping[split]}_images'][i].transpose(0,2,1),
        'labels' : sliced_data[f'{split_mapping[split]}_labels'][i][0]}
        
    for i in range(len(sliced_data[f'{split_mapping[split]}_images']))]
  • 這裡會對image進行transpose是因為跟我原本使用的Image reader方向不一致,因此我轉置一下,實務上基本沒有差異。
  • 因應資料格式,Transform也會需要更改。

具體可以參考preprocess.py,執行後可以得到下圖,基本上與先前無異。

實際差異

在修改了前處理以後,可以實際跑一次train.py看看。這邊大概要注意幾點是:

  • 由於整個Array已經讀進來了,所以使用一般的Dataset就可以,沒必要再Cache一次
  • 使用以後大幅縮短前面Cached2的時間,不需要等待好幾分鐘的png cache process!

一樣只訓練5個epoch,大概跑個幾分鐘就可以得到結果了:

  • 本來是期望可以縮短Cached,但訓練時間居然又縮短了!
  • 推測可能是monai.data.CacheDataset有進行更多一些meta data的I/O,或是讀取Cached的機制沒有這麼直接,造成實際上產生資料的速度會花上更多的時間。

結語

  • 有點意外,但之後實驗可以省更多時間!
  • 矩陣好、矩陣妙、矩陣呱呱叫!
  • 土法煉鋼有土法煉鋼的好

[Day21] EVALUATION ON TEST SET

前言

前幾日的文章,已經利用scheduler訓練出在Validation上還不錯的結果,今天就來test一下,順帶討論一下test的一些哲學吧!

什麼是Test Set?

一般在我們進行訓練模型的任務時,大致上就是分成Train、Validation以及Test的三個階段。讓我們回顧一下,其各自的角色分別是

  • Training:主要進行Back-Propagation時,計算梯度的材料。
  • Validation:用任務目標的指標,作為挑選超參數的依據。
  • Test:通常是模型從未看過的資料,用來評估一切訓練好調整好後模型的準確度。

Lightning-Module test module

在我們前面已經介紹、並建構用來做實驗的Lightning-Module之中,除了我們先前用過train_stepvalidation_step功能以外,自然也有相應的test_step

具體只要一樣在module的class內加上:

def test_step(self, batch: Any, batch_idx: int):
    inputs, preds, labels, loss = self.step(batch)
    self.log('test/loss', loss.item(), on_step=False, on_epoch=True, batch_size = inputs.shape[0])
    return {
        'loss' : loss,
        'preds' : preds,
        'labels' : labels
    }

接著一樣加上對應的epoch_end,就可以計算metrics了:

def test_epoch_end(self, validation_step_outputs: List[Any]):
    preds = torch.cat([output['preds'] for output in validation_step_outputs], dim=0).float()
    labels = torch.cat([output['labels'] for output in validation_step_outputs], dim=0).long()
    probs = torch.nn.Sigmoid()(preds)

    # compute metrics and log
    acc_score = torchmetrics.functional.accuracy(probs, labels, mdmc_average = 'global')
    auc_score = monai.metrics.compute_roc_auc(probs, labels, average='macro')
    self.log('test/acc', acc_score.item())
    self.log('test/auroc', auc_score.item())

之後重新build model的物件,把訓練好的權重讀進來,使用lightning-module對應的test函式,就可以進行test set的計算了:

# build model and load trained model
net = model.MultiLabelsModel(CONFIG)
net = net.load_from_checkpoint(CONFIG['evaluate']['weights_path'])

# initialize the Trainer
trainer = pl.Trainer(**CONFIG['evaluate']['tester'])

# test the model
trainer.test(net, dataloaders=data_generator)

具體可以參考 evaluation.py,一樣可以用

# python src/evaluate.py --config=hparams.yaml 
GPU available: True (cuda), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
Testing DataLoader 0: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 44/44 [00:06<00:00,  6.89it/s]
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃        Test metric        ┃       DataLoader 0        ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│         test/acc          │    0.9475677013397217     │
│        test/auroc         │    0.7314980626106262     │
│         test/loss         │    0.16936460137367249    │
└───────────────────────────┴───────────────────────────┘

結果跟驗證集的結果十分的接近,算是一個相當不錯的結果。

另外注意一下是,我這裡test時的batch size = 512,純粹就是比較快。而且正常情況下test時不論batch size多少都不應該影響計算結果。覺得結果怪怪時,不妨可以試試看,如果不同的話可能就是程式出現了什麼問題。

Test set的一些哲學問題

雖然這樣下小標題可能誇張了!但其實我認為在實務上,Test Set其實隱藏著很多的巧思。

首先是第一種常見的狀況就是,開發者過多且反覆的對Test Set計算結果。這個情況在各種資料的比賽跟以準確度為驗收標準的專案上尤其常見,大部分都會演變成Test Set儼然已經變成另外一個Validation set,在資料有限的狀況下,很難知道準確度的上升到底只是hyperparameter fit on test set還是真的找到了什麼insignt。有些團隊則會利用類似Cross-validation的方式來驗證模型的好壞以避免此情形,也不失一個好方法。

另一種狀況則是更根本的問題,被挑選的Test set其究竟代表什麼?是否與訓練樣本具有相同的分佈?是否與未來真實要處理的問題有相同分佈?種種的問題。分佈問題這一點在像是ChestMNIST資料量很龐大的資料集通常不是大問題,畢竟量大,抽樣基本上誤差也不會大到哪裡去,但小樣本就必須要很注意這東西。尤其是label的分佈,會直接影響到模型對label precition的偏好,不可不慎。

還有就是一些實務上會遇到的準確度下降的問題,這邊簡稱是各種Drifting,這個會在最後一天作為收尾來介紹。

本日小結

  • 介紹lightning-module的test功能
  • 一些Test set的murmur

[Day15] 資料預處理機制與優化 - 軟體篇 - 空間換取時間

[Day15] 資料預處理機制與優化 - 軟體篇 - 空間換取時間

前言

承襲前幾日的主題,今天繼續討論Preprocess優化的方式。今天處要討論的是針對I/O重複性優化的問題。

資料預處理機制

一樣我們先上架構圖,但稍微改一下。假設是在大量的訓練過程中,自然而然我們就需要做非常多次Preprocess + model computation (forward + backward),大概會變得像是下面這張圖一樣:

flowchart LR
    subgraph Main[ ]
        CPU <-->|preprocess| RAM
    end
    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
  Storage --> Main --> GPU
  Storage --> Main --> GPU
  Storage --> Main --> GPU
  Storage --> Main --> GPU
  Storage --> Main --> GPU

那這裡頭還有沒有什麼優化空間呢?有的,簡單一句,空間換取時間。

空間換取時間

理論上來說,同樣的影像(或是原始資料)在經過預處理以後,會是相同的衍生物(即使加入augmentation,至少在augment之前也是相同的東西)。於是這裡就有個可以介入的空間了,即是把預期會被重複執行的Preprocess結果儲存起來以便重複利用

概念上就可以畫成下面這張圖:

flowchart LR
    subgraph Main[ ]
      CPU <-->|preprocess| RAM
    end
    
    subgraph Temp
      temp[Process Data]
    end

    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
  Storage --> Main --> Temp --> GPU
  
  Temp --> GPU
  Temp --> GPU
  Temp --> GPU
  Temp --> GPU

而把整個Processed暫存的位置,具體而言又可以分成下列兩種:

  • RAM :
    • 優點:直接放在RAM內,可以往GPU直接送,同時節省運算跟I/O的時間,通常速度最快。
    • 缺點:佔用大量的RAM,過於大型的資料集不太適合。
    • 實作:可以用monai.data.CacheDataset直接實作,傳送門
  • Storage :
    • 優點:將前處理後的檔案放在Storge內,可以節省運算時間,而且不需要佔用到大量的RAM。
    • 缺點:沒有直接Cached來得快。
    • 實作:可以用monai.data.PersistentDataset傳送門

實際差異

由於MedMNIST非常的小,Persistent的作法不太顯著。我們這邊直接比較Cached到RAM的作法,可以看到訓練時間又更加的縮短了!Cached的實作可以參考這個commit,見下圖:

看起來似乎沒有差很多,但這些操作隨著前處理計算量跟資料量大小的增加而更加的效果顯著,尤其像是在醫療影像內動輒幾萬張解析度2000x2000的x光、有時間維度的超音波甚至是CT跟MRI。

額外的小murmur

  • 其實一開始medmnist套件內的dataloader,它是利用.npz的格式把整個資料集讀進來,直接使用差不多就是這個cached後的效率。
  • 主要還是為了有一整套處理流程才這樣編寫。(迷之音:根本湊課程吧你)
  • 實際上資料集如果夠大(大概就是大小比你的RAM還大時),用Presistent的話機率會比較高。
  • 用Presistent的時候要注意兩點:
    • 會產生.pt的cache檔案,不小,注意你的硬碟量
    • cache後,重跑程式時,檢查如果已經cache過就不會重新執行,所以如果preprocess方式有更動過的話,記得要重新cache

本日小節

  • 利用空間換取時間,再加速訓練流程

[Day12] Training Log and History

[Day12] Training Log and History

前言

本日的文章將介紹幾個常見的Deep Learning Log system,然後嘗試解決目前模型所看到的問題。

Lightning-Module Log 與 TensorBoard

在前一日的實作中,除了為了讓Callback可以參考metrics作為模型儲存的依據,而增加在validation_epoch_endself.log(val/puroc)以外,相信眼尖的讀者應該有發現,我們也分別在train_stepvalidation_step內新增了self.log('train/loss', loss.item())以及self.log('val/loss', loss.item()),如此一來便能開啟Lightning-Module在訓練的途中紀錄LOSS的功能。

另外可以在PyTorch-Lightning文件當中,可以看到其實預設是會使用TensorBoard的logger。所以其實先前訓練完會在/home/$USER/workspace/artefacts/weights內的就包含有TensorBoard的Log檔唷!(主要就是那個event檔)

此時可以使用Jupyter Lab已經設定好的Tensorboard:

就可以看到先前訓練模型的紀錄囉!

另一個推薦的選擇 -- WandB

TensorBoard可說是最老牌的開源工具,且當然,也有不少可以替代他的工具。在這邊我介紹一個我個人滿喜歡的一個服務WandB(Weights & Biaes)

有興趣的人可以直接到官網申請帳號,只要在Terminal打入

wandb login

然後輸入辦好帳號以後獲得的API KEY就完成初步的設置了。

接著只要在加到Trainer後就可以使用了

wandb_logger = pl.loggers.WandbLogger(project="iThome2022")
# set trainer
trainer = pl.Trainer(
    callbacks = checkpoint_callback,
    logger = wandb_logger, # 加在這裡
    default_root_dir = CONFIG['train']['weights_folder'],
    max_epochs = CONFIG['train']['max_epochs'],
    limit_train_batches = CONFIG['train']['steps_in_epoch'],
    accelerator = 'cuda',
    devices = 1, 
    profiler="simple")

實作

一樣到這次對應的commit底下,執行

python src/train.py --config=hparams.yaml

(註:這次有更新一些Dockerfile及Docker-compose的內容,建議重新Build,不然可能會有bug)

訓練完以後,登入以後可以在首頁看到目前有跑得實驗們的紀錄

點進去以後就可以到處看看囉!個人覺得最方便的則是可以互動式的去新增自己想看的圖

從上面這張比較Training與Validation的圖當中可以看到,Training很穩定的下降,而validation一開始上升,但後續就跟training十分接近了!評估起來應該是沒有Overfitting的問題。但考慮到auroc還不夠高,而且似乎尚未收斂的狀況,我想應該是太早就停下來訓練所導致。

下次可以訓練多一點Epoch來看看成效!

本日小節

  • 本日介紹兩種使用Training Log的工具
  • 模型看來要繼續訓練

[Day24] Regularization in Deep Learning

[Day24] Regularization in Deep Learning

前言

本篇開始之後的幾天,預計將介紹模型訓練的最後一個章節,正規化(Regularization)。本篇會先給一些Overview的介紹,後續的幾篇則會有實作。

什麼是正規化(Regularization)?

首先定義一下,台灣因為翻譯滿亂的關係,有時候Normalization也會翻成正規化,這兩個term在Deep Learning幾乎可以說是完全不同的東西,而今天這篇介紹的正規化指得就是Regularization。

先借用一下Ian Goodfellow大神出版的Deep Learning教科書中的定義:

we defined regularization as “any modification we make to a learning algorithm that is intended to reduce its generalization error but not its training error.”

Training Error的角色很顯而易見,就是我們在訓練途中,訓練集內的loss或是準確度。我們先前所作的optimizer、learning rate,無異就是為了要去減少我們訓練時的所產生的Training error。

Generalization Error (可參考wiki),一般會翻譯成泛化誤差,意思是指我們所訓練出來的模型,針對從未見過的資料,進行估計或預測時的誤差。這一概念其實很玄,從未見過那又要怎麼去計算?所以一般的情況下,我們也只能對泛化誤差進行某種程度上的估計。

常見的估計便是Testing Error,畢竟Test Set是模型從未看過的資料,因此是一個合理的估計。但問題是開發人員一旦反覆的對測試集進行計算,其實人看過自然會影響模型的選擇,因此某種程度上可以說測試集進行計算的次數越多,使用Testing Error對Generalization Error就很可能越不准。(可能啦...)這也是我先前在介紹Test時,所說的盡量不要一直對測試集進行計算的主因。

另一個term則是Validation Error,它的角色則是比較接近我們找一個與test set分佈接近的validation set,然後以能夠去得到Validation Error最小的方式,來去盡量獲得一個可能是Testing Error最小的模型。

那回到主題的部份,也就是說,一切我們為了降低generalization error的那些事情,就可以說是Regularization!也因此或許換個語言說,那些可以減緩Overfitting的方法或技術,就可以稱作Regularization。

有哪些實例

在一般不進行任何限制底下,只要模型的參數越多,網路的結構越複雜,幾乎都可以訓練到Training Error接近0的狀態,但套用到Test set甚至僅在Validation Set的狀況下,很可能就已經不好了,這種情況下就是所謂的Overfitting。概念圖(來源):

這時候所謂Regularization之稱的技巧,就有空間可以進入了。手法基本上有千千百百種,常見的Dropout Layer(基本上已經是每個模型必備,這邊就不介紹),甚至連early stopping(一種在若觀測到validation error不再下降便停止訓練的手法)也是一種Regularization。這個手法在Pytorch-Lightning上實作也十分容易:

from pytorch_lightning.callbacks.early_stopping import EarlyStopping
early_stopping_callback = EarlyStopping(monitor="val_loss", mode="min", min_delta=0.00, patience=3)
trainer = pl.Trainer(...,
                     callbacks = [early_stopping_callback])

但這個手法難的主要是怎樣叫做下降變慢?有沒有可能在訓練一些又下降了?基本上也是要實驗才會知道。

而後續的幾篇主要則主要會介紹下列幾種Regularization的方法,並進行實作:

  1. Label Smooth
    • 一種在訓練時,將loss在與label進行計算時進行模糊化的技巧。通常在實際label的定義具有模糊空間時會有奇效。
    • 例如,某x光圖片很像a病也很像b病,甚至是在醫學上a病以及b病的定義都還有爭議的情況。
  2. Data Augmentation
    • 在訓練時,對圖片進行一些不改變其內容結構的變換,藉此增加訓練集資料的豐富度,來避免模型Overfitting的方法。基本上可說是很多實驗的必備良藥了,尤其是在資料量不大的情況下,效果通常很顯著。
    • 例如,一隻狗的圖片鏡像後,還是一隻狗(是吧...?)
  3. Weight Decay
    • 主要是對模型參數進行一些限制,通常會把所有要限制的參數透過某函數進行總和後,當成loss的一部分,再進行訓練,透過避免極端的參數來避免Overfitting的可能。
    • 也很常被稱為Shrinkage method,其中一個主因也是此方法主要來自於正規化方法使用L1的Lasso Regression跟使用L2的Ridge Regression

總之,無意外的話,後續三天會實作這三項,並比較結果。

本日小節

  • 本日概況式的討論了正規化是什麼
  • 介紹一些常用的技法
  • 準備接下來幾天的實作

[Day28] Lp Regularization

[Day28] Lp Regularization

前言

本篇將延伸上一篇討論的L2 Regularization,延伸到所謂的Lp Regualrization,及其在Deep Learning中比較粗暴的作法。是作者本人覺得很喜歡的一個技術,不過實務上提昇通常很有限就是(汗)。

什麼是 Lp Regularization?

顧名思義,Lp Regularization,是利用Lp去進行Regularization的方式。而除了前一篇提過的L2是歐基理德距離以外,所謂的Lp指得便是線性代數中的Lp-norm

$$
{\left|x\right|}p = \left(\sum{i=1}^n |x_i|^p\right)^{1/p} \text{, vector } x = (x_1, x_2, ..., x_n)
$$

  • 可以看成是向量X在Lp空間底下的長度
  • 常見的L1就是絕對值加起來的平均
  • 常見的L2則等同於常見的Mean Square、歐基理德距離

具體限制的方式一樣是把沒有開根號版本的Lp-norm加入loss,(僅是為了比較好算梯度、以及避免rounding error)再進行學習,可以寫成類似下面的概念式:

$$
\begin{align}
L &= L_{class}(f(x, w), y) + \lambda |w|p^p, \
&= L
{class}(f(x, w), y) + \lambda \sum_{i=1}^{n} w_i^p,
\end{align}
$$

$$ \text{where } w \text{ is model weights}, f \text{ is the model}, \text{ and } \lambda \text{ is the weights penalty coeifficient} $$

如此一來,除了可以達到對權重的限制以外,也可以根據p這個超參數的設置,進一步進行不同的限制效果。

具體實例可以參考上一篇也有提到Google爸爸的這個視覺化工具。裡面有L1跟L2的比較,大約總結底下幾項:

  1. L1由於使用絕對值,其性質通常會把不重要的係數降到0。會具有稀疏的性質,但這有時這又跟深度學習一堆隱藏層權重的性質有點衝突,所以通常比較少用一點。另外,在傳統迴歸當中,使用L1的方法又稱為LASSO(least absolute shrinkage and selection operator),shrinkage的名稱也從這邊來。
  2. L2的話,同時也近似於上一篇介紹的Weight Decay,由於使用Mean Square,數學性質好,也容易簡化,通常只是縮小跟限制大小,不太容易歸0。也因此最常被使用。傳統迴歸則叫做Ridge Regression。
  3. p不等於1與2的情況,我個人也沒有使用過。僅知道通常會分成p = 0, 0 < p < 1, p > 1三種情況會有大致不同的性質可以運用。有興趣的朋友可以試試,應該也有一些文獻可以查詢。
  4. 另外還有一種對L1與L2進行線性混合的regularization,叫做Elastic Regression,也有一些人使用,有興趣的朋友也可以試試看,但就不在今天實作的範圍內。

Weigth decay 與 L2 Regularization的差異

上一篇文章有提過,只要使用Optimizer中的weight decay就可以達到類似L2 Regularization的限制效果。但其實還有一個差異尚未提到的則是,由於weight decay是直接實作在整個梯度,因此整個Regularization的範圍是模型內所有的參數。傳統的迴歸僅作用在乘法項的權重,而不作用在截距項的BIAS,因此實際上還是不大一樣。

由於筆者是統計出身的,總覺得連帶截距項也懲罰很奇怪。因此本作為本篇文章的目標,決定自刻一個只作用在乘法項的權重的Lp Regularization實作。

實作方法

實作方法一樣可以透過增加lightning-module內的函數來作到,首先新增對應的超參數到hparma.yaml

train:
  weight_decay:
    p: 2
    lambda: 0.00001 # 0.01 * 0.001

然後新增計算lp power sum的函數:

def lp_power_sum(tensor, p):
    if p == 2:
        return (tensor ** 2).sum()
    else:
        return (tensor.abs() ** p).sum()

將利用lp power sum計算懲罰項的函數到增加lightning-module內:

def lp_penalty(self):
    reg_loss = 0
    for name, weight in self.named_parameters():
        if 'weight' in name:
            reg_loss += lp_power_sum(weight, p = self.CONFIG['train']['weight_decay']['p'])
    return reg_loss

最後更改train_step內計算loss的方式:

def training_step(self, batch: Any, batch_idx: int):
    inputs, preds, labels, loss = self.step(batch)
    self.log('train/loss', loss.item(), on_step=True, on_epoch=True, batch_size = inputs.shape[0])

    reg_loss = self.lp_norm()
    self.log('train/reg_loss', reg_loss.item(), on_step=True, on_epoch=True, batch_size = inputs.shape[0])
    reg_loss = self.lp_penalty()
    self.log('train/reg_psum_loss', reg_loss.item(), on_step=True, on_epoch=True, batch_size = inputs.shape[0])


    if self.CONFIG['train']['weight_decay']['lambda'] > 0:
        loss += reg_loss * self.CONFIG['train']['weight_decay']['lambda']
        self.log('train/total_loss', loss.item(), on_step=True, on_epoch=True, batch_size = inputs.shape[0])

    return loss

基本上就大功告成了!

另外解釋幾點:

  • 先前做的weight decay實驗只有紀錄lp norm 而沒有power sum,因此這邊仍然計算並紀錄lp_norm,純粹是希望log能與作比較。
  • lambda 選擇 0.00001是因為 0.02 / 2 (weight decay用0.02,但它的微分與直接L2會差一半,因此除二) * 0.001 (初始learning rate,避免adaptive gradient造成過於快速的參數下降)

實作結果

一樣請參考這個commit進行實驗。

跟前幾次相比的結果如下圖:

  • Lambda沒有根據adaptive gradient去調整也有作,不過就是權重下降太快導致underfitting了
  • 雖然整體來看差異不大,但只在乘法項進行限制的L2 Regularization似乎達到了最好的表現,有點出乎意料。

本日小節

  • 實驗差點做不出來好險
  • Lp Regularization很浪漫,於是我還是決定實作

[Day 4] Data Preparation for MedMNIST

前言

前一日我們將原始影像檔案都抓下來到資料夾內了,本日將接續繼續進行資料的處理。依照個人的習慣主要會建立一個對應的長表格(long table or narrow data, 可參考wiki)進行管理,主要優點有下列兩點:

  1. 利於對每一個data point進行管理
  2. 資料視覺化方便

ChestMNIST 介紹

延續前一日的我們抓取來使用的ChestMNIST,資料集的詳細都可以透過該套件的函式得到,如下:

>>> from medmnist.info import INFO
>>> INFO['chestmnist']
{'python_class': 'ChestMNIST',
 'description': 'The ChestMNIST is based on the NIH-ChestXray14 dataset, a dataset comprising 112,120 frontal-view X-Ray images of 30,805 unique patients with the text-mined 14 disease labels, which could be formulized as a multi-label binary-class classification task. We use the official data split, and resize the source images of 1×1024×1024 into 1×28×28.',
 'url': 'https://zenodo.org/record/6496656/files/chestmnist.npz?download=1',
 'MD5': '02c8a6516a18b556561a56cbdd36c4a8',
 'task': 'multi-label, binary-class',
 'label': {'0': 'atelectasis',
  '1': 'cardiomegaly',
  '2': 'effusion',
  '3': 'infiltration',
  '4': 'mass',
  '5': 'nodule',
  '6': 'pneumonia',
  '7': 'pneumothorax',
  '8': 'consolidation',
  '9': 'edema',
  '10': 'emphysema',
  '11': 'fibrosis',
  '12': 'pleural',
  '13': 'hernia'},
 'n_channels': 1,
 'n_samples': {'train': 78468, 'val': 11219, 'test': 22433},
 'license': 'CC BY 4.0'}

看到輸出後可以得知,這主要是一個總共112,120個樣本的資料集,且具有下列性質的任務:

  • Multi-Label:一個樣本可能同時存在多個不同的Label
  • Binary Class:每一種Label是二元分類

而這些類別則分別是:

  • atelectasis:肺膨脹不全
  • cardiomegaly:心臟肥大
  • effusion:滲出液
  • infiltration:浸潤
  • mass:腫塊
  • nodule:結節
  • pneumonia:肺炎
  • pneumothorax:氣胸
  • consolidation:(肺)實變化
  • edema:肺水腫
  • emphysema:氣腫
  • fibrosis:纖維化
  • pleural thickening:胸膜增厚 (這個要回去查NIH Chest X-rays才會知道...)
  • hernia:疝氣

先讓大家有個初步的認識,翻譯全部是透過GOOGLE再個人挑選,有錯誤請指正,感謝!

輸出csv

基本上後續的工作就是根據資料的不同狀況,進行一些理解再整理,也可以一併進行抽樣。
此部份會很根據實際的樣本情形。

本文案例的話,由於MedMNIST已經整理的差不多了,基本上進行以下的小處理就可以得到結果

df = pd.read_csv('data/chestmnist.csv', header = None)
cols = ['split', 'img'] + list(INFO['chestmnist']['label'].values())
df = df.rename(columns={ i: col for i, col in enumerate(cols)})
df['img'] = df['img'].apply(lambda x: os.path.join('data/chestmnist/', x))

此外,整段可以參考該commit的src/make_dataset.py,一樣在workspace執行就可以了!

python src/make_dataset.py

執行以後就可以得到該csv囉

Data Exploration

在實際進入真正的建模之前,還是先多解資料一點吧!

先畫個圖理解一下概況,大概可以掌握有幾類是比較好發於資料集中的疾病

import pandas as pd
import plotnine as p9

df = pd.read_csv('data/dataset.csv')
df_long = pd.melt(df, id_vars=['split', 'img'])
df_long = df_long[df_long['value'] != 0]
(p9.ggplot(df_long) 
+ p9.geom_bar(p9.aes(x='variable', fill = 'split'))
+ p9.theme(axis_text_x=p9.element_text(angle=45))
)

另外也可以看一下各個切分的樣本分佈是不是夠接近,以免發生分佈差異,而預測不准的常見問題。

看起來是還好,應該可以安心的進入下一步囉!

本日小節

  • 拿到資料以後,今天進行了簡單的資料整理跟分析
  • 長資料有利於分析與管理的方便性
  • 先大概知道資料再往下走

[Day25] Label Smooth

Day25 Label Smooth

前言

本日將深入介紹並實作第一個Regularization的技巧--Label Smooth,翻譯的話,好像可以稱作標籤平滑吧。

什麼是Label Smooth?

在前一日的文章內已經簡介提到,Label Smooth是一種在訓練時,將loss在與label進行計算時進行模糊化的技巧。這裡我們以最單純的Binary Classification為例,在只有兩類的情況下,label通常就是陽性(y=1)以及陰性(y=0)。

而Label Smooth的技術就是類似把這個0跟1,視為機率的概念,加入一個允許模糊的參數α,讓模型可以進行一個不會太極端狀態的學習,具體就是把label的定義改成類似下者:
$$
y^{*} =
\begin{cases}
1 - \alpha, & \text{ if } y = 1 \
\alpha, & \text{ if } y = 0
\end{cases}
$$

因此α便是一個可以自訂來調整模糊空間的超參數,理論上也是需要Tune的。(Multiclass的情況可以參考這篇

為什麼要Label Smooth?

前一日也有提到,這是一個Regularization的方法,為的是降低Generalization Error。但思路是怎麼做的呢?在訓練反覆迭代的過程中,其實是會讓模型對於訓練集中的陽性樣本給出的結果越來越趨近1,而陰性樣本的結果則是越來越趨近於0。聽起來很不錯,但對嗎?這是不是也意味著,模型很有可能只能輸出很接近0跟1的結果?是不是模型在0到1之間的變化幅度會非常劇烈、鮮少出現中間值的情形?即使是未來的資料出現模型不存在的特徵,沒辦法給出不確定的中間機率值,而給出一個極端值呢?

Label Smooth便是一個處理這樣問題的技術,藉由調整label在計算loss時的數值,允許某一程度的模糊空間,進一步的避免訓練出來的模型變得只會去預測接近0與1的極端值。

另外補充,To Smooth or Not? When Label Smoothing Meets Noisy Labels有討論到noise rate的大小與label smooth與否及程度大小的研究,算是一個滿適合延伸的閱讀,有興趣的讀者可以再看看。

實作

實作的部份則需要土法煉鋼一些東西,首先定義一個調整label的函數:

def label_smoother(tensor, label_smooth_fact):
    return tensor * (1 - 2 * label_smooth_fact) + label_smooth_fact
  • 補充...1*(1-2α) + α = 1-α, 0*(1-2α) + α = α, 所以是上面的式子

然後讓我們在step內設定成,如果要做label smooth的話會進行更動:

def step(self, batch: Any):
    inputs, labels = batch['img'].to(self.device, non_blocking=True), batch['labels'].to(self.device, non_blocking=True)
    preds = self.forward(inputs)
    if self.CONFIG['train']['label_smooth'] > 0:
        smooth_label = label_smoother(labels, self.CONFIG['train']['label_smooth'])
        loss = self.loss_function(preds, smooth_label.float())
    else:
        loss = self.loss_function(preds, labels.float())
    return inputs, preds, labels, loss
  • 補充...不過我們式子內其實是可以general到α=0,所以其實不分case也沒差就是了

接著就可以進入訓練環節了!

而另外補充,常見的Cross Entropy實作上比較容易,可藉由直接調整參數來達到,可參考文件)

實作結果

一樣可以參考這個Commit進行實作,這裡設定的α=0.05。

首先讓我們看看loss,可以看到Label Smooth的緣故,會使得BCE計算loss的方式跟預測的方式都會改變,也因此會造成一個很大的格差。

另外來看看AUC的部份,個人是猜測可能由於

  1. α=0.05可能太大,反而造成收斂上的障礙
  2. label smooth或許需要更小的learning rate才能繼續收斂?
  3. 或許我的實作有什麼BUG?
    所以結果反而變差了

最後可以來看一下對outcome的影響,可以發現確實大部分的結果從接近0,被移動到了
α=0.05的部份了。

  • 另外,由於ChestMNIST大部分的Ground Truth都是0,因此模型輸出會大幅偏右

本日小節

  • 介紹及實作label smooth
  • 解讀結果

[Day 3] MedMNIST v2

前言

本日會開始進入最主體的Classification的部份,首先就從資料集開始介紹吧!這次會使用的是MedMNIST這個資料集,差不多就是Medical版的MNIST的意思。

MedMNIST v2

MedMNIST基本上是個包含了各種不同類型的醫療影像的資料集,分別由12個不同的2D醫療影像集與6個不同的3D醫療影像集所組成。所對應的Task基本上也都是Classification問題,其中的RetinaMNIST雖說是Ordinal Regression,但其實本質上也是一種Classification。影像的解析度部份則都壓縮到了28x28(x28),都非常小,所以非常適合新手作為練習使用。目前已經出到第二版,所以才會加個v2,主要差異是新增3D的6個資料集。
簡單看這張來自官網的Overview其實應該就可以理解整個資料集的概況:

MedMNIST v2 Overview

其餘詳細的部份,有興趣建議直接參考官網,本日的文章則會著重講解實際抓取下來使用的部份。

MedMNIST套件安裝

廢話不多說,先上GithubPyPI
安裝很簡單就一行

pip install medmnist

或是先前透過我提供的docker-compose.yml安裝環境的朋友們應該也都已經有了(請參考requirements.txt)。

MedMNIST套件使用

MedMNIST的函式庫內提供不少東西可以使用,尤其是還可以直接使用PyTorch的DataLoader,基本上直接套用再加上常見的訓練框架就可以開始做模型,可說是十分方便。

但實務上其實真正耗費大量時間的,往往是資料的準備及處理,撇除不太可能在這裡進行的標注,本系列文希望盡量模擬實際資料獲得的情形。

現在讓我們模擬較接近真實的情況,因此我們後續會採用一大筆影像檔再加上一個對照的csv檔案,這樣的形式開始。

所幸只要利用靈活的MedMNIST,以取得chestmnist為例,輸入以下的程式碼

import medmnist
from medmnist.info import INFO, DEFAULT_ROOT

DATASET = 'chestmnist'
TEMP = '/tmp'
FOLDER = 'data'
POSTFIX = 'png'

def make_dataset() -> None:
    for split in ["train", "val", "test"]:
        print(f"Saving {DATASET} {split}...")
        dataset = getattr(medmnist, INFO[DATASET]['python_class'])(
            split=split, root=TEMP, download=True)
        dataset.save(FOLDER, POSTFIX)  

也可以直接在workspace底下執行檔案

python src/make_dataset.py

等待片刻,執行好以後,就可以獲得像這樣的結構

如此一來,就準備好資料了!

結論

  • 本日介紹友善的練習用資料集MedMNIST及其使用的方法
  • 實作取得圖檔的方法
  • 為接下來的資料處理做準備

[Day19] Learning Rate Finder

[Day19] learning Rate and scheduler

前言

接續上一篇討論的Optimizer與Learning Rate,今天來討論關於Learning Rate以及其Scheduler的挑選。

Learning Rate Finder with Lightning tuner

上一篇我們有提及Learning Rate的挑選是個重要的問題,太小會訓練太慢,太大則怕梯度爆炸。但要到底要怎麼挑選呢?是否有任何的依據?

這裡介紹一個十分熱門的參考方法,就是所謂的Learning Rate Finder。透過一個有限次數內的step,每個step使用不同的Learning Rate進行模型的訓練,觀察其Loss的變化程度,依照變化程度去尋找收斂較快的Learning Rate。

具體的作法Pytorch-Lightning的Tuner,可以直接實作,例如下面的例子:

# set tunner
trainer = pl.Trainer(**CONFIG['train']['trainer'])
# tunner
lr_finder = trainer.tuner.lr_find(net, data_generator)

跑出來以後就會得到類似這張圖,以及其建議的Learning Rate:

那麼這次實驗的結果是建議 0.0058,就可以考慮從這個周圍去選取並開始訓練囉!

實作部份可以參考這個tuner.py,一樣只要打下列就能跑:

python src/tuner.py --config=hparam.yaml

另外關於Learning Rate Finder的來源我沒有查到,最早看到是在Deeplearning.ai的課程當中。若有讀者知道的話,也請告訴我,感謝!

Decide a Learning Rate and Scheduler

那麼拿到 0.0058 以後,其實也是有不少作法,可以就直接用這個值開始訓練。我個人的習慣則是會設置一個在這個值周圍的Scheduler,例如我個人的策略就會是,下降速度稍微慢一點沒關係,畢竟大多的情況通常都是必須要decay learning rate才比較有好結果,因此我會稍微再縮小一點,然後再下調某個range,用週期型的learning rate進行訓練。

所以我這邊使用的是CosineAnnealingLR,從0.001開始,然後最小值到0.00003,具體變化如下圖:

那麼接下來就讓我們進入訓練環節,明天來收菜吧!

本日小節

  • 介紹Learning Rate Finder
  • 介紹這次訓練會採用的參數

[Day18] optimizer and learning rate

[Day18] Optimizer and learning Rate

前言

經過前面的文章,目前在單一個訓練的節點內,能做的優化已經做的差不多了。接下來大概就是訓練次數以及一些超參數的調整。讓我們先回到一組很基礎但又很重要的超參數「Learning Rate」&「Optimizer」。

Backward-Propagation

讓我們首先回想一下,所謂的「用資料訓練模型」,具體而言到是作什麼事情?以大多的Deep Learning模型來說,不外乎就是在Backward-Propagation的大框架下,利用資料產生梯度,然後更新模型參數,接著再驗證準確度,直至有滿意(或可接受)的準確為止。

在這樣的框架底下,Learning Rate與Optimizer就有點男女主角的味道在。大致上每一個更新參數的Step可以寫作下面的式子:

$$ w_t = w_{t-1} - \alpha_t \times J(g_t, w_{t-1}), $$

其中

$$ w_t : t\text{-th step wieghts}, \quad \alpha_t: t\text{-th step learning rate},\quad J : \text{optimizer} $$

根據上面的式子,Learning Rate好比是,每一步(每次迭代)步伐的大小。而Optimizer則是,每一步走路的pattern。

Optimizer

一般大多關於Optimizer的文章及實驗結果都還是根據某一個情況實驗下得到的結果,暫且沒有最佳解,也因此不少人也會花時間在上面進行調整。比較有人用的optimizer,基本上在Pytorch裡頭都有實作,具體可以參考torch.optim

而Optimizer事實上非常難仔細描述,想視覺化理解的話,可以參考Alec Radford大神當年在reddit文章po的圖:

看能不能感知出些什麼?

在不少探討Optimizer到底是幹了些什麼事的文章中,其中滿經典的是The Marginal Value of Adaptive Gradient Methods in Machine Learning,主要在討論adaptive家族的optimizer(像是 AdaGrad、RMSProp、以及最常見的Adam)比起傳統的SGD,範化性(generalization)的能力差很多,算是值得一看,下圖是裡頭的實驗結果:

現在Adam由於其快速收斂的特性,仍是最受歡迎的演算法。不過也因為上述說的這個性質,曾聽說過有團隊的工作方式是先用Adam做實驗,產品化的模型則利用SGD再產出。

Learning Rate

Learning Rate也是一項藝術,太小不動,太大則亂飄甚至爆炸。概念上大概可以參考下圖(來源傳送門):

但大部分的情況,你問我要怎麼挑,老實說我也不知道,很多時候其實都是用1e-3或1e-4這種magic number開始慢慢tune,或是另外一種滿實際的方法是去找類似資料的文獻,參考他們的超參數選擇也是很好的出發點。另外也有一種自動挑選Learning Rate的方式,會在明日的文章做一起實作。

Learning Rate Scheduler

再者,即使挑到了好的初始Learning Rate,其實也很難確保後續是不是真的表現一樣好?由此因應而生的便是Learning Rate Scheduler,藉由在訓練的途中對learning rate進行變化,進而達到比較理想的訓練結果。不同的Scheduler可以參考下面這張圖(來源傳送門):

跟optimizer一樣,比較熱門的Pytorch都有實作,可參考torch.optim.lr_scheduler

本日小節

  • 簡介optimizer與learning rate
  • 簡介learning rate scheduler

[Day13] 資料預處理機制與優化 - 硬體篇

[Day13] 資料預處理機制與優化 - 硬體篇

前言

本日的文章打算來講一些資料讀取的機制以及優化的方式。

資料流與硬體

先上一個目前所採用的Training/Validation,在每一次進行時的資料流簡易流程圖。

flowchart LR
    subgraph Main[ ]
        CPU <-->|preprocess| RAM
    end
    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
  Storage --> Main --> GPU

主要的流程,大概可以分為以下幾個步驟:

  1. 影像檔從HDD(硬碟)讀進RAM (通常還會包含image decode)
  2. 在CPU與RAM之間進行資料前處理
  3. 把處理後的資料送到GPU Memory
  4. 利用已經送到GPU Memory內的資料,使用GPU Cores進行模型的運算

不同的硬體在其中扮演的角色可以得到不同的優化:

  • Storage的效能影響1. (e.g. SSD > HDD)
  • CPU與RAM的效能影響2. (e.g. DDR4 > DDR3)
  • GPU接口的支援規格影響3. (e.g. NV-LINK > PCIE 4.0 > PCIE 3.0)
  • GPU的效能影響4. (e.g. RTX 3090 > GTX 1080)

因此,硬體的選用($$$$$$$)與實驗的效率,有很直接的關聯!但卻也不是唯一。

各硬體間的速度

接下來讓我們一項一項的介紹以及給一些例子:

  • Storage: 所謂的硬碟,種類的差異跟速度差很大 參考
    • HDD (Hard Disk Drive) : 或指傳統的機械硬碟,讀取大約落在50M - 150M/s,各家有一些不同的機制會有各自的優化方式。
    • SATA SSD (Solid State Drive):以SATA為傳輸界面的SSD,速度一般落在 500M/s左右。
    • NVMe SSD:以PCIE作為傳輸界面的SSD,速度可達2000M/s
  • CPU(Central Processing Unit):就是**處理器,一般前處理包含image的decode跟各種padding轉換等,大部分都在這裡做(也有部份處理可以在GPU內直接做),CPU的水非常深,不適合做過多著墨。跟做機械學習有關,需要注意的是的點如下:
    • 架構:最常見的主要分x64跟arm,而x64又分intel跟AMD。主要差距在底層指令集的支援以及一些基礎的開發函式庫的選擇上。
      • 例如,APPLE的M1就是屬於ARM,所以有一些套件在換了新版的MACBOOK以後發現安裝不是很順遂就是屬於這種架構上的差異。
    • 時脈:跑單一程式時的時脈,正常情況是數值越高越快。
      • 例如,Intel Core i9-12900 2.4 GHz > Intel Core i7-12700 2.1 GHz
    • 核心數/執行緒數:一般而言是時脈跟執行緒數越多越好,表示可以同時跑類似功能的程式。
      • 例如,Intel Core i9-12900 6 Cores, 24 Threads > Intel Core i7-12700 12 Cores, 20 Threads
  • RAM(Random Access Memory):所謂電腦當中的主記憶體,又分:
    • 空間:主記憶體的大小,資料從硬碟decode到進入gpu前一般就都是放在這裡,通常是越大越好。
    • 速度:單位時間處理資料的速度。
      • 例如:DDR4 5100M/S > DDR3 1600M/s
  • GPU:就是顯示卡,模型運算就靠這個,以最廣泛被使用的NVIDIA來說:
    • Cuda Cores : NVIDIA GPU的計算核心,這其實就是gpu適合做Deep Learning的關鍵,由於計算單元很多,比起cpu更適合做大量平行運算的DNN模型,通常越多越好越快,新的架構一般也會比舊的架構快。
    • GPU Memory : GPU自己的記憶體,基本上都要放在這裡才能讓GPU進行計算。通常越大表示模型參數可以越多,Batch Size也可越大。
    • GPU interface : GPU與電腦之前的界面規格,通常不同的規格決定與RAM之間交換資料的速度上限。
      • 例如: NVIDIA自家的NVLink Gen 2 300 GB/s > PCIE 4.0 x4 8GB/s > PCIE 3.0 x4 4GB/s

所以咧?

講這麼多,嘴巴都是泡。其實不外乎就是就是希望做Deep Learning Engineering的人,也可以對所使用的硬體們有更多的認識,進而知道可能會遇到的問題並進一步去解決。

綜合上面所說,舉一些實際可能發生的狀況:

  • 選用了50MB/s的HDD,資料很多,訓練的時候大部分的時間,GPU都在等待I/O結束送東西過來。
  • CPU很慢,資料前處理花太多時間,GPU在等前處理。
  • 模型框架選用太大,GPU記憶體根本塞不下或是Cuda Cores無法在合理的時間內訓練出成果。

以上都是很常見的情形,有了一些硬體的先備知識以後,可以幫助我們解決一些軟體無法解決的問題!

本日小節

  • 寫一寫突然覺得該介紹一下硬體就寫了
  • 個人很rough的介紹,有錯請指正
  • 看不懂只想跑code的可以看下一篇

[Day10] Pytorch-Lightning

[Day10] Pytorch Lightning

前言

本日將簡單介紹Pytorch-Lightning,而在包含今日的未來幾天內,會將先前構築的程式碼,分段整合成Pytorch-Lightning的格式。本日的部份是forward propogation的部份。

Pytorch Lightning 是什麼?

Pytorch Lightning是一個標榜同時可以簡化工程作業量,又同時具備高擴充性的Pytorch相容框架。
其與Pytorch的關係,有點類似TensorFlowKeras

(註:筆者本人也用過一陣子Keras跟TensorFlow 2,Keras的操作更加簡易,但是Flexibility就不太令人滿意,尤其是要客製一些框架或是訓練策略的時候,反倒是TensorFlow 2還順手一些)

主要的概念跟作法,可以直接參考下列這個來自Pytorch Lightning官方文件 LIGHTNING IN 15 MINUTES的簡介影片:

為什麼會需要 Pytorch Lightning?

簡單看完影片以後,相信大概能有個概念。現在來舉一個最簡單的例子,讓我們先來回顧前幾日的train.py裡頭每個epoch的training跟validation是怎麼做的?

Training Phase:

inputs, labels = batch['img'].to(device), batch['labels'].float().to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()
epoch_loss += loss.item()

Validation Phase

for batch in pbar:
    step += 1
    val_images, val_labels =  batch['img'].to(device), batch['labels'].to(device)
    y_pred = torch.cat([y_pred, model(val_images)], dim=0)
    y = torch.cat([y, val_labels], dim=0)
    pbar.set_description('Validating ...')
y_prob = torch.nn.Sigmoid()(y_pred)
loss = loss_function(y_pred, y.float()).item()

有沒有發現驚人的重工之處?

基本上都是在forward propogation以後在計算loss,只是差在有沒有梯度下降的差異而已。而且其實重複的且類似但又不太一樣的程式碼,也是增加進行小修改時出錯的風險。

Lightning Module

Pytorch Lightning 裡最核心的api就屬 Lightning Module,只要把模型整合成這個物件,基本上就可以開啟 Pytorch Lightning內的各種強大的支援。

根據文件中的內容,可以透過這個api把上一段落內的training跟validation大致整成下面的架構如下:

import pytorch_lightning as pl

class MultiLabelsModel(pl.LightningModule):
    """
    Lightning Module of Multi-Labels Classification for ChestMNIST
    """
    def __init__(self, CONFIG):
        self.backbone = get_backbone(CONFIG)
        ... # 可以網羅各種的初始設定,通常我會把大部分的超參數
        ... # 還有一些實驗過程需要的額外物件放在這個地方

    def forward(self, x):
        y = self.backbone(x)  # model inference 的主體,使用很自由
        return y              # 不論是要加層,增加input或output都可以簡單實現

    def step(self, batch: Any):
        inputs, labels = batch['img'].to(self.device), batch['labels'].to(self.device)
        preds = self.forward(inputs)
        loss = self.loss_function(preds, labels.float())
        return inputs, preds, labels, loss

    def training_step(self, batch: Any, batch_idx: int):
        inputs, preds, labels, loss = self.step(batch)
        return loss

    def validation_step(self, batch: Any, batch_idx: int):
        inputs, preds, labels, loss = self.step(batch)
        return {
            'preds' : outputs,
            'labels' : labels
        }

    def validation_epoch_end(self, validation_step_outputs: List[Any]):
        preds = torch.cat([output['preds'] for output in validation_step_outputs], dim=0)
        labels = torch.cat([output['labels'] for output in validation_step_outputs], dim=0)
        probs = torch.nn.Sigmoid()(preds)
        
        # compute metrics and log
        acc_score = torchmetrics.functional.accuracy(probs, labels, mdmc_average = 'global')
        auc_score = monai.metrics.compute_roc_auc(probs, labels, average='macro')
    ...

透過呼叫共用的step,就可以讓分別對應的training_stepvalidation_step都能實現與原先相同的forward propogation。而要蒐集整個validation結果,進而計算accuracy與auc的部份,則可以在validation_epoch_end內,會自動將每個validation_step的output作為input輸入,就可以計算整個驗證集的指標了。

如此切割各個功能後,除了可讀性上比較好一些,要debug也會比較容易一些,可說是好處多多。後續還有許多設計檔的瑣碎工作需要做,就讓我們挪到後續幾天再來慢慢完成!

本日小節

  • 簡介 Pytorch Lightning
  • 介紹PyTorch的forward propogation整合成Lightning Module的形式

[Day08] Model Validation

[Day08] Model Validation with PyTorch

前言

前一日已經開始進行模型的訓練。本日將討論要如何確認或挑選訓練出來的模型是否真的好?真的朝著正確的方向在邁進呢?

過擬合 (Overfitting)

在訓練的過程當中,很多情況只要是Training Code沒有異常的bugs的情況底下,在訓練集上的loss通常只會不斷下降。

這是否表示,我訓練出來的模型正在不斷變好呢? 這個答案,你知、我知、獨眼龍也知道,當然是! 主因是在機械學習越來越發展以來,模型內的參數也隨之越來越多。在這種情況底下,模型很有可能會去把某些樣本或是巧合硬是記下來,換句人話就是模型把答案硬背下來了!(尤其是在Deep Learning時代下,參數又多,訓練集的樣本又每張都看過幾十幾百次的狀況下,更加的容易發生。)

上圖則是參考Wiki Overfitting條目當中的圖,其中綠色的線就是想表達一個過擬合的圖。

雖然它完全正確分出紅色藍色了,但我們事實上很害怕這樣的模型,如同上面所說的,它只是用極端的狀況去硬記訓練資料,進而在實際上無法套用到新的或實際的資料上。

驗證集(Validation Set)

為了去驗證我們的模型套用沒有學習過的資料時的效果,一般我們會保留一份資料用來檢查模型表現,這個資料子集通常我們就稱作為驗證集。(另外還有測試集,後續會再介紹。)

常見的具體實作,通常我們每次訓練模型到一個段落的時候,會使用當下的模型針對驗證集內的所有資料進行推論,並紀錄當下的模型在驗證集上各種metric的表現,進而評估模型的好壞。

以我們的Multi-Label Classification來說,我們最主要就是比較準確率(Accuracy)以及AUROC(Area Under the Receiver Operating Characteristic),這部份在TorchmetricsMONAI上都可以找到對應的函數可以使用。

本次的具體實作可以在每一個epoch的後面加上:

model.eval()
with torch.no_grad():
    y_pred = torch.tensor([], dtype=torch.float32, device=device)
    y = torch.tensor([], dtype=torch.long, device=device)
    pbar = tqdm.tqdm(data_generators['VALIDATION'], total = len(processed_datasets['VALIDATION']) // data_generators['VALIDATION'].batch_size)
    for batch in pbar:
        val_images, val_labels =  batch['img'].to(device), batch['labels'].to(device)
        y_pred = torch.cat([y_pred, model(val_images)], dim=0)
        y = torch.cat([y, val_labels], dim=0)
        pbar.set_description('Validating ...')
    y_prob = torch.nn.Sigmoid()(y_pred)
    loss = loss_function(y_pred, y.float()).item()
    acc_score = torchmetrics.functional.accuracy(y_prob, y, mdmc_average = 'global').item()
    auc_score = monai.metrics.compute_roc_auc(y_prob, y, average='macro').item()

這裡要注意幾個重要的小細節,分別是

  • model.eval():做推論的模式切換,沒有做的話像是Dropout或是Batch Normalization就會根據訓練的模式跑出不正確的結果。
  • with torch.no_grad():使用沒有梯度的模式進行運算,節省運算資源。

實作

新增了Validation以後的實作一樣放在Github對應的commit內,簡單執行,等待一下就可以得到結果:

# python src/train.py
----------
----------
epoch 24/25
Training Epoch 50/50train_loss: 0.2043:  98%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████▋  | 49/50 [00:31<00:00,  1.57it/s]
epoch 24 average loss: 0.1974
Validating ...: : 88it [00:10,  8.11it/s]                                                                                                                                                     
current epoch: 24 current loss : 0.1849 current AUC: 0.5837 current accuracy: 0.9489 best AUC: 0.5849 at epoch: 23
----------
epoch 25/25
Training Epoch 50/50train_loss: 0.2136:  98%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████▋  | 49/50 [00:29<00:00,  1.68it/s]
epoch 25 average loss: 0.1972
Validating ...: : 88it [00:11,  7.84it/s]                                                                                                                                                     
saved new best metric model
current epoch: 25 current loss : 0.1843 current AUC: 0.5866 current accuracy: 0.9490 best AUC: 0.5866 at epoch: 25
train completed, best_metric(AUC): 0.5866at epoch: 25

這裡可以注意到幾點分別為:

  • 最後的training loss / validation loss 分別是 0.2136 與 0.1972 算是還不錯,代表兩者都很接近,到第25個epoch都還沒有什麼嚴重的overfitting發生。
  • 準確率來到 0.9490,比起MedMNIST網站上還要高
  • 但是AUROC的部份,僅0.5866,跟MedMNIST上的0.778相差甚遠
  • 綜合上述兩點,大概可以了解到,目前模型大概就是偏保守的模型,大部分都預測0,所以準確率很高,但實際要抓到病徵的能力還遠遠不夠。

本日小節

  • 簡介Overfitting的概念
  • 加入驗證集進入訓練的環節
  • 模型有待加強!

[Day26] Data Augmentation

Day26 Data Augmentation

前言

本篇是 Regularization的第二篇,講得是DATA Augmentation。是一個非常常見的技巧!

什麼是影像增強(Data Augmentation)?

Data Augmentation是一個非常常用的技巧,大致上就是利用一些Computer Vision的技巧,對影像進行小幅度的修改,確保修改後的影像仍能具有原先希望辨別的特徵,以期望模型即使在圖片加入這些變化間,仍能具有判別影像特徵的能力。

具體實例可以參考albumentation函式庫中的展示圖:

大致上的重點便是同一張圖經過變化以後,還是要能看得出他是一隻鸚鵡(試想,如果連你都看不太懂了,要什麼都不會的模型學習是不是太強人所難了呢?),而我們同時也可使用不同的處理方法來疊合,創造出千變萬化的圖片出來讓模型學習!

這個方法同時也非常常見於資料量不大的專案中。但別忘了他的本質仍是一種Regularization,實際上資料量不大的模型訓練中,效果不好的結果,主要的問題是來自於小型的訓練集無法代表整體樣本,因此收斂於小樣本的模型,自然無法適應到整個宏觀世界。此時Data Augmentation的介入,便是盡可能的透過讓資料更接近整個宏觀世界的方式,來去增加模型訓練時收斂的難度,進而去讓模型能有更好的generalization error。

但這東西卻也不是加越多越好,它一樣是個需要tune的超參數組。常見的謬誤有做了過多不合理的變化,使得訓練資料變得已經有點過度扭曲,已經脫離宏觀世界的長相時(宏觀世界的借鑒則是驗證集及測試集),類似像下面這張圖(來源),有一張貓貓的頭已經完全被切掉了,這種難以辨識的東西如果一兩百張有1-2張還沒什麼關係,但太多可就不好了,所以實際也要注意一下。

另外補充一下,還有一個借鑒則是Data Augmentation也要符合資料與domain的知識。例如醫療影像中,由於人體骨頭的生理結構不會差異太大,不宜做過度的扭曲,如果跑出個斜的頭骨那還真不知道該如何是好?

推薦的函式庫

這邊推薦幾個我個人比較常用的函式庫:

  • torchvision
    • torch本家的cv函式庫,基本上簡單強大好用。
  • albumentation
    • 很熱門的Data Augmentation函式庫,各種神奇的處理很多,都可以試試看,但有些處理很特殊,可能會花很多時間進行前處理,要稍微注意一下效率。
  • MONAI
    • 本單元的老朋友,特點是可以支援2d、3d,而且很多專屬於醫療影像用的Augmentation。
  • TorchIO
    • 專門處理3d醫療影像的函式庫,有需要也可以試試看。

另外如果要把非monai的Augmentation整合起來一起使用的話,可以善用monai.transforms.Lambdad

實作

實作基本上不難,基本上一樣是準備一組monai.transforms

def prepare_data_aug(CONFIG: Dict) -> monai.transforms.transform:
    
    prob = CONFIG['train']['data_aug']['prob']
    transforms_data_aug = [
        monai.transforms.RandAffined(keys = ['img'], 
                                     prob=prob, 
                                     rotate_range = [-0.1, 0.1], 
                                     translate_range = [-3, 3], 
                                     scale_range = 0.1),
        monai.transforms.RandGaussianNoised(keys = ['img'], 
                                            prob=prob, 
                                            mean=0.0, std=0.1),
        monai.transforms.RandFlipd(keys = ['img'],
                                   prob=prob, 
                                   spatial_axis = [0])]
    
    transforms_data_aug = monai.transforms.Compose(transforms_data_aug)
    
    return transforms_data_aug

然後在做成dataloader前,套上dataset就可以了!

transforms_data_aug = prepare_data_aug(CONFIG)
processed_datasets['TRAIN'] = monai.data.Dataset(data = processed_datasets['TRAIN'], 
                                                 transform = transforms_data_aug)
  • 這邊注意一點是,通常不建議把Augmentation也套到validation或是test上,通常會造成很多不必要的變數,也會造成挑選模型的不一致性。

然後我們可以試跑看看,一張圖片可能有的變化性:

  • 第一個affine基本上就是各種非破壞結構的形狀變化,可詳見Affine transformation,醫學影像上十分常用(另外可以延伸查一下non-rigid的意思)
  • 有任何牽扯到補值的,醫療影像建議可以使用直接pad zeros的方式,你總不會想多補一塊肋骨進來吧...?
  • 顆粒應該就是高斯模糊
  • 看起來還行?

實作結果

一樣參考這個commit訓練,讓我們直接看結果:

增加了一點幅度的訓練難度,一開始上升的比較少一些。但並沒有脫離宏觀世界太多,一樣是獲得了差不多0.76左右的auroc結果!

本日小節

  • 介紹與實作Data Augmentation
  • 介紹幾個不錯的函式庫
  • 科普一下醫學影像的Data Augmentation

[Day30] 結語:回歸初心,資料是一切的根本

[Day30] 結語:回歸初心,資料是一切的根本

前言

滿多預計要討論的其他Task最後都沒有實作到,居然意外變成是純粹以Classification為例的深度學習實作範例了!在此作為系列篇的最後一篇,打算分享一下這段時間作Deep Learning的一些心得及想法。

勿過度沉迷於技術

其實這個小標同時也是在告誡自己,大家可以看到在[Day21]當中,其實我們的AUROC已經達到0.731,距離我們最後的0.772其實也僅是提升了0.041。但這0.041我們卻使用了大約構築第一次實驗結果的好幾倍時間來進行實驗才得到。

先分享一個非常寫實、也讓人又氣又好笑的深度學習迷因(出處):

身為一個理工宅出身的人,我個人認為深度學習的技術是迷人的。有趣的數學性質、各種實驗的曲線、以及加入各種調整以後的改善結果,那種「我再試一些超參數是不是就會變好」的心態,真的很可怕!我覺得裡面應該已經有一些多巴胺陷阱的成分。不過總歸來說,一切仍需要適度而止。尤其是不要陷入把Test Set當作另外一個Validation Set來不斷優化的無底洞陷阱中,一來是能改善的幅度有限,二來則是所見也可能並非是真正的改善。

那麼,這些操作,究竟值不值得呢?我的回答是:要看目標

回歸初心,專注目標

目標的話,個人的經驗目前可以粗分成兩種:

  • 研究型:通常研究型的問題,大多的情況比較會是追求某一個單點的突破。例如更加符合domain的損失函數、更加精良或高效的網路框架、或是更加高效的迭代方式諸如此類。通常是建議需要先釐清自己的研究目標,在單個點上作討論,不然東混西混的情況下,實驗與問題往往會無法收斂。
    • 例如:想證明A LOSS比B LOSS好,但卻花費大量經歷在那邊tune learning rate或是optimizer,卻也只好一些。殊不知很可能實際上A就是跟B差不多。
  • 商業型:商業型的本質其實更加單純,說穿了就是錢。什麼模型(或模型的組合)在可用的時間底下,效益能最好。能在考量到時間金錢成本的狀況下,要進行資源的配置。
    • 例如:一般而言,資源配置是PM的工作居多。但如果RD完全沒有意識到成本概念,只覺得PM根本不懂技術在亂搞,然後又剛好碰到PM本身技術底又不夠,只覺得RD不懂得捉大放小時,兩者的溝通根本是災難一場。
    • 例如:如果目標是半年內要有產品的Prototype,其產品需要A B C三個模型,某A模型已經達到95%的準確率了。究竟要追求更高的A還是花時間去作B與C?我的建議會是要評估。
      • 如果類似A模型的準確度在已知的文獻或人類表現極限差不多就是95%,且這個A對於最終服務的影響不致於大影響時,就不宜在投入過多成本。應該把成本優先完成B、C,打造雛形。
      • 如果A模型是產品的本身最主要的關鍵,例如提升1%可以為公司本業多創造多少收益的情況下,就十分適合投入時間持續進行研發。(當然本身的可行性還是要評估,而不是永無止盡的研發下去)
    • 千萬要避免的狀況是B、C都還沒有個底,在有限的成本底下還把資源投入到已經差不多到極限的A上。

資料是一切的根本

深度學習或是機械學習,最重要的一點其實是後面共通的那兩個字「學習」(Learning)。向什麼學習?資料(DATA)

由於很多技術實施者大多都是從被整理好的公開資料集開始進行學習,因此這一點其實很常被遺忘。整個系列的MedMNIST其實就屬於這類已經經過大量資料清理、驗證的標準資料集,而且又有模型預測的標準可供參考,供學習者能專注在演算法的學習與開發上。

但大多的AI專案做不出來,其實問題不外乎都是出現在根本的資料問題上,而這個問題又可以分成幾個層次討論,我這邊依嚴重度高到低依序討論:

  1. 實行根據不清楚的AI任務,例如,想做某種標籤的預測,但這個標籤,實際連給標籤的人都沒有定義上的共識就進入讓模型學習的流程,可想而知,連人都搞不懂,模型也很難懂。基本上我個人會稱謂這種是奇蹟型的開發專案。
  2. 驗收標準不清楚的AI任務,例如,想做某個疾病或是某個產線上瑕疵發生的預測或自動判斷,經過開發人員的努力可能已經有85%的成效了(可能是AUC、準確度之類的某個驗收指標),但團隊(包含老闆)沒有足夠的共識,只會一昧的追求表現更好,但殊不知模型的表現可能已經接近人類專家的表現能力,不太可能有繼續提高的機會。
  3. 定義清楚,但資料不足卻不自知的AI任務。例如,某案子目標與驗收標準都很明確,但團隊沉溺於用諸多技術在有限資料上的改進,殊不知根本問題其實只是資料樣本不夠大,不足以應付宏觀世界導致。這類屬於最輕的,利用適合的成本增加資料量即可!

以上大概是綜觀個人在職場上幾年的一些小心得。

在系列文的最後,感謝細心讀到最後一篇的觀眾們,如果有什麼想法也都歡迎在討論串提出來互相討論,或是也可以mail到我個人信箱進行交流。

系列文的Github連結

本系列文的實作一律都放在 https://github.com/SraRod/iThome2022
有興趣的讀者可以直接上去抓下來實作,有什麼issue也都可以直接在上面提出來。在此感謝你的閱讀!

[Day17] GPU optimization

[Day17] Optimizations for GPU computation

前言

前幾日的文章,討論到了在訓練過程中,從資料的I/O、前處理到放入GPU之前的優化方式。今天將討論在GPU內的優化方式。

自動混和精度 - automatic mixed precision

我們在先前的訓練時,預設的狀況下,從讀取資料到模型的訓練以及驗證,全部的數值都是使用float32(單精度浮點數,可參考wiki)。

數值部份的資料格式,除了最常見的float32以外,其實還存在很多不同的種,例如:只有01的Boolean、整數常用的Int16以及今天要討論到的主角-----大小只有Float32一半的半精度浮點數Flost16

  • 大小只有一半的意思是,例如同樣是0這個數值,如果以float32儲存的話,大小是32bits;若是使用float16儲存,則只佔16bits
  • float16的缺點則是因為容量較小,所以能描述的精準度較差,因此通常小數點的加減乘除起來的誤差也會較大,且值最大也只能容納到65519,超過則會被定義維為infinity。

所謂的AMP(automatic mixed precision,即自動混和精度),就是一種利用相同數值能以不同格式儲存所帶來的記憶體容量差異優勢來進行訓練的技術。更詳細的介紹可以觀看這篇文章,這裡截一張裡頭的重點圖:

其運作方式大致上是:

  1. 以Float16進行forward
    • 中間的各層衍生物由原本的Float32改以Float16儲存,因此節省大量的記憶體
  2. 透過loss以及backward計算出Float16梯度
  3. 將梯度轉換回Float32
    • 這裡有滿多有趣的問題要處理,例如精準度不高,容易產生梯度消失,對應的作法會放大梯度
    • FLOAT16的最大值太小(只有65519),容易產生梯度爆炸,對應的作法則是會把梯度進行clip
  4. 最後再以梯度更新回原始的float32格式,再進行權重的更新

其他一些加速的方式

除了今天主要介紹的AMP以外,還有一個個人覺得比較有感的加速則是Cudnn的Benchmark Mode。

詳細內容可以參考這篇討論,主要是由於cudnn內其實有多套不同的Convolution演算法實作,開啟這個功能主要是訓練時,cudnn會去自動調整針對當下tensor大小的最快最適合的演算法,來藉以加速。

實際實作很簡單,在pytroch裡面只要加上:

torch.backends.cudnn.benchmark = True

而Pytorch-Lightning則只要在Trainer內加上:

trainer = pl.Trainer(...,
                     benchmark = True)

就可以開啟這項功能了。

實際差異

現在讓我們實際來比較差異,這次主要針對有無AMP跟有無開啟CUDNN benchmark來做四組的比較:

左圖為訓練時的使用記憶體變化量,右圖是整體的計算時間

大概可以歸納出下列幾點:

  • 純論GPU節省量來說,單開AMP最省
  • 開啟CUDNN benchmark會比沒開來得快一些,但會多耗費一些記憶體(推估是為了記錄不同演算法的表現,所以需要額外資源)
  • 開啟AMP反而計算比沒有開啟還要來得慢
    • 根據此篇,似乎在Volta框架後,nvidia有針對float32的計算特別硬體架構上的優化設計,因此已經加速到跟float16沒有什麼差別
    • 筆者使用的GPU是下一代ampere的3080TI,所以可能因此使用起來速度沒有太大差,反而多了float32、float16間的轉換而增加了計算時間

結語

  • 介紹幾項優化GPU計算的方法
  • 後續的實驗應該會採用 FLOAT32 + CUDNN BENCHMARK MODE 或是 AMP + 加大batch size的方式進行實驗

[Day 5] Dataloader with PyTorch and MONAI

[Day05] Dataloader with PyTorch and MONAI

flowchart LR

Image --> Reader --> preprocess[all kinds of preprocess] --> tensor --> training/testing/serving

前言

在進行模型訓練之前,通常需要透過一連串的行為,將資料轉換成適合訓練的格式。前一日的文章構建了長資料來管理資料,本日的文章將透過PyTorch與MONAI來進行前處理。

Data Preprocess

一般來說資料準備好,要進入模型之前大致上會經歷以下流程:

這邊也分享一下每一個環節的大觀念:

  • Reader:讀檔案,也就是decode。選對正確的decoder至關重要。
  • all kinds of preprocess: 各式各樣的前處理。很多模型能做好的,關鍵通常都在這裡加入重要的Domain know how,或是有一些天馬行空式的突破。
  • Tensor:轉化成Tensor,通常就是轉化成pytorch或是tensorflow之類框架所使用的tensor,進行該框架底下的運算。不見得必要,依照框架甚至版本決定。

在monai裡頭,我們可以寫成像是下面這樣一個function的list,之後直接以pipeline的形式對所有資料作完整的前處理:

import monai

transforms = [
    monai.transforms.LoadImageD(keys = ['img']),
    monai.transforms.EnsureChannelFirstD(keys = ['img']),
    monai.transforms.ScaleIntensityD(keys = ['img']),
    monai.transforms.ToTensorD(keys = ['img'])
]

transforms = monai.transforms.Compose(transforms)

以上述的函式例子,我們假設每個進來的資料點都是一個dict(這些都是屬於monai.transfroms函數的DICT版,才會每個函數名稱後面都有一個D)。

而這個transforms則是可以針對每個dict裡頭img這個key,依序進行decode、調整channel、正規化pixel value到[0, 1]以及轉換成tensor。

Data Loader with Dictionary

有了前處理的函數以後,接著就可以讀取我們前一日進行的資料進來,接著轉換成list of dicts:

SPLITS = ['TRAIN', 'VALIDATION', 'TEST']
df = pd.read_csv('data/dataset.csv')
datasets = {split : df[df['split'] == split].to_dict('records') for split in SPLITS}

再下來,把資料點們跟上一段準備好的transforms進行結合成一個monai的dataset物件:

processed_datasets = {
    split : monai.data.Dataset(data = datasets[split], transform = transforms)    
    for split in SPLITS
}

最後,為dataset建立PyTorch的dataloader:

data_generators = {
    split : torch.utils.data.DataLoader(processed_datasets[split],
                                        batch_size = BATCH_SIZE,
                                        shuffle = True,
                                        collate_fn = monai.data.utils.pad_list_data_collate,
                                        pin_memory=torch.cuda.is_available())
    for split in SPLITS
}

接著就大功告成了!

補充:

  1. 通常不同split的dataloader是獨立的,以免混到。因此我用一堆dict的方式來進行管理,此為個人習慣。
  2. collate_fn = monai.data.utils.pad_list_data_collate 是個很有趣的東西,他可以幫你把各種前處理以後如果有少掉大小的圖檔pad成一致的大小,避免在製造batch的時候不會出錯。

Sampling

接著根據上述三個dataloader,可以簡單進行sampling並製圖:

看起來還不錯,詳細的產生方式可以參考這個檔案,一樣使用

python src/preprocess.py

就能得到結果。

本日小節

  • 以monai建立的transforms進行資料前處理
  • 視覺化處理後的影像(看起來沒什麼太大意外)

[Day06] Design a Model

[Day06] Design a Model

flowchart LR

Image --> Model
Model --> P1("Prob(atelectasis)")
Model --> P2("Prob(cardiomegaly)")
Model --> P3("...")
Model --> P4("Prob(hernia)")

前言

基礎的資料生成模組已經有了,那接下來今天會介紹基礎的模型定義方式。

模型的根本--任務(task)

在設計模型之前,要先體認到最根本的問題:我們要執行怎麼樣的任務?

根據我們前幾天資料的介紹,可以得知ChestMNIST是一項具有下列特徵的資料集:

  • 每一個資料點是一張胸腔X光影像
  • 每一張影像的解析度都是28x28
  • 由於是灰階,影像的channel是1 (相對於常見的rgb就是3)
  • Outcome的部份,則是分別預測影像之中是否存在給定的14個不同的症狀(且可能存在多個)

根據以上,可以進行一個基礎的設計來達成這個目標:

一個image in,然後會output出14個對應症狀機率值的模型。

design a backbone

這邊採用最常見的摺積神經網路(Convolutional Neural Network, CNN/ConvNet)作為開發,骨幹(backbone)的部份則是選取近年來很紅很高效的Efficient Net,是一個Google爸爸花了很多錢燒出來的框架,要好好的站在巨人的肩膀上。

現在很多地方都有 Efficient Net的實作可以直接採用,也包括MONAI。

這邊我們修改最小的B0來做使用。不過首先會發現一件問題是B0的設計是設計給ImageNet使用的,因此輸入的維度是常見的224x224:

>>> import monai
>>> monai.networks.nets.efficientnet.efficientnet_params['efficientnet-b0']
(1.0, 1.0, 224, 0.2, 0.2)

於是我們這裡稍微做一個小修改後再建立骨幹,以確保整個骨幹能夠正常運行:

monai.networks.nets.efficientnet.efficientnet_params['efficientnet-b0'] = (1.0, 1.0, 28, 0.2, 0.2)
model = monai.networks.nets.EfficientNetBN('efficientnet-b0', 
                                           spatial_dims = 2, # 表示2D 或 3D影像
                                           in_channels = 1, # 採用灰階 所以設定1
                                           num_classes = 14) # 14個不同的症狀

測試模型

這邊介紹一個好用的工具TorchInfo,可以用來觀看模型的各階層:

>>> import torchinfo
>>> torchinfo.summary(model, input_size=(16,1,28,28))
====================================================================================================
Layer (type:depth-idx)                             Output Shape              Param #
====================================================================================================
EfficientNetBN                                     [16, 14]                  --
├─ConstantPad2d: 1-1                               [16, 1, 29, 29]           --
├─Conv2d: 1-2                                      [16, 32, 14, 14]          288
├─BatchNorm2d: 1-3                                 [16, 32, 14, 14]          64
├─MemoryEfficientSwish: 1-4                        [16, 32, 14, 14]          --
├─Sequential: 1-5                                  [16, 320, 1, 1]           --
│    └─Sequential: 2-1                             [16, 16, 14, 14]          --
│    │    └─MBConvBlock: 3-1                       [16, 16, 14, 14]          1,448
│    └─Sequential: 2-2                             [16, 24, 7, 7]            --
│    │    └─MBConvBlock: 3-2                       [16, 24, 7, 7]            6,004
│    │    └─MBConvBlock: 3-3                       [16, 24, 7, 7]            10,710
│    └─Sequential: 2-3                             [16, 40, 4, 4]            --
│    │    └─MBConvBlock: 3-4                       [16, 40, 4, 4]            15,350
│    │    └─MBConvBlock: 3-5                       [16, 40, 4, 4]            31,290
│    └─Sequential: 2-4                             [16, 80, 2, 2]            --
│    │    └─MBConvBlock: 3-6                       [16, 80, 2, 2]            37,130
│    │    └─MBConvBlock: 3-7                       [16, 80, 2, 2]            102,900
│    │    └─MBConvBlock: 3-8                       [16, 80, 2, 2]            102,900
│    └─Sequential: 2-5                             [16, 112, 2, 2]           --
│    │    └─MBConvBlock: 3-9                       [16, 112, 2, 2]           126,004
│    │    └─MBConvBlock: 3-10                      [16, 112, 2, 2]           208,572
│    │    └─MBConvBlock: 3-11                      [16, 112, 2, 2]           208,572
│    └─Sequential: 2-6                             [16, 192, 1, 1]           --
│    │    └─MBConvBlock: 3-12                      [16, 192, 1, 1]           262,492
│    │    └─MBConvBlock: 3-13                      [16, 192, 1, 1]           587,952
│    │    └─MBConvBlock: 3-14                      [16, 192, 1, 1]           587,952
│    │    └─MBConvBlock: 3-15                      [16, 192, 1, 1]           587,952
│    └─Sequential: 2-7                             [16, 320, 1, 1]           --
│    │    └─MBConvBlock: 3-16                      [16, 320, 1, 1]           717,232
├─Identity: 1-6                                    [16, 320, 1, 1]           --
├─Conv2d: 1-7                                      [16, 1280, 1, 1]          409,600
├─BatchNorm2d: 1-8                                 [16, 1280, 1, 1]          2,560
├─MemoryEfficientSwish: 1-9                        [16, 1280, 1, 1]          --
├─AdaptiveAvgPool2d: 1-10                          [16, 1280, 1, 1]          --
├─Dropout: 1-11                                    [16, 1280]                --
├─Linear: 1-12                                     [16, 14]                  17,934
====================================================================================================
Total params: 4,024,906
Trainable params: 4,024,906
Non-trainable params: 0
Total mult-adds (M): 128.06
====================================================================================================
Input size (MB): 0.05
Forward/backward pass size (MB): 31.48
Params size (MB): 16.10
Estimated Total Size (MB): 47.63
====================================================================================================

最後以Batch Size = 16來實際測試 Forward Propogation:

>>> device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
>>> model = model.to(device)
>>> test_input = torch.rand([16,1,28,28]).to(device)
>>> test_output = model(test_input)
>>> print(test_output.shape)
torch.Size([16, 14])

一切大功告成!

本日小節

  • 根據任務展開的簡單模型需求設計
  • 以Efficient-B0來實作

[Day22] Transfer learning

[Day22] Transfer learning

前言

從本日的文章開始的幾天內會介紹一些額外還沒介紹到的訓練技巧,今天會先從Transfer Learning開始。

什麼是Transfer Learning?

讓我們先參考wikipedia的定義

很好,十分的General。如果有一點的數學底子的朋友千萬不要覺得這麼general是在講什麼鬼?因為它就是這麼如此的General,所以利用Transfer Learning概念實作的方式可說是包山包海。

那讓我們說回人話,基本上就是有個主要的task與domain組合是T需要學習時,如果能利用到任何一個task與domain組合S的知識,就可說是遷移學習(Transfer Learning)。

有哪些例子

好吧!這聽起來還是很抽象。這邊舉兩個目前我實務上也比較常用到的例子:

  • Fine-tine with a pre-trained weights
    • 主要是利用一些在其他的地方已經利用一些龐大的資料集訓練好的模型作為基底(S)。接著用來訓練我們目前想要的模型(T)。
    • 以Computer Vision來說,很常見的就是利用ImageNet訓練好的模型當做Pretrained Weights,然後再拿手上需要建模的資料來訓練。這部份幾乎所有2D的框架,包括ResNet、EfficientNet都有Pretrained版本可以使用。
    • 以Natural Language Processing來說,就是各家大大做出來的Bert及變體都是很流行的Pretrained。(Transformer似乎也很流行)
  • Multi-Task Learning
    • 這個稍微比較不同的方式是,大多的情況是利用在訓練的時候,同一個資料點,具有不同的資料特徵來進行協同訓練。
    • 例如,判別貓貓狗狗的模型(S),同時還要判別動物們在的空間是屬於室內室外(T)。也就是同個模型的OUTPUT會有兩組,除了貓狗以外,還要判別室內室外。
      • 例如這是一張在室外的阿柴,來源
      • 這是一張在室內亂跳的貓貓,來源
    • 上述例子就可以在訓練的時候,獲得兩組loss(判別貓狗、判別室內外)來協同訓練,模型能作到的事情更多工,(或許)更接近人類一點。
    • 醫療影像上也很類似,一個專業的醫師,一張胸腔X光,除了能看到我們想訓練的任務中某個病徵以外,可能還可以看出這個人是否抽煙,是否受過外傷...等,利用這些資訊來協同訓練,也就是一種Multi-Task Learning。
    • 作法上通常比較客製,蒐集資料上成本也會高上許多,而訓練的參數通常也更難調校。但個人經驗上通常模型表現會更為穩健。

更多關於Transfer Learning,有興趣的朋友可以參考台大李宏毅老師的線上課程

Pre-trained Sources

那回到我們系列文的實作,這裡主要會使用ImageNet Pretrained Weight的Fine-tune為例。不過回到一個重點,要去哪裡找這些pre-trained呢?以下推薦幾個相容PyTorch的套件:

  • pytorch-image-models:提供非常多不同backbone,而且大多都有imagenet的pretrained。
  • MONAI:本系列文的老朋友,這裡的強像是提供不少3D影像的框架加上Pre-trained。
  • Hugging Face:近年竄起的強大AI社群,有非常多論文的實作,以前他們的pretrained 可以直接使用。

使用ImageNet Pretrained Weight為例

以我們系列文實驗的monai.networks.nets.EfficientNetBN來說,只要將pretrained = True就可使用imagenet的pretrained來進行fine-tune了!(具體請參考文件)

具體修改過的code可以參這個commit,跑完以後讓我們來參考一下wandb上的結果。

使用Pre-trained的模型是不是訓練成效更快,而且獲得了更好效果呢!
(另外第二個cosine效果不太好,可能就是這組超參數在後面不太適合pretrained了呢!)

本日小節

  • 介紹transfer learning
  • 比較使用pre-trained的成效

[Day07] Model Training with PyTorch

[Day07] Model Training with PyTorch

前言

本日將利用前一篇建立好的模型,結合先前的資料前處理,利用基礎的PyTorch語法,進行模型的訓練。

資料讀取

根據先前的文章,我們可以藉由以下語法,得到Pytorch的Data Loader:

# prepare dataset
df = pd.read_csv('data/dataset.csv')
datasets = {split : df[df['split'] == split].to_dict('records') for split in SPLITS}

transforms = preprocess.prepare_transform()

processed_datasets = {
    split : monai.data.Dataset(data = datasets[split], transform = transforms)    
    for split in SPLITS
}
data_generators = {
    split : torch.utils.data.DataLoader(processed_datasets[split],
                                        batch_size = BATCH_SIZE,
                                        shuffle = True,
                                        collate_fn = monai.data.utils.pad_list_data_collate,
                                        pin_memory=torch.cuda.is_available())
    for split in SPLITS
}

但從前一篇當中,我們預計模型的輸出會是14維的一個向量,因此會需要把每個label全部Concat起來。對應到transforms,就必須先做以下的修改,而當中的labels就會是我們想要預測的14個症狀的Ground Truth:

LABEL_LIST = ['atelectasis', 'cardiomegaly', 'effusion', 'infiltration', 'mass', 'nodule', 'pneumonia', 'pneumothorax', 'consolidation', 'edema', 'emphysema', 'fibrosis', 'pleural', 'hernia']

transforms = [
        monai.transforms.LoadImageD(keys = ['img']),
        monai.transforms.EnsureChannelFirstD(keys = ['img']),
        monai.transforms.ScaleIntensityD(keys = ['img']),
        monai.transforms.ToTensorD(keys = ['img'] + LABEL_LIST), # 把label們也轉換成tensor
        monai.transforms.AddChanneld(keys = LABEL_LIST), # 整理他們的維度
        monai.transforms.ConcatItemsd(keys = LABEL_LIST, name = 'labels'), # 將整理後維度後的label進行疊合
    ]

接著進行抽樣就可以得到:

>>> for batch in data_generators['TEST']:
>>>     break;
>>> batch['labels'].shape
(256, 14)

損失函數(LOSS FUNCTION)與優化器(optimizer)

由於我們針對14個症狀,每一個類別都是二元分類問題,因此我這裡採用Torch中的BCEWithLogitsLoss作為Loss Function。

這裡做一下簡介,主要是由Binary Cross Entropy (基本上,其實也等同是Binomial Distribution的log-likelihood函數,不過差個負號跟常數倍)
$$
L_{BCE}(x, y) = - \frac{1}{k} \sum_{}
w_n \left[ y_n \cdot \log \sigma(x_n)
+ (1 - y_n) \cdot \log (1 - \sigma(x_n)) \right]
$$
Sigmoid Function (大多時候,在統計內也稱為Logistic Function)
$$
Sigmoid(x) = \frac{1}{1+\exp^{-x}}
$$
所組成。

比起先對model的結果取Sigmoid再算BCE,放在一起的好處主要是有數值計算上的好處。(試想,分開等於先取exponetial再取log ...)另外,除了資料以外,損失函數幾乎是整個機械學習領域水最深的一塊,因此這邊就不再多做什麼贅述。

另外,optimizer的部份則選用常見於實驗中的Adam,好處是收斂快速,不過實務上有一些研究表示Generalization error比起其他慢的匯差一些就是。

結合在一起也很容易,輸入以下即可:

loss_function = torch.nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), 1e-3)

訓練模型

建立好以上以後,就可以利用PyTorch透過反向傳播法(back-propogation)對模型進行訓練,參考語法如下:

inputs, labels = batch['img'].to(device), batch['labels'].float().to(device)
optimizer.zero_grad() # 初始化這次傳播的梯度
outputs = model(inputs) # 進行forward propogation
loss = loss_function(outputs, labels) # 計算當前的loss
loss.backward() # 利用loss計算梯度
optimizer.step() # 利用這次算出來的梯度,對模型的weights進行更新

實作

一樣本次的實作有放在Github內,簡單執行就可以得到結果:

# python src/train.py
----------
epoch 1/1
1/515, train_loss: 1.8068
2/515, train_loss: 1.6626
3/515, train_loss: 1.5855
4/515, train_loss: 1.4236
5/515, train_loss: 1.4121
6/515, train_loss: 1.3223
7/515, train_loss: 1.2272
8/515, train_loss: 1.1468
9/515, train_loss: 1.0351
10/515, train_loss: 1.0378

簡單執行了10個step。很好,loss有在穩定下降,這代表模型是有在訓練集當中學習到東西的!

本日小節

  • 調整一下先前的transforms 來符合模型的設計
  • 挑選loss以及optimizer來使用
  • 初步進行訓練,得到了loss可以下降的結果

[Day09] Deep Learning with Configuration

[Day09] Deep Learning with Configuration

前言

本日要介紹的是透過Configuration的方式來管理Deep Learning中的超參數。

超參數(Hyperparameters)

在前面的實作中,我們目前已經可以進行完整的資料前處理、模型訓練以及模型驗證。
在後續模型表現性的調教中,超參數的調整是一種很常見方式,也是十分多的學術論文在研究探討的議題。

那麼,什麼是超參數呢?既然有個超,是不是也有參數(parameters)?以下給一些簡要的介紹:

  • 參數:指那些可以透過演算法的途中,能被更新、或說被學習出來的數值。例如Model weights就是很顯而易見的參數。或給一個最簡單的例子,下面線性回歸式子中的$\beta_1$與$\beta_0$就是個參數,而我們通常用Least Square或是Maximum likelihood去找到最接近符合資料模樣的參數估計。
    $$
    y = \beta_1 x + \beta_0
    $$
  • 超參數:通常泛指那些不同於可以從資料中被演算法學習出來的參數,需要透過人工設定的方式所指定的那些數值。

那我們先前的實作中,已經出現了哪些超參數呢?以下為大家簡介:

  • 有進行變數命名的部份
    • BATCH_SIZE : 每批次進行梯度下降的影像數量
    • MAX_EPOCHS : 實驗最多進行幾次 epoch
    • STEPS_IN_EPOCH : 每個epoch進行幾次迭代(通常不設定就是預設完整掃完Training set一次)
  • 沒有進行命名,目前寫死的部份
    • 模型框架 (modified efficient-B0):這裡舉凡骨幹的選擇、各個layer以及是否透過pre-trained weights進行遷移學習(Transfer Learning)都算,可以細部調整的東西可說包山包海。
    • 優化器(optimizer = ADAM)
    • 學習率(learning rate = 1e-4)
    • 驗證模型的指標(Metric = AUROC)

... 不勝枚舉。

上述這些都是超參數的一部分,也因此管理好超參數,事實上就是深度學習開發當中很重要的一項工作。

Configuration File

個人則習慣透過一個YAML的設定檔,來對整個開發工作管理。其中除了超參數以外,也會用來管理一些整套程式碼運行途中需要用到的變數。

yaml檔的設定可以設定成這樣,第一層是功能層,然後才是變數層:

base:
  project: 'iThome2022'
  
make_dataset:
  dataset_from: 'chestmnist'
  tmp_folder: '/tmp'
  save_folder: 'data'
  img_subfilename: 'png'
  dataset_file: 'data/dataset.csv'

train:
  backbone: 'efficientnet-b0'
  batch_size: 128
  max_epochs: 25
  steps_in_epoch: 50
  data_loader:
    num_workers: 8
    prefetch_factor: 166
  loss_function: 'BCEWithLogitsLoss'
  optimizer: 'Adam'
  learning_rate: 0.0001

具體變更可以參考這個commit

實作

接著只要在新的commit底下,加一行使用設定好的hparams.yaml,就可以開始train model了:

# python src/train.py --config=hparams.yaml
----------
epoch 1/25
Training Epoch 14/50 train_loss: 1.8244:  28%|█▋    | 14/50 [00:14<00:28,  1.28it/s]

本日小節

  • 簡介模型中的參數與超參數
  • 利用yaml管理超參數及相關變數

[Day20] 實際訓練與結果分析

[Day20] 實際訓練與結果分析

前言

根據前一篇的Learning Rate設定後,實際進行了訓練。獲得了結果後,今天的文章將實際解釋實驗產出。

實驗設定

除了上一篇最後提及的CosineAnnealingLR以外,這裡也額外進行了兩個實驗進行對比,具體Learning Rate,隨著訓練過程的變化可以參考下圖:

optimizer一樣都使用Adam,依序分別是

  • learning rate = 0.0058 (lr finder suggestion)
  • learning rate = 0.0058 with ReduceLROnPlateau (monitor on val/auroc),大概在2xk-th step後就降到5.8e-6,最後跑到5.8e-9,非常的小
  • learning rate = 0.001 with CosineAnnealingLR between (0.001, 0.00003)

實驗結果

Machine Learning大多的方法都是圍繞在Loss的優化上,因此廢話不多說,先上loss圖:

看圖時大概可以搭配下列幾點服用:

  • 同顏色是相同的實驗,實線是Train、虛線是Val
  • 基本上每種顏色的pattern都很接近,從這邊可以推斷
    1. Train與Val沒有分佈基本上是相近的
    2. 模型確實都共同在Train裡面學到東西了,val也都一起跟著下降
  • 大約一開始在10k-th step以前,是lr_finder找到的0.0058的那兩個顏色對應的顏色,loss下降的比較快
  • 從頭到尾learning rate=0.0058的那組,初期下降快速,但後期基本上就卡住了。這種情況很可能是到後面的時候,Learning rate在那個時間點已經屬於偏大,無法找到比較好的點。
  • 搭配使用ReduceLROnPlateau的那組,下降的效率最好,基本上不到一半的歷程就已經得到不錯的結果。但後面無法繼續下降,這種情況可能反而是,到後期Learning rate已經太小,而導致無法都一直卡在某個Local Optimal出不太去導致。
  • 而使用CosineAnnealingLR的這組,就十分的有趣,首先是learning rate使用的比較小,初期收斂就比較小。接下來它還會因為週期性的learning rate變大導致結果又變差,但到最後卻反而有種繞過了什麼山頭,得到了最好結果的味道在。

其實上述的這些實驗結果,類似的情況反覆被我遇見,所以也是我個人近年來偏好使用CosineAnnealingLR的原因。

最後讓我們看一下,最主要的Metrics -- AUROC :

最好的那一個結果是在CosineAnnealingLR找到的AUROC = 0.7333。目前結果看起來還不錯了,雖然只是Validation set,但也十分接近MedMNIST裡頭最好的一樣也是使用28x28的0.769!

那到底Test的結果會如何?欲知後事如何,且待下回分解!

本日小節

  • 跑了幾個實驗比較結果
  • 結果看起來不錯,持續進步
  • 下一篇來跑test的結果以及講一些哲學問題

[Day11] Build a trainable Lightning-Module

[Day11] Build a trainable Lightning-Module

前言

接續前一日的文章,我們簡介了PyTorch Lightning以及如何利用Lightning Module將forward propagation進行簡單的封裝。今天我們將把Lightning Module完成,並利用新的格式進行模型訓練。

Lightning Module with Hyperparameters

先前已經利用YAML的方式進行超參數管理(詳見hparam.yaml),進一步,這裡可以把整個Configuration當作是整個Lightning Module的輸入,直接在模組內搭建模型的超參數。

module initialization

# initialize
def __init__(self, CONFIG : Dict, **kwargs):
    super().__init__()
    self.CONFIG = CONFIG # 把整個CONFIG放到CLASS內的一個property,以利其他函式可以重複利用。
    self.save_hyperparameters() # PL內建的儲存當次實驗超參數的函數
    self.backbone = get_backbone(CONFIG) # 用之前創立的函數,建立backbone
    self.loss_function = getattr(torch.nn, CONFIG['train']['loss_function'])() # 利用CONFIG設定需要的loss function

optimizer

def configure_optimizers(self):
    opt = getattr(torch.optim, self.CONFIG['train']['optimizer'])
    opt = opt(params=self.parameters(), 
              lr = self.CONFIG['train']['learning_rate'])
    return opt

這些都建立好以後,初步的一個lightning module就完成啦!

詳細可以看一下model.py,一樣可以使用下列指令來進行測試整個模型的建立狀況:

python src/model.py --config=hparams.yaml 

Trainer 與 Callback

在建立好Lightning-Module以後, 下一個問題便是怎麼使用它來訓練以及儲存結果了。這裡介紹兩個最常用的功能,分別是trainer以及callback。

在先前的PyTorch實作當中,我們使用了每個EPOCH訓練後,驗證集的AUROC來作為挑選模型的標準,進而儲存要選擇的模型。在PL當中,我們會利用一個Callback的物件,在模型過程中紀錄各式各樣的事情,例如下列的ModelCheckpoint就可以用來儲存模型的結果:

checkpoint_callback = pl.callbacks.ModelCheckpoint(dirpath=CONFIG['train']['weights_folder'],
                                                   monitor= 'val/auroc',
                                                   mode='max',
                                                   save_top_k=3,
                                                   filename = 'epoch_{epoch:02d}_val_loss_{val/loss:.2f}_val_acc_{val/acc:.2f}_val_auroc_{val/auroc:.2f}',
                                                   auto_insert_metric_name = False)

這邊用白話來講就是,利用指定的名稱,根據validation set的auroc,去儲存最高的三個權重組到指定的資料夾內。另外是為了在訓練途中,callback能夠認識到val\auroc,我們需要利用Lightning-Module本身的log功能,在訓練的時紀錄所需要的metrics:

def validation_epoch_end(self, validation_step_outputs: List[Any]):
    preds = torch.cat([output['preds'] for output in validation_step_outputs], dim=0)
    labels = torch.cat([output['labels'] for output in validation_step_outputs], dim=0)
    probs = torch.nn.Sigmoid()(preds)

    # compute metrics and log
    acc_score = torchmetrics.functional.accuracy(probs, labels, mdmc_average = 'global')
    auc_score = monai.metrics.compute_roc_auc(probs, labels, average='macro')
    self.log('val/acc', acc_score.item())
    self.log('val/auroc', auc_score.item())

最後是設置Trainer,以及要訓練的模型與對應的相關參數:

from src import model

net = model.MultiLabelsModel(CONFIG)
trainer = pl.Trainer(
        callbacks = checkpoint_callback,
        default_root_dir = CONFIG['train']['weights_folder'],
        max_epochs = CONFIG['train']['max_epochs'],
        limit_train_batches = CONFIG['train']['steps_in_epoch'],
        accelerator = 'cuda',
        devices = 1)
        
# model training
trainer.fit(net, 
            data_generators['TRAIN'], 
            data_generators['VALIDATION'])

接著就可以開始進行模型訓練了!

實作

一樣這次的實作有放到這個commit內,只要執行:

# python src/train.py --config=hparams.yaml
...
Epoch 24: 100%|█████████████████████████████████████████████| 138/138 [00:40<00:00,  3.42it/s, loss=0.194, v_num=16]

可以看到相同的參數底下,pl所訓練+驗證一個epoch的時間約是40秒。這項與先前在DAY08所實作的PyTorch訓練約30秒及驗證10秒加起來的數值相近,可見目前為止的效率而言是幾乎一樣的!

另一方面,以這次筆者實際訓練的結果為例,可以看到Callback所儲存的結果如下圖:

也與DAY08所得到的模型結果,auroc=0.58十分接近!

本日小節

  • 完成Lightning-Module
  • 使用checkpoint以及trainer完成訓練

[Day27] Weight Decay Regularization

[Day27] Weight Decay Regularization

前言

本篇將討論第三個預計介紹的Weight Decay Regularization技術,會先從L2開始講再帶到目前使用Optimizer的Weight Decay實作方式。

L2 Regularization

在講Weight Decay之前,首先先介紹其關係緊密相連的L2 Regularization。是一種利用L2-Norm(也就是所謂很常見的歐基理德距離):

$$
{\left|x\right|}2 = \sqrt{\left(\sum{i=1}^n x_i^2\right)} \text{, vector } x = (x_1, x_2, ..., x_n)
$$

來進行Regularization的方式。

那怎麼利用這個norm去作到Regularization呢?中心私路是這樣,一個overfitting的模型,一般被認為可能具有過度複雜以及不具無意義的pattern。而過度複雜的權重則很可能是這些過於取巧的pattern呈現的方式之一,L2 Regularization則是一種在學習時,利用L2-norm去限制權重的,進而避免overfitting的技術。另外,還有一種很常見的說法會說這個限制項,是對使用過大權重的懲罰(Penalty)項。

具體限制的方式則是把L2-norm混和加入loss再進行學習,可以寫成類似下面的概念式:

$$ L = L_{class}(f(x, w), y) + \lambda \sum_{i=1}^{n} w_i^2, \\ \quad\\ \text{where } w \text{ is model weights}, f \text{ is the model}, \text{ and } \lambda \text{ is the weights penalty coeifficient} $$

  • 一般在考慮到計算上具較佳的性質(例如微分較簡潔),因此會直接使用平方合(差一個根號)

如此一來在模型訓練的過程時,就會考量到權重的大小,避免使用過大,通常可以降低很多無意義的權重。還是不太懂的朋友可以參考Google爸爸的這個視覺化工具

Gradient and weight decay

那有了新的Loss函數後,下一步自然便是計算梯度並優化了。那是否有任何可以簡化梯度計算的方式呢?由於L2所增加的項,具有解析解,因此是有的:

$$ \begin{align} \frac{\partial L}{\partial w_i} & = \frac{\partial}{\partial w_i}(L_{class}(f(x, w), y) + \lambda \sum_{i=1}^{n} w_i^2) \\ &= \frac{\partial}{\partial w_i}(L_{class}(f(x, w), y)) + \lambda \frac{\partial}{\partial w_i}(\sum_{i=1}^{n} w_i^2) \\ &= \frac{\partial}{\partial w_i}(L_{class}(f(x, w), y)) + 2\lambda w_i \end{align} $$

簡化後的式子是原始LOSS所計算出的梯度、第二項則是原先權重的倍數,新增的第二項會讓有點權重逐漸衰變的意味在,也因此被稱作Weight Decay(起碼我是這樣理解的...)。

實作方式

在實作的部分,如果是最單純的SGD,想實作L2 Regularization的話,基本上便等同於在每次迭代時進行梯度的修改。因此可以看到文件中是有一個weight decay可以進行調整,便能直接實作。

但我們最常用的Adam呢?雖然裡頭一樣也有weight decay這個參數,可以進行類似SGD的方式來進行訓練,但Adam所採用的是Adaptive Gradient的方式,實質上與L2 Regularization並不等價,而且其表現也不佳,實驗結果可以參考這篇DECOUPLED WEIGHT DECAY REGULARIZATION

所幸的是,這篇論文提出了一種新的優化器叫AdamW,是一種修正了上述問題的方法。並且也有實作於PyTorch當中,我們也可以直接使用。文件參考,要注意的是這個優化器,預設weight decay rate是0.02,大部分優化器則是0。

實作結果

基本上一樣參考這個commit,我們也寫入新的config:

  optimizer: 
    name: 'AdamW'
    learning_rate: 0.001
    weight_decay: 0.02
    warmup_epochs: 5

經過訓練後,變可以得到下列的結果:

  • 收斂的結果跟效率都好上不少
  • 主因應該是這個搭配比較不會掉到所謂Adam的local optimal所導致

另外這裡我也有紀錄權重的L2-norm結果:

可以看到確實其L2-norm有不斷在下降,確實是有正常在工作。

本日小節

  • 使用AdamW進行Weight decay
  • 得到更加的結果

[Day29] Model Serving

[Day29] Model Serving with Triton Inference Server

前言

在一般的模型開發流程中,我們經歷過了Training、Testing之後。如果順利完成了一個不錯的模型,但那之後呢?今天要介紹的就是所謂的Serving,即模型佈署之道。

模型訓練收尾

在先前諸多的實驗中,最後選用上一篇所訓練出來的L2的參數。經過驗證src/evaluate.py以後可以得到下面的Testing結果:

Testing DataLoader 0: 100%|███████████████████████████████████████████████████████████████████████████████████| 44/44 [00:05<00:00,  7.58it/s]
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃        Test metric        ┃       DataLoader 0        ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│         test/acc          │    0.9476282000541687     │
│        test/auroc         │     0.772826611995697     │
│         test/loss         │    0.16255274415016174    │
└───────────────────────────┴───────────────────────────┘

基本上結果已經跟MedMNIST網站內的BenchMark差不多囉!可以放心應該已經適合結束實驗的階段。
(一般Task的允收標準除了BenchMark以外、還可以參考High Performance之類的)

佈署事前準備

對於版本與框架錯綜複雜的深度學習,佈署長久以來一直是個很棘手的問題。 Open Neural Network Exchange又簡稱ONNX,是一個嘗試將深度學習的模型進行標準化的格式。基本上任何模型如果能轉成這個格式,在佈署上便可以省掉不少麻煩。接著就讓我們來進行模型的格式轉換吧!

基本上PyTorch與PyTorch-Lightning對於onnx已經有相當不錯的支援了,只要下列幾行就便可轉檔:

net = model.MultiLabelsModel.load_from_checkpoint(CONFIG['evaluate']['weights_path'])
net.to_onnx('model.onnx',
            input_sample = torch.rand([1,3,28,28]),
            export_params=True)

接著事前的準備便完成啦!

Triton Inference Server

早期滿多實作都是架一個Flask API來作為serving的服務使用,使用上是容易上手,但其實裡頭使用Python來抓取模型、進行推論的主要媒介,其實對於記憶體跟運算速度都不是很有效率。

所謂站在巨人的肩膀上,這裡選用的是NVIDIA家出廠的Triton Inference Server來作為基礎Serving的服務,效能很不錯,不過東西很新,使用上很多小問題需要依靠社群的力量解決。也有不少家其他的Inference Server,例如TensorFlow Serving,不過我個人沒有用過,各位有興趣可以再嘗試看看。

具體的設定有點繁瑣,可以參考下列要點:

  1. 建立docker網路讓開發環境跟等等建立的triton可以連結
docker network create ithome_network
  1. 在docker-compose中加入網路設定,重啟開發環境
networks:
  default:
    external:
      name: ithome_network
  1. 建立Triton讀取模型的configuration,需要先設定一個pbtxt檔案,具體可以參考這邊
  2. 啟動Triton Inference Server
    Yaml檔設置 參考
version: "3.7"

networks:
  default:
    external:
      name: ithome_network

services:
  triton:
    container_name: "iThome2022_Triton"
    image: nvcr.io/nvidia/tritonserver:22.08-py3
    runtime: nvidia
    shm_size: '32GB'
    command:
      - tritonserver
      - --model-repository=/models
      - --log-verbose=1
      - --model-control-mode=explicit
      - --load-model=chestmnist_net
    volumes:
      - ./deploy:/models
    ports:
      - 8000:8000

然後按下:

docker-compose -f docker-compose-triton.yml up

就可以開啟了,另外要注意的是開啟GPU模式時triton的版本根本機的CUDA可能會有相容性問題,要稍微挑一下版本,由於個人用11.7所以選22.08。

以上都沒跳錯的話,一切就都設置完成囉!

Evaluation with Inference Server

那設定好API以後,就可以開始測試結果啦!只使用Python的朋友需要先裝一個triton開發的client可以進行測試:

pip install tritonclient[all]

接著可以利用這個工具,進行以下的post:

import tritonclient.grpc as grpcclient
import tritonclient.http as httpclient

def triton_run(processed_img, module_name, triton_api_path):
    with httpclient.InferenceServerClient(triton_api_path) as client:
        # initial inputs format
        inputs = [
            httpclient.InferInput("input", processed_img.shape, 'FP32')
        ]

        outputs = [
            httpclient.InferRequestedOutput("output"),
        ]

        inputs[0].set_data_from_numpy(processed_img.numpy())


        response = client.infer(module_name,
                                inputs,
                                model_version = '1',
                                request_id=str(1),
                                outputs=outputs)

        result = response.get_response()
        output = response.as_numpy("output")
        return output

仔細的內容可以參考src/evaluate_with_triton.py,一樣可以執行:

# python src/evaluate_with_triton.py --config=hparams.yaml 
100%|████████████████████████████████████████████████████████████████████████| 44/44 [00:04<00:00,  9.65it/s]
Inference Result : AUC = 0.7728351871517246

基本上結果大概在小數點四位內都與原生Pytorch的結果相同,看起來是個合理的模型佈署!

本日小節

  • 決定了一個模型
  • 進行模型佈署
  • 測試佈署結果

[Day14] 資料預處理機制與優化 - 軟體篇 - 優化預處理

[Day14] 資料預處理機制與優化 - 軟體篇 - 優化預處理

前言

前一日的文章,以硬體面討論了訓練的效率。但有了適當的硬體以後,要如何利用軟體去使用這些硬體呢?從今天開始的幾天內將介紹一些優化的方式。

資料預處理機制

先複習一下,上一篇文章內說的訓練流程:

flowchart LR
    subgraph Main[ ]
        CPU <-->|preprocess| RAM
    end
    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
  Storage --> Main --> GPU

今天將以橫向及縱向的兩種方向,來介紹增加Preprocess的方法,分別是:

  1. multiprocessing
  2. prefetch

multiprocessing

正常情形的程式,其實都是使用單一個執行序(thread)在執行程式。但在某些情況下,有些任務是可以被平行處理(詳見平行計算)進而加快計算效率的。

例如,資料的預處理就是個很好的例子。以影像為例,每一張影像我們需要讀取、解碼、然後進行前處理,這是一個執行序能為我們做到的事情。那理論上而言,在同一時間單位下,有多少個的執行序,便能完成多少倍的資料預處理(但實際上會有個增幅極限,因為考慮到storage、cpu以及ram在單位時間內的存取極限),大致上概念上就會是下圖的模樣:

flowchart LR
    subgraph Main[ ]
      subgraph CPU
        thread1
        thread2
        thread3
        thread4
      end
      thread1 <-->|preprocess| RAM
      thread2 <-->|preprocess| RAM
      thread3 <-->|preprocess| RAM
      thread4 <-->|preprocess| RAM
    end
    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
  Storage --> Main --> GPU

Prefetch

另一個可以考量的方式則是,GPU計算跟CPU前處理都需要時間對吧?那麼如果CPU前處理時,GPU閒置,GPU處理時,CPU閒置。這樣不是很浪費嗎?(慣老闆心態...)

Prefetch(或許可以譯作預取?),便是一個更加利用資源的方式。在GPU計算時,讓原本閒置的CPU先行進行下一個批次計算需要用到的預處理,透過同時壓榨CPU與GPU的計算能力來增加效能。概念大概就會長得像下面這張圖:

flowchart LR
    subgraph Main[ ]
        CPU <-->|preprocess| RAM
    end
  Storage --> Main --> GPU
  
  subgraph SameTime
    subgraph GPU[ ]
        gpu_core[GPU Cores] <-->|training+validation| gpu_m[GPU Memory]
    end
    subgraph Main2[ ]
        CPU2[CPU] <-->|preprocess| RAM2[CPU]
    end
    

    end
  
  
  
    subgraph GPU2[ ]
        gpu_core2[GPU Cores] <-->|training+validation| gpu_m2[GPU Memory]
    end
  Storage2[Storage] --> Main2 --> GPU2
  
  classDef bk fill:#fff,stroke:#333,stroke-width:4px
  class SameTime bk

另外可以參考參考這篇文章,裡頭有以下這張動態圖,讓人更好理解Prefetch能增加多少效率:

該怎麼做?

說的嘴巴都泡,但具體怎麼做呢?所幸,歸功於強大的社群功能,Torch內的DataLoader已經同時實作了這兩樣東西。參考官方文件
可以透過其中下列的參數進行設置:

  • num_workers:設置多少subprocess來平行進行資料預處理,0表示僅有主程式進行。
  • prefetch_factor:表示每一個worker會進行多少batch的prefetch。

具體實例如下:

torch.utils.data.DataLoader(dataset,
                            num_workers = 8,
                            prefetch_factor = 4)

實際差異

那麼實際差異到底有多少呢?讓我們實際各跑5個epoch比較看看。實驗結果如下表

進行了平行預處理以及prefetch的結果是原本的43%(245ms/579ms)唷!詳細設置可以參考無優化以及有優化的這兩個commit。

本日小節

  • 用軟體壓榨硬體獲得比較好的效率
  • 畫圖很辛苦(?)

[Day 2] Containerized Development for Deep Learning

[Day 2] Containerized Development for Deep Learning

前言

本日的重點就是介紹個人所使用的Containerized Development框架,說穿了就是分享個人所使用的DockerFile與Docker Compose設定檔。

什麼是Docker?

Docker是一套現在非常火紅、以容器為基礎的虛擬化框架,但比起傳統的虛擬機器使,使用上也十分的輕易近人,而且更重要的是有許多第三方已經建立好的Image,可以直接pull下來做使用,也算是一種站在巨人肩膀上的方式。Docker Compose則是一種使用Docker時的設置檔案,主要用來簡化Docker的使用方式以及再利用性。詳細就不多做介紹,建議可以直接參考官方文件:

為什麼要 Containerized Development

談這件事之前想先問下列兩種讀者問題:

  • 剛學資料科學的初學者:模型訓練結束,結果好棒棒,然後呢?別人怎麼用你的結果?
  • 正在帶Junior Data Scientists一起開發的Senior:不同Junior之間的環境整合得起來嗎?

其實這兩個問題都環繞著一個相同的核心,就是模型的再現性問題。而再現性又可以分兩個層面:

  1. 我如何用一樣的資料,訓練出一樣的模型?
  2. 我如何用一樣的Weight,在其他機器都可以正常佈署並得到相同(相似)的結果?

而 Containerized Development 便是一個對這兩種再現性都具有相當程度幫助的一個方式。(當然也有例外就是,但就是後話了)

實作開始

本次實作的repo

https://github.com/SraRod/iThome2022/tree/aa33395073d0e0dbf3321521f08415c41fbd7d9a
(註:原則上這幾個檔案應該會持續更新,各位也可以選擇main的最新檔案)

DockerFile & requirements.txt

Docker基本上在做的事情就是把已經建立好的Image,展開成一個Container再來運行。
而DockerFile的作用,基本上就是用來製作我們自己需要的Image!
可以參考我在連結中的DockerFile

首先我們一樣站在巨人的肩膀上,運用MONAI所提供的Image作為基礎

FROM projectmonai/monai:0.9.1

但實際使用會發現這位巨人的視力好像有點狀況,於是重裝一下opencv。
(註:應該是因為MONAI大部分都用PIL,所以裡頭沒有OPENCV)
(註:也可以在requirement裡面裝 opencv-headless,比較方便但Performance上有些許差異,看個人抉擇)

# install opencv dependency
RUN apt update &&\
    apt install libgl1-mesa-glx -y &&\
    conda install -c conda-forge nodejs=16.12.0 -y
    # for nodejs > 12...

接著則是利用requirements.txt安裝裡頭Python所需要的套件以及固定好的版本。

# specify version if you could...
COPY requirements.txt .

# install pyton dependency
RUN pip install --upgrade pip &&\
    pip install -r requirements.txt

最後就是同步一下內外跟專案的Python路徑,會讓一些小操作上更加順暢。就大功告成啦!

# set workspace to python path
ENV PYTHONPATH $PYTHONPATH

Docker-compose Configuration YAML

下一步則是 docker-compose 的設定檔,請參考我寫好的檔案

首先介紹一下,以下是基礎設定,解說寫在後面

version: '3.7'   # 採用的Confiuguration版本

services:
  jupyter:
    container_name: "iThome2022" # build起來以後的container name,沒有也可以,會根據資料夾跟service name自己命名
    build: . # 根據我們剛剛建立好的Dockerfile來build Image,預設檔名就是Dockerfile,可以不同,需加額外參數
    shm_size: '32GB' # Share Memory的設定,太低的話,PyTorch有些操作會出錯
    runtime: nvidia # 讓Container可以吃到NVIDIA的GPU
    user: root # 用root權限執行
    working_dir: /home/$USER/workspace # 設定workspace
    environment:  # 一些環境變數,基本上跟在LINUX相同
      NVIDIA_VISIBLE_DEVICES: 0 # 只吃第一張 GPU ,跟同事同主機多張GPU怕誤搶到時可以使用
      JUPYTER_ENABLE_LAB: 'yes' # 把Jupyter LAB 打開
      PYTHONPATH: /home/$USER/workspace # 同working_dir

這裡則是把container內部的空間作一個連動,類似ln -s
值得注意的是除了把repo的資料夾就投到workspace以外,這裡還分別做了與local user共享ssh key、git設定檔以及把jupyter放到外部的小巧思。可以讓使用者直接打開jupyter lab開發的時候更加順暢一些。

    volumes:
      - .:/home/$USER/workspace
      - ~/.ssh:/home/$USER/.ssh # for ssh
      - ~/.gitconfig:/home/$USER/.gitconfig # for git
      - ./.jupyter:/root/.jupyter

這邊就是port跟外部的連動,我是都會把juyter用的8888打到5566(得第一,不解釋)。
5000就是預留給額外服務用的,例如開發很常用到的Tensorboard 或是 MLFlow

    ports:
      - 5566:8888
      - 5000:5000

最後則是container建立好以後,需要執行的指令。這邊主要就是作jupyter的一些初始設定,然後再把服務架起來(以危險的方式,好孩子在佈署非個人化的服務時千萬不要學)。

    command: bash -c '
      cd /home/$USER/workspace
      && jupyter lab build
      && jupyter labextension install jupyterlab-plotly
      && jupyter labextension install @jupyter-widgets/jupyterlab-manager
      && jupyter lab --no-browser --allow-root --ip=0.0.0.0 --ContentsManager.allow_hidden=True --NotebookApp.token="" --NotebookApp.password=""'

執行方式

基本上就是把整個repo clone下來以後

docker-compose up -d

等待image拉下來跟build好以後,就是一套完整可以使用的jupyter lab啦!

結論

  • 分享個人的設定檔
  • 簡單介紹設定檔裡的內容
  • 個人不重要的小囉哩囉唆

[Day 1] PyTorch 生態鏈實戰運用 - 系列文概要

前言

PyTorch是目前產學界都十分活耀的深度學習框架,其中很大的一個原因來自於他豐富的生態鏈。本系列文將以醫療影像為例,介紹多個套件所共同組合出的實戰運用。

如何進行?

本系列文預計將整理目前筆者所使用的深度學習框架,採用一步一步慢慢堆疊的方式,並搭配到時候會發佈的github repository,將所採用的技術環節,以手把手的方式一個個commit加入並完整整個開發模組。

基本上每日的文章大致上會分成兩種概念文與實作文兩種,概念文主會要講述所採用套件或技術的觀念及其所被採用的理由,實作的部分則會有實際的coding內容,以及對應的git commit可以直接讓讀者去比較差異。

有哪些內容?

系列文會從開發環境以及Configuration開始介紹,具體上是希望各個學習深度學習的工程師或是資料科學家們,能夠對於自己所開發出來的東西有比較細步的掌握。

整個系列文基本上會環繞在下列的三個套件:

作為基礎框架來進行實作。會採用Deep Learning中最基本的任務Classification來做為系列文章的主體,接著一步一步加入不同Pytorch的生態系套件及訓練技巧,以此提升整個開發的效率跟嚴謹度,最後也會把開發好的模型佈署成API,進行所謂的Model Serving。資料上則會採用醫療影像相關的公開資料集,目前選定會是MedMNIST v2

另外依照實際進度,可能還會有一些3D Segmentation 甚至是 Registration的實例。

本日小節

  • 本系列文的難度應該會偏向新手
  • PyTorch生態系介紹 (筆者個人所使用的)
  • 資料是醫學影像
  • 內容很雜,有意見可以提出,大家教學相長。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.