3.3.4. Data augmentation#

Các bộ dữ liệu deep learning thường có kích thước rất lớn. Trong quá trình huấn luyện các model deep learning chúng ta không thể truyền toàn bộ dữ liệu vào mô hình cùng một lúc bởi dữ liệu thường có kích thước lớn hơn RAM máy tính. Xuất phát từ lý do này, các framework deep learning đều hỗ trợ các hàm huấn luyện mô hình theo generator. Dữ liệu sẽ không được khởi tạo ngay toàn bộ từ đầu mà sẽ huấn luyện đến đâu sẽ được khởi tạo đến đó theo từng phần nhỏ gọi là batch.

Tùy theo định dạng dữ liệu là text, image, data frame, numpy array,… mà chúng ta sẽ sử dụng những module tạo dữ liệu huấn luyện khác nhau.

Generator

Generator sẽ không trả về kết quả ngay mà chỉ tạo sẵn các ô nhớ lưu hàm generator mô tả cách tính. Do đó chúng ta sẽ không tốn chi phí thời gian để thực hiện các phép tính. Thực tế là chúng ta đang nợ máy tính kết quả trả về. Chỉ khi nào được gọi tên bằng cách kích hoạt trong hàm next() thì mới tính kết quả.

Chúng ta có thể thấy generator có lợi thế là:

  • Không sinh toàn bộ dữ liệu cùng một lúc, do đó sẽ nâng cao hiệu suất vì sử dụng ít bộ nhớ hơn.

  • Không phải chờ toàn bộ các vòng lặp được xử lý xong thì mới xử lý tiếp nên tiết kiệm thời gian tính toán.

Đó chính là lý do generator chính là giải pháp được lựa chọn cho huấn luyện mô hình deep learning với dữ liệu lớn.

def _gen_interest_rate(month):
    yield (1+0.01)**month - 1


periods = [1, 3, 6, 9, 12]
scales = [_gen_interest_rate(month) for month in periods]
print('scales of origin balance: ', scales)

[next(_gen_interest_rate(n)) for n in periods]
scales of origin balance:  [<generator object _gen_interest_rate at 0x106acd5b0>, <generator object _gen_interest_rate at 0x106acd1c0>, <generator object _gen_interest_rate at 0x106acd3f0>, <generator object _gen_interest_rate at 0x106acd7e0>, <generator object _gen_interest_rate at 0x106acd850>]
[0.010000000000000009,
 0.030301000000000133,
 0.061520150601000134,
 0.09368527268436089,
 0.12682503013196977]

3.3.4.1. In memory Dataset#

Khởi tạo các dataset ngay từ đầu và dữ liệu được lưu trữ trên memory. Phương pháp In memory Dataset sẽ phù hợp với các bộ dữ liệu kích thước nhỏ mà RAM có thể load được. Quá trình huấn luyện theo cách này thì nhanh hơn so với phương pháp Generator Dataset vì dữ liệu đã được chuẩn bị sẵn mà không tốn thời gian chờ khởi tạo batch. Tuy nhiên dễ xảy ra out of memory trong quá trình huấn luyện.

from tensorflow.keras.datasets import mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 [==============================] - 1s 0us/step
(60000, 28, 28)
(10000, 28, 28)
(60000,)
(10000,)

Như vậy các dữ liệu traintest của bộ dữ liệu mnist đã được load vào bộ nhớ. Tiếp theo chúng ta sẽ khởi tạo Dataset cho những dữ liệu in memory này bằng hàm tf.data.Dataset.from_tensor_slices(). Hàm này sẽ khai báo dữ liệu đầu vào cho mô hình huấn luyện.

import tensorflow as tf
train_dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))
valid_dataset = tf.data.Dataset.from_tensor_slices((X_test, y_test))
Metal device set to: Apple M1 Pro

Khi đó chúng ta đã có thể fit vào mô hình huấn luyện các dữ liệu được truyền vào tf.Dataset là (X_train, y_train).

Chúng ta cũng có thể áp dụng các phép biến đổi bằng các hàm như Dataset.map() hoặc Dataset.batch() để biến đổi dữ liệu trước khi fit vào model. Các bạn xem thêm tại tf.Dataset. Chẳng hạn trước khi truyền batch vào huấn luyện tôi sẽ thực hiện chuẩn hóa batch theo phân phối chuẩn.

import numpy as np
from tensorflow.keras.backend import std, mean
from tensorflow.math import reduce_std, reduce_mean

def _normalize(X_batch, y_batch):
    '''
    X_batch: matrix digit images, shape batch_size x 28 x 28
    y_batch: labels of digit.
    '''
    X_batch = tf.cast(X_batch, dtype = tf.float32)
    # Padding về 2 chiều các giá trị 0 để được shape là 32 x 32
    pad = tf.constant([[0, 0], [2, 2], [2, 2]])
    X_batch = tf.pad(X_batch, paddings=pad, mode='CONSTANT', constant_values=0)
    X_batch = tf.expand_dims(X_batch, axis=-1)
    mean = reduce_mean(X_batch)
    std = reduce_std(X_batch)
    X_norm = (X_batch-mean)/std
    return X_norm, y_batch

# batch(32): Trích xuất ra từ list (X_train, y_train) các batch_size có kích thước là 32.
# map(_normalize): Mapping đầu vào là các batch (X_batch, y_batch) kích thước 32 vào hàm số _normalize()
# Kết quả trả về là giá trị đã chuẩn hóa theo batch của X_batch và y_batch
train_dataset = train_dataset.batch(32).map(_normalize)
valid_dataset = valid_dataset.batch(32).map(_normalize)

train model

from tensorflow.keras.applications import MobileNet
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.optimizers import Adam

base_extractor = MobileNet(input_shape = (32, 32, 1), include_top = False, weights = None)
flat = Flatten()
den = Dense(10, activation='softmax')
model = Sequential([base_extractor, 
                   flat,
                   den])
model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 mobilenet_1.00_32 (Function  (None, 1, 1, 1024)       3228288   
 al)                                                             
                                                                 
 flatten (Flatten)           (None, 1024)              0         
                                                                 
 dense (Dense)               (None, 10)                10250     
                                                                 
=================================================================
Total params: 3,238,538
Trainable params: 3,216,650
Non-trainable params: 21,888
_________________________________________________________________

fit data batch

model.compile(Adam(), loss='sparse_categorical_crossentropy', metrics = ['accuracy'])
model.fit(train_dataset, validation_data = valid_dataset, epochs = 5)
Epoch 1/5
1875/1875 [==============================] - 68s 33ms/step - loss: 0.5088 - accuracy: 0.8390 - val_loss: 0.2128 - val_accuracy: 0.9398
Epoch 2/5
1875/1875 [==============================] - 63s 33ms/step - loss: 0.1546 - accuracy: 0.9567 - val_loss: 0.1376 - val_accuracy: 0.9616
Epoch 3/5
1875/1875 [==============================] - 62s 33ms/step - loss: 0.1276 - accuracy: 0.9672 - val_loss: 0.1276 - val_accuracy: 0.9689
Epoch 4/5
1875/1875 [==============================] - 63s 33ms/step - loss: 0.1001 - accuracy: 0.9742 - val_loss: 0.1002 - val_accuracy: 0.9732
Epoch 5/5
1875/1875 [==============================] - 63s 33ms/step - loss: 0.0882 - accuracy: 0.9783 - val_loss: 0.0625 - val_accuracy: 0.9851
<keras.callbacks.History at 0x177635e70>

3.3.4.2. Generator Dataset#

Theo cách Generator Dataset chúng ta sẽ qui định cách mà dữ liệu được tạo ra như thế nào thông qua một hàm generator. Quá trình huấn luyện đến đâu sẽ tạo batch đến đó. Do đó các bộ dữ liệu big data có thể được load theo từng batch sao cho kích thước vừa được dung lượng RAM. Theo cách huấn luyện này chúng ta có thể huấn luyện được các bộ dữ liệu có kích thước lớn hơn nhiều so với RAM bằng cách chia nhỏ chúng theo batch. Đồng thời có thể áp dụng thêm các step preprocessing data trước khi dữ liệu được đưa vào huấn luyện. Do đó đây thường là phương pháp được ưa chuộng khi huấn luyện các model deep learning.

3.3.4.2.1. Ví dụ#

import pandas as pd

hanoi = ['bún chả hà nội', 'chả cá lã vọng hà nội', 'cháo lòng hà nội', 'ô mai sấu hà nội', 'ô mai', 'chả cá', 'cháo lòng']
hochiminh = ['bánh canh sài gòn', 'hủ tiếu nam vang sài gòn', 'hủ tiếu bò sài gòn', 'banh phở sài gòn', 'bánh phở', 'hủ tiếu']
city = ['hanoi'] * len(hanoi) + ['hochiminh'] * len(hochiminh)
corpus = hanoi+hochiminh

data = pd.DataFrame({'city': city, 'food': corpus})
data.sample(5)
city food
12 hochiminh hủ tiếu
10 hochiminh banh phở sài gòn
1 hanoi chả cá lã vọng hà nội
8 hochiminh hủ tiếu nam vang sài gòn
5 hanoi chả cá
class Voc(object):
    """Class Voc có tác dụng khởi tạo index từ điển cho toàn bộ corpus (bộ văn bản)"""
    def __init__(self, corpus):
        self.corpus = corpus                     # list toàn bộ tên các món ăn
        self.dictionary = {'unk': 0}
        self._initialize_dict(corpus)

    def _add_dict_sentence(self, sentence):
        words = sentence.split(' ')
        for word in words:
            if word not in self.dictionary.keys():
                max_indice = max(self.dictionary.values())
                self.dictionary[word] = (max_indice + 1)

    def _initialize_dict(self, sentences):
        for sentence in sentences:
            self._add_dict_sentence(sentence)

    def _tokenize(self, sentence):
        words = sentence.split(' ')
        token_seq = [self.dictionary[word] for word in words]
        return np.array(token_seq)

voc = Voc(corpus = corpus)
voc.dictionary
{'unk': 0,
 'bún': 1,
 'chả': 2,
 'hà': 3,
 'nội': 4,
 'cá': 5,
 'lã': 6,
 'vọng': 7,
 'cháo': 8,
 'lòng': 9,
 'ô': 10,
 'mai': 11,
 'sấu': 12,
 'bánh': 13,
 'canh': 14,
 'sài': 15,
 'gòn': 16,
 'hủ': 17,
 'tiếu': 18,
 'nam': 19,
 'vang': 20,
 'bò': 21,
 'banh': 22,
 'phở': 23}

Tiếp theo chúng ta sẽ khởi tạo một random_generator có tác dụng lựa chọn ngẫu nhiên một tên món ăn trong corpustokenize chúng.

import tensorflow as tf

cat_indices = {
    'hanoi': 0,
    'hochiminh': 1
}

def generators():
    i = 0
    while True:
        i = np.random.choice(data.shape[0])
        sentence = data.iloc[i, 1]
        x_indice = voc._tokenize(sentence)
        label = data.iloc[i, 0]
        y_indice = cat_indices[label]
        yield x_indice, y_indice
        i += 1

random_generator = tf.data.Dataset.from_generator(
    generators,
    output_types = (tf.float16, tf.float16),
    output_shapes = ((None,), ())
)

random_generator
<_FlatMapDataset element_spec=(TensorSpec(shape=(None,), dtype=tf.float16, name=None), TensorSpec(shape=(), dtype=tf.float16, name=None))>
import numpy as np

# hàm shuffle(20) có tác dụng trộn lẫn ngẫu nhiên dữ liệu
# Sau đó dữ liệu được chia thành những batch có kích thước là 20 
# padding giá trị 0 sao cho bằng với độ dài của câu dài nhất bằng hàm padded_batch()
random_generator_batch = random_generator.shuffle(20).padded_batch(20, padded_shapes=([None], []))
sequence_batch, label = next(iter(random_generator_batch))

print(sequence_batch)
print(label)
tf.Tensor(
[[ 1.  2.  3.  4.  0.  0.]
 [ 8.  9.  3.  4.  0.  0.]
 [10. 11.  0.  0.  0.  0.]
 [13. 23.  0.  0.  0.  0.]
 [ 1.  2.  3.  4.  0.  0.]
 [10. 11.  0.  0.  0.  0.]
 [ 8.  9.  3.  4.  0.  0.]
 [10. 11.  0.  0.  0.  0.]
 [13. 23.  0.  0.  0.  0.]
 [17. 18.  0.  0.  0.  0.]
 [ 1.  2.  3.  4.  0.  0.]
 [10. 11. 12.  3.  4.  0.]
 [17. 18.  0.  0.  0.  0.]
 [ 2.  5.  0.  0.  0.  0.]
 [ 2.  5.  0.  0.  0.  0.]
 [10. 11.  0.  0.  0.  0.]
 [ 1.  2.  3.  4.  0.  0.]
 [17. 18. 21. 15. 16.  0.]
 [10. 11.  0.  0.  0.  0.]
 [ 2.  5.  6.  7.  3.  4.]], shape=(20, 6), dtype=float16)
tf.Tensor([0. 0. 0. 1. 0. 0. 0. 0. 1. 1. 0. 0. 1. 0. 0. 0. 0. 1. 0. 0.], shape=(20,), dtype=float16)

3.3.4.2.2. ImageGenerator#

ImageGenerator cũng là một dạng data generator được xây dựng trên framework keras và dành riêng cho dữ liệu ảnh.

Đây là một high level function nên cú pháp đơn giản, rất dễ sử dụng nhưng khả năng tùy biến và can thiệp sâu vào dữ liệu kém.

Khi khởi tạo ImageGenerator chúng ta sẽ khai báo các thủ tục preprocessing image trước khi đưa vào huấn luyện. Mình sẽ không quá đi sâu vào các kĩ thuật preprocessing data này. Bạn đọc quan tâm có thể xem thêm tại ImageDataGenerator.

import glob2
root_folder = 'Datasets/Dog-Cat-Classifier/Data/Train_Data/'

image_gen = tf.keras.preprocessing.image.ImageDataGenerator(
    rescale = 1./255, 
    rotation_range = 20,
    horizontal_flip = True
)


images, labels = next(image_gen.flow_from_directory(root_folder))
Found 1399 images belonging to 2 classes.
images.shape
(32, 256, 256, 3)

Hàm flow_from_directory() sẽ có tác dụng đọc các ảnh từ root_folder và lấy ra những thông tin bao gồm ma trận ảnh sau biến đổi và nhãn tương ứng. Cấu trúc cây thư mục của root_folder có dạng như sau:

| root-folder
----| sub-folder-class-1
----| sub-folder-class-2
----| ...
----| sub-folder-class-C

Trong đó bên trong các sub-folder-class-i là list toàn bộ các ảnh thuộc về một class. Hàm flow_from_directory() sẽ tự động xác định các file dữ liệu nào là ảnh để load vào quá trình huấn luyện mô hình. Ở đây trong root_folder chúng ta có 2 sub-folders tương ứng với 2 classes là dog, cat.

