← Înapoi la Blog
·7 min read

De la Prototip la Producție: Deployment unui Model ML pe Android

Pașii practici între un notebook Jupyter funcțional și un API de producție care deservește o aplicație mobilă — inclusiv conversia TFLite, deployment Flask și monitorizarea performanței.

Există un gap între „funcționează în notebook-ul meu Jupyter" și „funcționează fiabil în producție." Acolo se blochează majoritatea proiectelor ML.

Acest articol acoperă pașii practici pentru a lua un model PyTorch antrenat și a-l deploya ca sistem de inferență gata pentru producție, care deservește o aplicație Android. Este fluxul pe care îl folosim în mod repetat la AIVerse.

Stack-ul

  • Antrenare: Python, PyTorch, YOLOv5/YOLOv8
  • Conversie: ONNX, TensorFlow, TFLite
  • Serving (opțional): Flask/FastAPI pe server Linux
  • Runtime mobil: Android (Java/Kotlin) cu librăria TFLite Android

Faza 1: Curăță Artefactele de Antrenare

Înainte de a converti orice, modelul antrenat trebuie să fie reproductibil și curat:

Igiena checkpoint-urilor:

  • Salvează greutățile finale ale modelului (best.pt, nu doar last.pt)
  • Salvează configurația de antrenare (hiperparametri, setări de augmentare)
  • Înregistrează metricile de validare — vei avea nevoie de ele ca baseline pentru a verifica acuratețea după conversie

Validează pe date nevăzute înainte de conversie: Rulează inferența pe un eșantion reprezentativ din datele tale de producție (nu setul de validare) și verifică că rezultatele corespund așteptărilor. Dacă există surprize, rezolvă-le înainte de conversie — vor fi mai greu de debugat în TFLite.

Faza 2: Convertirea PyTorch → TFLite

Lanțul de conversie pentru YOLOv5/YOLOv8 la TFLite:

PyTorch (.pt) → ONNX (.onnx) → TensorFlow SavedModel → TFLite (.tflite)

Pasul 1: Export în ONNX

# Pentru YOLOv5:
python export.py --weights best.pt --include onnx --opset 12

# Pentru YOLOv8:
from ultralytics import YOLO
model = YOLO('best.pt')
model.export(format='onnx', opset=12)

Folosește opset 12 — are cea mai bună compatibilitate cu instrumentele de conversie TensorFlow.

Pasul 2: ONNX → TensorFlow SavedModel

pip install onnx-tf
python -c "
import onnx
from onnx_tf.backend import prepare

model = onnx.load('best.onnx')
tf_rep = prepare(model)
tf_rep.export_graph('saved_model')
"

Pasul 3: SavedModel → TFLite

import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_saved_model('saved_model')

# Pentru cuantizare INT8 (recomandat pentru mobil):
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset_gen
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.float32

tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
    f.write(tflite_model)

Funcția representative_dataset_gen este critică pentru cuantizarea INT8 — furnizează mostre de intrare pentru ca convertorul să calibreze intervalele de cuantizare:

def representative_dataset_gen():
    for image_path in sample_images[:100]:  # 100 mostre e suficient de obicei
        img = preprocess_image(image_path)  # același preprocesare ca la antrenare
        yield [img]

Verifică acuratețea după fiecare pas. Rulează aceleași 50-100 imagini de test prin PyTorch, ONNX, SavedModel și TFLite și compară outputurile. Fiecare pas de conversie poate introduce diferențe subtile.

Faza 3: Integrarea Android

Adaugă dependința:

// build.gradle
dependencies {
    implementation 'org.tensorflow:tensorflow-lite:2.14.0'
    implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'  // opțional
}

Încarcă și rulează modelul:

class MeterDetector(context: Context) {
    private val interpreter: Interpreter
    
    init {
        val model = FileUtil.loadMappedFile(context, "model.tflite")
        val options = Interpreter.Options().apply {
            numThreads = 4
        }
        interpreter = Interpreter(model, options)
    }
    
