<jupyter_start><jupyter_text>จาก ep ก่อน ๆ เราจะได้ Training Loop หลัก ๆ เป็นดังนี้ (ไม่รวม Metrics) ```    for e in range(epoch):        for xb, yb in train_dl:            yhatb = model(xb)            loss = loss_func(yhatb, yb)            loss.backward()            optim.step()            optim.zero_grad()```ถ้าเราต้องการเพิ่มเติม Logic การเทรนที่ซับซ้อนยิ่งขึ้น เช่น Early Stopping, Learning Rate Annealing, BatchNorm เราจะต้องแก้โค้ดนี้ แทรกตามบรรทัดต่าง ๆ เช่น ก่อนเริ่มเทรน, ก่อนเริ่ม Epoch, ก่อนอัพเดท Weight, หลังจากจบ 1 Epoch, หลังจากเทรนจบ, etc.ข้อเสียของการแทรกโค้ดแบบนี้ คือ ทำให้โค้ดใน Loop นี้ก็จะบวมขึ้นเรื่อย ๆ ส่งผลให้มีปัญหาในการ Maintain ทางแก้ก็คือ เราจะแทรกโค้ดไว้ก่อนเลยในทุกจุดที่เป็นไปได้ เป็นการ Call Function ภายนอก ที่ตั้งชื่อตาม Event ต่าง ๆ ของ Training Loop เรียกว่า [Callback](https://www.bualabs.com/archives/2238/what-is-callback-function-python-ep-6/) ถ้าใครอยากให้ Execute โค้ด ตรงตำแหน่งไหน ก็ตั้งชื่อฟังก์ชันให้ตรงกับชื่อ Event แล้วพาสมาให้กับ Training Loop เราจะได้ Training Loop ใหม่เป็นแบบนี้ เริ่มต้นที่หัวข้อ [5 Training Loop](5.-Training-Loop)```    cb.begin_fit()    for e in range(epoch):        cb.begin_epoch()        for xb, yb in train_dl:            cb.begin_batch()            yhatb = model(xb)            loss = loss_func(yhatb, yb)            cb.after_loss()            loss.backward()            cb.after_backward()            optim.step()            cb.after_step()            optim.zero_grad()        cb.after_epoch()    cb.after_fit()```  0. Magic<jupyter_code>%load_ext autoreload
%autoreload 2
%matplotlib inline<jupyter_output><empty_output><jupyter_text>1. Import<jupyter_code>import torch
from torch import tensor
from torch.nn import *
import torch.nn.functional as F
from torch.utils.data import *
from fastai import datasets
from fastai.metrics import accuracy
import pickle, gzip, math, torch, re
from IPython.core.debugger import set_trace<jupyter_output><empty_output><jupyter_text>2. Data<jupyter_code>class Dataset(Dataset):
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __len__(self):
        return len(self.x)
    def __getitem__(self, i):
        return self.x[i], self.y[i]
MNIST_URL='http://deeplearning.net/data/mnist/mnist.pkl'
def get_data():
    path = datasets.download_data(MNIST_URL, ext='.gz')
    with gzip.open(path, 'rb') as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding='latin-1')
    return map(tensor, (x_train, y_train, x_valid, y_valid))
x_train, y_train, x_valid, y_valid = get_data()
def normalize(x, m, s): 
    return (x-m)/s
from typing import *

def listify(o):
    if o is None: return []
    if isinstance(o, list): return o
    if isinstance(o, str): return [o]
    if isinstance(o, Iterable): return list(o)
    return [o]
_camel_re1 = re.compile('(.)([A-Z][a-z]+)')
_camel_re2 = re.compile('([a-z0-9])([A-Z])')

def camel2snake(name):
    s1 = re.sub(_camel_re1, r'\1_\2', name)
    return re.sub(_camel_re2, r'\1_\2', s1).lower()
    
    
