From 38196ef82f1bb2b752d8567e638f3162710bc9ac Mon Sep 17 00:00:00 2001 From: Hyung Jiwon Date: Fri, 24 Jul 2026 14:53:54 +0900 Subject: [PATCH] Add files via upload --- ...41\204\211\341\205\263\341\206\2701.ipynb" | 643 +++++++++++++++++ ...41\204\211\341\205\263\341\206\2702.ipynb" | 681 ++++++++++++++++++ 2 files changed, 1324 insertions(+) create mode 100644 "\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2701.ipynb" create mode 100644 "\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2702.ipynb" diff --git "a/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2701.ipynb" "b/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2701.ipynb" new file mode 100644 index 0000000..d21a6c5 --- /dev/null +++ "b/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2701.ipynb" @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "36b0e02d", + "metadata": { + "id": "36b0e02d" + }, + "source": [ + "# BOAZ BASE 2주차 과제 — RNN · LSTM · GRU\n", + "\n", + "이 노트북을 **본인 구글 드라이브로 사본 저장**한 뒤 진행하고, 완성한 `.ipynb`를 깃허브에 업로드해 주세요.\n", + "\n", + "**구성**\n", + "1. RNN 튜토리얼\n", + "2. RNN 과제\n", + "\n", + "튜토리얼은 **완성된 코드**로 제공됩니다.\n", + "먼저 셀을 순서대로 실행하면서 출력과 주석을 천천히 읽어보면서 진행하는걸 추천합니다." + ] + }, + { + "cell_type": "markdown", + "id": "57eca0d1", + "metadata": { + "id": "57eca0d1" + }, + "source": [ + "---\n", + "## 1. RNN 튜토리얼\n", + "\n", + "**Sequential data**를 다루는 가장 기본 모델이 RNN입니다.\n", + "이전 시점의 hidden state를 다음 시점으로 넘기면서 **과거 정보를 계승**합니다.\n", + "\n", + "**튜토리얼**: 길이 12짜리 0/1 시퀀스를 입력받아, 그 안에 `1-0-1` 패턴이 한 번이라도 나오면 1, 아니면 0을 맞히는 이진 분류기입니다.\n", + "\n", + "구조는 세 가중치(입력→은닉 $W_{xh}$, 은닉→은닉 $W_{hh}$, 은닉→출력 $W_{hy}$)와 $\\tanh$ 활성화가 `nn.RNN` 안에 그대로 들어 있다고 보면 됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0ccb6d29", + "metadata": { + "id": "0ccb6d29" + }, + "outputs": [], + "source": [ + "import random\n", + "import torch\n", + "import torch.nn as nn\n", + "from torch.utils.data import Dataset, DataLoader\n", + "from itertools import product\n", + "\n", + "SEED = 0\n", + "random.seed(SEED)\n", + "torch.manual_seed(SEED)\n", + "\n", + "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "639f37b3", + "metadata": { + "id": "639f37b3" + }, + "outputs": [], + "source": [ + "# 정답 label을 만드는 함수\n", + "# seq 안에 연속된 3칸이 1-0-1 이면 1, 아니면 0\n", + "def has_101_pattern(seq):\n", + " for i in range(len(seq) - 2):\n", + " if seq[i] == 1 and seq[i+1] == 0 and seq[i+2] == 1:\n", + " return 1\n", + " return 0" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ada1b1e2", + "metadata": { + "id": "ada1b1e2", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "7da6e19e-cbf2-411f-f5dc-d0add06877a7" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "전체 4096개 중 label=1 : 3015 (73.6%)\n", + "train 1729 / val 433\n", + "겹치는 시퀀스 : 0 개\n" + ] + } + ], + "source": [ + "# PyTorch에서 Dataset은 \"데이터 1개를 어떻게 꺼낼지\"를 표준화한 클래스입니다.\n", + "# 이걸 상속받아 내 task에 맞는 커스텀 Dataset을 만들어 모델에 기입합니다.\n", + "class PatternDataset(Dataset):\n", + " def __init__(self, seqs):\n", + " self.data = [(s, has_101_pattern(s)) for s in seqs]\n", + "\n", + " def __len__(self):\n", + " return len(self.data)\n", + " # 전체 sample 개수\n", + "\n", + " def __getitem__(self, idx):\n", + " seq, label = self.data[idx]\n", + " x = torch.tensor(seq, dtype=torch.long)\n", + " # (T,) embedding은 정수 인덱스를 받으므로 long\n", + " y = torch.tensor([label], dtype=torch.float)\n", + " # (1,) BCEWithLogitsLoss가 float label을 기대\n", + " return x, y\n", + "\n", + "# 데이터 생성 및 로더 세팅\n", + "all_seqs = [list(s) for s in product([0, 1], repeat=12)]\n", + "pos = [s for s in all_seqs if has_101_pattern(s) == 1]\n", + "neg = [s for s in all_seqs if has_101_pattern(s) == 0]\n", + "\n", + "rng = random.Random(SEED)\n", + "rng.shuffle(pos)\n", + "rng.shuffle(neg)\n", + "n = min(len(pos), len(neg))\n", + "balanced = pos[:n] + neg[:n]\n", + "rng.shuffle(balanced)\n", + "\n", + "n_train = int(len(balanced) * 0.8)\n", + "train_seqs, val_seqs = balanced[:n_train], balanced[n_train:]\n", + "\n", + "train_loader = DataLoader(PatternDataset(train_seqs), batch_size=64, shuffle=True)\n", + "# train은 섞어서 안정적으로\n", + "val_loader = DataLoader(PatternDataset(val_seqs), batch_size=256, shuffle=False)\n", + "# val은 섞을 필요 없음\n", + "\n", + "print(f\"전체 {len(all_seqs)}개 중 label=1 : {len(pos)} ({len(pos)/len(all_seqs):.1%})\")\n", + "print(f\"train {len(train_seqs)} / val {len(val_seqs)}\")\n", + "print(\"겹치는 시퀀스 :\", len(set(map(tuple, train_seqs)) & set(map(tuple, val_seqs))), \"개\")" + ] + }, + { + "cell_type": "markdown", + "id": "bd0b87b2", + "metadata": { + "id": "bd0b87b2" + }, + "source": [ + "**RNN 모델.** PyTorch 모델은 보통 `nn.Module`을 상속해서 만듭니다. `embed → rnn → fc` 순서로 통과시켜 마지막 hidden으로 분류합니다." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3895fdf2", + "metadata": { + "id": "3895fdf2" + }, + "outputs": [], + "source": [ + "class SimpleRNNClassifier(nn.Module):\n", + " def __init__(self, vocab_size=2, embed_dim=8, hidden_dim=16):\n", + " super().__init__()\n", + " self.embed = nn.Embedding(vocab_size, embed_dim)\n", + " # (0/1) 토큰 -> 벡터\n", + "\n", + " self.rnn = nn.RNN(input_size=embed_dim, hidden_size=hidden_dim, batch_first=True)\n", + " # 시퀀스를 왼쪽부터 읽으며 hidden 갱신\n", + "\n", + " self.fc = nn.Linear(hidden_dim, 1)\n", + " # 마지막 hidden -> 이진 분류 점수(logit)\n", + "\n", + " def forward(self, x):\n", + " # x: (B, T) 0/1 토큰, B=batch, T=시퀀스 길이\n", + " emb = self.embed(x)\n", + " # (B, T, E)\n", + " out, h_n = self.rnn(emb)\n", + " # out: (B, T, H) 모든 시점 hidden / h_n: (1, B, H) 마지막 hidden\n", + " last_h = h_n[-1]\n", + " # (B, H)\n", + " logit = self.fc(last_h)\n", + " # (B, 1)\n", + " return logit, out, h_n\n", + "\n", + " def forward_with_trace(self, x):\n", + " # 시간에 따른 hidden 변화를 눈으로 보기 위한 함수 (x는 (1, T) 단일 시퀀스 권장)\n", + " emb = self.embed(x)\n", + " # (1, T, E)\n", + " out, h_n = self.rnn(emb)\n", + " # out: (1, T, H)\n", + " logit = self.fc(h_n[-1])\n", + " # (1, 1)\n", + " return logit, out.squeeze(0)\n", + " # logit:(1,1), trace:(T, H)" + ] + }, + { + "cell_type": "markdown", + "id": "ef4328c9", + "metadata": { + "id": "ef4328c9" + }, + "source": [ + "학습(train) → 검증(val) → 마지막에 demo 시퀀스로 예측 확률과 hidden 변화를 출력합니다." + ] + }, + { + "cell_type": "code", + "source": [ + "def train_rnn():\n", + " model = SimpleRNNClassifier(embed_dim=8, hidden_dim=16).to(device)\n", + " criterion = nn.BCEWithLogitsLoss()\n", + " optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)\n", + "\n", + " for epoch in range(1, 6):\n", + " model.train()\n", + " total_loss = 0.0\n", + "\n", + " # 1. 학습 루프\n", + " for x, y in train_loader:\n", + " x, y = x.to(device), y.to(device)\n", + " logit, _, _ = model(x)\n", + " loss = criterion(logit, y)\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " total_loss += loss.item() * x.size(0)\n", + "\n", + " train_loss = total_loss / len(train_seqs)\n", + "\n", + " model.eval()\n", + " correct, total = 0, 0\n", + " with torch.no_grad():\n", + "\n", + " # 2. 검증 루프\n", + " for x, y in val_loader:\n", + " x, y = x.to(device), y.to(device)\n", + " prob = torch.sigmoid(model(x)[0])\n", + " pred = (prob >= 0.5).float()\n", + " correct += (pred == y).sum().item()\n", + " total += y.numel()\n", + "\n", + " # 3. 에포크 결과 출력\n", + " print(f\"[Epoch {epoch}] train_loss={train_loss:.4f} val_acc={correct/total:.4f}\")\n", + "\n", + " return model\n", + "\n", + "# ---- DEMO: hidden state ----\n", + "def demo(model, demo_seq):\n", + " model.eval()\n", + " x_demo = torch.tensor([demo_seq], dtype=torch.long).to(device)\n", + " # (1, T)\n", + "\n", + " with torch.no_grad():\n", + " logit, trace = model.forward_with_trace(x_demo)\n", + " prob = torch.sigmoid(logit).item()\n", + "\n", + " print(\"\\n=== DEMO ===\")\n", + " print(\"demo_seq :\", demo_seq)\n", + " print(\"정답(has 101) :\", has_101_pattern(demo_seq))\n", + " print(\"예측 확률(P=101):\", round(prob, 4))\n", + "\n", + " trace = trace.cpu()\n", + " # (T, H)\n", + " print(\"\\nhidden state 추적 (앞 3스텝 + 뒤 3스텝, 앞 6차원만):\")\n", + " T = len(demo_seq)\n", + " for t in list(range(3)) + list(range(T-3, T)):\n", + " vec = [round(v, 3) for v in trace[t][:6].tolist()]\n", + " print(f\" t={t:2d}, x_t={demo_seq[t]} -> h_t[:6]={vec}\")" + ], + "metadata": { + "id": "JBk4F7Qld_Gs" + }, + "id": "JBk4F7Qld_Gs", + "execution_count": 5, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "rnn_model = train_rnn()\n", + "\n", + "demo_has = [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", + "demo(rnn_model, demo_has)\n", + "\n", + "demo_no = [1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0]\n", + "demo(rnn_model, demo_no)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Rnzp7YeYeAPK", + "outputId": "0bb06c95-54a6-46d2-aa4a-1ef6de951178" + }, + "id": "Rnzp7YeYeAPK", + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[Epoch 1] train_loss=0.6571 val_acc=0.5381\n", + "[Epoch 2] train_loss=0.6594 val_acc=0.6836\n", + "[Epoch 3] train_loss=0.5738 val_acc=0.7691\n", + "[Epoch 4] train_loss=0.4141 val_acc=0.9400\n", + "[Epoch 5] train_loss=0.1163 val_acc=1.0000\n", + "\n", + "=== DEMO ===\n", + "demo_seq : [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n", + "정답(has 101) : 1\n", + "예측 확률(P=101): 0.9895\n", + "\n", + "hidden state 추적 (앞 3스텝 + 뒤 3스텝, 앞 6차원만):\n", + " t= 0, x_t=1 -> h_t[:6]=[-0.577, -0.849, 0.662, 0.989, -0.904, -0.609]\n", + " t= 1, x_t=0 -> h_t[:6]=[-0.492, 0.814, 0.875, 0.984, 0.932, 0.966]\n", + " t= 2, x_t=1 -> h_t[:6]=[-0.71, -0.908, 0.594, 0.952, 0.76, -0.851]\n", + " t= 9, x_t=0 -> h_t[:6]=[0.924, -0.96, 0.883, 0.753, 0.928, -0.982]\n", + " t=10, x_t=0 -> h_t[:6]=[0.924, -0.959, 0.882, 0.75, 0.929, -0.982]\n", + " t=11, x_t=0 -> h_t[:6]=[0.924, -0.959, 0.881, 0.748, 0.928, -0.981]\n", + "\n", + "=== DEMO ===\n", + "demo_seq : [1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0]\n", + "정답(has 101) : 0\n", + "예측 확률(P=101): 0.0964\n", + "\n", + "hidden state 추적 (앞 3스텝 + 뒤 3스텝, 앞 6차원만):\n", + " t= 0, x_t=1 -> h_t[:6]=[-0.577, -0.849, 0.662, 0.989, -0.904, -0.609]\n", + " t= 1, x_t=1 -> h_t[:6]=[-0.484, -0.568, 0.963, 1.0, -0.974, 0.385]\n", + " t= 2, x_t=0 -> h_t[:6]=[-0.807, 0.97, 0.616, 0.932, 0.982, 0.978]\n", + " t= 9, x_t=1 -> h_t[:6]=[-0.926, -0.587, 0.938, 1.0, -0.992, 0.867]\n", + " t=10, x_t=0 -> h_t[:6]=[-0.971, 0.989, 0.531, 0.885, 0.993, 0.998]\n", + " t=11, x_t=0 -> h_t[:6]=[-0.674, 0.534, -0.4, -0.839, 1.0, 0.675]\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "id": "db175122", + "metadata": { + "id": "db175122" + }, + "source": [ + "### Q1. 위 코드의 출력을 분석하고, `demo_has`와 `demo_no` 두 입력을 넣었을 때 예측 확률·hidden state가 어떻게 달랐는지 비교·서술하세요." + ] + }, + { + "cell_type": "markdown", + "id": "3301b9cd", + "metadata": { + "id": "3301b9cd" + }, + "source": [ + "Ans) **(t=0은 두 시퀀스 다 첫 값이 1이라 hidden state도 동일하게 나왔다. t=1부터 입력이 달라지면서 hidden state도 갈라지기 시작했고, demo_has는 t=2에서 1-0-1이 완성된 이후로는 hidden state가 거의 변하지 않고 유지됐다. 반면 demo_no는 끝까지 계속 값이 바뀌었다.결과적으로 t=11 hidden state가 두 시퀀스에서 거의 반대 값으로 나왔고, 그 최종 hidden state로 예측을 하다 보니 demo_has는 0.9895, demo_no는 0.0964로 상반된 확률이 나온 것 같다. RNN이 1-0-1을 봤는지를 hidden state에 기억해뒀다가 끝까지 들고 가서 판단에 쓰는 것으로 보인다.)**" + ] + }, + { + "cell_type": "markdown", + "source": [ + "###Q2. 아래 코드의 `TODO` 빈칸(`____`)을 모두 채운 뒤 순서대로 실행하고, 맨 아래 서술형 질문에 답하면 됩니다." + ], + "metadata": { + "id": "XaIllY535f98" + }, + "id": "XaIllY535f98" + }, + { + "cell_type": "code", + "source": [ + "import random\n", + "import torch\n", + "import torch.nn as nn\n", + "from torch.utils.data import Dataset, DataLoader\n", + "\n", + "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"" + ], + "metadata": { + "id": "9XE_7mcE5yJ1" + }, + "id": "9XE_7mcE5yJ1", + "execution_count": 7, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# 정답 label을 만드는 함수\n", + "def has_101_pattern(seq):\n", + " for i in range(len(seq) - 2):\n", + " if seq[i] == 1 and seq[i+1] == 0 and seq[i+2] == 1:\n", + " return 1\n", + " return 0\n", + "\n", + "# 학습용 커스텀 Dataset\n", + "class PatternDataset(Dataset):\n", + " def __init__(self, n_samples=5000, seq_len=12):\n", + " self.data = []\n", + " for _ in range(n_samples):\n", + " seq = [random.randint(0, 1) for _ in range(seq_len)]\n", + " self.data.append((seq, has_101_pattern(seq)))\n", + "\n", + " def __len__(self):\n", + " return len(self.data)\n", + "\n", + " def __getitem__(self, idx):\n", + " seq, label = self.data[idx]\n", + " x = torch.tensor(seq, dtype=torch.long)\n", + " # (T,) embedding은 정수 인덱스를 받으므로 long\n", + " y = torch.tensor([label], dtype=torch.float)\n", + " # (1,) BCEWithLogitsLoss는 float label을 기대\n", + " return x, y" + ], + "metadata": { + "id": "DNh2gheS52K9" + }, + "id": "DNh2gheS52K9", + "execution_count": 8, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### 1) RNN 모델 빈칸 채우기\n", + "`embed → rnn → fc` 순서로 시퀀스를 통과시켜, *마지막 hidden state*로 분류 점수(logit) 출력\n", + "\n", + "힌트: `nn.Embedding(vocab_size, embed_dim)`, `nn.RNN(input_size=?, hidden_size=?, batch_first=True)`, `out, h_n = self.rnn(emb)`, 마지막 hidden은 `h_n[-1]`." + ], + "metadata": { + "id": "qg7HhHNO55CG" + }, + "id": "qg7HhHNO55CG" + }, + { + "cell_type": "code", + "source": [ + "class SimpleRNNClassifier(nn.Module):\n", + " def __init__(self, vocab_size=2, embed_dim=8, hidden_dim=16):\n", + " super().__init__()\n", + "\n", + " self.embed = nn.Embedding(vocab_size, embed_dim)\n", + "\n", + " self.rnn = nn.RNN(input_size=embed_dim, hidden_size=hidden_dim, batch_first=True)\n", + " # 마지막 hidden -> 이진 분류 점수(logit) 1개\n", + " self.fc = nn.Linear(hidden_dim, 1)\n", + "\n", + " def forward(self, x):\n", + " # x: (B, T) 0/1 토큰 (B=batch, T=시퀀스 길이)\n", + "\n", + " emb = self.embed(x)\n", + "\n", + " out, h_n = out, h_n = self.rnn(emb)\n", + "\n", + " last_h = h_n[-1]\n", + " logit = self.fc(last_h)\n", + " # (B, 1)\n", + " return logit" + ], + "metadata": { + "id": "2FQT93Lw54e2" + }, + "id": "2FQT93Lw54e2", + "execution_count": 11, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "###2) 학습 루프 빈칸 채우기\n", + "학습(train) → 검증(val)을 5 epoch 반복한다. loss는 `BCEWithLogitsLoss`, optimizer는 `Adam`을 씁니다." + ], + "metadata": { + "id": "w-3hsuqL6Cvv" + }, + "id": "w-3hsuqL6Cvv" + }, + { + "cell_type": "code", + "source": [ + "def train():\n", + " train_ds = PatternDataset(n_samples=6000, seq_len=12)\n", + " val_ds = PatternDataset(n_samples=1000, seq_len=12)\n", + " train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)\n", + " # train은 섞어서\n", + " val_loader = DataLoader(val_ds, batch_size=256, shuffle=False)\n", + " # val은 안 섞음\n", + "\n", + " model = SimpleRNNClassifier(embed_dim=8, hidden_dim=16).to(device)\n", + "\n", + " criterion = nn.BCEWithLogitsLoss()\n", + "\n", + " optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n", + "\n", + " for epoch in range(1, 6):\n", + " model.train()\n", + " total_loss = 0.0\n", + " for x, y in train_loader:\n", + " x, y = x.to(device), y.to(device)\n", + " logit = model(x)\n", + "\n", + "\n", + " optimizer.zero_grad()\n", + "\n", + " loss = criterion(logit, y)\n", + "\n", + " loss.backward()\n", + "\n", + " optimizer.step()\n", + "\n", + " total_loss += loss.item() * x.size(0)\n", + " train_loss = total_loss / len(train_ds)\n", + "\n", + " # ---- 검증 ----\n", + " model.eval()\n", + " correct, total = 0, 0\n", + " with torch.no_grad():\n", + " for x, y in val_loader:\n", + " x, y = x.to(device), y.to(device)\n", + "\n", + " prob = torch.sigmoid(model(x))\n", + " # TODO: 0.5 이상이면 1로 예측 (float)\n", + " pred = (prob >= 0.5).float()\n", + " correct += (pred == y).sum().item()\n", + " total += y.numel()\n", + " print(f\"[Epoch {epoch}] train_loss={train_loss:.4f} val_acc={correct/total:.4f}\")\n", + "\n", + " return model\n", + "\n", + "model = train()" + ], + "metadata": { + "id": "ygermVvS6Bsf", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "df37e36f-5fc1-4db5-e7da-e11898da6948" + }, + "id": "ygermVvS6Bsf", + "execution_count": 12, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[Epoch 1] train_loss=0.5929 val_acc=0.7350\n", + "[Epoch 2] train_loss=0.5365 val_acc=0.7610\n", + "[Epoch 3] train_loss=0.5052 val_acc=0.7720\n", + "[Epoch 4] train_loss=0.4822 val_acc=0.7980\n", + "[Epoch 5] train_loss=0.3432 val_acc=0.9470\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "###3) 직접 넣어보기 (제공)\n", + "학습된 모델에 아래 두 시퀀스를 넣어 예측 확률을 확인하세요." + ], + "metadata": { + "id": "1b9vi88x6JgU" + }, + "id": "1b9vi88x6JgU" + }, + { + "cell_type": "code", + "source": [ + "def predict(model, seq):\n", + " model.eval()\n", + " x = torch.tensor([seq], dtype=torch.long).to(device)\n", + " with torch.no_grad():\n", + " prob = torch.sigmoid(model(x)).item()\n", + " print(f\"seq={seq} 정답(has 101)={has_101_pattern(seq)} 예측확률(P=101)={prob:.4f}\")\n", + "\n", + "predict(model, [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])\n", + "# 1-0-1 있음 -> 높아야 함\n", + "\n", + "predict(model, [1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0])\n", + "# 1-0-1 없음 -> 낮아야 함" + ], + "metadata": { + "id": "llF3SNGH6dh9", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "d445355a-6069-4268-f72c-267f699acf77" + }, + "id": "llF3SNGH6dh9", + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "seq=[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] 정답(has 101)=1 예측확률(P=101)=0.8816\n", + "seq=[1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0] 정답(has 101)=0 예측확률(P=101)=0.3256\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Q3. 두 입력의 예측 확률을 비교하고, RNN이 이 문제를 어떻게 푸는지 서술하세요.\n", + "(hidden state가 시퀀스를 왼쪽부터 읽으며 과거 정보를 어떻게 전달하는지 언급하면 좋습니다.)" + ], + "metadata": { + "id": "VcfP9jK-6gyP" + }, + "id": "VcfP9jK-6gyP" + }, + { + "cell_type": "markdown", + "source": [ + "Ans) **첫 번째 시퀀스(1,0,1,0,...)는 예측확률 0.8816으로 패턴이 있다고 판단했고, 두 번째 시퀀스(1,1,0,0,1,1,0,0,...)는 0.3256으로 패턴이 없다고 판단했다. 실제 정답과 방향이 일치한다.RNN은 시퀀스를 왼쪽부터 한 칸씩 읽으면서 hidden state를 계속 업데이트한다. 첫 번째 시퀀스는 t=0,1,2에서 1,0,1을 순서대로 읽는 순간 패턴을 인지하고, 그 정보를 hidden state에 담아 이후 시점(0이 계속 나오는 구간)까지 계속 들고 간다. 두 번째 시퀀스는 이런 1-0-1 구간이 없어서 hidden state가 그런 신호 없이 계속 갱신된다. 결국 모델은 마지막 시점의 hidden state 하나만 보고 최종 판단을 내리는데, 이 hidden state 안에 \"패턴을 봤는지\"에 대한 정보가 누적되어 있기 때문에 예측이 가능하다.)**" + ], + "metadata": { + "id": "DvO5A2YO6l0i" + }, + "id": "DvO5A2YO6l0i" + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git "a/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2702.ipynb" "b/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2702.ipynb" new file mode 100644 index 0000000..1164f07 --- /dev/null +++ "b/\341\204\207\341\205\256\341\206\253\341\204\211\341\205\245\341\206\250_Base_2\341\204\214\341\205\256\341\204\216\341\205\241_\341\204\211\341\205\265\341\206\257\341\204\211\341\205\263\341\206\2702.ipynb" @@ -0,0 +1,681 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "FyjHzKJjZWNy" + }, + "source": [ + "# LSTM vs GRU 성능 & 속도 비교\n", + "\n", + "**IMDB 영화 리뷰 감성 분류** 문제를 통해 LSTM과 GRU 두 모델을 직접 비교해보겠습니다 !\n", + "\n", + "> **실행 방법: Colab 상단 메뉴에서 `연결 > 런타임 유형 변경 > T4 GPU`로 설정한 뒤, 각 셀을 위에서부터 순서대로 실행해주세요.**" + ], + "id": "FyjHzKJjZWNy" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P6_dKvXCZWN4" + }, + "source": [ + "## 0. 실습 개요\n", + "\n", + "**태스크**: IMDB 영화 리뷰가 긍정(1)인지 부정(0)인지 분류하는 이진 분류 문제\n", + "\n", + "임베딩 층(Embedding) + 순환층(LSTM 또는 GRU) + 출력층(Dense) 구조를 동일하게 맞추고, 순환층만 LSTM ↔ GRU로 바꿔서 공정하게 비교." + ], + "id": "P6_dKvXCZWN4" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8k3SM-5gZWN5" + }, + "source": [ + "## 1단계. 라이브러리 불러오기 및 GPU 확인\n", + "\n", + "먼저 필요한 라이브러리를 불러오고, GPU가 잘 연결되어 있는지 확인합니다.\n", + "GPU가 없으면 학습 시간이 훨씬 오래 걸리니 꼭 확인해주세요!" + ], + "id": "8k3SM-5gZWN5" + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "2MSNFHZqZWN6", + "outputId": "4145b9c9-7eff-4b8a-a58d-db4572d8accb" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "사용 가능한 장치:\n", + "[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]\n", + "✅ GPU 연결 확인 완료!\n" + ] + } + ], + "source": [ + "import time\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "import tensorflow as tf\n", + "from tensorflow.keras.datasets import imdb\n", + "from tensorflow.keras.preprocessing.sequence import pad_sequences\n", + "from tensorflow.keras.models import Sequential\n", + "from tensorflow.keras.layers import Embedding, Dense\n", + "\n", + "# 재현성을 위한 시드 고정\n", + "tf.random.set_seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# GPU 사용 가능 여부 확인\n", + "print(\"사용 가능한 장치:\")\n", + "print(tf.config.list_physical_devices('GPU'))\n", + "if len(tf.config.list_physical_devices('GPU')) == 0:\n", + " print(\"⚠️ GPU가 연결되어 있지 않습니다. 연결 > 런타임 유형 변경 > T4 GPU로 설정해주세요.\")\n", + "else:\n", + " print(\"✅ GPU 연결 확인 완료!\")\n" + ], + "id": "2MSNFHZqZWN6" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PRQwMjr3ZWN8" + }, + "source": [ + "## 2단계. 데이터 준비 (IMDB 영화 리뷰)\n", + "\n", + "*Keras에는 IMDB 영화 리뷰 5만 건이 내장되어 있습니다.\n", + "단어는 이미 정수 인덱스로 변환되어 있어서 바로 학습에 사용할 수 있습니다.\n", + "\n", + "- `vocab_size`: 등장 빈도 상위 몇 개의 단어만 사용할지 (나머지는 무시)\n", + " \n", + " *keras에 있는 모든 단어를 사용하기에는 시간이 너무 오래걸림\n", + "- `max_len`: 리뷰 하나당 최대 단어 길이 (넘으면 자르고, 모자라면 0으로 채움 → padding)\n", + " \n", + " *문장 길이 맞추기" + ], + "id": "PRQwMjr3ZWN8" + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xPJIOBVeZWN8", + "outputId": "d012d845-62fe-46be-86d9-261bc9c78202" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz\n", + "\u001b[1m17464789/17464789\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 0us/step\n", + "학습 데이터 개수: 25000\n", + "테스트 데이터 개수: 25000\n", + "첫 번째 리뷰(정수 인덱스, 앞 20개): [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25]\n", + "첫 번째 리뷰의 라벨: 1 (1=긍정, 0=부정)\n", + "\n", + "padding 후 shape: (25000, 200) (샘플 수, 시퀀스 길이)\n" + ] + } + ], + "source": [ + "# 하이퍼파라미터 설정\n", + "vocab_size = 10000 # 상위 10,000개 단어만 사용\n", + "max_len = 200 # 리뷰 하나당 최대 200단어\n", + "embedding_dim = 32 # 임베딩 벡터 차원\n", + "\n", + "# 데이터 로드 (처음 실행 시 자동 다운로드됨)\n", + "(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=vocab_size)\n", + "\n", + "print(f\"학습 데이터 개수: {len(x_train)}\")\n", + "print(f\"테스트 데이터 개수: {len(x_test)}\")\n", + "print(f\"첫 번째 리뷰(정수 인덱스, 앞 20개): {x_train[0][:20]}\")\n", + "print(f\"첫 번째 리뷰의 라벨: {y_train[0]} (1=긍정, 0=부정)\")\n", + "\n", + "# 리뷰 길이를 max_len으로 통일 (padding)\n", + "x_train = pad_sequences(x_train, maxlen=max_len, padding='post', truncating='post')\n", + "x_test = pad_sequences(x_test, maxlen=max_len, padding='post', truncating='post')\n", + "\n", + "print(f\"\\npadding 후 shape: {x_train.shape} (샘플 수, 시퀀스 길이)\")\n" + ], + "id": "xPJIOBVeZWN8" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QSESN5JBZWN9" + }, + "source": [ + "## 3단계. 모델 설계\n", + "\n", + "**공정한 비교**를 위해 두 모델은 딱 한 층(LSTM ↔ GRU)만 다르고 나머지 구조는 동일합니다.\n", + "\n", + "```\n", + "입력 시퀀스\n", + " ↓\n", + "Embedding (단어 → 벡터)\n", + " ↓\n", + "LSTM 또는 GRU (시퀀스 정보 요약)\n", + " ↓\n", + "Dense (이진 분류, sigmoid)\n", + " ↓\n", + "출력 (0~1 사이 확률)\n", + "```" + ], + "id": "QSESN5JBZWN9" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bfVhznqBZWN-" + }, + "source": [ + "### 3-2. LSTM 셀 직접 구현해보기\n", + "\n", + "\n", + "LSTM은 셀 상태 $c_t$와 은닉 상태 $h_t$ 두 개를 함께 관리하고, 게이트가 3개(망각/입력/출력)입니다.\n", + "\n", + "$$f_t = \\sigma(x_t W_f + h_{t-1} U_f + b_f) \\quad \\text{(망각 게이트)}$$\n", + "$$i_t = \\sigma(x_t W_i + h_{t-1} U_i + b_i) \\quad \\text{(입력 게이트)}$$\n", + "$$o_t = \\sigma(x_t W_o + h_{t-1} U_o + b_o) \\quad \\text{(출력 게이트)}$$\n", + "$$\\tilde c_t = \\tanh(x_t W_c + h_{t-1} U_c + b_c) \\quad \\text{(후보 셀상태)}$$\n", + "$$c_t = f_t \\odot c_{t-1} + i_t \\odot \\tilde c_t \\quad \\text{(셀 상태 갱신)}$$\n", + "$$h_t = o_t \\odot \\tanh(c_t) \\quad \\text{(은닉 상태 갱신)}$$" + ], + "id": "bfVhznqBZWN-" + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "ru1OSd7xZWN-" + }, + "outputs": [], + "source": [ + "class LSTMCellCustom(tf.keras.layers.Layer):\n", + "\n", + " def __init__(self, units, **kwargs):\n", + " super().__init__(**kwargs)\n", + " self.units = units\n", + " self.state_size = [units, units] # LSTM은 상태가 2개(h, c)이므로 리스트로 지정\n", + " self.output_size = units\n", + "\n", + " def build(self, input_shape):\n", + " input_dim = input_shape[-1]\n", + " u = self.units\n", + "\n", + " # --- 망각 게이트(f_t) ---\n", + " self.Wf = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wf')\n", + " self.Uf = self.add_weight(shape=(u, u), initializer='orthogonal', name='Uf')\n", + " self.bf = self.add_weight(shape=(u,), initializer='ones', name='bf') # 초기값 1: 학습 초반 정보를 잘 유지하도록\n", + "\n", + " # --- 입력 게이트(i_t) ---\n", + " self.Wi = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wi')\n", + " self.Ui = self.add_weight(shape=(u, u), initializer='orthogonal', name='Ui')\n", + " self.bi = self.add_weight(shape=(u,), initializer='zeros', name='bi')\n", + "\n", + " # --- 출력 게이트(o_t) ---\n", + " self.Wo = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wo')\n", + " self.Uo = self.add_weight(shape=(u, u), initializer='orthogonal', name='Uo')\n", + " self.bo = self.add_weight(shape=(u,), initializer='zeros', name='bo')\n", + "\n", + " # --- 후보 셀상태(c_tilde) ---\n", + " self.Wc = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wc')\n", + " self.Uc = self.add_weight(shape=(u, u), initializer='orthogonal', name='Uc')\n", + " self.bc = self.add_weight(shape=(u,), initializer='zeros', name='bc')\n", + "\n", + " super().build(input_shape)\n", + "\n", + " def call(self, inputs, states):\n", + " # states[0]: 이전 은닉상태 h_{t-1}, states[1]: 이전 셀상태 c_{t-1}\n", + " x_t = inputs\n", + " h_prev, c_prev = states\n", + "\n", + " f_t = tf.sigmoid(tf.matmul(x_t, self.Wf) + tf.matmul(h_prev, self.Uf) + self.bf)\n", + " i_t = tf.sigmoid(tf.matmul(x_t, self.Wi) + tf.matmul(h_prev, self.Ui) + self.bi)\n", + " o_t = tf.sigmoid(tf.matmul(x_t, self.Wo) + tf.matmul(h_prev, self.Uo) + self.bo)\n", + " c_tilde = tf.tanh(tf.matmul(x_t, self.Wc) + tf.matmul(h_prev, self.Uc) + self.bc)\n", + " c_t = f_t * c_prev + i_t * c_tilde\n", + " h_t = o_t * tf.tanh(c_t)\n", + "\n", + "\n", + "\n", + "\n", + " return h_t, [h_t, c_t]\n" + ], + "id": "ru1OSd7xZWN-" + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " HINT \n", + "\n", + "tf.sigmoid, tf.matmul, tf.tanh 활용해서 식을 작성해주시면 됩니다 !\n", + "\n", + "
" + ], + "metadata": { + "id": "xvn2_QdAwqS8" + }, + "id": "xvn2_QdAwqS8" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5cKeTaVFZWN9" + }, + "source": [ + "### 3-1. GRU 셀 직접 구현해보기\n", + "\n", + "앞서 배운 GRU 수식을 그대로 코드로 옮기면 다음과 같습니다.\n", + "\n", + "$$r_t = \\sigma(x_t W_r + h_{t-1} U_r + b_r) \\quad \\text{(리셋 게이트)}$$\n", + "$$z_t = \\sigma(x_t W_z + h_{t-1} U_z + b_z) \\quad \\text{(업데이트 게이트)}$$\n", + "$$\\tilde h_t = \\tanh(x_t W_h + (r_t \\odot h_{t-1}) U_h + b_h) \\quad \\text{(후보 은닉상태)}$$\n", + "$$h_t = (1-z_t) \\odot h_{t-1} + z_t \\odot \\tilde h_t \\quad \\text{(최종 은닉상태)}$$\n", + "\n", + "`tf.keras.layers.RNN`이 시간 축(time step)을 따라 이 셀을 자동으로 반복 호출해줍니다.\n", + "우리는 **한 시점(time step)에서 무슨 계산이 일어나는지**만 `call()` 안에 정의하면 됩니다." + ], + "id": "5cKeTaVFZWN9" + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "Tk6p3EFlZWN9" + }, + "outputs": [], + "source": [ + "class GRUCellCustom(tf.keras.layers.Layer):\n", + "\n", + " def __init__(self, units, **kwargs):\n", + " super().__init__(**kwargs)\n", + " self.units = units\n", + " self.state_size = units\n", + " self.output_size = units\n", + "\n", + " def build(self, input_shape):\n", + " input_dim = input_shape[-1]\n", + " u = self.units\n", + "\n", + " # --- 리셋 게이트(r_t) 파라미터: W_r, U_r, b_r ---\n", + " self.Wr = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wr')\n", + " self.Ur = self.add_weight(shape=(u, u), initializer='orthogonal', name='Ur')\n", + " self.br = self.add_weight(shape=(u,), initializer='zeros', name='br')\n", + "\n", + " # --- 업데이트 게이트(z_t) 파라미터: W_z, U_z, b_z ---\n", + " self.Wz = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wz')\n", + " self.Uz = self.add_weight(shape=(u, u), initializer='orthogonal', name='Uz')\n", + " self.bz = self.add_weight(shape=(u,), initializer='zeros', name='bz')\n", + "\n", + " # --- 후보 은닉상태(h_tilde) 파라미터: W_h, U_h, b_h ---\n", + " self.Wh = self.add_weight(shape=(input_dim, u), initializer='glorot_uniform', name='Wh')\n", + " self.Uh = self.add_weight(shape=(u, u), initializer='orthogonal', name='Uh')\n", + " self.bh = self.add_weight(shape=(u,), initializer='zeros', name='bh')\n", + "\n", + " super().build(input_shape)\n", + "\n", + " def call(self, inputs, states):\n", + " # inputs: 현재 시점의 입력 x_t, states[0]: 이전 은닉상태 h_{t-1}\n", + " x_t = inputs\n", + " h_prev = states[0]\n", + "\n", + " r_t = tf.sigmoid(tf.matmul(x_t, self.Wr) + tf.matmul(h_prev, self.Ur) + self.br)\n", + " z_t = tf.sigmoid(tf.matmul(x_t, self.Wz) + tf.matmul(h_prev, self.Uz) + self.bz)\n", + " h_tilde = tf.tanh(tf.matmul(x_t, self.Wh) + tf.matmul(r_t * h_prev, self.Uh) + self.bh)\n", + " h_t = (1 - z_t) * h_prev + z_t * h_tilde\n", + "\n", + "\n", + "\n", + "\n", + " return h_t, [h_t]\n" + ], + "id": "Tk6p3EFlZWN9" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "58yZ7aEOZWN_" + }, + "source": [ + "### 3-3. 모델 불러오기\n", + "\n", + "실제로는 tensorflow에서 LSTM과 GRU를 제공합니다. 라이브러리 호출해서 비교해보겠습니다." + ], + "id": "58yZ7aEOZWN_" + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "wuxr6T5yZWN_" + }, + "outputs": [], + "source": [ + "from tensorflow.keras.layers import SimpleRNN, LSTM, GRU\n", + "\n", + "RNN_units = 32\n", + "batch_size = 64\n", + "epochs = 10\n", + "\n", + "# 모델 생성 함수 정의\n", + "def build_model(cell_type):\n", + " model = Sequential()\n", + " # 1) 단어 임베딩: 입력 시퀀스를 EMB_DIM 차원 벡터로 변환\n", + " model.add(Embedding(input_dim=vocab_size,\n", + " output_dim=embedding_dim,\n", + " input_length=max_len))\n", + " # 2) 순환 셀 추가\n", + " if cell_type == 'RNN':\n", + " model.add(SimpleRNN(RNN_units))\n", + " elif cell_type == 'LSTM':\n", + " model.add(LSTM(RNN_units))\n", + " elif cell_type == 'GRU':\n", + " model.add(GRU(RNN_units))\n", + " # 3) 출력층: sigmoid 활성화로 이진 분류\n", + " model.add(Dense(1, activation='sigmoid'))\n", + "\n", + " # 4) 컴파일: Adam 옵티마이저, binary_crossentropy 손실, 정확도 지표\n", + " model.compile(optimizer='adam',\n", + " loss='binary_crossentropy',\n", + " metrics=['accuracy'])\n", + " return model" + ], + "id": "wuxr6T5yZWN_" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "S2IDwgGcZWN_" + }, + "source": [ + "**참고**: `summary()` 결과에서 `Total params`를 비교해보세요.\n", + "같은 `hidden_units`를 사용해도 LSTM은 게이트(가중치 세트)가 4개(f, i, o, c_tilde), GRU는 3개(r, z, h_tilde)이기 때문에\n", + "**LSTM의 파라미터 수가 GRU보다 약 1.33배(≈4/3) 많습니다.**\n", + "(순환층 파라미터 수: LSTM = 4 × (게이트당 파라미터), GRU = 3 × (게이트당 파라미터))\n", + "\n", + "> 💡 `tf.keras.layers.LSTM` / `GRU` 같은 내장 레이어는 내부적으로 cuDNN 가속을 사용해서\n", + "> 우리가 만든 커스텀 셀보다 학습 속도가 훨씬 빠릅니다. 즉, 이번 실습의 속도 비교는\n", + "> **\"직접 구현한 두 모델끼리의 상대적인 속도 차이\"**를 보는 것이지, 실무에서 쓰는\n", + "> 최적화된 라이브러리 성능을 재는 것은 아니라는 점을 기억해주세요." + ], + "id": "S2IDwgGcZWN_" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SURbU0TWZWN_" + }, + "source": [ + "## 4단계. 모델 학습 및 결과 저장\n", + "\n" + ], + "id": "SURbU0TWZWN_" + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XU4UTrkPZWN_", + "outputId": "c90e20bd-71f3-4725-9835-001e70540cd4" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "▶ RNN 모델 학습 시작...\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/usr/local/lib/python3.12/dist-packages/keras/src/layers/core/embedding.py:100: UserWarning: Argument `input_length` is deprecated. Just remove it.\n", + " warnings.warn(\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Epoch 1/10\n", + "313/313 - 10s - 33ms/step - accuracy: 0.5021 - loss: 0.6941 - val_accuracy: 0.5048 - val_loss: 0.6931\n", + "Epoch 2/10\n", + "313/313 - 5s - 16ms/step - accuracy: 0.6156 - loss: 0.6402 - val_accuracy: 0.5000 - val_loss: 0.7170\n", + "Epoch 3/10\n", + "313/313 - 10s - 32ms/step - accuracy: 0.6973 - loss: 0.5078 - val_accuracy: 0.5036 - val_loss: 0.8171\n", + "Epoch 4/10\n", + "313/313 - 5s - 16ms/step - accuracy: 0.7357 - loss: 0.4163 - val_accuracy: 0.5042 - val_loss: 0.8924\n", + "Epoch 5/10\n", + "313/313 - 5s - 15ms/step - accuracy: 0.7560 - loss: 0.3784 - val_accuracy: 0.5174 - val_loss: 0.9658\n", + "Epoch 6/10\n", + "313/313 - 5s - 17ms/step - accuracy: 0.7663 - loss: 0.3681 - val_accuracy: 0.5098 - val_loss: 1.0417\n", + "Epoch 7/10\n", + "313/313 - 5s - 16ms/step - accuracy: 0.7600 - loss: 0.3880 - val_accuracy: 0.4982 - val_loss: 1.0328\n", + "Epoch 8/10\n", + "313/313 - 5s - 15ms/step - accuracy: 0.7667 - loss: 0.3621 - val_accuracy: 0.4902 - val_loss: 1.1012\n", + "Epoch 9/10\n", + "313/313 - 5s - 16ms/step - accuracy: 0.7850 - loss: 0.3369 - val_accuracy: 0.5008 - val_loss: 1.1349\n", + "Epoch 10/10\n", + "313/313 - 5s - 15ms/step - accuracy: 0.8006 - loss: 0.3220 - val_accuracy: 0.4924 - val_loss: 1.2362\n", + " RNN → 평균 에폭 시간: 6.02초 \n", + "▶ LSTM 모델 학습 시작...\n", + "Epoch 1/10\n", + "313/313 - 8s - 27ms/step - accuracy: 0.5608 - loss: 0.6739 - val_accuracy: 0.6004 - val_loss: 0.6553\n", + "Epoch 2/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.5709 - loss: 0.6729 - val_accuracy: 0.5442 - val_loss: 0.6780\n", + "Epoch 3/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.6126 - loss: 0.6609 - val_accuracy: 0.5918 - val_loss: 0.6707\n", + "Epoch 4/10\n", + "313/313 - 4s - 14ms/step - accuracy: 0.6091 - loss: 0.6575 - val_accuracy: 0.6004 - val_loss: 0.6617\n", + "Epoch 5/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.6431 - loss: 0.6425 - val_accuracy: 0.6194 - val_loss: 0.6492\n", + "Epoch 6/10\n", + "313/313 - 4s - 13ms/step - accuracy: 0.6646 - loss: 0.6153 - val_accuracy: 0.6476 - val_loss: 0.6280\n", + "Epoch 7/10\n", + "313/313 - 4s - 13ms/step - accuracy: 0.7097 - loss: 0.5664 - val_accuracy: 0.7936 - val_loss: 0.4992\n", + "Epoch 8/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.8437 - loss: 0.3979 - val_accuracy: 0.8228 - val_loss: 0.4306\n", + "Epoch 9/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.8781 - loss: 0.3322 - val_accuracy: 0.8348 - val_loss: 0.4250\n", + "Epoch 10/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.9107 - loss: 0.2614 - val_accuracy: 0.8468 - val_loss: 0.3997\n", + " LSTM → 평균 에폭 시간: 4.11초 \n", + "▶ GRU 모델 학습 시작...\n", + "Epoch 1/10\n", + "313/313 - 6s - 19ms/step - accuracy: 0.5142 - loss: 0.6923 - val_accuracy: 0.5380 - val_loss: 0.6846\n", + "Epoch 2/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.5890 - loss: 0.6610 - val_accuracy: 0.6130 - val_loss: 0.6306\n", + "Epoch 3/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.7889 - loss: 0.4710 - val_accuracy: 0.8274 - val_loss: 0.4288\n", + "Epoch 4/10\n", + "313/313 - 5s - 16ms/step - accuracy: 0.8267 - loss: 0.4323 - val_accuracy: 0.8112 - val_loss: 0.4926\n", + "Epoch 5/10\n", + "313/313 - 3s - 10ms/step - accuracy: 0.8712 - loss: 0.3512 - val_accuracy: 0.8264 - val_loss: 0.4710\n", + "Epoch 6/10\n", + "313/313 - 3s - 11ms/step - accuracy: 0.8885 - loss: 0.3046 - val_accuracy: 0.8414 - val_loss: 0.4306\n", + "Epoch 7/10\n", + "313/313 - 4s - 13ms/step - accuracy: 0.9162 - loss: 0.2371 - val_accuracy: 0.8410 - val_loss: 0.4236\n", + "Epoch 8/10\n", + "313/313 - 3s - 10ms/step - accuracy: 0.9384 - loss: 0.1874 - val_accuracy: 0.8532 - val_loss: 0.4138\n", + "Epoch 9/10\n", + "313/313 - 4s - 11ms/step - accuracy: 0.9555 - loss: 0.1483 - val_accuracy: 0.8558 - val_loss: 0.4300\n", + "Epoch 10/10\n", + "313/313 - 4s - 14ms/step - accuracy: 0.9671 - loss: 0.1208 - val_accuracy: 0.8546 - val_loss: 0.4479\n", + " GRU → 평균 에폭 시간: 3.96초 \n" + ] + } + ], + "source": [ + "# 에포크 타이머\n", + "import time\n", + "\n", + "class EpochTimer(tf.keras.callbacks.Callback):\n", + " def on_train_begin(self, logs=None):\n", + " self.epoch_times = []\n", + "\n", + " def on_epoch_begin(self, epoch, logs=None):\n", + " self._epoch_start = time.time()\n", + "\n", + " def on_epoch_end(self, epoch, logs=None):\n", + " elapsed = time.time() - self._epoch_start\n", + " self.epoch_times.append(elapsed)\n", + "\n", + "results = {}\n", + "for cell in ['RNN', 'LSTM', 'GRU']:\n", + " print(f'▶ {cell} 모델 학습 시작...')\n", + " model = build_model(cell)\n", + " timer = EpochTimer()\n", + "\n", + " history = model.fit(\n", + " x_train, y_train,\n", + " epochs=epochs,\n", + " batch_size=batch_size,\n", + " validation_split=0.2,\n", + " verbose=2,\n", + " callbacks=[timer]\n", + " )\n", + "\n", + " results[cell] = {\n", + " 'history': history.history,\n", + " 'avg_epoch_time': np.mean(timer.epoch_times),\n", + " }\n", + "\n", + " print(f' {cell} → 평균 에폭 시간: {results[cell][\"avg_epoch_time\"]:.2f}초 ')" + ], + "id": "XU4UTrkPZWN_" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qZZ7z7RBZWN_" + }, + "source": [ + "## 5단계. 학습 결과 시각화" + ], + "id": "qZZ7z7RBZWN_" + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 519 + }, + "id": "Tl0qSzLHZWOA", + "outputId": "02f504f5-865a-4c53-d864-594925126cd0" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 44160 (\\N{HANGUL SYLLABLE GEOM}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 51613 (\\N{HANGUL SYLLABLE JEUNG}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 51221 (\\N{HANGUL SYLLABLE JEONG}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 54869 (\\N{HANGUL SYLLABLE HWAG}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 46020 (\\N{HANGUL SYLLABLE DO}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 48708 (\\N{HANGUL SYLLABLE BI}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n", + "/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 44368 (\\N{HANGUL SYLLABLE GYO}) missing from font(s) DejaVu Sans.\n", + " fig.canvas.print_figure(bytes_io, **kw)\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/IAAAHWCAYAAADUwLIxAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAr1RJREFUeJzs3Xd8VFX6x/HPzKR3IKQQAglVOkoTKaJSxQI2EKVZV8WGqGBDVGDdVcQVlf3tClHUBbEgKiIRxUpRFBGkhRZagADpZSaZ+f0xZMiQBDKQ5KZ835pXbs4998xzww3kmdNMDofDgYiIiIiIiIjUCGajAxARERERERGR8lMiLyIiIiIiIlKDKJEXERERERERqUGUyIuIiIiIiIjUIErkRURERERERGoQJfIiIiIiIiIiNYgSeREREREREZEaRIm8iIiIiIiISA2iRF5ERERERESkBlEiLyIiIiIiIlKDKJEXEZEKk5CQgMlkcn14eXkRExPDuHHjOHDgQIn6/fr1w2QycfXVV5c4t2fPHkwmEy+99JKrbNWqVa62169fX+KacePGERQUVLE3dQ7KG8eff/7JDTfcQNOmTfHz8yMmJoYBAwbw2muvAfDss8+6fT/L+ujXr5/rdU0mEyEhIeTm5pZ4vR07driuKf59NVJGRgbTp0+na9euhIaG4uvrS9OmTRkxYgRffPGFW93if/4mkwmLxUJERAQ33HADW7ZsKdF2v379aN++famvm5qaislk4tlnn62M2xIREalUXkYHICIitc9zzz1HfHw8eXl5rFmzhoSEBH788Uc2bdqEn59fifqff/4569evp0uXLuV+jWeffZbPPvusIsOuUj///DOXXXYZTZo04c477yQqKop9+/axZs0aXn31Ve6//36uu+46WrRo4bomKyuLe+65h+HDh3Pddde5yiMjI13HXl5e5OTk8Nlnn3HTTTe5veZ7772Hn58feXl5lX+D5ZCUlMSgQYPYu3cvw4cPZ8yYMQQFBbFv3z6WLVvGVVddxTvvvMPo0aPdrnvggQfo1q0bNpuNjRs3MnfuXFatWsWmTZuIioqq0BijoqLIysoq9VxBQQFvvPEGt912W7nrlWbEiBF89tlnmM0l+1fsdjs33ngjb7/9doXXK80bb7zBxIkT8fIq/VfE8PBw9uzZU+56IiJSOZTIi4hIhRsyZAhdu3YF4I477iA8PJwXX3yRpUuXlkgumzRpQmZmJtOmTWPp0qXlar9z5858/vnn/Pbbb1x00UUVHn9VmD59OqGhofzyyy+EhYW5nTty5AgAHTt2pGPHjq7y1NRU7rnnHjp27Mitt95aaru+vr706tWL//3vfyW+1++//z5Dhw7lo48+qtibOQcFBQUMHz6cw4cP891339GrVy+381OnTmXFihUUFhaWuLZPnz7ccMMNrq9bt27NPffcwzvvvMNjjz1W4XGmpaWVmrBOnjwZu93uUb3SFBYWsnTpUvr371/i3PLly3n33XcrpV5p7HY7kyZN4oUXXihxLi8vz/XGUnnriYhI5dDQehERqXR9+vQBYOfOnSXOBQcH8/DDD/PZZ5/x22+/lau9+++/n3r16p3TsOiXXnoJk8nE3r17S5ybMmUKPj4+nDhxAnAORb/++uuJiorCz8+Pxo0bM3LkSNLT0z1+3dPt3LmTdu3alUjiASIiIs6r7VGjRvHll1+SlpbmKvvll1/YsWMHo0aNOuv1NpuN+vXrM378+BLnMjIy8PPzY9KkSa6y1157jXbt2hEQEEC9evXo2rUr77///hlfY/HixWzatImnn366RBJfZODAgQwZMuSs8Z7p+RIREamNlMiLiEilKxpiW69evVLPP/jggx4l5iEhIR4n/0VuuukmTCYTH3zwQYlzH3zwAQMHDqRevXpYrVYGDRrEmjVruP/++3n99de566672LVrl1uCfK6aNm3K+vXr2bRp03m3dbrrrrsOk8nExx9/7Cp7//33ueCCC8o1gsHb25vhw4ezZMkSrFar27klS5aQn5/PyJEjAfjPf/7DAw88QNu2bZk9ezbTpk2jc+fOrF279oyvUTQtoqyRBZ442/MlIiJS22hovYiIVLj09HRSU1PJy8tj7dq1TJs2DV9fX6666qpS64eEhPDQQw8xderUcg+Xf+CBB3jllVeYNm0an376ablja9KkCRdffDGLFi3i0UcfdZX/8ssv7Nq1y/Vmwl9//cXu3btZvHix2zDuZ555ptyvdSaTJk1iyJAhdO7cme7du9OnTx+uuOIKLrvsMry9vc+r7eDgYK666iref/99brvtNux2OwsXLuSee+4pdxsjRoxg3rx5rFixwu3PbdGiRTRr1sw1deKLL76gXbt2LF682KMYt27dSlhYGDExMW7l2dnZbgv1+fj4EBIS4lYnMzOT1NRU1xz5hx56CJPJxPXXX+9RDCIiIjWVeuRFRKTC9e/fn4YNGxIbG8sNN9xAYGAgS5cupXHjxmVeU9QrP23atHK9RmhoKA899BBLly7l999/9yi+ESNGsH79ereh2IsWLcLX15drr73W1T7AV199RU5Ojkftl8eAAQNYvXo111xzDX/88Qf/+Mc/GDRoEDExMeVeK+BMRo0axapVq0hJSeGbb74hJSWlXMPqi1x++eWEh4ezaNEiV9mJEydITExkxIgRrrKwsDD279/PL7/84lF8GRkZpa7s/+STT9KwYUPXR2kx33bbbTRs2JBGjRoxePBg0tPTWbBgAd26dfMoBhERkZpKibyIiFS4119/ncTERD788EOuvPJKUlNT8fX1PeM155KYP/jgg4SFhXk8V/7GG2/EbDa7klSHw8HixYsZMmSIq/c3Pj6eiRMn8t///pfw8HAGDRrE66+/XiHz44t069aNjz/+mBMnTrBu3TqmTJlCZmYmN9xwA3/99dd5tX3llVcSHBzMokWLeO+99+jWrZtHC5B5eXlx/fXX8+mnn5Kfnw/Axx9/jM1mc0vkH3/8cYKCgujevTstW7bkvvvu46effjpr+8HBwaWu8n7vvfeSmJhIYmKi22r8xT3zzDMkJibyySefMGbMGNLT00tdob08TCbTOV0nIiJiJCXyIiJS4bp3707//v25/vrrWbp0Ke3bt2fUqFFlbs9VpCgxr+xe+UaNGtGnTx/XPPk1a9aQnJzslqACvPzyy2zcuJEnnniC3NxcHnjgAdq1a8f+/fvL/Vrl4ePjQ7du3ZgxYwZvvvkmNpvN46Hqp/P19eW6667j7bff5pNPPvGoN77IyJEjyczM5MsvvwScawhccMEFdOrUyVWnTZs2bNu2jYULF9K7d28++ugjevfuzdSpU8/Y9gUXXEBaWhoHDhxwK2/VqhX9+/enf//+pW5VCNChQwf69+/PsGHDePvtt7nmmmu488472bdvn1s9Pz8/t2H6xRWNsijrNURERKozJfIiIlKpLBYLM2fO5ODBg8yZM+eMdYsS808//bTciflDDz3kUfJfZMSIEfzxxx9s27aNRYsWERAQwNVXX12iXocOHXjqqaf4/vvv+eGHHzhw4ABz58716LU8UTT3/NChQ+fd1qhRo/j999/JzMx0LU7nib59+xIdHc2iRYtITU3lm2++KfFmB0BgYCAjRoxg/vz5JCcnM3ToUKZPn37G/eqL5t2/9957Hsd1ur///e/k5eUxffp0t/KmTZuyb9++UpP5bdu2ueqIiIjUNErkRUSk0vXr14/u3bsze/bsMyZ3cCoxf+6558rVdvHkf8OGDeWO6frrr8disfC///2PxYsXc9VVVxEYGOg6n5GRQUFBgds1HTp0wGw2u4aan49vv/0Wh8NRonzZsmWAc2/083XZZZfx/PPPM2fOHKKiojy+3mw2c8MNN/DZZ5+xYMECCgoKSiTyx44dc/vax8eHtm3b4nA4sNlsZbZ900030bZtW55//nnWrFlTap3Svj+lad68Oddffz0JCQmkpKS4yq+88kpsNhv//ve/3erb7XbefPNNfHx8uOKKK8r1GiIiItWJVq0XEZEq8eijj3LjjTeSkJDA3/72tzLrhYaG8uCDD3rUw/7ggw/yyiuv8Mcff7gl42cSERHBZZddxqxZs8jMzCyRoH7zzTdMmDCBG2+8kVatWlFQUMCCBQuwWCzlWh3dZrPxwgsvlCivX78+9957L/fffz85OTkMHz6cCy64AKvVys8//8yiRYuIi4srdQ93T5nNZp566qnzamPEiBG89tprTJ06lQ4dOtCmTRu38wMHDiQqKopevXoRGRnJli1bmDNnDkOHDiU4OLjMdr29vfnkk08YNGgQvXv35rrrrqNPnz4EBgZy4MABli5d6urdL49HH32UDz74gNmzZ/P3v/8dgKuvvpqBAwfy8MMPs27dOi655BJycnJYunQpP/30Ey+88AINGzY892+OiIiIQZTIi4hIlbjuuuto3rw5L730EnfeeScWi6XMug899BCzZ88u98JyYWFhPPTQQ+c0vP7rr78mODiYK6+80u1cp06dGDRoEJ999hkHDhwgICCATp068eWXX3LxxReftW2r1crTTz9dorx58+bce++9vPTSSyxevJhly5bxf//3f1itVpo0acK9997LU089RVhYmEf3UlkuueQSYmNj2bdvX6nD6u+++27ee+89Zs2aRVZWFo0bN+aBBx4o1xsIrVq1YsOGDfzrX//ik08+4csvv8RqtRIZGUmPHj2YOnVqmVsWnq5r167069ePN998kylTphAaGorZbGbp0qX8/e9/Z+HChXz88cd4eXnRoUMH3n33XW655RaPvx8iIiLVgRJ5ERGpMOPGjWPcuHGlnjObzSQlJbmVrVq1qtS6YWFhpKWllSjv169fmcOtn332WY9Xr7/jjju44447Sj0XHx/PW2+95VF7RRISEkhISDhjncGDBzN48GCP2g0PDz/jcPPyvG5cXFy5h6yDc1X35OTkMs/fdddd3HXXXeVu73ShoaE8/fTTpb7pcboz/fmDc7rC6Xx9fZk6depZF98TERGpSTRHXkRERERERKQGUY+8iIiIyBmEh4eXWp6Xl+e2E0N565Vm2LBheHmV/LWsoKCAYcOGVVq90rz00ktlxhsUFORxPRERqXgmhyfj60RERERERETEUBpaLyIiIiIiIlKDKJEXERERERERqUGUyIuIiIiIiIjUIFrsrhR2u52DBw8SHByMyWQyOhwRERERERGp5RwOB5mZmTRq1Aiz+cx97krkS3Hw4EFiY2ONDkNERERERETqmH379tG4ceMz1lEiX4rg4GDA+Q0MCQkxOJqy2Ww2VqxYwcCBA/H29jY6HJFKpedd6hI971KX6HmXukTPu5xJRkYGsbGxrnz0TJTIl6JoOH1ISEi1T+QDAgIICQnRXwRS6+l5l7pEz7vUJXrepS7R8y7lUZ7p3VrsTkRERERERKQGUSIvIiIiIiIiUoMokRcRERERERGpQTRH/hw5HA4KCgooLCw0LAabzYaXlxd5eXmGxiHuLBYLXl5e2rpQREREREQqhRL5c2C1Wjl06BA5OTmGxuFwOIiKimLfvn1KGquZgIAAoqOj8fHxMToUERERERGpZZTIe8hut7N7924sFguNGjXCx8fHsCTabreTlZVFUFAQZrNmSVQHDocDq9XK0aNH2b17Ny1bttSfjYiIiIiIVCgl8h6yWq3Y7XZiY2MJCAgwNBa73Y7VasXPz0/JYjXi7++Pt7c3e/fudf35iIiIiIiIVBRlf+dIibOciZ4PERERERGpLMo2RERERERERGoQJfIiIiIiIiIiNYgSeREREREREZEaRIm8iIiIiIiISA2iVevriHHjxvH2228D4OXlRePGjbnxxht57rnn3FZVN5lM+Pr6sm3bNpo2beoqHzZsGGFhYSQkJLi1N3PmTCZPnuyqt2TJEoYPH47D4Sg1jhdffJEFCxbg5eX+6FmtVp588kkuvvhihgwZUuqOAPHx8XzyySclyt977z2mT59eYs/2goICRo8e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E6n1inrjr2lxDeH+4cYGIyIiJRXkw5J7wV4Aba6B9tcbHZGIiNRxHvfI5+TkEBlZ8l3oiIgIDa0X8dCutF2s2r8KEybGth1rdDgiIlKa7/4BRzZDQAMYOktD6kVExHAeJ/I9e/Zk6tSp5OXlucpyc3OZNm0aPXv2rNDgRGq7opXqL4u9jLjQOENjERGRUhxYDz++4jwe+jIEaXFfERExnsdD61999VUGDRpE48aN6dSpEwB//PEHfn5+fPXVVxUeoEhtdTTnKJ/v+hyA8e3HGxyNiIiUYMtzDql3FEK766CddhUREZHqweNEvn379uzYsYP33nuPrVu3AnDzzTdzyy234O/vX+EBitRW7215D5vdxoURF9I5orPR4YiIyOlWzYSjWyGwoXOBOxERkWrC46H1AAEBAdx55528/PLLvPzyy9xxxx3nnMS//vrrxMXF4efnR48ePVi3bt0Z66elpXHfffcRHR2Nr68vrVq1YtmyZa7zzz77LCaTye3jggsuOKfYRCpLti2bD7Z9AMC4duOMDUZEREra9wv8/C/n8VWvQGADY+MREREpplw98kuXLmXIkCF4e3uzdOnSM9a95ppryv3iixYtYuLEicydO5cePXowe/ZsBg0axLZt24iIiChR32q1MmDAACIiIvjwww+JiYlh7969hIWFudVr164dX3/9tetrLy+PBx6IVKqPtn9Epi2TuJA4+sX2MzocEREpzpYLS+4Bhx063ARtrjY6IhERETflynCHDRtGSkoKERERDBs2rMx6JpOJwsLCcr/4rFmzuPPOOxk/3jk/eO7cuXzxxRfMmzePyZMnl6g/b948jh8/zs8//4y3tzcAcXFxJep5eXkRFRVV7jhEqpLNbmPBlgUAjG03FrPpnAbGiIhIZfnmBTi2A4IiYciLRkcjIiJSQrkSebvdXurx+bBaraxfv54pU6a4ysxmM/3792f16tWlXrN06VJ69uzJfffdx6effkrDhg0ZNWoUjz/+OBaLxVVvx44dNGrUCD8/P3r27MnMmTNp0qRJmbHk5+eTn5/v+jojIwMAm82GzWY731utNEWxVecYpaQvdn9BSnYKDfwaMLjJYP35lZOed6lL9Lwbx7RvLZbVr2MCCoa8jMM7GPTnUKn0vEtdouddzsST58LjMefvvPMOI0aMwNfX163carWycOFCxowZU652UlNTKSwsLLEnfWRkpGsRvdPt2rWLb775hltuuYVly5aRlJTEvffei81mY+rUqQD06NGDhIQEWrduzaFDh5g2bRp9+vRh06ZNBAcHl9ruzJkzmTZtWonyFStWEBAQUK77MVJiYqLRIUg5ORwOXs98HYALuZCVX600OKKaR8+71CV63quWxZ5Pv61PEYSD5Pq9+T3JDknLzn6hVAg971KX6HmX0uTk5JS7rsnhcDg8adxisXDo0KESc9iPHTtGREREuYfWHzx4kJiYGH7++We3/ecfe+wxvvvuO9auXVvimlatWpGXl8fu3btdPfCzZs3in//8J4cOHSr1ddLS0mjatCmzZs3i9ttvL7VOaT3ysbGxpKamEhISUq77MYLNZiMxMZEBAwa4phpI9bb60Gru+/Y+/L38+XLYl4T4VN/nq7rR8y51iZ53A6Tvw/Ld3zH/uQhHcDQFd/0IfqFGR1Un6HmXukTPu5xJRkYG4eHhpKennzUP9bhH3uFwYDKZSpTv37+f0NDy/4MXHh6OxWLh8OHDbuWHDx8uc357dHQ03t7ebsPo27RpQ0pKClarFR8fnxLXhIWF0apVK5KSksqMxdfXt8QIAwBvb+8a8QNWU+IUWLDVOTf++pbX00ArIJ8TPe9Sl+h5r0QFVkheDTtWQNLXzm3mTjJdMwfv4HADg6ub9LxLXaLnXUrjyTNR7kT+wgsvdG3ndsUVV7itBF9YWMju3bsZPHhwuV/Yx8eHLl26sHLlStcCena7nZUrVzJhwoRSr+nVqxfvv/8+drsds9m5QNj27duJjo4uNYkHyMrKYufOnYwePbrcsYlUhi3HtrDm0BosJguj2+p5FBGpcukHICkRdiTCrlVgzTp1zmSG2B7QZRy07G9UhCIiIuVS7kS+KNnesGEDgwYNIigoyHXOx8eHuLg4rr/+eo9efOLEiYwdO5auXbvSvXt3Zs+eTXZ2tmsV+zFjxhATE8PMmTMBuOeee5gzZw4PPvgg999/Pzt27GDGjBk88MADrjYnTZrE1VdfTdOmTTl48CBTp07FYrFw8803exSbSEVL2JwAwMC4gTQKamRsMCIidUGhDfatdfa67/gajmx2Px/YEFoMgJYDoPll4F/PmDhFREQ8VO5Evmgxubi4OEaMGIGfn995v/iIESM4evQozzzzDCkpKXTu3Jnly5e7FsBLTk529bwDxMbG8tVXX/Hwww/TsWNHYmJiePDBB3n88cdddfbv38/NN9/MsWPHaNiwIb1792bNmjU0bNjwvOMVOVcHsw7y1Z6vABjfbrzB0YiI1GIZh5xD5XescPa652cUO2mCxt2ciXvLARDVCczaAlRERGoej+fIjx07tkIDmDBhQplD6VetWlWirGfPnqxZs6bM9hYuXFhRoYlUmAV/LaDQUcjF0RfTpkEbo8MREak9Cgtg/y8n57onQsqf7ucDGkCL/tByIDS/HALqGxOniIhIBfI4kS8sLOSVV17hgw8+IDk5GavV6nb++PHjFRacSG2Qnp/ORzs+AtQbLyJSIbKOnOp13/kN5KUXO2mCmItODpkfCI06g9lSVksiIiI1kseJ/LRp0/jvf//LI488wlNPPcWTTz7Jnj17WLJkCc8880xlxChSo32w7QNyC3JpXa81PRv1PPsFIiLizl4IB9afnOueCIc2uJ/3rwfNrzg51/0KCNJ0OhERqd08TuTfe+89/vOf/zB06FCeffZZbr75Zpo3b07Hjh1Zs2aN28JzInVdfmE+7215D4Cx7caWunWjiIiUIjsVklae7HVfCbkn3M9Hdz45130gxHRRr7uIiNQpHifyKSkpdOjQAYCgoCDS053D2a666iqefvrpio1OpIb7fOfnHMs7RlRgFIPjy789o4hInWO3w8HfT811P/Ab4Dh13jcUWlx+cq77FRAcaVioIiIiRvM4kW/cuDGHDh2iSZMmNG/enBUrVnDRRRfxyy+/4OvrWxkxitRIdofdteXcrW1uxdvsbWxAIiLVTc5x5xz3HSucc95zjrmfj+pwaq57425g8fjXFhERkVrJ438Rhw8fzsqVK+nRowf3338/t956K2+99RbJyck8/PDDlRGjSI20at8q9mTsIdg7mBta3WB0OCIixrPbIeUP5zz3HYlw4Fdw2E+d9wl27ufecoBzpfmQRsbFKiIiUo15nMj//e9/dx2PGDGCJk2asHr1alq2bMnVV19docGJ1GRFvfE3tb6JQO9AY4MRETFK7gnY+a0zcU/6GrKPuJ+PaActT24PF9sDLBq9JCIicjbnPUatZ8+e9OyplbhFittwZAO/H/kdb7M3t7S5xehwRESqjsPh3Mu9aLj8vnXgKDx13icImvU71ese2tiwUEVERGqqciXyS5cuLXeD11xzzTkHI1JbFPXGX9Xs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\n" + }, + "metadata": {} + } + ], + "source": [ + "plt.figure(figsize=(12, 5))\n", + "for cell in results:\n", + " plt.plot(results[cell]['history']['val_accuracy'],\n", + " label=f'{cell} 검증 정확도')\n", + "plt.title('RNN vs LSTM vs GRU 검증 정확도 비교')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Validation Accuracy')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()" + ], + "id": "Tl0qSzLHZWOA" + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9kkoa_QYZWOB" + }, + "source": [ + "## 6단계. 생각해봐주세요 !\n", + "\n", + "실습 결과를 바탕으로 아래 질문에 답해보세요.\n", + "\n", + "1. **정확도**: RNN, LSTM과 GRU 중 어느 모델의 테스트 정확도가 더 높나요? 그 차이가 통계적으로 유의미해 보이는 수준인가요, 아니면 오차 범위 수준인가요? 결과가 왜 이렇게 나왔는지 이유까지 설명해주세요.\n", + "2. **속도**: 학습 시간과 추론 시간에서 GRU가 LSTM보다 빨랐나요? 그 차이가 통계적으로 유의미해 보이는 수준인가요, 아니면 오차 범위 수준인가요? 결과가 왜 이렇게 나왔는지 이유까지 설명해주세요.\n", + "3. **하이퍼파라미터 실험**: `RNN_units`나 `epochs`, `max_len`, 'batch_size' 등을 바꿔가며 다시 실행해보고, 결과가 어떻게 달라지는지 관찰해보세요." + ], + "id": "9kkoa_QYZWOB" + }, + { + "cell_type": "markdown", + "source": [ + "**정답을 원하는 것이 아닙니다. 실습만 해보는 것이 아니라 결과에 대해 고민해보는 시간을 가지셨으면 좋겠습니다 ! 그럼 다들 BASE 활동 파이팅 하세요 😊**" + ], + "metadata": { + "id": "-u17NcA63Tmr" + }, + "id": "-u17NcA63Tmr" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10" + }, + "colab": { + "provenance": [], + "gpuType": "T4" + }, + "accelerator": "GPU" + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file