    fun detect(bitmap: Bitmap): List<Detection> {
        val input = preprocessBitmap(bitmap)  // resize la 640x640, normalizare
        val output = Array(1) { Array(25200) { FloatArray(85) } }
        
        interpreter.run(input, output)
        return postprocess(output)
    }
}

Pasul de preprocesare e critic — trebuie să corespundă exact cu ce a făcut pipeline-ul de antrenare:

private fun preprocessBitmap(bitmap: Bitmap): ByteBuffer {
    val resized = Bitmap.createScaledBitmap(bitmap, 640, 640, true)
    val buffer = ByteBuffer.allocateDirect(1 * 640 * 640 * 3 * 4)
    buffer.order(ByteOrder.nativeOrder())
    
    val pixels = IntArray(640 * 640)
    resized.getPixels(pixels, 0, 640, 0, 0, 640, 640)
    
    for (pixel in pixels) {
        buffer.putFloat(((pixel shr 16) and 0xFF) / 255.0f)  // R
        buffer.putFloat(((pixel shr 8) and 0xFF) / 255.0f)   // G
        buffer.putFloat((pixel and 0xFF) / 255.0f)            // B
    }
    return buffer
}

Dacă obții rezultate neașteptat de slabe pe dispozitiv, nepotrivirea de preprocesare este cauza cea mai frecventă. Printează primele 10 valori de pixeli din preprocesarea Python și Android și compară-le.

Faza 4: API-ul Flask (Opțional)

Pentru cazurile în care inferența on-device nu e fezabilă (model prea mare, dispozitiv prea lent, sau inferență partajată între mai mulți clienți), un API Flask funcționează bine:

from flask import Flask, request, jsonify
import torch
from PIL import Image
import io

app = Flask(__name__)
model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt')
model.eval()

@app.route('/detect', methods=['POST'])
def detect():
    if 'image' not in request.files:
        return jsonify({'error': 'No image'}), 400
    
    image_bytes = request.files['image'].read()
    image = Image.open(io.BytesIO(image_bytes))
    
    results = model(image)
    detections = results.pandas().xyxy[0].to_dict(orient='records')
    
    return jsonify({'detections': detections})

Deploy-uiește asta în spatele Nginx cu Gunicorn. Folosește un process manager (systemd sau supervisor) pentru a-l menține activ.

Faza 5: Monitorizare în Producție

Aceasta e partea pe care majoritatea tutorialelor o sar. Modelul tău se va degrada în timp pe măsură ce datele reale derivă din distribuția de antrenare.

Ce să loghezi:

  • Latența de inferență (p50, p95, p99)
  • Distribuțiile scorului de confidence — dacă media confidence scade, ceva s-a schimbat
  • Statisticile imaginilor de intrare (luminozitate medie, contrast)
  • Rata de eroare din validarea downstream (dacă ai ground truth din re-verificări)

Detecție simplă de anomalii: Urmărește o medie mobilă pe 7 zile a confidence-ului mediu. Dacă scade cu mai mult de 10% față de baseline, investighează. Asta prinde majoritatea schimbărilor de distribuție înainte să devină probleme vizibile pentru utilizatori.

Versionare modele: Păstrează modelele TFLite versionate (model_v1.tflite, model_v2.tflite). Când actualizezi modelul, rulează ambele în paralel o săptămână și compară outputurile.

Puncte Comune de Eșec

Din experiența noastră, iată unde lucrurile merg de obicei greșit:

  1. Nepotrivire preprocesare — cauza #1 pentru „funcționează în Python dar nu pe Android"
  2. Postprocesare output — parametrii NMS care funcționează la antrenare nu funcționează întotdeauna în condiții de iluminare din producție
  3. Erori de cuantizare — unii operatori nu se cuantizează curat; testează temeinic
  4. Memory leaks — obiectele Bitmap din Android trebuie reciclate explicit
  5. Thermal throttling — inferența continuă pe dispozitive mobile provoacă throttling CPU/GPU după ~10 minute

Ești blocat la tranziția prototip-producție pe proiectul tău ML? Contactează-ne — exact acest tip de provocare inginerească o rezolvăm.