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
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ệutest
: 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_labels
và y_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_score
và results_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ữaPrecision
vàRecall
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_names | probability | ||||
---|---|---|---|---|---|
33 | edamame | 0.991968 | |||
63 | macarons | 0.933333 | |||
88 | seaweed_salad | 0.925852 | |||
64 | miso_soup | 0.910931 | |||
91 | spaghetti_carbonara | 0.908382 | |||
... | ... | ... | |||
8 | bread_pudding | 0.564470 | |||
39 | foie_gras | 0.558824 | |||
93 | steak | 0.480769 | |||
16 | cheesecake | 0.024096 | |||
17 | cheese_plate | 0.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_path | predict_label | true_label | predict_prob | predict_class_name | true_class_name | result | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | /content/all_food_classes_10_percent/test/brea... | 52 | 0 | 0.343887 | gyoza | apple_pie | False | ||||||||
1 | /content/all_food_classes_10_percent/test/chee... | 0 | 0 | 0.999694 | apple_pie | apple_pie | True | ||||||||
2 | /content/all_food_classes_10_percent/test/beef... | 97 | 0 | 0.546775 | takoyaki | apple_pie | False | ||||||||
3 | /content/all_food_classes_10_percent/test/lobs... | 0 | 0 | 0.574915 | apple_pie | apple_pie | True | ||||||||
4 | /content/all_food_classes_10_percent/test/scal... | 0 | 0 | 0.676432 | apple_pie | apple_pie | True | ||||||||
... | ... | ... | ... | ... | ... | ... | ... | ||||||||
25245 | /content/all_food_classes_10_percent/test/lobs... | 100 | 100 | 1.000000 | waffles | waffles | True | ||||||||
25246 | /content/all_food_classes_10_percent/test/file... | 100 | 100 | 0.999858 | waffles | waffles | True | ||||||||
25247 | /content/all_food_classes_10_percent/test/eggs... | 100 | 100 | 0.788630 | waffles | waffles | True | ||||||||
25248 | /content/all_food_classes_10_percent/test/chic... | 100 | 100 | 0.995214 | waffles | waffles | True | ||||||||
25249 | /content/all_food_classes_10_percent/test/fren... | 100 | 100 | 0.990704 | waffles | waffles | True |
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_path | predict_label | true_label | predict_prob | predict_class_name | true_class_name | result | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
4106 | /content/all_food_classes_10_percent/test/sash... | 17 | 16 | 1.000000 | cheese_plate | cheesecake | False | ||||||||
21978 | /content/all_food_classes_10_percent/test/guac... | 30 | 87 | 1.000000 | deviled_eggs | scallops | False | ||||||||
4242 | /content/all_food_classes_10_percent/test/tira... | 17 | 16 | 1.000000 | cheese_plate | cheesecake | False | ||||||||
14035 | /content/all_food_classes_10_percent/test/tuna... | 66 | 56 | 1.000000 | nachos | huevos_rancheros | False | ||||||||
4158 | /content/all_food_classes_10_percent/test/croq... | 17 | 16 | 1.000000 | cheese_plate | cheesecake | False | ||||||||
... | ... | ... | ... | ... | ... | ... | ... | ||||||||
19368 | /content/all_food_classes_10_percent/test/stea... | 37 | 77 | 0.208971 | filet_mignon | pork_chop | False | ||||||||
19779 | /content/all_food_classes_10_percent/test/fala... | 54 | 79 | 0.207024 | hot_and_sour_soup | prime_rib | False | ||||||||
3011 | /content/all_food_classes_10_percent/test/file... | 13 | 12 | 0.205330 | caprese_salad | cannoli | False | ||||||||
25196 | /content/all_food_classes_10_percent/test/chic... | 87 | 100 | 0.186260 | scallops | waffles | False | ||||||||
5591 | /content/all_food_classes_10_percent/test/waff... | 90 | 22 | 0.178435 | spaghetti_bolognese | chocolate_mousse | False |
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_name | percentage | ||||
---|---|---|---|---|---|
cheese_plate | 248 | 0.992 | |||
cheesecake | 244 | 0.976 | |||
steak | 150 | 0.600 | |||
apple_pie | 123 | 0.492 | |||
chocolate_cake | 121 | 0.484 | |||
... | ... | ... | |||
spaghetti_carbonara | 17 | 0.068 | |||
oysters | 15 | 0.060 | |||
pho | 13 | 0.052 | |||
hot_and_sour_soup | 12 | 0.048 | |||
edamame | 3 | 0.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_name | percentage | ||||
---|---|---|---|---|---|
bread_pudding | 448 | 0.017743 | |||
tacos | 361 | 0.014297 | |||
strawberry_shortcake | 319 | 0.012634 | |||
takoyaki | 316 | 0.012515 | |||
fried_rice | 315 | 0.012475 | |||
... | ... | ... | |||
red_velvet_cake | 192 | 0.007604 | |||
apple_pie | 188 | 0.007446 | |||
pizza | 185 | 0.007327 | |||
steak | 166 | 0.006574 | |||
chocolate_cake | 162 | 0.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 )