{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Εργαστήριο – Διάλεξη 3: Γραμμικά μοντέλα και βελτιστοποίηση\n",
    "### Μηχανική Μάθηση και Εξόρυξη Γνώσης · ΗΜΜΥ ΕΛΜΕΠΑ · 9ο εξάμηνο\n",
    "Διδάσκων: Λευτέρης Κουμάκης\n",
    "\n",
    "---\n",
    "\n",
    "**Τι θα κάνουμε σήμερα**\n",
    "\n",
    "| Μέρος | Θέμα |\n",
    "|---|---|\n",
    "| Α | Γραμμική παλινδρόμηση: κανονική εξίσωση και R² |\n",
    "| Β | Gradient descent από το μηδέν: ρυθμός μάθησης, κλιμάκωση, SGD |\n",
    "| Γ | Πολυωνυμική παλινδρόμηση και καμπύλες μάθησης |\n",
    "| Δ | Ομαλοποίηση: Ridge, Lasso, early stopping |\n",
    "| Ε | Λογιστική και softmax παλινδρόμηση |\n",
    "\n",
    "Τα παραδείγματα αναπαράγουν με κώδικα τους αριθμούς των διαφανειών της Διάλεξης 3.\n",
    "Εκτελούμε κάθε κελί με **Shift + Enter**, με τη σειρά. Τα κελιά **✏️ Δοκιμάστε** σας ζητούν να αλλάξετε κάτι και να ξανατρέξετε.\n",
    "Στο τέλος υπάρχουν **ασκήσεις για το σπίτι**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-001",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Βασικές βιβλιοθήκες\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "np.random.seed(42)                 # αναπαραγώγιμα αποτελέσματα\n",
    "np.set_printoptions(precision=3, suppress=True)   # 3 δεκαδικά, χωρίς επιστημονική μορφή"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "---\n",
    "# Μέρος Α: Γραμμική παλινδρόμηση\n",
    "\n",
    "## Α1. Η κανονική εξίσωση με το χέρι\n",
    "Τα δεδομένα της διαφάνειας: (1, 2), (2, 3), (3, 5). Υπολογίζουμε θ̂ = (XᵀX)⁻¹ Xᵀ y βήμα βήμα."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.array([1., 2., 3.])\n",
    "y = np.array([2., 3., 5.])\n",
    "\n",
    "# Προσθέτουμε τη στήλη x0 = 1 για τον σταθερό όρο θ0\n",
    "X_b = np.c_[np.ones(len(x)), x]          # np.c_ ενώνει στήλες → πίνακας 3 × 2\n",
    "print(\"X_b =\\n\", X_b)\n",
    "\n",
    "XtX = X_b.T @ X_b                         # '@' = γινόμενο πινάκων\n",
    "print(\"XᵀX =\\n\", XtX)                    # [[3, 6], [6, 14]]\n",
    "print(\"det(XᵀX) =\", np.linalg.det(XtX).round(3))          # 3·14 − 6·6 = 6\n",
    "print(\"(XᵀX)⁻¹ =\\n\", np.linalg.inv(XtX))\n",
    "print(\"Xᵀy =\", X_b.T @ y)                                  # [10, 23]\n",
    "\n",
    "theta = np.linalg.inv(XtX) @ X_b.T @ y\n",
    "print(\"θ̂ =\", theta)                                         # [0.333, 1.5]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-004",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Στην πράξη ΔΕΝ αντιστρέφουμε πίνακες: το LinearRegression χρησιμοποιεί SVD (πιο σταθερό αριθμητικά)\n",
    "from sklearn.linear_model import LinearRegression\n",
    "\n",
    "lin_reg = LinearRegression().fit(x.reshape(-1, 1), y)      # reshape: από (3,) σε (3, 1)\n",
    "print(\"θ0 =\", round(lin_reg.intercept_, 3), \" θ1 =\", lin_reg.coef_.round(3))\n",
    "\n",
    "# Εναλλακτικά: ψευδοαντίστροφος (λειτουργεί και όταν ο XᵀX δεν αντιστρέφεται)\n",
    "print(\"pinv:\", np.linalg.pinv(X_b) @ y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-005",
   "metadata": {},
   "source": [
    "## Α2. Ο συντελεστής προσδιορισμού R²"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-006",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import r2_score\n",
    "\n",
    "y_hat = X_b @ theta                          # προβλέψεις: 1.833, 3.333, 4.833\n",
    "sse = np.sum((y - y_hat) ** 2)               # άθροισμα τετραγώνων σφαλμάτων\n",
    "sst = np.sum((y - y.mean()) ** 2)            # συνολική διακύμανση γύρω από τη μέση τιμή\n",
    "print(f\"SSE = {sse:.3f},  SST = {sst:.3f}\")\n",
    "print(f\"R² = 1 − SSE/SST = {1 - sse / sst:.3f}\")     # ≈ 0.964\n",
    "print(f\"R² (scikit-learn) = {r2_score(y, y_hat):.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-007",
   "metadata": {},
   "source": [
    "✏️ **Δοκιμάστε:** αλλάξτε το τελευταίο y από 5 σε 9. Πώς αλλάζουν τα θ και το R²; Γιατί ένα μόνο σημείο επηρεάζει τόσο πολύ;"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "---\n",
    "# Μέρος Β: Gradient descent από το μηδέν\n",
    "\n",
    "## Β1. Ένα βήμα με το χέρι\n",
    "Κλίση του MSE: ∇MSE(θ) = (2/m)·Xᵀ(Xθ − y). Ξεκινάμε από θ = (0, 0) με η = 0,1, όπως στη διαφάνεια."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mse(theta, X, y):\n",
    "    '''Μέσο τετραγωνικό σφάλμα του γραμμικού μοντέλου Xθ.'''\n",
    "    return np.mean((X @ theta - y) ** 2)\n",
    "\n",
    "def gradient(theta, X, y):\n",
    "    '''Κλίση του MSE ως προς θ: (2/m)·Xᵀ(Xθ − y).'''\n",
    "    m = len(y)\n",
    "    return 2 / m * X.T @ (X @ theta - y)\n",
    "\n",
    "theta = np.zeros(2)                         # αρχή: θ0 = θ1 = 0\n",
    "print(\"MSE στην αρχή:\", round(mse(theta, X_b, y), 3))          # 12.667\n",
    "g = gradient(theta, X_b, y)\n",
    "print(\"Κλίση:\", g)                                               # [-6.667, -15.333]\n",
    "\n",
    "eta = 0.1                                    # ρυθμός μάθησης\n",
    "theta = theta - eta * g                      # ΤΟ βήμα του gradient descent\n",
    "print(\"Νέα θ:\", theta)                                           # [0.667, 1.533]\n",
    "print(\"MSE μετά από 1 βήμα:\", round(mse(theta, X_b, y), 3))     # 0.216"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "## Β2. Ο ρυθμός μάθησης"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "def batch_gd(X, y, eta, n_epochs):\n",
    "    '''Batch gradient descent: επιστρέφει τις τελικές θ και το ιστορικό του MSE.'''\n",
    "    theta = np.zeros(X.shape[1])\n",
    "    history = [mse(theta, X, y)]\n",
    "    for epoch in range(n_epochs):\n",
    "        theta = theta - eta * gradient(theta, X, y)\n",
    "        history.append(mse(theta, X, y))\n",
    "    return theta, history\n",
    "\n",
    "plt.figure(figsize=(7, 4))\n",
    "for eta in [0.02, 0.1, 0.25]:\n",
    "    theta_final, hist = batch_gd(X_b, y, eta, n_epochs=10)\n",
    "    print(f\"η = {eta}: MSE ανά εποχή = {np.round(hist[:5], 2)} ...  τελικό θ = {theta_final}\")\n",
    "    plt.plot(hist, \"o-\", label=f\"η = {eta}\")\n",
    "plt.yscale(\"log\"); plt.xlabel(\"Εποχή\"); plt.ylabel(\"MSE (λογ. κλίμακα)\"); plt.legend(); plt.grid(alpha=0.3)\n",
    "plt.show()\n",
    "# η = 0.25: το MSE ΑΥΞΑΝΕΤΑΙ σε κάθε εποχή → απόκλιση (το «πηδάμε» την κοιλάδα)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-012",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Με αρκετές εποχές και κατάλληλο η, το GD φτάνει στη λύση της κανονικής εξίσωσης\n",
    "theta_gd, hist = batch_gd(X_b, y, eta=0.1, n_epochs=2000)\n",
    "print(\"GD:              \", theta_gd)\n",
    "print(\"Κανονική εξίσωση:\", np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-013",
   "metadata": {},
   "source": [
    "## Β3. Γιατί η κλιμάκωση επιταχύνει το gradient descent\n",
    "Δύο χαρακτηριστικά με πολύ διαφορετικές κλίμακες (π.χ. εμβαδόν σε m² και δωμάτια). Μετράμε πόσες εποχές χρειάζεται το GD μέχρι να πλησιάσει το ελάχιστο."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-014",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "area = rng.uniform(40, 150, 200)                     # m²: τιμές ~100\n",
    "rooms = rng.integers(1, 6, 200).astype(float)        # δωμάτια: τιμές ~3\n",
    "price = 1.5 * area + 10 * rooms + rng.normal(0, 10, 200)\n",
    "\n",
    "def epochs_to_converge(X, y, max_epochs=100_000):\n",
    "    '''Εποχές μέχρι το MSE να φτάσει στο 1% του βέλτιστου.\n",
    "    Ρυθμός μάθησης η = 1/λ_max: ο μεγαλύτερος «ασφαλής» για το συγκεκριμένο πρόβλημα.'''\n",
    "    X_b = np.c_[np.ones(len(X)), X]\n",
    "    hessian = 2 / len(y) * X_b.T @ X_b                   # «καμπυλότητα» της συνάρτησης κόστους\n",
    "    eigenvalues = np.linalg.eigvalsh(hessian)\n",
    "    eta = 1 / eigenvalues.max()\n",
    "    best = mse(np.linalg.lstsq(X_b, y, rcond=None)[0], X_b, y)   # το ελάχιστο MSE\n",
    "    theta = np.zeros(X_b.shape[1])\n",
    "    for epoch in range(max_epochs):\n",
    "        if mse(theta, X_b, y) <= 1.01 * best:\n",
    "            break\n",
    "        theta -= eta * gradient(theta, X_b, y)\n",
    "    # Δείκτης κατάστασης: πόσο «επιμηκυμένο» είναι το μπολ (1 = τέλεια στρογγυλό)\n",
    "    return epoch, eta, eigenvalues.max() / eigenvalues.min()\n",
    "\n",
    "X_raw = np.c_[area, rooms]\n",
    "X_scaled = StandardScaler().fit_transform(X_raw)\n",
    "\n",
    "for name, X_ in [(\"Χωρίς κλιμάκωση\", X_raw), (\"Με κλιμάκωση   \", X_scaled)]:\n",
    "    epochs, eta, cond = epochs_to_converge(X_, price)\n",
    "    print(f\"{name}: η = {eta:.2e}, δείκτης κατάστασης = {cond:,.0f}, εποχές = {epochs:,}\")\n",
    "# Χωρίς κλιμάκωση: χιλιάδες εποχές. Με κλιμάκωση: λίγες. Ίδιο πρόβλημα, ίδια λύση!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-015",
   "metadata": {},
   "source": [
    "## Β4. Στοχαστικό και mini-batch gradient descent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-016",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import SGDRegressor\n",
    "\n",
    "# SGDRegressor: στοχαστικό GD (ένα δείγμα κάθε φορά) με πρόγραμμα ρυθμού μάθησης\n",
    "sgd = SGDRegressor(max_iter=1000, tol=1e-5, penalty=None, eta0=0.01, random_state=42)\n",
    "sgd.fit(X_scaled, price)\n",
    "lin = LinearRegression().fit(X_scaled, price)\n",
    "print(\"SGD:    θ0 =\", sgd.intercept_.round(2), \" θ =\", sgd.coef_.round(2))\n",
    "print(\"Ακριβής:θ0 =\", round(lin.intercept_, 2), \" θ =\", lin.coef_.round(2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-017",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Mini-batch GD από το μηδέν: κάθε βήμα με 20 τυχαία δείγματα\n",
    "X_b_s = np.c_[np.ones(len(X_scaled)), X_scaled]\n",
    "theta = np.zeros(3)\n",
    "eta, batch_size = 0.05, 20\n",
    "for epoch in range(50):\n",
    "    idx = rng.permutation(len(price))                     # ανακατεύουμε σε κάθε εποχή\n",
    "    for start in range(0, len(price), batch_size):\n",
    "        b = idx[start:start + batch_size]                 # δείκτες του mini-batch\n",
    "        theta -= eta * gradient(theta, X_b_s[b], price[b])\n",
    "print(\"Mini-batch GD:\", theta.round(2))\n",
    "print(\"Εποχή = 200/20 = 10 βήματα · σύνολο\", 50 * 10, \"βήματα\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-018",
   "metadata": {},
   "source": [
    "---\n",
    "# Μέρος Γ: Πολυωνυμική παλινδρόμηση και καμπύλες μάθησης\n",
    "Δεδομένα από y = 0,5x² + x + 2 + θόρυβος, όπως στη διαφάνεια."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-019",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import PolynomialFeatures\n",
    "\n",
    "rng = np.random.default_rng(3)\n",
    "X_q = 6 * rng.random((100, 1)) - 3                         # x στο [-3, 3]\n",
    "y_q = 0.5 * X_q[:, 0] ** 2 + X_q[:, 0] + 2 + rng.normal(0, 1, 100)\n",
    "\n",
    "poly = PolynomialFeatures(degree=2, include_bias=False)    # προσθέτει τη στήλη x²\n",
    "X_poly = poly.fit_transform(X_q)\n",
    "print(\"Πρώτο δείγμα: x =\", X_q[0], \"→ [x, x²] =\", X_poly[0])\n",
    "\n",
    "lin_q = LinearRegression().fit(X_poly, y_q)\n",
    "print(f\"Εκτίμηση: ŷ = {lin_q.coef_[1]:.2f}x² + {lin_q.coef_[0]:.2f}x + {lin_q.intercept_:.2f}\")\n",
    "print(\"Αληθινή:  y = 0.50x² + 1.00x + 2.00\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-020",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Πόσα χαρακτηριστικά φτιάχνει το PolynomialFeatures; (n+d)!/(d!·n!) μαζί με τον σταθερό όρο\n",
    "from math import comb\n",
    "for n_feat, d in [(2, 3), (10, 3), (100, 3)]:\n",
    "    n_out = PolynomialFeatures(degree=d).fit(np.zeros((1, n_feat))).n_output_features_\n",
    "    print(f\"n = {n_feat:3d}, d = {d}: {n_out:,} χαρακτηριστικά (τύπος: {comb(n_feat + d, d):,})\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-021",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import learning_curve\n",
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "def plot_learning_curve(model, title, ax):\n",
    "    '''Σφάλμα εκπαίδευσης και επικύρωσης (5-fold CV) για αυξανόμενο μέγεθος training set.'''\n",
    "    sizes, train_scores, valid_scores = learning_curve(\n",
    "        model, X_q, y_q, train_sizes=np.linspace(0.05, 1.0, 20), cv=5,\n",
    "        scoring=\"neg_root_mean_squared_error\")\n",
    "    ax.plot(sizes, -train_scores.mean(axis=1), \"o-\", label=\"Εκπαίδευση\")\n",
    "    ax.plot(sizes, -valid_scores.mean(axis=1), \"s-\", label=\"Επικύρωση\")\n",
    "    ax.set_ylim(0, 3.5); ax.set_title(title); ax.set_xlabel(\"Μέγεθος training set\"); ax.set_ylabel(\"RMSE\")\n",
    "    ax.legend(); ax.grid(alpha=0.3)\n",
    "\n",
    "fig, axs = plt.subplots(1, 2, figsize=(12, 4))\n",
    "plot_learning_curve(LinearRegression(), \"Γραμμικό: υποπροσαρμογή\", axs[0])\n",
    "plot_learning_curve(make_pipeline(PolynomialFeatures(10, include_bias=False), LinearRegression()),\n",
    "                    \"Πολυώνυμο βαθμού 10: υπερπροσαρμογή\", axs[1])\n",
    "plt.tight_layout(); plt.show()\n",
    "# Αριστερά: δύο καμπύλες ψηλά και κοντά → περισσότερα δεδομένα ΔΕΝ βοηθούν\n",
    "# Δεξιά: μεγάλο κενό → περισσότερα δεδομένα ή ομαλοποίηση βοηθούν"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-022",
   "metadata": {},
   "source": [
    "---\n",
    "# Μέρος Δ: Ομαλοποίηση\n",
    "\n",
    "## Δ1. Το α «μαζεύει» τον συντελεστή (Ridge)\n",
    "Για ένα χαρακτηριστικό χωρίς σταθερό όρο: θ = Σxy / (Σx² + α) = 23 / (14 + α)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-023",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import Ridge\n",
    "\n",
    "x1 = np.array([[1.], [2.], [3.]])\n",
    "y1 = np.array([2., 3., 5.])\n",
    "for alpha in [0, 1, 10, 100]:\n",
    "    by_formula = 23 / (14 + alpha)\n",
    "    ridge = Ridge(alpha=alpha, fit_intercept=False).fit(x1, y1)    # χωρίς σταθερό όρο, όπως στον τύπο\n",
    "    print(f\"α = {alpha:3d}: τύπος θ = {by_formula:.3f} · scikit-learn θ = {ridge.coef_[0]:.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-024",
   "metadata": {},
   "source": [
    "## Δ2. Ridge έναντι Lasso: ποιοι συντελεστές μηδενίζονται;\n",
    "8 χαρακτηριστικά, από τα οποία μόνο τα 4 είναι χρήσιμα (αληθινοί συντελεστές 4, −3, 2, 0, 0, 1.5, 0, 0)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-025",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import Lasso\n",
    "\n",
    "rng = np.random.default_rng(3)\n",
    "X8 = rng.normal(0, 1, (60, 8))\n",
    "true_beta = np.array([4, -3, 2, 0, 0, 1.5, 0, 0])\n",
    "y8 = X8 @ true_beta + rng.normal(0, 1.5, 60)\n",
    "\n",
    "alphas = np.logspace(-3, 1, 40)\n",
    "ridge_coefs = np.array([Ridge(alpha=a * 100).fit(X8, y8).coef_ for a in alphas])\n",
    "lasso_coefs = np.array([Lasso(alpha=a, max_iter=20000).fit(X8, y8).coef_ for a in alphas])\n",
    "\n",
    "fig, axs = plt.subplots(1, 2, figsize=(12, 4))\n",
    "for ax, coefs, name in [(axs[0], ridge_coefs, \"Ridge (α × 100)\"), (axs[1], lasso_coefs, \"Lasso\")]:\n",
    "    ax.plot(alphas, coefs); ax.set_xscale(\"log\"); ax.axhline(0, color=\"black\", lw=0.8)\n",
    "    ax.set_title(name); ax.set_xlabel(\"α\"); ax.set_ylabel(\"Συντελεστές\")\n",
    "plt.tight_layout(); plt.show()\n",
    "\n",
    "lasso = Lasso(alpha=0.3).fit(X8, y8)\n",
    "print(\"Lasso (α = 0.3):\", lasso.coef_.round(2))\n",
    "print(\"Μηδενικοί συντελεστές:\", np.sum(lasso.coef_ == 0), \"από 8\")   # τα άχρηστα χαρακτηριστικά"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-026",
   "metadata": {},
   "source": [
    "## Δ3. Επιλογή του α με cross-validation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-027",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import RidgeCV, LassoCV\n",
    "\n",
    "ridge_cv = RidgeCV(alphas=np.logspace(-2, 3, 30)).fit(X8, y8)\n",
    "lasso_cv = LassoCV(cv=5, random_state=42).fit(X8, y8)\n",
    "print(\"Καλύτερο α (Ridge):\", round(ridge_cv.alpha_, 3))\n",
    "print(\"Καλύτερο α (Lasso):\", round(lasso_cv.alpha_, 3), \"→ συντελεστές\", lasso_cv.coef_.round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-028",
   "metadata": {},
   "source": [
    "## Δ4. Early stopping\n",
    "Εκπαιδεύουμε ένα πολυώνυμο βαθμού 20 με SGD, μία εποχή κάθε φορά, και κρατάμε το μοντέλο με το μικρότερο σφάλμα επικύρωσης."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-029",
   "metadata": {},
   "outputs": [],
   "source": [
    "from copy import deepcopy\n",
    "from sklearn.metrics import mean_squared_error\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X_tr, X_va, y_tr, y_va = train_test_split(X_q, y_q, test_size=0.3, random_state=42)\n",
    "preprocessing = make_pipeline(PolynomialFeatures(degree=20, include_bias=False), StandardScaler())\n",
    "X_tr_p = preprocessing.fit_transform(X_tr)       # fit ΜΟΝΟ στο training set\n",
    "X_va_p = preprocessing.transform(X_va)\n",
    "\n",
    "# learning_rate=\"constant\": σταθερός ρυθμός μάθησης, για να φανεί καθαρά η υπερπροσαρμογή\n",
    "sgd = SGDRegressor(penalty=None, eta0=0.005, learning_rate=\"constant\", random_state=42)\n",
    "best_rmse, best_epoch, val_errors = float(\"inf\"), None, []\n",
    "for epoch in range(1000):\n",
    "    sgd.partial_fit(X_tr_p, y_tr)                 # μία εποχή\n",
    "    rmse = mean_squared_error(y_va, sgd.predict(X_va_p)) ** 0.5\n",
    "    val_errors.append(rmse)\n",
    "    if rmse < best_rmse:                          # κρατάμε ΑΝΤΙΓΡΑΦΟ του καλύτερου μοντέλου\n",
    "        best_rmse, best_epoch, best_model = rmse, epoch, deepcopy(sgd)\n",
    "\n",
    "plt.figure(figsize=(7, 4)); plt.plot(val_errors); plt.axvline(best_epoch, color=\"red\", ls=\"--\")\n",
    "plt.xlabel(\"Εποχή\"); plt.ylabel(\"RMSE επικύρωσης\"); plt.ylim(1.1, 1.6); plt.grid(alpha=0.3); plt.show()\n",
    "print(f\"Καλύτερη εποχή: {best_epoch}, RMSE επικύρωσης = {best_rmse:.3f}\")\n",
    "print(f\"Τελευταία εποχή: RMSE επικύρωσης = {val_errors[-1]:.3f}  → μετά το ελάχιστο, αρχίζει υπερπροσαρμογή\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-030",
   "metadata": {},
   "source": [
    "---\n",
    "# Μέρος Ε: Λογιστική και softmax παλινδρόμηση\n",
    "\n",
    "## Ε1. Η σιγμοειδής και το παράδειγμα των εξετάσεων\n",
    "p̂ = σ(−4 + 1,5 · ώρες διαβάσματος)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-031",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoid(t):\n",
    "    '''Λογιστική (σιγμοειδής) συνάρτηση.'''\n",
    "    return 1 / (1 + np.exp(-t))\n",
    "\n",
    "t = np.linspace(-6, 6, 200)\n",
    "plt.figure(figsize=(6, 3.5)); plt.plot(t, sigmoid(t)); plt.axhline(0.5, ls=\":\", color=\"gray\")\n",
    "plt.xlabel(\"t = θᵀx\"); plt.ylabel(\"σ(t)\"); plt.grid(alpha=0.3); plt.show()\n",
    "\n",
    "for hours in [2, 4 / 1.5, 4]:\n",
    "    logit = -4 + 1.5 * hours\n",
    "    print(f\"{hours:.2f} ώρες: t = {logit:+.2f} → p = {sigmoid(logit):.2f} → {'επιτυχία' if logit >= 0 else 'αποτυχία'}\")\n",
    "print(\"odds πολλαπλασιάζονται ανά ώρα επί e^1.5 =\", round(np.exp(1.5), 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-032",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Log loss: πόσο «πληρώνει» το μοντέλο για κάθε πρόβλεψη\n",
    "for y_true, p in [(1, 0.9), (1, 0.5), (1, 0.1), (0, 0.1)]:\n",
    "    loss = -(y_true * np.log(p) + (1 - y_true) * np.log(1 - p))\n",
    "    print(f\"y = {y_true}, p̂ = {p}: κόστος = {loss:.3f}\")\n",
    "# Σίγουρο ΚΑΙ λάθος (y=1, p̂=0.1) → πολύ μεγάλο κόστος"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-033",
   "metadata": {},
   "source": [
    "## Ε2. Λογιστική παλινδρόμηση στο dataset iris\n",
    "Ταξινομούμε αν ένα λουλούδι είναι *Iris virginica* από το πλάτος του πετάλου (Géron, Κεφ. 4)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-034",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_iris\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "iris = load_iris(as_frame=True)\n",
    "X_iris = iris.data[[\"petal width (cm)\"]].values\n",
    "y_iris = (iris.target == 2).values                   # True αν virginica\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-035",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "cell-036",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-037",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-038",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "cell-039",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
