13.14. Nhận diện Giống Chó (ImageNet Dogs) trên Kaggle

Trong phần này, ta sẽ giải quyết thử thách nhận diện giống chó trong một cuộc thi trên Kaggle. Cuộc thi có địa chỉ tại

Trong cuộc thi này, ta cần nhận diện 120 giống chó khác nhau. Tập dữ liệu trong cuộc thi này thực chất là một tập con của tập dữ liệu ImageNet nổi tiếng. Khác với ảnh trong tập dữ liệu CIFAR-10 được sử dụng trong phần trước, các ảnh trong tập dữ liệu ImageNet có chiều dài và chiều rộng lớn hơn, đồng thời kích thước của chúng không nhất quán.

Fig. 13.14.1 mô tả thông tin trên trang web của cuộc thi. Để có thể nộp kết quả, trước tiên vui lòng đăng kí tài khoảng trên Kaggle.

../_images/kaggle-dog.jpg

Fig. 13.14.1 Trang web cuộc thi nhận diện giống chó. Tập dữ liệu cho cuộc thi này có thể được truy cập bằng cách nhấn vào thẻ “Data”.

Đầu tiên, ta nhập vào các gói thư viện hoặc các mô-đun cần cho cuộc thi.

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 time

npx.set_np()

13.14.1. Tải xuống 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 bao gồm \(10,222\) ảnh và tập kiểm tra bao gồm \(10,357\) ảnh. Tất cả các ảnh trong hai tập đều có định dạng JPEG. Các ảnh này gồm có ba kênh (màu) RGB và có chiều cao và chiều rộng khác nhau. Có tất cả 120 giống chó trong tập huấn luyện, gồm có Chó tha mồi (Labrador), Chó săn vịt (Poodle), Chó Dachshund, Samoyed, Huskie, Chihuahua, và Chó sục Yorkshire (Yorkshire Terriers).

13.14.1.1. Tải tập dữ liệu

Sau khi đăng nhập vào Kaggle, ta có thể chọn thẻ “Data” trong trang web cuộc thi nhận diện giống chó như mô tả trong Fig. 13.14.1 và tải tập dữ liệu về bằng cách nhấn vào nút “Download All”. Sau khi giải nén tệp đã tải về trong thư mục ../data, bạn có thể tìm thấy toàn bộ tập dữ liệu theo các đường dẫn sau:

  • ../data/dog-breed-identification/labels.csv
  • ../data/dog-breed-identification/sample_submission.csv
  • ../data/dog-breed-identification/train
  • ../data/dog-breed-identification/test

Có thể bạn đã nhận ra rằng cấu trúc trên khá giống với cấu trúc thư mục của cuộc thi CIFAR-10 trong Section 13.13, trong đó thư mục train/test/ lần lượt chứa ảnh chó để huấn luyện và kiểm tra, và labels.csv chứa nhãn cho các ảnh huấn luyện.

Tương tự, để đơn giản, chúng tôi cung cấp một tập mẫu nhỏ của tập dữ liệu kể trên, “train_valid_test_tiny.zip”. Nếu bạn sử dụng tập dữ liệu đầy đủ cho cuộc thi Kaggle, bạn cần thay đổi biến demo phía dưới thành False.

#@save
d2l.DATA_HUB['dog_tiny'] = (d2l.DATA_URL + 'kaggle_dog_tiny.zip',
                            '75d1ec6b9b2616d2760f211f72a83f73f3b83763')

# If you use the full dataset downloaded for the Kaggle competition, change
# the variable below to False
demo = True
if demo:
    data_dir = d2l.download_extract('dog_tiny')
else:
    data_dir = os.path.join('..', 'data', 'dog-breed-identification')
Downloading ../data/kaggle_dog_tiny.zip from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_dog_tiny.zip...

13.14.1.2. Tổ chức Tập dữ liệu

Ta có thể tổ chức tập dữ liệu tương tự như cách ta đã làm trong Section 13.13, tức là tách riêng một tập kiểm định từ tập huấn luyện, sau đó đưa các ảnh vào từng thư mục con theo nhãn của chúng.

Hàm reorg_dog_data dưới đây được sử dụng để đọc nhãn của dữ liệu huấn luyện, tách riêng tập kiểm định và tổ chức tập huấn luyện.

def reorg_dog_data(data_dir, valid_ratio):
    labels = d2l.read_csv_labels(os.path.join(data_dir, 'labels.csv'))
    d2l.reorg_train_valid(data_dir, labels, valid_ratio)
    d2l.reorg_test(data_dir)


batch_size = 4 if demo else 128
valid_ratio = 0.1
reorg_dog_data(data_dir, valid_ratio)

13.14.2. Tăng cường Ảnh

Trong phần này, kích thước ảnh lớn hơn phần trước. Dưới đây là một số kỹ thuật tăng cường ảnh có thể sẽ hữu dụng.

