Deep Learning với Tensorflow Module 5 phần 2: Convolutional Neural Network với dữ liệu nhiều 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_2_convolutional_neural_network_multi_class.ipynb

Trong phần trước, chúng ta đã xây dựng các mô hình phân loại hình ảnh 2 class với convolutional neural network (bạn có thể hiểu là mạng noron phức hợp nhưng những khái niệm này người viết sẽ không dịch sang tiếng Việt vì có thể nó không giải thích đầy đủ được ý nghĩa của nó). Các mô hình đạt hiệu suất khác nhau phụ thuộc vào số layer mà mô hình đó học được cũng như dữ liệu được đưa vào để train mô hình. Nhìn chung, với 2 class mô hình học khá tốt vì nó chỉ phân biệt hoặc cái này hoặc cái kia và những đặc trưng với sẽ dễ dàng phân biệt.

Tuy nhiên, nếu tập dữ liệu có nhiều class (VD: 10 class) thì rất có thể mô hình sẽ khó phân biệt đối tượng hơn vì tuy rằng mỗi class sẽ có những đặc trưng riêng, nhưng đôi khi chúng lại có một điểm nào đó tương đồng với nhau, điều đó sẽ khiến cho mô hình học khó phân biệt và nhận dạng đúng đối tượng đó là gì so với dữ liệu chỉ có 2 class.

Trong phần này, chúng ta cũng sẽ xây dựng mô hình dựa trên kiến trúc của TinyVGG từ CNN explainer với 10 class thay vì 2 class như phần trước.

Nội dụng của phần này gồm:

  1. Khai phá dữ liệu
  2. Chuẩn bị dữ liệu ( đồng bộ cấu trúc dữ liệu theo đúng định dạng )
  3. Khởi tạo mô hình
  4. Fit mô hình
  5. Đánh giá mô hình
  6. Cải thiện mô hình
  7. Lặp lại quá trình để mô hình tốt hơn

1. Khai phá dữ liệu

Trong phần này, chúng ta sẽ thực hiên các bước sau:

  • Tải xuống tập dữ liệu
  • Tìm hiểu cấu trúc tổng thể
  • Hiển thị tên của các class có trong tập dữ liệu
  • Hiển thị hình ảnh bất kỳ được lấy từ một số class ngẫu nhiên
!wget https://www.dropbox.com/s/xjyf4ug18zqvig0/10_food_classes.zip
    10_food_classes.zip 100%[===================>] 478.05M  75.4MB/s    in 6.0s    

    2021-09-07 16:04:12 (79.8 MB/s) - ‘10_food_classes.zip.1’ saved [501269035/501269035]
import zipfile

def unzip_file(filepath) : 
  zipref = zipfile.ZipFile(filepath)
  zipref.extractall()
  zipref.close()
  print("Unziped file")
unzip_file("10_food_classes.zip")

Unziped file

import os
def walk_through_directory(dirname) : 
  for pathname, dirnames, filenames in os.walk(dirname) : 
    print(f"Có {len(dirnames)} thư mục con và {len(filenames)} files trong thư mục {pathname}")
dirname = "10_food_classes"
walk_through_directory(dirname)

Có 2 thư mục con và 0 files trong thư mục 10_food_classes Có 10 thư mục con và 0 files trong thư mục 10_food_classes/train Có 0 thư mục con và 750 files trong thư mục 10_food_classes/train/prime_rib Có 0 thư mục con và 750 files trong thư mục 10_food_classes/train/greek_salad Có 0 thư mục con và 750 files trong thư mục ... 10_food_classes/test/pulled_pork_sandwich Có 0 thư mục con và 250 files trong thư mục 10_food_classes/test/clam_chowder Có 0 thư mục con và 250 files trong thư mục 10_food_classes/test/pad_thai

Tạo đường dẫn đến 2 tập dữ liệu traintest

train_dir = "10_food_classes/train"
test_dir = "10_food_classes/test"

Lấy danh sách các label của tập dữ liệu train