train_mean, train_std = x_train.mean(), x_train.std()
x_train = normalize(x_train, train_mean, train_std)
x_valid = normalize(x_valid, train_mean, train_std)
nh, bs = 100, 32
n, m = x_train.shape
c = (y_train.max()+1).numpy()
loss_func = F.cross_entropy
train_ds, valid_ds = Dataset(x_train, y_train), Dataset(x_valid, y_valid)
train_dl, valid_dl = DataLoader(train_ds, bs), DataLoader(valid_ds, bs)<jupyter_output><empty_output><jupyter_text>3. DataBunch<jupyter_code>class DataBunch():
    def __init__(self, train_dl, valid_dl, c=None):
        self.train_dl,self.valid_dl,self.c = train_dl,valid_dl,c

    @property
    def train_ds(self): return self.train_dl.dataset

    @property
    def valid_ds(self): return self.valid_dl.dataset<jupyter_output><empty_output><jupyter_text>ลองสร้าง DataBunch จาก train_dl, valid_dl และ c ที่เราสร้างไว้ก่อนหน้านี้<jupyter_code>data = DataBunch(train_dl, valid_dl, c)<jupyter_output><empty_output><jupyter_text>4. Model<jupyter_code>lr = 0.03
epoch = 10
nh = 50
def get_model():
    # loss function
    loss_func = F.cross_entropy
    model = Sequential(Linear(m, nh), ReLU(), Linear(nh,c))
    return model, loss_func
model, loss_func = get_model()
opt = torch.optim.SGD(model.parameters(), lr=lr)
class Learner():
    def __init__(self, model, opt, loss_func, data):
        self.model, self.opt, self.loss_func, self.data = model, opt, loss_func, data

learn = Learner(model, opt, loss_func, data)<jupyter_output><empty_output><jupyter_text>5. Training Loop  5.1 Training Loop แบบเดิม Training Loop ตามแบบ ep ก่อน ๆ<jupyter_code>def fit(epoch, learn):
    # e = epoch number
    for e in range(epoch):

        # Set Model in Train Mode
        learn.model.train()

        for xb, yb in learn.data.train_dl:
            yhatb = learn.model(xb)
            loss = learn.loss_func(yhatb, yb)
            loss.backward()
            learn.opt.step()
            learn.opt.zero_grad()

        # Set Model in Evaluation Mode
        learn.model.eval()

        # Metrics
        with torch.no_grad():
            # tot_loss = total loss, tot_acc = total accuracy
            tot_loss, tot_acc = 0., 0.
            for xb, yb in learn.data.valid_dl:
                yhatb = learn.model(xb)
                tot_acc += accuracy(yhatb, yb)
                tot_loss += learn.loss_func(yhatb, yb)
            # nv = number of validation batch
            nv = len(learn.data.valid_ds)/bs
            print(f'epoch={e}, valid_loss={tot_loss/nv}, valid_acc={tot_acc/nv}')            
    return tot_loss/nv, tot_acc/nv<jupyter_output><empty_output><jupyter_text>ลองเทสว่าเทรนได้<jupyter_code>fit(1, learn)<jupyter_output>epoch=0, valid_loss=0.20957346260547638, valid_acc=0.9422000050544739<jupyter_text>5.2 Training Loop with Callback เราจะสร้าง Training Loop เวอร์ชันมี Callback เพื่อรองรับการเทรนที่ซับซ้อนมากขึ้น แต่แทนที่เราจะสร้างเป็นฟังก์ชันเหมือนเดิม เราจะสร้างเป็น Class ชื่อ Runner มาห่อไว้ และ Refactor fit ออกเป็น one_batch และ all_batches<jupyter_code>class Runner():
    def __init__(self, cbs=None, cb_funcs=None):
        cbs = listify(cbs)
        for cbf in listify(cb_funcs):
            cb = cbf()
            setattr(self, cb.name, cb)
            cbs.append(cb)
        self.stop, self.cbs = False, [TrainEvalCallback()]+cbs

    @property
    def opt(self):          return self.learn.opt
    @property
    def model(self):        return self.learn.model
    @property
    def loss_func(self):    return self.learn.loss_func
    @property
    def data(self):         return self.learn.data

    def one_batch(self, xb, yb):
        try: 
            self.xb, self.yb = xb, yb
            self('begin_batch')
            self.pred = self.model(xb)
            self('after_pred')
            self.loss = self.loss_func(self.pred, yb)
            self('after_loss')
            if not self.in_train: return
            self.loss.backward()
            self('after_backward')
            self.opt.step()
            self('after_step')
            self.opt.zero_grad()
        except CancelBatchException: self('after_cancel_batch')
        finally: self('after_batch')
    
    def all_batches(self, dl):
        self.iters = len(dl)
        try:
            for xb, yb in dl:
                self.one_batch(xb, yb)
        except CancelEpochException: self('after_cancel_epoch')
    
    def fit(self, epochs, learn):
        self.epochs, self.learn, self.loss = epochs, learn, tensor(0.)