transform_train = gluon.data.vision.transforms.Compose([
    # Randomly crop the image to obtain an image with an area of 0.08 to 1 of
    # the original area and height to width ratio between 3/4 and 4/3. Then,
    # scale the image to create a new image with a height and width of 224
    # pixels each
    gluon.data.vision.transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
                                                   ratio=(3.0/4.0, 4.0/3.0)),
    gluon.data.vision.transforms.RandomFlipLeftRight(),
    # Randomly change the brightness, contrast, and saturation
    gluon.data.vision.transforms.RandomColorJitter(brightness=0.4,
                                                   contrast=0.4,
                                                   saturation=0.4),
    # Add random noise
    gluon.data.vision.transforms.RandomLighting(0.1),
    gluon.data.vision.transforms.ToTensor(),
    # Standardize each channel of the image
    gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406],
                                           [0.229, 0.224, 0.225])])

Trong quá trình kiểm tra, ta chỉ sử dụng một số bước tiền xử lý ảnh nhất định.

transform_test = gluon.data.vision.transforms.Compose([
    gluon.data.vision.transforms.Resize(256),
    # Crop a square of 224 by 224 from the center of the image
    gluon.data.vision.transforms.CenterCrop(224),
    gluon.data.vision.transforms.ToTensor(),
    gluon.data.vision.transforms.Normalize([0.485, 0.456, 0.406],
                                           [0.229, 0.224, 0.225])])

13.14.3. Đọc Dữ liệu

Như trong phần trước, ta có thể tạo thực thể ImageFolderDataset để đọc dữ liệu chứa các tệp ảnh gốc.

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')]

Ở đây, ta tạo các thực thể DataLoader giống như trong Section 13.13.

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.14.4. Định nghĩa Mô hình

Dữ liệu cho cuộc thi này là một phần của tập dữ liệu ImageNet. Do đó, ta có thể sử dụng cách tiếp cận được thảo luận trong Section 13.2 để lựa chọn mô hình đã được tiền huấn luyện trên toàn bộ dữ liệu ImageNet và sử dụng nó để trích xuất đặc trưng ảnh làm đầu vào cho một mạng tùy biến cỡ nhỏ. Gluon cung cấp một số mô hình đã được tiền huấn luyện. Ở đây, ta sử dụng mô hình ResNet-34 đã được tiền huấn luyện. Do dữ liệu của cuộc thi là tập con của tập dữ liệu tiền huấn luyện, ta đơn thuần sử dụng lại đầu vào của tầng đầu ra mô hình đã được tiền huấn luyện làm đặc trưng được được trích xuất. Sau đó, ta có thể thay thế tầng đầu ra gốc bằng một mạng đầu ra tùy biến cỡ nhỏ để huấn luyện bao gồm hai tầng kết nối đầy đủ. Khác với thí nghiệm trong Section 13.2, ở đây ta không huấn luyện lại mô hình trích xuất đặc trưng đã được tiền huấn luyện. Điều này giúp giảm thời gian huấn luyện và bộ nhớ cần thiết để lưu trữ gradient của tham số mô hình.

Độc giả cần lưu ý, trong quá trình tăng cường ảnh, ta sử dụng giá trị trung bình và độ lệch chuẩn của ba kênh RGB lấy từ toàn bộ dữ liệu ImageNet để chuẩn hóa. Điều này giúp dữ liệu nhất quán với việc chuẩn hóa của mô hình tiền huấn luyện.

def get_net(devices):
    finetune_net = gluon.model_zoo.vision.resnet34_v2(pretrained=True)
    # Define a new output network
    finetune_net.output_new = nn.HybridSequential(prefix='')
    finetune_net.output_new.add(nn.Dense(256, activation='relu'))
    # There are 120 output categories
    finetune_net.output_new.add(nn.Dense(120))
    # Initialize the output network
    finetune_net.output_new.initialize(init.Xavier(), ctx=devices)
    # Distribute the model parameters to the CPUs or GPUs used for computation
    finetune_net.collect_params().reset_ctx(devices)
    return finetune_net

Khi tính toán mất mát, đầu tiên ta sử dụng biến thành viên features để lấy đầu vào của tầng đầu ra trong mô hình được tiền huấn luyện làm đặc trưng trích xuất. Sau đó, ta sử dụng đặc trưng này làm đầu vào cho mạng đầu ra tùy biến cỡ nhỏ và tính toán đầu ra.

loss = gluon.loss.SoftmaxCrossEntropyLoss()

def evaluate_loss(data_iter, net, devices):
    l_sum, n = 0.0, 0
    for features, labels in data_iter:
        X_shards, y_shards = d2l.split_batch(features, labels, devices)
        output_features = [net.features(X_shard) for X_shard in X_shards]
        outputs = [net.output_new(feature) for feature in output_features]
        ls = [loss(output, y_shard).sum() for output, y_shard
              in zip(outputs, y_shards)]
        l_sum += sum([float(l.sum()) for l in ls])
        n += labels.size
    return l_sum / n

13.14.5. Định nghĩa Hàm Huấn luyện

