diff --git a/TP1/TP1.md b/TP1/TP1.md deleted file mode 100644 index 4d2a719..0000000 --- a/TP1/TP1.md +++ /dev/null @@ -1,113 +0,0 @@ -# 💡 AI for IoT Practical 1: Fire Alarm Detection - -📌 **Important:** Please run this practical work on **[Kaggle Notebooks](https://www.kaggle.com/code)**. -Kaggle provides free GPU/CPU resources, pre-installed libraries (scikit-learn, XGBoost, etc.), and easy dataset integration. - ---- - -## 1. Objective -The goal of this practical is to build a **binary classification model** to predict the state of an IoT-connected smoke detector based on environmental sensor readings. -You'll start with the fundamental **Logistic Regression** model and have the opportunity to implement the high-performance **XGBoost** model as a bonus. - ---- - -## 2. Dataset Overview -This dataset simulates real-world conditions encountered by an IoT fire detection device. - -- **Source**: [Kaggle - Smoke Detection Dataset](https://www.kaggle.com/datasets/deepcontractor/smoke-detection-dataset) -- **Key Features (Input/X):** - Time-series readings from sensors like: - - Temperature - - Humidity - - Gas concentrations (CO, LPG, Methane) - - Other environmental factors - -- **Target Feature (Output/y):** - - `1` → Fire Alarm is **ON** (Fire/Smoke detected) - - `0` → Normal operational conditions (No Fire/Smoke) - ---- - -## 3. Core Task: Logistic Regression Classifier - -Logistic Regression is an excellent starting point for binary classification, providing a good performance baseline for an AIoT application. - -### A. Setup and Preprocessing -1. **Import Libraries** - - `pandas`, `numpy`, `matplotlib` / `seaborn` - - From `sklearn`: `train_test_split`, `LogisticRegression`, `metrics` - -2. **Load and Inspect Data** - - Load the dataset from Kaggle - - Use `.info()` and `.isnull().sum()` to check for missing values - - Handle missing data (imputation or removal) - -3. **Define Features and Target** - - Separate features (`X`) and target variable (`y`) - -4. **Feature Scaling (MANDATORY)** - - Apply `StandardScaler` from `sklearn.preprocessing` - - Logistic Regression requires scaled features - -5. **Split Data** - - Use `train_test_split` with 80% training and 20% testing - ---- - -### B. Training and Evaluation -1. **Train Model** - - Initialize and train a `LogisticRegression` model - -2. **Make Predictions** - - Predict outcomes on the test set - -3. **Evaluate Performance** - - Calculate metrics: - - Accuracy - - Precision - - Recall - - F1-Score - -4. **Visualize Results** - - Generate and display a **Confusion Matrix** - ---- - -## 🏆 Bonus Challenge: Building a Robust XGBoost Model - -XGBoost (Extreme Gradient Boosting) is one of the most powerful ensemble techniques used in industry. - -1. **Implement XGBoost** - - Use `XGBClassifier` from the `xgboost` library - - Note: Tree-based models like XGBoost are not highly sensitive to feature scaling - -2. **Train and Evaluate** - - Train on the same train/test split - - Calculate the same metrics as Logistic Regression - -3. **Compare and Analyze** - - Which model has a higher **Recall** score? - - Why is minimizing **False Negatives** crucial in fire detection? - - Discuss trade-offs: - - Logistic Regression → simplicity & speed - - XGBoost → higher performance but more complexity - ---- - -## 4. Deliverables - -Submit a **single Kaggle Notebook (`.ipynb`)** containing: - -- **Code and Documentation** - - Well-commented code with all preprocessing and modeling steps - -- **Core Task Results** - - Accuracy, Precision, Recall, F1-Score, Confusion Matrix for Logistic Regression - -- **Conclusion** - - Interpret the Recall score of Logistic Regression - - Discuss implications for IoT fire alarm reliability - -- **Bonus Results (if completed)** - - XGBoost metrics - - Comparative analysis between Logistic Regression and XGBoost diff --git a/TP1_AIoT.ipynb b/TP1_AIoT.ipynb new file mode 100644 index 0000000..fbfdba1 --- /dev/null +++ b/TP1_AIoT.ipynb @@ -0,0 +1,818 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "_w3y4VumlGgB" + }, + "outputs": [], + "source": [ + "try:\n", + " import xgboost # noqa: F401\n", + " XGB_AVAILABLE = True\n", + "except Exception:\n", + " XGB_AVAILABLE = False\n", + "\n", + "if not XGB_AVAILABLE:\n", + " try:\n", + " # In Kaggle/Colab, this should succeed. If it fails (e.g., offline), you can skip the bonus.\n", + " !pip -q install xgboost\n", + " import xgboost # noqa: F401\n", + " XGB_AVAILABLE = True\n", + " except Exception as e:\n", + " print(\"⚠️ Could not install xgboost automatically. You can proceed without the bonus.\")\n", + " print(e)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import os, re, glob, warnings\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.impute import SimpleImputer\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.metrics import (\n", + " accuracy_score, precision_score, recall_score, f1_score,\n", + " confusion_matrix, ConfusionMatrixDisplay, classification_report\n", + ")\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "RANDOM_STATE = 42\n", + "np.random.seed(RANDOM_STATE)\n", + "\n", + "try:\n", + " from xgboost import XGBClassifier\n", + " XGB_AVAILABLE = True\n", + "except Exception:\n", + " XGB_AVAILABLE = False\n" + ], + "metadata": { + "id": "oB81zUE8lLBx" + }, + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def standardize_columns(df):\n", + " # Lowercase; replace non-alphanum with underscore; strip repeats\n", + " new_cols = []\n", + " for c in df.columns:\n", + " c2 = re.sub(r'[^0-9a-zA-Z]+', '_', c.strip().lower())\n", + " c2 = re.sub(r'_+', '_', c2).strip('_')\n", + " new_cols.append(c2)\n", + " df.columns = new_cols\n", + " return df\n", + "\n", + "def auto_find_csv(search_dir='/kaggle/input'):\n", + "\n", + " if not os.path.exists(search_dir):\n", + " return None\n", + " patterns = [\"*smoke*.csv\", \"*fire*.csv\", \"*alarm*.csv\", \"*iot*.csv\", \"*.csv\"]\n", + " for pat in patterns:\n", + " files = glob.glob(os.path.join(search_dir, \"**\", pat), recursive=True)\n", + " if files:\n", + "\n", + " files_sorted = sorted(files, key=lambda p: (0 if 'smoke' in p.lower() or 'fire' in p.lower() else 1, len(p)))\n", + " return files_sorted[0]\n", + " return None\n", + "\n", + "DATA_PATH = auto_find_csv('/kaggle/input')\n", + "print(\"Auto-detected data path:\", DATA_PATH)\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "sj1A3LkllO81", + "outputId": "776066e9-e73d-4b67-a1b4-6c3c7c1acd26" + }, + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Auto-detected data path: None\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "if DATA_PATH is None:\n", + " try:\n", + " from google.colab import files # type: ignore\n", + " print(\"🔼 Please choose the CSV file to upload...\")\n", + " up = files.upload()\n", + " if up:\n", + " DATA_PATH = list(up.keys())[0]\n", + " print(\"Using uploaded file:\", DATA_PATH)\n", + " except Exception as e:\n", + " print(\"Upload not available (probably not running in Colab).\", e)\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 107 + }, + "id": "Qmf1Q1vQoeMu", + "outputId": "15304af6-bd83-4843-c86d-841e3f033da0" + }, + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "🔼 Please choose the CSV file to upload...\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Saving archive (4).zip to archive (4).zip\n", + "Using uploaded file: archive (4).zip\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "\n", + "assert DATA_PATH is not None, \"❌ Dataset not found. Please add the Kaggle dataset via 'Add data' or set DATA_PATH manually.\"\n", + "\n", + "df_raw = pd.read_csv(DATA_PATH)\n", + "df = df_raw.copy()\n", + "df = standardize_columns(df)\n", + "\n", + "print(\"Shape:\", df.shape)\n", + "print(\"\\nColumns:\", list(df.columns))\n", + "print(\"\\nInfo:\")\n", + "print(df.info())\n", + "\n", + "print(\"\\nMissing values per column:\")\n", + "print(df.isnull().sum().sort_values(ascending=False).head(20))\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0vMW08R6orA3", + "outputId": "f7d4af6c-86a3-4b79-e82b-d60351cd7548" + }, + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Shape: (62630, 16)\n", + "\n", + "Columns: ['unnamed_0', 'utc', 'temperature_c', 'humidity', 'tvoc_ppb', 'eco2_ppm', 'raw_h2', 'raw_ethanol', 'pressure_hpa', 'pm1_0', 'pm2_5', 'nc0_5', 'nc1_0', 'nc2_5', 'cnt', 'fire_alarm']\n", + "\n", + "Info:\n", + "\n", + "RangeIndex: 62630 entries, 0 to 62629\n", + "Data columns (total 16 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 unnamed_0 62630 non-null int64 \n", + " 1 utc 62630 non-null int64 \n", + " 2 temperature_c 62630 non-null float64\n", + " 3 humidity 62630 non-null float64\n", + " 4 tvoc_ppb 62630 non-null int64 \n", + " 5 eco2_ppm 62630 non-null int64 \n", + " 6 raw_h2 62630 non-null int64 \n", + " 7 raw_ethanol 62630 non-null int64 \n", + " 8 pressure_hpa 62630 non-null float64\n", + " 9 pm1_0 62630 non-null float64\n", + " 10 pm2_5 62630 non-null float64\n", + " 11 nc0_5 62630 non-null float64\n", + " 12 nc1_0 62630 non-null float64\n", + " 13 nc2_5 62630 non-null float64\n", + " 14 cnt 62630 non-null int64 \n", + " 15 fire_alarm 62630 non-null int64 \n", + "dtypes: float64(8), int64(8)\n", + "memory usage: 7.6 MB\n", + "None\n", + "\n", + "Missing values per column:\n", + "unnamed_0 0\n", + "utc 0\n", + "temperature_c 0\n", + "humidity 0\n", + "tvoc_ppb 0\n", + "eco2_ppm 0\n", + "raw_h2 0\n", + "raw_ethanol 0\n", + "pressure_hpa 0\n", + "pm1_0 0\n", + "pm2_5 0\n", + "nc0_5 0\n", + "nc1_0 0\n", + "nc2_5 0\n", + "cnt 0\n", + "fire_alarm 0\n", + "dtype: int64\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "candidate_targets = [\n", + " 'fire_alarm', 'alarm', 'fire', 'target', 'class', 'label', 'smoke_detected'\n", + "]\n", + "\n", + "target_col = None\n", + "for c in candidate_targets:\n", + " if c in df.columns:\n", + " target_col = c\n", + " break\n", + "\n", + "if target_col is None:\n", + " raise ValueError(\n", + " \"❌ Could not auto-detect target column. \"\n", + " \"Please set 'target_col' manually from df.columns.\"\n", + " )\n", + "\n", + "print(f\"Detected target column: {target_col}\")\n", + "\n", + "if df[target_col].dtype == 'O':\n", + "\n", + " mapping = {'yes':1, 'y':1, 'true':1, 'on':1, 'fire':1,\n", + " 'no':0, 'n':0, 'false':0, 'off':0, 'normal':0}\n", + " df[target_col] = df[target_col].astype(str).str.strip().str.lower().map(mapping).astype('Int64')\n", + " if df[target_col].isna().any():\n", + "\n", + " df[target_col] = pd.factorize(df[target_col].astype(str))[0]\n", + "else:\n", + "\n", + " unique_vals = sorted(df[target_col].dropna().unique())\n", + " if not set(unique_vals).issubset({0,1}):\n", + " df[target_col] = (df[target_col] > 0).astype(int)\n", + "\n", + "drop_like = ['id', 'index', 'timestamp', 'time', 'date']\n", + "to_drop = [c for c in df.columns if any(k in c for k in drop_like) and c != target_col]\n", + "\n", + "features = [c for c in df.columns if c != target_col and c not in to_drop]\n", + "X = df[features].copy()\n", + "y = df[target_col].astype(int).copy()\n", + "\n", + "print(f\"Using {len(features)} features. Dropped: {to_drop}\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "EJPbV15eozRI", + "outputId": "227a4981-2587-41da-b28d-c0322463d996" + }, + "execution_count": 9, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Detected target column: fire_alarm\n", + "Using 14 features. Dropped: ['humidity']\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y\n", + ")\n", + "\n", + "imputer = SimpleImputer(strategy='median')\n", + "X_train_imp = imputer.fit_transform(X_train)\n", + "X_test_imp = imputer.transform(X_test)\n", + "\n", + "print(\"Train shape:\", X_train_imp.shape, \"Test shape:\", X_test_imp.shape)\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KIMvpRX1pAJX", + "outputId": "234d9497-2672-4f24-9cc1-03ed2226f22b" + }, + "execution_count": 10, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Train shape: (50104, 14) Test shape: (12526, 14)\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "lr_pipe = Pipeline([\n", + " ('scaler', StandardScaler()),\n", + " ('clf', LogisticRegression(max_iter=1000, solver='lbfgs', random_state=RANDOM_STATE))\n", + "])\n", + "\n", + "lr_pipe.fit(X_train_imp, y_train)\n", + "y_pred_lr = lr_pipe.predict(X_test_imp)\n", + "y_prob_lr = lr_pipe.predict_proba(X_test_imp)[:,1]\n", + "\n", + "metrics_lr = {\n", + " 'Accuracy': accuracy_score(y_test, y_pred_lr),\n", + " 'Precision': precision_score(y_test, y_pred_lr, zero_division=0),\n", + " 'Recall': recall_score(y_test, y_pred_lr, zero_division=0),\n", + " 'F1-Score': f1_score(y_test, y_pred_lr, zero_division=0)\n", + "}\n", + "print(\"=== Logistic Regression Metrics ===\")\n", + "for k,v in metrics_lr.items():\n", + " print(f\"{k}: {v:.4f}\")\n", + "\n", + "cm = confusion_matrix(y_test, y_pred_lr)\n", + "disp = ConfusionMatrixDisplay(confusion_matrix=cm)\n", + "fig = plt.figure()\n", + "disp.plot(values_format='d')\n", + "plt.title(\"Confusion Matrix - Logistic Regression\")\n", + "plt.show()\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 576 + }, + "id": "3pmc_dDJpIcD", + "outputId": "515694a2-7736-4db2-bb86-bf55c00f41b5" + }, + "execution_count": 11, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "=== Logistic Regression Metrics ===\n", + "Accuracy: 0.9796\n", + "Precision: 0.9895\n", + "Recall: 0.9818\n", + "F1-Score: 0.9856\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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\n" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "code", + "source": [ + "metrics_xgb = None\n", + "if XGB_AVAILABLE:\n", + " xgb = XGBClassifier(\n", + " n_estimators=200,\n", + " max_depth=5,\n", + " learning_rate=0.1,\n", + " subsample=0.9,\n", + " colsample_bytree=0.9,\n", + " reg_lambda=1.0,\n", + " tree_method=\"hist\",\n", + " random_state=RANDOM_STATE,\n", + " n_jobs=1,\n", + " eval_metric=\"logloss\"\n", + " )\n", + "\n", + " xgb.fit(X_train_imp, y_train)\n", + " y_pred_xgb = xgb.predict(X_test_imp)\n", + " y_prob_xgb = xgb.predict_proba(X_test_imp)[:,1]\n", + "\n", + " metrics_xgb = {\n", + " 'Accuracy': accuracy_score(y_test, y_pred_xgb),\n", + " 'Precision': precision_score(y_test, y_pred_xgb, zero_division=0),\n", + " 'Recall': recall_score(y_test, y_pred_xgb, zero_division=0),\n", + " 'F1-Score': f1_score(y_test, y_pred_xgb, zero_division=0)\n", + " }\n", + " print(\"=== XGBoost Metrics ===\")\n", + " for k,v in metrics_xgb.items():\n", + " print(f\"{k}: {v:.4f}\")\n", + "\n", + " cm2 = confusion_matrix(y_test, y_pred_xgb)\n", + " disp2 = ConfusionMatrixDisplay(confusion_matrix=cm2)\n", + " fig = plt.figure()\n", + " disp2.plot(values_format='d')\n", + " plt.title(\"Confusion Matrix - XGBoost\")\n", + " plt.show()\n", + "else:\n", + " print(\"⚠️ XGBoost not available. Skip bonus or install it in the first cell.\")\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 576 + }, + "id": "A52NSKR0pPxB", + "outputId": "ba1e721b-262f-48bd-ab7f-82e0023783a2" + }, + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "=== XGBoost Metrics ===\n", + "Accuracy: 0.9999\n", + "Precision: 1.0000\n", + "Recall: 0.9999\n", + "F1-Score: 0.9999\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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\n" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "code", + "source": [ + "rows = []\n", + "rows.append({'Model':'LogReg', **metrics_lr})\n", + "if metrics_xgb is not None:\n", + " rows.append({'Model':'XGBoost', **metrics_xgb})\n", + "\n", + "results_df = pd.DataFrame(rows).set_index('Model')\n", + "display(results_df.style.format(\"{:.4f}\"))\n", + "\n", + "best_model_by_recall = results_df['Recall'].idxmax()\n", + "print(f\"Highest Recall: {best_model_by_recall} ({results_df.loc[best_model_by_recall, 'Recall']:.4f})\")\n", + "\n", + "from IPython.display import Markdown, display as md_display\n", + "analysis = f'''\n", + "### Discussion\n", + "- **Recall** focuses on catching as many *actual fires* as possible.\n", + "- In fire detection, **False Negatives** (missed fires) are dangerous and must be minimized.\n", + "- **Logistic Regression**: simple, fast, and interpretable — a solid baseline.\n", + "- **XGBoost**: often achieves higher performance (including recall) but is more complex and heavier.\n", + "- Based on the table above, the model with higher recall here is **{best_model_by_recall}**.\n", + "'''\n", + "md_display(Markdown(analysis))\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 296 + }, + "id": "GtcNC9Tupcjp", + "outputId": "8251872e-a78a-4f38-caa8-974f4d1ca4a8" + }, + "execution_count": 14, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
 AccuracyPrecisionRecallF1-Score
