+5

Cơ bản về fastai (P2) - DataBlock API

Mayfest2023 ContentCreator

TIếp nối bài viết lần trước về các tính năng mì ăn liền của fastai, trong bài viết hôm nay, mình sẽ giới thiệu cho các bạn về để xử lý dữ liệu và hệ thống callback của fastai. Let's get started.

DataBlock API

Thư viện fastai được thiết kế theo kiểu phân tầng. Ở trên cùng là tầng applications cho phép chúng ta train mô hình chỉ với vài dòng code như đã thấy ở bài viết trước.

ảnh.png

Ví dụ để tạo DataLoaders cho bài toán phân loại ảnh, ta có thể dùng đoạn code sau:

path = untar_data(URLs.MNIST)
dls = ImageDataLoaders.from_folder(path=path, train='training', valid='testing')

Các phương thức có sẵn trong fastai như from_folder sẽ khá là tiện nếu data của chúng ta được tổ chức theo chuẩn như kiểu bộ MNIST. Nhưng trên thực tế thì data có muôn hình vạn trạng, không có theo format cụ thể nào cả. Vì thế, fastai cung cấp cho ta một API khá linh hoạt để load data gọi là DataBlock API.

Thử inspect source code của ImageDataLoaders.from_folder?? trên jupyter ta sẽ thấy đoạn code sau:

ImageDataLoaders.from_folder(
    path,
    train='train',
    valid='valid',
    valid_pct=None,
    seed=None,
    vocab=None,
    item_tfms=None,
    batch_tfms=None,
    *,
    bs: 'int' = 64,
    val_bs: 'int' = None,
    shuffle: 'bool' = True,
    device=None,
)
Source:   
    @classmethod
    @delegates(DataLoaders.from_dblock)
    def from_folder(cls, path, train='train', valid='valid', valid_pct=None, seed=None, vocab=None, item_tfms=None,
                    batch_tfms=None, **kwargs):
        "Create from imagenet style dataset in `path` with `train` and `valid` subfolders (or provide `valid_pct`)"
        splitter = GrandparentSplitter(train_name=train, valid_name=valid) if valid_pct is None else RandomSplitter(valid_pct, seed=seed)
        get_items = get_image_files if valid_pct else partial(get_image_files, folders=[train, valid])
        dblock = DataBlock(blocks=(ImageBlock, CategoryBlock(vocab=vocab)),
                           get_items=get_items,
                           splitter=splitter,
                           get_y=parent_label,
                           item_tfms=item_tfms,
                           batch_tfms=batch_tfms)
        return cls.from_dblock(dblock, path, path=path, **kwargs)