import pathlib
pathdir = pathlib.Path(train_dir)
class_names = [item.name for item in pathdir.glob("*")]
num_classes = len(class_names)
print(f"Danh sách tên các class : {class_names}")
print(f"Có {num_classes} classes")

Danh sách tên các class : ['prime_rib', 'greek_salad', 'spaghetti_bolognese', 'filet_mignon', 'panna_cotta', 'bruschetta', 'garlic_bread', 'pulled_pork_sandwich', 'clam_chowder', 'pad_thai'] Có 10 classes

Lấy ngẫu nhiên một số hình ảnh từ một class ngẫu nhiên và hiển thị. Trước tiên, chúng ta sẽ viết một hàm để thực thi quá trình này.

import matplotlib.pyplot as plt 
import random
import math
def get_random_images(target_dir, n_samples=1) : 
  target_class_names = random.sample(os.listdir(target_dir),k=n_samples)
  target_class_paths = [os.path.join(target_dir, class_name) for class_name in target_class_names]
  n_cols = 3 
  n_rows = math.ceil(n_samples / n_cols)
  plt.figure(figsize=(n_cols * 6, n_rows * 4))
  for i, class_path in enumerate(target_class_paths) :
    random_image_name = random.choice(os.listdir(class_path))
    random_image_path = os.path.join(class_path, random_image_name)
    image = plt.imread(random_image_path)
    plt.subplot(n_rows, n_cols, i+1)
    plt.imshow(image)
    plt.axis(False)
    plt.title(target_class_names[i])
get_random_images(train_dir,5)

Chuẩn bị dữ liệu

Là quá trình load dữ liệu và đồng bộ tất cả về cùng một định dạng trước khi đưa vào mô hình train. Như trong phần trước, chúng ta sẽ sử dụng ImageDataGenerator để thực hiện bước này.

import tensorflow as tf
from tensorflow.keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(rescale=1/255.)
test_datagen = ImageDataGenerator(rescale=1/255.)
train_data = train_datagen.flow_from_directory(
    train_dir,
    target_size=(224,224),
    class_mode="categorical",
    batch_size=32,
    shuffle=True
)
test_data = test_datagen.flow_from_directory(
    test_dir,
    target_size=(224,224),
    class_mode="categorical",
    batch_size=32,
    shuffle=True
)

Found 7500 images belonging to 10 classes. Found 2500 images belonging to 10 classes.

Với tập dữ liệu 2 class, chúng ta sử dụng class_modebinary, nhưng khi có trên 2 class thì thay đổi thành categorical.

Tại sao luôn đưa kích thước hình ảnh là (224,224)? Thực ra, bạn có thể điều chỉnh kích thước tùy theo ý mình, nhưng 224x224 là một kích thước được sử dụng rất phổ biến dành cho quá trình chuẩn bị dữ liệu hình ảnh

3. Khởi tạo mô hình

3.1 Mô hình cơ sở model_1

Mô hình cơ sở này sẽ giống với mô hình cơ sở được sử dụng cho vấn đề phân loại 2 class những có một số điều chình :

  • Thay đổi output layer thành 10 (với 2 class là 1)
  • Thay đổi activation function của output layer thành softmax (thay vì sigmoid cho 2 class)
  • Thay đổi loss function khi compile thành categorical_crossentropy (thay vì binary_crossentropy cho 2 class)
from tensorflow.keras import Sequential, layers
tf.random.set_seed(42)

model_1 = 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(num_classes, activation="softmax")
])

