Deep Learning với Tensorflow Module 7 phần 2: Dự án nhận nhận diện món ăn qua hình ả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 nội dung chi tiết bạn có thể đọc tại notebook project 1 101 classes food

Ở phần trước, chúng ta đã train và lưu mô hình. Quá trình thực hiện khá tốt, nó đánh bại khả mô hình từng đạt top 1 chính xác vào nằm 2016.

Nội dung bài viết này sẽ nối tiếp với phần trước, nếu quá trình thực hiện bị căt ngang khiến notebook của bạn không thực hiện được, bạn có thể tham khảo lai phần trước. Nội dung bài viết:

Đánh giá mô hình, dự đoán và phân tích các yếu tố trong mô hình từ dữ liệu test.

Quá trình thực hiện các bước trên khá dài, rất có thể google colab sẽ ngắt kết nối khiến mọi thứ trở về ban đầu. Chúng ta không thể cứ train lại từ đầu vì thời gian thực hiện công đoạn trên mất rất nhiều thời gian. May mắn thay chúng ta đã lưu mô hình vào Google Drive như bước cuối cùng của phần trên. Lúc này, bạn có thể upload lên, hoặc tạo một zip file gửi lên cloud và tải xuống.

!wget https://www.dropbox.com/s/4ktbe743z0pj0d5/101_classes_food_fine_tune_model.zip
unzip_file("101_classes_food_fine_tune_model.zip")

Unzipped file

Sử dụng tf.keras.models.load_model() để load mô hình vừa upload lên.

loaded_model_fine_tune = tf.keras.models.load_model("101_classes_food_fine_tune_model")
loaded_model_fine_tune
<keras.engine.functional.Functional at 0x7f0dc5824450>

Kiểm định mô hình vừa được load với tập dữ liệu test để so sánh với mô hình trước đã đúng hay chưa.

🔑Lưu ý: Trong trường hợp notebook chưa hoạt động hoặc bị khởi động lại, dữ liệu test sẽ bị mất, bạn cần thực hiện lại các nội dung để tạo lại dữ liệu test: 1. Sử dụng Bộ dữ liệu TensorFlow để tải xuống 2. Khám phá dữ liệu 3. Tạo hàm tiền xử lý dữ liệu 4. Phân cụm & chuẩn bị tập dữ liệu để lập mô hình (làm cho tập dữ liệu của chúng ta được load nhanh).

Nhớ rằng đừng chạy mô hình vì chúng ta đã có load mô hình rồi, nhưng nếu bạn chưa có bạn có thể chạy toàn bộ notebook (chuẩn bị sẵn 🍵 để tận hưởng nhé 😆)

loaded_model_fine_tune.evaluate(test_data)
790/790 [==============================] - 123s 110ms/step - loss: 1.0836 - accuracy: 0.7741
[1.083551049232483, 0.7740989923477173]

Có vẻ đã giống với mô hình đã được tải xuống. Thay vì sử dụng test_data, chúng ta sẽ tải một phiên bản mà phần trước chúng ta đã làm việc với nó. Lý do là test_data trong tfds không theo thứ tự cố định (nó thay đổi ngẫu nhiên mỗi khi thực thi code trong cell) , trong khi test_data trước đây chúng ta giữ nguyên, không xáo trộn dữ liệu nhằm để dễ dàng quan sát và theo dõi.

!wget https://www.dropbox.com/s/8pakapzwodimtvw/all_food_classes_10_percent.zip
unzip_file("all_food_classes_10_percent.zip")

Unzipped file

🔑Lưu ý: Chúng ta chỉ quan tâm dữ liệu test còn train chúng ta không dùng đến

all_classes_test_dir = "/content/all_food_classes_10_percent/test"
import tensorflow as tf
from tensorflow.keras.preprocessing import image_dataset_from_directory