Model    
LogReg0.97960.98950.98180.9856
XGBoost0.99991.00000.99990.9999
\n" + ] + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Highest Recall: XGBoost (0.9999)\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/markdown": "\n### Discussion\n- **Recall** focuses on catching as many *actual fires* as possible. \n- In fire detection, **False Negatives** (missed fires) are dangerous and must be minimized.\n- **Logistic Regression**: simple, fast, and interpretable — a solid baseline. \n- **XGBoost**: often achieves higher performance (including recall) but is more complex and heavier.\n- Based on the table above, the model with higher recall here is **XGBoost**.\n" + }, + "metadata": {} + } + ] + }, + { + "cell_type": "code", + "source": [ + "os.makedirs(\"outputs\", exist_ok=True)\n", + "results_path = os.path.join(\"outputs\", \"metrics_comparison.csv\")\n", + "results_df.to_csv(results_path)\n", + "print(\"Saved metrics to:\", results_path)\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "TKrq8PMSplgd", + "outputId": "64706526-7c37-43f7-f476-67a86fd835bd" + }, + "execution_count": 15, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Saved metrics to: outputs/metrics_comparison.csv\n" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/TP4/TP4_Submission.zip b/TP4/TP4_Submission.zip new file mode 100644 index 0000000..f05c25d Binary files /dev/null and b/TP4/TP4_Submission.zip differ diff --git a/TP4/ai_logic/bench_latency.py b/TP4/ai_logic/bench_latency.py new file mode 100644 index 0000000..6b1c1b3 --- /dev/null +++ b/TP4/ai_logic/bench_latency.py @@ -0,0 +1,29 @@ +import time, json, statistics, sys +import paho.mqtt.client as m +dev="esp-01" +lats=[] +def on(c,u,msg): + try: + j=json.loads(msg.payload.decode()) + if j.get("device_id")!=dev: return + t_ms=int(j.get("t_ms",0)) + if t_ms<=0: return + now=int(time.time()*1000) + lats.append(now - t_ms) + print(f"latency_ms={lats[-1]}", flush=True) + if len(lats)>=10: + c.disconnect() + except Exception as e: + print("err",e, file=sys.stderr) +c=m.Client() +c.on_message=on +c.connect("broker.mqtt.cool",1883,60) +c.subscribe("esp32/data",1) +c.loop_forever() +if lats: + avg=statistics.mean(lats) + p95=sorted(lats)[int(0.95*(len(lats)-1))] + open("report.md","a",encoding="utf-8").write(f"\\n\\n## Latency Results\\n- Samples: {len(lats)}\\n- Avg latency: {avg:.1f} ms\\n- P95 latency: {p95:.1f} ms\\n") + print("Appended latency stats to report.md") +else: + print("No samples captured") diff --git a/TP4/ai_logic/latency_sub.py b/TP4/ai_logic/latency_sub.py new file mode 100644 index 0000000..ca457f8 --- /dev/null +++ b/TP4/ai_logic/latency_sub.py @@ -0,0 +1,12 @@ +import json,time +import paho.mqtt.client as m +def on(c,u,msg): + j=json.loads(msg.payload.decode()) + now=int(time.time()*1000) + t_ms=int(j.get("t_ms",0)) + print(f"seq={j.get('seq')} latency_ms={now - t_ms}", flush=True) +c=m.Client() +c.on_message=on +c.connect("broker.mqtt.cool",1883,60) +c.subscribe("esp32/data",1) +c.loop_forever() diff --git a/TP4/ai_logic/mqtt_ai_subscriber.py b/TP4/ai_logic/mqtt_ai_subscriber.py new file mode 100644 index 0000000..5301f96 --- /dev/null +++ b/TP4/ai_logic/mqtt_ai_subscriber.py @@ -0,0 +1,136 @@ +# =========================================== +# File: TP4/ai_logic/mqtt_ai_subscriber.py +# Python MQTT subscriber that: +# - يستقبل JSON من "esp32/data" +# - يقرر prediction بسيط ويُنشر JSON إلى "esp32/control" مع qos=1 +# - جاهز للاستبدال بالـ ML pipeline لاحقاً +# =========================================== +import json +import time +import argparse +import logging +import os +import pickle + +import paho.mqtt.client as mqtt + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") +LOGGER = logging.getLogger("mqtt_ai_subscriber") + +BROKER = os.getenv("MQTT_BROKER", "broker.mqtt.cool") +PORT = int(os.getenv("MQTT_PORT", 1883)) +TOPIC_IN = "esp32/data" +TOPIC_OUT = "esp32/control" +MODELS_DIR = "models" + +def load_model_if_exists(name): + """تحميل النموذج المحفوظ إذا وُجد، وإرجاع None خلاف ذلك""" + path = os.path.join(MODELS_DIR, f"{name}_pipeline.pkl") + if os.path.isfile(path): + LOGGER.info("Loading model from: %s", path) + with open(path, "rb") as f: + m = pickle.load(f) + return m + LOGGER.info("Model file not found: %s", path) + return None + +def json_to_features(msg_json): + """حوّل JSON الوارد إلى متجه ميزات حسب TP2. + عدّلي هذه الدالة لتتناسب مع ميزاتك الحقيقية. + """ + try: + temp = float(msg_json.get("temperature")) + hum = float(msg_json.get("humidity")) + except Exception as e: + LOGGER.error("Missing or invalid feature fields: %s", e) + return None + return [[temp, hum]] + +def build_control_msg(device_id, model_name, pred, prob): + return { + "device_id": device_id, + "model": model_name, + "prediction": int(pred), + "probability": float(prob), + "timestamp": int(time.time()) + } + +class MQTTInferenceClient: + def __init__(self, model_name="lr"): + self.model_name = model_name + self.model = load_model_if_exists(model_name) + self.client = mqtt.Client() + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + # set reconnection delays + self.client.reconnect_delay_set(min_delay=1, max_delay=120) + + def on_connect(self, client, userdata, flags, rc): + LOGGER.info("Connected to broker %s:%s rc=%s", BROKER, PORT, rc) + client.subscribe(TOPIC_IN, qos=1) + + def on_message(self, client, userdata, msg): + try: + payload = msg.payload.decode() + j = json.loads(payload) + except Exception as e: + LOGGER.error("Invalid JSON received: %s", e) + return + LOGGER.info("Received on %s: %s", msg.topic, j) + + device_id = j.get("device_id") + if not device_id: + LOGGER.warning("Message missing device_id; ignoring") + return + + features = json_to_features(j) + if features is None: + LOGGER.warning("Feature extraction failed; ignoring") + return + + # If a real model exists, use it; otherwise use simple rule + if self.model is not None: + try: + import numpy as np + feats = np.array(features) + if hasattr(self.model, "predict_proba"): + prob = float(self.model.predict_proba(feats)[0][1]) + pred = int(self.model.predict(feats)[0]) + else: + pred = int(self.model.predict(feats)[0]) + prob = 1.0 + except Exception as e: + LOGGER.exception("Model inference failed, falling back to rule: %s", e) + # fallback rule + pred = 1 if features[0][0] > 30 else 0 + prob = 0.9 if pred == 1 else 0.1 + else: + # simple rule: temperature-based + pred = 1 if features[0][0] > 30 else 0 + prob = 0.95 if pred == 1 else 0.05 + + ctrl = build_control_msg(device_id, self.model_name, pred, prob) + payload_out = json.dumps(ctrl) + result = client.publish(TOPIC_OUT, payload_out, qos=1) + if result.rc != mqtt.MQTT_ERR_SUCCESS: + LOGGER.error("Publish failed with rc=%s", result.rc) + else: + LOGGER.info("Published control: %s", payload_out) + + def start(self): + while True: + try: + LOGGER.info("Connecting to broker %s:%s", BROKER, PORT) + self.client.connect(BROKER, PORT, keepalive=60) + self.client.loop_forever() + except Exception as e: + LOGGER.exception("MQTT connection error, retrying in 5s: %s", e) + time.sleep(5) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="MQTT AI subscriber") + parser.add_argument("--model", choices=["lr", "xgb"], default="lr", help="Which model to use") + args = parser.parse_args() + + cli = MQTTInferenceClient(model_name=args.model) + cli.start() diff --git a/TP4/models/lr_pipeline.pkl b/TP4/models/lr_pipeline.pkl new file mode 100644 index 0000000..6489460 Binary files /dev/null and b/TP4/models/lr_pipeline.pkl differ diff --git a/TP4/platformio.ini b/TP4/platformio.ini new file mode 100644 index 0000000..16f42ca --- /dev/null +++ b/TP4/platformio.ini @@ -0,0 +1,12 @@ +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 + +lib_deps = + knolleary/PubSubClient@^2.8 + adafruit/DHT sensor library@^1.4.4 + adafruit/Adafruit Unified Sensor@^1.1.14 + marcoschwartz/LiquidCrystal_I2C@^1.1.4 + bblanchon/ArduinoJson@^6.21.3 diff --git a/TP4/report.md b/TP4/report.md new file mode 100644 index 0000000..a1ad169 --- /dev/null +++ b/TP4/report.md @@ -0,0 +1,33 @@ +# TP4 — Report + +## 1) Setup +- Device ID: esp-01 +- Broker: broker.mqtt.cool:1883 +- Topics: esp32/data, esp32/control, esp32/status/esp-01 +- QoS: publish=0 (ESP32) / subscribe=1 (control) +- Build time: 2025-11-12 00:40:20 + +## 2) Models +- lr_pipeline.pkl size: 1137 bytes +- Features used from ESP32: temperature, humidity (+ features[] placeholder) + +## 3) MQTT Flow +- ESP32 → esp32/data (JSON: device_id, temperature, humidity, seq, t_ms, features[], timestamp) +- Python subscriber → esp32/control (JSON: device_id, model, prediction, probability, timestamp) +- LED toggled based on prediction (1=ON, 0=OFF) + +## 4) Latency (to fill) +- Method: compare consecutive seq + timestamps in logs +- Sample E2E (ESP publish → control received): ____ ms +- Notes: public broker variability + +## 5) Robustness +- Reconnect/backoff, LWT online/offline, JSON validation + +## 6) Optional: HTTP/REST +- POST /infer {device_id, temperature, humidity, ...} → {prediction, probability} +- ESP32: WiFiClient/HTTPClient + +## 7) Conclusions +- Feasibility on ESP32, trade-offs MQTT vs REST +\n\n## Latency Results\n- Samples: 10\n- Avg latency: 1762905387579.1 ms\n- P95 latency: 1762905422916.0 ms\n \ No newline at end of file diff --git a/TP4/src/main.cpp b/TP4/src/main.cpp new file mode 100644 index 0000000..d646933 --- /dev/null +++ b/TP4/src/main.cpp @@ -0,0 +1,235 @@ +/* =========================================== + File: TP4/src/main.cpp + ESP32 firmware (MQTT publisher + subscriber) + - ينشر JSON على "esp32/data" + - يشترك على "esp32/control" ويتعامل مع JSON تحكم + - يعلن الحالة على "esp32/status/" (online/offline) + =========================================== */ + +#include +#include +#include +#include +#include + +// ---------- Pins & Peripherals ---------- +#define DHTPIN 15 +#define DHTTYPE DHT22 +#define LED_PIN 2 +#define LCD_ADDR 0x27 // غيّريها إلى 0x3F إذا الشاشة سوداء + +// ---------- WiFi ---------- +const char* ssid = "Wokwi-GUEST"; +const char* password = ""; + +// ---------- MQTT ---------- +const char* mqtt_server = "broker.mqtt.cool"; +const int mqtt_port = 1883; + +#define MQTT_TOPIC_OUT "esp32/data" +#define MQTT_TOPIC_IN "esp32/control" + +WiFiClient espClient; +PubSubClient client(espClient); +DHT dht(DHTPIN, DHTTYPE); +LiquidCrystal_I2C lcd(LCD_ADDR, 16, 2); + +String currentCommand = "---"; +const char* device_id = "esp-01"; + +// عدّاد تسلسلي للرسائل +static uint32_t SEQ = 0; + +// payload التجريبي ذو 12 ميزة (نحدّث أول ميزتين من DHT) +const int N_FEATURES = 12; +float X[N_FEATURES] = {20.0, 57.36, 0, 400, 12306, 18520, 939.735, 0.0, 0.0, 0.0, 0.0, 0.0}; + +// ---------- Helpers ---------- +String statusTopic() { + return String("esp32/status/") + device_id; +} + +void publishStatus(const char* state) { + // retained=true حتى يعرف المشتركون الحالة فور الاشتراك + String msg = String("{\"device_id\":\"") + device_id + "\",\"status\":\"" + state + "\"}"; + client.publish(statusTopic().c_str(), msg.c_str(), true); +} + +void setup_wifi() { + WiFi.mode(WIFI_STA); + Serial.print("Connecting to WiFi"); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\nWiFi connected! IP: " + WiFi.localIP().toString()); +} + +void handleControlJson(const String& msg) { + StaticJsonDocument<256> doc; + DeserializationError err = deserializeJson(doc, msg); + if (err) { + // دعم نص بسيط ON/OFF + if (msg.equalsIgnoreCase("ON")) { + digitalWrite(LED_PIN, HIGH); + currentCommand = "ON"; + } else if (msg.equalsIgnoreCase("OFF")) { + digitalWrite(LED_PIN, LOW); + currentCommand = "OFF"; + } + lcd.setCursor(0, 1); + lcd.print("CMD:"); + lcd.print(currentCommand); + lcd.print(" "); + return; + } + + const char* incoming_id = doc["device_id"] | ""; + if (String(incoming_id) != String(device_id)) { + Serial.println("Control not for this device (device_id mismatch). Ignoring."); + return; + } + + const char* model = doc["model"] | ""; + int prediction = doc["prediction"] | -1; + float probability = doc["probability"] | 0.0; + + Serial.printf("model=%s prediction=%d prob=%.2f\n", model, prediction, probability); + + if (prediction == 1) { + digitalWrite(LED_PIN, HIGH); + currentCommand = "ON"; + } else if (prediction == 0) { + digitalWrite(LED_PIN, LOW); + currentCommand = "OFF"; + } else { + currentCommand = "NONE"; + } + + lcd.setCursor(0, 1); + lcd.print("CMD:"); + lcd.print(currentCommand); + lcd.print(" "); +} + +void callback(char* topic, byte* message, unsigned int length) { + String msg; + msg.reserve(length); + for (unsigned int i = 0; i < length; i++) msg += (char)message[i]; + msg.trim(); + + Serial.print("Received on "); + Serial.print(topic); + Serial.print(": "); + Serial.println(msg); + + handleControlJson(msg); +} + +void reconnect() { + while (!client.connected()) { + Serial.print("Attempting MQTT connection... "); + String clientId = "ESP32Client-"; + clientId += String((uint32_t)esp_random(), HEX); + + // إعداد LWT (offline) + String willMsg = String("{\"device_id\":\"") + device_id + "\",\"status\":\"offline\"}"; + String willTop = statusTopic(); + + // connect(clientId, willTopic, willQos, willRetain, willMessage) + if (client.connect(clientId.c_str(), willTop.c_str(), 1, true, willMsg.c_str())) { + Serial.println("connected"); + client.subscribe(MQTT_TOPIC_IN, 1); // QoS=1 لاستقبال التحكم + publishStatus("online"); // أعلن أننا Online + } else { + Serial.print("failed, rc="); + Serial.print(client.state()); + Serial.println(" retrying in 5s"); + delay(5000); + } + } +} + +void publishSensor(float t, float h, unsigned long now_ms) { + // تحديث الميزات + X[0] = t; + X[1] = h; + + // تحديث شاشة LCD + lcd.setCursor(0, 0); + lcd.print("T:"); + lcd.print(t, 1); + lcd.print("C H:"); + lcd.print(h, 0); + lcd.print("% "); + + lcd.setCursor(0, 1); + lcd.print("CMD:"); + lcd.print(currentCommand); + lcd.print(" "); + + // بناء JSON + DynamicJsonDocument outDoc(512); + outDoc["device_id"] = device_id; + outDoc["temperature"] = t; + outDoc["humidity"] = h; + outDoc["seq"] = (uint32_t)SEQ++; // عداد الرسائل + outDoc["t_ms"] = (uint32_t)now_ms; // وقت الإرسال بالمللي على ESP32 + JsonArray arr = outDoc.createNestedArray("features"); + for (int i = 0; i < N_FEATURES; i++) arr.add(X[i]); + outDoc["timestamp"] = (unsigned long)(now_ms / 1000); // seconds since boot + + // انشر (نستخدم String لتفادي مشاكل حجم البفر، ورفعنا بافر MQTT إلى 512) + String payload; + serializeJson(outDoc, payload); + bool ok = client.publish(MQTT_TOPIC_OUT, payload.c_str()); + if (ok) { + Serial.print("Published: "); + Serial.println(payload); + } else { + Serial.println("Publish failed"); + } +} + +// ---------- Arduino entry points ---------- +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + + lcd.init(); + lcd.backlight(); + lcd.clear(); + lcd.print("Starting..."); + dht.begin(); + + setup_wifi(); + + client.setServer(mqtt_server, mqtt_port); + client.setCallback(callback); + client.setBufferSize(512); // مهم: حتى لا يُقص JSON + client.setKeepAlive(30); + client.setSocketTimeout(5); +} + +unsigned long lastMsg = 0; +const unsigned long interval = 3000; // كل 3 ثوانٍ + +void loop() { + if (!client.connected()) reconnect(); + client.loop(); + + unsigned long now = millis(); + if (now - lastMsg > interval) { + lastMsg = now; + + float h = dht.readHumidity(); + float t = dht.readTemperature(); + if (isnan(h) || isnan(t)) { + Serial.println("Failed reading from DHT sensor!"); + return; + } + + publishSensor(t, h, now); + } +} diff --git a/TP4/wokwi.toml b/TP4/wokwi.toml new file mode 100644 index 0000000..2a728e5 --- /dev/null +++ b/TP4/wokwi.toml @@ -0,0 +1,9 @@ +[wokwi] +version = 1 +firmware = ".pio/build/esp32dev/firmware.bin" +elf = ".pio/build/esp32dev/firmware.elf" + +[connections.phantomio] +# Enable PhantomIO for serial and telemetry +enabled = true +port = "serial" diff --git a/TP_2_AIoT.ipynb b/TP_2_AIoT.ipynb new file mode 100644 index 0000000..45e4f44 --- /dev/null +++ b/TP_2_AIoT.ipynb @@ -0,0 +1,427 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "v45v2zeZdZDd" + }, + "outputs": [], + "source": [ + "import os, pickle, warnings, sys\n", + "from time import perf_counter\n", + "import numpy as np\n", + "import pandas as pd\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.metrics import accuracy_score, f1_score\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "code", + "source": [ + "SEED = 42\n", + "USE_DEMO_DATA = True # ← لو لديك X_train... غيّرها إلى False واستخدم قسم \"YOUR DATA\" بالأسفل\n", + "SAVE_DIR = \"./outputs\"\n", + "os.makedirs(SAVE_DIR, exist_ok=True)\n" + ], + "metadata": { + "id": "VlrayDxyd16T" + }, + "execution_count": 2, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def load_demo_data(test_size=0.2, random_state=SEED):\n", + " from sklearn.datasets import load_breast_cancer\n", + " from sklearn.model_selection import train_test_split\n", + " X, y = load_breast_cancer(return_X_y=True, as_frame=True)\n", + " X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=test_size, random_state=random_state, stratify=y\n", + " )\n", + " return X_train, X_test, y_train, y_test\n" + ], + "metadata": { + "id": "1M-22Pr2d4ch" + }, + "execution_count": 3, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def load_your_data():\n", + " # مثال (عدّل حسب بياناتك):\n", + " # return X_train, X_test, y_train, y_test\n", + " return None\n", + "\n", + "if USE_DEMO_DATA:\n", + " X_train, X_test, y_train, y_test = load_demo_data()\n", + "else:\n", + " maybe = load_your_data()\n", + " if maybe is None:\n", + " raise ValueError(\"رجاءً زوّد X_train, X_test, y_train, y_test في load_your_data().\")\n" + ], + "metadata": { + "id": "sZvT6v8Rd6le" + }, + "execution_count": 4, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def build_lr_pipeline():\n", + " return Pipeline([\n", + " (\"scaler\", StandardScaler()),\n", + " (\"clf\", LogisticRegression(max_iter=1000, solver=\"lbfgs\", random_state=SEED))\n", + " ])\n", + "\n", + "def build_xgb_pipeline():\n", + " try:\n", + " from xgboost import XGBClassifier\n", + " except Exception as e:\n", + " print(\"⚠️ XGBoost غير مثبت. ثبّت الحزمة ثم أعد التشغيل: pip install xgboost\")\n", + " return None\n", + " return Pipeline([\n", + " (\"scaler\", StandardScaler()), # ثابت للتوحيد مع الـ MLOps\n", + " (\"clf\", XGBClassifier(\n", + " n_estimators=100,\n", + " max_depth=4,\n", + " learning_rate=0.1,\n", + " subsample=0.8,\n", + " colsample_bytree=0.8,\n", + " reg_lambda=1.0,\n", + " tree_method=\"hist\", # أسرع/أخف عادة\n", + " n_jobs=1,\n", + " random_state=SEED,\n", + " eval_metric=\"logloss\",\n", + " ))\n", + " ])\n" + ], + "metadata": { + "id": "67420LsRd9jQ" + }, + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def train_and_eval(pipe, name, X_train, y_train, X_test, y_test):\n", + " t0 = perf_counter()\n", + " pipe.fit(X_train, y_train)\n", + " train_time_s = perf_counter() - t0\n", + "\n", + " y_pred = pipe.predict(X_test)\n", + " acc = accuracy_score(y_test, y_pred)\n", + " f1 = f1_score(y_test, y_pred, average=\"weighted\")\n", + "\n", + " return {\n", + " \"Model\": name,\n", + " \"Accuracy\": acc,\n", + " \"F1\": f1,\n", + " \"Train_Time_s\": train_time_s\n", + " }" + ], + "metadata": { + "id": "5iuNDdHbeAwF" + }, + "execution_count": 6, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def measure_model_size(pipe, out_path):\n", + " b = pickle.dumps(pipe, protocol=pickle.HIGHEST_PROTOCOL)\n", + " with open(out_path, \"wb\") as f:\n", + " f.write(b)\n", + " size_kb = len(b) / 1024.0\n", + " return size_kb\n" + ], + "metadata": { + "id": "pNVsNYbReFAf" + }, + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def measure_inference_time(pipe, X_test):\n", + " t0 = perf_counter()\n", + " _ = pipe.predict(X_test)\n", + " total_s = perf_counter() - t0\n", + " single_ms = (total_s / len(X_test)) * 1000.0\n", + " return total_s, single_ms\n" + ], + "metadata": { + "id": "htN0rujzeG9y" + }, + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "results_metrics = []\n", + "results_sizes = []\n", + "results_timing = []\n" + ], + "metadata": { + "id": "DmBo7_sQeIyG" + }, + "execution_count": 9, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "lr_pipe = build_lr_pipeline()\n", + "lr_metrics = train_and_eval(lr_pipe, \"LR-Pipeline\", X_train, y_train, X_test, y_test)\n", + "lr_size_kb = measure_model_size(lr_pipe, os.path.join(SAVE_DIR, \"lr_pipeline.pkl\"))\n", + "lr_inf_total_s, lr_inf_single_ms = measure_inference_time(lr_pipe, X_test)\n", + "\n", + "results_metrics.append(lr_metrics)\n", + "results_sizes.append({\"Model\": \"LR-Pipeline\", \"Size_KB\": lr_size_kb})\n", + "results_timing.append({\n", + " \"Model\": \"LR-Pipeline\",\n", + " \"Total_Test_Inference_Time_s\": lr_inf_total_s,\n", + " \"Single_Inference_Time_ms\": lr_inf_single_ms\n", + "})" + ], + "metadata": { + "id": "nUmbLXH2eKvV" + }, + "execution_count": 10, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "xgb_pipe = build_xgb_pipeline()\n", + "if xgb_pipe is not None:\n", + " xgb_metrics = train_and_eval(xgb_pipe, \"XGB-Pipeline\", X_train, y_train, X_test, y_test)\n", + " xgb_size_kb = measure_model_size(xgb_pipe, os.path.join(SAVE_DIR, \"xgb_pipeline.pkl\"))\n", + " xgb_inf_total_s, xgb_inf_single_ms = measure_inference_time(xgb_pipe, X_test)\n", + "\n", + " results_metrics.append(xgb_metrics)\n", + " results_sizes.append({\"Model\": \"XGB-Pipeline\", \"Size_KB\": xgb_size_kb})\n", + " results_timing.append({\n", + " \"Model\": \"XGB-Pipeline\",\n", + " \"Total_Test_Inference_Time_s\": xgb_inf_total_s,\n", + " \"Single_Inference_Time_ms\": xgb_inf_single_ms\n", + " })\n" + ], + "metadata": { + "id": "komGY7XheOTR" + }, + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "metrics_df = pd.DataFrame(results_metrics).set_index(\"Model\")\n", + "sizes_df = pd.DataFrame(results_sizes).set_index(\"Model\")\n", + "timing_df = pd.DataFrame(results_timing).set_index(\"Model\")\n", + "\n", + "metrics_path = os.path.join(SAVE_DIR, \"metrics.csv\")\n", + "sizes_path = os.path.join(SAVE_DIR, \"model_sizes.csv\")\n", + "timing_path = os.path.join(SAVE_DIR, \"inference_times.csv\")\n", + "\n", + "metrics_df.to_csv(metrics_path)\n", + "sizes_df.to_csv(sizes_path)\n", + "timing_df.to_csv(timing_path)\n", + "\n", + "print(\"\\n===== Task 1: Scores =====\")\n", + "print(metrics_df.round(4))\n", + "print(\"\\n===== Task 2.1: Model Sizes (KB) =====\")\n", + "print(sizes_df.round(2))\n", + "print(\"\\n===== Task 2.2: Inference Times =====\")\n", + "print(timing_df.round(6))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "jCafju_jeQ1X", + "outputId": "235db039-03f5-401c-c6d9-64c0b408d4bf" + }, + "execution_count": 12, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "===== Task 1: Scores =====\n", + " Accuracy F1 Train_Time_s\n", + "Model \n", + "LR-Pipeline 0.9825 0.9825 0.0730\n", + "XGB-Pipeline 0.9561 0.9558 0.1675\n", + "\n", + "===== Task 2.1: Model Sizes (KB) =====\n", + " Size_KB\n", + "Model \n", + "LR-Pipeline 2.61\n", + "XGB-Pipeline 104.52\n", + "\n", + "===== Task 2.2: Inference Times =====\n", + " Total_Test_Inference_Time_s Single_Inference_Time_ms\n", + "Model \n", + "LR-Pipeline 0.012247 0.107430\n", + "XGB-Pipeline 0.009566 0.083915\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Task 2.3: ESP32 Analysis (auto paragraph)" + ], + "metadata": { + "id": "gmYQlVL_eaem" + } + }, + { + "cell_type": "code", + "metadata": { + "id": "62a33869" + }, + "source": [ + "def esp32_analysis_text(sizes_df, timing_df):\n", + " # مواصفات تقريبية: RAM ~ 520 KB، Flash بعدة ميغابايت (حسب اللوحة)\n", + " SRAM_KB = 520.0\n", + " ONE_SEC_BUDGET_MS = 1000.0 # مثال: استدلال كل ثانية\n", + "\n", + " def row_or_none(df, name):\n", + " return df.loc[name] if name in df.index else None\n", + "\n", + " lr_s = row_or_none(sizes_df, \"LR-Pipeline\")\n", + " xg_s = row_or_none(sizes_df, \"XGB-Pipeline\")\n", + " lr_t = row_or_none(timing_df, \"LR-Pipeline\")\n", + " xg_t = row_or_none(timing_df, \"XGB-Pipeline\")\n", + "\n", + " lines = []\n", + " lines.append(\"تحليل الجاهزية للنشر على ESP32:\\n\")\n", + "\n", + " # الذاكرة\n", + " lines.append(\"1) الملاءمة الذاكرية:\")\n", + " if lr_s is not None:\n", + " lines.append(f\" • LR: حجم النموذج ≈ {lr_s['Size_KB']:.1f} KB (يُخزَّن عادةً في الـFlash ويُحمَّل جزئيًا/كاملًا إلى الـSRAM وقت الاستدلال).\")\n", + " if xg_s is not None:\n", + " lines.append(f\" • XGB: حجم النموذج ≈ {xg_s['Size_KB']:.1f} KB.\")\n", + " lines.append(f\" - قياس مرجعي: ESP32 يملك نحو {SRAM_KB:.0f} KB SRAM إجمالي (والحيّز المتاح للتطبيق أقل من ذلك).\")\n", + " if (lr_s is not None) and (xg_s is not None):\n", + " more_constrained = \"XGB-Pipeline\" if xg_s['Size_KB'] > lr_s['Size_KB'] else \"LR-Pipeline\"\n", + " lines.append(f\" ⇒ الأكثر تقييدًا من ناحية الحجم هو: {more_constrained}.\")\n", + " lines.append(\"\")\n", + "\n", + " # الزمن\n", + " lines.append(\"2) الكفاءة الزمنية (استدلال آنٍ كل 1 ثانية كمثال):\")\n", + " if lr_t is not None:\n", + " lines.append(f\" • LR: زمن الاستدلال المفرد ≈ {lr_t['Single_Inference_Time_ms']:.3f} ms.\")\n", + " if xg_t is not None:\n", + " lines.append(f\" • XGB: زمن الاستدلال المفرد ≈ {xg_t['Single_Inference_Time_ms']:.3f} ms.\")\n", + " lines.append(f\" - بميزانية ~{ONE_SEC_BUDGET_MS:.0f} ms/ث، كلاهما عادةً مقبول إذا كان الزمن بالملّي ثوانٍ أحادية أو عشرات قليلة.\")\n", + " lines.append(\"\")\n", + "\n", + " # الخلاصة + تحسينات\n", + " choice = None\n", + " if (lr_s is not None) and (xg_s is not None) and (lr_t is not None) and (xg_t is not None):\n", + " # قرار بسيط: اختر الأصغر والأسرع\n", + " score_lr = lr_s['Size_KB'] * 0.5 + lr_t['Single_Inference_Time_ms'] * 0.5\n", + " score_xgb = xg_s['Size_KB'] * 0.5 + xg_t['Single_Inference_Time_ms'] * 0.5\n", + " choice = \"LR-Pipeline\" if score_lr <= score_xgb else \"XGB-Pipeline\"\n", + " elif lr_s is not None and lr_t is not None:\n", + " choice = \"LR-Pipeline\"\n", + "\n", + " lines.append(\"3) الخلاصة:\")\n", + " if choice is not None:\n", + " lines.append(f\" • الخيار الأنسب على أساس الكفاءة وقيود ESP32: **{choice}**.\")\n", + " else:\n", + " lines.append(\" • تعذّر الحسم لغياب قياسات أحد النموذجين.\")\n", + "\n", + " lines.append(\" • لو احتجت لنشر النموذج الأقل كفاءة: استخدم تقنيات التحسين مثل: تقليل الميزات، التكميم 8-bit أو fixed-point، تقليل عدد الأشجار/العمق (لـXGB)، التحويل إلى C/MCU عبر m2cgen أو Treelite، واستخدام استدلال أحادي الخيط، مع إدارة ذاكرة صارمة.\")\n", + " return \"\\n\".join(lines)" + ], + "execution_count": 31, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "print(\"\\n===== Task 2.3: ESP32 Analysis =====\")\n", + "print(esp32_analysis_text(sizes_df, timing_df))\n", + "\n", + "print(f\"\\n📁 Saved to: {os.path.abspath(SAVE_DIR)}\")\n", + "print(\" - metrics.csv (الدقة و F1 وزمن التدريب)\")\n", + "print(\" - model_sizes.csv (أحجام النماذج بالـKB)\")\n", + "print(\" - inference_times.csv (زمن الاستدلال الكلّي والمفرد)\")\n", + "print(\" - lr_pipeline.pkl / xgb_pipeline.pkl\")" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "veQJ5tnxkCSS", + "outputId": "06467f04-7047-4a5d-f491-b3d85c2c8f85" + }, + "execution_count": 32, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "===== Task 2.3: ESP32 Analysis =====\n", + "تحليل الجاهزية للنشر على ESP32:\n", + "\n", + "1) الملاءمة الذاكرية:\n", + " • LR: حجم النموذج ≈ 2.6 KB (يُخزَّن عادةً في الـFlash ويُحمَّل جزئيًا/كاملًا إلى الـSRAM وقت الاستدلال).\n", + " • XGB: حجم النموذج ≈ 104.5 KB.\n", + " - قياس مرجعي: ESP32 يملك نحو 520 KB SRAM إجمالي (والحيّز المتاح للتطبيق أقل من ذلك).\n", + " ⇒ الأكثر تقييدًا من ناحية الحجم هو: XGB-Pipeline.\n", + "\n", + "2) الكفاءة الزمنية (استدلال آنٍ كل 1 ثانية كمثال):\n", + " • LR: زمن الاستدلال المفرد ≈ 0.107 ms.\n", + " • XGB: زمن الاستدلال المفرد ≈ 0.084 ms.\n", + " - بميزانية ~1000 ms/ث، كلاهما عادةً مقبول إذا كان الزمن بالملّي ثوانٍ أحادية أو عشرات قليلة.\n", + "\n", + "3) الخلاصة:\n", + " • الخيار الأنسب على أساس الكفاءة وقيود ESP32: **LR-Pipeline**.\n", + " • لو احتجت لنشر النموذج الأقل كفاءة: استخدم تقنيات التحسين مثل: تقليل الميزات، التكميم 8-bit أو fixed-point، تقليل عدد الأشجار/العمق (لـXGB)، التحويل إلى C/MCU عبر m2cgen أو Treelite، واستخدام استدلال أحادي الخيط، مع إدارة ذاكرة صارمة.\n", + "\n", + "📁 Saved to: /content/outputs\n", + " - metrics.csv (الدقة و F1 وزمن التدريب)\n", + " - model_sizes.csv (أحجام النماذج بالـKB)\n", + " - inference_times.csv (زمن الاستدلال الكلّي والمفرد)\n", + " - lr_pipeline.pkl / xgb_pipeline.pkl\n" + ] + } + ] + } + ] +} \ No newline at end of file