Bài 2: Autograd | Deep Learning cơ bản
 

Bài 2: Autograd

| Posted in Pytorch

Bài toán Machine Learning

Machine Learning là gì

Machine là máy móc, Learning là học nên Machine Learning là dạy cho máy học. Bình thường mọi người học từ sách vở, từ youtube, từ bài giảng thầy cô. Thế máy học từ đâu? Máy học từ dữ liệu (data).

Thuật toán Machine Learning học từ dữ liệu

Ở một bài toán Machine Learning sẽ có 2 bước cơ bản:

  • Training: Ở bước này mình sẽ dùng data dạy cho máy học và cái máy học ra được gọi là model.
  • Prediction: Dùng model học được ở bước trên để đi dự đoán các giá trị mới.

Ví dụ như bài toán dự đoán giá nhà, input là các thông tin về căn nhà như diện tích nhà, số phòng ngủ, diện dích mặt tiền, khoảng cách đến trung tâm thương mại,… và output là giá căn nhà. Để dễ cho việc biểu diễn mình giả sử giá nhà chỉ phụ thuộc vào diện tích.

Dataset thì bên bất động sản sẽ cho mình những căn nhà đã được định giá trước đó, gồm diện tích và giá nhà. Bây giờ ở bước training mình sẽ học ra model.

Nhưng thực sự model là gì? Model là một hàm f nào đấy, hàm f có thể đơn giản hoặc phức tạp tùy vào thuật toán mình chọn. Ví dụ với thuật toán linear regression, f(x) = ax+b, hay với mạng Neural Network hàm f là một hàm hợp phức tạp, f(x) = f^{(L)}(...f^{(2)}(f^{(1)}(x)))

Ở bước training mình nói đi tìm model, chính là đi tìm hàm f, hay nói cách khác là tìm các tham số của hàm f. Trong trường hợp, thuật toán linear regression, mình sẽ đi tìm a, b, sao cho:

f(input) \simeq output

Các bước trong bài toán ML

Giả sử ở bước training, mình tìm được a = 13, b = 20. Do đó model của mình (hay hàm f) sẽ là:

f(x) = 13x + 20

Ở bước dự đoán, giờ có thông số 1 căn nhà mới, mình hoàn toàn có thể dự đoán được giá. Ví dụ, căn nhà có diện tích 60 sẽ có giá: 13 * 60 + 20 = 800.

Mọi người nên đọc về thuật toán Linear regressionGradient descent, ở series Deep Learning cơ bản mình đã viết kĩ rồi.

Các bước trong bài toán Machine Learning

Thông thường ở 1 bài toán về Deep Learning sẽ có các bước:

  • Visualize dữ liệu.
  • Preprocess dữ liệu.
  • Chọn model cho bài toán.
  • Tạo loss function.
  • Tối ưu loss function để tìm tham số của model bằng thuật toán gradient descent.

Ở thuật toán gradient descent, mình cần tính đạo hàm của loss function (L) với các tham số của model. Ở mô hình neural network, sẽ tính đạo hàm L với các tham số qua thuật toán backpropagation.

Phần này khá phức tạp, nên đa phần khi mọi người dùng các framework về DL, thì các thư viện sẽ tính đạo hàm giúp mọi người. Ở Pytorch cũng vậy, cơ chế tính đạo hàm trong Pytorch được gọi là Autograd (AUTOMATIC DIFFERENTIATION PACKAGE)

Autograd

Trước khi bắt đầu mọi người nên xem lại về chain rule.

Bài trước mình đã học về tensor rồi. Thì tất cả mọi thứ liên quan đến bài toán ML từ load dữ liệu, tham số của model, output đều là tensor.

Requires_grad

Để pytorch lưu và tính toán đạo hàm của 1 tensor, mình gán thuộc tính requires_grad = True cho tensor đó và Pytorch chỉ cho phép float tensor được gán thuộc tính requires_grad = True.

Bài trước khi mình khởi tạo tensor thì mặc định requires_grad = False.

a = torch.tensor([2., 3.])
a.requires_grad # False
# Gán thuộc tính requires_grad = True
a = torch.tensor([2., 3.], requires_grad=True)
a.requires_grad # True

# Hoặc
a = torch.tensor([2., 3.])
a.requires_grad = True

Thuộc tính requires_grad lây lan, tức là nếu a có requires_grad=True, thì các tensor khác được tính toán từ a, cũng sẽ có requires_grad=True.