Ta sẽ lựa chọn mô hình và điều chỉnh siêu tham số dựa trên chất lượng mô hình trên tập kiểm định. Hàm huấn luyện mô hình train chỉ huấn luyện mạng đầu ra tùy biến cỡ nhỏ.

def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
          lr_decay):
    # Only train the small custom output network
    trainer = gluon.Trainer(net.output_new.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', 'valid loss'])
    for epoch in range(num_epochs):
        metric = d2l.Accumulator(2)
        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()
            X_shards, y_shards = d2l.split_batch(features, labels, devices)
            output_features = [net.features(X_shard) for X_shard in X_shards]
            with autograd.record():
                outputs = [net.output_new(feature) for feature in output_features]
                ls = [loss(output, y_shard).sum() for output, y_shard
                      in zip(outputs, y_shards)]
            for l in ls:
                l.backward()
            trainer.step(batch_size)
            metric.add(sum([float(l.sum()) for l in ls]), labels.shape[0])
            timer.stop()
            if (i + 1) % (num_batches // 5) == 0:
                animator.add(epoch + i / num_batches,
                             (metric[0] / metric[1], None))
        if valid_iter is not None:
            valid_loss = evaluate_loss(valid_iter, net, devices)
            animator.add(epoch + 1, (None, valid_loss))
    if valid_iter is not None:
        print(f'train loss {metric[0] / metric[1]:.3f}, '
              f'valid loss {valid_loss:.3f}')
    else:
        print(f'train loss {metric[0] / metric[1]:.3f}')
    print(f'{metric[1] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(devices)}')

13.14.6. 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ố dưới đây có thể được điều chỉnh: num_epochs, lr_periodlr_decay. Ví dụ, ta có thể tăng số lượng epoch. Do lr_periodlr_decay được thiết lập bằng 10 và 0.1, tốc độ học của thuật toán tối ưu sẽ được nhân với 0.1 sau mỗi 10 epoch.

devices, num_epochs, lr, wd = d2l.try_all_gpus(), 5, 0.01, 1e-4
lr_period, lr_decay, net = 10, 0.1, get_net(devices)
net.hybridize()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
      lr_decay)
train loss 2.394, valid loss 2.422
389.2 examples/sec on [gpu(0)]
../_images/output_kaggle-dog_vn_094b6a_21_1.svg

13.14.7. Dự đoán trên tập Kiểm tra và Nộp Kết quả lên Kaggle

Sau khi thu được một thiết kế mô hình và các siêu tham số vừa ý, ta sử dụng tất cả dữ liệu huấn luyện (bao gồm dữ liệu kiểm định) để huấn luyện lại mô hình, sau đó thực hiện dự đoán trên tập kiểm tra. Chú ý rằng các dự đoán được lấy từ mạng đầu ra mà ta đã huấn luyện.

net = get_net(devices)
net.hybridize()
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
      lr_decay)

preds = []
for data, label in test_iter:
    output_features = net.features(data.as_in_ctx(devices[0]))
    output = npx.softmax(net.output_new(output_features))
    preds.extend(output.asnumpy())
ids = sorted(os.listdir(
    os.path.join(data_dir, 'train_valid_test', 'test', 'unknown')))
with open('submission.csv', 'w') as f:
    f.write('id,' + ','.join(train_valid_ds.synsets) + '\n')
    for i, output in zip(ids, preds):
        f.write(i.split('.')[0] + ',' + ','.join(
            [str(num) for num in output]) + '\n')
train loss 2.332
386.6 examples/sec on [gpu(0)]
../_images/output_kaggle-dog_vn_094b6a_23_1.svg

Chạy đoạn mã trên sẽ sinh tệp “submission.csv”. Định dạng của tệp này nhất quán với yêu cầu của cuộc thi Kaggle. Cách thức nộp kết quả tương tự như trong Section 4.10.

13.14.8. Tóm tắt

  • Ta có thể sử dụng mô hình đã được tiền huấn luyện trên tập dữ liệu ImageNet để trích xuất đặc trưng và chỉ huấn luyện trên mạng đầu ra tùy biến cỡ nhỏ.
  • Điều này cho phép ta có thể thực hiện dự đoán trên tập con của tập dữ liệu ImageNet với chi phí bộ nhớ và tính toán thấp hơn.

13.14.9. Bài tập

  1. Khi sử dụng toàn bộ dữ liệu Kaggle, bạn sẽ thu được kết quả như thế nào khi tăng batch_size (kích thước batch) và num_epochs (số lượng epoch)?
  2. Bạn có đạt được kết quả tốt hơn nếu sử dụng mô hình đã được tiền huấn luyện sâu hơn không?
  3. Quét mã QR để tham gia thảo luận và trao đổi ý tưởng về các phương pháp đã được sử dụng và kết quả thu được từ cộng đồng Kaggle. Bạn có thể nghĩ ra một ý tưởng hay kỹ thuật tốt hơn không?

13.14.10. Thảo luận

13.14.11. 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
  • Đỗ Trường Giang
  • Nguyễn Văn Quang
  • Phạm Hồng Vinh
  • Nguyễn Văn Cường