Bài 3: Neural Network
Trước khi bắt đầu bài này, mọi người nên xem các kiến thức về mạng Neural Network trước. Bài này chỉ tập trung vào xây dựng Neural Network trong Pytorch và các kĩ thuật liên quan.
Nội dung
Mô hình Neural Network
Một mô hình Neural Network sẽ có ít nhất 2 layers: input layer và output layer. Ngoài ra, mô hình còn có thể có các hidden layer.
Dữ liệu từ input layer sẽ được tính feedforward qua các hidden layer để ra được output layer.
Sau khi có giá trị dữ đoán \hat{y} thì mình sẽ tạo loss function và dùng thuật toán backpropagation để tính đạo hàm của loss với các tham số W, b, rồi dùng thuật toán gradient descent để cập nhật hệ số và tối ưu loss function.
Neural Network với Pytorch
Pytorch hỗ trợ thư viện torch.nn để xây dựng neural network. Nó bao gồm các khối cần thiết để xây dựng nên 1 mạng neural network hoàn chỉnh. Mỗi layer trong mạng gọi là một module và được kế thừa từ nn.Module. Mỗi module sẽ có thuộc tính Parameter (ví dụ W, b trong Linear Regression) để được tối ưu trong quá trình mô hình học.
Phần dưới mình sẽ xây dựng mô hình Linear regression sử dụng torch.nn
Linear regression
Mình sẽ dùng lại dataset bài trước, input là diện tích và output là giá nhà.
Về cơ bản thì linear regression cũng là một mô hình neural network đơn giản, chỉ có input layer và output layer, không có các hidden layer.
Mô hình linear regression
# nn.Linear(số_node_input, số node_output) trong layer đó
linear_model = nn.Linear(1, 1)
Mình có thể lấy ra được các tham số trong linear_model
list(linear_model.parameters())
# [Parameter containing: tensor([[14.9464]], requires_grad=True), Parameter containing:
tensor([1.1261], requires_grad=True)]
Trong mỗi layer sẽ gồm 2 Parameters là W và b tương ứng. Mình thấy là thuộc tính requires_grad của các Parameter đều là True để có thể tính backward loss và dùng gradient descent.
Loss thì mình dùng hàm nn.MSELoss() chứ không cần implement lại.
loss_fn = nn.MSELoss()
Trong bài trước mình phải tự tính L.backward() rồi thực hiện gradient descent bằng tay, tuy nhiên là Pytorch hỗ trợ các optimizer trong nn.optim để giúp mọi người thực hiện gradient descent luôn. Mình cần truyền cho optimizer biết là: mình muốn thực hiện gradient descent với những tham số nào (thường là tất cả các tham số trong model) cũng như learning rate là bao nhiêu.
optimizer = optim.SGD(linear_model.parameters(), lr=0.00004)
Sau đó ở mỗi epoch:
for epoch in range(1, n_epochs + 1):
y_hat = model(x)
loss = loss_fn(y_hat, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
Mình tính giá trị dự đoán, sau đó tính loss, rồi gọi loss.backward() để tính đạo hàm ngược lại cho các W, b. Tuy nhiên vì mỗi khi gọi loss.backward() thì đạo hàm ở các tham số sẽ cộng dồn lại, nên mình cần gọi zero_grad để gán đạo hàm ở các tham số bằng 0 trước khi gọi hàm backward(). Cuối cùng gọi optimizer.step() để thực hiện update gradient descent trên các tham số trong optimizer.
Khi mình cho dữ liệu vào model để học, thì thay vì cho học từng dữ liệu một, mình sẽ cho học nhiều điểm dữ liệu một lúc theo batch, dữ liệu mình truyền vào sẽ có kích thước (batch_size, 1) hay trong trường hợp tổng quát với NN là (batch_size, num_features), viết tắt (N*d).
x = torch.tensor(data[:,0], dtype=torch.float32) # x.shape (30)
# Thêm chiều vào vị trí tương ứng, ví dụ (30) -> (30, 1).
x = x.unsqueeze(1) # x.shape (30, 1)
Cuối cùng xong mình sẽ tìm được model, các điểm xanh dương là dữ liệu của mình. Còn đường đỏ là model dự đoán.
Neural Network
Mình sẽ xây dựng mạng CNN cho bài toán phân loại ảnh MNIST.
Dữ liệu ảnh trong dataset MNIST là ảnh xám và có kích thước 28*28. Bài toán input là 1 ảnh, output xem ảnh đấy là số mấy 0->9.
Mình sẽ dùng Pytorch để xây dựng mô hình CNN đơn giản.
Ở Pytorch khi xây dựng 1 model mới thì mình sẽ tạo 1 class kế thừa từ nn.Module cho model đấy. Sau đó khởi tạo các layers của model trong hàm __init__
def __init__(self):
super(Net, self).__init__()
# input 1 channel, output 32 channel, kernel size 3*3
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 32, 3, padding=1)
self.fc1 = nn.Linear(14*14*32, 128) # 14*14 from image dimension
self.fc2 = nn.Linear(128, 10)
Sau đó mình sẽ implement hàm forward, input sẽ là x, output sẽ là \hat{y}, giá trị dự đoán của model.
def forward(self, x):
# Max pooling 2*2
x = F.relu(self.conv1(x))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
# Flatten về dạng vector để input vào mạng NN
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
x đầu vào của hàm forward chính là input đầu vào của model, là 1 batch ảnh xám MNIST kích thước (batch_size, 28, 28, 1). Sau đó được lần lượt đi qua:
- conv1 (batch_size, 28, 28, 32) -> activation relu.
- conv2 (batch_size, 28, 28, 32) -> activation relu -> max pooling (batch_size, 14, 14, 32).
- view (batch_size, 14*14*32)
- fc1 (batch_size, 128) -> relu
- fc2 (batch_size, 10): output 10 node để thực hiện phân loại ảnh.
Mọi người có thể in các layer trong mạng ra
net = Net()
print(net)
'''
Net(
(conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=6272, out_features=128, bias=True)
(fc2): Linear(in_features=128, out_features=10, bias=True)
)
'''
Để lấy các tham số trong mạng
params = list(net.parameters())
print(len(params)) # 8, do có 4 layers, mỗi layer có 2 Parameter là W và b
Test thử feedforward của mạng, để xem kích thước, chiều đúng chưa
# shape: batch_size * depth * height * weight (N*C*H*W)
input = torch.randn(1, 1, 28, 28)
out = net(input)
print(out)
Bài này mình chỉ xây dựng model thôi, bài sau mình sẽ hướng dẫn train.
Code cho bài này mọi người xem ở đây.
Neural Network Tips
Hooks function
Như bài trước mình học về Autograd, thì mình biết là ở mỗi layer sẽ có 2 quá trình là forward và backward, thì Pytorch hook có 2 kiểu: forward hook và backward hook.
- Forward hook: sẽ được gọi khi hàm forward được gọi
- Backward hook: sẽ được gọi khi hàm backward được gọi.
Các hook function có thể được register với Module hoặc Tensor. Ví dụ: mình có thể register hook function cho conv layer để debug hoặc điều chỉnh weight, gradient của layer đó.
net = Net()
def print_info(self, input, output):
# input là 1 tuple các input
# output là 1 tensor, giá trị của output năm ở output.data
print('Inside ' + self.__class__.__name__ + ' forward')
print('')
print('input: ', type(input), ', len: ', len(input))
print('input[0]: ', type(input[0]), ', shape: ', input[0].shape)
print('output: ', type(output), ', len: ', len(output), output.data.shape)
# register 1 hàm thành forward hook
net.conv2.register_forward_hook(print_info)
# hàm forward hook sẽ được gọi khi model tính forward qua hàm conv1
out = net(input)
'''
Inside Conv2d forward
input: <class 'tuple'> , len: 1
input[0]: <class 'torch.Tensor'> , shape: torch.Size([1, 32, 28, 28])
output: <class 'torch.Tensor'> , len: 1 torch.Size([1, 32, 28, 28])
'''
Tương tự, mình cũng có thể register 1 hàm thành backward hook.
net = Net()
def print_backward_info(self, grad_input, grad_output):
# grad_input và grad_output đều là tuple
print('Inside ' + self.__class__.__name__ + ' backward')
print('grad_input: ', type(grad_input), ', len: ', len(grad_input))
print('grad_output: ', type(grad_output), ', len: ', len(grad_output))
print('grad_output[0]: ', type(grad_output[0]), ', size: ', grad_output[0].shape)
# register 1 hàm thành backward hook
net.conv1.register_backward_hook(print_backward_info)
out = net(input)
err = loss_fn(out, target)
# hàm backward hook sẽ được gọi khi loss backward qua layer conv1
err.backward()
'''
Inside Conv2d backward
grad_input: <class 'tuple'> , len: 3
grad_output: <class 'tuple'> , len: 1
grad_output[0]: <class 'torch.Tensor'> , size: torch.Size([1, 32, 28, 28])
'''
Hook có thể được sử dụng để:
- Debug bằng cách in ra, visualize giá trị của gradient, weight value, activation (feature map) value.
- Modify gradient trong quá trình backward.
Model __call__ vs forward
Về cơ bản thì khi mọi người build xong model thì kết quả của việc mọi người dùng __call__ (net(input)) hay dùng forward là như nhau.
input = torch.randn(1, 1, 28, 28)
out_call = net(input)
out_forward = net.forward(input)
out_call == out_forward # True
Tuy nhiên __call__ ngoài việc thực hiện forward sẽ gọi các hook nữa, thế nên khi dùng mọi người nên dùng __call__ và tránh dùng forward.
def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
hook(self, input)
result = self.forward(*input, **kwargs)
for hook in self._forward_hooks.values():
hook(self, input, result)
# TODO
return result
nn.relu vs F.relu
Mọi người để ý ở trên, để áp dụng ReLU activation mình dùng
F.relu(...)
Tuy nhiên trong module torch.nn cũng có
torch.nn.ReLU(...)
Về cơ bản thì 2 hàm thực hiện chức năng giống nhau, áp dụng ReLU activation function. Tuy nhiên điểm khác nhau lớn nhất là: F.relu như một hàm tính ReLU bình thường, tuy nhiên nn.ReLU tạo ra 1 nn.Module giống như các layer Linear, Conv2d. Do đó nn.ReLU có thể thêm vào nn.Sequential, cũng như register các hàm hook.
Những hàm để xây dựng model như Linear, Conv2d, ReLU, max_pool2d đều có cả 2 dạng nn và F. Nó phụ thuộc vào phong cách code và xây dựng model của mỗi người. Mình thì thường dùng nn với những hàm có tham số, còn không có tham số như ReLU, max_pool2d thì dùng functional (F).