Trong phương thức from_folder, một object gọi là DataBlock được khởi tạo. Các bạn có thể nghĩ 1 DataBlock là một list các chỉ dẫn để khởi tạo DataLoaders:

  • Định dạng data là gì? (ảnh, audio, text, tabular data, ...)
  • Làm thế nào để tạo 1 list các phần tử trong dataset? (ví dụ từ đường dẫn của bộ MNIST, là sao để tạo 1 list tất cả đường dẫn tới ảnh trong dataset.
  • Label từng phần tử trong list như thế nào?
  • Transform các phần tử trong dataset như thế nào?
  • Tạo tập validation như thế nào?

TIếp theo hãy thử tạo tự tạo DataBlock nhé. Đầu tiên ta bắt đầu với 1 DataBlock trống

dblock = DataBlock()

Khi đứng 1 mình thì DataBlock chỉ là 1 list các chỉ dẫn cách để tập hợp data. Muốn truy cập vào data của DataBlock thì trước hết phát chỉ cho nó biết lấy data từ đâu. Ở dưới ta dùng hàm get_image_files trong fastai để lấy tất cả đường dẫn file ảnh trong path.

path = untar_data(URLs.MNIST)
fnames = get_image_files(path)

Ta có thể dùng DataBlock để convert source thành 2 object DatasetsDataLoaders bằng 2 phương thức DataBlock.datasets hoặc DataBlock.dataloaders. DataLoaders thì các bạn đã thấy ở phần trước rồi, còn Datasets cũng tương tự, chỉ là wrapper của training Dataset và validation Dataset. Do source của chúng ta là một list các đường dẫn không đóng thành batch được, gọi .dataloaders ở đây sẽ bị lỗi nên ở đây mình sẽ convert về Datasets:

dsets = dblock.datasets(source=fnames)
print(dsets.train[0])

Output:

(Path('/home/root/.fastai/data/mnist_png/training/7/59513.png'), Path('/home/root/.fastai/data/mnist_png/training/7/59513.png'))

Mặc định, DataBlock sẽ cho rằng data của chúng ta có 2 thứ: input cho mô hình và label nên đường dẫn bị lặp lại 2 lần ở đây. Một cách làm khác là ta có thể set tham số get_items của DataBlock bằng hàm get_image_files:

dblock = DataBlock(get_items=get_image_files)
dsets = dblock.datasets(source=path)
print(dsets.train[0])

Bây giờ thì thay vì để source là list các đường dẫn, ta chỉ cần truyền vào thư mục gốc và DataBlock sẽ tự gọi get_image_files để lấy list các đường dẫn.

Tiếp theo, ta cần cho DataBlock biết cách để trích xuất label của ảnh từ đường dẫn của nó. Bộ dữ liệu MNIST được format như sau

ảnh.png

Trong mỗi thư mục từ 0-9 là các ảnh tương ứng với class đó. Vì vậy, ta cần viết 1 function để convert từ đường dẫn Path('/home/root/.fastai/data/mnist_png/training/7/59513.png') thành 7:

def parent_label(fname: Path):
    return Path(fname).parent.name

Tiếp theo ta truyển hàm label vào DataBlock để lấy label từ đường dẫn:

dblock = DataBlock(
    get_items=get_image_files,
    get_y=parent_label
)
dsets = dblock.datasets(path)
print(dsets.train[0])

Output:

(Path('/home/root/.fastai/data/mnist_png/training/5/50497.png'), '5')

Okay. Giờ ta đã có input và target. Việc tiếp theo cần làm là chỉ cho DataBlock cách xử lý chúng như thế nào. Với input thì là dạng ảnh còn target thì là dạng category. Các dạng dữ liệu khác nhau được biểu diễn bằng các block trong DataBlock, ở đây ta sẽ dùng ImageBlockCategoryBlock

dblock = DataBlock(
    blocks=(ImageBlock(cls=PILImageBW), CategoryBlock),
    get_items=get_image_files,
    get_y=parent_label
)
dsets = dblock.datasets(path)
print(dsets.train[0])

Output:

(PILImageBW mode=L size=28x28, TensorCategory(5))

Ta có thể thấy DataBlock đã tự thêm các transform cần thiết để đọc ảnh (trong fastai họ dùng thư viện pillow) và convert label từ string sang tensor. Các bạn có thể truy cập vào mapping giữa các category và index tương ứng qua dsets.vocab:

['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

Tiếp theo ta cần chỉ cho DataBlock cách chia training và validation set. Với bộ MNIST ta có thể dùng GrandparentSplitter hoặc nếu bạn thích chia ngẫu nhiên thì có thể dùng RandomSplitter:

dblock = DataBlock(
    blocks=(ImageBlock(cls=PILImageBW), CategoryBlock),
    get_items=get_image_files,
    get_y=parent_label,
    splitter=GrandparentSplitter(train_name='training', valid_name='testing')
    #splitter=RandomSplitter()
)

Thường thì khi train mô hình, ta thường train theo từng mini-batch, tức là input đầu vào phải cùng kích thước với nhau. Nên khi viết Dataset bằng Pytorch ta thường có thêm bước resize ảnh về cùng một kích thước hoặc thêm data augmentation cho ảnh. Trong DataBlock thì ta có thể thực hiện bước này thông qua item_tfms (MNIST thì cùng kích thước sẵn rồi nên resize ở đây để demo là chính

dblock = DataBlock(
    blocks=(ImageBlock(cls=PILImageBW), CategoryBlock),
    get_items=get_image_files,
    get_y=parent_label,
    splitter=GrandparentSplitter(train_name='training', valid_name='testing'),
    item_tfms=Resize(32)
)

Ngoài item transform thì fastai còn support batch transform khá là hữu ích cho việc normalize dữ liệu hay support các augmentation chạy trên GPU

dblock = DataBlock(
    blocks=(ImageBlock(cls=PILImageBW), CategoryBlock),
    get_items=get_image_files,
    get_y=parent_label,
    splitter=GrandparentSplitter(train_name='training', valid_name='testing'),
    item_tfms=Resize(32),
    batch_tfms=Normalize.from_stats(mean=[0.5], std=[0.5])
)

Done. Với DataBlock như trên thì ta đã có thể tạo DataLoaders như bài viết trước và đem đi train mô hình được rồi

dls = dblock.dataloaders(path, bs=64)
dls.show_batch()

ảnh.png

Lần này thì mình không dùng mô hình có sẵn nữa mà dùng 1 mạng CNN đơn giản:

model = nn.Sequential(
    nn.Conv2d(1, 8, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(8),
    nn.ReLU(),
    nn.Conv2d(8, 16, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(16),
    nn.ReLU(),
    nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(32),
    nn.ReLU(),
    nn.Conv2d(32, 16, kernel_size=3, stride=2, padding=1),
    nn.BatchNorm2d(16),
    nn.ReLU(),
    nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
    Flatten()
)

Khởi tạo Learner

learner = Learner(dls, model, loss_func=nn.CrossEntropyLoss(), metrics=accuracy)
learner.summary()

Output:

Sequential (Input shape: 64 x 1 x 32 x 32)
============================================================================
Layer (type)         Output Shape         Param #    Trainable 
============================================================================
                     64 x 8 x 16 x 16    
Conv2d                                    80         True      
BatchNorm2d                               16         True      
ReLU                                                           
____________________________________________________________________________
                     64 x 16 x 8 x 8     
Conv2d                                    1168       True      
BatchNorm2d                               32         True      
ReLU                                                           
____________________________________________________________________________
                     64 x 32 x 4 x 4     
Conv2d                                    4640       True      
BatchNorm2d                               64         True      
ReLU                                                           
____________________________________________________________________________
                     64 x 16 x 2 x 2     
Conv2d                                    4624       True      
BatchNorm2d                               32         True      
ReLU                                                           
____________________________________________________________________________
                     64 x 10 x 1 x 1     
Conv2d                                    1450       True      
____________________________________________________________________________
                     64 x 10             
Flatten                                                        
____________________________________________________________________________

Total params: 12,106
Total trainable params: 12,106
Total non-trainable params: 0

Optimizer used: <function Adam at 0x7f123a175ee0>
Loss function: FlattenedLoss of CrossEntropyLoss()

Callbacks:
  - TrainEvalCallback
  - CastToTensor
  - Recorder
  - ProgressCallback

Khi tạo Learner, chỉ có 2 tham số bắt buộc là dataloaders và mô hình. Khi truyền dataloaders vào thì không bắt buộc sử dụng DataLoaders được tạo từ DataBlock mà các bạn có thể tự viết Dataset pytorch và gọi Dataloader như bình thường sau đó bọc Dataloader train và validation như thế này: `DataLoaders(train_loader, val_loader).

Và công việc cuối cùng là train mô hình:

learner.fit(n_epoch=5, lr=1e-4)

ảnh.png

Kết luận

Ở trên mình đã hướng dẫn các bạn cách sử dụng DataBlock API của fastai. DataBlock API cung cấp cho người dùng 1 cách load data linh hoạt hơn so với các factory method có sẵn. Nếu sử dụng DataBlock vẫn chưa đủ linh hoạt cho nhu cầu của các bạn thì fastai còn cung cấp 1 tầng API thấp hơn gọi là mid-level API, nhưng cái này chắc sẽ để cho bài viết sau. Cám ơn mọi người đã đọc bài viết. Mong các bạn tiếp tục ủng hộ.

Reference


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí