Deep Learning với Tensorflow Module 5 phần 1.2: Convolutional Neural Network với dữ liệu 2 class

MVT
Đang cập nhật

Để có thể xem chi tiết toàn bộ quá trình thực hiện của phần này, bạn nên xem trên notebook |https://github.com/mthang1801/deep-learning/blob/main/docs/03_1_convolutional_neural_network_binary_class.ipynb

Ở những module trước, chúng ta đã khái quát những vấn đề cơ bản của Tensorflow và tự tay xây dựng mô hình để xử lý những vấn đề khác nhau. Trong module này, chúng ta sẽ đi vào những vấn đề cụ thể và xem cách thức hoạt động của một loại neural network đặc biệt trong deep learning, nó được sử dụng rất phổ biến trong thị giác máy tính (computer vision) đó là convolutional neural networks (CNNs)

🔑 Lưu ý : Trong deep learning, có rất nhiều loại kiến trúc mô hình khác nhau được sử dụng để giải quyết cho những vấn đề cụ thể. Chẳng hạn như sử dụng Convolution neural network để dự đoán dữ liệu hình ảnh, hoặc dữ liệu dưới dạng văn bản. Tuy nhiên, trong thực tế không phải lúc nào những kiến trúc được sử dụng phổ biến là tốt nhất, mà còn có những cái khác còn có thể tốt hơn.

Trong bài viết này, chúng ta sẽ xây dựng và sử dụng mô hình với kiến trúc CNNs để dự đoán dữ liệu hình ảnh.

Nội dung :

  1. Chuẩn bị tập dữ liệu
    • Kiểm tra và đọc dữ liệu
  2. Kiến trúc của Convolutional Neural Network
  3. Ví dụ
    • model_1 Sử dụng Conv2D layer
    • model_2 Sử dụng Dense layer
  4. Những bước để xây dựng mô hình phân loại 2 class với CNNs
    • 4.1 Khám phá, tìm hiểu cấu trúc của tập dữ liệu
    • 4.2 Chuẩn bị dữ liệu cho mô hình
    • 4.3 Tạo mô hình CNN (bắt đầu với mô hình đơn giản làm cơ sở)
    • 4.4 Fitting mô hình (mô hình tìm kiếm các đặc trưng trong dữ liệu weights , bias )
    • 4.5 Đánh giá mô hình
    • 4.6 Cải thiện mô hình
    • 4.7 Mô hình dự đoán

4. Những bước để xây dựng mô hình phân loại 2 class với CNNs

4.1 Khám phá, tìm hiểu cấu trúc của tập dữ liệu

Khi bạn mới nhận được tập dữ liệu, điều đầu tiên bạn sẽ làm là gì?

Khi chúng ta chưa biết hoặc chưa hiểu sâu sắc về tập dữ liệu đó, điều đầu tiên nên làm là khám phá, khai thác các thông tin, tìm hiểu cấu trúc có trong tập dữ liệu đó. Một ý tưởng khá hợp lý đó là vẽ biểu đồ hoặc biểu diễn hình ảnh kèm theo các tên của label.

Trong trường hợp của chúng ta, tập dữ liệu được chia làm 2 phần là traintest với mỗi tập có 2 class là phofried_rice. Trong mỗi class này gồm có các file hình ảnh có kích thước khác nhau, nhưng có đặc điểm chung là chúng đều có cùng kênh màu (color channel).

Việc trước tiên ta sẽ quan sát hình ảnh của mỗi class

plt.figure(figsize=(12,7))
pho_images = plot_random_images("pho_fried_rice/train", "pho", 2)
fried_rice_images = plot_random_images("pho_fried_rice/train", "fried_rice",2)

4.2 Chuẩn bị dữ liệu (preprocessing data)

Chuẩn bị dữ liệu là một trong những bước quan trọng nhất mà dự án machine learning nào cũng đều bắt buộc phải có để chia tập dữ liệu lớn thành các tập dữ liệu nhỏ hơn gồm traintest, và đây cũng là một trong những bước khó khăn nhất.

Trong trường hợp này, dữ liệu đã được chia sẵn thành traintest. Thông thường, chúng ta còn có thể chia tập dữ liệu thêm validation, nhưng việc này sẽ làm sau.

Đối với những dự án phân loại hình ảnh, việc tách dữ liệu thành các folder con như traintest cho mỗi class là một tiêu chuẩn.

Để bắt đầu, chúng ta sẽ chỉ ra đường dẫn liên kết của traintest

train_dir = "pho_fried_rice/train"
test_dir = "pho_fried_rice/test"

Bước tiếp theo, chia các dữ liệu thành từng cụm (batches)

Một cụm (batch) là một tập hợp nhỏ của một tập dữ liệu mà mô hình có thể theo dõi được trong quá trình train. Ví dụ, thay vì đưa hết 10000 hình ảnh vào mô hình cùng một lúc và cố gắng để cho mô hình tìm kiếm các đặc trưng trong từng hình ảnh, thì mô hình theo cụm trong một lần sẽ chỉ cần lấy 32 hình ảnh để tìm kiếm đặc trưng đó.