a = torch.tensor([2., 3.], requires_grad=True)
a.requires_grad # True
b = a**2
c = 2*b
print(b.requires_grad) # True
print(c.requires_grad) # True

Kể cả phép tính của 1 tensor có requires_grad và 1 tensor không có requires_grad thì output vẫn ra 1 tensor có requires_grad

a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([1., 1.])

print(a.requires_grad) # True
print(b.requires_grad) # False

c = a + b # [3., 4.]
print(c.requires_grad) # True

Khi tensor có requires_grad, Pytorch sẽ xây computational graph.

x = torch.tensor([3])
y = torch.tensor([10])
a = torch.tensor([1.], requires_grad=True)
b = torch.tensor([2.], requires_grad=True)

y_hat = a*x + b
z = y_hat - y
L = z**2

Giả sử a, b là tham số của model, L là loss function. Giờ mình tính đạo hàm của L với a, b để thực hiện thuật toán gradient descent.

Computation Graph

Nhưng tensor màu đỏ có thuộc tính requires_grad = True, còn màu đen thì requires_grad = False. Những tensor được khoanh bởi hình tròn màu xanh lá cây được gọi là leaf tensor. Leaf tensor là những tensor do mình khởi tạo, chứ không phải do mình tạo ra trong quá trình tính toán như y_hat, z.

print(x.is_leaf) # True
print(a.is_leaf) # True
print(y_hat.is_leaf) # False
print(L.is_leaf) # False

Nhận xét: trong mô hình neural network thì tất cả các weight, bias đều là leaf tensor.

Trong Pytorch, để tính đạo hàm L với a, b, mình gọi hàm.

L.backward()

Khi đó Pyotrch sẽ tính đạo hàm của L với các leaf tensor có thuộc tính requires_grad = True và lưu vào thuộc tính grad của tensor. Để tính đạo hàm ngược lại thì Pytorch cũng dùng chain rule để tính.

Backward
\displaystyle\frac{\partial L}{\partial a} = \frac{\partial L}{\partial z} * \frac{\partial z}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial ax} * \frac{\partial ax}{\partial a} = 2z * 1 * 1 * x = 2zx \newline \newline \newline

Ví dụ như ở trên, x = 3, y_hat = 5, z = -5 nên \displaystyle\frac{\partial L}{\partial a} = -30

\displaystyle\frac{\partial L}{\partial b} = \frac{\partial L}{\partial z} * \frac{\partial z}{\partial \hat{y}} * \frac{\partial \hat{y}}{\partial b} = 2z = -10

Kết quả tính đạo hàm ngược của Pytorch tương tự

print(a.grad) # -30
print(b.grad) # -10

Function

Pytorch tính đạo hàm ngược cho chain rule như thế nào?

Các tensor không phải leaf node và có autograd = True sẽ có thuộc tính grad_fn để lưu lại phép tính thực hiện ở bước đấy, để tính chain rule ngược lại.

print(x.grad_fn) # None
print(a.grad_fn) # None
print(y_hat.grad_fn) # AddBackward0 
print(z.grad_fn) # SubBackward0 
print(L.grad_fn) # PowBackward0

Mình có thể dùng thư viện torchviz để visualize computational graph trong Pytorch.

from torchviz import make_dot
make_dot(L)
Computation Graph (by torchviz)

Các class MulBackward, AddBackward, SubBackward, PowBackward đều được kế thừa từ torch.nn.Autograd.Function, trong đó có 2 hàm quan trọng:

  • def forward(ctx, input): nhận các tensor inputs, và trả về tensor output. Biến ctx để lưu lại các tensor cần thiết trong quá trình backward (chain rule).
  • def backward(ctx, grad_output): grad_output chứa đạo hàm của loss đến tensor ở node đấy, ctx lấy các giá trị lưu ở hàm forward để tính đạo hàm ngược qua node đó.

Ví dụ: mình có thể custom hàm bình phương như thế này.

class MySquare(torch.autograd.Function):

  @staticmethod
  def forward(ctx, input):
    ctx.save_for_backward(input)
    return input**2

  @staticmethod
  def backward(ctx, grad_output):
    input, = ctx.saved_tensors
    return 2*input*grad_output

# alias để gọi hàm
my_square = MySquare.apply

# xây lại graph
x = torch.tensor([3])
y = torch.tensor([10])
a = torch.tensor([1.], requires_grad=True)
b = torch.tensor([2.], requires_grad=True)

