13.13. Phân loại ảnh (CIFAR-10) trên Kaggle¶
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.
Chúng ta thử nghiệm trên tập dữ liệu CIFAR-10 trong Section 13.1. Đâ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
Hình Fig. 13.13.1 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.

Fig. 13.13.1 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”.¶
Trước tiên, nạp các gói và mô-đun cần cho cuộc thi này.
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()
13.13.1. Tải và Tổ chức tập dữ liệu¶
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 \(50,000\) ảnh. Tập kiểm tra chứa \(300,000\) ảnh, trong đó \(10,000\) ảnh được sử dụng để tính điểm, \(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 \(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 Fig. 13.13.1 hiển thị một số ảnh máy bay, xe hơi và chim trong tập dữ liệu.
13.13.2. Tải tập Dữ liệu¶
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 Fig. 13.13.1
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
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ụ.
Để 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 \(1000\) ảnh huấn luyện đầu tiên và \(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
.
#@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/'
Downloading ../data/kaggle_cifar10_tiny.zip from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_cifar10_tiny.zip...
13.13.3. Tổ chức tập Dữ liệu¶
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ó.
#@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())))
# training examples: 1000
# classes: 10
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 \(n\) là số ảnh của lớp có ít mẫu nhất, và
\(r\) là tỉ số thì ta sẽ dùng \(\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ó \(50,000\) ảnh, do đó ta sẽ có
\(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
\(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.
#@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
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.
#@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'))
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 đó.
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)
Chúng ta chỉ thiết lập kích thước batch là \(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à \(128\). Ta sử
dụng \(10\%\) mẫu huấn luyện làm tập kiểm định để tinh chỉnh các
siêu tham số.
batch_size = 4 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)
13.13.3.1. Tăng cường Ảnh¶
Để 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.
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])])
Để đả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.
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])])
13.13.3.2. Đọc tập Dữ liệu¶
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.
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']]
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.
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')
13.13.3.3. Định nghĩa Mô hình¶
Ở 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
Section 7.6 nhằm cải thiện hiệu suất thực thi.
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)
Tiếp theo, ta định nghĩa mô hình ResNet-18.
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
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.
def get_net(devices):
num_classes = 10
net = resnet18(num_classes)
net.initialize(ctx=devices, init=init.Xavier())
return net
loss = gluon.loss.SoftmaxCrossEntropyLoss()
13.13.3.4. Định nghĩa Hàm Huấn luyện¶
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.
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)}')
13.13.3.5. Huấn luyện và Kiểm định Mô hình¶
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.
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)
loss 2.308, train acc 0.121, valid acc 0.188
617.4 examples/sec on [gpu(0)]
13.13.3.6. Phân loại Tập Kiểm tra và Nộp Kết quả trên Kaggle¶
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.
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)
loss 2.356, train acc 0.107
640.2 examples/sec on [gpu(0)]
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 Section 4.10.
13.13.3.7. Tóm tắt¶
- 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.
13.13.3.8. Bài tập¶
- 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. - 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?
- 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?
13.13.3.9. Thảo luận¶
13.13.3.10. 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