Деплоим ML проект, используя Flask как REST API, и делаем доступным через приложение на Flutter

—

от автора

Введение

Машинное обучение уже везде и, пожалуй, почти невозможно найти софт, не использующий его прямо или косвенно. Давайте создадим небольшое приложение, способное загружать изображения на сервер для последующего распознавания с помощью ML. А после сделаем их доступными через мобильное приложение с текстовым поиском по содержимому.

Мы будем использовать Flask для нашего REST API, Flutter для мобильного приложения и Keras для машинного обучения. В качестве базы данных для хранения информации о содержимом изображений используем MongoDB, а для получения информации возьмём уже натренированную модель ResNet50. При необходимости мы сможем заменить модель, используя методы save_model() и load_model(), доступные в Keras. Последний потребует около 100 Мб при первоначальной загрузке модели. Почитать о других доступных моделях можно в документации.

Начнём с Flask

Если вы незнакомы с Flask, то создать роут на нём можно просто добавив к контроллеру декоратор app.route(‘/’), где app — переменная приложения. Пример:

from flask import Flask  app = Flask(__name__)  @app.route('/')  def hello_world():     return 'Hello, World!'

При запуске и переходе по дефолтному адресу 127.0.0.1:5000/ мы увидим ответ Hello World! О том, как сделать что-то посложнее, можно почитать в документации.

Приступим же к созданию полноценного бэкенда:

import os import tensorflow as tf from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image as img from keras.preprocessing.image import img_to_array import numpy as np from PIL import Image from keras.applications.resnet50 import ResNet50,decode_predictions,preprocess_input from datetime import datetime import io from flask import Flask,Blueprint,request,render_template,jsonify from modules.dataBase import collection as db

Как можно заметить импорты содержат tensorflow, который мы будем использовать как бэкенд для keras, а так же numpy для работы с мультиразмерными массивами.

mod = Blueprint('backend', __name__, template_folder='templates', static_folder='./static') UPLOAD_URL = 'http://192.168.1.103:5000/static/' model = ResNet50(weights='imagenet') model._make_predict_function()

На первой строчке мы создаём блюпринт для более удобной организации приложения. Из-за этого надо будет использовать mod.route(‘/’) для декорирования контроллера. Предварительно натренированная на imagenet модель Resnet50 нуждается в вызове _make_predict_function() для инициализации. Без этого шага есть вероятность получить ошибку. А другую модель можно использовать, заменив строку

model = ResNet50(weights='imagenet')

на 

model = load_model('saved_model.h5')

Вот как будет выглядеть контроллер:

@mod.route('/predict', methods=['POST']) def predict():        if request.method == 'POST':         # проверяем, что прислали файл         if 'file' not in request.files:            return "someting went wrong 1"                user_file = request.files['file']         temp = request.files['file']         if user_file.filename == '':             return "file name not found ..."                  else:             path = os.path.join(os.getcwd()+'\\modules\\static\\'+user_file.filename)             user_file.save(path)             classes = identifyImage(path)             db.addNewImage(                 user_file.filename,                 classes[0][0][1],                 str(classes[0][0][2]),                 datetime.now(),                 UPLOAD_URL+user_file.filename)              return jsonify({                 "status":"success",                 "prediction":classes[0][0][1],                 "confidence":str(classes[0][0][2]),                 "upload_time":datetime.now()                 })

В коде выше загруженное изображение передаётся в метод identifyImage(file_path), который реализован так:

def identifyImage(img_path):        image = img.load_img(img_path, target_size=(224,224))     x = img_to_array(image)     x = np.expand_dims(x, axis=0)     # images = np.vstack([x])     x = preprocess_input(x)     preds = model.predict(x)     preds = decode_predictions(preds, top=1)     print(preds)     return preds

Сначала мы преобразуем изображение к размеру 224*224, т.к. именно он нужен нашей модели. Затем передаём в model.predict() предварительно обработанные байты изображения. Теперь наша модель может предсказать, что находится на изображении (top=1 нужен чтобы получить единственный самый вероятный результат).

Сохраним полученные данные о содержимом изображения в MongoDB с помощью функции db.addData(). Вот релевантная часть кода:

from pymongo import MongoClient from bson import ObjectId   client = MongoClient("mongodb://localhost:27017")  # host uri   db = client.image_predition #Select the database   image_details = db.imageData  def addNewImage(i_name, prediction, conf, time, url):     image_details.insert({         "file_name":i_name,         "prediction":prediction,         "confidence":conf,         "upload_time":time,         "url":url     })      def getAllImages():     data = image_details.find()     return data

Так как мы использовали блюпринт, код для API можно разместить в отдельном файле:

from flask import Flask,render_template,jsonify,Blueprint mod = Blueprint('api',__name__,template_folder='templates') from modules.dataBase import collection as db from bson.json_util import dumps  @mod.route('/') def api():     return dumps(db.getAllImages())

Как можно заметить, для возвращения данных БД мы используем json. Посмотреть на результат можно по адресу 127.0.0.1:5000/api

Выше, разумеется, только самые важные куски кода. Полностью проект можно посмотреть в GitHub репозитории. А больше о Pymongo можно почитать здесь.

Создаём приложение Flutter

Мобильная версия будет получать изображения и данные об их содержимом по REST API. Вот что получится в итоге:

ImageData класс инкапсулирует данные об изображении:

import 'dart:convert'; import 'package:http/http.dart' as http; import 'dart:async';  class ImageData { //  static String  BASE_URL ='http://192.168.1.103:5000/';   String uri;   String prediction;   ImageData(this.uri,this.prediction); }  Future<List<ImageData>> LoadImages() async {   List<ImageData> list;   //complete fetch ....   var data = await http.get(      'http://192.168.1.103:5000/api/');   var jsondata = json.decode(data.body);   List<ImageData> newslist = [];    for (var data in jsondata) {     ImageData n = ImageData(data['url'],data['prediction']);     newslist.add(n);   }    return newslist;  }

Здесь мы получаем json, преобразуем его в список объектов ImageData и возвращаем во Future Builder с помощью функции LoadImages()

Загрузка изображений на сервер

  uploadImageToServer(File imageFile)async   {     print("attempting to connecto server......");     var stream = new http.ByteStream(DelegatingStream.typed(imageFile.openRead()));       var length = await imageFile.length();       print(length);        var uri = Uri.parse('http://192.168.1.103:5000/predict');        print("connection established.");      var request = new http.MultipartRequest("POST", uri);       var multipartFile = new http.MultipartFile('file', stream, length,           filename: basename(imageFile.path));           //contentType: new MediaType('image', 'png'));        request.files.add(multipartFile);       var response = await request.send();       print(response.statusCode);   }

Чтобы сделать Flask доступным в локальной сети отключите режим дебага и найдите ipv4 адрес, используя ipconfig. Запустить локальный сервер можно так:

app.run(debug=False, host='192.168.1.103', port=5000)

Иногда файрвол может мешать приложению обращаться к локалхосту, тогда его придётся перенастроить или отключить.


Весь исходный код приложения доступен на гитхабе. Вот ссылки, которые помогут разобраться в происходящем:

Keras : https://keras.io/

Flutter : https://flutter.dev/

MongoDB : https://www.tutorialspoint.com/mongodb/

Курс Гарварда по Python и Flask: https://www.youtube.com/watch?v=j5wysXqaIV8&t=5515s (особенно важны лекции 2,3,4)

GitHub : https://github.com/SHARONZACHARIA


ссылка на оригинал статьи https://habr.com/ru/post/460995/