Để sử dụng API tf.data.Dataset, chúng ta cần load và chuẩn bị hình ảnh với phương thức tf.keras.preprocessing.image_dataset_from_directory()

test_data_all_classes = image_dataset_from_directory(
    all_classes_test_dir,
    image_size=(224,224),
    label_mode="int",
    shuffle=False
)

Found 25250 files belonging to 101 classes.

test_data_all_classes
<BatchDataset shapes: ((None, 224, 224, 3), (None,)), types: (tf.float32, tf.int32)>

Kiểu cấu trúc trong test_data_all_classes là một tuple ((None, 224, 224, 3), (None,)) với kiểu dữ liệu (tf.float32, tf.int32)

Với test_data là một tuple ((None, 224, 224, 3), (None,)) có kiểu dữ liệu (tf.float32, tf.int64)

Cả 2 cấu trúc này đều có cùng kiểu, nên chúng ta có thể đưa test_data_all_classes vào mô hình để đánh giá và dự đoán được.

class_names = test_data_all_classes.class_names
class_names[:20]
    ['apple_pie',
     'baby_back_ribs',
     'baklava',
     'beef_carpaccio',
     'beef_tartare',
     'beet_salad',
     'beignets',
     'bibimbap',
     'bread_pudding',
     'breakfast_burrito',
     'bruschetta',
     'caesar_salad',
     'cannoli',
     'caprese_salad',
     'carrot_cake',
     'ceviche',
     'cheese_plate',
     'cheesecake',
     'chicken_curry',
     'chicken_quesadilla']

Kiểm định khả năng dự đoán của mô hình với test_data_all_classes

results_loaded_model_fine_tune = loaded_model_fine_tune.evaluate(test_data_all_classes)
results_loaded_model_fine_tune
    790/790 [==============================] - 91s 113ms/step - loss: 1.3493 - accuracy: 0.7610
    [1.3492883443832397, 0.7609900832176208]

Mô hình được kiểm định khá tương đồng với mô hình trước. Bây giờ, chúng ta sẽ thực hiện dự đoán mô hình trên test_data_all_classes. Vì dữ liệu có 101 class, nên với mỗi dữ liệu được mô hình dự đoán sẽ tạo thành một mảng với 101 phần tử là xác suất của 101 class. Và có tất cả 25250 dữ liệu được đưa vào mô hình. Do đó, mô hình sẽ trả về 25250 kết quả tính toán dưới dạng mảng.

y_pred_probs = loaded_model_fine_tune.predict(test_data_all_classes,verbose=1)
y_pred_probs
790/790 [==============================] - 76s 94ms/step
array([[1.9735955e-05, 2.1675705e-05, 3.4982816e-04, ..., 4.7145440e-07,
    6.4711705e-05, 1.3072124e-04],
   ...
    8.8283684e-07, 9.9070412e-01]], dtype=float32)]])

Cuối cùng, hình dạng output của quá trình dự đoán này sẽ là :

y_pred_probs.shape

(25250, 101)

Để lấy được label dự đoán của hình ảnh dữ liệu đưa vào, chúng ta sẽ lấy vị trí của phần tử nào có giá trị lớn nhất trong mảng đó, hay nói cách khác là tìm xác suất nào lớn nhất thì nó sẽ đại diện cho mảng đó làm label.

y_pred_labels = tf.argmax(y_pred_probs,axis=1).numpy()
y_pred_labels
array([ 52,   0,  97, ..., 100, 100, 100])

Label dự đoán đã lấy được. Tuy nhiên, vẫn chưa có label thực sự để so sánh. test_data_all_classes vừa rồi chúng ta đã sử dụng API image_dataset_from_directory để load và phân cụm cho dữ liệu. Bây giờ chúng ta sẽ lấy label của nó bằng cách tạo vòng lặp, đồng thời sử dụng phương thức unbatch() để đưa dữ liệu về theo thứ tự ban đầu, không còn gom chúng thành từng cụm nữa.

