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

MVT
Đang cập nhật

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 traintest. Trong folder traintest 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òn test chỉ là phần dự đoán mô hình sau khi quá trình trainvalidation 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)
2 thư mục và 2 tập tin trong thư mục dog-breed-identification
0 thư mục và 10222 tập tin trong thư mục dog-breed-identification/train
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
idbreed
0000bec180eb18c7604dcecc8fe0dba07boston_bull
1001513dfcb2ffafc82cccf4d8bbaba97dingo
2001cdf01b096e06d78e9e5112d419397pekinese
300214f311d5d2247d5dfe4fe24b2303dbluetick
40021f9ceb3235effd7fcde7f7538ed62golden_retriever
.........
10217ffd25009d635cfd16e793503ac5edef0borzoi
10218ffd3f636f7f379c51ba3648a9ff8254fdandie_dinmont
10219ffe2ca6c940cddfee68fa3cc6c63213fairedale
10220ffe5f6d8e2bff356e9482a80a6e29aacminiature_pinscher
10221fff43b07992508bc822f33d8ffd902aechesapeake_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_deerhound126
maltese_dog117
afghan_hound116
entlebucher115
bernese_mountain_dog114
......
brabancon_griffon67
komondor67
golden_retriever67
eskimo_dog66
briard66

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_pathlabel
0dog-breed-identification/train/000bec180eb18c7...boston_bull
1dog-breed-identification/train/001513dfcb2ffaf...dingo
2dog-breed-identification/train/001cdf01b096e06...pekinese
3dog-breed-identification/train/00214f311d5d224...bluetick
4dog-breed-identification/train/0021f9ceb3235ef...golden_retriever
.........
10217dog-breed-identification/train/ffd25009d635cfd...borzoi
10218dog-breed-identification/train/ffd3f636f7f379c...dandie_dinmont
10219dog-breed-identification/train/ffe2ca6c940cddf...airedale
10220dog-breed-identification/train/ffe5f6d8e2bff35...miniature_pinscher
10221dog-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 TrueFalse, chúng ta đưa chúng về dạng 10 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ả traintest. 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 traintest 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 Xy ở 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, testvalidation. 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ới predict() 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 ảnh
  • y (list) mặc định None: label của hình ảnh
  • batch_size (mặc định 32) :Kích thước dữ liệu trong một cụm.
  • valid_data (bool) mặc định False : Liệu có phải kiểu dữ liệu để kiểm định validation hay không.
  • test_data (bool) mặc định False : Liệu có phải kiểu dữ liệu để thử nghiệm test 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_datavalid_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 ảnh
  • labels : 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ới base_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

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ư lossaccuracy.

Để 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à ResNetV50EfficientNetB0 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 efficientnetb0False 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.


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