.. 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