Tại sao phải phân cụm? Có một số lý do như sau :

  • Nếu cho 10.000 hình ảnh (hoặc có thể hơn) vào cùng một lúc sẽ dẫn đến bộ nhớ xử lý không đủ dung lượng (sức chứa) để có thể chứa số lượng hình ảnh khổng lồ đó.
  • Việc có gắng tìm kiếm các điểm đặc trưng của 10.000 hình ảnh trong 1 lần có thể làm cho mô hình học không tốt.

Vây trong một cụm nên chưa bao nhiêu hình ảnh ? Đó là tùy ở bạn, nhưng theo những chuyên gia trong lĩnh vực này thì nó là 32. batch size of 32 is good for your health.

Để chuyển tập dữ liệu thành từng cụm, chúng ta sẽ tạo một generatorImageDataGenerator

from tensorflow.keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(rescale=1/255.)
test_datagen = ImageDataGenerator(rescale=1/255.)

ImageDataGenerator giúp chúng ta chuẩn bị các hình ảnh và phân chia chúng theo từng cụm cũng như thực hiện các phép biến đổi trước khi chúng được load vào mô hình. Tham số rescale là một trong những phép biến đổi đó. Nhờ có tham số rescale với 1/255. có nghĩa là lấy tất cả các giá trị chia cho 255. Điều này dẫn đến việc tất cả các giá trị của hình ảnh được chuẩn hóa ( chuyển thành giá trị từ 0-1 ).

Bây giờ, thông qua các instance của ImageDataGenerator đã được khởi tạo ở trên, chúng ta có thể load hình ảnh từ đường dẫn tương ứng bằng cách sử dụng phương thức flow_from_directory của nó.

train_data = train_datagen.flow_from_directory(
    directory=train_dir,
    target_size=(224,224),
    class_mode="binary",
    batch_size=32,
)

test_data = train_datagen.flow_from_directory(
    directory=test_dir,
    target_size=(224,224),
    class_mode="binary",
    batch_size=32,
)

Found 1500 images belonging to 2 classes. Found 500 images belonging to 2 classes.

Có thể thấy 1500 hình ảnh cho 2 class trong tập train, 500 hình ảnh cho 2 class trong tập test.

  • Do thư mục đã được cấu trúc trước đó, các classes đều được tạo ra bởi tên thư mục con nằm trong train_dirtest_dir.
  • Tham số target_size dùng để định nghĩa kích thước hình ảnh nhập vào theo định dạng (height, width)
  • Tham số class_mode dùng để định nghĩa kiểu mô hình được sử dụng, với mô hình phân loại có 2 class là binary, với nhiều class là categorical.
  • Tham số batch_size định nghĩa có bao nhiêu hình ảnh được đưa vào trong mỗi cụm. Chúng ta sẽ lấy 32 làm giá trị mặc định.

Với 1500 hình ảnh trong tập train sẽ chia làm bao nhiêu cụm ?

1500 / 32 # -> 47 cụm

46.875

len(train_dataset)

47

Kiểm tra kích thước của môt cụm trong số 47 cụm trên

# Get a sample of the training data batch 
images, labels = train_data.next()# get the 'next' batch of images/labels

len(images), len(labels)

(32, 32)

Xem hình ảnh bên trong sẽ có gì, và kích thước của nó trông như thế nào :

images[0], images[0].shape

(array([[[0.7725491 , 0.83921576, 0.94117653], [0.7725491 , 0.83921576, 0.94117653], [0.77647066, 0.8431373 , 0.9450981 ], ...,
[0.8352942 , 0.89019614, 0.9333334 ], [0.8470589 , 0.8941177 , 0.9490197 ], [0.8431373 , 0.89019614, 0.9450981 ]]], dtype=float32), (224, 224, 3))

Vậy còn giá trị labels ?

labels
array([1., 0., 0., 0., 0., 0., 0., 1., 0., 1., 1., 0., 0., 0., 1., 0., 1.,1., 1., 0., 1., 1., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],dtype=float32)

Vì tham số class_modebinary nên label chỉ có 2 trường hợp hoặc 0 (pho) hoặc 1 (fried rice).

Dữ liệu hiện tại đã sẵn sàng, đến lúc cấu hình cho mô hình để tìm các mẫu đặc trưng giữa ma trận hình ảnh và labels

4.3 Tạo mô hình CNN (bắt đầu với mô hình đơn giản làm cơ sở)

Có bao giờ bạn tự hỏi kiến trúc mô hình mặc định trông như thế nào chưa? Và thực tế có rất nhiều câu trả lời cho câu hỏi này. Một phương pháp đơn giản cho các mô hình thị giác máy tính là sử dụng kiến trúc mô hình đang hoạt động tốt nhất trên ImageNet ( một tập hợp lớn các hình ảnh đa dạng để đánh giá các mô hình thị giác máy tính khác nhau).

Tuy nhiên, để bắt đầu những điều đơn giản nhất, chúng ta sẽ xây dựng mô hình nhỏ để đạt được kết quả cơ sở rồi sẽ tìm cách để cải thiện chúng.

🔑Lưu ý: Trong deep learning, một mô hình càng nhỏ thì số lượng layer càng ít hơn so với những mô hình hiện đại.

