.. raw:: html
.. _sec_kaggle_cifar10:
Phân loại ảnh (CIFAR-10) trên Kaggle
====================================
.. raw:: html
Cho đến lúc này, ta đang sử dụng gói ``data`` của Gluon để lấy trực tiếp
các tập dữ liệu dưới định dạng tensor. Tuy nhiên, trong thực tế thì các
tập dữ liệu ảnh thường ở định dạng tập tin. Trong phần này, ta sẽ sử
dụng các tập tin ảnh gốc và từng bước tổ chức, đọc và chuyển đổi các tập
tin này sang định dạng tensor.
.. raw:: html
Chúng ta thử nghiệm trên tập dữ liệu CIFAR-10 trong
:numref:`sec_image_augmentation`. Đây là một tập dữ liệu quan trọng
trong lĩnh vực thị giác máy tính. Bây giờ, ta sẽ áp dụng kiến thức đã
học ở các phần trước để tham gia vào cuộc thi phân loại ảnh CIFAR-10
trên Kaggle. Địa chỉ trang web của cuộc thi tại
https://www.kaggle.com/c/cifar-10
.. raw:: html
Hình :numref:`fig_kaggle_cifar10` cho biết thông tin trên trang web
của cuộc thi. Để nộp kết quả, vui lòng đăng ký một tài khoản Kaggle
trước.
.. raw:: html
.. _fig_kaggle_cifar10:
.. figure:: ../img/kaggle_cifar10.png
:width: 600px
Thông tin trang web cuộc thi phân loại ảnh CIFAR-10. Tập dữ liệu cho
cuộc thi có thể truy cập bằng cách chọn vào thẻ “Data”.
.. raw:: html
Trước tiên, nạp các gói và mô-đun cần cho cuộc thi này.
.. code:: python
import collections
from d2l import mxnet as d2l
import math
from mxnet import autograd, gluon, init, npx
from mxnet.gluon import nn
import os
import pandas as pd
import shutil
import time
npx.set_np()
.. raw:: html
Tải và Tổ chức tập dữ liệu
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. raw:: html
Dữ liệu cuộc thi được chia thành tập huấn luyện và tập kiểm tra. Tập
huấn luyện chứa :math:`50,000` ảnh. Tập kiểm tra chứa :math:`300,000`
ảnh, trong đó :math:`10,000` ảnh được sử dụng để tính điểm,
:math:`290,000` ảnh còn lại dùng để ngăn ngừa việc gán nhãn thủ công vào
tập kiểm tra rồi nộp kết quả đã gán nhãn. Định dạng ảnh trong cả hai tập
dữ liệu là PNG, với chiều cao và chiều rộng là 32 pixel với ba kênh màu
(RGB). Các ảnh được phân thành :math:`10` hạng mục: máy bay, xe hơi,
chim, mèo, nai, chó, ếch, ngựa, thuyền và xe tải. Góc trên bên trái của
:numref:`fig_kaggle_cifar10` hiển thị một số ảnh máy bay, xe hơi và
chim trong tập dữ liệu.
.. raw:: html
Tải tập Dữ liệu
~~~~~~~~~~~~~~~
.. raw:: html
Sau khi đăng nhập vào Kaggle, ta có thể chọn thẻ “Data” trên trang của
cuộc thi phân loại ảnh CIFAR-10 như trong :numref:`fig_kaggle_cifar10`
và tải tập dữ liệu này bằng cách nhấp chuột vào nút “Download All”. Sau
khi giải nén tập tin đã tải về vào thư mục ``../data``, và giải nén
``train.7z`` và ``test.7z`` trong tập tin này, bạn sẽ tìm thấy toàn bộ
tập dữ liệu ở đường dẫn thư mục sau:
- ../data/cifar-10/train/[1-50000].png
- ../data/cifar-10/test/[1-300000].png
- ../data/cifar-10/trainLabels.csv
- ../data/cifar-10/sampleSubmission.csv
.. raw:: html
Các thư mục ``train`` và ``test`` chứa các ảnh cho việc huấn luyện và
kiểm tra tương ứng, tập tin ``trainLabels.csv`` chứa các nhãn dùng cho
ảnh huấn luyện và tập tin ``sample_submission.csv`` là một tệp nộp ví
dụ.
.. raw:: html
Để việc bắt đầu đơn giản hơn, chúng tôi cung cấp một mẫu thu nhỏ của tập
dữ liệu này: chứa :math:`1000` ảnh huấn luyện đầu tiên và :math:`5` ảnh
kiểm tra ngẫu nhiên. Để sử dụng toàn bộ tập dữ liệu của cuộc thi Kaggle,
bạn cần đặt biến ``demo`` thành ``False``.
.. code:: python
#@save
d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip',
'2068874e4b9a9f0fb07ebe0ad2b29754449ccacd')
# If you use the full dataset downloaded for the Kaggle competition, set
# `demo` to False
demo = True
if demo:
data_dir = d2l.download_extract('cifar10_tiny')
else:
data_dir = '../data/cifar-10/'
.. parsed-literal::
:class: output
Downloading ../data/kaggle_cifar10_tiny.zip from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_cifar10_tiny.zip...
.. raw:: html
Tổ chức tập Dữ liệu
~~~~~~~~~~~~~~~~~~~
.. raw:: html
Ta cần tổ chức tập dữ liệu để thuận tiện cho việc huấn luyện và kiểm
tra. Hãy bắt đầu bằng cách đọc các nhãn từ tập tin csv. Hàm sau đây trả
về một từ điển thực hiện ánh xạ tên tập tin (không bao gồm phần mở rộng)
sang nhãn của nó.
.. code:: python
#@save
def read_csv_labels(fname):
"""Read fname to return a name to label dictionary."""
with open(fname, 'r') as f:
# Skip the file header line (column name)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]
return dict(((name, label) for name, label in tokens))
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# training examples:', len(labels))
print('# classes:', len(set(labels.values())))
.. parsed-literal::
:class: output
# training examples: 1000
# classes: 10
.. raw:: html
Kế tiếp, ta định nghĩa hàm ``reorg_train_valid`` để phân đoạn tập kiểm
định từ tập huấn luyện gốc. Tham số ``valid_ratio`` trong hàm này là tỉ
số của số mẫu trong tập kiểm định đối với số mẫu trong tập huấn luyện
gốc. Cụ thể, gọi :math:`n` là số ảnh của lớp có ít mẫu nhất, và
:math:`r` là tỉ số thì ta sẽ dùng :math:`\max(\lfloor nr\rfloor,1)` ảnh
trong mỗi lớp làm tập kiểm định. Ta hãy chọn ``valid_ratio=0.1`` làm ví
dụ. Vì tập ảnh huấn luyện gốc có :math:`50,000` ảnh, do đó ta sẽ có
:math:`45,000` ảnh dùng để huấn luyện và lưu ở thư mục
“``train_valid_test/train``” khi tinh chỉnh các siêu tham số, trong khi
:math:`5,000` ảnh còn lại sử dụng làm tập kiểm định sẽ được lưu ở thư
mục “``train_valid_test/valid``”. Sau khi tổ chức dữ liệu, ảnh của một
lớp sẽ được đặt ở cùng thư mục để đọc chúng sau này.
.. code:: python
#@save
def copyfile(filename, target_dir):
"""Copy a file into a target directory."""
d2l.mkdir_if_not_exist(target_dir)
shutil.copy(filename, target_dir)
#@save
def reorg_train_valid(data_dir, labels, valid_ratio):
# The number of examples of the class with the least examples in the
# training dataset
n = collections.Counter(labels.values()).most_common()[-1][1]
# The number of examples per class for the validation set
n_valid_per_label = max(1, math.floor(n * valid_ratio))
label_count = {}
for train_file in os.listdir(os.path.join(data_dir, 'train')):
label = labels[train_file.split('.')[0]]
fname = os.path.join(data_dir, 'train', train_file)
# Copy to train_valid_test/train_valid with a subfolder per class
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train_valid', label))
if label not in label_count or label_count[label] < n_valid_per_label:
# Copy to train_valid_test/valid
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'valid', label))
label_count[label] = label_count.get(label, 0) + 1
else:
# Copy to train_valid_test/train
copyfile(fname, os.path.join(data_dir, 'train_valid_test',
'train', label))
return n_valid_per_label
.. raw:: html
Hàm ``reorg_test`` dưới đây được dùng để tổ chức tập kiểm tra để thuận
tiện cho việc đọc tệp trong quá trình dự đoán.
.. code:: python
#@save
def reorg_test(data_dir):
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(data_dir, 'train_valid_test', 'test',
'unknown'))
.. raw:: html
Sau cùng, ta sử dụng một hàm để gọi các hàm ``read_csv_labels``,
``reorg_train_valid``, và ``reorg_test`` đã được định nghĩa trước đó.
.. code:: python
def reorg_cifar10_data(data_dir, valid_ratio):
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
reorg_train_valid(data_dir, labels, valid_ratio)
reorg_test(data_dir)
.. raw:: html
Chúng ta chỉ thiết lập kích thước batch là :math:`4` đối với tập dữ liệu
chạy thử. Trong suốt quá trình huấn luyện và kiểm thử thật sự, nên sử
dụng tập huấn luyện đầy đủ của cuộc thi Kaggle và ``batch_size`` nên
được thiết lập một giá trị số nguyên lớn hơn như là :math:`128`. Ta sử
dụng :math:`10\%` mẫu huấn luyện làm tập kiểm định để tinh chỉnh các
siêu tham số.
.. code:: python
batch_size = 4 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)
.. raw:: html
Tăng cường Ảnh
--------------
.. raw:: html
Để tránh hiện tượng quá khớp, ta sẽ áp dụng tăng cường ảnh. Ví dụ, ta có
thể lật ngẫu nhiên các ảnh bằng cách thêm
``transforms.RandomFlipLeftRight()``. Ta cũng có thể thực hiện chuẩn hóa
trên ba kênh màu RGB của ảnh bằng cách sử dụng
``transforms.Normalize()``. Dưới đây, chúng tôi liệt kê một số thao tác
tăng cường ảnh để bạn có thể lựa chọn sử dụng hoặc chỉnh sửa tùy theo
nhu cầu.
.. code:: python
transform_train = gluon.data.vision.transforms.Compose([
# Magnify the image to a square of 40 pixels in both height and width
gluon.data.vision.transforms.Resize(40),
# Randomly crop a square image of 40 pixels in both height and width to
# produce a small square of 0.64 to 1 times the area of the original
# image, and then shrink it to a square of 32 pixels in both height and
# width
gluon.data.vision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
ratio=(1.0, 1.0)),
gluon.data.vision.transforms.RandomFlipLeftRight(),
gluon.data.vision.transforms.ToTensor(),
# Normalize each channel of the image
gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
.. raw:: html
Để đảm bảo tính chắc chắn của đầu ra trong quá trình kiểm tra, ta chỉ
thực hiện chuẩn hóa trên ảnh.
.. code:: python
transform_test = gluon.data.vision.transforms.Compose([
gluon.data.vision.transforms.ToTensor(),
gluon.data.vision.transforms.Normalize([0.4914, 0.4822, 0.4465],
[0.2023, 0.1994, 0.2010])])
.. raw:: html
Đọc tập Dữ liệu
---------------
.. raw:: html
Tiếp theo, ta tạo đối tượng ``ImageFolderDataset`` để đọc tập dữ liệu đã
được tổ chức ở trên bao gồm các tệp ảnh gốc, trong đó mỗi ví dụ gồm có
ảnh và nhãn.
.. code:: python
train_ds, valid_ds, train_valid_ds, test_ds = [
gluon.data.vision.ImageFolderDataset(
os.path.join(data_dir, 'train_valid_test', folder))
for folder in ['train', 'valid', 'train_valid', 'test']]
.. raw:: html
Trong ``DataLoader`` ta chỉ rõ thao tác tăng cường ảnh đã xác định ở
trên. Trong suốt quá trình huấn luyện, ta chỉ sử dụng tập kiểm định để
đánh giá mô hình, do đó cần đảm bảo tính chắc chắn của đầu ra. Trong quá
trình dự đoán, ta sẽ huấn luyện mô hình trên tập huấn luyện và tập kiểm
định gộp lại để tận dụng tất cả dữ liệu có gán nhãn.
.. code:: python
train_iter, train_valid_iter = [gluon.data.DataLoader(
dataset.transform_first(transform_train), batch_size, shuffle=True,
last_batch='discard') for dataset in (train_ds, train_valid_ds)]
valid_iter = gluon.data.DataLoader(
valid_ds.transform_first(transform_test), batch_size, shuffle=False,
last_batch='discard')
test_iter = gluon.data.DataLoader(
test_ds.transform_first(transform_test), batch_size, shuffle=False,
last_batch='keep')
.. raw:: html
Định nghĩa Mô hình
------------------
.. raw:: html
Ở phần này, ta xây dựng các khối phần dư dựa trên lớp ``HybridBlock``,
khối này có đôi chút khác biệt so với cách lập trình trong
:numref:`sec_resnet` nhằm cải thiện hiệu suất thực thi.
.. code:: python
class Residual(nn.HybridBlock):
def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
super(Residual, self).__init__(**kwargs)
self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1,
strides=strides)
self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2D(num_channels, kernel_size=1,
strides=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm()
self.bn2 = nn.BatchNorm()
def hybrid_forward(self, F, X):
Y = F.npx.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return F.npx.relu(Y + X)
.. raw:: html
Tiếp theo, ta định nghĩa mô hình ResNet-18.
.. code:: python
def resnet18(num_classes):
net = nn.HybridSequential()
net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
nn.BatchNorm(), nn.Activation('relu'))
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.HybridSequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(Residual(num_channels, use_1x1conv=True, strides=2))
else:
blk.add(Residual(num_channels))
return blk
net.add(resnet_block(64, 2, first_block=True),
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
return net
.. raw:: html
Thử thách phân loại ảnh CIFAR-10 bao gồm 10 hạng mục. Ta sẽ thực hiện
khởi tạo ngẫu nhiên Xavier trên mô hình trước khi bắt đầu huấn luyện.
.. code:: python
def get_net(devices):
num_classes = 10
net = resnet18(num_classes)
net.initialize(ctx=devices, init=init.Xavier())
return net
loss = gluon.loss.SoftmaxCrossEntropyLoss()
.. raw:: html
Định nghĩa Hàm Huấn luyện
-------------------------
.. raw:: html
Ta tiến hành lựa chọn mô hình và điều chỉnh các siêu tham số tùy theo
kết quả của mô hình trên tập kiểm định. Tiếp theo, ta định nghĩa hàm
huấn luyện mô hình ``train``. Ta ghi lại thời gian huấn luyện mỗi epoch
nhằm giúp ta có thể so sánh thời gian mà các mô hình khác nhau yêu cầu.
.. code:: python
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay):
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'learning_rate': lr, 'momentum': 0.9, 'wd': wd})
num_batches, timer = len(train_iter), d2l.Timer()
animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs],
legend=['train loss', 'train acc', 'valid acc'])
for epoch in range(num_epochs):
metric = d2l.Accumulator(3)
if epoch > 0 and epoch % lr_period == 0:
trainer.set_learning_rate(trainer.learning_rate * lr_decay)
for i, (features, labels) in enumerate(train_iter):
timer.start()
l, acc = d2l.train_batch_ch13(
net, features, labels.astype('float32'), loss, trainer,
devices, d2l.split_batch)
metric.add(l, acc, labels.shape[0])
timer.stop()
if (i + 1) % (num_batches // 5) == 0:
animator.add(epoch + i / num_batches,
(metric[0] / metric[2], metric[1] / metric[2],
None))
if valid_iter is not None:
valid_acc = d2l.evaluate_accuracy_gpus(net, valid_iter, d2l.split_batch)
animator.add(epoch + 1, (None, None, valid_acc))
if valid_iter is not None:
print(f'loss {metric[0] / metric[2]:.3f}, '
f'train acc {metric[1] / metric[2]:.3f}, '
f'valid acc {valid_acc:.3f}')
else:
print(f'loss {metric[0] / metric[2]:.3f}, '
f'train acc {metric[1] / metric[2]:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(devices)}')
.. raw:: html
Huấn luyện và Kiểm định Mô hình
-------------------------------
.. raw:: html
Bây giờ ta có thể huấn luyện và kiểm định mô hình. Các siêu tham số sau
có thể được điều chỉnh: ``num_epochs``, ``lr_period`` và ``lr_decay``.
Ta có thể tăng số epoch. Để đơn giản, ở đây ta chỉ huấn luyện 5 epoch.
Do ``lr_period`` và ``lr_decay`` được đặt lần lượt bằng 50 và 0.1, tốc
độ học của thuật toán tối ưu sẽ giảm đi 10 lần sau mỗi 50 epoch.
.. code:: python
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.1, 5e-4
lr_period, lr_decay, net = 50, 0.1, get_net(devices)
net.hybridize()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)
.. parsed-literal::
:class: output
loss 2.308, train acc 0.121, valid acc 0.188
617.4 examples/sec on [gpu(0)]
.. figure:: output_kaggle-cifar10_vn_fe059a_31_1.svg
.. raw:: html
Phân loại Tập Kiểm tra và Nộp Kết quả trên Kaggle
-------------------------------------------------
.. raw:: html
Sau khi thu được thiết kế mô hình và các siêu tham số vừa ý, ta sử dụng
toàn bộ tập huấn luyện (bao gồm tập kiểm định) để huấn luyện lại mô hình
và tiến hành phân loại tập kiểm tra.
.. code:: python
net, preds = get_net(devices), []
net.hybridize()
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
lr_decay)
for X, _ in test_iter:
y_hat = net(X.as_in_ctx(devices[0]))
preds.extend(y_hat.argmax(axis=1).astype(int).asnumpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort(key=lambda x: str(x))
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.synsets[x])
df.to_csv('submission.csv', index=False)
.. parsed-literal::
:class: output
loss 2.356, train acc 0.107
640.2 examples/sec on [gpu(0)]
.. figure:: output_kaggle-cifar10_vn_fe059a_33_1.svg
.. raw:: html
Sau khi chạy đoạn mã trên, ta sẽ thu được tệp “submission.csv”. Tệp này
có định dạng phù hợp với yêu cầu của cuộc thi trên Kaggle. Cách thức nộp
kết quả giống với cách thức trong :numref:`sec_kaggle_house`.
Tóm tắt
-------
.. raw:: html
- Ta có thể tạo một đối tượng ``ImageFolderDataset`` để đọc tập dữ liệu
gồm có các tệp ảnh gốc.
- Ta có thể sử dụng mạng nơ-ron tích chập, tăng cường ảnh, và lập trình
hybrid để tham gia vào cuộc thi phân loại ảnh.
Bài tập
-------
.. raw:: html
1. Sử dụng tập dữ liệu CIFAR-10 đầy đủ cho cuộc thi trên Kaggle. Thay
đổi ``batch_size`` và ``num_epochs`` lần lượt bằng 128 và 100. Quan
sát độ chính xác và xem bạn có thể đạt thứ hạng bao nhiêu trong cuộc
thi này.
2. Bạn có thể đạt độ chính xác bằng bao nhiêu nếu không sử dụng tăng
cường ảnh?
3. Quét mã QR để truy cập các thảo luận liên quan và trao đổi ý tưởng về
các phương pháp được sử dụng và kết quả thu được với mọi người. Bạn
có khám phá ra kĩ thuật nào khác tốt hơn không?
Thảo luận
---------
- `Tiếng Anh - MXNet `__
- `Tiếng Việt `__
Những người thực hiện
---------------------
Bản dịch trong trang này được thực hiện bởi:
- Đoàn Võ Duy Thanh
- Lê Khắc Hồng Phúc
- Nguyễn Mai Hoàng Long
- Phạm Hồng Vinh
- Đỗ Trường Giang
- Phạm Hồng Vinh
- Nguyễn Văn Cường