#         set_trace()

        try:
            for cb in self.cbs: cb.set_runner(self)
            self('begin_fit')
            for epoch in range(epochs):
                self.epoch = epoch
                if not self('begin_epoch'): self.all_batches(self.data.train_dl)

                with torch.no_grad():
                    if not self('begin_validate'): self.all_batches(self.data.valid_dl)
                self('after_epoch')
        except CancelTrainException: self('after_cancel_train')
        finally: 
            self('after_fit')
            self.train = None

    def __call__(self, cb_name):
        # return True = Cancel, return False = Continue (Default)
        res = False
        # check if at least one True return True
        for cb in sorted(self.cbs, key=lambda x: x._order): res = res or cb(cb_name)
        return res<jupyter_output><empty_output><jupyter_text>เราจะสร้าง Class Callback เอาไว้เป็น Base Class สำหรับทุก ๆ Callback สังเกต \_order คือลำดับในการเรียก Callback และ \_\_call\_\_ จะ Return True เมื่อต้องการให้หยุด<jupyter_code>class Callback():
    _order = 0
    def set_runner(self, run): self.run = run
    def __getattr__(self, k): return getattr(self.run, k)

    @property
    def name(self):
        name = re.sub(r'Callback$', '', self.__class__.__name__)
        return camel2snake(name or 'callback')
    
    def __call__(self, cb_name):
        f = getattr(self, cb_name, None)
        if f and f(): return True
        return False<jupyter_output><empty_output><jupyter_text>เราจะสร้าง Callback ตัวอย่าง ที่จะสลับโหมด train/eval ของโมเดล โดยอัตโนมัติ และคำนวน n_epochs, n_iter ว่าเทรนถึง Batch ไหนแล้ว Epoch ไหนแล้ว<jupyter_code>class TrainEvalCallback(Callback):
    def begin_fit(self):
        self.run.n_epochs = 0.
        self.run.n_iter = 0
    
    def begin_epoch(self):
        self.run.n_epochs = self.epoch  
        self.model.train()
        self.run.in_train=True

    def after_batch(self):
        if not self.in_train: return
        self.run.n_epochs += 1./self.iters
        self.run.n_iter += 1

    def begin_validate(self):
        self.model.eval()
        self.run.in_train=False<jupyter_output><empty_output><jupyter_text>ประกาศ Exception เอาไว้เป็น Control Flow เวลาต้องการ ยกเลิกการเทรน Batch นั้น ๆ, Epoch นั้น ๆ หรือ ยกเลิกการเทรนไปหมดเลย<jupyter_code>class CancelTrainException(Exception): pass
class CancelEpochException(Exception): pass
class CancelBatchException(Exception): pass<jupyter_output><empty_output><jupyter_text>เราจะสร้าง Callback ทดสอบ ที่จะทำงานยกเลิกการเทรน เมื่อเราเทรนมากกว่า 5 Epoch<jupyter_code>class TestCallback(Callback):
    _order = 1
#     def after_step(self):
#         print(f'n_iter = {self.n_iter}')
#         if self.n_iter > 5: raise CancelTrainException()
    def after_epoch(self):
        print(f'n_epochs = {self.n_epochs}')
        if self.n_epochs > 5: raise CancelTrainException()<jupyter_output><empty_output><jupyter_text>สร้าง Runner โดยพาส TestCallback เข้าไป<jupyter_code>runner = Runner(cb_funcs=TestCallback)<jupyter_output><empty_output><jupyter_text>ลองสั่งเทรนไป 10 Epoch ดูว่าจะหยุดเมื่อ Epoch > 5 หรือไม่<jupyter_code>runner.fit(10, learn)<jupyter_output>n_epochs = 1.000000000000019
n_epochs = 2.0000000000000275
n_epochs = 2.9999999999996807
n_epochs = 3.9999999999996807
n_epochs = 5.000000000000375<jupyter_text>Training Loop fit แบบใหม่ (Runner with Callback) ทำงานได้ถูกต้อง  Credit * https://course.fast.ai/videos/?lesson=9* http://yann.lecun.com/exdb/mnist/<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12554545454545454, 'fraction_numerical': 0.01681818181818182, 'mean_word_length': 3.2474903474903476, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 66, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 5, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '83486', 'license': 'MIT', 'n_tokens_mistral': 4713, 'n_tokens_neox': 4645, 'n_words': 815, 'path': 'nbs/16h-callbackhandler.ipynb', 'repo_name': 'gnoparus/bualabs', 'size': '11000'}