y_true_labels = [] 
for image, label in test_data_all_classes.unbatch() : 
  y_true_labels.append(label.numpy())
y_true_labels[:5]
 [0, 0, 0, 0, 0]

OK, label thực sự của dữ liệu test_data_all_classes đã có. Chúng ta sẽ thử đánh giá độ chính xác giữa y_true_labelsy_pred_labels bằng phương thức có sẵn trong sklearn.metrics.accuracy_score để xem nó có khớp với quá trình kiểm định mô hình như đã làm hay không.

from sklearn.metrics import accuracy_score
accuracy_score(y_true_labels, y_pred_labels)

0.760990099009901

accuracy_scoreresults_loaded_model_fine_tune.accuracy đã giống với nhau. Có vẻ tiến trình thực hiện đang đi đúng hướng 😁.

Trong sklearn.metrics ngoài accuracy_score còn một vài phương thức đánh giá độ chính xác cũng như hiệu suất của mô hình. Chẳng hạn như confusion_matrix, nhưng nếu sử dụng nó đơn thuần sẽ rất khó hiểu vì chỉ là ma trận mà không có bất kỳ ý nghĩa nào. Cho nên, chúng ta sẽ sử dụng plot_confusion_matrix trong utility_functions để vẽ matrix được rõ ràng.

Trước tiên, chúng ta cần import lại utility_functions

!wget https://www.dropbox.com/s/v4sla7jvi9cltg8/utility_functions.py
from utility_functions import plot_confusion_matrix
plot_confusion_matrix(y_true_labels, y_pred_labels, class_names)

Với 101 class, confusion_matrix phải scale dữ liệu và số class cho đủ với khung hình, bạn có thể click vào hình để phóng to và quan sát dễ dàng hơn.

Thêm vào các phương thức đánh giá, chúng ta sẽ sử dụng thêm classification_report và output bằng kiểu cấu trúc dict

Bạn còn nhớ ý nghĩa của các phương pháp đánh giá trong nó thể hiện điều gì không?

  • Precision : Tỷ lệ dương đúng (TP đoán 1 và đúng) trên tổng số mẫu. Nếu dương đúng càng lớn thì dương sai càng nhỏ (FP đoán 1 và sai)
  • Recall : Tỷ lệ dương đúng (TP) trên tổng số dương đúng (TP) và âm sai (FN) (mô hình dự đoán 0 nhưng sai), Recall cao hơn dẫn đến ít âm giả.
  • F1-score : Kết hợp giữa PrecisionRecall thành một không gian đo. 1 là tốt nhất, 0 là tệ nhất

Do F1-score là sự kết hợp của 2 phương thức trên nên chúng ta sẽ lấy F1-score làm thước đo để đánh giá mô hình.

from sklearn.metrics import classification_report
metrics_report_dict = classification_report(y_true_labels, y_pred_labels, output_dict=True)
metrics_report_dict

