مستندات فنی

راهنمای توسعه و یکپارچه‌سازی مدل

این سند نحوه ساخت مدل پیش‌بینی سازگار با پلتفرم 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 از اطلاعات این فایل برای نمایش مدل در سایت، ثبت اطلاعات توسعه‌دهنده و تعیین قابلیت‌های مدل استفاده می‌کند.

manifest.json
{
    "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)

این فایل قلب میکروسرویس شماست. در ادامه یک نمونه کامل با مدل فرضی «پیش‌بینی ریسک دیابت» آورده شده است:

توجه مهم: کد زیر یک نمونه آموزشی با داده‌های فرضی است. شما باید آن را مطابق با مدل و داده‌های واقعی خود تغییر دهید.
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 از این اندپوینت‌ها برای ارتباط با مدل استفاده می‌کند:

GET /health

بررسی سلامت سرویس. سامانه به صورت دوره‌ای این اندپوینت را فراخوانی می‌کند.

پاسخ نمونه:
{
    "status": "healthy",
    "model_name": "Diabetes Risk Prediction",
    "version": "1.0"
}
GET /info

بازگرداندن اطلاعات کامل مدل برای نمایش در پنل مدیریت و صفحه مدل.

GET /schema

بازگرداندن اسکیمای فرم ورودی. سامانه SmartDx فرم ورودی کاربر را بر اساس این اسکیما به صورت پویا تولید می‌کند.

POST /predict

دریافت داده‌های ورودی و بازگرداندن نتیجه پیش‌بینی همراه با نمودارهای تفسیری.

بدنه درخواست:
{
    "data": {
        "age": 52,
        "bmi": 28.5,
        "glucose": 130,
        "blood_pressure": 85,
        "family_history": "yes",
        "physical_activity": "low"
    }
}

راهنمای تعریف اسکیما

اسکیما تعیین می‌کند که فرم ورودی چه فیلدهایی داشته باشد. هر فیلد می‌تواند یکی از انواع زیر باشد:

number
فیلد عددی (سن، قند خون، BMI و ...)
select
فیلد انتخابی با گزینه‌های مشخص (جنسیت، بله/خیر و ...)
text
فیلد متنی آزاد (در صورت نیاز)
ویژگی‌های هر فیلد
ویژگی نوع توضیح الزامی
namestringنام یکتای فیلد (نام ستون در دیتاست)
label_fastringبرچسب فارسی برای نمایش به کاربر
label_enstringبرچسب انگلیسی
typestringنوع فیلد: number، select یا text
requiredbooleanآیا پر کردن فیلد الزامی است؟
optionsarrayگزینه‌ها (فقط برای نوع select)شرطی
help_text_fastringمتن راهنمای فیلد
orderintegerترتیب نمایش فیلد در فرم
گروه‌بندی فیلدها: با استفاده از field_groups می‌توانید فیلدها را در بخش‌های معنادار (مثلاً «اطلاعات فردی»، «نتایج آزمایشگاهی») دسته‌بندی کنید. این کار باعث خوانایی بهتر فرم برای کاربر می‌شود.

فایل وابستگی‌ها (requirements.txt)

تمام کتابخانه‌های پایتونی که مدل شما به آنها نیاز دارد باید در این فایل ذکر شوند:

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:

بررسی سلامت:

Bash
curl http://localhost:5001/health

دریافت اسکیما:

Bash
curl http://localhost:5001/schema

ارسال درخواست پیش‌بینی:

Bash
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"
    }
  }'
در محیط تولید (Production): مدل شما توسط سامانه SmartDx با استفاده از systemd مدیریت و اجرا خواهد شد. محدودیت‌های منابع (حافظه و CPU) به صورت خودکار اعمال می‌شود.

چک‌لیست نهایی قبل از ارسال

قبل از ارسال مدل به تیم SmartDx، موارد زیر را بررسی کنید:

  • فایل app.py بدون خطا اجرا می‌شود
  • اندپوینت /health پاسخ {"status": "healthy"} را برمی‌گرداند
  • اندپوینت /schema اسکیمای معتبر با تمام فیلدها را برمی‌گرداند
  • اندپوینت /predict نتیجه پیش‌بینی همراه با نمودارهای SHAP را برمی‌گرداند
  • فایل manifest.json کامل و JSON معتبر است
  • تمام وابستگی‌ها در requirements.txt ذکر شده‌اند
  • تمام فایل‌های مدل در پوشه artifacts/ قرار دارند
  • با داده‌های نمونه تست شده و نتیجه صحیح بازمی‌گرداند

نیاز به راهنمایی بیشتر دارید؟

تیم فنی SmartDx آماده پاسخگویی به سؤالات شماست.

support@smartdx.ir