Từng bước triển khai một mô hình Deep Learning lên "server"
Bài đăng này đã không được cập nhật trong 5 năm
Ở bài viết này mình sẽ hướng dẫn từng bước xây dựng và đưa một mô hình DL lên môi trường "production".
Bài viết chỉ hướng dẫn, tổng hợp lại những bài viết về DL có sẵn, sử dụng thư viện có sẵn(TensorFlow) nên bài viết không có chút gì về toán hay thuật toán.
Khi mình bắt đầu đọc về DL, ML... Mình đã đọc khá nhiều các bài viết về việc tạo ra các model DL, ML và đa số các bài viết thường dừng lại ở bước đánh, thử nghiệm model. Việc triển khai lên server thì ít được nhắc tới, "Làm thế nào để tạo được 1 http api để các service khác có thể sử dụng được model đã train đó?" - Đây là câu hỏi mình nhận được từ một vài người.
Việc đưa một model lên môi trường production hay đơn giản chỉ là tạo một http service là việc tương đối đơn giản so với các bước tạo ra 1 model, nhưng với những người không quá thành thạo việc sử dụng ngôn ngữ lập trình, hay các model được training sẵn đã đáp ứng được yêu cầu của họ, thì việc triển khai này sẽ là vấn đề lớn nhất
Training một mô hình DL
Để thực hiện hướng dẫn đơn giản nhất, mình sử dụng hướng dẫn và dữ liệu từ bài viết https://www.tensorflow.org/tutorials/image_retraining
Những yêu cầu bắt buộc trước khi thực hiện theo các hướng dẫn:
- Có thể lập trình bằng ngôn ngữ Python
- Cài đặt môi trường lập trình cho Python
- Cài đặt gói
tensorflow
(Khuyên dùngvirtualenv
https://www.tensorflow.org/install/pip)
Kiểm tra version tensorflow
đã được cài đặt bằng lệnh
python -c "import tensorflow as tf; print(tf.__version__)"
Như trong hình thì mình sử dụng môi trường env
ở thư mục simple-image-classifier-serving
và tensorflow
đang là version 1.11.0 (mới nhất )
Retrain
Như đã nói ở trên, hướng dẫn này mình sẽ sử dụng dữ liệu và mô hình được xây dựng sẵn, nên mình sẽ không phải xây dựng dữ liệu để train nữa, chỉ tải về và dùng thôi.
Việc tạo dữ liệu cho bài toán phân lớp ảnh thì mình thường dùng các công cụ để tài ảnh từ Google image search, trong đó có Chrome extension Fatkun Batch Download Image
và pip package google_images_download
-
Tải dữ liệu training
Tại thư mục project sử dụng command:
curl -O http://download.tensorflow.org/example_images/flower_photos.tgz && \ mkdir training_data && \ tar xzf flower_photos.tgz -C ./training_data;
Các công việc mà command thực hiện:
- Tải xuống file nén chứa dữ liệu có tên
flower_photos.tgz
(nặng 218MB) - Tạo ra một thư mục có tên
training_data
- Giải nén dữ liệu vào thư mục vừa tạo.
Kết quả chúng ta có:
- Tải xuống file nén chứa dữ liệu có tên
-
Training
Tạo một thư mục có tên
scripts
để chứaretrain
script, thư mụctf_files
để chứa các file sinh ra cần thiết.-
Tải file retrain.py
curl -Lo ./scripts/retrain.py https://github.com/tensorflow/hub/raw/master/examples/image_retraining/retrain.py
-
Retraining
Để thực hiện retrain, chúng ta có thể sử dụng command đơn giản
python scripts/retrain.py --image-dir ./training_data/flower_photos
Với lệnh này, nhiều cấu hình sẽ là giá trị mặc định (có thể đọc nội dung file retrain.py)
Chúng ta sẽ thay đổi một số giá trị mặc định:
python scripts/retrain.py \ --output_graph=tf_files/flowers_retrained_graph.pb \ --output_labels=tf_files/flowers_labels.txt \ --image_dir=training_data/flower_photos \ --how_many_training_steps=200
Các thông số đã khá rõ ràng về chức năng,
how_many_training_steps
giảm số lượng số bước train (mặc định là 4000), theo "lý thuyết" mô hìnhImageNet
sẽ tối ưu với số bước train là 4000, nhưng để tiết kiệm thời gian mình sẽ giảm xuống.Thông thường để train xong dữ liệu này, mình thường mất 30 phút cho chế độ mặc định(mình dùng laptop có chip xử lý yếu -
Intel(R) Core(TM) i5-7267U CPU @ 3.10GHz
(len))Nếu có gặp lỗi:
Traceback (most recent call last): File "scripts/retrain.py", line 133, in <module> import tensorflow_hub as hub ModuleNotFoundError: No module named 'tensorflow_hub'
Các bạn hãy cài thêm package
tensorflow_hub
:pip install tensorflow_hub
Các bạn dùng macos có thể sẽ gặp một lỗi liên quan tới chứng chỉ xác thực
ssl
khi script thực hiện lệnhINFO:tensorflow:Downloading TF-Hub Module 'https://tfhub.dev/google/imagenet/inception_v3/feature_vector/1'
. Các bạn có thể tìm hướng dẫn trên mạng Internet, cập nhật ssl cerfiticates cho python(lưu ý, việc cập nhật ssl cer phải thực hiện ở môi trường python global, không phải môi trường virtualenv).Quá trình retrain:
Trong quá trình training
Hoàn thành quá trình training. Chúng ta có file model và file lable.
Thời gian train: 14m57s
Năng lượng: Pin laptop tụt từ 69% về 63%
-
Sử dụng mô hình vừa train
Tải xuống file label_image.py
curl -Lo ./scripts/label_image.py https://github.com/tensorflow/tensorflow/raw/master/tensorflow/examples/label_image/label_image.py
Xác định nhãn của một bức ảnh
python scripts/label_image.py \
--graph=tf_files/flowers_retrained_graph.pb \
--labels=tf_files/flowers_labels.txt \
--input_layer=Placeholder \
--output_layer=final_result \
--image=training_data/flower_photos/daisy/21652746_cc379e0eea_m.jpg
Kết quả:
2018-10-14 13:25:24.667487: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
daisy 0.9510209
sunflowers 0.033528723
dandelion 0.008771326
tulips 0.005201864
roses 0.001477076
Chúng ta có thể thêm 2 dòng này vào đầu file label_image.py
để loại bỏ warning được in ra std:
# Just disables the warning, doesn't enable AVX/FMA
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
Mô hình đã "hoạt động", nhưng chúng ta vừa sử dụng một training data để thực hiện test, như thế là chưa "khách quan". Mình sẽ lên mạng tải một bức ảnh hoa hướng dương về để thử nghiệm.
Kết quả (khoc2)
python scripts/label_image.py \
--graph=tf_files/flowers_retrained_graph.pb \
--labels=tf_files/flowers_labels.txt \
--input_layer=Placeholder \
--output_layer=final_result \
--image=test_data/flower_photos/huong_duong_do.jpg
sunflowers 0.9275313
daisy 0.034505673
dandelion 0.019276036
tulips 0.010451643
roses 0.008235327
Serving model
Ở phần này, chúng ta sẽ xây dựng một http api sử dụng model ở phần trên để tạo ra một service phân loại ảnh.
Yêu cầu: Client request tới service gửi kèm 1 bức ảnh (upload, url), trả lại kết quả đánh nhãn cho ảnh đó.
HTTP API với Flask
Mới làm quen với ngôn ngữ python, nên mình chọn Flask vì package này đơn giản và có nhiều hướng dẫn.
Tạo file app.py
#!flask/bin/python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello World!"
@app.route('/classify', methods=['POST', 'GET'])
def classify():
try:
if request.method == 'POST':
return jsonify({
'method': "POST"
})
elif request.method == 'GET' and request.args.get('image_url', '') != '':
image_url = request.args.get('image_url')
return jsonify({
'method': 'GET',
'image_url': image_url
})
else:
return jsonify({
'message': 'Action is not defined!'
}), 404
except Exception as e:
return repr(e), 500
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=12480)
Chạy ứng dụng
python server/app.py
Dùng Postman để test thử các route.
Tạo file utils.py
trong thư mục common
chứa các hàm hỗ trợ lưu file từ việc upload hay từ url
import os
import urllib.request
UPLOAD_FOLDER = './temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def download_image_from_url(image_url):
filename = image_url.split('/')[-1]
urllib.request.urlretrieve(image_url, UPLOAD_FOLDER + '/' + filename)
return filename
def save_upload_file(file):
"""
Save file
:param file: request.files['image']
"""
filename = file.filename
if file and allowed_file(filename):
file.save(os.path.join(UPLOAD_FOLDER, filename))
return filename
else:
return ''
Cập nhật lại file app.py
cho các trường hợp upload file hay truyền lên url
#!flask/bin/python
from flask import Flask, request, jsonify
import common.utils as utils
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello World!"
@app.route('/classify', methods=['POST', 'GET'])
def classify():
try:
if request.method == 'POST':
# check if the post request has the image part
if 'image' not in request.files:
return jsonify({
'message': 'No file part'
}), 400
file = request.files['image']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
return jsonify({
'message': 'No selected file'
}), 400
filename = utils.save_upload_file(file)
return jsonify({
'method': "POST",
'filename': filename
})
elif request.method == 'GET' and request.args.get('image_url', '') != '':
image_url = request.args.get('image_url')
filename = utils.download_image_from_url(image_url)
return jsonify({
'method': 'GET',
'image_url': image_url,
'file_name': filename
})
else:
return jsonify({
'message': 'Action is not defined!'
}), 404
except Exception as e:
return repr(e), 500
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=12480)
Mọi thứ hoạt động tốt, file được lưu trong thư mục ./temp
Hàm hỗ trợ phân lớp ảnh.
Như các bạn đã thấy ở trên, để phân loại được 1 ảnh ta phải truyền khá nhiều tham số cho file label_image.py
, từ file này mình sẽ viết lại một file wrap lại function phân loại ảnh, đầu vào chỉ là tên ảnh, đầu ra là kết quả phân loại ảnh.
Việc viết lại cũng khá đơn giản, chúng ta chỉ cần fix các giá trị mặc định (label, model...) `common/tf_classify
import os
import tensorflow as tf
# Just disables the warning, doesn't enable AVX/FMA
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# Relative path with app.py file
UPLOAD_FOLDER = './temp'
LABELS_FILE = './tf_files/flowers_labels.txt'
MODEL_FILE = './tf_files/flowers_retrained_graph.pb'
def read_tensor_from_image_file(file_name,
input_height=299,
input_width=299,
input_mean=0,
input_std=255):
input_name = "file_reader"
# output_name = "normalized"
file_reader = tf.read_file(file_name, input_name)
if file_name.endswith(".png"):
image_reader = tf.image.decode_png(
file_reader, channels=3, name="png_reader")
elif file_name.endswith(".gif"):
image_reader = tf.squeeze(
tf.image.decode_gif(file_reader, name="gif_reader"))
elif file_name.endswith(".bmp"):
image_reader = tf.image.decode_bmp(file_reader, name="bmp_reader")
else:
image_reader = tf.image.decode_jpeg(
file_reader, channels=3, name="jpeg_reader")
float_caster = tf.cast(image_reader, tf.float32)
dims_expander = tf.expand_dims(float_caster, 0)
resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width])
normalized = tf.divide(tf.subtract(resized, [input_mean]), [input_std])
sess = tf.Session()
result = sess.run(normalized)
return result
def label_image(image_name):
image_path = UPLOAD_FOLDER + '/' + image_name
# Read in the image_path
image_data = read_tensor_from_image_file(image_path)
# Loads label file, strips off carriage return
label_lines = [line.rstrip() for line
in tf.gfile.GFile(LABELS_FILE)]
# Unpersists graph from file
output = {}
with tf.gfile.FastGFile(MODEL_FILE, 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
_ = tf.import_graph_def(graph_def, name='')
# Feed the image_data as input to the graph and get first prediction
with tf.Session() as sess:
softmax_tensor = sess.graph.get_tensor_by_name('final_result:0')
# print(softmax_tensor)
predictions = sess.run(softmax_tensor,
{'Placeholder:0': image_data})
# Sort to show labels of first prediction in order of confidence
top_k = predictions[0].argsort()[-len(predictions[0]):][::-1]
for node_id in top_k:
human_string = label_lines[node_id]
score = predictions[0][node_id]
output[human_string] = float(score)
print('%s (score = %.5f)' % (human_string, score))
return output
Sử dụng ở file app.py
output = image_classifier.label_image(filename)
return jsonify({
'filename': filename,
'labels': output
})
Kết quả thử phân loại ảnh từ url
curl -X GET http://127.0.0.1:12480/classify?image_url=https://kenh14cdn.com/2016/sunflower-002-1470471840262.jpg'
{
"filename": "sunflower-002-1470471840262.jpg",
"labels": {
"daisy": 0.09283825755119324,
"dandelion": 0.031017528846859932,
"roses": 0.010368549264967442,
"sunflowers": 0.8581708669662476,
"tulips": 0.007604836020618677
}
}
Sau khi thử vài ảnh, chúng ta có thể thấy thời gian request là khá lâu (trung bình 6s). Nguyên nhân vì trong hàm label_image
lặp lại các công việc đọc label file, load graph, khởi tạo session. Chúng ta sẽ tối ưu lại hàm này, bằng cách đưa các công việc trên ra ngoài, chỉ làm 1 lần:
def label_image(image_name, label_lines, sess):
image_path = UPLOAD_FOLDER + '/' + image_name
# Read in the image_path
image_data = read_tensor_from_image_file(image_path)
output = {}
# Feed the image_data as input to the graph and get first prediction
softmax_tensor = sess.graph.get_tensor_by_name('final_result:0')
# print(softmax_tensor)
predictions = sess.run(softmax_tensor,
{'Placeholder:0': image_data})
# Sort to show labels of first prediction in order of confidence
top_k = predictions[0].argsort()[-len(predictions[0]):][::-1]
for node_id in top_k:
human_string = label_lines[node_id]
score = predictions[0][node_id]
output[human_string] = float(score)
print('%s (score = %.5f)' % (human_string, score))
return output
Khởi tạo các biến cần thiết cho hàm label_image
ở file app.py
# Loads label file, strips off carriage return
label_lines = [line.rstrip() for line
in tf.gfile.GFile(LABELS_FILE)]
# Unpersists graph from file
with tf.gfile.FastGFile(MODEL_FILE, 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
_ = tf.import_graph_def(graph_def, name='')
sess = tf.Session()
Giờ chạy lại server, chúng ta thấy chỉ request đầu tiên mất nhiều thời gian, còn những request sau đó thì mất ít thời gian hơn(< 1s).
Kết luận
Việc triển khai model ML lên server là công việc không quá phức tạp, nhưng cũng có nhiều vấn đề mà chúng ta cần lưu ý.
Hy vọng với bài viết của mình, các bạn sẽ có thêm ý tưởng để xây dựng sản phẩm cho riêng mình.
Bài viết được đăng bởi cùng tác giả tại link
All rights reserved