model_1.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)
model_1.summary()
    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    conv2d (Conv2D)              (None, 222, 222, 10)      280       
    _________________________________________________________________
    conv2d_1 (Conv2D)            (None, 220, 220, 10)      910       
    _________________________________________________________________
    max_pooling2d (MaxPooling2D) (None, 110, 110, 10)      0         
    _________________________________________________________________
    conv2d_2 (Conv2D)            (None, 108, 108, 10)      910       
    _________________________________________________________________
    conv2d_3 (Conv2D)            (None, 106, 106, 10)      910       
    _________________________________________________________________
    max_pooling2d_1 (MaxPooling2 (None, 53, 53, 10)        0         
    _________________________________________________________________
    flatten (Flatten)            (None, 28090)             0         
    _________________________________________________________________
    dense (Dense)                (None, 10)                280910    
    =================================================================
    Total params: 283,920
    Trainable params: 283,920
    Non-trainable params: 0
    _________________________________________________________________

4. Fit mô hình

model_1_history = model_1.fit(
    train_data,
    steps_per_epoch=len(train_data), 
    epochs=5, 
    validation_data=test_data, 
    validation_steps=len(test_data)
)

Epoch 1/5 235/235 [==============================] - 57s 233ms/step - loss: 2.0480 - accuracy: 0.2640 - val_loss: 1.7429 - val_accuracy: 0.3896 Epoch 2/5 235/235 [==============================] - 55s 233ms/step - loss: 1.6231 - accuracy: 0.4520 - val_loss: 1.5554 - val_accuracy: 0.4616 Epoch 3/5 235/235 [==============================] - 55s 234ms/step - loss: 1.2469 - accuracy: 0.5884 - val_loss: 1.5787 - val_accuracy: 0.4356 Epoch 4/5 235/235 [==============================] - 55s 234ms/step - loss: 0.7235 - accuracy: 0.7681 - val_loss: 1.9446 - val_accuracy: 0.3944 Epoch 5/5 235/235 [==============================] - 55s 233ms/step - loss: 0.3029 - accuracy: 0.9112 - val_loss: 2.6389 - val_accuracy: 0.3944

Mô hình với 10 class train lâu hơn mô hình 2 class rất nhiều. Đó là vì tập dữ liệu được đưa vào mô hình hiện tại có sô lượng hình ảnh lớn hơn rất nhiều. Với mô hình 2 class, có tất cả 2000 hình ảnh, với train là 1500 và test500 trong khi với mô hình 10 classes có tất cả 10000 hình, train mỗi class là 750, tổng 10 class là 7500, test mỗi class là 250, tổng 10 class là 2500.

Khi dữ liệu càng nhiều thì thời gian để mô hình train sẽ càng lâu.

5. Đánh giá mô hình

model_1.evaluate(test_data)

79/79 [==============================] - 13s 161ms/step - loss: 2.6389 - accuracy: 0.3944

[2.6389076709747314, 0.3944000005722046]

Vẽ đường loss_curves để đánh giá quá trình train của mô hình qua mỗi epoch

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("Accuracy")
  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_1_history)

Qua biểu đồ có thể thấy : - Với biểu đồ Accuracy, Train accuracy tăng rất nhiều trong khi val_accuracy lại không tăng, thậm chí còn bị giảm - Với biểu đồ Loss train_loss giảm rất nhiều trong khi val_loss lại có chiều hướng tăng.

👉 Có thể thấy mô hình trong quá trình train học rất tốt nhưng khi đưa những dữ liệu chưa từng được học vào, nó hoạt động rất tệ. Điều này chứng tỏ mô hình hiện tại đang bị overfitting

6. Cải thiện mô hình