y_hat = a*x + b
z = y_hat - y
L = my_square(z)
make_dot(L)
Custom function in Computation Graph

Và nếu mình tính backward ngược lại thì kết quả ra giống ở trên

L.backward()
print(a.grad) # -30
print(b.grad) # -10

Trong trường hợp tổng quát, hàm forward tính xuôi bình thường, còn hàm backward sẽ có dạng như thế này:

def backward (ctx, grad_output):
        # thuộc tính grad chỉ cần thiết nếu ở leaf tensor.
	self.Tensor.grad = grad_output

        # duyệt qua các input đến node này để trả đạo hàm ngược lại.
	for inp in self.inputs:
		if inp.grad_fn is not None:
                        # local_grad là đạo hàm trong node đang tính
			new_incoming_gradients = grad_output * local_grad(self.Tensor, inp)
			
			inp.grad_fn.backward(new_incoming_gradients)
		else:
			pass

Backward

Nếu mọi người gọi hàm backward với vector Tensor thì Pytorch sẽ báo lỗi.

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2*x + 1
y.backward() # RuntimeError: grad can be implicitly created only for scalar outputs

Mọi người thấy nó báo lỗi là chỉ backward được với 1 số thực thôi, thực ra cũng hợp lý vì loss function của mình cũng là 1 số thôi, nên hàm mặc định Pytorch chỉ hỗ trợ mọi người backward lại với 1 số thực.

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2*x + 1
z = sum(y)
z.backward()

print(x.grad) # 2, 2, 2
Đạo hàm số thực với vector

Mình tạm quy ước với nhau theo sách Math for Machine Learning, vector sẽ viết dạng cột. Còn hàm f từ 1 vector (x) sang 1 số thực, thì đạo hàm f với x sẽ là 1 vector dạng hàng. Như trong ví dụ trên:

Còn về chuyện đạo hàm của 1 vector với input là vector, output sẽ là 1 Jacobian matrix.

Jacobian matrix

Pytorch cũng hỗ trợ mọi người tính backward lại với tensor vector qua thuộc tính gradient, với ý nghĩa gradient ở đây được giả định là đạo hàm của loss function với tensor đang gọi backward.

gradient attribute của backward

Mọi người thấy, khi gọi y.backward mình truyền 1 vector tensor có kích thước bằng kích thước của y, ý nghĩa có thể hiểu đó chính là đạo hàm của loss với y.

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2*x + 1
y.backward(gradient=torch.tensor([1, 2, 1]))

print(x.grad) # 2, 4, 2
Ví dụ gradient của backward

Dynamic Computation Graph

Pytorch chỉ build computational graph khi hàm forward của tensor được gọi khi chạy chương trình.

x = torch.tensor([1., 2., 3.], requires_grad=True) # graph chưa được tạo
y = 2*x + 1 # bắt đầu tạo graph khi chạy qua dòng này

Khi tạo graph các non-leaf node (tensor) được cấp phát bộ nhớ, context (ctx) được tạo để lưu các biến tạm cho quá trình backward. Sau đó, khi mình gọi backward, thì đạo hàm được tính ngược lại cho các leaf tensor và sau đó graph bị hủy, vùng nhớ lưu các non-leaf node, biến tạm trong context được giải phóng. Do đó mình không thể backward 2 lần liên tiếp.

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2*x + 1
z = sum(y)
z.backward()
print(x.grad) # tensor([2., 2., 2.])
z.backward() # RuntimeError: Trying to backward through the graph a second time, but the saved intermediate results have already been freed. Specify retain_graph=True when calling .backward() or autograd.grad() the first time.

Tuy nhiên khi train model DL, mình cần train nhiều epoch, mỗi epoch lại có nhiều step, nên mình cần gọi backward nhiều lần để tính đạo hàm ngược lại. Để thực hiện backward nhiều lần mình cần để thuộc tính retain_graph = True.

loss.backward(retain_graph=True)

Tuy nhiên khi mình backward nhiều lần thì đạo hàm sẽ cộng dồn vào leaf tensor.

x = torch.tensor([1., 2., 3.], requires_grad=True)
y = 2*x + 1
z = sum(y)
z.backward(retain_graph=True)
print(x.grad) # 2, 2, 2
z.backward(retain_graph=True)
print(x.grad) # 4, 4, 4
z.backward(retain_graph=True)
print(x.grad) # 6, 6, 6