# Tiếp theo ta sẽ khởi tạo một tf.Dataset từ generator thông qua hàm from_generator().
# Khai báo bắt buộc định dạng dữ liệu input và output thông qua tham số output_types và output shape thông qua tham số output_shapes
# Như vậy kết quả trả ra sẽ là những batch có kích thước 32 và ảnh có kích thước 256 x 256 và nhãn tương ứng của ảnh.
image_gen_dataset = tf.data.Dataset.from_generator(
    image_gen.flow_from_directory, 
    args = ([root_folder]),
    output_types=(tf.float32, tf.float32), 
    output_shapes=([32,256,256,3], [32, 1])
)

3.3.4.2.3. Customize ImageGenerator#

Giả sử bạn có một bộ dữ liệu ảnh mà kích thước các ảnh là khác biệt nhau. Đồng thời bạn cũng muốn can thiệp sâu hơn vào bức ảnh trước khi đưa vào huấn luyện như giảm nhiễu bằng bộ lọc Gausianblur, rotate ảnh, crop, zoom ảnh, …. Nếu sử dụng các hàm mặc định của image preprocessing trong ImageGenerator thì sẽ gặp hạn chế đó là bị giới hạn bởi một số phép biến đổi mà hàm này hỗ trợ. Sử dụng high level framework tiện thì rất tiện nhưng khi muốn can thiệp sâu thì rất khó. Muốn can thiệp được sâu vào bên trong các biến đổi chúng ta phải customize lại một chút ImageGenerator.

!pip install opencv-python
Collecting opencv-python
  Downloading opencv_python-4.7.0.72-cp37-abi3-macosx_11_0_arm64.whl (32.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 32.6/32.6 MB 9.6 MB/s eta 0:00:0000:01m00:01
?25hRequirement already satisfied: numpy>=1.21.2 in /Users/datkhong/miniconda3/lib/python3.10/site-packages (from opencv-python) (1.24.2)
Installing collected packages: opencv-python
Successfully installed opencv-python-4.7.0.72
import numpy as np
from tensorflow.keras.utils import Sequence, to_categorical
import cv2
import glob2

class DataGenerator(Sequence):
    'Generates data for Keras'
    def __init__(self,
                 all_filenames, 
                 labels, 
                 batch_size, 
                 index2class,
                 input_dim,
                 n_channels,
                 n_classes=2, 
                 shuffle=True):
        '''
        all_filenames: list toàn bộ các filename
        labels: nhãn của toàn bộ các file
        batch_size: kích thước của 1 batch
        index2class: index của các class
        input_dim: (width, height) đầu vào của ảnh
        n_channels: số lượng channels của ảnh
        n_classes: số lượng các class 
        shuffle: có shuffle dữ liệu sau mỗi epoch hay không?
        '''
        self.all_filenames = all_filenames
        self.labels = labels
        self.batch_size = batch_size
        self.index2class = index2class
        self.input_dim = input_dim
        self.n_channels = n_channels
        self.n_classes = n_classes
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        '''
        Số lượng step trong một epoch.
        
        return:
          Trả về số lượng batch/1 epoch
        '''
        return int(np.floor(len(self.all_filenames) / self.batch_size))

    def __getitem__(self, index):
        '''
        Trong quá trình huấn luyện chúng ta cần phải access vào từng batch trong bộ dữ liệu. 
        Hàm __getitem__() sẽ khởi tạo batch theo thứ tự của batch được truyền vào hàm.
        
        params:
          index: index của batch
        return:
          X, y cho batch thứ index
        '''
        # Lấy ra indexes của batch thứ index
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

        # List all_filenames trong một batch
        all_filenames_temp = [self.all_filenames[k] for k in indexes]

        # Khởi tạo data
        X, y = self.__data_generation(all_filenames_temp)

        return X, y

    def on_epoch_end(self):
        '''
        Đây là hàm được tự động run mỗi khi một epoch huấn luyện bắt đầu và kết thúc. 
        Tại hàm này chúng ta sẽ xác định các hành động khi bắt đầu hoặc kết thúc một epoch như: 
        - Có shuffle dữ liệu hay không?
        - Điều chỉnh lại tỷ lệ các class tước khi fit vào model,….
        
        Shuffle dữ liệu khi epochs end hoặc start.
        '''
        self.indexes = np.arange(len(self.all_filenames))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self, all_filenames_temp):
        '''
        Hàm này sẽ được gọi trong __getitem__(). __data_generation() sẽ trực tiếp 
        biến đổi dữ liệu và quyết định các kết quả dữ liệu trả về cho người dùng. 
        Tại hàm này ta có thể thực hiện các phép preprocessing image.
        
        params:
          all_filenames_temp: list các filenames trong 1 batch
        return:
          Trả về giá trị cho một batch.
        '''
        X = np.empty((self.batch_size, *self.input_dim, self.n_channels))
        y = np.empty((self.batch_size), dtype=int)

        # Khởi tạo dữ liệu
        for i, fn in enumerate(all_filenames_temp):
            # Đọc file từ folder name
            img = cv2.imread(fn)
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            img = cv2.resize(img, self.input_dim)
            label = os.path.basename(fn)
            label = self.index2class[label[:3]]
    
            X[i,] = img

            # Lưu class
            y[i] = label
        return X, y