Như đã thấy ở mô hình trên đang gặp vấn đề về overfitting. Để ngăn chặn việc học quá tốt nhưng kiểm tra quá tệ này của mô hình chúng ta có thể thực hiện một số cách sau :

  • Tăng cường thêm dữ liệu : Khi dữ liệu được đưa vào càng nhiều thì mô hình sẽ có thêm nhiều cơ hội để tìm kiếm các đặc tính riêng của dữ liệu đó
  • Làm cho mô hình Đơn giản lại: Đôi khi mô hình quá phức tạp sẽ khiến cho nó học các mẫu từ dữ liệu quá tốt và không có khả năng tổng quát hóa đối với những dữ liệu mà nó chưa từng thấy. Một cách để đơn giản hóa mô hình là giảm đi số lượng layer hoặc các units trong layers đó.
  • Sử dụng data augmentation : Biến đổi hình dạng của dữ liệu sẽ làm cho mô hình học kỹ hơn vì nó không còn đơn giản như hình gốc. Nếu một mô hình có thể học được các mẫu dữ data augmentation, mô hình đó có thể tổng quát hóa tốt hơn đối với dữ liệu mà nó chưa từng thấy.
  • Sử dụng transfer learning: việc tái sử dụng mô hình đã được train trước đó là một trong những cách tốt để giúp cải thiên mô hình và tiết kiệm thời gian train mô hình. Trong trường hợp này, sử dụng mô hình thị giác máy tính để train lại trên tập dữ liệu lớn và sau đó điều chỉnh một chút để phù hợp hơn với loại dữ liệu mà ta đang sử dụng.

6.1 Đơn giản hóa mô hình

Chúng ta sẽ loại bỏ đi một số layer để mô hình trở nên đơn giản hơn

model_2 = Sequential([
                      layers.Conv2D(10,3,activation="relu", input_shape=(224,224,3)),                      
                      layers.MaxPool2D(),
                      layers.Conv2D(10,3,activation="relu"),
                      layers.MaxPool2D(),
                      layers.Flatten(),
                      layers.Dense(num_classes, activation="softmax")
])

model_2.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_2_history = model_2.fit(
    train_data,
    steps_per_epoch=len(train_data),
    epochs=5,
    validation_data=test_data, 
    validation_steps=len(test_data)
)

Epoch 1/5 235/235 [==============================] - 51s 213ms/step - loss: 1.9779 - accuracy: 0.3143 - val_loss: 1.6614 - val_accuracy: 0.4208 Epoch 2/5 235/235 [==============================] - 50s 212ms/step - loss: 1.4735 - accuracy: 0.5020 - val_loss: 1.4819 - val_accuracy: 0.4824 Epoch 3/5 235/235 [==============================] - 50s 214ms/step - loss: 1.1278 - accuracy: 0.6304 - val_loss: 1.6527 - val_accuracy: 0.4368 Epoch 4/5 235/235 [==============================] - 50s 212ms/step - loss: 0.7117 - accuracy: 0.7767 - val_loss: 1.7475 - val_accuracy: 0.4372 Epoch 5/5 235/235 [==============================] - 50s 211ms/step - loss: 0.3847 - accuracy: 0.8912 - val_loss: 2.1692 - val_accuracy: 0.4100

plot_loss_curves(model_2_history)

So với model_1 thì model_2 dù đã được giản hóa số layer những có vẻ như phương pháp này không hiệu quả để ngăn chặn overfitting.

Tiếp theo, chúng ta sẽ sử dụng data augmentation

train_datagen_augmentation = ImageDataGenerator(
    rescale=1/255.,
    rotation_range=0.2,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True
)
train_data_augmented = train_datagen_augmentation.flow_from_directory(
    train_dir,
    target_size=(224,224),
    batch_size=32, 
    class_mode="categorical",
    shuffle=True
)

Found 7500 images belonging to 10 classes.

Chúng ta sẽ clone lại kiến trúc của mô hình cơ sở (model_1) để compile và fit mô hình

# model 3 sẽ sử dụng cấu trúc tương tự như model_1
initial_epoch = 5 
model_3 = tf.keras.models.clone_model(model_2)

model_3.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_3_history = model_3.fit(
    train_data_augmented, 
    steps_per_epoch=len(train_data_augmented),
    epochs=initial_epoch, 
    validation_data = test_data, 
    validation_steps=len(test_data)
)

