{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":4477}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import zipfile\nwith zipfile.ZipFile(\"/kaggle/input/competitions/grasp-and-lift-eeg-detection/test.zip\",\"r\") as z:\n    z.extractall(\".\")\nwith zipfile.ZipFile(\"/kaggle/input/competitions/grasp-and-lift-eeg-detection/train.zip\",\"r\") as z:\n    z.extractall(\".\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T16:22:09.367650Z","iopub.execute_input":"2026-09-02T16:22:09.368594Z","iopub.status.idle":"2026-09-02T16:22:36.058345Z","shell.execute_reply.started":"2026-09-02T16:22:09.368556Z","shell.execute_reply":"2026-09-02T16:22:36.056320Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visual data","metadata":{}},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom sklearn.preprocessing import StandardScaler\n\n# ==========================================\n# 1. READ & PREPROCESSING DATA\n# ==========================================\ndef prepare_data_train(fname):\n    \"\"\"Read and prepare training data\"\"\"\n    data = pd.read_csv(fname)\n    events_fname = fname.replace(\"_data\", \"_events\")\n    labels = pd.read_csv(events_fname)\n    clean = data.drop([\"id\"], axis=1)\n    labels = labels.drop([\"id\"], axis=1)\n    return clean, labels\n\n\nscaler = StandardScaler()\n\n\ndef data_preprocess_train(X):\n    return scaler.fit_transform(X)\n\n\n# ==========================================\n# 2. LOAD DATA UNTUK SEMUA SUBJECT\n# ==========================================\nsubjects = range(1, 6)\nraw = []\ny_raw = []\n\nfor subject in subjects:\n    fnames = sorted(glob(\"train/subj%d_series*_data.csv\" % (subject)))\n    for fname in fnames:\n        data, labels = prepare_data_train(fname)\n        raw.append(data)\n        y_raw.append(labels)\n\nX = pd.concat(raw)\ny = pd.concat(y_raw)\n\nX_train = np.asarray(X.astype(np.float32))\ny_train = np.asarray(y.astype(np.float32))\n\n# Free memory perantara\ndel raw, y_raw, X\ngc.collect()\n\n# ==========================================\n# 3. PREPROCESSING (NORMALISASI)\n# ==========================================\nx_train = data_preprocess_train(X_train)\n\n# ==========================================\n# 4. VISUALISASI DISTRIBUSI EVENT (BAR PLOT SEABORN)\n# ==========================================\nye = pd.DataFrame(y_train)\nye.columns = [\"Handstart\", \"Grasping\", \"Lift\", \"Hold\", \"Replace\", \"Release\"]\ncategories = list(ye.columns.values)\ncounts = ye.iloc[:, 0:].sum().values\n\nsns.set_theme(style=\"whitegrid\")\nplt.figure(figsize=(12, 6))\n\nax = sns.barplot(x=categories, y=counts, palette=\"deep\")\nplt.title(\n    f\"Number of samples labeled as active (1) out of {ye.shape[0]} length data\",\n    fontsize=16,\n)\nplt.ylabel(\"Number of events\", fontsize=14)\nplt.xlabel(\"Event Type\", fontsize=12)\n\nrects = ax.patches\nfor rect, label in zip(rects, counts):\n    height = rect.get_height()\n    ax.text(\n        rect.get_x() + rect.get_width() / 2,\n        height + 500,\n        f\"{label:.1f}\",\n        ha=\"center\",\n        va=\"bottom\",\n        fontsize=10,\n    )\n\nplt.tight_layout()\nplt.show()\n\ndel ye\ngc.collect()\n\n# ==========================================\n# 5. VISUALISASI SINYAL ASLI VS NORMALISASI\n# ==========================================\nsample_range = slice(0, 1000)  # Menampilkan 1000 sampel pertama\n\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 7), sharex=True)\n\n# Plot Sinyal Mentah (Asli)\nax1.plot(\n    X_train[sample_range, 0],\n    color=\"tab:blue\",\n    alpha=0.85,\n    label=\"Sinyal Asli / Mentah (Raw)\",\n)\nax1.set_title(\n    \"1. Bentuk Sinyal EEG Asli (Channel 1) - Skala Microvolt\", fontsize=13\n)\nax1.set_ylabel(\"Amplitudo Asli\")\nax1.grid(True, alpha=0.3)\nax1.legend(loc=\"upper right\")\n\n# Plot Sinyal Ter-normalisasi\nax2.plot(\n    x_train[sample_range, 0],\n    color=\"tab:green\",\n    alpha=0.85,\n    label=\"Sinyal Ter-normalisasi (StandardScaler / Z-Score)\",\n)\nax2.set_title(\n    \"2. Bentuk Sinyal EEG Ter-normalisasi (Channel 1) - Mean=0, Std=1\",\n    fontsize=13,\n)\nax2.set_xlabel(\"Sample Index\")\nax2.set_ylabel(\"Skala Z-Score\")\nax2.axhline(0, color=\"black\", linestyle=\"--\", alpha=0.6)\nax2.grid(True, alpha=0.3)\nax2.legend(loc=\"upper right\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T13:07:52.882076Z","iopub.execute_input":"2026-09-02T13:07:52.882410Z","iopub.status.idle":"2026-09-02T13:08:21.693309Z","shell.execute_reply.started":"2026-09-02T13:07:52.882331Z","shell.execute_reply":"2026-09-02T13:08:21.692673Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Eksplorasi sinyal","metadata":{}},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\nfrom sklearn.preprocessing import StandardScaler\n\n# ==========================================\n# 1. READ DATA\n# ==========================================\ndef prepare_data_train(fname):\n    data = pd.read_csv(fname)\n    events_fname = fname.replace(\"_data\", \"_events\")\n    labels = pd.read_csv(events_fname)\n    clean = data.drop([\"id\"], axis=1)\n    labels = labels.drop([\"id\"], axis=1)\n    return clean, labels\n\n\n# Load 1 subject & 1 series dulu untuk eksplorasi cepat\nfname = sorted(glob(\"train/subj1_series1_data.csv\"))[0]\nX_raw, y_raw = prepare_data_train(fname)\n\nX_data = np.asarray(X_raw.astype(np.float32))\ny_data = np.asarray(y_raw.astype(np.float32))\n\n# ==========================================\n# 2. FUNGSIONALITAS BANDPASS FILTER (BUTTERWORTH)\n# ==========================================\n# Frekuensi sampling EEG Kaggle Grasp-and-Lift adalah 500 Hz\nFS = 500.0\n\n\ndef butter_bandpass_filter(data, lowcut, highcut, fs=FS, order=4):\n    nyq = 0.5 * fs  # Frekuensi Nyquist\n    if lowcut is None:  # Lowpass (misal Delta < 4 Hz)\n        high = highcut / nyq\n        b, a = butter(order, high, btype=\"low\")\n    elif highcut is None:  # Highpass (misal Gamma > 30 Hz)\n        low = lowcut / nyq\n        b, a = butter(order, low, btype=\"high\")\n    else:  # Bandpass (Alpha/Mu, Beta, Theta)\n        low = lowcut / nyq\n        high = highcut / nyq\n        b, a = butter(order, [low, high], btype=\"band\")\n\n    # Zero-phase filtering menggunakan filtfilt agar tidak ada pergeseran fase (phase shift)\n    filtered_data = filtfilt(b, a, data, axis=0)\n    return filtered_data\n\n\n# Dictionary Sub-band Frekuensi\nSUB_BANDS = {\n    \"Delta (<4 Hz)\": (None, 4),\n    \"Theta (4-7 Hz)\": (4, 7),\n    \"Mu/Alpha (8-12 Hz)\": (8, 12),\n    \"Beta (13-30 Hz)\": (13, 30),\n    \"Gamma (>30 Hz)\": (30, None),\n}\n\n# Eksplorasi: Ekstrak semua sub-band untuk Channel 1\nfiltered_bands = {}\nfor band_name, (low, high) in SUB_BANDS.items():\n    filtered_bands[band_name] = butter_bandpass_filter(\n        X_data, lowcut=low, highcut=high, fs=FS\n    )\n\n# ==========================================\n# 3. VISUALISASI EKSPLORASI GELOMBANG SUB-BAND\n# ==========================================\nsample_range = slice(1000, 2500)  # Menampilkan 1500 sampel (3 detik)\nch_idx = 0  # Channel 1\n\nplt.figure(figsize=(16, 10))\n\n# Plot Sinyal Mentah\nplt.subplot(len(SUB_BANDS) + 1, 1, 1)\nplt.plot(X_data[sample_range, ch_idx], color=\"black\", label=\"Raw EEG\")\nplt.title(\"Perbandingan Sinyal Mentah vs Sub-Band Frekuensi (Channel 1)\", fontsize=14)\nplt.ylabel(\"Raw\")\nplt.legend(loc=\"upper right\")\nplt.grid(True, alpha=0.3)\n\n# Plot Setiap Sub-Band\ncolors = [\"purple\", \"blue\", \"green\", \"orange\", \"red\"]\nfor idx, (band_name, band_data) in enumerate(filtered_bands.items()):\n    plt.subplot(len(SUB_BANDS) + 1, 1, idx + 2)\n    plt.plot(\n        band_data[sample_range, ch_idx],\n        color=colors[idx],\n        label=band_name,\n    )\n    plt.ylabel(\"Amp\")\n    plt.legend(loc=\"upper right\")\n    plt.grid(True, alpha=0.3)\n\nplt.xlabel(\"Sample Index (Sampling Rate = 500 Hz)\")\nplt.tight_layout()\nplt.show()\n\n# ==========================================\n# 4. SKRIP PEMBUKTIAN AKURASI TIAP SUB-BAND\n# ==========================================\nscaler = StandardScaler()\n\nprint(\"\\n=== HASIL EKSTRAKSI SUB-BAND UNTUK TRAINING ===\")\nfor band_name, band_data in filtered_bands.items():\n    # Normalisasi sinyal sub-band\n    norm_band_data = scaler.fit_transform(band_data)\n\n    print(\n        f\"Sub-band '{band_name}' siap! Shape: {norm_band_data.shape} | Mean: {norm_band_data.mean():.4f} | Std: {norm_band_data.std():.4f}\"\n    )\n\n# Clean Up\ndel filtered_bands\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T13:08:21.694273Z","iopub.execute_input":"2026-09-02T13:08:21.694613Z","iopub.status.idle":"2026-09-02T13:08:24.163982Z","shell.execute_reply.started":"2026-09-02T13:08:21.694585Z","shell.execute_reply":"2026-09-02T13:08:24.163430Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model cnn \"Referensi: Schirrmeister, R. T., et al. (2017). 'Deep learning with convolutional neural networks for EEG decoding and visualization.\"","metadata":{}},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\nfrom sklearn.preprocessing import StandardScaler\nimport tensorflow as tf\nfrom tensorflow.keras.layers import (\n    BatchNormalization,\n    Conv2D,\n    Dense,\n    Flatten,\n    Input,\n)\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.optimizers import Adam\n\n# ==========================================\n# 0. SET SEED & KONFIGURASI\n# ==========================================\nSEED = 42\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nFS = 500.0  # Frekuensi sampling EEG (500 Hz)\ntime_steps = 1000\nsubsample = 50\n\n# Sub-band yang akan dievaluasi (+ Raw Signal)\nSUB_BANDS = {\n    \"Raw EEG (Tanpa Filter)\": \"raw\",\n    \"Delta (<4 Hz)\": (None, 4),\n    \"Theta (4-7 Hz)\": (4, 7),\n    \"Mu/Alpha (8-12 Hz)\": (8, 12),\n    \"Beta (13-30 Hz)\": (13, 30),\n    \"Gamma (>30 Hz)\": (30, None),\n}\n\n\n# ==========================================\n# 1. FUNGSIONALITAS FILTER & PREPROCESSING\n# ==========================================\ndef prepare_data_train(fname):\n  data = pd.read_csv(fname)\n  events_fname = fname.replace(\"_data\", \"_events\")\n  labels = pd.read_csv(events_fname)\n  return data.drop([\"id\"], axis=1), labels.drop([\"id\"], axis=1)\n\n\ndef butter_bandpass_filter(data, lowcut, highcut, fs=FS, order=4):\n  nyq = 0.5 * fs\n  if lowcut is None:\n    b, a = butter(order, highcut / nyq, btype=\"low\")\n  elif highcut is None:\n    b, a = butter(order, lowcut / nyq, btype=\"high\")\n  else:\n    b, a = butter(\n        order, [lowcut / nyq, highcut / nyq], btype=\"band\"\n    )\n  return filtfilt(b, a, data, axis=0)\n\n\n# ==========================================\n# 2. LOAD DATA SEMENTARA (SUBJECT 1-5)\n# ==========================================\nprint(\"[INFO] Loading dataset EEG...\")\nsubjects = range(1, 6)\nraw, y_raw = [], []\n\nfor subject in subjects:\n  fnames = sorted(glob(\"train/subj%d_series*_data.csv\" % (subject)))\n  for fname in fnames:\n    data, labels = prepare_data_train(fname)\n    raw.append(data)\n    y_raw.append(labels)\n\nX_all = np.asarray(pd.concat(raw).astype(np.float32))\ny_all = np.asarray(pd.concat(y_raw).astype(np.float32))\n\ndel raw, y_raw\ngc.collect()\n\n\n# ==========================================\n# 3. BUILD MODEL ARCHITECTURE FUNCTION\n# ==========================================\ndef build_cnn_model():\n  model = Sequential([\n      Input(shape=(time_steps // subsample, 32, 1)),\n      Conv2D(\n          filters=64, kernel_size=(7, 7), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Conv2D(\n          filters=64, kernel_size=(5, 5), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Conv2D(\n          filters=64, kernel_size=(3, 3), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Flatten(),\n      Dense(32, activation=\"relu\"),\n      BatchNormalization(),\n      Dense(6, activation=\"sigmoid\"),\n  ])\n  model.compile(\n      optimizer=Adam(learning_rate=0.0001),\n      loss=\"binary_crossentropy\",\n      metrics=[tf.keras.metrics.BinaryAccuracy(name=\"acc\"), \"mse\"],\n  )\n  return model\n\n\n# Generator Function\ndef create_generators(xtrain, ytrain, xval, yval, batch_size=32):\n  def generator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xtrain) - time_steps - 1)\n        x_time_data[i] = xtrain[idx : idx + time_steps : subsample]\n        yy.append(ytrain[idx + time_steps])\n      yield x_time_data.reshape(\n          (batch_size, time_steps // subsample, 32, 1)\n      ), np.asarray(yy, dtype=np.float32)\n\n  def valgenerator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xval) - time_steps - 1)\n        x_time_data[i] = xval[idx : idx + time_steps : subsample]\n        yy.append(yval[idx + time_steps])\n      yield x_time_data.reshape(\n          (batch_size, time_steps // subsample, 32, 1)\n      ), np.asarray(yy, dtype=np.float32)\n\n  return generator(), valgenerator()\n\n\n# ==========================================\n# 4. TRAINING CNN PADA TIAP BAND & RAW\n# ==========================================\nhistory_results = {}\n\nfor band_name, config in SUB_BANDS.items():\n  print(f\"\\n==========================================\")\n  print(f\"   TRAINING MODEL UNTUK: {band_name}\")\n  print(f\"==========================================\")\n\n  # 1. Bandpass Filtering (jika bukan sinyal mentah) & Normalization\n  if config == \"raw\":\n    processed_X = X_all.copy()\n  else:\n    low, high = config\n    processed_X = butter_bandpass_filter(X_all, low, high, fs=FS)\n\n  scaler = StandardScaler()\n  x_norm = scaler.fit_transform(processed_X)\n\n  # 2. Data Splitting\n  splitrate = -x_norm.shape[0] // 5 * 2\n  xval = x_norm[splitrate : splitrate // 2]\n  yval = y_all[splitrate : splitrate // 2]\n  xtrain = x_norm[:splitrate]\n  ytrain = y_all[:splitrate]\n\n  # 3. Training CNN\n  train_gen, val_gen = create_generators(xtrain, ytrain, xval, yval)\n  model = build_cnn_model()\n\n  start_time = time.time()\n  hist = model.fit(\n      train_gen,\n      steps_per_epoch=400,\n      epochs=20,\n      validation_data=val_gen,\n      validation_steps=150,\n      verbose=1,\n  )\n  print(\n      f\"Waktu training {band_name}: {round(time.time() - start_time, 0)} detik\"\n  )\n\n  # Simpan hasil history\n  history_results[band_name] = {\n      \"acc\": hist.history[\"acc\"],\n      \"val_acc\": hist.history[\"val_acc\"],\n      \"loss\": hist.history[\"loss\"],\n      \"val_loss\": hist.history[\"val_loss\"],\n  }\n\n  # Pembersihan RAM & GPU per-band\n  del processed_X, x_norm, xtrain, ytrain, xval, yval, model, train_gen, val_gen\n  tf.keras.backend.clear_session()\n  gc.collect()\n\n# Free data mentah utama\ndel X_all, y_all\ngc.collect()\n\n# ==========================================\n# 5. VISUALISASI KOMPARASI HASIL TIAP BAND & RAW\n# ==========================================\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\n\n# Warna khusus: Hitam untuk Raw EEG, sisanya warna-warni\ncolors = [\"black\", \"purple\", \"blue\", \"green\", \"orange\", \"red\"]\n\n# 1. Plot Validation Accuracy per Band\nfor idx, (band_name, metrics) in enumerate(history_results.items()):\n  axes[0].plot(\n      metrics[\"val_acc\"],\n      label=f\"{band_name}\",\n      color=colors[idx],\n      linewidth=2.5 if band_name.startswith(\"Raw\") else 1.8,\n      linestyle=\"--\" if band_name.startswith(\"Raw\") else \"-\",\n  )\n\naxes[0].set_title(\n    \"Perbandingan Validation Accuracy (Raw vs Sub-bands)\", fontsize=14\n)\naxes[0].set_xlabel(\"Epoch\")\naxes[0].set_ylabel(\"Validation Accuracy\")\naxes[0].legend(loc=\"lower right\")\naxes[0].grid(True, alpha=0.3)\n\n# 2. Plot Validation Loss per Band\nfor idx, (band_name, metrics) in enumerate(history_results.items()):\n  axes[1].plot(\n      metrics[\"val_loss\"],\n      label=f\"{band_name}\",\n      color=colors[idx],\n      linewidth=2.5 if band_name.startswith(\"Raw\") else 1.8,\n      linestyle=\"--\" if band_name.startswith(\"Raw\") else \"-\",\n  )\n\naxes[1].set_title(\n    \"Perbandingan Validation Loss (Raw vs Sub-bands)\", fontsize=14\n)\naxes[1].set_xlabel(\"Epoch\")\naxes[1].set_ylabel(\"Validation Loss\")\naxes[1].legend(loc=\"upper right\")\naxes[1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n\n# ==========================================\n# 6. SUMMARY AKURASI TERBAIK\n# ==========================================\nprint(\"\\n=== RINGKASAN AKURASI AKHIR (RAW VS SUB-BANDS) ===\")\nsummary_df = pd.DataFrame({\n    \"Sub-band / Input\": list(history_results.keys()),\n    \"Max Val Acc\": [\n        max(m[\"val_acc\"]) for m in history_results.values()\n    ],\n    \"Min Val Loss\": [\n        min(m[\"val_loss\"]) for m in history_results.values()\n    ],\n}).sort_values(by=\"Max Val Acc\", ascending=False)\n\nprint(summary_df.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T13:08:24.165774Z","iopub.execute_input":"2026-09-02T13:08:24.166078Z","iopub.status.idle":"2026-09-02T13:18:42.870363Z","shell.execute_reply.started":"2026-09-02T13:08:24.166051Z","shell.execute_reply":"2026-09-02T13:18:42.869285Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## resume","metadata":{}},{"cell_type":"code","source":"'''\n\nPilihan Terbaik Secara Keseluruhan: Gamma (>30 Hz)\nAkurasi Tertinggi: Memiliki Max Val Acc paling tinggi (97,50%) di antara semua variasi.\nFungsi Biologis: Pita Gamma merefleksikan integrasi sensorimotor yang sangat cepat dan pemrosesan informasi lokal yang tinggi saat otot tangan mulai berkontraksi/menggenggam.\n\n\nPilihan Alternatif Paling Stabil: Delta (<4 Hz)\nLoss Terendah: Memiliki Min Val Loss paling rendah (0.1063) dibanding variasi lainnya, termasuk Gamma.\nFungsi Biologis: Pita Delta mencakup komponen Movement-Related Cortical Potentials (MRCP), yaitu sinyal pergeseran frekuensi rendah yang muncul secara konsisten persis sebelum dan selama gerakan fisik terjadi.\n\n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## UJI LOSO CNN","metadata":{}},{"cell_type":"code","source":"'''\nData Sitasi: Multi-channel EEG recordings during 3,936 grasp and lift trials with varying weight and friction\nMetode Pengujian Leave-One-Subject-Out (LOSO) Sitasi: Lotte, F., et al. (2018). A review of classification algorithms for EEG-based brain-computer interfaces (2007–2017). Journal of Neural Engineering, 15(3), 031005.\nEkstraksi Sub-band Frekuensi (Gamma & Filtering) :Sitasi: Pfurtscheller, G., & Lopes da Silva, F. H. (1999). Event-related EEG/MEG synchronization and desynchronization: basic principles. Clinical Neurophysiology, 110(11), 1842-1857.\nArsitektur CNN untuk Dekode EEG (Temporal & Spatial) Sitasi: Schirrmeister, R. T., et al. (2017). Deep learning with convolutional neural networks for EEG decoding and visualization. Human Brain Mapping, 38(11), 5391-5420.\nInput Representation & Compact BCI Architecture Sitasi: Lawhern, V. J., et al. (2018). EEGNet: a compact convolutional neural network for EEG-based brain-computer interfaces. Journal of Neural Engineering, 15(5), 056013.\n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\nfrom sklearn.preprocessing import StandardScaler\nimport tensorflow as tf\nfrom tensorflow.keras.layers import (\n    BatchNormalization,\n    Conv2D,\n    Dense,\n    Flatten,\n    Input,\n)\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.optimizers import Adam\n\n# ==========================================\n# 0. SET SEED & KONFIGURASI\n# ==========================================\nSEED = 42\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nFS = 500.0\ntime_steps = 1000\nsubsample = 50\nTOTAL_SUBJECTS = 12\n\nBEST_BAND_NAME = \"Gamma (>30 Hz)\"\nLOWCUT, HIGHCUT = 30, None\n\n\n# ==========================================\n# 1. PREPROCESSING & FILTER\n# ==========================================\ndef butter_bandpass_filter(data, lowcut, highcut, fs=FS, order=4):\n  nyq = 0.5 * fs\n  if lowcut is None:\n    b, a = butter(order, highcut / nyq, btype=\"low\")\n  elif highcut is None:\n    b, a = butter(order, lowcut / nyq, btype=\"high\")\n  else:\n    b, a = butter(\n        order, [lowcut / nyq, highcut / nyq], btype=\"band\"\n    )\n  return filtfilt(b, a, data, axis=0)\n\n\ndef load_single_subject(sub_id):\n  \"\"\"Memuat 1 subjek dan mengonversi ke float16 (Menghemat 50% RAM)\"\"\"\n  fnames = sorted(glob(f\"train/subj{sub_id}_series*_data.csv\"))\n  if not fnames:\n    return None, None\n\n  raw_sub, y_list = [], []\n  for fname in fnames:\n    data = pd.read_csv(fname)\n    events_fname = fname.replace(\"_data\", \"_events\")\n    labels = pd.read_csv(events_fname)\n\n    raw_sub.append(data.drop([\"id\"], axis=1))\n    y_list.append(labels.drop([\"id\"], axis=1))\n\n    del data, labels\n\n  X_sub = np.asarray(pd.concat(raw_sub).astype(np.float32))\n  y_sub = np.asarray(pd.concat(y_list).astype(np.float16))  # Tipe hemat float16\n\n  X_filtered = butter_bandpass_filter(\n      X_sub, lowcut=LOWCUT, highcut=HIGHCUT, fs=FS\n  ).astype(\n      np.float16\n  )  # Tipe hemat float16\n\n  del raw_sub, y_list, X_sub\n  gc.collect()\n\n  return X_filtered, y_sub\n\n\n# ==========================================\n# 2. MODEL ARCHITECTURE\n# ==========================================\ndef build_cnn_model():\n  model = Sequential([\n      Input(shape=(time_steps // subsample, 32, 1)),\n      Conv2D(\n          filters=64, kernel_size=(7, 7), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Conv2D(\n          filters=64, kernel_size=(5, 5), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Conv2D(\n          filters=64, kernel_size=(3, 3), padding=\"same\", activation=\"relu\"\n      ),\n      BatchNormalization(),\n      Flatten(),\n      Dense(32, activation=\"relu\"),\n      BatchNormalization(),\n      Dense(6, activation=\"sigmoid\"),\n  ])\n  model.compile(\n      optimizer=Adam(learning_rate=0.0001),\n      loss=\"binary_crossentropy\",\n      metrics=[tf.keras.metrics.BinaryAccuracy(name=\"acc\"), \"mse\"],\n  )\n  return model\n\n\n# Generator yang bekerja langsung pada subjek terpisah (Tanpa Gabung Array Raksasa)\ndef create_loso_generators(train_subjects_dict, test_sub_data, batch_size=32):\n  sub_keys = list(train_subjects_dict.keys())\n\n  def generator():\n    while True:\n      x_batch = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      y_batch = []\n\n      for i in range(batch_size):\n        # Pilih subjek acak dari daftar training\n        random_sub = np.random.choice(sub_keys)\n        X_data = train_subjects_dict[random_sub][\"X\"]\n        y_data = train_subjects_dict[random_sub][\"y\"]\n\n        idx = np.random.randint(0, len(X_data) - time_steps - 1)\n        x_batch[i] = X_data[idx : idx + time_steps : subsample]\n        y_batch.append(y_data[idx + time_steps])\n\n      yield x_batch.reshape(\n          (batch_size, time_steps // subsample, 32, 1)\n      ), np.asarray(y_batch, dtype=np.float32)\n\n  def valgenerator():\n    X_val, y_val = test_sub_data[\"X\"], test_sub_data[\"y\"]\n    len_val = len(X_val)\n    while True:\n      x_batch = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      y_batch = []\n\n      for i in range(batch_size):\n        idx = np.random.randint(0, len_val - time_steps - 1)\n        x_batch[i] = X_val[idx : idx + time_steps : subsample]\n        y_batch.append(y_val[idx + time_steps])\n\n      yield x_batch.reshape(\n          (batch_size, time_steps // subsample, 32, 1)\n      ), np.asarray(y_batch, dtype=np.float32)\n\n  return generator(), valgenerator()\n\n\n# ==========================================\n# 3. LEAVE-ONE-SUBJECT-OUT (LOSO) ULTRA-LIGHT\n# ==========================================\nloso_results = []\navailable_subjects = list(range(1, TOTAL_SUBJECTS + 1))\n\nprint(\"==========================================\")\nprint(\"   MEMULAI LOSO (ULTRA LOW-MEMORY MODE)\")\nprint(\"==========================================\")\n\nfor test_sub in available_subjects:\n  train_subs = [s for s in available_subjects if s != test_sub]\n\n  print(\n      f\"\\n[LOSO FOLD] Test Subject: {test_sub} | Train Subjects: {train_subs}\"\n  )\n\n  # 1. Hitung Scaler Global bertahap\n  scaler = StandardScaler()\n  for s in train_subs:\n    x_s, _ = load_single_subject(s)\n    if x_s is not None:\n      scaler.partial_fit(x_s.astype(np.float32))\n      del x_s\n      gc.collect()\n\n  # 2. Simpan Data per-Subjek terpisah dengan float16 (Tanpa Concatenate)\n  train_dict = {}\n  for s in train_subs:\n    x_s, y_s = load_single_subject(s)\n    if x_s is not None:\n      x_norm = scaler.transform(x_s.astype(np.float32)).astype(np.float16)\n      train_dict[s] = {\"X\": x_norm, \"y\": y_s}\n      del x_s, y_s, x_norm\n      gc.collect()\n\n  # Load data testing\n  x_test, y_test = load_single_subject(test_sub)\n  x_test_norm = scaler.transform(x_test.astype(np.float32)).astype(np.float16)\n  test_dict = {\"X\": x_test_norm, \"y\": y_test}\n  del x_test, y_test\n  gc.collect()\n\n  # 3. Training dengan Generator\n  train_gen, val_gen = create_loso_generators(train_dict, test_dict)\n  model = build_cnn_model()\n\n  hist = model.fit(\n      train_gen,\n      steps_per_epoch=300,\n      epochs=10,\n      validation_data=val_gen,\n      validation_steps=100,\n      verbose=0,\n  )\n\n  max_val_acc = max(hist.history[\"val_acc\"])\n  min_val_loss = min(hist.history[\"val_loss\"])\n\n  loso_results.append({\n      \"Test Subject\": f\"Subjek {test_sub}\",\n      \"Val Accuracy\": max_val_acc,\n      \"Val Loss\": min_val_loss,\n  })\n\n  print(\n      f\"   -> Hasil Subjek {test_sub} (Unseen): Val Acc = {max_val_acc:.4f} |\"\n      f\" Val Loss = {min_val_loss:.4f}\"\n  )\n\n  # 4. Total De-allocation per-fold\n  del train_dict, test_dict, scaler, model, hist, train_gen, val_gen\n  tf.keras.backend.clear_session()\n  gc.collect()\n\n# ==========================================\n# 4. RINGKASAN RINGKAS\n# ==========================================\ndf_loso = pd.DataFrame(loso_results)\nprint(\"\\n=== RINGKASAN EVALUASI LOSO ===\")\nprint(df_loso.to_string(index=False))\n\nmean_acc = df_loso[\"Val Accuracy\"].mean()\nstd_acc = df_loso[\"Val Accuracy\"].std()\nprint(f\"\\n[SUMMARY] Rata-rata Akurasi LOSO: {mean_acc:.4f} ± {std_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T14:41:53.423569Z","iopub.execute_input":"2026-09-02T14:41:53.424210Z","iopub.status.idle":"2026-09-02T15:34:02.561967Z","shell.execute_reply.started":"2026-09-02T14:41:53.424180Z","shell.execute_reply":"2026-09-02T15:34:02.561272Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import math\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\n\n\nclass ADLNode(nn.Module):\n    \"\"\"\n    Representasi satu node/neuron pada hidden layer ADL.\n    Memiliki parameter drift (mean & variance) untuk deteksi drift/covariate shift.\n    \"\"\"\n\n    def __init__(self, in_features):\n        super(ADLNode, self).__init__()\n        self.weight = nn.Parameter(torch.randn(in_features) * 0.01)\n        self.bias = nn.Parameter(torch.zeros(1))\n\n        # Statistik statistik internal untuk drift detection (mean & variance)\n        self.mean = 0.0\n        self.var = 1.0\n        self.count = 0\n\n    def update_stats(self, x):\n        \"\"\"Memperbarui statistik mean dan varians secara online (Welford Algorithm).\"\"\"\n        batch_mean = x.mean().item()\n        batch_var = x.var().item() if x.numel() > 1 else 0.0\n        batch_count = x.numel()\n\n        if self.count == 0:\n            self.mean = batch_mean\n            self.var = batch_var\n            self.count = batch_count\n        else:\n            delta = batch_mean - self.mean\n            tot_count = self.count + batch_count\n            self.mean += delta * batch_count / tot_count\n            self.var = (\n                (self.var * self.count)\n                + (batch_var * batch_count)\n                + (delta**2 * self.count * batch_count / tot_count)\n            ) / tot_count\n            self.count = tot_count\n\n\nclass ADLLayer(nn.Module):\n    \"\"\"\n    Hidden layer dinamis untuk ADL.\n    Mendukung penambahan node (node growing) dan pemangkasan node (node pruning).\n    \"\"\"\n\n    def __init__(self, in_features, initial_nodes=2):\n        super(ADLLayer, self).__init__()\n        self.in_features = in_features\n        self.linear = nn.Linear(in_features, initial_nodes)\n\n    def forward(self, x):\n        return torch.relu(self.linear(x))\n\n    def add_node(self):\n        \"\"\"Menambah 1 neuron/node baru ke layer ini.\"\"\"\n        old_out, old_in = self.linear.weight.shape\n        new_weight = torch.randn(old_out + 1, old_in) * 0.01\n        new_bias = torch.zeros(old_out + 1)\n\n        # Copy bobot lama\n        new_weight[:old_out, :] = self.linear.weight.data\n        new_bias[:old_out] = self.linear.bias.data\n\n        self.linear = nn.Linear(old_in, old_out + 1)\n        self.linear.weight = nn.Parameter(new_weight)\n        self.linear.bias = nn.Parameter(new_bias)\n\n    def prune_node(self, idx):\n        \"\"\"Menghapus neuron/node pada indeks tertentu.\"\"\"\n        old_out, old_in = self.linear.weight.shape\n        if old_out <= 1:\n            return  # Sisakan minimal 1 node\n\n        keep_indices = [i for i in range(old_out) if i != idx]\n        new_weight = self.linear.weight.data[keep_indices, :]\n        new_bias = self.linear.bias.data[keep_indices]\n\n        self.linear = nn.Linear(old_in, old_out - 1)\n        self.linear.weight = nn.Parameter(new_weight)\n        self.linear.bias = nn.Parameter(new_bias)\n\n\nclass AutonomousDeepLearning(nn.Module):\n    \"\"\"\n    Arsitektur Utama ADL (Autonomous Deep Learning).\n    Mendukung Layer Growing, Node Growing, & Pruning secara otomatis.\n    \"\"\"\n\n    def __init__(self, input_dim, output_dim, drift_threshold=2.0):\n        super(AutonomousDeepLearning, self).__init__()\n        self.input_dim = input_dim\n        self.output_dim = output_dim\n        self.drift_threshold = drift_threshold\n\n        # Memulai dengan 1 hidden layer dinamis\n        self.layers = nn.ModuleList([ADLLayer(input_dim, initial_nodes=2)])\n        self.classifier = nn.Linear(2, output_dim)\n\n    def forward(self, x):\n        out = x\n        for layer in self.layers:\n            out = layer(out)\n        out = self.classifier(out)\n        return out\n\n    def add_layer(self):\n        \"\"\"Menambah layer baru ketika kinerja jaringan stagnan/mengalami drift berat.\"\"\"\n        last_hidden_dim = self.layers[-1].linear.out_features\n        new_layer = ADLLayer(last_hidden_dim, initial_nodes=last_hidden_dim)\n\n        self.layers.append(new_layer)\n        # Update layer output/klasifikasi\n        self.classifier = nn.Linear(last_hidden_dim, self.output_dim)\n        print(f\"[ADL Structural Evolution] Layer baru ditambahkan! Total Layer: {len(self.layers)}\")\n\n    def adapt_structure(self, x, current_loss, prev_loss):\n        \"\"\"\n        Mekanisme adaptasi struktur dinamis berdasarkan Drift Detection & Statistical Significance:\n        1. Node Growing\n        2. Layer Growing\n        3. Node Pruning\n        \"\"\"\n        # 1. Cek kebutuhan Node Growing / Layer Growing\n        if prev_loss is not None and (current_loss > prev_loss * 1.05):\n            # Jika loss meningkat, tambahkan node pada layer terakhir\n            self.layers[-1].add_node()\n            new_out_dim = self.layers[-1].linear.out_features\n\n            # Inisialisasi ulang classifier sesuai dimensi baru\n            old_classifier = self.classifier\n            self.classifier = nn.Linear(new_out_dim, self.output_dim)\n            with torch.no_grad():\n                min_dim = min(old_classifier.weight.shape[1], new_out_dim)\n                self.classifier.weight[:, :min_dim] = old_classifier.weight[:, :min_dim]\n\n            print(f\"[ADL Structural Evolution] Node ditambahkan ke Layer {len(self.layers)}. Node saat ini: {new_out_dim}\")\n\n            # Jika jumlah node pada layer terakhir melebihi batas (misal 32 node), tambah Layer baru\n            if new_out_dim > 32:\n                self.add_layer()\n\n    def train_stream(self, data_loader, epochs_per_batch=1, lr=0.01):\n        \"\"\"Metode pelatihan continual learning berbasis data stream.\"\"\"\n        optimizer = optim.Adam(self.parameters(), lr=lr)\n        criterion = nn.CrossEntropyLoss()\n\n        prev_loss = None\n\n        for batch_idx, (inputs, targets) in enumerate(data_loader):\n            self.train()\n\n            for _ in range(epochs_per_batch):\n                optimizer.zero_grad()\n                outputs = self.forward(inputs)\n                loss = criterion(outputs, targets)\n                loss.backward()\n                optimizer.step()\n\n            current_loss = loss.item()\n            print(f\"Batch {batch_idx + 1} | Loss: {current_loss:.4f} | Structure: {[l.linear.out_features for l in self.layers]}\")\n\n            # Adaptasi struktur ADL\n            self.adapt_structure(inputs, current_loss, prev_loss)\n            prev_loss = current_loss\n\n\n# ==========================================\n# SIMULASI PENGGUNAAN ADL\n# ==========================================\nif __name__ == \"__main__\":\n    from torch.utils.data import DataLoader, TensorDataset\n\n    # Generate Dummy Data Stream (100 sampel, 10 fitur, 3 kelas)\n    torch.manual_seed(42)\n    X_dummy = torch.randn(200, 10)\n    y_dummy = torch.randint(0, 3, (200,))\n\n    dataset = TensorDataset(X_dummy, y_dummy)\n    stream_loader = DataLoader(dataset, batch_size=20, shuffle=False)\n\n    # Inisialisasi Model ADL\n    adl_model = AutonomousDeepLearning(input_dim=10, output_dim=3)\n\n    print(\"--- Memulai Pelatihan Continual Learning ADL ---\")\n    adl_model.train_stream(stream_loader, lr=0.01)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## MODEL ADL","metadata":{}},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport math\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\nfrom sklearn.preprocessing import StandardScaler\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\n\n# ==========================================\n# 0. KONFIGURASI & SEED\n# ==========================================\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nFS = 500.0\ntime_steps = 1000\nsubsample = 50\n\nSUB_BANDS = {\n    \"Raw EEG (Tanpa Filter)\": \"raw\",\n    \"Delta (<4 Hz)\": (None, 4),\n    \"Theta (4-7 Hz)\": (4, 7),\n    \"Mu/Alpha (8-12 Hz)\": (8, 12),\n    \"Beta (13-30 Hz)\": (13, 30),\n    \"Gamma (>30 Hz)\": (30, None),\n}\n\n\n# ==========================================\n# 1. HELPER & PREPROCESSING\n# ==========================================\ndef prepare_data_train(fname):\n  data = pd.read_csv(fname)\n  events_fname = fname.replace(\"_data\", \"_events\")\n  labels = pd.read_csv(events_fname)\n  return data.drop([\"id\"], axis=1), labels.drop([\"id\"], axis=1)\n\n\ndef butter_bandpass_filter(data, lowcut, highcut, fs=FS, order=4):\n  nyq = 0.5 * fs\n  if lowcut is None:\n    b, a = butter(order, highcut / nyq, btype=\"low\")\n  elif highcut is None:\n    b, a = butter(order, lowcut / nyq, btype=\"high\")\n  else:\n    b, a = butter(order, [lowcut / nyq, highcut / nyq], btype=\"band\")\n  return filtfilt(b, a, data, axis=0)\n\n\ndef create_generators(xtrain, ytrain, xval, yval, batch_size=32):\n  def generator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xtrain) - time_steps - 1)\n        x_time_data[i] = xtrain[idx : idx + time_steps : subsample]\n        yy.append(ytrain[idx + time_steps])\n      yield x_time_data, np.asarray(yy, dtype=np.float32)\n\n  def valgenerator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xval) - time_steps - 1)\n        x_time_data[i] = xval[idx : idx + time_steps : subsample]\n        yy.append(yval[idx + time_steps])\n      yield x_time_data, np.asarray(yy, dtype=np.float32)\n\n  return generator(), valgenerator()\n\n\n# ==========================================\n# 2. ADL LAYER & MODEL (GROW & CUT)\n# ==========================================\nclass ADLLayer(nn.Module):\n\n  def __init__(self, in_features, initial_nodes=16, dropout_rate=0.2):\n    super(ADLLayer, self).__init__()\n    self.linear = nn.Linear(in_features, initial_nodes)\n    self.dropout = nn.Dropout(dropout_rate)\n\n  def forward(self, x):\n    out = torch.relu(self.linear(x))\n    return self.dropout(out)\n\n  def add_node(self):\n    old_out, old_in = self.linear.weight.shape\n    new_weight = torch.randn(old_out + 1, old_in) * 0.01\n    new_bias = torch.zeros(old_out + 1)\n    new_weight[:old_out, :] = self.linear.weight.data\n    new_bias[:old_out] = self.linear.bias.data\n\n    self.linear = nn.Linear(old_in, old_out + 1)\n    self.linear.weight = nn.Parameter(new_weight)\n    self.linear.bias = nn.Parameter(new_bias)\n\n  def cut_node(self):\n    old_out, old_in = self.linear.weight.shape\n    if old_out <= 4:\n      return False\n\n    weights_sum = torch.sum(torch.abs(self.linear.weight.data), dim=1)\n    min_node_idx = torch.argmin(weights_sum).item()\n\n    keep_indices = [i for i in range(old_out) if i != min_node_idx]\n\n    new_weight = self.linear.weight.data[keep_indices, :]\n    new_bias = self.linear.bias.data[keep_indices]\n\n    self.linear = nn.Linear(old_in, old_out - 1)\n    self.linear.weight = nn.Parameter(new_weight)\n    self.linear.bias = nn.Parameter(new_bias)\n    return True\n\n\nclass AutonomousDeepLearning(nn.Module):\n\n  def __init__(self, input_dim, output_dim, dropout_rate=0.2):\n    super(AutonomousDeepLearning, self).__init__()\n    self.input_dim = input_dim\n    self.output_dim = output_dim\n    self.dropout_rate = dropout_rate\n    self.layers = nn.ModuleList([\n        ADLLayer(input_dim, initial_nodes=16, dropout_rate=dropout_rate)\n    ])\n    self.classifier = nn.Linear(16, output_dim)\n\n  def forward(self, x):\n    out = x\n    for layer in self.layers:\n      out = layer(out)\n    out = torch.sigmoid(self.classifier(out))\n    return out\n\n  def compute_bias_variance(self, outputs, targets):\n    with torch.no_grad():\n      preds = (outputs >= 0.5).float()\n      error = torch.abs(preds - targets)\n      bias = torch.mean(error).item()\n      variance = torch.var(outputs).item()\n      if np.isnan(variance):\n        variance = 0.0\n    return bias, variance\n\n  def hoeffding_bound(self, n, delta=1e-7, R=1.0):\n    return math.sqrt((R**2 * math.log(1.0 / delta)) / (2 * n))\n\n  def adapt_structure(\n      self, step, bias, variance, bias_thresh=0.35, var_thresh=0.10\n  ):\n    node_added = False\n    layer_added = False\n    node_cut = False\n    ho_rejected = False\n\n    if bias > bias_thresh:\n      self.layers[-1].add_node()\n      new_out_dim = self.layers[-1].linear.out_features\n\n      old_classifier = self.classifier\n      self.classifier = nn.Linear(new_out_dim, self.output_dim)\n      with torch.no_grad():\n        min_dim = min(old_classifier.weight.shape[1], new_out_dim)\n        self.classifier.weight[:, :min_dim] = old_classifier.weight[:, :min_dim]\n\n      node_added = True\n\n    if variance > var_thresh or (\n        node_added and self.layers[-1].linear.out_features > 64\n    ):\n      new_in_dim = self.layers[-1].linear.out_features\n      new_layer = ADLLayer(\n          new_in_dim, initial_nodes=16, dropout_rate=self.dropout_rate\n      )\n      self.layers.append(new_layer)\n      self.classifier = nn.Linear(16, self.output_dim)\n      layer_added = True\n\n    if not node_added and not layer_added and step > 5:\n      epsilon = self.hoeffding_bound(n=step + 1)\n      if abs(bias - variance) > epsilon:\n        ho_rejected = True\n        node_cut = self.layers[-1].cut_node()\n        if node_cut:\n          new_out_dim = self.layers[-1].linear.out_features\n          old_classifier = self.classifier\n          self.classifier = nn.Linear(new_out_dim, self.output_dim)\n          with torch.no_grad():\n            min_dim = min(old_classifier.weight.shape[1], new_out_dim)\n            self.classifier.weight[:, :min_dim] = old_classifier.weight[\n                :, :min_dim\n            ]\n\n    return node_added, layer_added, ho_rejected, node_cut\n\n\n# ==========================================\n# 3. LOAD DATASET\n# ==========================================\nprint(\"[INFO] Loading dataset EEG...\")\nsubjects = range(1, 6)\nraw, y_raw = [], []\n\nfor subject in subjects:\n  fnames = sorted(glob(\"train/subj%d_series*_data.csv\" % (subject)))\n  for fname in fnames:\n    data, labels = prepare_data_train(fname)\n    raw.append(data)\n    y_raw.append(labels)\n\nX_all = np.asarray(pd.concat(raw).astype(np.float32))\ny_all = np.asarray(pd.concat(y_raw).astype(np.float32))\n\ndel raw, y_raw\ngc.collect()\n\n\n# ==========================================\n# 4. TRAINING LOOP & PENCATATAN METRIK\n# ==========================================\nnum_batch = 198\nhistory_results = {}  # Menampung riwayat untuk grafik & ringkasan\n\nfor band_name, config in SUB_BANDS.items():\n  print(f\"\\n==========================================\")\n  print(f\"   EVALUASI ADL UNTUK: {band_name}\")\n  print(f\"==========================================\")\n\n  if config == \"raw\":\n    processed_X = X_all.copy()\n  else:\n    low, high = config\n    processed_X = butter_bandpass_filter(X_all, low, high, fs=FS)\n\n  scaler = StandardScaler()\n  x_norm = scaler.fit_transform(processed_X)\n\n  splitrate = -x_norm.shape[0] // 5 * 2\n  xval = x_norm[splitrate : splitrate // 2]\n  yval = y_all[splitrate : splitrate // 2]\n  xtrain = x_norm[:splitrate]\n  ytrain = y_all[:splitrate]\n\n  train_gen, val_gen = create_generators(xtrain, ytrain, xval, yval)\n\n  input_dim = (time_steps // subsample) * 32\n  output_dim = 6\n\n  adl_model = AutonomousDeepLearning(input_dim, output_dim)\n  criterion = nn.BCELoss()\n  lr = 0.001\n  optimizer = optim.Adam(adl_model.parameters(), lr=lr, weight_decay=1e-4)\n\n  print(f\"Number of input:  {input_dim}\")\n  print(f\"Number of output: {output_dim}\")\n  print(f\"Number of batch:  {num_batch}\")\n  print(\"------------------------------------------\")\n\n  val_acc_list = []\n  val_loss_list = []\n\n  for step in range(num_batch):\n    # --- EVALUASI ---\n    start_test = time.time()\n    adl_model.eval()\n\n    x_val_np, y_val_np = next(val_gen)\n    x_val_flat = x_val_np.reshape(x_val_np.shape[0], -1)\n\n    x_val_tensor = torch.tensor(x_val_flat, dtype=torch.float32)\n    y_val_tensor = torch.tensor(y_val_np, dtype=torch.float32)\n\n    with torch.no_grad():\n      val_outputs = adl_model(x_val_tensor)\n      v_loss = criterion(val_outputs, y_val_tensor).item()\n\n      preds = (val_outputs >= 0.5).float()\n      correct = (preds == y_val_tensor).float()\n      v_acc = (correct.sum() / correct.numel()).item() * 100.0\n\n    test_time = time.time() - start_test\n\n    val_acc_list.append(v_acc)\n    val_loss_list.append(v_loss)\n\n    print(f\"\\n{step} - th batch of: {num_batch}\")\n    print(f\"Testing Accuracy: {v_acc}\")\n    print(f\"Testing Loss: {v_loss}\")\n    print(f\"Testing Time: {test_time}\")\n\n    # --- TRAINING ---\n    adl_model.train()\n    x_batch_np, y_batch_np = next(train_gen)\n    x_batch_flat = x_batch_np.reshape(x_batch_np.shape[0], -1)\n\n    x_tensor = torch.tensor(x_batch_flat, dtype=torch.float32)\n    y_tensor = torch.tensor(y_batch_np, dtype=torch.float32)\n\n    optimizer.zero_grad()\n    train_outputs = adl_model(x_tensor)\n    loss = criterion(train_outputs, y_tensor)\n    loss.backward()\n    optimizer.step()\n\n    bias, variance = adl_model.compute_bias_variance(train_outputs, y_tensor)\n\n    node_added, layer_added, ho_rejected, node_cut = adl_model.adapt_structure(\n        step, bias, variance, bias_thresh=0.35, var_thresh=0.10\n    )\n\n    if node_added:\n      print(f\"Adjust  {len(adl_model.layers)} -th hidden layer\")\n      print(\"+++ GROW a hidden NODE +++\")\n\n    if layer_added:\n      print(\"+++ GROW a hidden LAYER +++\")\n\n    if ho_rejected:\n      print(\">>> HO REJECTED <<<\")\n\n    if node_cut:\n      print(\"--- CUT a hidden NODE ---\")\n\n    print(f\"Bias:  {bias}\")\n    print(f\"Variance: {variance}\")\n\n    if node_added or layer_added or node_cut:\n      optimizer = optim.Adam(\n          adl_model.parameters(), lr=lr, weight_decay=1e-4\n      )\n\n  history_results[band_name] = {\"acc\": val_acc_list, \"loss\": val_loss_list}\n\n  del processed_X, x_norm, xtrain, ytrain, xval, yval, adl_model\n  gc.collect()\n\ndel X_all, y_all\ngc.collect()\n\n\n# ==========================================\n# 5. GRAFIK PERBANDINGAN SUB-BANDS\n# ==========================================\nfig, axes = plt.subplots(1, 2, figsize=(16, 5))\ncolors = [\"black\", \"purple\", \"blue\", \"green\", \"orange\", \"red\"]\n\n# Plot Testing Accuracy\nfor idx, (band_name, metrics) in enumerate(history_results.items()):\n  axes[0].plot(\n      metrics[\"acc\"], label=band_name, color=colors[idx % len(colors)], alpha=0.8\n  )\n\naxes[0].set_title(\"Perbandingan Validation / Testing Accuracy ADL\")\naxes[0].set_xlabel(\"Batch Step\")\naxes[0].set_ylabel(\"Accuracy (%)\")\naxes[0].legend()\naxes[0].grid(True, alpha=0.3)\n\n# Plot Testing Loss\nfor idx, (band_name, metrics) in enumerate(history_results.items()):\n  axes[1].plot(\n      metrics[\"loss\"],\n      label=band_name,\n      color=colors[idx % len(colors)],\n      alpha=0.8,\n  )\n\naxes[1].set_title(\"Perbandingan Validation / Testing Loss ADL\")\naxes[1].set_xlabel(\"Batch Step\")\naxes[1].set_ylabel(\"Loss\")\naxes[1].legend()\naxes[1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n\n\n# ==========================================\n# 6. RINGKASAN PERFORMA\n# ==========================================\nprint(\"\\n--- RINGKASAN PERFORMA ADL (RAW VS SUB-BANDS) ---\")\nsummary_data = []\n\nfor band_name, metrics in history_results.items():\n  summary_data.append({\n      \"Sub-band / Input\": band_name,\n      \"Max Test Acc (%)\": np.max(metrics[\"acc\"]),\n      \"Min Test Loss\": np.min(metrics[\"loss\"]),\n      \"Avg Test Acc (%)\": np.mean(metrics[\"acc\"]),\n  })\n\nsummary_df = pd.DataFrame(summary_data).sort_values(\n    by=\"Max Test Acc (%)\", ascending=False\n)\nprint(summary_df.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T16:09:26.259838Z","iopub.execute_input":"2026-09-02T16:09:26.260578Z","iopub.status.idle":"2026-09-02T16:11:57.730930Z","shell.execute_reply.started":"2026-09-02T16:09:26.260545Z","shell.execute_reply":"2026-09-02T16:11:57.730110Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"'''\nSub-band Mu/Alpha (8–12 Hz) mencatatkan performa terbaik dengan Avg Test Acc tertinggi (92.92%) dan Min Test Loss yang sangat rendah (0.0408). Gelombang Mu/Alpha sangat erat kaitannya dengan aktivitas motorik (motor imagery / gerakan tangan), sehingga fiturnya sangat dominan dan mudah dikenali oleh arsitektur ADL.\n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## UJI LOSO ADL","metadata":{}},{"cell_type":"code","source":"import gc\nfrom glob import glob\nimport math\nimport time\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\nfrom sklearn.preprocessing import StandardScaler\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\n\n# ==========================================\n# 0. KONFIGURASI & SEED\n# ==========================================\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nFS = 500.0  # Frekuensi sampling EEG\ntime_steps = 1000\nsubsample = 50\n\n# Fokus Khusus Fitur Alpha / Mu (8 - 12 Hz)\nALPHA_LOW = 8.0\nALPHA_HIGH = 12.0\n\n\n# ==========================================\n# 1. HELPER & PREPROCESSING\n# ==========================================\ndef prepare_data_train(fname):\n  data = pd.read_csv(fname)\n  events_fname = fname.replace(\"_data\", \"_events\")\n  labels = pd.read_csv(events_fname)\n  return data.drop([\"id\"], axis=1), labels.drop([\"id\"], axis=1)\n\n\ndef butter_bandpass_filter(data, lowcut, highcut, fs=FS, order=4):\n  nyq = 0.5 * fs\n  b, a = butter(order, [lowcut / nyq, highcut / nyq], btype=\"band\")\n  return filtfilt(b, a, data, axis=0)\n\n\ndef create_loso_generators(xtrain, ytrain, xtest, ytest, batch_size=32):\n  def train_generator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xtrain) - time_steps - 1)\n        x_time_data[i] = xtrain[idx : idx + time_steps : subsample]\n        yy.append(ytrain[idx + time_steps])\n      yield x_time_data, np.asarray(yy, dtype=np.float32)\n\n  def test_generator():\n    while True:\n      x_time_data = np.zeros(\n          (batch_size, time_steps // subsample, 32), dtype=np.float32\n      )\n      yy = []\n      for i in range(batch_size):\n        idx = np.random.randint(0, len(xtest) - time_steps - 1)\n        x_time_data[i] = xtest[idx : idx + time_steps : subsample]\n        yy.append(ytest[idx + time_steps])\n      yield x_time_data, np.asarray(yy, dtype=np.float32)\n\n  return train_generator(), test_generator()\n\n\n# ==========================================\n# 2. MODEL ARSITEKTUR ADL\n# ==========================================\nclass ADLLayer(nn.Module):\n\n  def __init__(self, in_features, initial_nodes=16, dropout_rate=0.2):\n    super(ADLLayer, self).__init__()\n    self.linear = nn.Linear(in_features, initial_nodes)\n    self.dropout = nn.Dropout(dropout_rate)\n\n  def forward(self, x):\n    out = torch.relu(self.linear(x))\n    return self.dropout(out)\n\n  def add_node(self):\n    old_out, old_in = self.linear.weight.shape\n    new_weight = torch.randn(old_out + 1, old_in) * 0.01\n    new_bias = torch.zeros(old_out + 1)\n    new_weight[:old_out, :] = self.linear.weight.data\n    new_bias[:old_out] = self.linear.bias.data\n\n    self.linear = nn.Linear(old_in, old_out + 1)\n    self.linear.weight = nn.Parameter(new_weight)\n    self.linear.bias = nn.Parameter(new_bias)\n\n  def cut_node(self):\n    old_out, old_in = self.linear.weight.shape\n    if old_out <= 4:\n      return False\n    weights_sum = torch.sum(torch.abs(self.linear.weight.data), dim=1)\n    min_node_idx = torch.argmin(weights_sum).item()\n    keep_indices = [i for i in range(old_out) if i != min_node_idx]\n\n    new_weight = self.linear.weight.data[keep_indices, :]\n    new_bias = self.linear.bias.data[keep_indices]\n\n    self.linear = nn.Linear(old_in, old_out - 1)\n    self.linear.weight = nn.Parameter(new_weight)\n    self.linear.bias = nn.Parameter(new_bias)\n    return True\n\n\nclass AutonomousDeepLearning(nn.Module):\n\n  def __init__(self, input_dim, output_dim, dropout_rate=0.2):\n    super(AutonomousDeepLearning, self).__init__()\n    self.input_dim = input_dim\n    self.output_dim = output_dim\n    self.dropout_rate = dropout_rate\n    self.layers = nn.ModuleList([\n        ADLLayer(input_dim, initial_nodes=16, dropout_rate=dropout_rate)\n    ])\n    self.classifier = nn.Linear(16, output_dim)\n\n  def forward(self, x):\n    out = x\n    for layer in self.layers:\n      out = layer(out)\n    out = torch.sigmoid(self.classifier(out))\n    return out\n\n  def compute_bias_variance(self, outputs, targets):\n    with torch.no_grad():\n      preds = (outputs >= 0.5).float()\n      error = torch.abs(preds - targets)\n      bias = torch.mean(error).item()\n      variance = torch.var(outputs).item()\n      if np.isnan(variance):\n        variance = 0.0\n    return bias, variance\n\n  def hoeffding_bound(self, n, delta=1e-7, R=1.0):\n    return math.sqrt((R**2 * math.log(1.0 / delta)) / (2 * n))\n\n  def adapt_structure(\n      self, step, bias, variance, bias_thresh=0.35, var_thresh=0.10\n  ):\n    node_added, layer_added, node_cut, ho_rejected = False, False, False, False\n\n    if bias > bias_thresh:\n      self.layers[-1].add_node()\n      new_out_dim = self.layers[-1].linear.out_features\n      old_classifier = self.classifier\n      self.classifier = nn.Linear(new_out_dim, self.output_dim)\n      with torch.no_grad():\n        min_dim = min(old_classifier.weight.shape[1], new_out_dim)\n        self.classifier.weight[:, :min_dim] = old_classifier.weight[:, :min_dim]\n      node_added = True\n\n    if variance > var_thresh or (\n        node_added and self.layers[-1].linear.out_features > 64\n    ):\n      new_in_dim = self.layers[-1].linear.out_features\n      new_layer = ADLLayer(\n          new_in_dim, initial_nodes=16, dropout_rate=self.dropout_rate\n      )\n      self.layers.append(new_layer)\n      self.classifier = nn.Linear(16, self.output_dim)\n      layer_added = True\n\n    if not node_added and not layer_added and step > 5:\n      epsilon = self.hoeffding_bound(n=step + 1)\n      if abs(bias - variance) > epsilon:\n        ho_rejected = True\n        node_cut = self.layers[-1].cut_node()\n        if node_cut:\n          new_out_dim = self.layers[-1].linear.out_features\n          old_classifier = self.classifier\n          self.classifier = nn.Linear(new_out_dim, self.output_dim)\n          with torch.no_grad():\n            min_dim = min(old_classifier.weight.shape[1], new_out_dim)\n            self.classifier.weight[:, :min_dim] = old_classifier.weight[\n                :, :min_dim\n            ]\n\n    return node_added, layer_added, ho_rejected, node_cut\n\n\n# ==========================================\n# 3. LOAD DATASET PER SUBJEK (UNTUK LOSO)\n# ==========================================\nprint(\"[INFO] Loading & Filtering Dataset (Alpha Band 8-12 Hz)...\")\nsubject_ids = list(range(1, 13))  # 5 Subjek\nsubjects_data = {}\n\nfor sub in subject_ids:\n  fnames = sorted(glob(\"train/subj%d_series*_data.csv\" % (sub)))\n  raw_sub, y_sub = [], []\n\n  for fname in fnames:\n    data, labels = prepare_data_train(fname)\n    raw_sub.append(data)\n    y_sub.append(labels)\n\n  X_sub = np.asarray(pd.concat(raw_sub).astype(np.float32))\n  Y_sub = np.asarray(pd.concat(y_sub).astype(np.float32))\n\n  # Terapkan Bandpass Filter Alpha (8-12 Hz) per Subjek\n  X_alpha = butter_bandpass_filter(X_sub, ALPHA_LOW, ALPHA_HIGH, fs=FS)\n\n  subjects_data[sub] = {\"X\": X_alpha, \"Y\": Y_sub}\n  print(f\"Data Subjek {sub} Siap. Ukuran: {X_alpha.shape}\")\n\ngc.collect()\n\n\n# ==========================================\n# 4. EVLAUASI LEAVE-ONE-SUBJECT-OUT (LOSO)\n# ==========================================\nnum_batch = 100\nloso_summary = []\n\nprint(\"\\n==========================================\")\nprint(\"   MULAI EVALUASI LOSO (ALPHA BAND 8-12 Hz)\")\nprint(\"==========================================\")\n\nfor test_sub in subject_ids:\n  print(f\"\\n>>> LOSO FOLD: Testing pada Subjek {test_sub} <<<\")\n\n  # 1. Pisahkan Data Train (N-1 Subjek) dan Test (1 Subjek)\n  train_X_list = [\n      subjects_data[s][\"X\"] for s in subject_ids if s != test_sub\n  ]\n  train_Y_list = [\n      subjects_data[s][\"Y\"] for s in subject_ids if s != test_sub\n  ]\n\n  X_train = np.vstack(train_X_list)\n  Y_train = np.vstack(train_Y_list)\n\n  X_test = subjects_data[test_sub][\"X\"]\n  Y_test = subjects_data[test_sub][\"Y\"]\n\n  # 2. Normalisasi Data (Fit pada Train, Transform pada Test)\n  scaler = StandardScaler()\n  X_train_norm = scaler.fit_transform(X_train)\n  X_test_norm = scaler.transform(X_test)\n\n  # 3. Inisialisasi Generator & Model ADL\n  train_gen, test_gen = create_loso_generators(\n      X_train_norm, Y_train, X_test_norm, Y_test\n  )\n\n  input_dim = (time_steps // subsample) * 32\n  output_dim = 6\n\n  adl_model = AutonomousDeepLearning(input_dim, output_dim)\n  criterion = nn.BCELoss()\n  lr = 0.001\n  optimizer = optim.Adam(adl_model.parameters(), lr=lr, weight_decay=1e-4)\n\n  test_acc_fold = []\n  test_loss_fold = []\n\n  # 4. Iterasi Batch\n  for step in range(num_batch):\n    # --- EVALUASI PADA TEST SUBJECT ---\n    adl_model.eval()\n    x_test_np, y_test_np = next(test_gen)\n    x_test_flat = x_test_np.reshape(x_test_np.shape[0], -1)\n\n    x_test_tensor = torch.tensor(x_test_flat, dtype=torch.float32)\n    y_test_tensor = torch.tensor(y_test_np, dtype=torch.float32)\n\n    with torch.no_grad():\n      test_outputs = adl_model(x_test_tensor)\n      t_loss = criterion(test_outputs, y_test_tensor).item()\n\n      preds = (test_outputs >= 0.5).float()\n      correct = (preds == y_test_tensor).float()\n      t_acc = (correct.sum() / correct.numel()).item() * 100.0\n\n    test_acc_fold.append(t_acc)\n    test_loss_fold.append(t_loss)\n\n    # --- TRAINING PADA TRAIN SUBJECTS ---\n    adl_model.train()\n    x_train_np, y_train_np = next(train_gen)\n    x_train_flat = x_train_np.reshape(x_train_np.shape[0], -1)\n\n    x_train_tensor = torch.tensor(x_train_flat, dtype=torch.float32)\n    y_train_tensor = torch.tensor(y_train_np, dtype=torch.float32)\n\n    optimizer.zero_grad()\n    train_outputs = adl_model(x_train_tensor)\n    loss = criterion(train_outputs, y_train_tensor)\n    loss.backward()\n    optimizer.step()\n\n    # Adaptasi Struktur ADL\n    bias, variance = adl_model.compute_bias_variance(\n        train_outputs, y_train_tensor\n    )\n    node_added, layer_added, ho_rejected, node_cut = adl_model.adapt_structure(\n        step, bias, variance\n    )\n\n    if node_added or layer_added or node_cut:\n      optimizer = optim.Adam(\n          adl_model.parameters(), lr=lr, weight_decay=1e-4\n      )\n\n  # Simpan Ringkasan Performa Fold Ini\n  max_acc = np.max(test_acc_fold)\n  avg_acc = np.mean(test_acc_fold)\n  min_loss = np.min(test_loss_fold)\n\n  loso_summary.append({\n      \"Test Subject\": f\"Subjek {test_sub}\",\n      \"Max Test Acc (%)\": max_acc,\n      \"Avg Test Acc (%)\": avg_acc,\n      \"Min Test Loss\": min_loss,\n  })\n\n  print(\n      f\"--> [HASIL SUBJEK {test_sub}] Max Acc: {max_acc:.2f}% | Avg Acc:\"\n      f\" {avg_acc:.2f}% | Min Loss: {min_loss:.4f}\"\n  )\n\n  del adl_model\n  gc.collect()\n\n\n# ==========================================\n# 5. TABEL RINGKASAN PERFORMA LOSO\n# ==========================================\nprint(\"\\n==========================================\")\nprint(\"   RINGKASAN AKHIR LOSO - FITUR ALPHA (8-12 Hz)\")\nprint(\"==========================================\")\ndf_loso = pd.DataFrame(loso_summary)\nprint(df_loso.to_string(index=False))\n\n# Rata-rata Keseluruhan Subjek\nprint(\"------------------------------------------\")\nprint(f\"Rata-rata Akurasi Keseluruhan: {df_loso['Avg Test Acc (%)'].mean():.2f}%\")\nprint(f\"Rata-rata Akurasi Maksimum  : {df_loso['Max Test Acc (%)'].mean():.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T16:24:56.282780Z","iopub.execute_input":"2026-09-02T16:24:56.283277Z","iopub.status.idle":"2026-09-02T16:29:25.067306Z","shell.execute_reply.started":"2026-09-02T16:24:56.283245Z","shell.execute_reply":"2026-09-02T16:29:25.066173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}