Nhận diện chữ viết với PyTorch

Nhận diện chữ viết với PyTorch

Nội dung bài viết

  1. Giới thiệu
  2. Xây dựng mô hình bằng PyTorch
  3. Kết luận
  4. Bài tiếp theo

Giới thiệu

Trong bài viết này, mình sẽ hướng dẫn các bạn xây dựng mô hình nhận diện chữ viết trên tập dữ liệu MNIST bằng cách sử dụng PyTorch.

Tập dữ liệu MNIST

MNIST được giới thiệu năm 1998 bởi Yann Lecun và cộng sự nhằm đánh giá các mô hình phân lớp. MNIST là tập dữ liệu chữ viết từ 0 đến 9.

png

Trong đó, mỗi hình là một ảnh đen trắng chứa một số được viết tay có kích thước là 28x28. Bộ dataset vô cùng đồ sộ với khoảng 60k data training và 10k data test và được sử dụng phổ biến trong các thuật toán nhận dạng ảnh.

Website chính thức của tập dữ liệu: http://yann.lecun.com/exdb/mnist/.

Source code

Toàn bộ mã nguồn sử dụng trong bài viết được cung cấp tại link sau: quanhua92/deeplearning.vn

Xây dựng mô hình bằng PyTorch

Cấu trúc project

Trong project này, mình sẽ dùng 2 file là main.pymodels.py. Trong đó, main.py sẽ chứa các đoạn code để thực hiện quá trình training và models.py sẽ chứa mô hình network được dùng trong main.py.

.
├── main.py
└── models.py

Import thư viện và một số tham số

Đầu tiên, chúng ta cần import một số thư viện sẽ sử dụng trong file main.py.

# main.py
import torch
import matplotlib.pyplot as plt
import numpy as np
import torchvision

from torch import nn, optim
from torchvision import datasets, transforms

Ngoài ra, chúng ta sẽ định nghĩa một số tham số sẽ dùng trong phần sau bao gồm:

  • batch_size: Lượng ảnh sẽ dùng trong 1 batch.
  • learning_rate: Learning Rate sẽ dùng với optimizer.
  • num_epochs: Số lần sử dụng toàn bộ dữ liệu để train.
batch_size = 32
learning_rate = 0.01
num_epochs = 20

Data Loader

Tiếp theo, chúng ta sẽ sử dụng package datasets.MNIST có sẵn trong torchvision để tự động tải về bộ dữ liệu vào thư mục data và áp dụng một số transforms. Dữ liệu MNIST sẽ có miền giá trị [0, 1]. Do đó, để chuẩn hóa dữ liệu về mean bằng 0 và std bằng 1, ta cần sử dụng hàm Normalize với mean của toàn dữ liệu bằng 0.1307 và std bằng 0.3081.

# Data Loader
train_loader = torch.utils.data.DataLoader(
        datasets.MNIST("data", train=True, download=True,
                       transform=transforms.Compose([
                               transforms.ToTensor(),
                               transforms.Normalize((0.1307, ), (0.3081, )),
                               ])),
        batch_size=batch_size,
        shuffle=True)

val_loader = torch.utils.data.DataLoader(
        datasets.MNIST("data", train=False, download=True,
                       transform=transforms.Compose([
                               transforms.ToTensor(),
                               transforms.Normalize((0.1307, ), (0.3081, )),
                               ])),
        batch_size=batch_size,
        shuffle=False)

Chúng ta sẽ thử hiển thị một số hình có trong bộ dữ liệu bằng đoạn code sau:

# Visualize Data
def imshow(img, mean, std):
    img = img / std + mean # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

dataiter = iter(train_loader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images), 0.1307, 0.3081)
print(labels)

Bạn sẽ có được một hình tương tự như sau:

png

Ngoài ra, trong console, bạn sẽ thấy nhãn của từng hình như sau:

tensor([1, 8, 1, 1, 4, 9, 1, 3, 0, 8, 8, 0, 0, 1, 4, 1, 4, 1, 0, 9, 4, 8, 4, 5,
        5, 6, 6, 3, 3, 8, 4, 2])

Bên cạnh đó, nếu bạn thử gọi images.shapelabels.shape. Bạn sẽ có kết quả như sau:

  • images.shape: torch.Size([32, 1, 28, 28]). Trong đó, 32 là batch size, 1 là số channel, 28 là chiều cao ảnh và 28 cuối là chiều rộng của ảnh. Do format mặc định của PyTorch là NCHW.
  • labels.shape: torch.Size([32]). Ở đây, chúng ta có 32 nhãn tương ứng với batch size là 32.

Model

Như vậy, chúng ta đã có thể load được dữ liệu để train và validation. Tiếp theo, chúng ta sẽ xây dựng một mạng Convolution Neural Network (CNN) đơn giản với 1 layer conv và 1 layer fully connected. Đoạn code sau sẽ ở trong file models.py:

