Neural Network หรือ Artificial Neural Network คือ โครงข่ายประสาทเทียม เป็นสาขาหนึ่งของปัญญาประดิษฐ์ Artificial Intelligence (AI) เป็นแนวคิดที่ออกแบบระบบโครงข่ายคอมพิวเตอร์ ให้เลียนแบบการทำงานของสมองมนุษย์

ใน ep นี้เราจะมาดูกันว่า ภายใน Neural Network นั้นทำงานอย่างไร และเราจะมาสร้าง 2 Layers Deep Neural Network กันตั้งแต่ Tensor, Matrix และฟังก์ชันคณิตศาสตร์พื้นฐาน บวก ลบ คูณ หาร แบบเข้าใจง่าย ๆ ไปทีละขั้นด้วยกัน

Neural Networks with Matrices. Credit https://ml-cheatsheet.readthedocs.io/en/latest/forwardpropagation.html
Neural Networks with Matrices. Credit https://ml-cheatsheet.readthedocs.io/en/latest/forwardpropagation.html

เรามาเริ่มกันเลยดีกว่า

Open In Colab

Neural Networks 101 - 2 Layers Deep Neural Networks from Scratch

สอนสร้าง นิวรอลเน็ตเวิร์ก ทีละขั้นตอน แบบเข้าใจง่าย

0. Magic

In [62]:
%load_ext autoreload
%autoreload 2
%matplotlib inline
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

1. Import

In [0]:
from pathlib import Path
from IPython.core.debugger import set_trace
from fastai import datasets
import pickle, gzip, math, torch
from torch import tensor, nn
import operator

ประกาศฟังก์ชันที่ใช้ในการเทส ค่าใกล้เคียงกัน และค่าใกล้ 0 เนื่องจาก ตัวเลขแบบ Floating Point จะไม่สามารถเปรียบเทียบว่าเท่ากันหรือไม่ได้ตรง ๆ เพราะจะมีความคลาดเคลื่อนอยู่ โดยเฉพาะอย่างยิ่งเลขมาก ๆ และเลขน้อย ๆ เช่น 123456789.0 หรือ 0.123456789 แต่เราจะใช้ค่า tol (tolerance) ว่าถ้าห่างกันไม่เกินค่านี้ ก็ถือว่ามีค่าเท่ากัน

In [0]:
# tol = tolerance
def test(a,b,cmp,cname=None):
    if cname is None: cname=cmp.__name__
    assert cmp(a,b),f"{cname}:\n{a}\n{b}"

def near(a,b): return torch.allclose(a, b, rtol=1e-3, atol=1e-5)
def test_near(a,b): test(a,b,near)    
    
def test_near_zero(a, tol=1e-3): assert a.abs() < tol, f"Near Zero: {a}"    

2. Download Dataset

เคสนี้เราจะใช้ MNIST Dataset เหมือนเดิม

In [0]:
MNIST_URL='http://deeplearning.net/data/mnist/mnist.pkl'
In [0]:
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, y ทั้ง Training Set และ Validation Set

In [0]:
x_train, y_train, x_valid, y_valid = get_data()

3. เตรียมข้อมูล

หา mean และ standard deviation ของข้อมูล Training Set

In [68]:
train_mean, train_std = x_train.mean(), x_train.std()
train_mean, train_std
Out[68]:
(tensor(0.1304), tensor(0.3073))

Normalize คือการทำให้ข้อมูล มี mean = 0 และ standard deviation (std) = 1 ด้วยกัน ลบด้วย mean หารด้วย std

In [0]:
# x = data, m = mean, s = standard deviation
def normalize(x, m, s): 
    return (x-m)/s
In [0]:
x_train = normalize(x_train, train_mean, train_std)

Normalize ข้อมูล Validation Set ด้วย mean, std ของ Training Set เพื่อปรับให้เป็น Scale เดียวกัน

In [0]:
x_valid = normalize(x_valid, train_mean, train_std)

ลองดูค่า mean, std หลังจาก Normalize เรียบร้อยแล้ว

In [72]:
train_mean_after, train_std_after = x_train.mean(), x_train.std()
train_mean_after, train_std_after
Out[72]:
(tensor(0.0001), tensor(1.))

ได้ mean เป็นจุดทศนิยมน้อยมาก ๆ ใกล้เคียง 0 และ std ใกล้เคียง 1

In [0]:
test_near_zero(train_mean_after)
In [0]:
test_near_zero(1-train_std_after)

n = จำนวน Record, m จำนวน Feature, c = จำนวน class

In [75]:
n, m = x_train.shape
c = y_train.max()+1
n, m, c
Out[75]:
(50000, 784, tensor(10))

