{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 실습 07 · 거리 기반 학습 — kNN · K-means · 트리 · 프로토타입\n",
    "\n",
    "> **연계 강의자료:** M7 「거리 기반 학습과 퓨샷」 · 13주차\n",
    "> **목표:** kNN의 k 민감도, K-means의 엘보우, 의사결정트리의 정보이득을 sklearn으로 실험하고, 프로토타입(클래스 평균) 분류로 퓨샷의 원리를 재현한다.\n",
    ">\n",
    "> 기말 '시나리오 → 알고리즘 식별' 3문항(kNN/K-means/Prototypical)의 실습판.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. kNN — k가 답을 바꾼다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np, matplotlib.pyplot as plt\n",
    "from sklearn.datasets import make_blobs\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "\n",
    "X, y = make_blobs(n_samples=120, centers=[[2,4],[5,2]], cluster_std=1.3, random_state=7)\n",
    "\n",
    "fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n",
    "xx, yy = np.meshgrid(np.linspace(X[:,0].min()-1, X[:,0].max()+1, 200),\n",
    "                     np.linspace(X[:,1].min()-1, X[:,1].max()+1, 200))\n",
    "for ax, k in zip(axes, [1, 5, 25]):\n",
    "    clf = KNeighborsClassifier(n_neighbors=k).fit(X, y)\n",
    "    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)\n",
    "    ax.contourf(xx, yy, Z, alpha=.25, cmap=\"coolwarm\")\n",
    "    ax.scatter(X[:,0], X[:,1], c=y, s=18, cmap=\"coolwarm\", edgecolors=\"k\", linewidths=.3)\n",
    "    ax.set_title(f\"k = {k}\")\n",
    "plt.suptitle(\"k가 작으면 경계가 데이터 하나하나에 휘둘리고(과적합), 크면 뭉개진다(과소적합)\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 과제 1-1\n`weights=\"distance\"`(가중 투표, 1/d)로 바꾸면 k=25의 경계가 어떻게 달라지는가?\n단순 다수결 방식과 가중 투표의 차이를 강의자료 M7 실습 1과 연결해 설명하라."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. K-means와 엘보우 — 레이블 없이 묶기"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cluster import KMeans\n",
    "\n",
    "X2, _ = make_blobs(n_samples=180, centers=3, cluster_std=1.0, random_state=23)\n",
    "\n",
    "sse = []\n",
    "for k in range(1, 9):\n",
    "    km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X2)\n",
    "    sse.append(km.inertia_)          # inertia_ = SSE\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
    "km3 = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X2)\n",
    "axes[0].scatter(X2[:,0], X2[:,1], c=km3.labels_, s=18, cmap=\"viridis\")\n",
    "axes[0].scatter(*km3.cluster_centers_.T, marker=\"X\", s=200, c=\"red\")\n",
    "axes[0].set_title(\"k=3 군집과 중심(X)\")\n",
    "axes[1].plot(range(1, 9), sse, \"o-\")\n",
    "axes[1].set_xlabel(\"k\"); axes[1].set_ylabel(\"SSE\"); axes[1].set_title(\"엘보우 — 꺾이는 곳이 적정 k\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 과제 2-1\n`n_init=1`로 두고 `random_state`를 바꿔 가며 여러 번 돌려 보라. 최종 SSE가 달라지는 경우가 있는가?\nK-means가 초기값에 민감한 이유와 sklearn이 기본으로 `n_init`번 재시작하는 이유를 설명하라."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 의사결정트리 — 좋은 질문부터 (정보이득)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_iris\n",
    "from sklearn.tree import DecisionTreeClassifier, plot_tree\n",
    "\n",
    "iris = load_iris()\n",
    "tree = DecisionTreeClassifier(criterion=\"entropy\", max_depth=3, random_state=0)\n",
    "tree.fit(iris.data, iris.target)\n",
    "\n",
    "plt.figure(figsize=(13, 6))\n",
    "plot_tree(tree, feature_names=iris.feature_names, class_names=iris.target_names,\n",
    "          filled=True, fontsize=9)\n",
    "plt.title(\"criterion='entropy' → ID3 계열 (지니면 CART) — 각 노드의 entropy 감소가 정보이득\")\n",
    "plt.show()\n",
    "print(\"훈련 정확도:\", tree.score(iris.data, iris.target))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 과제 3-1\n",
    "`max_depth`를 없애고(무제한) 훈련 정확도를 보라. 1.0이 되는가? 그것이 왜 위험한가?\n",
    "(M5의 과대적합 처방 중 무엇이 트리의 '가지치기'에 해당하는지 연결해 보라.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 프로토타입 분류 — 퓨샷의 원리 재현\n",
    "\n",
    "Prototypical Networks의 핵심(클래스 대표 = 지원 샘플 임베딩의 평균, 분류 = 최단 거리)을\n",
    "손글씨 숫자 데이터에서 **클래스당 5장만으로** 재현한다. 진짜 ProtoNet은 임베딩 공간 f를\n",
    "학습하지만, 여기서는 원리 확인을 위해 픽셀 공간을 그대로 쓴다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "digits = load_digits()\n",
    "Xtr, Xte, ytr, yte = train_test_split(digits.data, digits.target,\n",
    "                                      train_size=0.5, random_state=0, stratify=digits.target)\n",
    "\n",
    "K = 5   # 클래스당 지원 샘플 수 (5-shot)\n",
    "rng = np.random.default_rng(0)\n",
    "prototypes = np.stack([\n",
    "    Xtr[rng.choice(np.where(ytr == c)[0], K, replace=False)].mean(axis=0)\n",
    "    for c in range(10)\n",
    "])   # (10, 64) — 클래스별 평균 벡터\n",
    "\n",
    "# 쿼리 분류 = 가장 가까운 프로토타입\n",
    "dists = ((Xte[:, None, :] - prototypes[None, :, :]) ** 2).sum(-1)   # (n, 10)\n",
    "pred = dists.argmin(axis=1)\n",
    "acc = (pred == yte).mean()\n",
    "print(f\"10-way {K}-shot 프로토타입 분류 정확도: {acc:.1%}  (클래스당 단 {K}장으로!)\")\n",
    "\n",
    "# 비교: 같은 5장씩만 쓴 kNN(k=1)\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "support_idx = np.concatenate([rng.choice(np.where(ytr == c)[0], K, replace=False) for c in range(10)])\n",
    "knn = KNeighborsClassifier(1).fit(Xtr[support_idx], ytr[support_idx])\n",
    "print(f\"같은 지원 셋의 1-NN 정확도:           {knn.score(Xte, yte):.1%}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### 과제 4-1\nK를 1, 3, 10으로 바꿔 정확도 변화를 그래프로 그려 보라. K=1일 때 프로토타입 분류와 1-NN이\n같아지는 이유는 무엇인가? 또 진짜 Prototypical Networks가 픽셀 평균 대신 **학습된 임베딩**의\n평균을 쓰는 이유를 강의자료 M7 그림 5의 캡션과 연결해 설명하라."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}