Thế nên khi ở mỗi step dùng gradient descent xong mình thường zero_grad trước khi sang step khác.

Pytorch xây Dynamic Computation Graph khi chạy chương trình khi qua hàm forward của non-leaf tensor, còn Tensorflow 1.x xây Static Computation Graphs, tức là Graph được xây trước, sau đó graph được chạy bằng cách “feed” giá trị vào các placeholder.

Với Dynamic Computation Graph, mình có thể thay đổi kiến trúc khi chạy chương trình. Do đó quá trình debug sẽ đơn giản hơn. Ở Tensorflow 2.x cũng sử dụng Dynamic Computation Graph, chi tiết mọi người xem thêm ở đây.

Tips

Requires_grad = False

Trong quá trình chạy mình cũng có thể gán requires_grad = False cho tensor, ví dụ để freeze một vài layers không cập nhật hệ số, khi đó tensor sẽ không tham gia vào computation graph.

x = torch.tensor([3])
y = torch.tensor([10])
a = torch.tensor([1.], requires_grad=True)
b = torch.tensor([2.], requires_grad=True)
c = a*x
y_hat = c + b
z = y_hat - y
L = z**2
make_dot(L)
x = torch.tensor([3])
y = torch.tensor([10])
a = torch.tensor([1.], requires_grad=True)
a.requires_grad = False
b = torch.tensor([2.], requires_grad=True)
c = a*x
y_hat = c + b
z = y_hat - y
L = z**2
make_dot(L)

Mọi người thấy khi để a.requires_grad = False, thì a và c để được bỏ ra khỏi computation graph.

torch.no_grad()

Trong quá trình inference thì mình không cần đạo hàm ngược lại nên không cần lưu giá trị biến tạm ở các node, mà đơn giản chỉ tính forward bình thường. Pytorch hỗ trợ context manager torch.no_grad cho việc này, khi nào mọi người không cần dùng backward thì hãy dùng torch.no_grad để giảm thiểu bộ nhớ và tính toán. Hay hiểu đơn giản hơn thì trong context của torch.no_grad thì tất cả các tensor có thuộc tính requires_grad = False.

with torch.no_grad:
	inference code goes here 

Ra ngoài context của torch.no_grad thì các thuộc tính của tensor autograd được dùng như bình thường.

Áp dụng Autograd cho bài toán linear regression

Mình sẽ dùng dữ liệu nhà ở trên để chạy thuật toán linear regression với Pytorch

Đầu tiên mình sẽ load dữ liệu

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch

# load dữ liệu, chuyển về dạng numpy
data = pd.read_csv('data_linear.csv').values

# chuyển dữ liệu về dạng torch
x = torch.tensor(data[:,0])
y = torch.tensor(data[:,1])

Model y = ax+b

# hàm model f(x) = ax+b
def model(x, a, b):
    return a * x + b

Hàm loss function

# hàm loss function, Mean Squared Error
def loss_fn(y_hat, y):
    squared_diffs = (y_hat - y)**2
    return squared_diffs.mean()

Hàm training

# Hàm training
def training_loop(n_epochs, learning_rate, params, x, y):
    a, b = params
    # Lưu loss qua epoch để vẽ đồ thị loss
    losses = []
    for epoch in range(1, n_epochs + 1):
        # nếu có grad ở tham số a, b thì zero đi, tránh trường hợp cộng dồn grad
        if a.grad is not None:
            a.grad.zero_()
        if b.grad is not None:
            b.grad.zero_()
        
        # xây model, loss
        y_hat = model(x, a, b)
        loss = loss_fn(y_hat, y)
        
        # gọi backward để tính đạo hàm ngược của loss với tham số a, b
        loss.backward()

        # update a,b bằng thuật toán gradient descent, để torch.no_grad thì mình không cần backward ở bước này
        with torch.no_grad():
            a -= learning_rate * a.grad
            b -= learning_rate * b.grad
        
        if epoch % 1 == 0:
            losses.append(loss.item())
            print('Epoch %d, Loss %f' % (epoch, float(loss)))
    return a, b, losses

Dự đoán giá trị mới

# Dự đoán giá trị mới, x = 50
x = torch.tensor(50)
with torch.no_grad():
    y_hat = model(x, a, b)
    print(y_hat)

Code bài này mọi người lấy ở đây.


Deep Learning cơ bản ©2024. All Rights Reserved.
Powered by WordPress. Theme by Phoenix Web Solutions