4. Model

เราจะสร้าง 2 Hidden Layers Neural Networks ที่มี 50 Neuron ในแต่ละ Hidden Layer

In [0]:
# nh = number of hidden
nh = 50

กำหนดค่าเริ่มต้นของ Weight และ Bias ด้วยค่า Random แบบ Normal Distribution (Gaussian)

In [0]:
w1 = torch.randn(m, nh)
b1 = torch.zeros(nh)
w2 = torch.randn(nh, 1)
b2 = torch.zeros(1)

ลองดูค่า mean, std ของ Weight

In [78]:
w1.mean(), w1.std()
Out[78]:
(tensor(0.0008), tensor(0.9970))
In [0]:
# x = data, w = weight, b = bias
def lin(x, w, b): 
    return x@w + b
In [0]:
def relu(x):
    return x.clamp_min(0.)
In [0]:
# xb = x batch, l2 = layer 2, a1 = activation 1
def model(xb):
    l0 = xb
    l1 = lin(l0, w1, b1)
    a1 = relu(l1)
    l2 = lin(a1, w2, b2)    
    return l2

yhat (y hat) คือ y ที่ได้จากการคำนวนของโมเดล ที่เราจะเอาไปเปรียบเทียบกับ y จริง ๆ จาก Training Set หรือ Validation Set

In [0]:
yhat = model(x_train)
In [83]:
yhat.shape, y_train.shape
Out[83]:
(torch.Size([50000, 1]), torch.Size([50000]))

5. Loss Function

ในที่นี้เราจะใช้ Mean Squared Error แทน Cross Entropy Loss เพื่อลดความซับซ้อนของโค้ดให้เข้าใจง่ายขึ้น (แต่จริง ๆ ไม่เหมาะกับงาน Classification แบบนี้)

จาก shape ด้านบน yhat เป็น tensor 2 มิติ เราจะ squeeze มิติสุดท้าย (-1) ทิ้งไป ก่อนลบกับ y

In [0]:
def mse(yhat, y): return ((yhat.squeeze(-1)-y)**2).mean()

แปลงจาก Long เป็น Float ก่อนส่งให้ฟังก์ชัน

In [0]:
y_train, y_valid = y_train.float(), y_valid.float()
In [86]:
loss = mse(yhat, y_train)
loss
Out[86]:
tensor(14467.1260)

6. Backpropagation and Gradients

ประกาศฟังก์ชัน Diff ของฟังก์ชันด้านบน เพื่อใช้ใน Backpropagation หา Gradient ของ Loss ที่ขึ้นกับทุก ๆ Parameter

In [0]:
# inp = input, targ = target
def mse_grad(inp, targ): 
    inp.g = (2. * (inp.squeeze()-targ).unsqueeze(-1)/inp.shape[0])
#     print(inp.shape, inp.g.shape)
    assert inp.shape == inp.g.shape
In [0]:
# inp = input, out = output
def relu_grad(inp, out): 
    inp.g = (inp>0).float() * out.g
    assert inp.shape == inp.g.shape
In [0]:
def lin_grad(inp, out, w, b): 
    inp.g = out.g @ w.t()    
    w.g = inp.squeeze(-1).t() @ out.g
    b.g = out.g.sum(0)
    assert inp.shape == inp.g.shape
    assert w.shape == w.g.shape
    assert b.shape == b.g.shape    

ประกาศฟังก์ชัน ให้รันทั้ง forward และ backward หา Gradient

In [0]:
def forward_and_backward(inp, targ):
    # forward
    l1 = inp @ w1 + b1
    a1 = relu(l1)
    l2 = a1 @ w2 + b2
    loss = mse(l2, targ)
    
    # backward
    mse_grad(l2, targ)
    lin_grad(a1, l2, w2, b2)
    relu_grad(l1, a1)
    lin_grad(inp, l1, w1, b1)   
In [91]:
x_train.shape, y_train.shape
Out[91]:
(torch.Size([50000, 784]), torch.Size([50000]))
In [0]:
forward_and_backward(x_train, y_train)

ได้ค่า Gradient ออกมา

In [93]:
w1.g.mean(), w1.g.std()
Out[93]:
(tensor(0.0717), tensor(50.5915))

เราจะ Clone Gradient ที่ตำนวนได้เอาไว้ก่อน เอาไว้เทียบกับของ Model เวอร์ชันต่อ ๆ ไป

In [0]:
w1g = w1.g.clone()
w2g = w2.g.clone()
b1g = b1.g.clone()
b2g = b2.g.clone()
ig  = x_train.g.clone()

6.1 แอบใช้ PyTorch หา Gradient