Mô hình dưới đây được xây dựng từ 3 layer của CNN

model_4 = Sequential([
  layers.Conv2D(10,3,input_shape=(224,224,3),activation="relu"), # input layer (specify input shape)
  layers.Conv2D(10,3,activation="relu"),
  layers.MaxPool2D(),
  layers.Flatten(),
  layers.Dense(1, activation="sigmoid") # output layer (specify output shape)
])

Kiến trúc mô hình CNN cơ bản được được train xong. Và kiến trúc cụ thể như sau :

Input -> Conv2D + Relu Layer(non-lieanrities) -> Pooling Layer -> Full connected (dense layer) như Output

Trong Conv2D layer có một số component như sau :

  • 2D nghĩa là dữ liệu đưa vào có 2 chiều (height,width). Mặc dù dữ liệu hình ảnh có 3 kênh màu nhưng convolution sẽ chạy qua mỗi kênh đó một cách riêng biệt.
  • filters số lượng các feature extractor sẽ đi qua các hình ảnh
  • kernel_size kích thước của các filters, VD một kernel_size có kích thước (3,3) thì có nghĩa là mỗi filter sẽ là ma trận (3,3) trong mỗi lần trượt. kernel càng thấp nó sẽ trích xuất các đặc tính chi tiết hơn.
  • stride số pixel mà filter "nhảy" sau mỗi lần. VD : stride = 2 tức là di chuyển qua 2 pixel tại một thời điểm.
  • padding các giá trị có thể là same, valid. Với same sẽ không tạo vùng bao phủ bên ngoài, do đó kích thước của output sẽ giống với input. Trái lại, với valid sẽ cắt bớt các pixel dư thưa nơi filter không phù hợp (VD: hình ảnh có kích thước 224px độ rộng chia cho kernel_size là 3 sẽ là (224/3 = 74.6) nghĩa là các pixel lẻ sẽ bị gạt bỏ.

feature nghĩa là gì ? feature có thể hiểu là đặc tính hay là một phần quan trong của một hình ảnh. VD feature của pho có thể là những đoạn thẳng dài thể hiện chiều dài của sợi phở, hoặc với fried_rice có thể là những chấm bầu dục, hoặc có màu vàng đặc trưng cho cơm chiên.

Một điều quan trọng cần nhớ những features này không được định nghĩa bởi chúng ta, mà thay vào đó, mô hình sẽ học được chúng từ việc áp dụng những fiters khác nhau thông qua hình ảnh.

Bây giờ, ta sẽ compile mô hình

model_4.compile(
    loss="binary_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

4.4 Fitting mô hình (mô hình tìm kiếm các đặc trưng trong dữ liệu weights , bias )

Mô hình đã được compile. Lúc này có thể fit được rồi. Nhưng bạn cần lưu ý 2 khái niệm mới trong mô hình :

  • steps_per_epoch: số cụm (batches) trong mô hình sẽ đi qua mỗi epoch. Trong trường hợp này, chúng ta muốn mô hình đi qua tất cả các cụm nên nó sẽ bằng với chiều dài của train_data (1500 images in batches of 32 = 1500/32 = ~47 steps)
  • validation_steps : cũng tương tự như steps_per_epoch ngoại trừ nó áp dụng cho validation_data(500 test images in batches of 32 = 500/32 = ~16 steps)
model_4_history = model_4.fit(
    train_data,
    steps_per_epoch=len(train_data), 
    epochs=5, 
    validation_data=test_data, 
    validation_steps=len(test_data)
)
    Epoch 1/5
    47/47 [==============================] - 11s 230ms/step - loss: 1.0092 - accuracy: 0.5413 - val_loss: 0.6392 - val_accuracy: 0.7480
   ...

    Epoch 5/5
    47/47 [==============================] - 11s 226ms/step - loss: 0.1702 - accuracy: 0.9487 - val_loss: 0.5286 - val_accuracy: 0.7620
model_4.summary()
    Model: "sequential_3"
    ...
    Total params: 122,191
    Trainable params: 122,191
    Non-trainable params: 0

4.5 Đánh giá mô hình

Để đánh giá mô hình, có thể lấy biến được gán khi gọi hàm fit để theo dõi quá trình học của mô hình qua mỗi epoch. Cụ thê ở đây là biến model_4_history

import pandas as pd
pd.DataFrame(model_4_history.history).plot(figsize=(12,7))

<matplotlib.axes._subplots.AxesSubplot at 0x7f7fbce61510>

Nhìn qua biểu đồ có thể thấy :

  • loss giảm nhanh, trong khi val_loss không giảm mấy. Có vẻ như mô hình đang bị overfitting

🔑 Lưu ý: Khi validation loss trong mô hình đang giảm mà bắt đầu có dấu hiệu tăng thì đó có thể là hiện tượng overfitting. Có thể hiểu trong quá trình train, mô hình học các mẫu đặc tính rất tốt, dường như là thuộc lòng nên khi đưa vào những dữ liệu mà nó chưa từng gặp, nó sẽ không làm được.

Để trưc quan hơn, ta sẽ tách biểu đồ trên thành 2 biểu đồ con, một cái là accuracy và loss accuracy, còn lại là loss và val_loss

def plot_loss_curves(history) : 
  history = history.history 
  acc, val_acc = history["accuracy"], history["val_accuracy"]
  loss, val_loss = history["loss"], history["val_loss"]

  plt.figure(figsize=(16,6))
  plt.subplot(121)
  plt.plot(acc, label="train accuracy")
  plt.plot(val_acc, label="val accuracy")
  plt.title("Accurcay")
  plt.legend()

  plt.subplot(122)
  plt.plot(loss, label="train Loss")
  plt.plot(val_loss, label="val loss")
  plt.title("Loss")
  plt.legend()
plot_loss_curves(model_4_history)

Vị trí lý tưởng cho 2 đường này là nối tiếp nhau. Trong biểu đồ này, đường val_loss nên giảm xuống. Nếu như có khoảng cách giữa đường train và đường loss quá lớn, thì điều đó có nghĩa mô hình đang bị overfitting,

model_4.summary()
    Model: "sequential_3"
    ...
    Total params: 122,191
    Trainable params: 122,191
    Non-trainable params: 0

4.6 Cải thiện mô hình

Viẹc diều chỉnh mô hình machine learning xuất phát từ 3 bước :

  1. Tạo mô hình cơ sở
  2. Vượt qua mô hình cơ sở nhưng xảy ra overfitting
  3. Giảm overfitting

Chúng ta đã qua bước 1 và 2. Có vài cách có thể giảm overfit cho mô hình :

  1. Tăng số lượng convolutional layers
  2. Tăng số lượng filters

Nhưng thay vì tập trung làm cho các đường cong của mô hình được phù hợp với nhau, chúng ta sẽ thực hiện bước 3, làm như thế nào để giảm được overfitting?

Khi mô hình học quá tốt trên tập dữ liệu train, nhưng khi gặp một dữ liệu nào đó chưa từng thấy, nó gặp rắc rối và không hoạt động tốt như lúc học. Trong thực tế cũng vậy, mô hình tốt phải là mô hình có khả năng thích ứng tốt với những sự vật đang diễn ra chứ không phải những gì nó đã biết rồi.

Chính vì vậy, trong mô hình tiếp theo, sẽ có một số điều chỉnh vế số lượng tham số và giám sát song song các đường trainning.

Chúng ta sẽ xây dựng thêm 2 mô hình với một số layer bổ sung thêm : + 1 ConvNet với max pooling + 1 ConvNet với max pooling and data augmentation

Mô hình đầu tiên sẽ có kiến trúc như sau :

Input -> Conv layers + ReLU layers (non-linearities) + Max Pooling layers -> Fully connected (dense layer) as Output

model_5

# Create the model (this can be our baseline, a 3 layer Convolutional Neural Network)
model_5 = Sequential([
  layers.Conv2D(10,3,activation="relu", input_shape=(224,224,3)),  
  layers.MaxPool2D(),
  layers.Conv2D(10,3,activation="relu"),
  layers.MaxPool2D(),
  layers.Conv2D(10,3,activation="relu"),
  layers.MaxPool2D(),
  layers.Flatten(),
  layers.Dense(1, activation="sigmoid")
])

model_5.compile(
    loss="binary_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_5_history = model_5.fit(
    train_data,
    steps_per_epoch=len(train_data),
    epochs=5,
    validation_data=test_data,
    validation_steps=len(test_data)
)
    Epoch 1/5
    47/47 [==============================] - 11s 224ms/step - loss: 0.7117 - accuracy: 0.5313 - val_loss: 0.6209 - val_accuracy: 0.7080
    ...

    Epoch 5/5
    47/47 [==============================] - 10s 217ms/step - loss: 0.4365 - accuracy: 0.8060 - val_loss: 0.4079 - val_accuracy: 0.8320

Trước khi vễ loss_curves, ta sẽ kiểm tra kiến trúc model_5 trước :

model_5.summary()

    Model: "sequential_4"
    ...
    Total params: 8,861
    Trainable params: 8,861
    Non-trainable params: 0
    _________________________________________________________________

Mỗi lần gọi MaxPool2D, kích thước của hình giảm đi một nửa. Layer MaxPool2D lấy output của mỗi Conv2D và chỉ lấy các đặc tính quan trọng, còn lại nó loại bỏ đi.

Nếu tham số pool_size càng lớn thì MaxPool sẽ càng loại bỏ nhiều đặc tính trong hình ra càng nhiều. Tuy nhiên, khi loại đi như vậy nó có thể sẽ bỏ sót các đặc tính quan trọng của hình đó dẫn đến việc mô hình bỏ sót không hoc được gì cả.

Kết quả của việc gộp này làm giảm đáng kể trainable params (8,861 của model_5 so với 122,191 của model_4)

# Plot loss curves of model_5 results
plot_loss_curves(model_5_history)

model_6 (data augmentation không xáo trộn dữ liệu)

Các đường cong giữa train và val đã gần nhau. Tuy nhiên, trong qúa loss vẫn còn có lúc tăng, có lẽ nếu train lâu hơn thì mô hình vẫn sẽ bị overfitting.

Một cách để giúp cho mô hình không học "thuộc lòng" đó nhằm ngăn chặn overfitting, đó là tạo data augmentation.

Trước tiên chúng ta vẫn sẽ khởi tạo ImageDataGenerator, trong nó có chứa các tham số để làm hình ảnh "biến dạng" ngẫu nhiên nên ta sẽ thêm các tham số vào class này.

🤔 Data augmentation là gì ?

Data augmentation là quá trình thay đổi dữ liệu train giúp cho các dữ liệu trở nên phong phú, đa dạng hơn (thay đổi góc hình, zoom hình, làm cho hình giãn ra hoặc co lại, lật ngược hình ảnh so với ban đầu...) thay vì chỉ là những hình chụp đơn thuần như trước. Điều này cho phép mô hình hoc được nhiều hơn.

Việc sử dụng data augmentation sẽ là một cách giúp mô hình ngăn chặn overfitting và làm cho mô hình có tính khái quát hơn.

🔑Lưu ý : Data augmentation thường chỉ được dùn cho quá trình train, không sử dụng cho test cũng như validation. Việc sử dụng ImageDataGenerator với các tham số của data augmentation được xây dựng sẵn trong đó sẽ giúp cho hình ảnh được giữ nguyên trong thư mục cho đến khi load vào mô hình, chúng sẽ được thao tác ngẫu nhiên.

train_datagen_augmented = ImageDataGenerator(rescale=1/255, 
                                          rotation_range=0.2,
                                          shear_range=0.2,
                                          width_shift_range=0.2, 
                                          height_shift_range=0.2,
                                          zoom_range=0.2,
                                          horizontal_flip=True,
                                          )
train_datagen = ImageDataGenerator(rescale=1/255.)
test_datagen =  ImageDataGenerator(rescale=1/255.)

Trong phương thức flow_from_directory của mỗi instance trên có tham số shuffle mặc định là True, nghĩa là dữ liệu được đưa vào sẽ được xáo trộn, không để dữ liệu theo thứ tự ban đầu. Trước tiên, với mô hình dưới, chúng ta sẽ để shuffleFalse tức là giữ nguyên thứ tự của dự liệu được đưa vào để xem mô hình học có đạt hiệu quả hay không.

print("Augmented training images:")
train_data_augmented = train_datagen_augmented.flow_from_directory(
    train_dir,
    target_size=(224,224),
    class_mode="binary",
    shuffle=False   
)

print("Non-augmented training images:")
train_data = train_datagen.flow_from_directory(
    train_dir,
    target_size=(224,224),
    class_mode="binary",
    shuffle=False
)

print("Unchanged test images:")
test_data =test_datagen.flow_from_directory(
    test_dir, 
    target_size=(224,224),
    class_mode="binary"
)

Augmented training images: Found 1500 images belonging to 2 classes. Non-augmented training images: Found 1500 images belonging to 2 classes. Unchanged test images: Found 500 images belonging to 2 classes.

Thiết lập hình ảnh để xem sự thay khác biệt giữa hình ảnh ban đầu và hình ảnh sau khi được augmented

images,labels = train_data.next()
augmented_images, augmented_labels = train_data_augmented.next()
random_image_index = random.choice(range(len(images)))
plt.figure(figsize=(12,4))
plt.subplot(121)
plt.imshow(images[random_image_index])
plt.title(f"Hình ảnh ban đầu")
plt.subplot(122)
plt.imshow(augmented_images[random_image_index])
plt.title(f"Hình ảnh sau khi được augmented")

Text(0.5, 1.0, 'Hình ảnh sau khi được augmented')

Hình ảnh sau khi được augmented trông có khá giống với hình ảnh ban đầu, nhưng nó bị lật ngược và kéo giãn ra theo cả chiều ngang và chiều cao. Điều này sẽ tăng độ khó làm cho mô hình sẽ buộc phải cố gắng học các đặc tính trong một hình ảnh kém hoàn mỹ hơn. Thực tế, đây cũng là cách làm phổ biến khi train mô hình sử dụng hình ảnh

🤔 Vậy có nên sử dụng data augmentation không? Và nên thay đổi hình dạng của chúng ở mức độ nào cho phù hợp ?

Data augmentation là một cách để ngăn chặn việc học quá tốt của mô hình dẫn đến khi test nó không thể làm tốt như trong quá trình học (overfitting). Nếu mô hình bị overfitting, thì bạn có thể thử với data augmentation, còn bình thường thì không cần thiết.

Còn về việc thay đổi tỉ lệ, hình dạng của hình ảnh bao nhiêu là phù hợp thì đó là tùy thuộc vào cách thiết lập mà bạn muốn, nó không quy tiêu chuẩn nào cả, chỉ là dựa trên sự trải nghiệm của bạn đối với dữ liệu hình ảnh đó.

Tiếp theo, chúng ta sẽ xây dựng mô hình dựa trên data augmented để xem nó có ảnh hưởng như thế nào đến quá trình train của mô hình

model_6 = Sequential([
  layers.Conv2D(10,3,activation="relu",input_shape=(224,224,3)),
  layers.MaxPool2D(),
  layers.Conv2D(10,3,activation="relu"),
  layers.MaxPool2D(),
  layers.Conv2D(10,3,activation="relu"),
  layers.MaxPool2D(),
  layers.Flatten(),
  layers.Dense(1, activation="sigmoid")
])

model_6.compile(
    loss="binary_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_6_history = model_6.fit(
    train_data_augmented,
    steps_per_epoch=len(train_data_augmented),
    epochs=5, 
    validation_data=test_data,
    validation_steps=len(test_data)
)
    Epoch 1/5
    47/47 [==============================] - 26s 535ms/step - loss: 0.7019 - accuracy: 0.4700 - val_loss: 0.6909 - val_accuracy: 0.5460
   ...

    Epoch 5/5
    47/47 [==============================] - 24s 513ms/step - loss: 0.6750 - accuracy: 0.5920 - val_loss: 0.6333 - val_accuracy: 0.6220

Sao mô hình train lại kém hơn so với trước nhỉ? 🤔

Đó là bởi vì khi chúng ta tạo mô hình train_data_augmented, chúng ta đã tắt tính năng shuffle làm cho các dữ liệu được đưa vào mô hình theo thứ tự chứ không bị xáo trộn khiến cho khi mô hình train theo từng cụm với 32 hình ảnh, nó chỉ toàn thấy một kiểu hình ảnh hoặc của pho hoặc của fried_rice. Đến khi train xong bước đó, nó sẽ tiến hành kiểm tra qua validation thì nó không nhận diện một cách đúng đắn được hình ảnh đó là gì.

Do đó, việc trộn hình ảnh của các class vào trong một cụm để mô hình train là cực kỳ quan trọng, nó giúp cho mô hình thay vì chỉ học đơn điệu một class duy nhất thì nó học được nhiều đặc tính khác nhau của các class gắn liền với đặc tính đó.

Mô hình trên đây là một thử nghiệm nhỏ giúp bạn có thể hiểu được cách thức hoạt động của mô hình khi train với dữ liệu bất kỳ. Nếu mô hình tốt nhưng dữ liệu không tốt thì kết qủa mà nó đem lại cũng không tốt. Nên việc chuẩn bị dữ liệu trước khi train là vô cùng quan trọng.

Bây giờ, chúng ta sẽ thay đổi shuffleTrue để làm cho dữ liệu được xáo trộn.

Nếu bạn để ý, model_5model_6 cùng có một cấu trúc, nhưng thời gian thực thi mỗi epoch của chúng lại chênh lệch rất lớn (model_5 chỉ 10s trong khi model_6 đến ~24s). Đó là vì khi train với dữ liệu được augmented, ImageDataGenerator sẽ copy hình ảnh gốc rồi làm "biến dạng", thay đổi dữ liệu hình ảnh đó trước khi đưa vào trong mô hình. Một ưu điểm của cách làm này là nó không làm thay đổi hình dạng của hình ảnh gốc nhưng nhược điểm là mất rất nhiều thời gian để xử lý.

🔑Key : Một phương pháp giúp tăng tốc độ xử lý tập dữ liệu bạn có thể tham khảo tại TensorFlow's parrallel reads and buffered prefecting options.

plot_loss_curves(model_6_history)

Có vẻ như đường loss vẫn có thời điểm nó tăng lên (đường loss lý tưởng tưởng nhất là đường không quá nhọn, và là đường dốc xuống)

Mô hình tiếp theo sẽ sử dụng data_augmentation được xáo trộn. Liệu nó có hoạt động tốt hơn so với chưa được xáo trộn không ? Chúng ta sẽ xây dựng nó ngay bây giờ. Trước hết, sẽ chúng ta sẽ định nghĩa lai instance của ImageDataGenerator

model_7 (data augmentation được xáo trộn dữ liệu)

train_data_augmented_shuffled = train_datagen_augmented.flow_from_directory(
    train_dir,
    target_size=(224,224), 
    batch_size=32,
    class_mode="binary",
    shuffle=True
)

Found 1500 images belonging to 2 classes.

Thay vì viết lại toàn bộ mô hình, model_7 sẽ copy từ model_6 và chỉ cần compile và fit lại.

model_7 = tf.keras.models.clone_model(model_6)

model_7.compile(
    loss="binary_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_7_history = model_7.fit(
    train_data_augmented_shuffled,
    steps_per_epoch=len(train_data_augmented_shuffled),
    epochs=5, 
    validation_data=test_data,
    validation_steps=len(test_data)
)

Epoch 1/5 47/47 [==============================] - 26s 542ms/step - loss: 0.6797 - accuracy: 0.5893 - val_loss: 0.5845 - val_accuracy: 0.7340 ...

Epoch 5/5
47/47 [==============================] - 25s 530ms/step - loss: 0.4819 - accuracy: 0.7700 - val_loss: 0.4796 - val_accuracy: 0.7660
plot_loss_curves(model_7_history)

Có thể thay rằng hiệu suất mà model_7 đem lại được cải thiện đáng kể so với model_6. Đó là công dụng của việc trộn dữ liệu trước khi đưa vào từng cụm trong mô hình. Khi train, mô hình có thể quan học được cả dữ liệu từ 2 class phofried_rice ở mỗi cụm và có thể đánh giá được những gì nó đã học từ cả 2 class hơn là chỉ 1 class.

Đường loss của model_7 có vẻ giảm nhiều và đồng đều giữa trainvalidation so với model_6 nhưng nó vẫn có thời điểm tăng. Có thể do mô hình chưa đủ độ phức tạp để mô hình học được tốt hơn. Chúng ta sẽ tiếp tục cải thiện mô hình.

model_8 Cải thiện mô hình sử dụng data augmentation

Chúng ta đã train khá nhiều mô hình trên tập dữ liệu và chúng đem lai kết quả khá khả quan. Nhưng mô hình có thể hoạt động tốt hơn nữa không?

Ở những module trước, bạn có nhớ cách để cải thiện mô hình không? Bây giờ, chúng ta sẽ cùng điểm lại một số phương pháp để cải thiện mô hình :

  • Tăng số lượng layers cho mô hình (Thêm Conv2D layers)
  • Tăng số lượng filters trong mỗi Convolutional layer( VD : 10, hoặc 32,64,128... những con số này không cố định, chúng thường được tìm thấy thông qua quá trình thử và sai).
  • Train mô hình lâu hơn (tăng số epochs)
  • Tìm learning_rate tốt cho mô hình
  • Tăng thêm nhiều dữ liệu (Tạo điều kiện cho mô hình học được nhiều hơn)
  • Sử dụng transfer learning để tận dụng lại mô hình đã được xây dựng và học trước đó và thay đổi các tham số cho phù hợp với trường hợp của chúng ta.

Việc thay đổi các thiết lập trước đó (ngoại trừ 2 ý cuối) trong quá trình phát triển mô hình thường được gọi là hyperparameter tuning (điều chỉnh hyperparameter)

hyperparameter tuning có thể hiểu như là món ăn đã được chế biến sẵn, bạn chỉ cần nêm nếm sao cho phù hợp với khẩu vị của mình.

model_8 tới đây sẽ quay trở lại nơi ta bắt đầu xây dựng mô hình (model_1 hay the TinyVGG architecture from CNN explainer)

model_8 = Sequential([
    layers.Conv2D(10,3,activation="relu",input_shape=(224,224,3)),
    layers.Conv2D(10,3,activation="relu"),
    layers.MaxPool2D(),
    layers.Conv2D(10,3,activation="relu"),
    layers.Conv2D(10,3,activation="relu"),
    layers.MaxPool2D(),
    layers.Flatten(),
    layers.Dense(1, activation="sigmoid")
])

model_8.compile(
    loss="binary_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_8_history = model_8.fit(
    train_data_augmented_shuffled,
    steps_per_epoch=len(train_data_augmented_shuffled),
    epochs=5, 
    validation_data=(test_data),
    validation_steps=len(test_data)
)

Epoch 1/5 47/47 [==============================] - 26s 548ms/step - loss: 0.6902 - accuracy: 0.5667 - val_loss: 0.6233 - val_accuracy: 0.6180 ...

47/47 [==============================] - 26s 542ms/step - loss: 0.5087 - accuracy: 0.7473 - val_loss: 0.4506 - val_accuracy: 0.7780
model_8.summary()
    Model: "sequential_8"
    ...
    Total params: 31,101
    Trainable params: 31,101
    Non-trainable params: 0
    _________________________________________________________________
model_1.summary( )
    Model: "sequential"
    ...
    Total params: 31,101
    Trainable params: 31,101
    Non-trainable params: 0
plot_loss_curves(model_8_history)
plt.suptitle("model_8 VGG Tiny architecture")

Text(0.5, 0.98, 'model_8 VGG Tiny architecture')

plot_loss_curves(model_1_history)
plt.suptitle("model_1 VGG Tiny architecture")

Text(0.5, 0.98, 'model_1 VGG Tiny architecture')

model_8 các đường cong đồng đều hơn, nhưng hiệu suất của mô hình traintest không thể hơn được so với model_1. Đó là vì khi đưa hình ảnh biến dạng vào, mô hình sẽ học được nhiều thứ hơn nhưng cốt lõi của dữ liệu sẽ không được nhiều.

4.7 Dự đoán với mô hình được train

Một mô hình tốt là mô hình có khả năng dự đoán các dữ liệu mà nó chưa từng biết đến chính xác. Để kiểm chứng điều này, chúng ta sẽ lấy một số hình ảnh bên ngoài liên quan đến phofried_rice để test mô hình.

# download hình ảnh được lấy trên Google 
!wget https://www.dropbox.com/s/7p20cozvy41oaq1/pho-com-chien-test.zip
    pho-com-chien-test. 100%[===================>] 916.78K  4.44MB/s    in 0.2s    

    2021-09-07 03:23:55 (4.44 MB/s) - ‘pho-com-chien-test.zip’ saved [938786/938786]
unzip_file("pho-com-chien-test.zip")

Unziped file

predicted_dir = "pho-com-chien-test"

random_image_name = random.choice(os.listdir(predicted_dir))
target_image_dir = os.path.join(predicted_dir, random_image_name)
target_image = plt.imread(target_image_dir)
plt.imshow(target_image)

<matplotlib.image.AxesImage at 0x7f7f5d87c390>

Trước khi dự đoán mô hình, chúng ta cần tạo một hàm để load và đưa hình ảnh về đúng định dạng mà chúng ta đã thiết lập từ trước

def load_and_prep_image(image_dir, shape=(224,224)) : 
  image = tf.io.read_file(image_dir) 
  image = tf.image.decode_image(image,channels=3)
  # resize image 
  image = tf.image.resize(image,size=shape)
  return image / 255.
pho = load_and_prep_image("/content/pho-com-chien-test/pho1.jpg")
pho, pho.shape

(<tf.Tensor: shape=(224, 224, 3), dtype=float32, numpy= array([[[0.00000000e+00, 1.17647061e-02, 5.88235296e-02], [3.92156886e-03, 1.960784 [3.92156886e-03, 1.96078438e-02, 6.66666701e-02], ..., [1.53221697e-01, 1.53221697e-01, 1.53221697e-01], [1.53221697e-01, 1.53221697e-01, 1.53221697e-01], [1.68627456e-01, 1.68627456e-01, 1.68627456e-01]]], dtype=float32)>, TensorShape([224, 224, 3]))

Tuyệt vời, cấu trúc ma trận của hình ảnh đã đúng như định dạng. Thử dự đoán xem sao ?

try : 
  model_8.predict(pho)
except ValueError as err: 
  print(err)
    in user code:            
        ValueError: Input 0 of layer sequential_8 is incompatible with the layer: : expected min_ndim=4, found ndim=3. Full shape received: (32, 224, 3)

Có lỗi xảy ra.

Mặc dù hình ảnh dự đoán đã có cùng kích thước với hình ảnh được train nhưng nó vẫn còn thiếu chiều. expected min_ndim=4, found ndim=3 nghĩa là nó mong muốn được nhận một hình ảnh có cấu trúc 4 chiều nhưng thực tế chỉ có 3.

Khi mô hình được train theo cụm nó sẽ lấy chiều đầu tiên làm kích thước tính theo số cụm (batch size) còn lại nó sẽ giữ nguyên kích thước của hình ảnh. Do đó, mô hình được train có hình dạng (số cụm, 224,224,3). Vậy nên chúng ta cần tăng thêm số chiều cho hình ảnh được dự đoán là 1 ở chiều đầu tiên tượng trưng cho số cụm như hình dạng của mô hình.

Để tăng số chiều trong tensorflow có thể sử dụng tf.expand_dims.

# Tăng số chiều cho hình ảnh pho
print(f"Hình dạng của pho : {pho.shape}")
pho = tf.expand_dims(pho, axis=0)
print(f"Hình dạng của pho sau khi tăng thêm 1 chiều : {pho.shape}")

Hình dạng của pho : (224, 224, 3) Hình dạng của pho sau khi tăng thêm 1 chiều : (1, 224, 224, 3)

OK, lúc này có thể đưa hình ảnh vào mô hình để dự đoán

pho_pred_prob = model_8.predict(pho)
pho_pred_prob

array([[0.8494561]], dtype=float32)

Các dự đoán xuất hiện ở dạng xác suất chứ không phải ở dạng label của nó. Nói cách khác, xác suất để hình ảnh là lớp này hoặc lớp khác là bao nhiêu.

Nhưng khi chúng ta đang xử lý vấn để binary class, nếu xác suất dự đoán >=0.5 thì mô hình dự đoán là class 1. Ngược lại, nếu <0.5 thì mô hình dự đoán là class 0.

Do đó, pho_pred dự đoán là 0.225 nghĩa là 0. Chúng ta sẽ làm tròn xác suất dự đoán để nó chỉ là 0 hoặc 1 bằng tf.round()

pho_pred_label = class_names[int(tf.round(pho_pred_prob[0]))]
pho_pred_label

'pho'

Thay vì dự đoán thủ công như trên, chúng ta sẽ tạo hàm để có thể dự đoán số hình ảnh mà chúng ta muốn

import math
def make_predict_images(model, test_dir, n_samples=None, image_shape=(224,224)) :   
  if n_samples is None: 
    images_dir = [os.path.join(test_dir, image_dir) for image_dir in os.listdir(test_dir)]
  else :
    random_images_name = random.sample(os.listdir(test_dir), k=n_samples)
    images_dir = [os.path.join(test_dir, ) for image_name in random_images_name]
  n_cols=3
  n_rows= math.ceil(len(images_dir) / n_cols)
  plt.figure(figsize=(n_cols*5, n_rows * 5))
  for image_index, image_dir in enumerate(images_dir) :     
    plt.subplot(n_rows, n_cols, image_index+1)
    image = load_and_prep_image(image_dir, shape=image_shape)    
    model_pred_prob = model.predict(tf.expand_dims(image, axis=0))
    model_pred = class_names[int(tf.round(model_pred_prob[0]))]   
    plt.imshow(image)
    plt.title(f"Dự đoán : {model_pred} ")
    plt.axis(False)
make_predict_images(model_8, predicted_dir)


Bài viết có liên quan