Thông tin nhóm:
Mục đích:
Toàn bộ source code từ model - backend - frontend có thể truy cập nhanh thông qua repo thangbuiq/simple_mlops, nằm ở đường link sau:
1. Import library
2. Data Collection
2.1. About the dataset
2.2. Prepare dataset
3. Explore Data Analysis (EDA)
4. Model
4.1. Data Preprocessing
4.2 Model Architecture
4.3 Train Test Split
4.4 Model Training and Validation
4.5 Compare Model
4.5.1 VGG16
4.5.2. VGG19
4.5.3. ResNet50
4.5.4. ResNet152
4.5.5. So sánh loss và accuracy trên tập test ở từng model
4.5.6. Chọn lựa model có dung lượng/kích thước phù hợp để serving
5. Model Serving as API
6. Deployment On Amazon Elastic Compute Cloud (AWS EC2) with Github Actions
7. Tự động hóa quy trình Deploy (CI/CD với Github Actions)
8. Full Source Code: Website, Model, API
Import những thư viện cần thiết từ keras, xử lý ảnh, và validation model (confusion matrix)
import warnings
warnings.filterwarnings('ignore')
!pip install -q seaborn visualkeras
import visualkeras
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import plotly.graph_objects as go
from PIL import Image
import cv2
import os
import tensorflow as tf
from keras import layers
from keras.layers import Input, Lambda, Dense, Flatten, Dropout, BatchNormalization
from keras.models import Model, load_model
from keras.optimizers import Adam
from keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau
from keras.applications.vgg16 import VGG16
from keras.applications.vgg19 import VGG19
from keras.applications.resnet50 import ResNet50
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
[trash, glass, plastic, organic, paper, cardboard]
: https://www.kaggle.com/datasets/asdasdasasdas/garbage-classification[battery, biological, white-glass, brown-glass, cardboard, clothes, green-glass, metal, paper, plastic, shoes, trash]
: https://www.kaggle.com/datasets/mostafaabla/garbage-classification[trash, glass, plastic, organic, paper, cardboard]
. Nhưng class trash
gây nhiễu cho model (dễ thấy trong class này có luôn cả túi nhựa đã xuất hiện ở class plastic
và các đồ ăn đã xuất hiện ở class organic
.directory_path = "/kaggle/input/d/asdasdasasdas/garbage-classification/Garbage classification/Garbage classification/trash"
image_files = sorted([file for file in os.listdir(directory_path) if file.lower().endswith(('.jpg', '.jpeg', '.png'))])[:20]
fig, axes = plt.subplots(4, 5, figsize=(15, 12))
for i, image_file in enumerate(image_files):
img = Image.open(os.path.join(directory_path, image_file))
ax = axes[i // 5, i % 5]
ax.imshow(img)
ax.axis('off')
plt.show()
Do đó chúng ta phải loại bỏ class này để tránh việc gây nhiễu cho model, class trash
từ giờ sẽ được thay thế bằng class battery
được bổ sung từ Data 12 classes (link 2 bên trên)
Data 12 class sẽ không được sử dụng chính mà chỉ dùng để bổ sung cho data 6 class, do:
- Con số 12 class là khá nhiều cho số class cần thiết trong bài toán cần xử lý.
- Ngoài ra, việc Data 12 class có quá nhiều class gây nhiễu như
[green-glass, white-glass, brown-glass...]
là không cần thiết cho bài toán và gây áp lực về hiệu năng, độ chính xác cho model.- Data 12 class khá mất cân bằng về số lượng giữa các class (unbalanced data).
directory_path = "/kaggle/input/d/asdasdasasdas/garbage-classification/Garbage classification/Garbage classification/glass"
image_files = sorted([file for file in os.listdir(directory_path) if file.lower().endswith(('.jpg', '.jpeg', '.png'))])[:20]
fig, axes = plt.subplots(4, 5, figsize=(15, 12))
for i, image_file in enumerate(image_files):
img = Image.open(os.path.join(directory_path, image_file))
ax = axes[i // 5, i % 5]
ax.imshow(img)
ax.axis('off')
plt.show()
root='/kaggle/input/garbage-classification/garbage_classification/'
data={}
for i in os.listdir(root):
for j in os.walk(root+i):
for k in j[2]:
data[root+i+'/'+k]=i
data=pd.DataFrame(data.items(),columns=['path','class_'])
data['class_'].value_counts().plot(kind='bar')
plt.title('Data 12 Class (VERY UNBALANCED)')
plt.xlabel('Class')
plt.ylabel('Count')
plt.show()
white-glass
làm data chính cho class glass
trong bộ dữ liệu mới (tránh nhầm lẫn, gây nhiễu với các class khác như metal, battery,...).data[data['class_'] == 'white-glass']['class_'].value_counts()
class_ white-glass 775 Name: count, dtype: int64
Do class
white-glass
này có 775 ảnh nên nhóm sẽ dùng con số này để làm tiêu chuẩn khi mix các ảnh từ hai tập dữ liệu để data balance hơn.
Nhóm đã lọc lại một số class (label lại data) và upload dataset này lên kaggle.
Data mới gồm 6 class [metal, glass, organic, paper, battery, plastic], trong đó:
Code bổ sung và relabeling data:
import os
import shutil
original_path = '/kaggle/input/d/asdasdasasdas/garbage-classification/Garbage classification/Garbage classification/'
bonus_data_path = '/kaggle/input/garbage-classification/garbage_classification/'
destination_path = '/kaggle/working/modified_dataset/'
if os.path.exists(destination_path) and os.path.isdir(destination_path):
shutil.rmtree(destination_path)
os.makedirs(destination_path, exist_ok=True)
classes_to_remove = ['cardboard', 'trash', 'glass'] # in original data
new_classes = ['battery', 'biological', 'white-glass'] # in bonus data
target_image_count = 775
for class_name in os.listdir(original_path):
class_path = os.path.join(original_path, class_name)
if class_name in classes_to_remove:
continue
new_class_name = class_name
if class_name in new_classes:
new_class_name = new_classes[new_classes.index(class_name)]
new_class_path = os.path.join(destination_path, new_class_name)
os.makedirs(new_class_path, exist_ok=True)
files_to_copy = os.listdir(class_path)[:target_image_count]
for file_name in files_to_copy:
file_path = os.path.join(class_path, file_name)
shutil.copy(file_path, new_class_path)
# If the class has fewer than target_image_count images, fill up from bonus data
if len(os.listdir(new_class_path)) < target_image_count:
remaining_images = target_image_count - len(os.listdir(new_class_path))
print(f"{class_name} has {len(os.listdir(new_class_path))} images and missing {remaining_images} to fill {target_image_count}.")
if class_name == "glass":
bonus_class_path = os.path.join(bonus_data_path, "white-glass")
else:
bonus_class_path = os.path.join(bonus_data_path, class_name)
if bonus_class_path:
bonus_files = os.listdir(bonus_class_path)
copied_names = set(os.listdir(new_class_path))
for file_name in bonus_files:
if remaining_images == 0:
break
new_file_name = file_name
counter = 1
while new_file_name in copied_names:
base_name, extension = os.path.splitext(file_name)
new_file_name = f"{class_name}_{counter}{extension}"
counter += 1
file_path = os.path.join(bonus_class_path, file_name)
new_file_path = os.path.join(new_class_path, new_file_name)
shutil.copy(file_path, new_file_path)
copied_names.add(new_file_name)
remaining_images -= 1
# Process the bonus dataset to copy battery and organic data
for class_name in os.listdir(bonus_data_path):
class_path = os.path.join(bonus_data_path, class_name)
if class_name in new_classes:
new_class_name = class_name
if class_name in new_classes:
new_class_name = new_classes[new_classes.index(class_name)]
if new_class_name == "biological":
new_class_path = os.path.join(destination_path, "organic")
elif new_class_name == "white-glass":
new_class_path = os.path.join(destination_path, "glass")
else:
new_class_path = os.path.join(destination_path, new_class_name)
os.makedirs(new_class_path, exist_ok=True)
files_to_copy = os.listdir(class_path)[:target_image_count]
for file_name in files_to_copy:
file_path = os.path.join(class_path, file_name)
shutil.copy(file_path, new_class_path)
print("\nFINISH: Dataset modification complete.")
for class_ in os.listdir(destination_path):
count_class = len(os.listdir(os.path.join(destination_path, class_)))
print(f"{class_} has {count_class} images.")
metal has 410 images and missing 365 to fill 775. paper has 594 images and missing 181 to fill 775. plastic has 482 images and missing 293 to fill 775. FINISH: Dataset modification complete. battery has 775 images. glass has 775 images. paper has 775 images. metal has 775 images. plastic has 775 images. organic has 775 images.
data_path = '/kaggle/input/garbage-classification-6-classes-775class/'
data_classes = os.listdir(data_path)
data_classes
['metal', 'glass', 'organic', 'paper', 'battery', 'plastic']
metal
, glass
, organic
, paper
, battery
, và plastic
.for class_ in os.listdir(data_path):
count_class = 0
for photo in os.listdir(data_path + class_):
count_class += 1
print(str(class_) + " has " + str(count_class) + " images.")
metal has 775 images. glass has 775 images. organic has 775 images. paper has 775 images. battery has 775 images. plastic has 775 images.
path
: đường dẫn đến ảnh.type_trash
: loại rác của ảnh.data = pd.DataFrame()
for class_ in os.listdir(data_path):
temp = pd.DataFrame()
temp['path'] = np.nan
temp['type_trash'] = class_
i = 0
for photo in os.listdir(data_path + class_):
temp.loc[i, 'path'] = data_path + class_ + "/" + photo
temp.loc[i, 'type_trash'] = class_
i += 1
data = pd.concat([data, temp], ignore_index=True)
del temp
data.shape
(4650, 2)
Dataframe sẽ gồm có 4650 dòng (tức 4650 ảnh) và 2 cột
data = data.sample(frac=1)
data.reset_index(drop=True, inplace=True)
data.head(3)
path | type_trash | |
---|---|---|
0 | /kaggle/input/garbage-classification-6-classes... | plastic |
1 | /kaggle/input/garbage-classification-6-classes... | glass |
2 | /kaggle/input/garbage-classification-6-classes... | plastic |
def display_images_with_labels(df, idx):
row = df.iloc[idx]
image_path = row['path']
image_label = row['type_trash']
img = mpimg.imread(image_path)
plt.imshow(img)
plt.title(f'Label: {image_label}')
plt.axis('off')
plt.show()
display_images_with_labels(data, 1040)
image_preprocessing
:from keras.applications.vgg16 import preprocess_input
def image_preprocessing(img_path):
img = image.load_img(img_path, target_size=(224, 224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = preprocess_input(img)
return img
Các bước thực hiện quá trình trên gồm có:
img_path
với kích thước chuẩn là (224,224)
(VGG16 yêu cầu kích thước đầu vào của ảnh là 224x224).(224, 224, 3)
; trong đó 3 tức là 3 kênh màu RGB.(1, 224, 224, 3)
-> tensor
. preprocess_input
để chuyển đổi hình ảnh đầu vào từ RGB sang BGR, sau đó lấy trung bình mỗi kênh màu.def plot_images(original, preprocessed):
fig, axs = plt.subplots(1, 2, figsize=(10, 5))
axs[0].imshow(original)
axs[0].set_title('Original Image')
axs[0].axis('off')
axs[1].imshow(np.squeeze(preprocessed, axis=0))
axs[1].set_title('Preprocessed Image')
axs[1].axis('off')
plt.show()
img_path = '/kaggle/input/garbage-classification-6-classes-775class/plastic/plastic125.jpg'
original_image = Image.open(img_path)
preprocessed_image = image_preprocessing(img_path)
plot_images(original_image, preprocessed_image)
fine-tuning pre-trained model
.BATCH_SIZE = 64
n_classes = 6
# VGG16 base model
conv_base = VGG16(
include_top=False,
weights='imagenet',
input_shape=(224, 224, 3)
)
# Freeze all layers except the last two
for layer in conv_base.layers[:-2]:
layer.trainable = False
# Fine-tune model
top_model = conv_base.output
top_model = Flatten(name="flatten")(top_model)
top_model = Dense(1024, activation='relu')(top_model) # Increased units
top_model = BatchNormalization()(top_model)
top_model = Dropout(0.5)(top_model)
top_model = Dense(512, activation='relu')(top_model) # Increased units
top_model = BatchNormalization()(top_model)
top_model = Dropout(0.5)(top_model)
output_layer = Dense(n_classes, activation='softmax')(top_model)
# Final Model
model = Model(inputs=conv_base.input, outputs=output_layer)
visualkeras.layered_view(model, to_file='model_architecture.png', legend=True)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5 58889256/58889256 [==============================] - 0s 0us/step
model.summary()
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 224, 224, 3)] 0 block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 flatten (Flatten) (None, 25088) 0 dense (Dense) (None, 1024) 25691136 batch_normalization (Batch (None, 1024) 4096 Normalization) dropout (Dropout) (None, 1024) 0 dense_1 (Dense) (None, 512) 524800 batch_normalization_1 (Bat (None, 512) 2048 chNormalization) dropout_1 (Dropout) (None, 512) 0 dense_2 (Dense) (None, 6) 3078 ================================================================= Total params: 40939846 (156.17 MB) Trainable params: 28581894 (109.03 MB) Non-trainable params: 12357952 (47.14 MB) _________________________________________________________________
def plot_random_images(generator, num_images=20):
# Get a batch of data from the generator
images, labels = next(generator)
random_indices = np.random.choice(images.shape[0], num_images, replace=False)
fig, axs = plt.subplots(4, 5, figsize=(15, 12))
fig.suptitle('Random 20 Images from the Generator', fontsize=16)
for i, ax in enumerate(axs.flatten()):
index = random_indices[i]
image = images[index]
label = labels[index]
ax.imshow(image)
ax.set_title(f'Class: {np.argmax(label)}')
ax.axis('off')
plt.show()
gen_train = ImageDataGenerator(preprocessing_function=preprocess_input) # VGG16 preprocessing
full_data = gen_train.flow_from_directory(data_path, target_size = (224, 224), batch_size = BATCH_SIZE, class_mode="categorical")
plot_random_images(full_data)
Found 4650 images belonging to 6 classes.
filenames = full_data.filenames
labels = full_data.labels
class_mapping = {value: str(key) for key, value in full_data.class_indices.items()}
labels = [class_mapping[label] for label in labels]
# Split the data into training and testing sets
train_filenames, test_filenames, train_labels, test_labels = train_test_split(filenames, labels, test_size=0.1, random_state=42)
train_df = pd.DataFrame({'filename': train_filenames, 'class': train_labels})
test_df = pd.DataFrame({'filename': test_filenames, 'class': test_labels})
# Create separate generators for training and testing using flow_from_dataframe
train_data = gen_train.flow_from_dataframe(train_df, directory=data_path, target_size=(224, 224),
batch_size=BATCH_SIZE, class_mode="categorical",
shuffle=True, seed=42)
test_data = gen_train.flow_from_dataframe(test_df, directory=data_path, target_size=(224, 224),
batch_size=BATCH_SIZE, class_mode="categorical",
shuffle=False)
Found 4185 validated image filenames belonging to 6 classes. Found 465 validated image filenames belonging to 6 classes.
Trong quá trình train model, nhóm có sử dụng một số kỹ thuật như sau:
Nhóm sẽ sử dụng 4 kiến trúc khác nhau để train model. Do việc train model tốn thời gian nên dưới đây là link train từng model mà nhóm đã làm rồi up lại lên kaggle.
Để sử dụng từng model trên, nhóm chỉ cần save model thành các file .h5
rồi load lên sử dụng.
vgg16 = load_model("/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_16.h5")
output_class = ["battery", "glass", "metal","organic", "paper", "plastic"]
def preprocessing_input(img_path):
img = image.load_img(img_path, target_size=(224, 224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
img = preprocess_input(img) # VGG16 preprocess_input
return img
def plot_images(original, preprocessed):
fig, axs = plt.subplots(1, 2, figsize=(10, 5))
axs[0].imshow(original)
axs[0].set_title('Original Image')
axs[0].axis('off')
# Remove the batch dimension for display
preprocessed = np.squeeze(preprocessed, axis=0)
axs[1].imshow(preprocessed)
axs[1].set_title('Preprocessed Image')
axs[1].axis('off')
plt.show()
def predict_user(img_path):
img = preprocessing_input(img_path)
plot_images(Image.open(img_path), img)
predicted_array = model.predict(img)
predicted_value = output_class[np.argmax(predicted_array)]
predicted_accuracy = round(np.max(predicted_array) * 100, 2)
print("Your waste material is", predicted_value, "with", predicted_accuracy, "% accuracy.")
predictions = vgg16.predict(test_data)
# Get the predicted class labels
predicted_labels = np.argmax(predictions, axis=1)
# Get the true class labels
true_labels = test_data.classes
# Compute confusion matrix
conf_matrix = confusion_matrix(true_labels, predicted_labels)
# Plot confusion matrix using seaborn
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
xticklabels=test_data.class_indices.keys(),
yticklabels=test_data.class_indices.keys())
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
# Print classification report
class_names = list(test_data.class_indices.keys())
print(classification_report(true_labels, predicted_labels, target_names=class_names))
8/8 [==============================] - 20s 751ms/step
precision recall f1-score support battery 0.98 1.00 0.99 87 glass 0.95 0.86 0.91 73 metal 0.92 0.88 0.90 69 organic 0.95 0.98 0.97 64 paper 1.00 0.98 0.99 92 plastic 0.85 0.94 0.89 80 accuracy 0.94 465 macro avg 0.94 0.94 0.94 465 weighted avg 0.95 0.94 0.94 465
vgg19 = load_model("/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_19.h5")
predictions = vgg19.predict(test_data)
# Get the predicted class labels
predicted_labels = np.argmax(predictions, axis=1)
# Get the true class labels
true_labels = test_data.classes
# Compute confusion matrix
conf_matrix = confusion_matrix(true_labels, predicted_labels)
# Plot confusion matrix using seaborn
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
xticklabels=test_data.class_indices.keys(),
yticklabels=test_data.class_indices.keys())
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
# Print classification report
class_names = list(test_data.class_indices.keys())
print(classification_report(true_labels, predicted_labels, target_names=class_names))
8/8 [==============================] - 2s 286ms/step
precision recall f1-score support battery 0.95 0.99 0.97 87 glass 0.90 0.90 0.90 73 metal 0.90 0.88 0.89 69 organic 0.95 0.97 0.96 64 paper 0.94 0.98 0.96 92 plastic 0.94 0.85 0.89 80 accuracy 0.93 465 macro avg 0.93 0.93 0.93 465 weighted avg 0.93 0.93 0.93 465
resnet50 = load_model("/kaggle/input/garbage-classifier-model/garbage_classifier.resnet50.h5")
predictions = resnet50.predict(test_data)
# Get the predicted class labels
predicted_labels = np.argmax(predictions, axis=1)
# Get the true class labels
true_labels = test_data.classes
# Compute confusion matrix
conf_matrix = confusion_matrix(true_labels, predicted_labels)
# Plot confusion matrix using seaborn
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
xticklabels=test_data.class_indices.keys(),
yticklabels=test_data.class_indices.keys())
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
# Print classification report
class_names = list(test_data.class_indices.keys())
print(classification_report(true_labels, predicted_labels, target_names=class_names))
8/8 [==============================] - 5s 276ms/step
precision recall f1-score support battery 0.98 0.99 0.98 87 glass 0.87 0.93 0.90 73 metal 0.86 0.87 0.86 69 organic 1.00 0.97 0.98 64 paper 0.95 0.96 0.95 92 plastic 0.92 0.85 0.88 80 accuracy 0.93 465 macro avg 0.93 0.93 0.93 465 weighted avg 0.93 0.93 0.93 465
resnet152 = load_model("/kaggle/input/garbage-classifier-model/garbage_classifier.resnet152.h5")
predictions = resnet152.predict(test_data)
# Get the predicted class labels
predicted_labels = np.argmax(predictions, axis=1)
# Get the true class labels
true_labels = test_data.classes
# Compute confusion matrix
conf_matrix = confusion_matrix(true_labels, predicted_labels)
# Plot confusion matrix using seaborn
plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
xticklabels=test_data.class_indices.keys(),
yticklabels=test_data.class_indices.keys())
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
# Print classification report
class_names = list(test_data.class_indices.keys())
print(classification_report(true_labels, predicted_labels, target_names=class_names))
8/8 [==============================] - 6s 448ms/step
precision recall f1-score support battery 0.97 1.00 0.98 87 glass 0.88 0.88 0.88 73 metal 0.86 0.90 0.88 69 organic 0.98 0.98 0.98 64 paper 0.97 1.00 0.98 92 plastic 0.93 0.82 0.87 80 accuracy 0.93 465 macro avg 0.93 0.93 0.93 465 weighted avg 0.93 0.93 0.93 465
Cuối cùng, để có thể quyết định chọn model nào là ổn định và tốt nhất cho việc serving API, ta so sánh loss và accuracy của từng model.
models = [vgg16, vgg19, resnet50, resnet152]
model_names = ['VGG16', 'VGG19', 'ResNet50', 'ResNet152']
loss_values = []
accuracy_values = []
# Evaluate and store loss/accuracy for each model
for model, name in zip(models, model_names):
loss, accuracy = model.evaluate_generator(test_data)
loss_values.append(loss)
accuracy_values.append(accuracy)
loss_color = 'lightblue'
accuracy_color = 'mediumseagreen'
fig, ax1 = plt.subplots(figsize=(10, 6))
sns.barplot(x=model_names, y=loss_values, color=loss_color, ax=ax1, label='Loss')
ax2 = ax1.twinx()
sns.lineplot(x=model_names, y=accuracy_values, color=accuracy_color, marker='o', ax=ax2, label='Accuracy')
ax1.set_xlabel('Model')
ax1.set_ylabel('Loss', color='black')
ax2.set_ylabel('Accuracy', color='black')
plt.title('Loss and Accuracy for Models')
ax1.legend(loc='upper left')
ax2.legend(loc='upper right')
plt.show()
Như kết quả trển, thứ tự loss và accuracy tốt nhất lần lượt là:
Để xem và so sánh kích thước của các model, ta viết hàm lấy kích thước dựa theo link từng file:
def get_file_size(file_path):
if os.path.exists(file_path):
# Lấy kích thước của file và chuyển đổi thành đơn vị MB
size_in_bytes = os.path.getsize(file_path)
size_in_mb = size_in_bytes / (1024 * 1024) # 1 MB = 1024 KB = 1024 * 1024 bytes
return size_in_mb
size_vgg16 = get_file_size('/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_16.h5')
size_vgg19 = get_file_size('/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_19.h5')
size_resnet50 = get_file_size('/kaggle/input/garbage-classifier-model/garbage_classifier.resnet50.h5')
size_resnet152 = get_file_size('/kaggle/input/model-resnet/model_resnet152.h5')
# Đường dẫn đến các file .h5
file_paths = [
'/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_16.h5',
'/kaggle/input/garbage-classifier-model/garbage_classifier.vgg_19.h5',
'/kaggle/input/garbage-classifier-model/garbage_classifier.resnet50.h5',
'/kaggle/input/garbage-classifier-model/garbage_classifier.resnet152.h5'
]
# Kích thước của các file
file_sizes = [get_file_size(file_path) for file_path in file_paths]
# Tên của các mô hình
model_names = ['VGG16', 'VGG19', 'ResNet50', 'ResNet152']
for model, size in zip(model_names, file_sizes):
print(f"Size (MB) of model {model}: {size:.2f} MB")
Size (MB) of model VGG16: 374.35 MB Size (MB) of model VGG19: 100.56 MB Size (MB) of model ResNet50: 97.39 MB Size (MB) of model ResNet152: 231.06 MB
Mặc dù
VGG16
vàResNet152
cho kết quả ổn định và tốt nhất nhưng kích thước của model là rất lớn (374.35MB và 231.06MB). Khi serving model này thành API và đưa lên AWS đòi hỏi chi phí, tài nguyên cao và tốc độ xử lý chậm. Do đó, chúng ta có thể sử dụng model tốn ít bộ nhớ hơn nhưResNet50
. Model này có accuracy thua 2 model tốt còn lại 0.1-0.2% và loss thì kém hơn 0.01-0.02 nhưng kích thước ít hơn 2-3 lần như so sánh ở trên.
-> Như vậy, việc chọn ResNet50
sẽ là tối ưu nhất về hiệu năng model lẫn hiệu năng phần cứng.
/docs
để dễ dàng kiểm tra và sử dụng.Các hàm sử dụng cho các route được định nghĩa ở file utils.py sau: https://github.com/thangbuiq/simple_mlops/blob/main/backend/utils.py
app = FastAPI
gồm tên và mô tả tương ứngapp.add_middleware
: cho phép yêu cầu từ list nguồn origins ở trước.trash
: Sử dụng Pydantic để định nghĩa một json-schema có tên là "trash" với một trường là "type_trash" kiểu dữ liệu là string (bắt buộc người dùng API phải theo format này).Các endpoint của API:
upload
và download
dùng để debug/test.@app.get("/")
: @app.post("/predict")
:@app.post("/get-advice")
:Sử dụng uvicorn.run
để chạy ứng dụng FastAPI với cài đặt host là "0.0.0.0" và port là 8000.
Để làm một ví dụ minh họa cho API (call và sử dụng API mà chúng em đã implement), chúng em viết một app React đơn giản gồm các chức năng chính như: upload ảnh, mở camera chụp ảnh, drag and drop ảnh và paste ảnh,... -> Tất cả những thao tác này sẽ gửi ảnh vào trong một formData đến lần lượt là route /predict, và sau đó là /get-advice ở API host trên cùng một nơi với app frontend (port 8000).
Bây giờ, ta nhìn qua lại kiến trúc software được sử dụng để deploy:
Để một app chạy thành công trên server, trước tiên ta phải chạy nó thành công ở máy local. Để giảm tải việc setup quá nhiều thứ trên server, ta sẽ containerize toàn bộ app của chúng ta lại thành một docker image có tên proenv_app
(kích thước image là 1.49GB). Cuối cùng khi deploy, ta chỉ cần lên server và cài mỗi Docker.
Trước tiên, ta cần tạo một máy EC2 trên AWS. Tụi em sử dụng chính account cá nhân của mình và bắt đầu lựa chọn instance với chi phí phù hợp.
Ở đây, nhóm đã thử qua các instance type có free-tier lần lượt là "t2.nano", "t2.micro", "t3.micro" và nhận thấy được hiệu năng của hai cấu hình máy này không thể đáp ứng nổi việc host hay deploy một model Keras. Do đó nhóm đã chọn một lựa chọn có phần tốn kém hơn chút là "t3.small". (với instance image sử dụng là Amazon Linux)
Nếu ta chạy liên tục máy Linux t3.small trong 1 tháng thì chi phí rơi vào khoảng 0.0209 x 24 x 30 = 15.04 $ = 362.000 VNĐ
Do nhóm chỉ mở vào những lúc cần thiết khi debug nên chi phí chỉ khoảng vài chục nghìn đồng
Cuối cùng ta set inbound rules lần lượt cho security group attach vào EC2 đã tạo với các port như sau:
nginx
.Sau khi đã set inbound rules, ta thành công truy cập vào đường dẫn http://PublicIPv4DNS:8888. Đây sẽ là app frontend fetch response từ API http://PublicIPv4DNS:8000/ROUTE của chúng ta.
Chi phí về RAM, CPU khi chạy container của chúng ta trên server:
Để tránh việc deploy thủ công dài dòng lặp đi lặp lại mỗi lần có thay đổi mới trên app, nhóm đã implement một workflow/pipeline CI/CD viết bằng Github Actions, pipeline sẽ thực hiện theo thứ tự như sau:
Release Tag -> Build Docker Image With Tag -> SSH vào EC2 -> Pull Docker Image -> Docker Run với Các Args cần thiết
File workflow được định nghĩa ở thư mục sau: https://github.com/thangbuiq/simple_mlops/tree/main/.github/workflows
Toàn bộ source code về model, backend, frontend, lẫn deployment hay cách Dockerize đều được nhóm public source ở repository sau: https://github.com/thangbuiq/simple_mlops.git
Thành quả cuối cùng sau khi user upload hay chụp một ảnh: