{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Lab 6 - Semi-Supervised Classification 🏷️🦠\n",
    "### CIS 350: AI for Cybersecurity - Smart Defense for the Digital Business\n",
    "\n",
    "**Worth:** 50 points  ·  **Estimated time:** ~60 minutes  ·  **Submit:** this `.ipynb` in Canvas\n",
    "**Required software:** Google Colab (or Jupyter) with `scikit-learn`, `numpy`, `matplotlib` (preinstalled in Colab).\n",
    "**Prerequisites:** Weeks 3-7 - supervised learning, train/test split, and this week's semi-supervised idea.\n",
    "\n",
    "## Background\n",
    "Labeling malware takes scarce expert time, so a real team often has only a **few** labeled files but **millions** of unlabeled ones. **Semi-supervised learning** uses BOTH. In this lab you will hide almost all the labels, train a normal **supervised** model on the few that remain, then train a **semi-supervised** model that also uses the unlabeled files - and see how much the unlabeled data helps.\n",
    "\n",
    "## Learning objectives\n",
    "By the end you can: (1) explain semi-supervised learning; (2) simulate scarce labels with the `-1` convention; (3) train a supervised baseline and a semi-supervised model; (4) compare them and explain why unlabeled data helped.\n",
    "\n",
    "Run cells top to bottom with **Shift + Enter** and fill every **`TODO`** blank (`____`).\n",
    "\n",
    "> **Academic integrity:** individual or pair work. **AI tools are not permitted** on labs."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 1 · Meet the data\n",
    "Each file has two behavioral features. Benign (0) and malware (1) form two interleaving groups."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")   # keep output clean (harmless LabelSpreading notes)\n",
    "import numpy as np\n",
    "from sklearn.datasets import make_moons\n",
    "\n",
    "# 600 files, each described by two behavioral features (a 2-D \"fingerprint\").\n",
    "# Benign (0) and malware (1) form two interleaving groups - NOT linearly separable.\n",
    "X, y = make_moons(n_samples=600, noise=0.12, random_state=7)\n",
    "# y is the TRUE label for every file. In real life we would NOT have all of these -\n",
    "# we simulate that by hiding most of them below.\n",
    "print(\"files:\", X.shape[0], \" features:\", X.shape[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 2 · Train / test split\n",
    "We keep a test set to judge both models fairly on unseen files."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "# TODO 1: hold back 40% of files to test on (stratify keeps class balance).\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "        X, y, test_size=____, random_state=0, stratify=y)\n",
    "print(\"train:\", len(X_train), \" test:\", len(X_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 3 · Simulate scarce labels\n",
    "Reality check: we only have a FEW labels. Keep 10 (5 per class); mark the rest `-1` (unlabeled)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "rng = np.random.default_rng(1)\n",
    "benign_idx  = rng.permutation(np.where(y_train == 0)[0])[:5]\n",
    "malware_idx = rng.permutation(np.where(y_train == 1)[0])[:5]\n",
    "labeled = np.concatenate([benign_idx, malware_idx])\n",
    "\n",
    "# y_semi keeps real labels for the 10, and -1 (unlabeled) for everyone else.\n",
    "y_semi = np.full(len(y_train), -1)\n",
    "y_semi[labeled] = y_train[labeled]\n",
    "# TODO 2: how many files are still unlabeled (-1)?\n",
    "print(\"labeled:\", (y_semi != -1).sum(), \" unlabeled:\", (y_semi == ____).sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 4 · Supervised baseline (few labels only)\n",
    "A normal classifier can only use the 10 labeled files - it ignores the rest."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.metrics import accuracy_score\n",
    "# TODO 3: train kNN on ONLY the labeled rows.\n",
    "sup = KNeighborsClassifier(n_neighbors=3).fit(X_train[labeled], y_train[____])\n",
    "acc_sup = accuracy_score(y_test, sup.predict(X_test))\n",
    "print(\"supervised accuracy (10 labels):\", round(acc_sup, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 5 · Semi-supervised model (labels + unlabeled)\n",
    "LabelSpreading uses the whole `X_train` and the `-1` marks to learn from structure."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "from sklearn.semi_supervised import LabelSpreading\n",
    "# TODO 4: fit on the FULL training data and the y_semi labels (with -1s).\n",
    "ssl = LabelSpreading(kernel=\"knn\", n_neighbors=10).fit(____, y_semi)\n",
    "acc_ssl = accuracy_score(y_test, ssl.predict(X_test))\n",
    "print(\"semi-supervised accuracy:\", round(acc_ssl, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 6 · Compare\n",
    "How much did the unlabeled data help?"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# TODO 5: print the improvement (semi-supervised minus supervised).\n",
    "print(\"supervised:\", round(acc_sup, 3))\n",
    "print(\"semi-supervised:\", round(acc_ssl, 3))\n",
    "print(\"improvement:\", round(acc_ssl - ____, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 7 · See why (optional visualization)\n",
    "The unlabeled points reveal the two groups the 10 labels alone could not show."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "pred = ssl.transduction_   # the label the model gave each training file\n",
    "plt.scatter(X_train[:, 0], X_train[:, 1], c=pred, cmap=\"coolwarm\", s=12, alpha=0.6)\n",
    "plt.scatter(X_train[labeled, 0], X_train[labeled, 1], c=\"black\", s=60, marker=\"*\",\n",
    "            label=\"the 10 labeled files\")\n",
    "plt.legend(); plt.title(\"Labels spread to similar unlabeled files\"); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 8 · Reflection questions\n",
    "Double-click and replace each *your answer here*."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Your answers**\n",
    "\n",
    "1. **How many files were labeled vs unlabeled in training?** *your answer here*\n",
    "2. **What accuracy did the supervised (labels-only) model get? Why is it limited?** *your answer here*\n",
    "3. **What accuracy did the semi-supervised model get? How much did unlabeled data help?** *your answer here*\n",
    "4. **In your own words, HOW did the unlabeled data improve the model?** *your answer here*\n",
    "5. **Why is semi-supervised learning a good fit for real malware detection?** *your answer here*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ✅ Before you submit\n",
    "- [ ] **Runtime -> Run all** with no errors.  - [ ] All `TODO` blanks filled.\n",
    "- [ ] All 5 reflection questions answered.  - [ ] **File -> Download -> Download .ipynb**, upload to Canvas.\n",
    "\n",
    "## Troubleshooting\n",
    "- `NameError` -> you skipped a cell; run from the top with **Run all**.\n",
    "- Semi-supervised accuracy not higher -> confirm you passed `y_semi` (with `-1`s) to LabelSpreading, and the FULL `X_train` (labeled + unlabeled), not just the labeled rows.\n",
    "- A `RuntimeWarning` about divide -> harmless; we silenced it in the first cell.\n",
    "\n",
    "## Extension (optional, ungraded)\n",
    "Change `n_labeled` to 4, 20, or 40 and re-run. Plot supervised vs. semi-supervised accuracy as labels grow. When does the gap disappear?"
   ]
  }
 ]
}