راهنمای توسعه و یکپارچهسازی مدل
این سند نحوه ساخت مدل پیشبینی سازگار با پلتفرم SmartDx را شرح میدهد.
مدل شما به صورت یک میکروسرویس Flask مستقل اجرا شده و از طریق HTTP/JSON با سامانه اصلی ارتباط برقرار میکند.
معرفی کلی
مدل شما در SmartDx به عنوان یک میکروسرویس مستقل Flask اجرا میشود. این معماری به شما اجازه میدهد از هر کتابخانهای (Scikit-learn, PyTorch, XGBoost و غیره) بدون تداخل با سامانه اصلی استفاده کنید. ارتباط بین SmartDx و مدل شما از طریق پروتکل HTTP و فرمت JSON برقرار میشود.
مستقل و ایزوله
هر مدل در محیط مجازی خودش اجرا شده و تداخلی با سایر مدلها نداردارتباط HTTP/JSON
تبادل دادهها از طریق پروتکلهای استاندارد وب انجام میشودمدیریت خودکار
سامانه SmartDx پورت، منابع و راهاندازی مدل را مدیریت میکندنیازمندیهای فنی
الزامی
- Python 3.10+
- Flask (برای ایجاد وبسرویس)
اختیاری (بسته به مدل شما)
- scikit-learn, pandas, numpy
- shap, matplotlib
- joblib یا pickle
- هر کتابخانه دیگری که مدل شما نیاز دارد
ساختار فایلها
پروژه شما باید طبق ساختار زیر سازماندهی شود. فایلهای مشخص شده با «الزامی» حتماً باید وجود داشته باشند:
your_model/
├── app.py # الزامی: فایل اصلی برنامه Flask
├── manifest.json # الزامی: متادیتا و اطلاعات مدل
├── requirements.txt # الزامی: لیست وابستگیهای پایتون
├── artifacts/ # فایلهای آموزشدیده مدل شما
│ ├── model.joblib
│ ├── imputer.joblib
│ ├── label_encoders.joblib
│ └── ...
└── utils/ # اختیاری: ماژولهای کمکی
└── preprocessing.py
artifacts/ محل قرارگیری تمام فایلهای باینری مدل شماست (مانند فایلهای .joblib، .pkl، .h5 و غیره).
فایل تنظیمات (manifest.json)
این فایل شناسنامه مدل شماست. سامانه SmartDx از اطلاعات این فایل برای نمایش مدل در سایت، ثبت اطلاعات توسعهدهنده و تعیین قابلیتهای مدل استفاده میکند.
{
"name_fa": "پیشبینی ریسک دیابت",
"name_en": "Diabetes Risk Prediction",
"description_fa": "پیشبینی احتمال ابتلا به دیابت نوع ۲ بر اساس ویژگیهای فردی و آزمایشگاهی",
"description_en": "Predicts probability of Type 2 diabetes based on personal and lab features",
"developer": {
"name": "علی محمدی",
"name_en": "Ali Mohammadi",
"email": "ali@example.com",
"phone": "09121234567",
"institution_fa": "دانشگاه علوم پزشکی تهران",
"institution_en": "Tehran University of Medical Sciences"
},
"version": "1.0",
"last_updated": "2025-03-01",
"algorithm": "GradientBoosting",
"capabilities": {
"has_shap": true,
"shap_format": "image",
"has_feature_importance": true,
"has_confidence_interval": false,
"has_interpretation": true
},
"followup": {
"enabled": true,
"days": 180,
"question_fa": "آیا بیمار پس از شش ماه به دیابت مبتلا شده است؟",
"question_en": "Has the patient developed diabetes after six months?",
"options": [
{"value": 1, "label_fa": "بله", "label_en": "Yes"},
{"value": 0, "label_fa": "خیر", "label_en": "No"}
]
},
"metrics": {
"accuracy": 0.82,
"auc": 0.87,
"sensitivity": 0.79,
"specificity": 0.85,
"f1": 0.80
},
"resource_hints": {
"typical_memory_mb": 256,
"typical_time_seconds": 2
}
}
توضیح فیلدها
| فیلد | توضیح | الزامی |
|---|---|---|
name_fa / name_en | نام مدل به فارسی و انگلیسی | |
description_fa / description_en | توضیح کوتاه درباره عملکرد مدل | |
developer | اطلاعات توسعهدهنده (نام، ایمیل، وابستگی سازمانی) | |
version | شماره نسخه مدل | |
algorithm | نام الگوریتم یادگیری ماشین مورد استفاده | |
capabilities | قابلیتهای مدل: SHAP، تفسیر، بازه اطمینان و ... | |
followup | تنظیمات پیگیری بیمار پس از پیشبینی | |
metrics | معیارهای عملکرد مدل (دقت، AUC و ...) | |
resource_hints | حدود مصرف حافظه و زمان اجرا |
فایل اصلی برنامه (app.py)
این فایل قلب میکروسرویس شماست. در ادامه یک نمونه کامل با مدل فرضی «پیشبینی ریسک دیابت» آورده شده است:
"""
میکروسرویس مدل SmartDx
مدل: پیشبینی ریسک دیابت (نمونه آموزشی)
نویسنده: علی محمدی
"""
from flask import Flask, request, jsonify
import joblib
import pandas as pd
import numpy as np
import shap
import matplotlib
matplotlib.use('Agg') # بکاند غیرتعاملی
import matplotlib.pyplot as plt
from io import BytesIO
import base64
import os
import json
app = Flask(__name__)
# =============================================================================
# بارگذاری فایلهای مدل
# =============================================================================
ARTIFACTS_DIR = os.path.join(os.path.dirname(__file__), 'artifacts')
model = joblib.load(os.path.join(ARTIFACTS_DIR, 'model.joblib'))
scaler = joblib.load(os.path.join(ARTIFACTS_DIR, 'scaler.joblib'))
feature_names = joblib.load(os.path.join(ARTIFACTS_DIR, 'feature_names.joblib'))
# بارگذاری مانیفست
with open(os.path.join(os.path.dirname(__file__), 'manifest.json'), 'r', encoding='utf-8') as f:
MANIFEST = json.load(f)
# =============================================================================
# توابع کمکی
# =============================================================================
def fig_to_base64(fig, format='png', dpi=150):
"""تبدیل نمودار matplotlib به رشته base64"""
buffer = BytesIO()
fig.savefig(buffer, format=format, dpi=dpi, bbox_inches='tight',
facecolor='white', edgecolor='none')
buffer.seek(0)
img_str = base64.b64encode(buffer.read()).decode('utf-8')
plt.close(fig)
return f"data:image/{format};base64,{img_str}"
def preprocess_input(data):
"""
پیشپردازش دادههای ورودی برای پیشبینی
Args:
data: دیکشنری از نام_فیلد -> مقدار
Returns:
دیتافریم pandas آماده برای پیشبینی
"""
df = pd.DataFrame([data])
# مرتبسازی ستونها مطابق با ترتیب آموزش مدل
df = df.reindex(columns=feature_names, fill_value=np.nan)
# نرمالسازی مقادیر عددی
df_scaled = pd.DataFrame(
scaler.transform(df),
columns=feature_names
)
return df_scaled
def get_risk_category(probability):
"""دستهبندی ریسک بر اساس احتمال"""
if probability < 0.3:
return {
'category': 'low',
'label_fa': 'ریسک پایین',
'label_en': 'Low Risk',
'color': '#4caf50'
}
elif probability < 0.6:
return {
'category': 'medium',
'label_fa': 'ریسک متوسط',
'label_en': 'Medium Risk',
'color': '#ff9800'
}
else:
return {
'category': 'high',
'label_fa': 'ریسک بالا',
'label_en': 'High Risk',
'color': '#f44336'
}
def generate_shap_plots(model, X, feature_names_display):
"""تولید نمودارهای SHAP"""
plots = {}
try:
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
# برای طبقهبندی دودویی، مقادیر کلاس مثبت
if isinstance(shap_values, list):
shap_values = shap_values[1]
# نمودار آبشاری
fig, ax = plt.subplots(figsize=(10, 6))
shap.waterfall_plot(
shap.Explanation(
values=shap_values[0],
base_values=explainer.expected_value if not isinstance(explainer.expected_value, list) else explainer.expected_value[1],
data=X.iloc[0],
feature_names=feature_names_display
),
show=False
)
plots['waterfall'] = fig_to_base64(plt.gcf())
plt.close('all')
except Exception as e:
print(f"SHAP error: {e}")
plots['error'] = str(e)
return plots
def generate_feature_importance(model, X, feature_names_display):
"""تولید دادههای اهمیت ویژگیها"""
try:
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
if isinstance(shap_values, list):
shap_values = shap_values[1]
importance = np.abs(shap_values[0])
sorted_idx = np.argsort(importance)[::-1]
effects = []
for idx in sorted_idx:
if shap_values[0][idx] > 0:
effects.append('positive')
else:
effects.append('negative')
return [
{
'feature': feature_names[sorted_idx[i]],
'feature_fa': feature_names_display[sorted_idx[i]],
'importance': float(importance[sorted_idx[i]]),
'effect': effects[i]
}
for i in range(len(sorted_idx))
]
except Exception as e:
print(f"Feature importance error: {e}")
return []
# =============================================================================
# تعریف اسکیمای فیلدهای ورودی
# =============================================================================
SCHEMA = {
"fields": [
{
"name": "age",
"label_fa": "سن",
"label_en": "Age",
"type": "number",
"required": True,
"help_text_fa": "سن بیمار به سال",
"order": 1
},
{
"name": "bmi",
"label_fa": "شاخص توده بدنی",
"label_en": "BMI",
"type": "number",
"required": True,
"help_text_fa": "وزن (کیلوگرم) تقسیم بر مجذور قد (متر)",
"order": 2
},
{
"name": "glucose",
"label_fa": "قند خون ناشتا",
"label_en": "Fasting Glucose",
"type": "number",
"required": True,
"help_text_fa": "میزان قند خون ناشتا (mg/dL)",
"order": 3
},
{
"name": "blood_pressure",
"label_fa": "فشار خون",
"label_en": "Blood Pressure",
"type": "number",
"required": True,
"help_text_fa": "فشار خون دیاستولیک (mmHg)",
"order": 4
},
{
"name": "family_history",
"label_fa": "سابقه خانوادگی دیابت",
"label_en": "Family History",
"type": "select",
"required": True,
"options": [
{"value": "no", "label_fa": "خیر", "label_en": "No"},
{"value": "yes", "label_fa": "بله", "label_en": "Yes"}
],
"order": 5
},
{
"name": "physical_activity",
"label_fa": "سطح فعالیت بدنی",
"label_en": "Physical Activity",
"type": "select",
"required": True,
"options": [
{"value": "low", "label_fa": "کم", "label_en": "Low"},
{"value": "moderate", "label_fa": "متوسط", "label_en": "Moderate"},
{"value": "high", "label_fa": "زیاد", "label_en": "High"}
],
"order": 6
}
],
"field_groups": [
{
"name_fa": "اطلاعات فردی",
"name_en": "Personal Info",
"fields": ["age", "bmi", "family_history", "physical_activity"]
},
{
"name_fa": "نتایج آزمایشگاهی",
"name_en": "Lab Results",
"fields": ["glucose", "blood_pressure"]
}
]
}
# نگاشت نام فارسی ویژگیها
FEATURE_NAMES_FA = {field['name']: field['label_fa'] for field in SCHEMA['fields']}
# =============================================================================
# اندپوینتهای API
# =============================================================================
@app.route('/health', methods=['GET'])
def health_check():
"""بررسی سلامت سرویس"""
return jsonify({
'status': 'healthy',
'model_name': MANIFEST['name_en'],
'version': MANIFEST['version']
})
@app.route('/info', methods=['GET'])
def get_info():
"""بازگرداندن اطلاعات مدل برای پنل مدیریت"""
return jsonify({
'success': True,
'info': {
'name_fa': MANIFEST['name_fa'],
'name_en': MANIFEST['name_en'],
'description_fa': MANIFEST['description_fa'],
'description_en': MANIFEST['description_en'],
'developer': MANIFEST['developer'],
'version': MANIFEST['version'],
'last_updated': MANIFEST['last_updated'],
'algorithm': MANIFEST['algorithm'],
'metrics': MANIFEST['metrics'],
'capabilities': MANIFEST['capabilities'],
'followup': MANIFEST['followup']
}
})
@app.route('/schema', methods=['GET'])
def get_schema():
"""بازگرداندن اسکیمای فرم برای تولید پویای فرم ورودی"""
return jsonify({
'success': True,
'schema': SCHEMA
})
@app.route('/predict', methods=['POST'])
def predict():
"""انجام پیشبینی بر اساس دادههای ورودی"""
try:
data = request.json.get('data', {})
# اعتبارسنجی فیلدهای الزامی
missing_fields = []
for field in SCHEMA['fields']:
if field['required'] and field['name'] not in data:
missing_fields.append(field['name'])
if missing_fields:
return jsonify({
'success': False,
'error': {
'code': 'MISSING_FIELDS',
'message_fa': f'فیلدهای الزامی وارد نشده: {", ".join(missing_fields)}',
'message_en': f'Missing required fields: {", ".join(missing_fields)}',
'fields': missing_fields
}
}), 400
# پیشپردازش ورودی
X = preprocess_input(data)
# انجام پیشبینی
probability = model.predict_proba(X)[0][1]
prediction_class = int(probability >= 0.5)
# تعیین دستهبندی ریسک
risk = get_risk_category(probability)
# نام فارسی ویژگیها
feature_names_display = [FEATURE_NAMES_FA.get(fn, fn) for fn in feature_names]
# تولید نمودارهای SHAP
shap_plots = generate_shap_plots(model, X, feature_names_display)
# تولید اهمیت ویژگیها
feature_importance = generate_feature_importance(model, X, feature_names_display)
# ساخت پاسخ
response = {
'success': True,
'prediction': {
'probability': float(probability),
'probability_formatted': f'{probability * 100:.1f}٪',
'class': prediction_class,
'class_label_fa': 'در معرض خطر' if prediction_class == 1 else 'کمخطر',
'class_label_en': 'At Risk' if prediction_class == 1 else 'Low Risk',
'risk_category': risk['category'],
'risk_label_fa': risk['label_fa'],
'risk_label_en': risk['label_en'],
'risk_color': risk['color']
},
'explanations': {
'shap': {
'format': 'image',
**shap_plots
},
'feature_importance': {
'format': 'data',
'values': feature_importance[:10]
}
},
'interpretation': {
'summary_fa': f'احتمال ابتلا به دیابت نوع ۲ برای این بیمار {probability * 100:.1f}٪ برآورد میشود.',
'summary_en': f'The probability of developing Type 2 diabetes is estimated at {probability * 100:.1f}%.',
'key_factors_fa': [
f'{fi["feature_fa"]}: تأثیر {"مثبت" if fi["effect"] == "positive" else "منفی"}'
for fi in feature_importance[:3]
]
}
}
return jsonify(response)
except Exception as e:
return jsonify({
'success': False,
'error': {
'code': 'PREDICTION_ERROR',
'message_fa': 'خطا در پردازش پیشبینی',
'message_en': str(e)
}
}), 500
# =============================================================================
# اجرای برنامه
# =============================================================================
if __name__ == '__main__':
# پورت توسط سامانه SmartDx اختصاص داده میشود
# پیشفرض 5001 برای تست محلی
import sys
port = int(sys.argv[1]) if len(sys.argv) > 1 else 5001
app.run(host='127.0.0.1', port=port, debug=False)
اندپوینتهای مورد نیاز
مدل شما باید ۴ اندپوینت زیر را پیادهسازی کند. سامانه SmartDx از این اندپوینتها برای ارتباط با مدل استفاده میکند:
/health
بررسی سلامت سرویس. سامانه به صورت دورهای این اندپوینت را فراخوانی میکند.
پاسخ نمونه:{
"status": "healthy",
"model_name": "Diabetes Risk Prediction",
"version": "1.0"
}
/info
بازگرداندن اطلاعات کامل مدل برای نمایش در پنل مدیریت و صفحه مدل.
/schema
بازگرداندن اسکیمای فرم ورودی. سامانه SmartDx فرم ورودی کاربر را بر اساس این اسکیما به صورت پویا تولید میکند.
/predict
دریافت دادههای ورودی و بازگرداندن نتیجه پیشبینی همراه با نمودارهای تفسیری.
بدنه درخواست:{
"data": {
"age": 52,
"bmi": 28.5,
"glucose": 130,
"blood_pressure": 85,
"family_history": "yes",
"physical_activity": "low"
}
}
راهنمای تعریف اسکیما
اسکیما تعیین میکند که فرم ورودی چه فیلدهایی داشته باشد. هر فیلد میتواند یکی از انواع زیر باشد:
number
فیلد عددی (سن، قند خون، BMI و ...)
select
فیلد انتخابی با گزینههای مشخص (جنسیت، بله/خیر و ...)
text
فیلد متنی آزاد (در صورت نیاز)
ویژگیهای هر فیلد
| ویژگی | نوع | توضیح | الزامی |
|---|---|---|---|
name | string | نام یکتای فیلد (نام ستون در دیتاست) | |
label_fa | string | برچسب فارسی برای نمایش به کاربر | |
label_en | string | برچسب انگلیسی | |
type | string | نوع فیلد: number، select یا text | |
required | boolean | آیا پر کردن فیلد الزامی است؟ | |
options | array | گزینهها (فقط برای نوع select) | شرطی |
help_text_fa | string | متن راهنمای فیلد | |
order | integer | ترتیب نمایش فیلد در فرم |
field_groups میتوانید فیلدها را در بخشهای معنادار (مثلاً «اطلاعات فردی»، «نتایج آزمایشگاهی») دستهبندی کنید. این کار باعث خوانایی بهتر فرم برای کاربر میشود.
فایل وابستگیها (requirements.txt)
تمام کتابخانههای پایتونی که مدل شما به آنها نیاز دارد باید در این فایل ذکر شوند:
flask>=2.3.0
pandas>=2.0.0
numpy>=1.24.0
scikit-learn>=1.3.0
shap>=0.42.0
matplotlib>=3.7.0
joblib>=1.3.0
pip freeze > requirements.txt استفاده نکنید زیرا کتابخانههای غیرضروری هم اضافه میشود.
تست و راهاندازی
راهاندازی محلی (برای توسعه)
مراحل زیر را برای اجرای محلی مدل خود دنبال کنید:
cd your_model
python -m venv venv
source venv/bin/activate # در ویندوز: venv\Scripts\activate
pip install -r requirements.txt
python app.py 5001
با استفاده از curl یا نرمافزار Postman:
بررسی سلامت:
curl http://localhost:5001/health
دریافت اسکیما:
curl http://localhost:5001/schema
ارسال درخواست پیشبینی:
curl -X POST http://localhost:5001/predict \
-H "Content-Type: application/json" \
-d '{
"data": {
"age": 52,
"bmi": 28.5,
"glucose": 130,
"blood_pressure": 85,
"family_history": "yes",
"physical_activity": "low"
}
}'
چکلیست نهایی قبل از ارسال
قبل از ارسال مدل به تیم SmartDx، موارد زیر را بررسی کنید:
-
فایل
app.pyبدون خطا اجرا میشود -
اندپوینت
/healthپاسخ{"status": "healthy"}را برمیگرداند -
اندپوینت
/schemaاسکیمای معتبر با تمام فیلدها را برمیگرداند -
اندپوینت
/predictنتیجه پیشبینی همراه با نمودارهای SHAP را برمیگرداند
-
فایل
manifest.jsonکامل و JSON معتبر است -
تمام وابستگیها در
requirements.txtذکر شدهاند -
تمام فایلهای مدل در پوشه
artifacts/قرار دارند - با دادههای نمونه تست شده و نتیجه صحیح بازمیگرداند