# models.py
from torch import nn

class SimpleModel(nn.Module):
    def __init__(self, num_classes=10):
        super(SimpleModel, self).__init__()

        self.conv1 = nn.Sequential(
                nn.Conv2d(1, 32, kernel_size=5, padding=2, stride=1),
                nn.ReLU(),
                nn.MaxPool2d(kernel_size=2, stride=2))

        self.fc = nn.Linear(14 * 14 * 32, num_classes)

    def forward(self, x):
        out = self.conv1(x)
        out = out.view(out.size(0), -1)
        out = self.fc(out)

        return out

Ở đây, mình tạo một class SimpleModel kế thừa từ nn.Module. Đây sẽ là mô hình mạng chúng ta sẽ dụng trong file main.py.

Mình tạo 1 lớp là self.conv bao gồm các bước là nn.Conv2d -> nn.Relu() -> nn.MaxPool2d.

  • nn.Conv2d: có input là 1 channel và output là 32 channels. Ngoài ra, kernel_size là 5 với padding bằng 2 và stride 1. Do đó, output của lớp nn.Conv2d này sẽ là một feature map có kích thước là [N, 32, 28, 28].
  • nn.Relu: là activation function RELU
  • nn.MaxPool2d: là max pooling layer. Trong đó ta dùng kernel_size bằng 2 và stride bằng 2. Do đó, output của lớp này là feature map có kích thước là [N, 32, 14, 14].

Ngoài ra, mình còn tạo ra một lớp fully connected là self.fc với input là 14 * 14 * 32 khớp với output của lớp self.conv1 và output là số lượng class bằng 10.

Trong hàm forward với input x là một batch có kích thước [N, 32, 28, 28]. Đầu tiên, mình cho x qua lớp self.conv1. Sau đó, mình dùng hàm view để chuyển biến out từ một tensor 4 chiều [N, 32, 14, 14] thành một tensor 2 chiều là [N, 32 * 14 * 14]. Trong đó, out.size(0) chính là batch_size ban đầu bằng 32.

Quay lại file main.py, chúng ta sẽ dùng GPU nếu có thể. Do đó, mình tạo một biến device để có thể dùng GPU hoặc CPU tùy theo máy của bạn.

# main.py
# Get Device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Chúng ta sẽ tạo một biến model bằng class SimpleModel đã tạo ở trước. Ở đây, mình cũng dùng hàm .to để chuyển model vào GPU nếu có.

# main.py
# Model
from models import SimpleModel
model = SimpleModel().to(device)

Loss Function và Optimizer

Chúng ta cần tạo loss function và optimizer để có thể train model. MNIST có 10 class nên đây là một bài toán phân lớp với nhiều class. Ngoài ra, model ở trên không sử dụng lớp Softmax ở cuối nên chúng ta sẽ dùng CrossEntropyLoss để PyTorch chủ động tính loss bằng Log Softmax và Negative Log Likelihood loss.

Bên cạnh đó, chúng ta sẽ dùng optimizer có sẵn trong package optim của PyTorch là optim.SGD với learning rate được định nghĩa ở đầu bài viết.

# Loss function
criterion = nn.CrossEntropyLoss()

# Optimizer
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

Training

Sau khi có đầy đủ data, model, loss function và optimizer. Chúng ta đã có thể tiến hành train model.

# main.py
num_steps = len(train_loader)

for epoch in range(num_epochs):

    # ---------- TRAINING ----------
    # set model to training
    model.train()

    total_loss = 0

    for i, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)

        # Zero gradients
        optimizer.zero_grad()

        # Forward
        outputs = model(images)

        # Compute Loss
        loss = criterion(outputs, labels)

        # Backward
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

        # Print Log
        if (i + 1) % 100 == 0:
            print("Epoch {}/{} - Step: {}/{} - Loss: {:.4f}".format(
                    epoch, num_epochs, i, num_steps, total_loss / (i + 1)))

Ở đây, các bạn cần chú ý là phải gọi optimizer.zero_grad() trước khi gọi loss.backward() để xóa hết gradient của batch cũ trước khi tính gradient cho batch hiện tại. Bên cạnh đó, mình cũng gọi hàm to của imageslabels để chuyển data vào GPU nếu có.

Validation

Bước validation cũng được thực hiện trong mỗi epoch để ta có thể biết được độ chính xác của từng epoch.

    # ---------- VALIDATION ----------
    # set model to evaluating
    model.eval()

    val_losses = 0

    with torch.no_grad():
        correct = 0
        total = 0
        for _, (images, labels) in enumerate(val_loader):
            images, labels = images.to(device), labels.to(device)

            outputs = model(images)

            _, predicted = torch.max(outputs, 1)

            loss = criterion(outputs, labels)

            val_losses += loss.item()

            total += labels.size(0)
            correct += (predicted == labels).sum().item()

        print("Epoch {} - Accuracy: {} - Validation Loss : {:.4f}".format(
                epoch,
                correct / total,
                val_losses / (len(val_loader))))

