Deep Learning với Tensorflow Module 5 phần 2: Convolutional Neural Network với dữ liệu nhiều class
Để 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:
- Khai phá dữ liệu
- Chuẩn bị dữ liệu ( đồng bộ cấu trúc dữ liệu theo đúng định dạng )
- Khởi tạo mô hình
- Fit mô hình
- Đánh giá mô hình
- Cải thiện mô hình
- 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 train
và test
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_mode
là binary
, 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à test
là 500
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 train
và test
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()