ใช้ PyTorch Autograd มาช่วยหา Gradient เอาไว้เช็คความถูกต้อง

In [0]:
xt2 = x_train.clone().requires_grad_(True)
w12 = w1.clone().requires_grad_(True)
w22 = w2.clone().requires_grad_(True)
b12 = b1.clone().requires_grad_(True)
b22 = b2.clone().requires_grad_(True)
In [0]:
def forward(inp, targ):
    # forward pass:
    l1 = inp @ w12 + b12
    l2 = relu(l1)
    out = l2 @ w22 + b22
    # we don't actually need the loss in backward!
    return mse(out, targ)
In [97]:
loss = forward(xt2, y_train)
loss
Out[97]:
tensor(14467.1260, grad_fn=<MeanBackward0>)

เรียก backward ให้ PyTorch ทำ Backpropagation กับทุกตัวแปรที่ requires_grad=True ด้านบน

In [0]:
loss.backward()

เทสว่า Gradient จาก PyTorch ตรงกับที่เรา Clone ไว้หรือไม่

In [0]:
test_near(w22.grad, w2g)
test_near(b22.grad, b2g)
test_near(w12.grad, w1g)
test_near(b12.grad, b1g)
test_near(xt2.grad, ig )

เทสผ่าน แสดงว่า Gradient ที่เราคำนวนถูกต้อง ตรงกับที่ PyTorch Autograd ช่วยคำนวน

7. Refactor Layer to Class

เราจะมา Refactor โค้ด ด้านบน แต่ละส่วน Layer, Activation Function, Loss Function ให้เป็น Class ของตัวเอง

In [0]:
class Relu():
    def __call__(self, inp):
        self.inp = inp
        self.out = inp.clamp_min(0.)
        return self.out
    def backward(self):
        self.inp.g = (self.inp>0).float()*self.out.g
In [0]:
class Lin():
    def __init__(self, w, b): self.w, self.b = w, b
        
    def __call__(self, inp):
        self.inp = inp
        self.out = inp @ self.w + self.b
        return self.out
    def backward(self):
        self.inp.g = self.out.g @ self.w.t()
        self.w.g = self.inp.squeeze(-1).t() @ self.out.g
        self.b.g = self.out.g.sum(0)
In [0]:
class Mse():
    def __call__(self, inp, targ):
        self.inp, self.targ = inp, targ
        self.out = (inp.squeeze() - targ).pow(2).mean()
        return self.out
    def backward(self):
        self.inp.g = 2. * (self.inp.squeeze() - self.targ).unsqueeze(-1) / self.inp.shape[0]        

เราจะสร้างโมเดล Neural Networks 2 Hidden Layer จาก Class ที่เราเพิ่ง Refactor ด้านบน

In [0]:
class Model():
    def __init__(self, w1, b1, w2, b2):
        self.layers = [Lin(w1, b1), Relu(), Lin(w2, b2)]
        self.loss = Mse()
    def __call__(self, x, targ):
#         set_trace()
        for l in self.layers:
            x = l(x)
        return self.loss(x, targ)
    def backward(self):
        self.loss.backward()        
        for l in reversed(self.layers):
            l.backward()

เคลียร์ Gradient

In [0]:
w1.g, b1.g, w2.g, b2.g = [None]*4

เราจะใช้ ข้อมูลเดิม, Weight และ Bias เดิม รันผ่านโมเดล แล้ว Backpropagate เพื่อเช็ค Gradient ว่าโมเดลทำงานได้ถูกต้องหรือไม่

In [0]:
model = Model(w1, b1, w2, b2)
In [106]:
loss = model(x_train, y_train)
loss
Out[106]:
tensor(14467.1260)

เรียก backward ให้โมเดล ทำการ Backpropagation หา Gradient จาก Layer หลังสุดมาหน้าสุด

In [0]:
model.backward()
In [108]:
w1.g.mean(), w1.g.std()
Out[108]:
(tensor(0.0717), tensor(50.5915))

เทสว่าถูกต้อง ตรงกับที่คำนวนไว้ตอนแรก

In [0]:
test_near(w2g, w2.g)
test_near(b2g, b2.g)
test_near(w1g, w1.g)
test_near(b1g, b1.g)
test_near(ig, x_train.g)

ถูกต้อง ได้ค่าตรงกัน ไม่มี Error

8. Refactor Layer Class to Module

ใน Class ต่าง ๆ เราจะเห็นโค้ดซ้ำ ๆ กันอยู่ เช่น โค้ดสำหรับ cache ค่า input เราสามารถ Refactor โค้ดเหล่านี้ เป็น Abstract Class ชื่อ Module ขึ้นมา เพื่อให้ Layer ชนิดต่าง ๆ มา Inherit ได้