{'0': {'f1-score': 0.5799086757990868, 'precision': 0.675531914893617, 'recall': 0.508, 'support': 250}, '1': {'f1-score': 0.75390625, 'precision': 0.7366412213740458, 'recall': 0.772, 'support': 250}, ... '99': {'f1-score': 0.6148148148148148, 'precision': 0.5724137931034483, 'recall': 0.664, 'support': 250}, 'accuracy': 0.760990099009901, 'macro avg': {'f1-score': 0.7603100053867384, 'precision': 0.7682695728900006, 'recall': 0.7609900990099011, 'support': 25250}, 'weighted avg': {'f1-score': 0.7603100053867381, 'precision': 0.7682695728900009, 'recall': 0.760990099009901, 'support': 25250}} ```

f1_scores_dict = {}

for key, value in metrics_report_dict.items() : 
  if key == "accuracy": 
    break
  f1_scores_dict[class_names[int(key)]] = value["f1-score"]

f1_scores_dict
    {'apple_pie': 0.5799086757990868,
     'baby_back_ribs': 0.75390625,
     'baklava': 0.8385826771653544,
     ...
     'tiramisu': 0.7388535031847133,
     'tuna_tartare': 0.6148148148148148,
     'waffles': 0.8418891170431211}

Đây là kết quả F1-score được trích ra tử phương pháp classification_report dưới dạng dict. Để đánh giá trực quan với sô liệu dày đặc như vậy, chúng ta sẽ vẽ biểu đồ. Nhưng trước tiên, cần phải sắp xếp các giá trị này theo thứ tự. Để sắp xếp các giá trị này, chúng ta sẽ sử dụng DataFrame trong thư viện pandas.

import matplotlib.pyplot as plt 
import pandas as pd
f1_scores_df = pd.DataFrame({"class_names" : f1_scores_dict.keys(), "probability" : f1_scores_dict.values()}).sort_values(by="probability", ascending=False)
f1_scores_df
class_namesprobability
33edamame0.991968
63macarons0.933333
88seaweed_salad0.925852
64miso_soup0.910931
91spaghetti_carbonara0.908382
.........
8bread_pudding0.564470
39foie_gras0.558824
93steak0.480769
16cheesecake0.024096
17cheese_plate0.009029

101 rows × 2 columns

fig, ax = plt.subplots(figsize=(15,30))
rects = ax.barh(f1_scores_df["class_names"], f1_scores_df["probability"], color="orange")
ax.set_title("Xác suất dự đoán đúng label của tập dữ liệu",fontsize=24)
ax.set_xlabel("Tỉ lệ dự đoán")
ax.set_ylabel("Tên label") 
ax.invert_yaxis()
ax.set_xlim([0,1.1])

for rect in rects : 
  x,y = rect.get_width(), rect.get_y()
  plt.text(x*1.01,y, f"{x:.2f}", ha="left", va="top", fontsize=12)

Từ biểu đồ trên, có thể dễ dàng thấy được mô hình có khả năng dự đoán class nào là tốt nhất, class nào chưa được tốt...

Ngoài ra, chúng ta sẽ sử dụng DataFrame để lập bảng phân tích các yếu tố trong mô hình dự đoán như sau :

Đầu tiên, thu thập tất cả các đường dẫn đến file ảnh trong dữ liệu test :

import os

image_paths = [] 

for image_path in test_data_all_classes.list_files(os.path.join(all_classes_test_dir, "*", "*")) : 
  image_paths.append(image_path.numpy().decode("utf-8"))

image_paths[:5]
    ['/content/all_food_classes_10_percent/test/breakfast_burrito/3702900.jpg',
     '/content/all_food_classes_10_percent/test/cheesecake/34970.jpg',
     '/content/all_food_classes_10_percent/test/beef_carpaccio/2152088.jpg',
     '/content/all_food_classes_10_percent/test/lobster_roll_sandwich/2676709.jpg',
     '/content/all_food_classes_10_percent/test/scallops/3639679.jpg']

Tạo bảng tổng hợp về kết quả dự đoán cho từng file ảnh cụ thể :

results_df = pd.DataFrame({
    "image_path" : image_paths, 
    "predict_label" : y_pred_labels, 
    "true_label" : y_true_labels, 
    "predict_prob" : tf.reduce_max(y_pred_probs,axis=1), 
    "predict_class_name" : [class_names[i] for i in y_pred_labels], 
    "true_class_name" : [class_names[i] for i in y_true_labels], 
    "result" : y_pred_labels == y_true_labels
})

results_df
image_pathpredict_labeltrue_labelpredict_probpredict_class_nametrue_class_nameresult
0/content/all_food_classes_10_percent/test/brea...5200.343887gyozaapple_pieFalse
1/content/all_food_classes_10_percent/test/chee...000.999694apple_pieapple_pieTrue
2/content/all_food_classes_10_percent/test/beef...9700.546775takoyakiapple_pieFalse
3/content/all_food_classes_10_percent/test/lobs...000.574915apple_pieapple_pieTrue
4/content/all_food_classes_10_percent/test/scal...000.676432apple_pieapple_pieTrue
........................
25245/content/all_food_classes_10_percent/test/lobs...1001001.000000waffleswafflesTrue
25246/content/all_food_classes_10_percent/test/file...1001000.999858waffleswafflesTrue
25247/content/all_food_classes_10_percent/test/eggs...1001000.788630waffleswafflesTrue
25248/content/all_food_classes_10_percent/test/chic...1001000.995214waffleswafflesTrue
25249/content/all_food_classes_10_percent/test/fren...1001000.990704waffleswafflesTrue

25250 rows × 7 columns

Tìm top những hình ảnh có xác suất cao nhưng dự đoán sai :

top_worst_image_prediction = results_df[results_df["result"] == False].sort_values(by="predict_prob", ascending=False)
top_worst_image_prediction
image_pathpredict_labeltrue_labelpredict_probpredict_class_nametrue_class_nameresult
4106/content/all_food_classes_10_percent/test/sash...17161.000000cheese_platecheesecakeFalse
21978/content/all_food_classes_10_percent/test/guac...30871.000000deviled_eggsscallopsFalse
4242/content/all_food_classes_10_percent/test/tira...17161.000000cheese_platecheesecakeFalse
14035/content/all_food_classes_10_percent/test/tuna...66561.000000nachoshuevos_rancherosFalse
4158/content/all_food_classes_10_percent/test/croq...17161.000000cheese_platecheesecakeFalse
........................
19368/content/all_food_classes_10_percent/test/stea...37770.208971filet_mignonpork_chopFalse
19779/content/all_food_classes_10_percent/test/fala...54790.207024hot_and_sour_soupprime_ribFalse
3011/content/all_food_classes_10_percent/test/file...13120.205330caprese_saladcannoliFalse
25196/content/all_food_classes_10_percent/test/chic...871000.186260scallopswafflesFalse
5591/content/all_food_classes_10_percent/test/waff...90220.178435spaghetti_bolognesechocolate_mousseFalse

6035 rows × 7 columns

Top những class được dự đoán sai nhiều nhất và tỉ lệ dự đoán sai :

top_worst_class_prediction = results_df.loc[results_df["result"] == False, "true_class_name"].value_counts()
top_worst_class_prediction = pd.DataFrame(top_worst_class_prediction)
top_worst_class_prediction["percentage"] = top_worst_class_prediction["true_class_name"] / 250.
top_worst_class_prediction
true_class_namepercentage
cheese_plate2480.992
cheesecake2440.976
steak1500.600
apple_pie1230.492
chocolate_cake1210.484
.........
spaghetti_carbonara170.068
oysters150.060
pho130.052
hot_and_sour_soup120.048
edamame30.012

101 rows × 2 columns

Top những class được dự đoán nhiều nhất, và tỉ lệ của nó trên tổng số file

top_most_classes_prediction = results_df["predict_class_name"].value_counts() 
top_most_classes_prediction = pd.DataFrame(top_most_classes_prediction)
top_most_classes_prediction["percentage"] = top_most_classes_prediction / len(results_df)
top_most_classes_prediction
predict_class_namepercentage
bread_pudding4480.017743
tacos3610.014297
strawberry_shortcake3190.012634
takoyaki3160.012515
fried_rice3150.012475
.........
red_velvet_cake1920.007604
apple_pie1880.007446
pizza1850.007327
steak1660.006574
chocolate_cake1620.006416

101 rows × 2 columns

fig,ax = plt.subplots(figsize=(20,45))
top_most_classes_prediction.plot.barh(ax=ax)
ax.invert_yaxis()

Dự đoán hình ảnh ngẫu nhiên trên tập dữ liệu test

Để dự đoán một hình ảnh nào đó, chúng ta cần load và đồng bộ hình ảnh theo một kích thước đã được định sẵn theo mô hình train. VD : mô hình được fit với dữ liệu input là train_data có hình dạng là một tuple ((None, 224, 224, 3), (None,)) có kiểu dữ liệu (tf.float32, tf.int64) với mỗi phần tử đại diện cho hình dạng của image và label.

Vậy thì khi đưa vào model.predict(data) thì data trong đó cũng phải là một tuple ((None, 224, 224, 3), (None,)). Với tf.data.Dataset là một tensor có chiều đầu tiên là batch_size, trong khi image và label chưa được định nghĩa cho chiều đầu tiên đó. Vì vậy chúng ta sẽ sử dụng tf.expand_dims() để thêm cho cả image và label chiều đầu tiên là 1. Chúng ta sẽ sử dụng phương thức tf.data.Dataset.from_tensor() để định nghĩa kiểu dữ liệu cho image và label. Sau đó, sử dụng tf.data.Dataset.zip() để chập image và label thành một tuple như mong muốn. Cụ thể như sau :

  • Load và chuẩn bị hình ảnh
  • Tạo hàm lấy hình ảnh ngẫu nhiên và dự đoán hình ảnh đó.
import random

def load_and_preprocess_image(image_path, shape=(224,224),norm=False ) : 
  image = tf.io.read_file(image_path)
  image = tf.image.decode_image(image,channels=3)
  image = tf.image.resize(image, size=(224,224))
  if norm : 
    return image / 255. 
  return image
import random 
import matplotlib.pyplot as plt 

def plot_random_image_and_predict(model, n_samples=1) : 
  if n_samples > 7 : 
    print(f"Số mẫu không vượt quá 7")
    return 
  if n_samples < 1 : 
    n_samples = 7

  random_class_names = random.sample(os.listdir(all_classes_test_dir),k=n_samples)
  random_class_dirs = [os.path.join(all_classes_test_dir, class_name) for class_name in random_class_names ]
  for class_dir in random_class_dirs : 
    random_image_name = random.choice(os.listdir(class_dir)) 
    random_image_path = os.path.join(class_dir, random_image_name)
    image_tensor = load_and_preprocess_image(random_image_path)    
    for i, class_name in enumerate(class_names) : 
      if class_name == class_dir.split("/")[-1] : 
        image_label_index = i 
        image_label_name = class_name
    # Tạo input_image_tensor với 4 chiều (batch_size,height, width, color channels )
    input_image_tensor = tf.data.Dataset.from_tensors(tf.expand_dims(image_tensor,axis=0))
    # Tạo label tensor với 2 chiều (batch_size, label-encoded)
    input_label_index = tf.data.Dataset.from_tensors(tf.expand_dims(image_label_index,axis=0))
    # Chập input_image_tensor và input_label_index
    input_tensors = tf.data.Dataset.zip((input_image_tensor,input_label_index))

    pred_probs = model.predict(input_tensors)
    pred_label_index = tf.argmax(tf.squeeze(pred_probs)).numpy()    

    color = "red"
    if pred_label_index == image_label_index :
      color = "green"

    pred_df = pd.DataFrame({"class_name" : class_names, "probability" : pred_probs[0]}).sort_values(by="probability",ascending=False)[:5]
    pred_df.set_index("class_name",inplace=True)  
    fig, (ax1,ax2) = plt.subplots(1,2,figsize=(20,6))
    ax1.imshow(image_tensor/255.)
    ax1.axis(False)
    ax1.set_title(f"Actual: {image_label_name}\nPredict: {class_names[pred_label_index]} ", color=color)

    pred_df.plot.bar(ax=ax2,legend=False)    
    ax2.get_children()[0].set_color(color)
plot_random_image_and_predict(loaded_model_fine_tune,n_samples=7 )


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