{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# 실습 05 · XOR 신경망 — 역전파를 손으로\n\n> **연계 강의자료:** M5 「신경망과 역전파」 · 9~10주차\n> **목표:** 프레임워크 없이 numpy로 2-2-1 신경망의 순전파·역전파를 구현하고, Gradio UI로 학습률을 바꿔 가며 수렴/발산을 관찰한다.\n>\n> 강의자료 M5 실습 2(XOR 실시간 학습)의 구현 원본. 다시 실행할 때마다 서로 다른 해로 수렴하지만 결과는 같다는 점이 관찰 포인트다.\n"
  },
  {
   "cell_type": "code",
   "source": [
    "!pip install gradio"
   ],
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "2rKXoxxyP_r8",
    "outputId": "7a190a2c-51c7-42d0-f141-246127f0eebe"
   },
   "execution_count": 2,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Collecting gradio\n",
      "  Downloading gradio-5.5.0-py3-none-any.whl.metadata (16 kB)\n",
      "Collecting aiofiles<24.0,>=22.0 (from gradio)\n",
      "  Downloading aiofiles-23.2.1-py3-none-any.whl.metadata (9.7 kB)\n",
      "Requirement already satisfied: anyio<5.0,>=3.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (3.7.1)\n",
      "Collecting fastapi<1.0,>=0.115.2 (from gradio)\n",
      "  Downloading fastapi-0.115.4-py3-none-any.whl.metadata (27 kB)\n",
      "Collecting ffmpy (from gradio)\n",
      "  Downloading ffmpy-0.4.0-py3-none-any.whl.metadata (2.9 kB)\n",
      "Collecting gradio-client==1.4.2 (from gradio)\n",
      "  Downloading gradio_client-1.4.2-py3-none-any.whl.metadata (7.1 kB)\n",
      "Requirement already satisfied: httpx>=0.24.1 in /usr/local/lib/python3.10/dist-packages (from gradio) (0.27.2)\n",
      "Collecting huggingface-hub>=0.25.1 (from gradio)\n",
      "  Downloading huggingface_hub-0.26.2-py3-none-any.whl.metadata (13 kB)\n",
      "Requirement already satisfied: jinja2<4.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (3.1.4)\n",
      "Collecting markupsafe~=2.0 (from gradio)\n",
      "  Downloading MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.0 kB)\n",
      "Requirement already satisfied: numpy<3.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (1.26.4)\n",
      "Requirement already satisfied: orjson~=3.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (3.10.10)\n",
      "Requirement already satisfied: packaging in /usr/local/lib/python3.10/dist-packages (from gradio) (24.1)\n",
      "Requirement already satisfied: pandas<3.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (2.2.2)\n",
      "Requirement already satisfied: pillow<12.0,>=8.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (10.4.0)\n",
      "Requirement already satisfied: pydantic>=2.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (2.9.2)\n",
      "Collecting pydub (from gradio)\n",
      "  Downloading pydub-0.25.1-py2.py3-none-any.whl.metadata (1.4 kB)\n",
      "Collecting python-multipart==0.0.12 (from gradio)\n",
      "  Downloading python_multipart-0.0.12-py3-none-any.whl.metadata (1.9 kB)\n",
      "Requirement already satisfied: pyyaml<7.0,>=5.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (6.0.2)\n",
      "Collecting ruff>=0.2.2 (from gradio)\n",
      "  Downloading ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (25 kB)\n",
      "Collecting safehttpx<1.0,>=0.1.1 (from gradio)\n",
      "  Downloading safehttpx-0.1.1-py3-none-any.whl.metadata (4.1 kB)\n",
      "Collecting semantic-version~=2.0 (from gradio)\n",
      "  Downloading semantic_version-2.10.0-py2.py3-none-any.whl.metadata (9.7 kB)\n",
      "Collecting starlette<1.0,>=0.40.0 (from gradio)\n",
      "  Downloading starlette-0.41.2-py3-none-any.whl.metadata (6.0 kB)\n",
      "Collecting tomlkit==0.12.0 (from gradio)\n",
      "  Downloading tomlkit-0.12.0-py3-none-any.whl.metadata (2.7 kB)\n",
      "Requirement already satisfied: typer<1.0,>=0.12 in /usr/local/lib/python3.10/dist-packages (from gradio) (0.12.5)\n",
      "Requirement already satisfied: typing-extensions~=4.0 in /usr/local/lib/python3.10/dist-packages (from gradio) (4.12.2)\n",
      "Collecting uvicorn>=0.14.0 (from gradio)\n",
      "  Downloading uvicorn-0.32.0-py3-none-any.whl.metadata (6.6 kB)\n",
      "Requirement already satisfied: fsspec in /usr/local/lib/python3.10/dist-packages (from gradio-client==1.4.2->gradio) (2024.10.0)\n",
      "Collecting websockets<13.0,>=10.0 (from gradio-client==1.4.2->gradio)\n",
      "  Downloading websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.6 kB)\n",
      "Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio) (3.10)\n",
      "Requirement already satisfied: sniffio>=1.1 in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio) (1.3.1)\n",
      "Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio<5.0,>=3.0->gradio) (1.2.2)\n",
      "Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from httpx>=0.24.1->gradio) (2024.8.30)\n",
      "Requirement already satisfied: httpcore==1.* in /usr/local/lib/python3.10/dist-packages (from httpx>=0.24.1->gradio) (1.0.6)\n",
      "Requirement already satisfied: h11<0.15,>=0.13 in /usr/local/lib/python3.10/dist-packages (from httpcore==1.*->httpx>=0.24.1->gradio) (0.14.0)\n",
      "Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from huggingface-hub>=0.25.1->gradio) (3.16.1)\n",
      "Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from huggingface-hub>=0.25.1->gradio) (2.32.3)\n",
      "Requirement already satisfied: tqdm>=4.42.1 in /usr/local/lib/python3.10/dist-packages (from huggingface-hub>=0.25.1->gradio) (4.66.6)\n",
      "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/dist-packages (from pandas<3.0,>=1.0->gradio) (2.8.2)\n",
      "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas<3.0,>=1.0->gradio) (2024.2)\n",
      "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.10/dist-packages (from pandas<3.0,>=1.0->gradio) (2024.2)\n",
      "Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.0->gradio) (0.7.0)\n",
      "Requirement already satisfied: pydantic-core==2.23.4 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2.0->gradio) (2.23.4)\n",
      "Requirement already satisfied: click>=8.0.0 in /usr/local/lib/python3.10/dist-packages (from typer<1.0,>=0.12->gradio) (8.1.7)\n",
      "Requirement already satisfied: shellingham>=1.3.0 in /usr/local/lib/python3.10/dist-packages (from typer<1.0,>=0.12->gradio) (1.5.4)\n",
      "Requirement already satisfied: rich>=10.11.0 in /usr/local/lib/python3.10/dist-packages (from typer<1.0,>=0.12->gradio) (13.9.3)\n",
      "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.8.2->pandas<3.0,>=1.0->gradio) (1.16.0)\n",
      "Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.10/dist-packages (from rich>=10.11.0->typer<1.0,>=0.12->gradio) (3.0.0)\n",
      "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.10/dist-packages (from rich>=10.11.0->typer<1.0,>=0.12->gradio) (2.18.0)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface-hub>=0.25.1->gradio) (3.4.0)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests->huggingface-hub>=0.25.1->gradio) (2.2.3)\n",
      "Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.10/dist-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer<1.0,>=0.12->gradio) (0.1.2)\n",
      "Downloading gradio-5.5.0-py3-none-any.whl (56.7 MB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m56.7/56.7 MB\u001b[0m \u001b[31m12.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading gradio_client-1.4.2-py3-none-any.whl (319 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m319.8/319.8 kB\u001b[0m \u001b[31m19.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading python_multipart-0.0.12-py3-none-any.whl (23 kB)\n",
      "Downloading tomlkit-0.12.0-py3-none-any.whl (37 kB)\n",
      "Downloading aiofiles-23.2.1-py3-none-any.whl (15 kB)\n",
      "Downloading fastapi-0.115.4-py3-none-any.whl (94 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m94.7/94.7 kB\u001b[0m \u001b[31m6.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading huggingface_hub-0.26.2-py3-none-any.whl (447 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m447.5/447.5 kB\u001b[0m \u001b[31m29.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)\n",
      "Downloading ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.0 MB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.0/11.0 MB\u001b[0m \u001b[31m90.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading safehttpx-0.1.1-py3-none-any.whl (8.4 kB)\n",
      "Downloading semantic_version-2.10.0-py2.py3-none-any.whl (15 kB)\n",
      "Downloading starlette-0.41.2-py3-none-any.whl (73 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m73.3/73.3 kB\u001b[0m \u001b[31m4.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading uvicorn-0.32.0-py3-none-any.whl (63 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m63.7/63.7 kB\u001b[0m \u001b[31m4.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hDownloading ffmpy-0.4.0-py3-none-any.whl (5.8 kB)\n",
      "Downloading pydub-0.25.1-py2.py3-none-any.whl (32 kB)\n",
      "Downloading websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (130 kB)\n",
      "\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m130.2/130.2 kB\u001b[0m \u001b[31m8.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hInstalling collected packages: pydub, websockets, uvicorn, tomlkit, semantic-version, ruff, python-multipart, markupsafe, ffmpy, aiofiles, starlette, huggingface-hub, safehttpx, gradio-client, fastapi, gradio\n",
      "  Attempting uninstall: markupsafe\n",
      "    Found existing installation: MarkupSafe 3.0.2\n",
      "    Uninstalling MarkupSafe-3.0.2:\n",
      "      Successfully uninstalled MarkupSafe-3.0.2\n",
      "  Attempting uninstall: huggingface-hub\n",
      "    Found existing installation: huggingface-hub 0.24.7\n",
      "    Uninstalling huggingface-hub-0.24.7:\n",
      "      Successfully uninstalled huggingface-hub-0.24.7\n",
      "Successfully installed aiofiles-23.2.1 fastapi-0.115.4 ffmpy-0.4.0 gradio-5.5.0 gradio-client-1.4.2 huggingface-hub-0.26.2 markupsafe-2.1.5 pydub-0.25.1 python-multipart-0.0.12 ruff-0.7.3 safehttpx-0.1.1 semantic-version-2.10.0 starlette-0.41.2 tomlkit-0.12.0 uvicorn-0.32.0 websockets-12.0\n"
     ]
    }
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 633
    },
    "id": "2aUxAtxjPvld",
    "outputId": "a8bb8734-81f4-42ae-ef03-f6d8201d3ea8"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Running Gradio in a Colab notebook requires sharing enabled. Automatically setting `share=True` (you can turn this off by setting `share=False` in `launch()` explicitly).\n",
      "\n",
      "Colab notebook detected. To show errors in colab notebook, set debug=True in launch()\n",
      "* Running on public URL: https://f01bcba1d3ccc69585.gradio.live\n",
      "\n",
      "This share link expires in 72 hours. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)\n"
     ]
    },
    {
     "output_type": "display_data",
     "data": {
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ],
      "text/html": [
       "<div><iframe src=\"https://f01bcba1d3ccc69585.gradio.live\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
      ]
     },
     "metadata": {}
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import networkx as nx\n",
    "import gradio as gr\n",
    "from PIL import Image\n",
    "from io import BytesIO\n",
    "\n",
    "class XORNetwork:\n",
    "    def __init__(self, learning_rate=0.3):\n",
    "        self.learning_rate = learning_rate\n",
    "        self.epoch = 0\n",
    "        self.error_history = []\n",
    "        self.weight_history = []\n",
    "        self.initialize_weights()\n",
    "\n",
    "    def initialize_weights(self):\n",
    "        init_scale = np.sqrt(2.0 / (2 + 2))\n",
    "        self.weights = {\n",
    "            'w11': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'w12': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'w21': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'w22': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'v1': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'v2': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'b1': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'b2': (np.random.rand() * 2 - 1) * init_scale,\n",
    "            'bo': (np.random.rand() * 2 - 1) * init_scale\n",
    "        }\n",
    "\n",
    "    def sigmoid(self, x):\n",
    "        return 1 / (1 + np.exp(-x))\n",
    "\n",
    "    def forward_pass(self, inputs):\n",
    "        hidden1_sum = inputs[0] * self.weights['w11'] + inputs[1] * self.weights['w21'] + self.weights['b1']\n",
    "        hidden1 = self.sigmoid(hidden1_sum)\n",
    "\n",
    "        hidden2_sum = inputs[0] * self.weights['w12'] + inputs[1] * self.weights['w22'] + self.weights['b2']\n",
    "        hidden2 = self.sigmoid(hidden2_sum)\n",
    "\n",
    "        output_sum = hidden1 * self.weights['v1'] + hidden2 * self.weights['v2'] + self.weights['bo']\n",
    "        output = self.sigmoid(output_sum)\n",
    "\n",
    "        return {\n",
    "            'hidden1': hidden1,\n",
    "            'hidden2': hidden2,\n",
    "            'output': output,\n",
    "            'hidden1_sum': hidden1_sum,\n",
    "            'hidden2_sum': hidden2_sum,\n",
    "            'output_sum': output_sum\n",
    "        }\n",
    "\n",
    "    def backward_pass(self, inputs, target):\n",
    "        forward_result = self.forward_pass(inputs)\n",
    "        hidden1 = forward_result['hidden1']\n",
    "        hidden2 = forward_result['hidden2']\n",
    "        output = forward_result['output']\n",
    "\n",
    "        output_error = target - output\n",
    "        output_delta = output_error * output * (1 - output)\n",
    "\n",
    "        hidden1_delta = output_delta * self.weights['v1'] * hidden1 * (1 - hidden1)\n",
    "        hidden2_delta = output_delta * self.weights['v2'] * hidden2 * (1 - hidden2)\n",
    "\n",
    "        self.weights['v1'] += self.learning_rate * output_delta * hidden1\n",
    "        self.weights['v2'] += self.learning_rate * output_delta * hidden2\n",
    "\n",
    "        self.weights['w11'] += self.learning_rate * hidden1_delta * inputs[0]\n",
    "        self.weights['w12'] += self.learning_rate * hidden2_delta * inputs[0]\n",
    "        self.weights['w21'] += self.learning_rate * hidden1_delta * inputs[1]\n",
    "        self.weights['w22'] += self.learning_rate * hidden2_delta * inputs[1]\n",
    "\n",
    "        self.weights['b1'] += self.learning_rate * hidden1_delta\n",
    "        self.weights['b2'] += self.learning_rate * hidden2_delta\n",
    "        self.weights['bo'] += self.learning_rate * output_delta\n",
    "\n",
    "        # Record weight history for visualization\n",
    "        self.weight_history.append(self.weights.copy())\n",
    "\n",
    "        error = (output_error ** 2) / 2\n",
    "        return error\n",
    "\n",
    "    def train(self, training_data, max_epochs=10000, error_threshold=0.001):\n",
    "        self.epoch = 0\n",
    "        self.error_history = []\n",
    "        self.weight_history = []\n",
    "        running = True\n",
    "\n",
    "        while running and self.epoch < max_epochs:\n",
    "            total_error = 0\n",
    "            for example in training_data:\n",
    "                inputs = example['inputs']\n",
    "                target = example['target']\n",
    "                error = self.backward_pass(inputs, target)\n",
    "                total_error += error\n",
    "\n",
    "            avg_error = total_error / len(training_data)\n",
    "            self.error_history.append(avg_error)\n",
    "            self.epoch += 1\n",
    "\n",
    "            if avg_error < error_threshold:\n",
    "                running = False\n",
    "\n",
    "    def visualize_training(self):\n",
    "        plt.figure(figsize=(10, 5))\n",
    "        plt.plot(range(1, len(self.error_history) + 1), self.error_history, color='b', label='Average Error')\n",
    "        plt.xlabel('Epoch')\n",
    "        plt.ylabel('Error')\n",
    "        plt.title('Training Error over Epochs')\n",
    "        plt.legend()\n",
    "        plt.grid(True)\n",
    "        buf = BytesIO()\n",
    "        plt.savefig(buf, format='png')\n",
    "        buf.seek(0)\n",
    "        plt.close()\n",
    "        return Image.open(buf)\n",
    "\n",
    "    def visualize_network(self):\n",
    "        G = nx.DiGraph()\n",
    "        G.add_nodes_from([\"Input 1\", \"Input 2\", \"Hidden 1\", \"Hidden 2\", \"Output\", \"Bias 1\", \"Bias 2\", \"Bias Output\"])\n",
    "\n",
    "        # Adding edges with weights\n",
    "        G.add_edge(\"Input 1\", \"Hidden 1\", weight=self.weights['w11'])\n",
    "        G.add_edge(\"Input 1\", \"Hidden 2\", weight=self.weights['w12'])\n",
    "        G.add_edge(\"Input 2\", \"Hidden 1\", weight=self.weights['w21'])\n",
    "        G.add_edge(\"Input 2\", \"Hidden 2\", weight=self.weights['w22'])\n",
    "        G.add_edge(\"Hidden 1\", \"Output\", weight=self.weights['v1'])\n",
    "        G.add_edge(\"Hidden 2\", \"Output\", weight=self.weights['v2'])\n",
    "        G.add_edge(\"Bias 1\", \"Hidden 1\", weight=self.weights['b1'])\n",
    "        G.add_edge(\"Bias 2\", \"Hidden 2\", weight=self.weights['b2'])\n",
    "        G.add_edge(\"Bias Output\", \"Output\", weight=self.weights['bo'])\n",
    "\n",
    "        pos = {\n",
    "            \"Input 1\": (0, 2), \"Input 2\": (0, 0),\n",
    "            \"Hidden 1\": (1, 2.5), \"Hidden 2\": (1, -0.5),\n",
    "            \"Output\": (2, 1),\n",
    "            \"Bias 1\": (0.5, 3.5), \"Bias 2\": (0.5, -1.5), \"Bias Output\": (1.5, 3.5)\n",
    "        }\n",
    "\n",
    "        plt.figure(figsize=(10, 6))\n",
    "        edge_labels = {(u, v): f\"{d['weight']:.2f}\" for u, v, d in G.edges(data=True)}\n",
    "        nx.draw(G, pos, with_labels=True, node_size=3000, node_color='lightblue', font_size=10, font_weight='bold')\n",
    "        nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color='red')\n",
    "        plt.title(\"Neural Network Visualization with Weights and Biases\")\n",
    "        buf = BytesIO()\n",
    "        plt.savefig(buf, format='png')\n",
    "        buf.seek(0)\n",
    "        plt.close()\n",
    "        return Image.open(buf)\n",
    "\n",
    "    def visualize_weight_changes(self):\n",
    "        plt.figure(figsize=(12, 8))\n",
    "        for key in self.weights.keys():\n",
    "            weights_over_time = [w[key] for w in self.weight_history]\n",
    "            plt.plot(weights_over_time, label=key)\n",
    "        plt.xlabel('Epoch')\n",
    "        plt.ylabel('Weight Value')\n",
    "        plt.title('Weight Changes Over Training')\n",
    "        plt.legend()\n",
    "        plt.grid(True)\n",
    "        buf = BytesIO()\n",
    "        plt.savefig(buf, format='png')\n",
    "        buf.seek(0)\n",
    "        plt.close()\n",
    "        return Image.open(buf)\n",
    "\n",
    "    def predict(self, inputs):\n",
    "        forward_result = self.forward_pass(inputs)\n",
    "        hidden1_sum = forward_result['hidden1_sum']\n",
    "        hidden2_sum = forward_result['hidden2_sum']\n",
    "        output_sum = forward_result['output_sum']\n",
    "        output = forward_result['output']\n",
    "\n",
    "        hidden1_equation = f\"({inputs[0]} * {self.weights['w11']:.3f}) + ({inputs[1]} * {self.weights['w21']:.3f}) + {self.weights['b1']:.3f} = {hidden1_sum:.3f}\"\n",
    "        hidden2_equation = f\"({inputs[0]} * {self.weights['w12']:.3f}) + ({inputs[1]} * {self.weights['w22']:.3f}) + {self.weights['b2']:.3f} = {hidden2_sum:.3f}\"\n",
    "        output_equation = f\"({forward_result['hidden1']:.3f} * {self.weights['v1']:.3f}) + ({forward_result['hidden2']:.3f} * {self.weights['v2']:.3f}) + {self.weights['bo']:.3f} = {output_sum:.3f}\"\n",
    "\n",
    "        return (f\"Input: {inputs}\\n\"\n",
    "                f\"Hidden Layer 1 Calculation: {hidden1_equation}, Activation: {forward_result['hidden1']:.3f}\\n\"\n",
    "                f\"Hidden Layer 2 Calculation: {hidden2_equation}, Activation: {forward_result['hidden2']:.3f}\\n\"\n",
    "                f\"Output Layer Calculation: {output_equation}, Output (after sigmoid): {output:.3f}\")\n",
    "\n",
    "    def gradio_interface(self):\n",
    "        def train_and_visualize(learning_rate, max_epochs):\n",
    "            self.learning_rate = learning_rate\n",
    "            self.train(training_data, max_epochs=max_epochs)\n",
    "            training_plot = self.visualize_training()\n",
    "            network_plot = self.visualize_network()\n",
    "            weight_change_plot = self.visualize_weight_changes()\n",
    "            predictions = \"\\n\\n\".join([self.predict(example['inputs']) for example in training_data])\n",
    "            return f\"Training complete. Check the graphs for details.\\n\\nPredictions:\\n{predictions}\", training_plot, network_plot, weight_change_plot\n",
    "\n",
    "        def reset_network():\n",
    "            self.initialize_weights()\n",
    "            self.error_history = []\n",
    "            self.weight_history = []\n",
    "            return \"Network reset complete. Ready for new training.\"\n",
    "\n",
    "        training_data = [\n",
    "            {'inputs': [0, 0], 'target': 0},\n",
    "            {'inputs': [0, 1], 'target': 1},\n",
    "            {'inputs': [1, 0], 'target': 1},\n",
    "            {'inputs': [1, 1], 'target': 0}\n",
    "        ]\n",
    "\n",
    "        with gr.Blocks() as demo:\n",
    "            gr.Markdown(\"# XOR Neural Network Training\")\n",
    "            with gr.Row():\n",
    "                learning_rate = gr.Slider(0.01, 1.0, step=0.01, label=\"Learning Rate\")\n",
    "                max_epochs = gr.Slider(100, 10000, step=100, label=\"Max Epochs\")\n",
    "            train_button = gr.Button(\"Train Network\")\n",
    "            reset_button = gr.Button(\"Reset Network\")\n",
    "            output_text = gr.Textbox(label=\"Training Output\")\n",
    "            training_plot = gr.Image(type=\"pil\", label=\"Training Error Plot\")\n",
    "            network_plot = gr.Image(type=\"pil\", label=\"Network Visualization\")\n",
    "            weight_change_plot = gr.Image(type=\"pil\", label=\"Weight Changes Over Time\")\n",
    "\n",
    "            train_button.click(train_and_visualize, inputs=[learning_rate, max_epochs], outputs=[output_text, training_plot, network_plot, weight_change_plot])\n",
    "            reset_button.click(reset_network, outputs=output_text)\n",
    "\n",
    "        demo.launch()\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    training_data = [\n",
    "        {'inputs': [0, 0], 'target': 0},\n",
    "        {'inputs': [0, 1], 'target': 1},\n",
    "        {'inputs': [1, 0], 'target': 1},\n",
    "        {'inputs': [1, 1], 'target': 0}\n",
    "    ]\n",
    "\n",
    "    network = XORNetwork(learning_rate=0.3)\n",
    "    network.gradio_interface()\n",
    "\n"
   ]
  }
 ]
}