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 doarlast.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:
- Nepotrivire preprocesare — cauza #1 pentru „funcționează în Python dar nu pe Android"
- Postprocesare output — parametrii NMS care funcționează la antrenare nu funcționează întotdeauna în condiții de iluminare din producție
- Erori de cuantizare — unii operatori nu se cuantizează curat; testează temeinic
- Memory leaks — obiectele
Bitmapdin Android trebuie reciclate explicit - 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.