Epoch 1/5 235/235 [==============================] - 123s 523ms/step - loss: 2.2490 - accuracy: 0.1727 - val_loss: 1.9919 - val_accuracy: 0.3076 Epoch 2/5 235/235 [==============================] - 120s 511ms/step - loss: 2.0394 - accuracy: 0.2863 - val_loss: 1.8941 - val_accuracy: 0.3416 Epoch 3/5 235/235 [==============================] - 120s 511ms/step - loss: 1.9728 - accuracy: 0.3167 - val_loss: 1.7776 - val_accuracy: 0.3688 Epoch 4/5 235/235 [==============================] - 119s 507ms/step - loss: 1.9337 - accuracy: 0.3340 - val_loss: 1.7723 - val_accuracy: 0.3868 Epoch 5/5 235/235 [==============================] - 119s 507ms/step - loss: 1.9062 - accuracy: 0.3359 - val_loss: 1.7074 - val_accuracy: 0.4096

plot_loss_curves(model_3_history)

👍 Các đường traintest trong 2 biểu đồ đã gần như đi theo cùng một hướng. Mặc dù mô hình học không thực sự tốt với data augmentation nhung nó đã không còn tình tràng overfitting như 2 mô hình trước.

Mô hình mới chỉ train được qua 5 epochs, nếu train lâu hơn liệu khả năng học của mô hình này liệu có tốt hơn không? Chúng ta sẽ thử cho mô hình chạy thêm 5 epoch nữa

num_epochs = initial_epoch + 5 

model_3.compile(
    loss="categorical_crossentropy",
    optimizer="adam",
    metrics=["accuracy"]
)

model_3_history = model_3.fit(
    train_data_augmented,
    steps_per_epoch=len(train_data_augmented),
    epochs=num_epochs, 
    initial_epoch=initial_epoch,
    validation_data=test_data,
    validation_steps=len(test_data)
)

Epoch 6/10 235/235 [==============================] - 121s 513ms/step - loss: 1.8822 - accuracy: 0.3517 - val_loss: 1.6873 - val_accuracy: 0.4236 Epoch 7/10 235/235 [==============================] - 120s 510ms/step - loss: 1.8341 - accuracy: 0.3740 - val_loss: 1.7538 - val_accuracy: 0.3776 Epoch 8/10 235/235 [==============================] - 120s 509ms/step - loss: 1.8023 - accuracy: 0.3859 - val_loss: 1.6559 - val_accuracy: 0.4268 Epoch 9/10 235/235 [==============================] - 119s 505ms/step - loss: 1.7463 - accuracy: 0.4035 - val_loss: 1.5375 - val_accuracy: 0.4804 Epoch 10/10 235/235 [==============================] - 116s 494ms/step - loss: 1.6953 - accuracy: 0.4228 - val_loss: 1.5873 - val_accuracy: 0.4568

plot_loss_curves(model_3_history)

Sau khi train thêm, mô hình có vẻ bị overfitting trở lại.😟

7. Lặp lại quá trình để mô hình tốt hơn

Chúng ta có thể tiếp tục làm điều này. Việc tái cấu trúc lại kiến trúc của mô hình, thay đổi số layer, các units trong layers, thay đổi learning_rate, train mô hình lâu hơn thậm chí tìm phương thức khác để xử lý dữ liệu trước khi đưa vào mô hình ngoài data augmentation.

Một điều thú vị mà chúng ta chưa đề cập đến đó là transfer learning. Tuy nhiên, chủ đề này sẽ là phần sau, và nó có rất nhiều điều thú vị trong đó. Trước khi kết thúc module này, chúng ta sẽ sử dụng mô hình được train ở trên để dự đoán hình xem hình ảnh đó là gì.

8. Dự đoán hình ảnh

mô hình hiện tại đã học được các label :

class_names

['prime_rib', 'greek_salad', 'spaghetti_bolognese', 'filet_mignon', 'panna_cotta', 'bruschetta', 'garlic_bread', 'pulled_pork_sandwich', 'clam_chowder', 'pad_thai']

Tải xuông tập hình ảnh được lấy từ Google, mỗi hình ảnh đại diện cho 1 class trên :