In [0]:
class Module():
    def __call__(self, *args):
        self.args = args
        self.out = self.forward(*args)
        return self.out
    def forward(self, inp):
        raise Exception('Not implemented')
    def backward(self):
        self.bwd(self.out, *self.args)        
In [0]:
class Relu(Module):
    def forward(self, inp):
        return inp.clamp_min(0.)
    def bwd(self, out, inp):
        inp.g = (inp>0).float()*out.g
In [0]:
class Lin(Module):
    def __init__(self, w, b):
        self.w, self.b = w, b
    def forward(self, inp):
        return inp @ self.w + self.b
    def bwd(self, out, inp):
        inp.g = out.g @ self.w.t()
        self.w.g = inp.squeeze(-1).t() @ out.g
        self.b.g = out.g.sum(0)
In [0]:
class Mse(Module):
    def forward(self, inp, targ):
        return (inp.squeeze() - targ).pow(2).mean()
    def bwd(self, out, inp, targ):
        inp.g = 2. * (inp.squeeze() - targ).unsqueeze(-1) / inp.shape[0]        

เราจะสร้างโมเดล Neural Networks 2 Hidden Layer จาก Module ที่เราเพิ่ง Refactor ด้านบน

เราจะใช้ ข้อมูลเดิม, Weight และ Bias เดิม รันผ่านโมเดล แล้ว Backpropagate เพื่อเช็ค Gradient ว่าโมเดลทำงานได้ถูกต้องหรือไม่

In [0]:
class Model():
    def __init__(self):
        self.layers = [Lin(w1, b1), Relu(), Lin(w2, b2)]
        self.loss = Mse()
    def __call__(self, x, targ):
#         set_trace()
        for l in self.layers:
            x = l(x)
        return self.loss(x, targ)
    def backward(self):
        self.loss.backward()        
        for l in reversed(self.layers):
            l.backward()

เคลียร์ Gradient

In [0]:
w1.g, b1.g, w2.g, b2.g = [None] * 4

สร้างโมเดล

In [0]:
model = Model()
In [0]:
loss = model(x_train, y_train)

เรียก backward ให้โมเดล ทำการ Backpropagation หา Gradient จาก Layer หลังสุดมาหน้าสุด

In [0]:
model.backward()

เทสว่าถูกต้อง ตรงกับที่คำนวนไว้ตอนแรก

In [0]:
test_near(w2g, w2.g)
test_near(b2g, b2.g)
test_near(w1g, w1.g)
test_near(b1g, b1.g)
test_near(ig, x_train.g)

ถูกต้อง ได้ค่าตรงกัน ไม่มี Error

9. Refactor Model to Module

ให้ nn.Module จัดการในส่วน Backprogation ของ Model ทำให้โค้ดสั้นลงอีก เปลี่ยน Lin Layer ไปใช้ PyTorch nn.Linear และเอา Loss Function ออกจากโมเดล

In [0]:
class Model(nn.Module):
    # number of input feature, number of hidden feature, number of output feature
    def __init__(self, n_in, nh, n_out):
        super().__init__()
#         set_trace()
        self.layers = [nn.Linear(n_in, nh), nn.ReLU(), nn.Linear(nh, n_out)]        
    def __call__(self, x):
        for l in self.layers:
            x = l(x)
        return x

10. สรุป

  1. เราได้เรียนรู้การสร้าง 2 Layers Deep Neural Networks ตั้งแต่เริ่มต้น คูณเมตริกซ์, Activation Function, Loss Function, ไปจน Backpropagation, หา Gradient ส่วนเรื่องการเทรนโมเดล ปรับ Hyperparameter เราจะอธิบายต่อไป
  2. เราได้ Refactor โค้ด นำมาสร้างเป็น Class, เป็น Module เพื่อเอาไว้ Reuse นำไปใช้สร้างโมเดล Deep Neural Networks ที่ซับซ้อนขึ้นอีก ในอนาคต
  3. เราได้ Initialize กำหนดค่าเริ่มต้นของ Weight และ Bias ด้วยค่า Random แบบ Normal Distribution (Gaussian)

Credit

In [0]:
 

แชร์ให้เพื่อน:

Keng Surapong on FacebookKeng Surapong on GithubKeng Surapong on Linkedin
Keng Surapong
Project Manager at Bua Labs
The ultimate test of your knowledge is your capacity to convey it to another.

Published by Keng Surapong

The ultimate test of your knowledge is your capacity to convey it to another.

Enable Notifications.    Ok No thanks