Tới đây, chúng ta đã có đầy đủ code để tiến hành training và validation. Các bạn có thể vào mục Source Code để lấy toàn bộ mã nguồn.

Tiến hành training và validation

Khi chạy toàn bộ đoạn code trên, chúng ta sẽ có kết quả như sau:

Epoch 1/20 - Step: 99/1875 - Loss: 0.7878
Epoch 1/20 - Step: 199/1875 - Loss: 0.5821
Epoch 1/20 - Step: 299/1875 - Loss: 0.4925
Epoch 1/20 - Step: 399/1875 - Loss: 0.4436
Epoch 1/20 - Step: 499/1875 - Loss: 0.4060
Epoch 1/20 - Step: 599/1875 - Loss: 0.3769
Epoch 1/20 - Step: 699/1875 - Loss: 0.3541
Epoch 1/20 - Step: 799/1875 - Loss: 0.3372
Epoch 1/20 - Step: 899/1875 - Loss: 0.3196
Epoch 1/20 - Step: 999/1875 - Loss: 0.3076
Epoch 1/20 - Step: 1099/1875 - Loss: 0.2957
Epoch 1/20 - Step: 1199/1875 - Loss: 0.2855
Epoch 1/20 - Step: 1299/1875 - Loss: 0.2782
Epoch 1/20 - Step: 1399/1875 - Loss: 0.2696
Epoch 1/20 - Step: 1499/1875 - Loss: 0.2611
Epoch 1/20 - Step: 1599/1875 - Loss: 0.2528
Epoch 1/20 - Step: 1699/1875 - Loss: 0.2457
Epoch 1/20 - Step: 1799/1875 - Loss: 0.2402
Epoch 1 - Accuracy: 0.9678 - Validation Loss : 0.1147
Epoch 1/20 - Step: 99/1875 - Loss: 0.1141
..............................
Epoch 20/20 - Step: 99/1875 - Loss: 0.0206
Epoch 20/20 - Step: 199/1875 - Loss: 0.0185
Epoch 20/20 - Step: 299/1875 - Loss: 0.0176
Epoch 20/20 - Step: 399/1875 - Loss: 0.0185
Epoch 20/20 - Step: 499/1875 - Loss: 0.0188
Epoch 20/20 - Step: 599/1875 - Loss: 0.0193
Epoch 20/20 - Step: 699/1875 - Loss: 0.0197
Epoch 20/20 - Step: 799/1875 - Loss: 0.0197
Epoch 20/20 - Step: 899/1875 - Loss: 0.0195
Epoch 20/20 - Step: 999/1875 - Loss: 0.0197
Epoch 20/20 - Step: 1099/1875 - Loss: 0.0196
Epoch 20/20 - Step: 1199/1875 - Loss: 0.0193
Epoch 20/20 - Step: 1299/1875 - Loss: 0.0196
Epoch 20/20 - Step: 1399/1875 - Loss: 0.0194
Epoch 20/20 - Step: 1499/1875 - Loss: 0.0198
Epoch 20/20 - Step: 1599/1875 - Loss: 0.0201
Epoch 20/20 - Step: 1699/1875 - Loss: 0.0201
Epoch 20/20 - Step: 1799/1875 - Loss: 0.0199
Epoch 20 - Accuracy: 0.9873 - Validation Loss : 0.0389

Các bạn có thể thấy rằng, ở epoch đầu tiên, chúng ta có độ chính xác trên tập validation là 96.78 %. Sau khi hoàn thành 20 epochs, chúng ta đã có được độ chính xác là 98.73 %.

Kết luận

Chúng ta đã hoàn thành được toàn bộ pipeline từ xây dựng data, tạo model và tiến hành huấn luyện mô hình CNN cho tập dữ liệu MNIST với độ chính xác là 98.73 %.

Các bạn có thể thấy rằng việc sử dụng PyTorch cho phép chúng ta tiếp cận sâu vào tất cả các thành phần của quá trình huấn luyện một cách đơn giản và vẫn đạt được hiệu suất tối ưu. Đối với framework deep learning khác như Keras, các bạn có thể làm rất đơn giản nhưng khi bạn cần can thiệp sâu vào thì khá khó. Còn đối với framework như Tensorflow thì theo mình thấy phức tạp hơn PyTorch rất nhiều.

Bài tiếp theo

Trong bài tiếp theo, mình sẽ viết về cách chọn mô hình để đạt kết quả tốt hơn bao gồm số channels, số lượng lớp, data augmentation …

comments powered by Disqus