!wget https://www.dropbox.com/s/eqmanc3f6zwwzpq/multi_class_test.zip
    multi_class_test.zi 100%[===================>]   1.95M  --.-KB/s    in 0.07s   

    2021-09-07 16:35:19 (28.4 MB/s) - ‘multi_class_test.zip.1’ saved [2049945/2049945]
unzip_file("multi_class_test.zip")

Unziped file

walk_through_directory("multi_class_test")

Có 0 thư mục con và 10 files trong thư mục multi_class_test

os.listdir("multi_class_test")

['prime_rib.jpg', 'bruschetta.jpg', 'garlic_bread.jpeg', 'filet_mignon.jpg', 'panna_cotta.jpg', 'clam_chowder.jpg', 'pulled_pork_sandwich.jpeg', 'greek_salad.jpg', 'pad_thai.jpg', 'spaghetti_bolognese.jpeg']

Vì đây là những hình ảnh được tải xuống chưa qua quá trình xử lý cũng như đồng bộ dữ liệu theo định dạng mà mô hình đã train trước đó nên chúng ta chưa thể đưa dữ liệu vào mô hình để train được. Trước hết, chúng ta sẽ tạo hàm để load hình và đồng bộ dữ liệu theo đúng định dạng

def load_and_prep_image(filepath, shape=(224,224)) : 
  # load hình ảnh 
  image = tf.io.read_file(filepath)
  # decode hình ảnh thành các giá trị dưới dạng ma trận 
  image = tf.image.decode_image(image, channels=3)
  # thay đổi kích thước hình
  image = tf.image.resize(image, size=shape)
  # trả về giá trị được chuẩn hóa
  return image/255.

Ví dụ lấy thử một hình để test hàm trên :

load_and_prep_image("/content/multi_class_test/clam_chowder.jpg")

<tf.Tensor: shape=(224, 224, 3), dtype=float32, numpy= array([[[0.7390756 , 0.73515403, 0.71946776], [0.7211435 , 0.7172219 , 0.70153564], [0.70350146, 0.6995799 , 0.6838936 ], ..., [0.9422966 , 0.95013976, 0.9462182 ], [0.9455635 , 0.94192153, 0.9437425 ], [0.9742141 , 0.9585278 , 0.9624494 ]]], dtype=float32)>

Ok, hàm chuẩn bị dữ liệu đã sẵn sàng, chúng ta sẽ tạo một vòng lặp để load và đồng bộ đinh dạng hình ảnh trong folder "multi_class_test" và đưa vào mô hình dự đoán, nhưng như phần trước, mô hình sẽ nhận một dữ liệu 4 chiều (batch_size, width, height, color channel), trong khi hình ảnh trong dữ liệu chỉ có 3. Do đó, cần phải thêm 1 chiều đầu tiên cho hình ảnh trước khi đưa vào mô hình dự đoán.

import pandas as pd
predicted_images_path = [os.path.join("multi_class_test", image_name) for image_name in os.listdir("multi_class_test")]

for index, image_path in enumerate(predicted_images_path) :   
  image = load_and_prep_image(image_path)
  actual_label = image_path.split("/")[-1].split(".")[0]
  label_index = 0
  for i, class_name in enumerate(class_names) : 
    if actual_label.strip().lower() == class_name.strip().lower() : 
      label_index = i
  pred_probs = model_3.predict(tf.expand_dims(image, axis=0))
  max_index = tf.argmax(tf.squeeze(pred_probs))
  pred_label = class_names[max_index]
  fig, (ax1,ax2) = plt.subplots(1,2,figsize=(16,6))
  ax1.imshow(image)
  ax1.axis(False)
  if actual_label.strip().lower() ==  pred_label.strip().lower() :
    color = "green"
  else : 
    color = "red"
  ax1.set_title(f"Actual: {actual_label}\n, Predict: {pred_label} with {pred_probs[0][max_index]*100:.2f}%", color=color)  
  pd.DataFrame(tf.squeeze(pred_probs), index=[class_names]).plot.bar(ax=ax2, legend=None)
  ax2.get_children()[label_index].set_color("red")
  plt.show()


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