import os
dict_labels = {
    'dog': 0,
    'cat': 1
}
root = r"/Users/datkhong/Library/CloudStorage/GoogleDrive-k55.1613310017@ftu.edu.vn/My Drive/GitCode/My_learning/1. DA - DS/3. Learning/2_Notebooks/5_Deep_learning/"
root_folder = root + r'Datasets/Dog-Cat-Classifier/Data/Train_Data/*/*'
fns = glob2.glob(root_folder)
print(len(fns))

image_generator = DataGenerator(
    all_filenames = fns,
    labels = None,
    batch_size = 32,
    index2class = dict_labels,
    input_dim = (224, 224),
    n_channels = 3,
    n_classes = 2,
    shuffle = True
)

X, y = image_generator.__getitem__(1)

print(X.shape)
print(y.shape)
1399
(32, 224, 224, 3)
(32,)

Như vậy ta có thể thấy, tại mỗi lượt huấn luyện model lấy ra một batch có kích thước là 32. Mặc dù ảnh của chúng ta có kích thước khác nhau nhưng đã được resize về chung một kích thước là width x height = 224 x 224.

from tensorflow.keras.applications import MobileNet
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.optimizers import Adam

base_extractor = MobileNet(input_shape = (224, 224, 3), include_top = False, weights = 'imagenet')
flat = Flatten()
den = Dense(1, activation='sigmoid')
model = Sequential([base_extractor, flat, den])
model.summary()

# chúng ta chỉ cần thay generator vào vị trí của train data trong hàm fit()
model.compile(optimizer='adam', loss='binary_crossentropy', metrics = ['accuracy'])
model.fit(image_generator, epochs = 5)
Model: "sequential_12"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 mobilenet_1.00_224 (Functio  (None, 7, 7, 1024)       3228864   
 nal)                                                            
                                                                 
 flatten_12 (Flatten)        (None, 50176)             0         
                                                                 
 dense_12 (Dense)            (None, 1)                 50177     
                                                                 
=================================================================
Total params: 3,279,041
Trainable params: 3,257,153
Non-trainable params: 21,888
_________________________________________________________________
Epoch 1/5
43/43 [==============================] - 11s 167ms/step - loss: 0.8531 - accuracy: 0.9041
Epoch 2/5
43/43 [==============================] - 7s 164ms/step - loss: 0.3894 - accuracy: 0.9448
Epoch 3/5
43/43 [==============================] - 7s 165ms/step - loss: 0.1809 - accuracy: 0.9688
Epoch 4/5
43/43 [==============================] - 7s 165ms/step - loss: 0.1497 - accuracy: 0.9709
Epoch 5/5
43/43 [==============================] - 7s 163ms/step - loss: 0.1265 - accuracy: 0.9724
<keras.callbacks.History at 0x306e78f10>