Deep Learning với Tensorflow Module 8 phần 1: Dự án xây dựng mô hình nhận diện giống chó trong ảnh
Bài viết này là phần tóm tắt quá trình thực hiện của dự án. Để xem toàn bộ nội dung bạn có thể đọc tại notebook project 2 dog breed idintification
Trong dự án này, chúng ta sẽ sử dụng deep learning kết hợp transfer learning để xây dựng mô hình phân loại giống chó 🐶 từ một hình ảnh bất kỳ.
Dữ liệu để đưa train, kiểm định mô hình và test sẽ được lấy từ Kaggle dog breed identification competition. Trong đó có 10000+ hình ảnh gồm 120 giống chó khác nhau.
Dự án này được gọi là phân loại ảnh nhiều lớp (multi classification) vì có đến 120 class. Nếu chúng ta chỉ cố gắng phân loại 2 class (chẳng hạn như phân biệt hình ảnh đó là chó hay là mèo) thì nó sẽ được gọi là phân loại nhị phân (binary class).
Nội dung phần 1 của bài viết này bao gồm : 1. Tải dữ liệu và khám phá dữ liệu 2. Tiền xử lý dữ liệu 3. Khởi tạo và train mô hình + Giới thiệu về các hàm callback giám sát mô hình train + Xây dựng mô hình ResnetV50 trích xuất đặc trưng + Xây dựng mô hình EfficientNet trích xuất đặc trưng + Tinh chỉnh mô hình trích xuất đặc trưng. + Đánh giá mô hình tinh chỉnh + Lưu mô hình
1. Tải dữ liệu và khám phá dữ liệu
Bạn có thể tải dữ liệu từ Kaggle dog breed identification competition về máy và sau đó upload ngay tại Google Colab, hoặc lưu trong Google Driver. Một cách khác là nén dữ liệu dưới dạng file zip và đưa lên cloud.
Tập dữ liệu gồm có 2 folder train
và test
. Trong folder train
và test
là những file ảnh mà tên của nó được thể hiện dưới dạng id
.
Đối với train
, bạn có thể tìm thấy label của nó thông qua file labels.csv
. Trong khi folder test
là nơi để sau này chúng ta sẽ tiến hành dự đoán và nếu cần thiết bạn có thể deploy lên Kaggle dog breed identification competition.
🔑Lưu ý : Chúng ta sẽ chỉ làm việc trên folder
train
, còntest
chỉ là phần dự đoán mô hình sau khi quá trìnhtrain
vàvalidation
kết thúc.
!wget https://www.dropbox.com/s/e1x3kpy1ct31h0e/dog-breed-identification.zip
Để tiết kiệm thời gian cũng như giúp cho notebook được rõ ràng, thay vì phải viết lại các hàm đã sử dụng rất nhiều lần từ các module trước, bạn có thể tạo hàm utility_functions.py
rồi import
hoặc tải trưc tiếp xuống.
!wget https://www.dropbox.com/s/v4sla7jvi9cltg8/utility_functions.py
from utility_functions import unzip_file,walk_through_directory
Đầu tiên, bạn cần giải nén file zip. Vì file zip này không khi giải nén sẽ có folder train
, test
và tập tin labels.csv
... nên sẽ khiến cho thư mục hiện tại sẽ rất khó nhìn. Vì vậy, chúng ta sẽ tạo một thư mục có tên dog-breed-identification
, sau đó, dời file dog-breed-identification.zip
vào trong thư mục này rồi mới tiến hành giải nén.
import os import shutil zip_name = "dog-breed-identification.zip" dog_breed_dir = "dog-breed-identification" if not os.path.isdir(dog_breed_dir): # Tạo thư mục os.mkdir(dog_breed_dir) # Đưa tập tin vào thư mục vừa khởi tạo shutil.move(zip_name,dog_breed_dir) zip_path = os.path.join(dog_breed_dir, zip_name) # chuyển vị trí hoạt động os.chdir(dog_breed_dir) # Giải nén tập tin unzip_file(zip_name) # Xóa tập tin .zip os.remove(zip_name) # Dời vị trí về ban đầu os.chdir("..")
Khám phá các dữ liệu trong các thư mục con của dog-breed-identification
walk_through_directory(dog_breed_dir)
Có 2 thư mục và 2 tập tin trong thư mục dog-breed-identification Có 0 thư mục và 10222 tập tin trong thư mục dog-breed-identification/train Có 0 thư mục và 10357 tập tin trong thư mục dog-breed-identification/test
Trong thư mục train
chỉ gồm các hình ảnh và tên của nó là những id
. Tuy nhiên, tập tin label.csv
đã cung cấp thông tin của những id
này để chúng ta có thể xử lý, lọc dữ liệu lại.
import pandas as pd import numpy as np
File labels.csv
mô tả label của mỗi hình ảnh trong folder train
, tên hình ảnh được thể hiện dưới dạng id
của nó.
labels_csv = pd.read_csv(os.path.join(dog_breed_dir, "labels.csv")) labels_csv
id | breed | ||||
---|---|---|---|---|---|
0 | 000bec180eb18c7604dcecc8fe0dba07 | boston_bull | |||
1 | 001513dfcb2ffafc82cccf4d8bbaba97 | dingo | |||
2 | 001cdf01b096e06d78e9e5112d419397 | pekinese | |||
3 | 00214f311d5d2247d5dfe4fe24b2303d | bluetick | |||
4 | 0021f9ceb3235effd7fcde7f7538ed62 | golden_retriever | |||
... | ... | ... | |||
10217 | ffd25009d635cfd16e793503ac5edef0 | borzoi | |||
10218 | ffd3f636f7f379c51ba3648a9ff8254f | dandie_dinmont | |||
10219 | ffe2ca6c940cddfee68fa3cc6c63213f | airedale | |||
10220 | ffe5f6d8e2bff356e9482a80a6e29aac | miniature_pinscher | |||
10221 | fff43b07992508bc822f33d8ffd902ae | chesapeake_bay_retriever |
10222 rows × 2 columns
Thống kê só lượng hình ảnh của mỗi giống chó.
dog_breeds_counts_df = labels_csv["breed"].value_counts() dog_breeds_counts_df = pd.DataFrame(dog_breeds_counts_df).rename(columns={"breed" : "count"}).sort_values(by="count",ascending=False) dog_breeds_counts_df
count | |||
---|---|---|---|
scottish_deerhound | 126 | ||
maltese_dog | 117 | ||
afghan_hound | 116 | ||
entlebucher | 115 | ||
bernese_mountain_dog | 114 | ||
... | ... | ||
brabancon_griffon | 67 | ||
komondor | 67 | ||
golden_retriever | 67 | ||
eskimo_dog | 66 | ||
briard | 66 |
120 rows × 1 columns
import matplotlib.pyplot as plt
fig,ax = plt.subplots(figsize=(15,45)) rects = ax.barh(dog_breeds_counts_df.index, dog_breeds_counts_df["count"]) ax.set_title("Bảng thống kê số lượng giống chó trong dữ liệu") ax.set_xlabel("Số lượng") ax.set_ylabel("Tên giống chó") ax.set_xlim([0, dog_breeds_counts_df["count"].max() + 20]) ax.invert_yaxis() for rect in rects : x,y = rect.get_width(), rect.get_y() plt.text(x*1.01,y, f"{x}", ha="left", va="top", fontsize=14)
Biểu đồ thống kê số file ảnh cho từng giống chó trong tập dữ liệu train
Sử dụng pandas
để tạo bảng đường dẫn đến file ảnh và tên giống chó trong ảnh.
dog_breed_df = pd.DataFrame({ "image_path" : [os.path.join(dog_breed_dir , "train", id + ".jpg") for id in labels_csv["id"]] , "label" : labels_csv["breed"] })
dog_breed_df
image_path | label | ||||
---|---|---|---|---|---|
0 | dog-breed-identification/train/000bec180eb18c7... | boston_bull | |||
1 | dog-breed-identification/train/001513dfcb2ffaf... | dingo | |||
2 | dog-breed-identification/train/001cdf01b096e06... | pekinese | |||
3 | dog-breed-identification/train/00214f311d5d224... | bluetick | |||
4 | dog-breed-identification/train/0021f9ceb3235ef... | golden_retriever | |||
... | ... | ... | |||
10217 | dog-breed-identification/train/ffd25009d635cfd... | borzoi | |||
10218 | dog-breed-identification/train/ffd3f636f7f379c... | dandie_dinmont | |||
10219 | dog-breed-identification/train/ffe2ca6c940cddf... | airedale | |||
10220 | dog-breed-identification/train/ffe5f6d8e2bff35... | miniature_pinscher | |||
10221 | dog-breed-identification/train/fff43b07992508b... | chesapeake_bay_retriever |
10222 rows × 2 columns
Kiểm tra xem có bao nhiêu giống chó trong tập dữ liệu
class_names = labels_csv["breed"].unique() class_names.sort()
class_names
array(['affenpinscher', 'afghan_hound', 'african_hunting_dog', 'airedale', 'american_staffordshire_terrier', 'appenzeller', 'australian_terrier',..., 'vizsla', 'walker_hound', 'weimaraner', 'welsh_springer_spaniel', 'west_highland_white_terrier', 'whippet', 'wire-haired_fox_terrier', 'yorkshire_terrier'], dtype=object)
len(class_names) => 120
Thực sự có 120 giống chó. Tiếp theo, chúng ta sẽ tạo plot_random_images
để hàm vẽ hình ảnh ngẫu nhiên từ những đường dẫn đến file ảnh ở trên. Hàm này nhận tham số n_samples
được truyền vào số lượng hình ảnh hiển thị.
import random import os import math def plot_random_images(n_samples=1) : if n_samples < 1 : n_samples = 1 random_image_indices = random.sample(range(len(dog_breed_df)), k=n_samples) n_cols = 3 n_rows = math.ceil(n_samples / n_cols) plt.figure(figsize=(n_cols*6, n_rows*4)) for i, image_index in enumerate(random_image_indices) : image_path, label = dog_breed_df.loc[image_index] image = plt.imread(image_path) plt.subplot(n_rows,n_cols,i+1) plt.imshow(image) plt.title(label) plt.axis(False)
plot_random_images(5)
Vì label hiện tại có dạng label-encoded
nghĩa là nó đại diện vị trí label trong 120 class. Chúng ta sẽ để nó các label này thành một mảng gồm 120 phần tử, nếu label có giá trị bao nhiêu thì vị trí tương ứng trong mảng sẽ là True
, còn lại là False
.
bool_labels = [label == class_names for label in dog_breed_df["label"]] bool_labels[:3]
[array([False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False]), array([False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False]), array([False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False])]
print(dog_breed_df["label"][0]) print(np.where(dog_breed_df["label"][0] == class_names)[0][0]) print(bool_labels[0].argmax()) print(bool_labels[0].astype(int))
boston_bull 19 19 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
bool_labels[:2]
[array([False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False]), array([False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False])]
Thay vì để các mảng labels trên dưới dạng True
và False
, chúng ta đưa chúng về dạng 1
và 0
vì mô hình trong machine learning chỉ hiểu dữ liệu đưới dạng số. Để làm được điều này, chúng ta sẽ ép kiểu cho mảng bool_labels
trên.
Trong machine learning, X
là dữ liệu được đưa vào mô hình để nhận dạng các đặc trưng của một đối tượng (class) nào đó. Còn y
là label hay cũng được gọi là (class) giúp mô hình nhận dạng dữ liệu X
.
X = dog_breed_df["image_path"].values y = np.int16(bool_labels) X[:3], y[:3]
(array(['dog-breed-identification/train/000bec180eb18c7604dcecc8fe0dba07.jpg', 'dog-breed-identification/train/001513dfcb2ffafc82cccf4d8bbaba97.jpg', 'dog-breed-identification/train/001cdf01b096e06d78e9e5112d419397.jpg'], dtype=object), array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int16))
Vì dữ liệu hiện tại gồm có cả train
và test
. Tuy nhiên, chỉ có train
mới có cả dữ liệu ảnh lẫn label để mô hình nhận dạng, còn test
chỉ có dữ liệu ảnh thuần. Thông thường, chúng ta cần có cả 2 loại train
và test
với đầy đủ cả dữ liệu ảnh lẫn label để sau mỗi epoch mô hình học xong, nó sẽ kiểm định lại quá trình học trong epoch đó. Nếu mô hình chỉ học xong mà không kiểm định thì không thể đo lường được tính đúng đắn của nó trên tập dữ liệu mà nó chưa từng được học. Vì thế, thay vì cần test
chúng ta còn có thêm một lựa chọn nữa là tập dữ liệu validation
để kiểm định mô hình sau khi học. Vậy làm sao để có validation
?
Chúng ta sẽ tách dữ tách dữ liệu X
và y
ở trên thành 2 phần, một phần dùng để train
thường chiếm 70%-80% tổng số mẫu, valid
từ 15%-20%. Và thay vì tách dữ liệu bằng kỹ thuật thủ công, chúng ta sẽ sử dụng train_test_split
trong sklearn
vì nó không chỉ giúp chúng ta tách dữ liệu theo đúng tỉ lệ mẫu mà nó còn xáo trộn dữ liệu ngẫu nhiên trước khi tách để dữ liệu không đúng như thứ tự ban đầu nhằm tránh các dữ liệu liên tiếp nhau lại có tương quan với nhau.
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.8, random_state=42) X_train.shape, X_valid.shape, y_train.shape, y_valid.shape
((8177,), (2045,), (8177, 120), (2045, 120))
Số dữ liệu để train
có 8177 hình ảnh, dữ liệu để kiểm dịnh (validation
) là 2045 hình ảnh. Tất cả đều có hình ảnh và label.
2. Tiền xử lý dữ liệu (preprocessing data)
Tiền xử lý dữ liệu là quá trình đồng bộ dữ liệu theo đúng một định dạng, kích thước và kiểu dữ liệu để giúp cho mô hình train có thể học nhất quán.
Quá trình tiền xử lý dữ liệu hình ảnh sử dụng tensorflow gồm các bước sau :
+ Đọc hình ảnh thông qua đường dẫn của nó với tf.io.read_file
+ Chuyển dữ liệu hình ảnh thành tensor thông qua tf.image.decode_jpeg
+ Đồng bộ tất cả hình ảnh về chung kích thước tf.image.resize
Tạo hàm load_and_preprocess_image
để load và đồng bộ ảnh.
import tensorflow as tf
IMAGE_SHAPE = (224,224) def load_and_preprocess_image(image_path) : image = tf.io.read_file(image_path) image = tf.image.decode_jpeg(image,channels=3) image = tf.image.resize(image,size=IMAGE_SHAPE) return image
Hàm load_and_preprocess_image
sẽ giúp cho quá trình xử lý ảnh được đồng bộ với nhau.
Nhưng dữ liệu của chúng ta khá lớn, nếu đưa một lúc cả ngàn hình ảnh vào mô hình train sẽ khiến bộ nhớ của GPU có thể không chứa hết được, và lỗi sẽ xảy ra. Do đó, chúng ta cần chuyển tất cả dữ liệu hình ảnh vừa được load và xử lý trong load_and_preprocess_image
cùng với label của nó thành từng cụm. API tf.data.Dataset
sẽ đưa dữ liệu thành từng cụm.
🔑Cụm dữ liệu (batch) : Là một tập hợp lượng nhỏ các dữ liệu (cũng như label của nó) lại với nhau để mô hình trong một thời điểm chỉ học một lượng dữ liệu không quá lớn. VD: cụm 32 dữ liệu nghĩa là nó chỉ chứa 32 hình ảnh và 32 label tương ứng để khi đưa vào mô hình, trong một thời điểm nó chỉ học qua 32 dữ liệu này. Sau khi học hết, nó sẽ qua cụm tiếp theo. Số cụm dữ liệu sẽ bằng tổng dữ liệu chia cho số dữ liệu trong một đơn vị cụm.
Chúng ta sẽ tạo cụm dữ liệu, trong đó chứa cả hình ảnh và label tương ứng. Trước tiên, tạo hàm get_image_label
để đưa hình ảnh và label lai với nhau trong tuple
def get_image_label(image_path, label) : image = load_and_preprocess_image(image_path) return image, label
Trong tập dữ liệu này có cả 3 kiểu train
, test
và validation
. Tất cả các dữ liệu trong mỗi kiểu này chúng ta sẽ đưa chúng về thành từng cụm, quy trình thực hiện khá tương đồng nhau, chỉ có một số khác biệt sau:
+ Với kiểu train
, dữ liệu đưa vào cần cả hình ảnh và label, sau đó dữ liệu phải được xáo trộn thứ tự so với ban đầu.
+ Với kiểu validation
, dữ liệu đưa vào cần cả hình ảnh và label,nhưng không cần xáo trộn thứ tự vì mô hình chỉ cần kiểm định chứ mô hình không học từ dữ liệu này.
+ Với kiểu test
, dữ liệu không có label nên chỉ cần đưa hình ảnh vào là đủ.
🔑Lưu ý: mô hình học kiểu dữ liệu gì, hình dạng, kích thước của nó như thế nào thì sau này, khi nó tiến hành kiểm định với
evaluate()
, hoặc dự đoán xác suất vớipredict()
thì kích thước dữ liệu được đưa vào cũng phải tương tự với dữ liệu được train. VD:32,224,224,3)
là kích thước dữ liệu train, thì khi kiểm định mô hình, bạn cũng cần một tensor với 4 chiểu tương tự như vây.
Cụ thể, hàm create_data_batches
sẽ sử dụng để tạo 3 kiểu dữ liệu trên với các tham số như sau :
X
(list str) : đường dẫn đến các file ảnhy
(list) mặc địnhNone
: label của hình ảnhbatch_size
(mặc định 32) :Kích thước dữ liệu trong một cụm.valid_data
(bool) mặc địnhFalse
: Liệu có phải kiểu dữ liệu để kiểm địnhvalidation
hay không.test_data
(bool) mặc địnhFalse
: Liệu có phải kiểu dữ liệu để thử nghiệmtest
hay không.
def create_data_batches(X,y=None, batch_size=32, valid_data=False, test_data=False) : if test_data : print(f"Đang xử lý và chuyển dữ liệu test thành từng cụm.") data = tf.data.Dataset.from_tensor_slices(tf.constant(X)).map(load_and_preprocess_image).batch(batch_size) print(f"Đã xử lý xong! 💡") return data elif valid_data : if y is None : print(f"Tập dữ liệu để kiểm định cần phải có label y.") return print(f"Đang xử lý và chuyển dữ liệu kiểm định thành từng cụm.") data = tf.data.Dataset.from_tensor_slices((tf.constant(X), tf.constant(y))) data = data.map(get_image_label).batch(batch_size) print(f"Đã xử lý xong! 💡") return data else : data = tf.data.Dataset.from_tensor_slices((tf.constant(X), tf.constant(y))) data = data.map(get_image_label).shuffle(len(X)).batch(batch_size) return data
Để xây dựng mô hình, chúng ta sẽ tạo train_data
để mô hình học dữ liệu và valid_data
để mô hình kiểm định sau mỗi lần train.
train_data = create_data_batches(X_train, y_train) valid_data = create_data_batches(X_valid, y_valid, valid_data=True)
Đang xử lý và chuyển dữ liệu kiểm định thành từng cụm. Đã xử lý xong! 💡
Cấu trúc của train_data
và valid_data
sẽ có dạng một tuple với chưa hình ảnh và label. None
là số cụm chưa được thiết lập, chỉ khi bắt đầu chạy mô hình, nó sẽ định hình số lượng cụm cho qúa trình train.
train_data, valid_data
(<BatchDataset shapes: ((None, 224, 224, 3), (None, 120)), types: (tf.float32, tf.int16)>, <BatchDataset shapes: ((None, 224, 224, 3), (None, 120)), types: (tf.float32, tf.int16)>)
Tạo hàm kiểm tra hình ảnh ngẫu nhiên plot_images_from_batch
với :
images
: Danh sách các hình ảnhlabels
: Danh sách tên labels tương ứng với những hình ảnh đó.
import random import math def plot_images_from_batch(images, labels) : n_cols = 5 n_rows = math.ceil(len(images) / n_cols) plt.figure(figsize=(n_cols*3, n_rows*3)) for i, image in enumerate(images) : plt.subplot(n_rows, n_cols, i+1) plt.imshow(image/ 255.) plt.title(class_names[np.argmax(labels[i])]) plt.axis(False)
Để tính toán hiệu quả, một cụm là một tập hợp các Tensors được gắn kết chặt chẽ. Vì vậy, để xem dữ liệu trong một cụm, chúng ta phải giải nén nó. Chúng ta có thể làm như vậy bằng cách gọi phương thức as_numpy_iterator()
trên cụm dữ liệu. Điều này sẽ biến một cụm dữ liệu thành một vòng lặp. Sử dụng keys word next()
để lấy danh sách các hình ảnh, labels trong một cụm tiếp theo dựa trên thứ tự vòng lặp. Trong trường hợp của chúng tôi, next()
sẽ trả về một cụm gồm 32 hình ảnh và cặp nhãn.
images, labels = next(train_data.as_numpy_iterator()) plot_images_from_batch(images, labels)
images, labels = next(train_data.as_numpy_iterator()) plot_images_from_batch(images, labels)
3. Khởi tạo và train mô hình
Khi dữ liệu đã được chuẩn bị, thời điểm thích hợp để khởi tạo và train mô hình đã tới.
Trong phần này, chúng ta sẽ sử dụng 2 mô hình đã được train trước đó để cho chúng học dữ liệu và tiến hành kiểm định thay vì phải tự xây dựng lại mô hình từ ban đầu.
🔑 Lưu ý : Trước khi bắt đầu giải quyết một vấn đề gì đó, bạn nên đặt câu hỏi rằng vấn đề đó đã có người giải quyết chưa? nếu đã có người giải quyết rồi, bạn nên tham khảo cách thức giải quyết của họ trước khi nghĩ đến việc sẽ tự giải quyết vấn đề đó. Trong machine learning cũng vậy, vấn đề mà bạn đã và đang muốn xây dựng mô hình có thể đã có người hoặc tổ chức nào đó đã làm trước đó rồi, bạn nên tham khảo mô hình của họ.
Chúng ta sẽ sử dụng một mô đã được xây dựng trước đó từ TensorFlow Hub.
TensorFlow Hub là một mã nguồn mở, nơi bạn có thể tìm thấy các mô hình học máy đã được train trước đó cho vấn đề bạn đang giải quyết.
Sử dụng mô hình học máy được train trước thường được gọi là Transfer learning.
🔑 Tại sao lại phải sử dụng mô hình đã được train trước đó?
- Việc xây dựng một mô hình học máy và train nó từ đầu có thể tốn kém và mất thời gian.
- Transfer learning giúp cải thiện một số điều này bằng cách lấy những gì mô hình khác đã học và sử dụng thông tin đó cho vấn đề của riêng bạn.
🔑 Làm cách nào để chúng ta có thể chọn một mô hình phù hợp?
Chúng ta cần xác định vấn đề đang cần giải quyết, sau đó tìm top những mô hình đang có được tỉ lệ cao từ paperwithcode
Bài viết này sẽ giới thiệu đến bạn 2 kiến trúc mô hình transfer learning:
+ 1. Xây dựng mô hình ResnetV50
+ 2. Xây dựng mô hình EfficientnetB0
Để tải mô hình transfer learning, bạn có thể tìm kiếm trên image classification. hoặc sử dụng API của tensorflow tf.keras.applications.[tên mô hình]
(bài viết này sẽ sử dụng API)
Trước khi xây dựng một mô hình, có một số điều chúng ta cần xác định:
- Hình dạng input (hình ảnh, ở dạng Tensors) cho mô hình của.
- Hình dạng output (label của hình ảnh, ở dạng Tensors) cho mô hình
INPUT_SHAPE = (224,224,3) OUTPUT_SHAPE = len(class_names)
Định nghĩa một mô hình Deep Learning trong Keras có thể đơn giản như nói, "đây là các layer của mô hình, hình input và hình dạng output, chúng ta hãy bắt đầu!"
Vì chúng ta sẽ xây dựng 2 mô hình hoặc bạn có thể xây dựng thêm, nên để có thể tái khởi tạo mô hình nhiều lần, chúng ta sẽ tạo hàm có thể sử dụng được nhiều lần có tên là create_model()
.
Hàm này gồm các tham số sau :
- input_shape : Hình dạng input của dữ liệu
- output_shape : Hình dạng output của mô hình
- base_model : mô hình đã được train trước đó
- norm (bool) mặc định là
False
: có chuẩn hóa dữ liệu hay không. Tùy vào từng mô hình, vớibase_model
là EfficientB0 thì trong nó của nó đã có sẵn môt layer để scale dữ liệu, còn ResnetV50 thì không nên cần phải scale dữ liệu cho mô hình này.
Vai trò của hàm này :
- Khởi tạo mô hình
- Định nghĩa inputs layer với hình dạng của input.
- Khởi tạo mô hình với mô hình cơ sở
base_model
làm một layer. - Định nghĩa outputs layer với số lượng output được mô hình trả về.
- Định nghĩa mô hình với inputs và outputs trên
- Biên dịch mô hình :
- Loss function : Mục đích của loss function là để tính toán số lượng mà một mô hình nên tìm cách giảm thiểu trong quá trình train.
- Optimizer : Trình tối ưu hóa là một trong hai tham số cần thiết để biên dịch mô hình Keras. Nó thay đổi hiệu quả của quá trình học của mô hình.
- Metrics : là một dụng cụ để đo lường hiệu suất của mô hình của bạn.
- Build mô hình :
- cho nó biết hình dạng input mà nó sẽ nhận được.
- Return mô hình
- Khởi tạo mô hình
def create_model(input_shape, output_shape, base_model, norm=False) : inputs = layers.Input(input_shape) if norm : x = layers.Rescaling(scale=1/255.)(inputs) else : x = inputs x = base_model(x, training=False) x = layers.GlobalAveragePooling2D()(x) x = layers.Dense(output_shape)(x) outputs = layers.Activation("softmax")(x) model = Model(inputs, outputs) model.compile( loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"] ) model.build(input_shape) return model
Giới thiệu về các hàm callback giám sát mô hình train
Trước khi tạo mô hình, chúng ta sẽ sử dụng một số callback function để thực hiện giám sát quá và dừng mô hình khi nó không còn cải thiện trong trình học của mô hình.
Tensorboard Callback
TensorBoard giúp cung cấp một cách trực quan để theo dõi tiến trình trong mô hình của bạn trong và sau khi train.
Nó có thể được sử dụng trực tiếp ngay trên notebook để theo dõi các phép đo lường về hiệu suất của một mô hình như loss
và accuracy
.
Để thiết lập Tensorboard callback, chúng ta cần :
+ Load Tensorboard notebook extension
+ Tạo tensorboard callback để save logs đến đường dẫn và đưa nó vào tham số callbacks trong mô hình khi gọi phương thức fit()
.
+ Quan sát nhật ký train của mô hình bằng cách sử dụng %tensorboard
(phần này sẽ làm sau cùng)
%load_ext tensorboard
import datetime def create_tensorboard_callback(dir_name, experiment_name) : log_dir = os.path.join(dir_name, experiment_name, datetime.datetime.now().strftime("%d%m%Y-%H%M%S")) tensorboard_cb = tf.keras.callbacks.TensorBoard(log_dir) print(f"Đã lưu tensorboard vào {log_dir}") return tensorboard_cb
ModelCheckpoint Callback
Callback function tiếp theo sẽ là ModelCheckpoint
, nó có tác dụng giám sát quá trình học của mô hình qua mỗi epoch (mặc định) hoặc có thể thay đổi theo nhu cầu của mỗi người, trong ModelCheckpoint
có các tham số sau :
+ path_dir
: Đường dẫn để lưu model_checkpoint lại
+ monitor
(mặc định val_loss
) : giám sát quá trình train của mô hình dựa trên phương pháp đo lường (có thể thay bằng val_accuracy
)
+ save_best_only
(mặc định là False
) : Chỉ lưu mô hình có val_loss
hoặc val_accuracy
tốt nhất.
+ save_weight_only
: Lưu các trọng số tốt nhất của mô hình đưa trên val_loss
hoặc val_accuracy
.
model_checkpoint_resnet = "model_checkpoints/resnet.ckpt" model_checkpoint_efficient = "model_checkpoints/efficient.ckpt" def create_model_checkpoint(checkpoint_path) : return tf.keras.callbacks.ModelCheckpoint(checkpoint_path, monitor="val_accuracy", save_best_only=True, save_weights_only=True, verbose=1 )
Early Stopping Callback
🤔 Mô hình nên được train bao lâu, bao nhiêu epochs?
Rất khó có thể biết được khi nào mô hình học dữ liệu không thể cải thiện thêm được nữa, nếu chúng ta để thời gian train quá ngắn sẽ làm cho mô hình mất đi cơ hội để cải thiện khả năng dự đoán chính xác của nó. Nhưng nếu để train quá lâu, mô hình đến một thời điểm nào đó không còn khả năng cải thiện nữa thì sẽ dẫn đến lãng phí thời gian cũng như dễ gây ra hiện tượng overfitting
cho mô hình.
Vì vậy, một giải pháp tốt là chúng ta sẽ để mô hình train theo thời gian tùy ý (số epoch tùy ý), có thể 100 epoch, thậm chí lớn hơn. Sau đó, khi mô hình không có khả năng cải thiện, mô hình sẽ tự động ngắt không train nữa. Và để giúp quy trình này có thể thực hiện như vậy, EarlyStopping Callback
chính là sự lựa chọn.
Trong EarlyStopping Callback
có tham số :
+ patience
: Số lần mô hình không cải thiện ở epoch này so với epoch trước. VD: patience = 5
nghĩa là nếu 5 lần epoch lần này không cải thiện hoặc thậm chí còn tệ hơn epoch trước thì mô hình sẽ lập tức ngắt không train nữa.
# Định dạng của đoạn này là mã
early_stopping_cb = tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=3, verbose=1)
Trước khi bắt đầu khởi tạo và train mô hình, bạn cần kiểm tra xem Google Colab đã kết nối với GPU chưa
!nvidia-smi -L -> GPU 0: Tesla K80 (UUID: GPU-b6ea57a2-0113-96a5-d1e3-748853ce9bec)
Chúng ta sẽ build 2 mô hình là ResNetV50
và EfficientNetB0
từ API trong Keras đế so sánh hiệu năng hoạt động của chúng. Và chúng ta sẽ sử dụng các trọng số (weights) đã train trước đó của mô hình để train trên dữ liệu của chúng ta. Do đó, để cố định chúng thì cần thiết lập thuộc tính trainable
của mô hình là False
.
⚠️ Vì bộ nhớ RAM trong Colab chỉ 12GB, rất có thể không đáp ứng được toàn bộ dữ liệu, mô hình train, và quá trình xử lý sau đó, nên bạn hãy lưu lại mô hình trong Driver để có thể load lại mô hình phòng trường hợp Colab sẽ ngắt bất cứ lúc nào.
from tensorflow.keras import Model, Sequential, layers
Xây dựng mô hình ResnetV50 trích xuất đặc trưng
Trước tiên, chúng ta sẽ khởi tạo và train với mô hình ResnetV50
resnet_base_model = tf.keras.applications.ResNet50V2(include_top=False) resnet_base_model.trainable=False
resnet_model = create_model(INPUT_SHAPE, OUTPUT_SHAPE, resnet_base_model, norm=True) resnet_model.summary()
Model: "model_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_6 (InputLayer) [(None, 224, 224, 3)] 0 _________________________________________________________________ rescaling_3 (Rescaling) (None, 224, 224, 3) 0 _________________________________________________________________ resnet50v2 (Functional) (None, None, None, 2048) 23564800 _________________________________________________________________ global_average_pooling2d_1 ( (None, 2048) 0 _________________________________________________________________ dense_1 (Dense) (None, 120) 245880 _________________________________________________________________ activation_1 (Activation) (None, 120) 0 ================================================================= Total params: 23,810,680 Trainable params: 245,880 Non-trainable params: 23,564,800 _________________________________________________________________
resnet_model_history = resnet_model.fit( train_data, steps_per_epoch=len(train_data), epochs=5, validation_data=valid_data, validation_steps=len(valid_data), callbacks=[ create_tensorboard_callback("dog_breed", "resnet_model_feature_extraction"), create_model_checkpoint(model_checkpoint_resnet) ] )
Đã lưu tensorboard vào dog_breed/resnet_model_feature_extraction/18092021-021445 Epoch 1/5 256/256 [==============================] - 121s 285ms/step - loss: 1.7753 - accuracy: 0.5561 - val_loss: 1.1180 - val_accuracy: 0.6758 Epoch 00001: val_accuracy improved from -inf to 0.67579, saving model to model_checkpoints/resnet.ckpt ... Epoch 5/5 256/256 [==============================] - 83s 275ms/step - loss: 0.1401 - accuracy: 0.9735 - val_loss: 1.1123 - val_accuracy: 0.7090 Epoch 00005: val_accuracy improved from 0.70807 to 0.70905, saving model to model_checkpoints/resnet.ckpt
from utility_functions import plot_loss_curves
plot_loss_curves(resnet_model_history)
Tiếp theo, khởi tạo và train mô hình EfficientNetB0
Xây dựng mô hình EfficientNetB0 trích xuất đặc trưng
efficientb0_base_model = tf.keras.applications.EfficientNetB0(include_top=False) efficientb0_base_model.trainable=False
efficientnet_model = create_model(INPUT_SHAPE, OUTPUT_SHAPE, efficientb0_base_model, norm=False) efficientnet_model.summary()
Model: "model_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_7 (InputLayer) [(None, 224, 224, 3)] 0 _________________________________________________________________ efficientnetb0 (Functional) (None, None, None, 1280) 4049571 _________________________________________________________________ global_average_pooling2d_2 ( (None, 1280) 0 _________________________________________________________________ dense_2 (Dense) (None, 120) 153720 _________________________________________________________________ activation_2 (Activation) (None, 120) 0 ================================================================= Total params: 4,203,291 Trainable params: 153,720 Non-trainable params: 4,049,571 _________________________________________________________________
efficientnet_model_history = efficientnet_model.fit( train_data, steps_per_epoch=len(train_data), epochs=5, validation_data=valid_data, validation_steps=len(valid_data), callbacks=[ create_tensorboard_callback("dog_breed", "resnet_model_feature_extraction"), create_model_checkpoint(model_checkpoint_efficient) ] )
Đã lưu tensorboard vào dog_breed/resnet_model_feature_extraction/18092021-022320 Epoch 1/5 256/256 [==============================] - 62s 168ms/step - loss: 1.7942 - accuracy: 0.6397 - val_loss: 0.8405 - val_accuracy: 0.7985 Epoch 00001: val_accuracy improved from -inf to 0.79853, saving model to model_checkpoints/efficient.ckpt ... Epoch 5/5 256/256 [==============================] - 52s 154ms/step - loss: 0.2393 - accuracy: 0.9514 - val_loss: 0.5762 - val_accuracy: 0.8259 Epoch 00005: val_accuracy did not improve from 0.82738
plot_loss_curves(efficientnet_model_history)
Có thể thấy hiệu năng học dữ liệu từ 2 mô hình khá tương đồng với nhau, training accuracy
đạt > 90%, training loss
đều < 2. Tuy nhiên, đến quá trình kiểm định mô hình, có thể thấy rõ sự chênh lệch giữa chúng.
val accuracy
của EfficientNetB0
đạt độ chính xác > 80%, trong khi val accuracy
của ResnetV50
chỉ đạt ~70%. Có thể khẳng định mô hình được train với EfficientNetB0
đạt hiệu năng tốt hơn rất nhiều so với ResnetV50
trong quá trình kiểm định dữ liệu chưa mà mô hình chưa từng biết đến.
Tinh chỉnh mô hình trích xuất đặc trưng.
Mô hình EfficientB0
là mô hình tốt hơn, vì thế chúng ta sẽ cố gắng cải thiện để xem mô hình bằng cách cho phép một số layer con trong layer efficientnetb0
của mô hình efficientnet_model
được phép train như sau :
Đầu tiên chúng ta sẽ kiểm tra các layer trong efficientnet_model
:
for layer in efficientnet_model.layers : print(layer.name, layer.trainable)
input_7 True efficientnetb0 False global_average_pooling2d_2 True dense_2 True activation_2 True
Có thể thấy tất cả các layer đều được phép train là các layer mà chúng ta định nghĩa, còn layer efficientnetb0
là False
vì trước đó chúng ta đã sử dụng set thuộc tính trainable=False
.
Tiếp theo, kiểm tra các layer con trong layer efficientnetb0
:
for layer in efficientnet_model.layers[1].layers: print(layer.name, layer.trainable)
input_5 False rescaling_2 False normalization_1 False stem_conv_pad False stem_conv False stem_bn False stem_activation False ... block7a_project_conv False block7a_project_bn False top_conv False top_bn False top_activation False
OK, tất cả các layer con đều được cố định, không được phép train. Bây giờ là bước tinh chỉnh mô hình, hay có thể hiểu là cho phép một số hoặc tất cả layer trong mô hình cơ sở efficientnetb0
được phép train. Vì dữ liệu train không quá lớn (>8000), chúng ta sẽ chỉ cho một số layer được train, cụ thể trong trường hợp này là 5 layer cuối cùng (trên cùng, gần với output layer nhất).
🔑 Lưu ý: Tinh chỉnh mô hình trích xuất đặc trưng nghĩa là cho các layer trong mô hình đã được train trước đó được phép train lại trên dữ liệu hiện tại. Bạn có thể cho phép train một vài layer hoặc tất cả layer tùy thuộc vào dữ liệu, kích thước của chúng. Nếu dữ liệu bạn có rất nhiều thì nên cho nhiều layer có thể train, nhưng nếu quá ít như trường hợp này thì chỉ nên cho một vài layer được train. Nếu dữ liệu ít nhưng bạn lại cho nhiều layer train sẽ làm giảm hiệu năng của mô hình vì nó sẽ phá bỏ các trọng số đã được train trước đó để train lại dự liệu của bạn, mà càng ít dữ liệu thì nó sẽ học không được nhiều.
for layer in efficientnet_model.layers[1].layers[-5:] : layer.trainable=True
for layer in efficientnet_model.layers[1].layers: print(layer.name, layer.trainable)
input_5 False rescaling_2 False normalization_1 False ... block7a_se_excite False block7a_project_conv True block7a_project_bn True top_conv True top_bn True top_activation True
5 layer cuối cùng của efficientnetb0
đã được phép train lai. Bây giờ sẽ biên dich lại mô hình.
🔑 Lưu ý: Trong mô hình, nếu có bất kỳ một sự điều chỉnh nào, dù là nhỏ nhất, chúng ta cần phải biên dịch (compile) trước khi tiến hành cho mô hình train dữ liệu (
fit
).
efficientnet_model.compile( loss="categorical_crossentropy" , optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001), metrics=["accuracy"] )
Vì lúc này chúng ta sẽ cho mô hình train trong một thời gian dại (epoch lớn), qua mỗi chu kỳ train (epoch) mô hình sẽ tiếp thu được ngày càng ít kiến thức hơn so với ban đầu. Nên về sau nếu mô hình cứ học với tốc độ như lúc đầu sẽ rất khó để kiếm những đặc trưng ít ỏi nằm sâu trong dữ liệu đó. Do đó, khi thấy mô hình cải thiện ít hoặc không cải thiện, chúng ta sẽ điều chỉnh tốc độ học của mô hình chậm lại, để nó học kỹ hơn dữ liệu đó.
Để làm được điều đó, chúng ta sẽ sử dụng một callback function tên là ReduceLROnPlateau
để làm giảm learning_rate
nếu mô hình cải thiện kém trong khả năng val_loss
hoặc val_accuracy
.
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor="val_loss", # phương pháp giam sát hiệu năng của so với epoch trước để quyết định có nên giảm learning rate hay không factor=0.2, # giảm bao nhiêu lần so với learning rate trước đó patience=10, # Sau bao nhiêu lần không cải thiện sẽ giảm leanring rate verbose=1, min_lr=1e-6) # learning rate tối thiểu không được phép giảm nữa
efficientnet_model_fine_tune_history = efficientnet_model.fit( train_data, steps_per_epoch=len(train_data), epochs=100, initial_epoch=efficientnet_model_history.epoch[-1], validation_data=valid_data, validation_steps=len(valid_data), callbacks=[ create_tensorboard_callback("dog_breed", "efficientnet_fine_tune"), tf.keras.callbacks.ModelCheckpoint(filepath="model_checkpoints/efficientb0_fine_tune", monitor="val_loss", save_best_model=True, verbose=1), reduce_lr, early_stopping_cb ] )
Đã lưu tensorboard vào dog_breed/efficientnet_fine_tune/18092021-022852 Epoch 5/100 256/256 [==============================] - 61s 165ms/step - loss: 0.1808 - accuracy: 0.9702 - val_loss: 0.5628 - val_accuracy: 0.8220 Epoch 00005: saving model to model_checkpoints/efficientb0_fine_tune INFO:tensorflow:Assets written to: model_checkpoints/efficientb0_fine_tune/assets ... Epoch 12/100 256/256 [==============================] - 55s 166ms/step - loss: 0.1390 - accuracy: 0.9820 - val_loss: 0.5578 - val_accuracy: 0.8264 Epoch 00012: saving model to model_checkpoints/efficientb0_fine_tune INFO:tensorflow:Assets written to: model_checkpoints/efficientb0_fine_tune/assets Epoch 00012: early stopping
from utility_functions import compare_history
compare_history(efficientnet_model_history, efficientnet_model_fine_tune_history)
Đánh giá mô hình tinh chỉnh
result_efficient_model_fine_tune = efficientnet_model.evaluate(valid_data) result_efficient_model_fine_tune
64/64 [==============================] - 8s 120ms/step - loss: 0.5578 - accuracy: 0.8264 [0.5578241348266602, 0.8264058828353882]
Lưu mô hình
save_dir = "/content/drive/MyDrive/transfer_learning/dog_breed_model"
efficientnet_model.save(save_dir)
Phần tiếp theo chúng ta sẽ tiến hành đánh giá mô hình.