From e1655ac800bcecb6e709db3a64dcbfa7b46c8433 Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Mon, 14 Feb 2022 14:19:16 +0800 Subject: [PATCH 1/2] add nn --- .gitignore | 6 +- experiments/source_separation/devt.ipynb | 1994 +++++++++++++++++ experiments/ssl_classifier/deep4net.py | 449 ++++ .../ssl_classifier/devt_deep_cnn.ipynb | 1130 ++++++++++ .../ssl_classifier/main_cca_hsssvep.py | 107 + .../ssl_classifier/main_deep4net_hsssvep.py | 602 +++++ .../main_eegnet_hsssvep-run2-all-views.py | 193 ++ .../ssl_classifier/main_eegnet_hsssvep.py | 186 ++ .../ssl_classifier/main_ssl_hsssvep.py | 240 ++ .../ssl_classifier/main_trca_hsssvep.py | 103 + experiments/two-pathway/Untitled.ipynb | 1048 +++++++++ requirements.txt | 4 +- splearn/cross_decomposition/cca.py | 5 +- .../classifier.py | 0 splearn/data/__init__.py | 6 + splearn/data/hsssvep.py | 75 + splearn/data/multiple_subjects.py | 111 + splearn/data/pytorch_dataset.py | 65 + splearn/data/utils.py | 4 + splearn/filter/butterworth.py | 403 ++++ splearn/filter/channels.py | 86 + splearn/nn/base/__init__.py | 2 + splearn/nn/base/classifier.py | 63 + splearn/nn/base/lightning.py | 43 + splearn/nn/loss.py | 42 + splearn/nn/models/EEGNet/CompactEEGNet.py | 64 + splearn/nn/models/EEGNet/__init__.py | 0 .../nn/models/SSLClassifier/SSLClassifier.py | 71 + splearn/nn/models/SimSiam/SimSiam.py | 124 + splearn/nn/models/SimSiam/__init__.py | 1 + splearn/nn/models/__init__.py | 3 + splearn/nn/modules/conv1d.py | 117 + splearn/nn/modules/conv2d.py | 327 +++ splearn/nn/modules/functional.py | 31 + splearn/nn/modules/positional_encoding.py | 27 + .../modules/relative_multi_head_attention.py | 96 + .../nn/modules/residual_connection_module.py | 26 + splearn/nn/optimization.py | 359 +++ splearn/nn/utils.py | 42 + splearn/utils/__init__.py | 2 + splearn/utils/config.py | 17 + splearn/utils/logger.py | 32 + tutorials/Butterworth Filter.ipynb | 233 ++ 43 files changed, 8535 insertions(+), 4 deletions(-) create mode 100644 experiments/source_separation/devt.ipynb create mode 100644 experiments/ssl_classifier/deep4net.py create mode 100644 experiments/ssl_classifier/devt_deep_cnn.ipynb create mode 100644 experiments/ssl_classifier/main_cca_hsssvep.py create mode 100644 experiments/ssl_classifier/main_deep4net_hsssvep.py create mode 100644 experiments/ssl_classifier/main_eegnet_hsssvep-run2-all-views.py create mode 100644 experiments/ssl_classifier/main_eegnet_hsssvep.py create mode 100644 experiments/ssl_classifier/main_ssl_hsssvep.py create mode 100644 experiments/ssl_classifier/main_trca_hsssvep.py create mode 100644 experiments/two-pathway/Untitled.ipynb rename splearn/{classes => cross_decomposition}/classifier.py (100%) create mode 100644 splearn/data/hsssvep.py create mode 100644 splearn/data/multiple_subjects.py create mode 100644 splearn/data/pytorch_dataset.py create mode 100644 splearn/data/utils.py create mode 100644 splearn/filter/butterworth.py create mode 100644 splearn/filter/channels.py create mode 100644 splearn/nn/base/__init__.py create mode 100644 splearn/nn/base/classifier.py create mode 100644 splearn/nn/base/lightning.py create mode 100644 splearn/nn/loss.py create mode 100644 splearn/nn/models/EEGNet/CompactEEGNet.py create mode 100644 splearn/nn/models/EEGNet/__init__.py create mode 100644 splearn/nn/models/SSLClassifier/SSLClassifier.py create mode 100644 splearn/nn/models/SimSiam/SimSiam.py create mode 100644 splearn/nn/models/SimSiam/__init__.py create mode 100644 splearn/nn/models/__init__.py create mode 100644 splearn/nn/modules/conv1d.py create mode 100644 splearn/nn/modules/conv2d.py create mode 100644 splearn/nn/modules/functional.py create mode 100644 splearn/nn/modules/positional_encoding.py create mode 100644 splearn/nn/modules/relative_multi_head_attention.py create mode 100644 splearn/nn/modules/residual_connection_module.py create mode 100644 splearn/nn/optimization.py create mode 100644 splearn/nn/utils.py create mode 100644 splearn/utils/__init__.py create mode 100644 splearn/utils/config.py create mode 100644 splearn/utils/logger.py create mode 100644 tutorials/Butterworth Filter.ipynb diff --git a/.gitignore b/.gitignore index 7d67f36..34d909f 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,8 @@ dmypy.json # System Files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +_devt/ +tensorboard_logs/ +run_logs/ diff --git a/experiments/source_separation/devt.ipynb b/experiments/source_separation/devt.ipynb new file mode 100644 index 0000000..cccdbb5 --- /dev/null +++ b/experiments/source_separation/devt.ipynb @@ -0,0 +1,1994 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# import os\n", + "cwd = os.getcwd()\n", + "import sys\n", + "path = os.path.join(cwd, \"..\\\\..\\\\\")\n", + "sys.path.append(path)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "from torch.utils.data import DataLoader\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "from torchlibrosa.stft import ISTFT, STFT, magphase\n", + "\n", + "import pytorch_lightning\n", + "from pytorch_lightning import Trainer, seed_everything\n", + "from pytorch_lightning.callbacks import LearningRateMonitor\n", + "from pytorch_lightning.loggers import TensorBoardLogger\n", + "\n", + "import logging\n", + "import warnings\n", + "logging.getLogger('lightning').setLevel(0)\n", + "warnings.filterwarnings('ignore')\n", + "pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR)\n", + "\n", + "from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP\n", + "from splearn.filter.butterworth import butter_bandpass_filter\n", + "from splearn.filter.notch import notch_filter\n", + "from splearn.filter.channels import pick_channels\n", + "from splearn.utils import Logger, Config\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n" + ] + }, + { + "data": { + "text/plain": [ + "1234" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "config = {\n", + " \"run_name\": \"ssl_hsssvep\",\n", + " \"data\": {\n", + " \"load_subject_ids\": np.arange(1,36),\n", + " \"selected_channels\": [\"PO8\", \"PZ\", \"PO7\", \"PO4\", \"POz\", \"PO3\", \"O2\", \"Oz\", \"O1\"],\n", + " \"input_channels\": 9,\n", + " \"target_sources_num\": 40,\n", + " \"sample_length\": 250,\n", + " \"num_classes\": 40\n", + " },\n", + " \"training\": {\n", + " \"num_epochs\": 100,\n", + " \"num_warmup_epochs\": 10,\n", + " \"learning_rate\": 0.03,\n", + " # \"gpus\": torch.cuda.device_count(),\n", + " \"gpus\": [0],\n", + " \"batchsize\": 256\n", + " },\n", + " \"model\": {\n", + " \"projection_size\": 1024,\n", + " \"optimizer\": \"adamw\",\n", + " \"scheduler\": \"cosine_with_warmup\",\n", + " },\n", + " \"testing\": {\n", + " \"test_subject_ids\": np.arange(33,34),\n", + " \"kfolds\": np.arange(0,3),\n", + " },\n", + " \"seed\": 1234\n", + "}\n", + "\n", + "main_logger = Logger(filename_postfix=config[\"run_name\"])\n", + "main_logger.write_to_log(\"Config\")\n", + "main_logger.write_to_log(config)\n", + "\n", + "config = Config(config)\n", + "\n", + "seed_everything(config.seed)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Load subject: 1\n", + "Load subject: 2\n", + "Load subject: 3\n", + "Load subject: 4\n", + "Load subject: 5\n", + "Load subject: 6\n", + "Load subject: 7\n", + "Load subject: 8\n", + "Load subject: 9\n", + "Load subject: 10\n", + "Load subject: 11\n", + "Load subject: 12\n", + "Load subject: 13\n", + "Load subject: 14\n", + "Load subject: 15\n", + "Load subject: 16\n", + "Load subject: 17\n", + "Load subject: 18\n", + "Load subject: 19\n", + "Load subject: 20\n", + "Load subject: 21\n", + "Load subject: 22\n", + "Load subject: 23\n", + "Load subject: 24\n", + "Load subject: 25\n", + "Load subject: 26\n", + "Load subject: 27\n", + "Load subject: 28\n", + "Load subject: 29\n", + "Load subject: 30\n", + "Load subject: 31\n", + "Load subject: 32\n", + "Load subject: 33\n", + "Load subject: 34\n", + "Load subject: 35\n", + "Final data shape: (35, 240, 9, 250)\n", + "train_loader (5440, 9, 250) (5440,)\n", + "val_loader (2720, 9, 250) (2720,)\n", + "test_loader (240, 9, 250) (240,)\n" + ] + } + ], + "source": [ + "def onehot_targets(targets):\n", + " return (np.arange(targets.max()+1) == targets[...,None]).astype(int)\n", + "\n", + "\n", + "def func_preprocessing(data):\n", + " data_x = data.data\n", + " # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4']\n", + " selected_channels = config.data.selected_channels\n", + " data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels)\n", + " # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0)\n", + " data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6)\n", + " start_t = 125\n", + " end_t = 125 + 250\n", + " data_x = data_x[:,:,:,start_t:end_t]\n", + " data.set_data(data_x)\n", + "\n", + "\n", + "def leave_one_subject_out(data, **kwargs):\n", + " \n", + " test_subject_id = kwargs[\"test_subject_id\"] if \"test_subject_id\" in kwargs else 1\n", + " kfold_k = kwargs[\"kfold_k\"] if \"kfold_k\" in kwargs else 0\n", + " kfold_split = kwargs[\"kfold_split\"] if \"kfold_split\" in kwargs else 3\n", + " \n", + " # get test data\n", + " # test_sub_idx = data.subject_ids.index(test_subject_id)\n", + " test_sub_idx = np.where(data.subject_ids == test_subject_id)[0][0]\n", + " selected_subject_data = data.data[test_sub_idx]\n", + " selected_subject_targets = data.targets[test_sub_idx]\n", + " # selected_subject_targets = onehot_targets(selected_subject_targets)\n", + " test_dataset = PyTorchDataset(selected_subject_data, selected_subject_targets)\n", + " # num_targets = selected_subject_targets.shape[1]\n", + "\n", + " # get train val data\n", + " indices = np.arange(data.data.shape[0])\n", + " train_val_data = data.data[indices!=test_sub_idx, :, :, :]\n", + " \n", + " train_val_data = train_val_data.reshape((train_val_data.shape[0]*train_val_data.shape[1], train_val_data.shape[2], train_val_data.shape[3]))\n", + " train_val_targets = data.targets[indices!=test_sub_idx, :]\n", + " train_val_targets = train_val_targets.reshape((train_val_targets.shape[0]*train_val_targets.shape[1]))\n", + " \n", + " # train test split\n", + " (X_train, y_train), (X_val, y_val) = data.dataset_split_stratified(train_val_data, train_val_targets, k=kfold_k, n_splits=kfold_split)\n", + " # y_train = onehot_targets(y_train)\n", + " # y_val = onehot_targets(y_val)\n", + " # print(\"X_train.shape, X_val.shape\", X_train.shape, X_val.shape, y_train.shape, y_val.shape)\n", + " \n", + " # create dataset\n", + " train_dataset = PyTorchDataset(X_train, y_train)\n", + " val_dataset = PyTorchDataset(X_val, y_val)\n", + "\n", + " return train_dataset, val_dataset, test_dataset\n", + "\n", + "data = MultipleSubjects(\n", + " dataset=HSSSVEP, \n", + " root=os.path.join(path, \"../data/hsssvep\"), \n", + " subject_ids=config.data.load_subject_ids, \n", + " func_preprocessing=func_preprocessing,\n", + " func_get_train_val_test_dataset=leave_one_subject_out,\n", + " verbose=True, \n", + ")\n", + "\n", + "print(\"Final data shape:\", data.data.shape)\n", + "\n", + "test_subject_id = 33\n", + "kfold_k = 0\n", + "\n", + "train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + "train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + "val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + "print(\"train_loader\", train_loader.dataset.data.shape, train_loader.dataset.targets.shape)\n", + "print(\"val_loader\", val_loader.dataset.data.shape, val_loader.dataset.targets.shape)\n", + "print(\"test_loader\", test_loader.dataset.data.shape, test_loader.dataset.targets.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + " \n", + "# class ResUNet143_Subbandtime(nn.Module, Base):\n", + "# def __init__(self, input_channels, target_sources_num):\n", + "# super(ResUNet143_Subbandtime, self).__init__()\n", + " \n", + "# self.input_channels = input_channels\n", + "# self.target_sources_num = target_sources_num\n", + "\n", + "# window_size = 64\n", + "# hop_size = 25\n", + "# center = True\n", + "# pad_mode = \"reflect\"\n", + "# window = \"hann\"\n", + "# activation = \"leaky_relu\"\n", + "# momentum = 0.01\n", + "\n", + "# self.subbands_num = 1 # 4\n", + "# self.K = 4 # outputs: |M|, cos∠M, sin∠M, Q\n", + "\n", + "# self.downsample_ratio = 2 ** 3 # 5 # This number equals 2^{#encoder_blcoks}\n", + "\n", + "# self.stft = STFT(\n", + "# n_fft=window_size,\n", + "# hop_length=hop_size,\n", + "# win_length=window_size,\n", + "# window=window,\n", + "# center=center,\n", + "# pad_mode=pad_mode,\n", + "# freeze_parameters=True,\n", + "# )\n", + "\n", + "# self.istft = ISTFT(\n", + "# n_fft=window_size,\n", + "# hop_length=hop_size,\n", + "# win_length=window_size,\n", + "# window=window,\n", + "# center=center,\n", + "# pad_mode=pad_mode,\n", + "# freeze_parameters=True,\n", + "# )\n", + " \n", + "# self.bn0 = nn.BatchNorm2d(window_size // 2 + 1, momentum=momentum)\n", + " \n", + "# self.encoder_block1 = EncoderBlockRes4B(\n", + "# in_channels=input_channels,\n", + "# out_channels=32,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + " \n", + "# self.encoder_block2 = EncoderBlockRes4B(\n", + "# in_channels=32,\n", + "# out_channels=64,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block3 = EncoderBlockRes4B(\n", + "# in_channels=64,\n", + "# out_channels=128,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block4 = EncoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=256,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block5 = EncoderBlockRes4B(\n", + "# in_channels=256,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block6 = EncoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + " \n", + "# conv_block_in_channels = 128 # 384\n", + " \n", + "# self.conv_block7a = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7b = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7c = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7d = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + " \n", + "# self.decoder_block1 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(1, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block2 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block3 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=256,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block4 = DecoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=128,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block5 = DecoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=64,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block6 = DecoderBlockRes4B(\n", + "# in_channels=64,\n", + "# out_channels=32,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "\n", + "# self.after_conv_block1 = EncoderBlockRes4B(\n", + "# in_channels=32,\n", + "# out_channels=32,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "\n", + "# self.after_conv2 = nn.Conv2d(\n", + "# in_channels=32,\n", + "# out_channels=target_sources_num\n", + "# * input_channels\n", + "# * self.K\n", + "# * self.subbands_num,\n", + "# kernel_size=(1, 1),\n", + "# stride=(1, 1),\n", + "# padding=(0, 0),\n", + "# bias=True,\n", + "# )\n", + " \n", + "# self.out_conv_block = EncoderBlockRes4B(\n", + "# in_channels=target_sources_num\n", + "# * input_channels\n", + "# * self.subbands_num,\n", + "# out_channels=target_sources_num,\n", + "# kernel_size=(1, 1),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "\n", + "# self.init_weights()\n", + " \n", + "# def init_weights(self):\n", + "# init_bn(self.bn0)\n", + "# init_layer(self.after_conv2)\n", + " \n", + "# def feature_maps_to_wav(\n", + "# self,\n", + "# input_tensor: torch.Tensor,\n", + "# sp: torch.Tensor,\n", + "# sin_in: torch.Tensor,\n", + "# cos_in: torch.Tensor,\n", + "# audio_length: int,\n", + "# ) -> torch.Tensor:\n", + "# r\"\"\"Convert feature maps to waveform.\n", + "# Args:\n", + "# input_tensor: (batch_size, target_sources_num * input_channels * self.K, time_steps, freq_bins)\n", + "# sp: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + "# sin_in: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + "# cos_in: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + "# Outputs:\n", + "# waveform: (batch_size, target_sources_num * input_channels, segment_samples)\n", + "# \"\"\"\n", + "# batch_size, _, time_steps, freq_bins = input_tensor.shape\n", + "\n", + "# x = input_tensor.reshape(\n", + "# batch_size,\n", + "# self.target_sources_num,\n", + "# self.input_channels,\n", + "# self.K,\n", + "# time_steps,\n", + "# freq_bins,\n", + "# )\n", + "# # x: (batch_size, target_sources_num, input_channles, K, time_steps, freq_bins)\n", + "\n", + "# mask_mag = torch.sigmoid(x[:, :, :, 0, :, :])\n", + "# _mask_real = torch.tanh(x[:, :, :, 1, :, :])\n", + "# _mask_imag = torch.tanh(x[:, :, :, 2, :, :])\n", + "# linear_mag = torch.tanh(x[:, :, :, 3, :, :])\n", + "# _, mask_cos, mask_sin = magphase(_mask_real, _mask_imag)\n", + "# # mask_cos, mask_sin: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + "# # Y = |Y|cos∠Y + j|Y|sin∠Y\n", + "# # = |Y|cos(∠X + ∠M) + j|Y|sin(∠X + ∠M)\n", + "# # = |Y|(cos∠X cos∠M - sin∠X sin∠M) + j|Y|(sin∠X cos∠M + cos∠X sin∠M)\n", + "# out_cos = (\n", + "# cos_in[:, None, :, :, :] * mask_cos - sin_in[:, None, :, :, :] * mask_sin\n", + "# )\n", + "# out_sin = (\n", + "# sin_in[:, None, :, :, :] * mask_cos + cos_in[:, None, :, :, :] * mask_sin\n", + "# )\n", + "# # out_cos: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "# # out_sin: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + "# # Calculate |Y|.\n", + "# out_mag = F.relu_(sp[:, None, :, :, :] * mask_mag + linear_mag)\n", + "# # out_mag: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + "# # Calculate Y_{real} and Y_{imag} for ISTFT.\n", + "# out_real = out_mag * out_cos\n", + "# out_imag = out_mag * out_sin\n", + "# # out_real, out_imag: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + "# # Reformat shape to (n, 1, time_steps, freq_bins) for ISTFT.\n", + "# shape = (\n", + "# batch_size * self.target_sources_num * self.input_channels,\n", + "# 1,\n", + "# time_steps,\n", + "# freq_bins,\n", + "# )\n", + "# out_real = out_real.reshape(shape)\n", + "# out_imag = out_imag.reshape(shape)\n", + "\n", + "# # ISTFT.\n", + "# x = self.istft(out_real, out_imag, audio_length)\n", + "# # (batch_size * target_sources_num * input_channels, segments_num)\n", + "\n", + "# # Reshape.\n", + "# waveform = x.reshape(\n", + "# batch_size, self.target_sources_num * self.input_channels, audio_length\n", + "# )\n", + "# # (batch_size, target_sources_num * input_channels, segments_num)\n", + "\n", + "# return waveform\n", + " \n", + "# def forward(self, x):\n", + " \n", + "# subband_x = x\n", + "\n", + "# mag, cos_in, sin_in = self.wav_to_spectrogram_phase(subband_x)\n", + "# # mag, cos_in, sin_in: (batch_size, input_channels * subbands_num, time_steps, freq_bins)\n", + " \n", + " \n", + " \n", + "# # Batch normalize on individual frequency bins.\n", + "# x = mag.transpose(1, 3)\n", + "# x = self.bn0(x)\n", + "# x = x.transpose(1, 3)\n", + "# # (batch_size, input_channels * subbands_num, time_steps, freq_bins)\n", + " \n", + "# # Pad spectrogram to be evenly divided by downsample ratio.\n", + "# origin_len = x.shape[2]\n", + "# pad_len = (\n", + "# int(np.ceil(x.shape[2] / self.downsample_ratio)) * self.downsample_ratio\n", + "# - origin_len\n", + "# )\n", + "# x = F.pad(x, pad=(0, 0, 0, pad_len))\n", + "# # x: (batch_size, input_channels * subbands_num, padded_time_steps, freq_bins)\n", + " \n", + "# # Let frequency bins be evenly divided by 2, e.g., 257 -> 256\n", + "# x = x[..., 0 : x.shape[-1] - 1] # (bs, input_channels, T, F)\n", + "# # x: (batch_size, input_channels * subbands_num, padded_time_steps, freq_bins)\n", + " \n", + "# # UNet\n", + "# print(\"x\", x.shape)\n", + "# (x1_pool, x1) = self.encoder_block1(x) # x1_pool: (bs, 32, T / 2, F / 2)\n", + "# # print(x1_pool.shape, x1.shape)\n", + "# (x2_pool, x2) = self.encoder_block2(x1_pool) # x2_pool: (bs, 64, T / 4, F / 4)\n", + "# # print(x2_pool.shape, x2.shape)\n", + "# (x3_pool, x3) = self.encoder_block3(x2_pool) # x3_pool: (bs, 128, T / 8, F / 8)\n", + "# # print(x3_pool.shape, x3.shape)\n", + "# # (x4_pool, x4) = self.encoder_block4(x3_pool) # x4_pool: (bs, 256, T / 16, F / 16)\n", + "# # (x5_pool, x5) = self.encoder_block5(x4_pool) # x5_pool: (bs, 384, T / 32, F / 32)\n", + "# # (x6_pool, x6) = self.encoder_block6(x5_pool) # x6_pool: (bs, 384, T / 32, F / 64)\n", + "# (x_center, _) = self.conv_block7a(x3_pool) # (bs, 384, T / 32, F / 64)\n", + "# (x_center, _) = self.conv_block7b(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # (x_center, _) = self.conv_block7c(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # (x_center, _) = self.conv_block7d(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # x7 = self.decoder_block1(x_center, x6) # (bs, 384, T / 32, F / 32)\n", + "# # x8 = self.decoder_block2(x7, x5) # (bs, 384, T / 16, F / 16)\n", + "# # x9 = self.decoder_block3(x8, x4) # (bs, 256, T / 8, F / 8)\n", + "# # print(\"x_center.shape, x3.shape\", x_center.shape, x3.shape)\n", + "# x10 = self.decoder_block4(x_center, x3) # (bs, 128, T / 4, F / 4)\n", + "# x11 = self.decoder_block5(x10, x2) # (bs, 64, T / 2, F / 2)\n", + "# x12 = self.decoder_block6(x11, x1) # (bs, 32, T, F)\n", + "# print(\"x12\", x12.shape)\n", + " \n", + "# (x, _) = self.after_conv_block1(x12) # (bs, 32, T, F)\n", + " \n", + "# x = self.after_conv2(x)\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles * self.K, T, F')\n", + "# # print(33, \"x.shape\", x.shape)\n", + "\n", + "# # Recover shape\n", + "# x = F.pad(x, pad=(0, 1)) # Pad frequency, e.g., 256 -> 257.\n", + "\n", + "# x = x[:, :, 0:origin_len, :]\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles * self.K, T, F')\n", + "# print(99, x.shape)\n", + "# audio_length = subband_x.shape[2]\n", + " \n", + "# # Recover each subband spectrograms to subband waveforms. Then synthesis\n", + "# # the subband waveforms to a waveform.\n", + "# C1 = x.shape[1] // self.subbands_num\n", + "# C2 = mag.shape[1] // self.subbands_num\n", + "\n", + "# separated_subband_audio = torch.cat(\n", + "# [\n", + "# self.feature_maps_to_wav(\n", + "# input_tensor=x[:, j * C1 : (j + 1) * C1, :, :],\n", + "# sp=mag[:, j * C2 : (j + 1) * C2, :, :],\n", + "# sin_in=sin_in[:, j * C2 : (j + 1) * C2, :, :],\n", + "# cos_in=cos_in[:, j * C2 : (j + 1) * C2, :, :],\n", + "# audio_length=audio_length,\n", + "# )\n", + "# for j in range(self.subbands_num)\n", + "# ],\n", + "# dim=1,\n", + "# )\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles, segment_samples)\n", + " \n", + "# separated_subband_audio = torch.unsqueeze(separated_subband_audio, 2)\n", + "# (y, _) = self.out_conv_block(separated_subband_audio)\n", + " \n", + "# y = torch.squeeze(y, 2)\n", + " \n", + "# return y\n", + " \n", + " \n", + " \n", + "# tmp_x = torch.rand(3, 9, 1000)\n", + "# # \n", + "# # input_dict = {\n", + "# # \"waveform\": tmp_x\n", + "# # }\n", + "\n", + "# tmp_layer = ResUNet143_Subbandtime(input_channels=9, target_sources_num=10)\n", + "# tmp_y = tmp_layer(tmp_x)\n", + "# tmp_y.shape\n", + "\n", + "# # torch.Size([3, 9, 10, 33]) torch.Size([3, 9, 10, 33]) torch.Size([3, 9, 10, 33])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def init_layer(layer: nn.Module):\n", + " r\"\"\"Initialize a Linear or Convolutional layer.\"\"\"\n", + " nn.init.xavier_uniform_(layer.weight)\n", + "\n", + " if hasattr(layer, \"bias\"):\n", + " if layer.bias is not None:\n", + " layer.bias.data.fill_(0.0)\n", + " \n", + "def init_bn(bn: nn.Module):\n", + " r\"\"\"Initialize a Batchnorm layer.\"\"\"\n", + " bn.bias.data.fill_(0.0)\n", + " bn.weight.data.fill_(1.0)\n", + " bn.running_mean.data.fill_(0.0)\n", + " bn.running_var.data.fill_(1.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# -*- coding: utf-8 -*-\n", + "\"\"\"Common 2D convolutions\n", + "\"\"\"\n", + "\n", + "import math\n", + "import torch\n", + "import torch.nn as nn\n", + "from torch import Tensor\n", + "from torch.nn.utils import weight_norm\n", + "import torch.nn.functional as F\n", + "from typing import Tuple, List\n", + "\n", + "from splearn.nn.modules.functional import Swish\n", + "from splearn.nn.utils import get_class_name\n", + "\n", + "\n", + "class Conv2d(nn.Module):\n", + " \"\"\"\n", + " Input: 4-dim tensor\n", + " Shape [batch, in_channels, H, W]\n", + " Return: 4-dim tensor\n", + " Shape [batch, out_channels, H, W]\n", + " \n", + " Args:\n", + " in_channels : int\n", + " Should match input `channel`\n", + " out_channels : int\n", + " Return tensor with `out_channels`\n", + " kernel_size : int or 2-dim tuple\n", + " stride : int or 2-dim tuple, default: 1\n", + " padding : int or 2-dim tuple or True\n", + " Apply `padding` if given int or 2-dim tuple. Perform TensorFlow-like 'SAME' padding if True\n", + " dilation : int or 2-dim tuple, default: 1\n", + " groups : int or 2-dim tuple, default: 1\n", + " w_in: int, optional\n", + " The size of `W` axis. If given, `w_out` is available.\n", + " \n", + " Usage:\n", + " x = torch.randn(1, 22, 1, 256)\n", + " conv1 = Conv2dSamePadding(22, 64, kernel_size=17, padding=True, w_in=256)\n", + " y = conv1(x)\n", + " \"\"\"\n", + " def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=\"SAME\", dilation=1, groups=1, w_in=None, bias=True):\n", + " super().__init__()\n", + " \n", + " padding = padding\n", + " self.kernel_size = kernel_size = kernel_size\n", + " self.stride = stride = stride\n", + " self.dilation = dilation = dilation\n", + " \n", + " self.padding_same = False\n", + " if padding == \"SAME\":\n", + " self.padding_same = True\n", + " padding = (0,0)\n", + " \n", + " if isinstance(padding, int):\n", + " padding = (padding, padding)\n", + " \n", + " if isinstance(kernel_size, int):\n", + " self.kernel_size = kernel_size = (kernel_size, kernel_size)\n", + " \n", + " if isinstance(stride, int):\n", + " self.stride = stride = (stride, stride)\n", + " \n", + " if isinstance(dilation, int):\n", + " self.dilation = dilation = (dilation, dilation)\n", + " \n", + " self.conv = nn.Conv2d(\n", + " in_channels, \n", + " out_channels, \n", + " kernel_size=kernel_size, \n", + " stride=stride, \n", + " padding=0 if padding==True else padding, \n", + " dilation=dilation, \n", + " groups=groups,\n", + " bias=bias\n", + " )\n", + " \n", + " self.weight = self.conv.weight\n", + " \n", + " if w_in is not None:\n", + " self.w_out = int( ((w_in + 2 * padding[1] - dilation[1] * (kernel_size[1]-1)-1) / 1) + 1 )\n", + " if self.padding_same == \"SAME\": # if SAME, then replace, w_out = w_in, obviously\n", + " self.w_out = w_in\n", + " \n", + " def forward(self, x):\n", + " if self.padding_same == True:\n", + " x = self.pad_same(x, self.kernel_size, self.stride, self.dilation)\n", + " return self.conv(x)\n", + " \n", + " # Calculate asymmetric TensorFlow-like 'SAME' padding for a convolution\n", + " def get_same_padding(self, x: int, k: int, s: int, d: int):\n", + " return max((math.ceil(x / s) - 1) * s + (k - 1) * d + 1 - x, 0)\n", + "\n", + " # Dynamically pad input x with 'SAME' padding for conv with specified args\n", + " def pad_same(self, x, k: List[int], s: List[int], d: List[int] = (1, 1), value: float = 0):\n", + " ih, iw = x.size()[-2:]\n", + " pad_h, pad_w = self.get_same_padding(ih, k[0], s[0], d[0]), self.get_same_padding(iw, k[1], s[1], d[1])\n", + " if pad_h > 0 or pad_w > 0:\n", + " x = F.pad(x, [pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2], value=value)\n", + " return x\n", + " \n", + "######\n", + "\n", + "class ConvBlockRes(nn.Module):\n", + " def __init__(self, in_channels, out_channels, kernel_size, activation, momentum):\n", + " r\"\"\"Residual block.\"\"\"\n", + " super(ConvBlockRes, self).__init__()\n", + "\n", + " self.activation = activation\n", + " \n", + " padding = [kernel_size[0] // 2, kernel_size[1] // 2]\n", + "\n", + " self.bn1 = nn.BatchNorm2d(in_channels, momentum=momentum)\n", + " # self.bn2 = nn.BatchNorm2d(out_channels, momentum=momentum)\n", + "\n", + " self.conv1 = nn.Conv2d(\n", + " in_channels=in_channels,\n", + " out_channels=out_channels,\n", + " kernel_size=kernel_size,\n", + " stride=(1, 1),\n", + " dilation=(1, 1),\n", + " padding=padding,\n", + " bias=False,\n", + " )\n", + " \n", + " # self.conv2 = nn.Conv2d(\n", + " # in_channels=out_channels,\n", + " # out_channels=out_channels,\n", + " # kernel_size=kernel_size,\n", + " # stride=(1, 1),\n", + " # dilation=(1, 1),\n", + " # padding=padding,\n", + " # bias=False,\n", + " # )\n", + "\n", + " if in_channels != out_channels:\n", + " self.shortcut = nn.Conv2d(\n", + " in_channels=in_channels,\n", + " out_channels=out_channels,\n", + " kernel_size=(1, 1),\n", + " stride=(1, 1),\n", + " padding=(0, 0),\n", + " )\n", + " self.is_shortcut = True\n", + " else:\n", + " self.is_shortcut = False\n", + "\n", + " self.init_weights()\n", + "\n", + " def init_weights(self):\n", + " init_bn(self.bn1)\n", + " # init_bn(self.bn2)\n", + " init_layer(self.conv1)\n", + " # init_layer(self.conv2)\n", + "\n", + " if self.is_shortcut:\n", + " init_layer(self.shortcut)\n", + "\n", + " def forward(self, x):\n", + " origin = x\n", + " x = self.conv1(F.leaky_relu_(self.bn1(x), negative_slope=0.01))\n", + " # x = self.conv2(F.leaky_relu_(self.bn2(x), negative_slope=0.01))\n", + "\n", + " if self.is_shortcut:\n", + " x1 = self.shortcut(origin) \n", + " return x1 + x\n", + " else:\n", + " return origin + x\n", + " \n", + " \n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "\n", + "\n", + "# activation = \"leaky_relu\"\n", + "# momentum = 0.01\n", + "# tmp_layer = ConvBlockRes(in_channels=9, out_channels=9, kernel_size=(3,3), activation=activation, momentum=momentum)\n", + "# tmp_x = torch.rand(3, 9, 240, 240)\n", + "# tmp_y = tmp_layer(tmp_x)\n", + "# tmp_y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List#, NoReturn\n", + "\n", + "\n", + "class Base:\n", + " def __init__(self):\n", + " r\"\"\"Base function for extracting spectrogram, cos, and sin, etc.\"\"\"\n", + " pass\n", + "\n", + " def spectrogram(self, input: torch.Tensor, eps: float = 0.0) -> torch.Tensor:\n", + " r\"\"\"Calculate spectrogram.\n", + " Args:\n", + " input: (batch_size, segments_num)\n", + " eps: float\n", + " Returns:\n", + " spectrogram: (batch_size, time_steps, freq_bins)\n", + " \"\"\"\n", + " (real, imag) = self.stft(input)\n", + " return torch.clamp(real ** 2 + imag ** 2, eps, np.inf) ** 0.5\n", + "\n", + " def spectrogram_phase(\n", + " self, input: torch.Tensor, eps: float = 0.0\n", + " ) -> List[torch.Tensor]:\n", + " r\"\"\"Calculate the magnitude, cos, and sin of the STFT of input.\n", + " Args:\n", + " input: (batch_size, segments_num)\n", + " eps: float\n", + " Returns:\n", + " mag: (batch_size, time_steps, freq_bins)\n", + " cos: (batch_size, time_steps, freq_bins)\n", + " sin: (batch_size, time_steps, freq_bins)\n", + " \"\"\"\n", + " (real, imag) = self.stft(input)\n", + " mag = torch.clamp(real ** 2 + imag ** 2, eps, np.inf) ** 0.5\n", + " cos = real / mag\n", + " sin = imag / mag\n", + " return mag, cos, sin\n", + "\n", + " def wav_to_spectrogram_phase(\n", + " self, input: torch.Tensor, eps: float = 1e-10\n", + " ) -> List[torch.Tensor]:\n", + " r\"\"\"Convert waveforms to magnitude, cos, and sin of STFT.\n", + " Args:\n", + " input: (batch_size, channels_num, segment_samples)\n", + " eps: float\n", + " Outputs:\n", + " mag: (batch_size, channels_num, time_steps, freq_bins)\n", + " cos: (batch_size, channels_num, time_steps, freq_bins)\n", + " sin: (batch_size, channels_num, time_steps, freq_bins)\n", + " \"\"\"\n", + " batch_size, channels_num, segment_samples = input.shape\n", + "\n", + " # Reshape input with shapes of (n, segments_num) to meet the\n", + " # requirements of the stft function.\n", + " x = input.reshape(batch_size * channels_num, segment_samples)\n", + "\n", + " mag, cos, sin = self.spectrogram_phase(x, eps=eps)\n", + " # mag, cos, sin: (batch_size * channels_num, 1, time_steps, freq_bins)\n", + "\n", + " _, _, time_steps, freq_bins = mag.shape\n", + " mag = mag.reshape(batch_size, channels_num, time_steps, freq_bins)\n", + " cos = cos.reshape(batch_size, channels_num, time_steps, freq_bins)\n", + " sin = sin.reshape(batch_size, channels_num, time_steps, freq_bins)\n", + "\n", + " return mag, cos, sin\n", + "\n", + " def wav_to_spectrogram(\n", + " self, input: torch.Tensor, eps: float = 1e-10\n", + " ) -> List[torch.Tensor]:\n", + "\n", + " mag, cos, sin = self.wav_to_spectrogram_phase(input, eps)\n", + " return mag\n", + " \n", + " \n", + "class EncoderBlockRes4B(nn.Module):\n", + " def __init__(\n", + " self, in_channels, out_channels, kernel_size, downsample, activation, momentum, \n", + " ):\n", + " r\"\"\"Encoder block, contains 8 convolutional layers.\"\"\"\n", + " super(EncoderBlockRes4B, self).__init__()\n", + "\n", + " self.conv_block1 = ConvBlockRes(\n", + " in_channels, out_channels, kernel_size, activation, momentum,\n", + " )\n", + " self.conv_block2 = ConvBlockRes(\n", + " out_channels, out_channels, kernel_size, activation, momentum,\n", + " )\n", + " # self.conv_block3 = ConvBlockRes(\n", + " # out_channels, out_channels, kernel_size, activation, momentum\n", + " # )\n", + " # self.conv_block4 = ConvBlockRes(\n", + " # out_channels, out_channels, kernel_size, activation, momentum\n", + " # )\n", + " self.downsample = downsample\n", + "\n", + " def forward(self, x):\n", + " encoder = self.conv_block1(x)\n", + " encoder = self.conv_block2(encoder)\n", + " # encoder = self.conv_block3(encoder)\n", + " # encoder = self.conv_block4(encoder)\n", + " encoder_pool = F.avg_pool2d(encoder, kernel_size=self.downsample)\n", + " return encoder_pool, encoder\n", + " \n", + "class DecoderBlockRes4B(nn.Module):\n", + " def __init__(\n", + " self, in_channels, out_channels, kernel_size, upsample, activation, momentum\n", + " ):\n", + " r\"\"\"Decoder block, contains 1 transpose convolutional and 8 convolutional layers.\"\"\"\n", + " super(DecoderBlockRes4B, self).__init__()\n", + " self.kernel_size = kernel_size\n", + " self.stride = upsample\n", + " self.activation = activation\n", + "\n", + " self.conv1 = torch.nn.ConvTranspose2d(\n", + " in_channels=in_channels,\n", + " out_channels=out_channels,\n", + " kernel_size=self.stride,\n", + " stride=self.stride,\n", + " padding=(0, 0),\n", + " bias=False,\n", + " dilation=(1, 1),\n", + " )\n", + "\n", + " self.bn1 = nn.BatchNorm2d(in_channels, momentum=momentum)\n", + " self.conv_block2 = ConvBlockRes(\n", + " out_channels * 2, out_channels, kernel_size, activation, momentum\n", + " )\n", + " # self.conv_block3 = ConvBlockRes(\n", + " # out_channels, out_channels, kernel_size, activation, momentum\n", + " # )\n", + " # self.conv_block4 = ConvBlockRes(\n", + " # out_channels, out_channels, kernel_size, activation, momentum\n", + " # )\n", + " # self.conv_block5 = ConvBlockRes(\n", + " # out_channels, out_channels, kernel_size, activation, momentum\n", + " # )\n", + "\n", + " self.init_weights()\n", + "\n", + " def init_weights(self):\n", + " init_bn(self.bn1)\n", + " init_layer(self.conv1)\n", + "\n", + " def forward(self, input_tensor, concat_tensor):\n", + " x = self.conv1(F.relu_(self.bn1(input_tensor)))\n", + " x = torch.cat((x, concat_tensor), dim=1)\n", + " x = self.conv_block2(x)\n", + " # x = self.conv_block3(x)\n", + " # x = self.conv_block4(x)\n", + " # x = self.conv_block5(x)\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([3, 10])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + " \n", + "class MyModel(nn.Module, Base):\n", + " def __init__(self, input_channels, target_sources_num):\n", + " super(MyModel, self).__init__()\n", + " \n", + " self.input_channels = input_channels\n", + " self.target_sources_num = target_sources_num\n", + " \n", + " signal_length = 250\n", + "\n", + " window_size = 64\n", + " hop_size = 25\n", + " center = True\n", + " pad_mode = \"reflect\"\n", + " window = \"hann\"\n", + " activation = \"leaky_relu\"\n", + " momentum = 0.01\n", + "\n", + " self.subbands_num = 1 # 4\n", + " self.K = 4 # outputs: |M|, cos∠M, sin∠M, Q\n", + "\n", + " self.downsample_ratio = 2 ** 3 # 5 # This number equals 2^{#encoder_blcoks}\n", + "\n", + " self.stft = STFT(\n", + " n_fft=window_size,\n", + " hop_length=hop_size,\n", + " win_length=window_size,\n", + " window=window,\n", + " center=center,\n", + " pad_mode=pad_mode,\n", + " freeze_parameters=True,\n", + " )\n", + "\n", + " self.istft = ISTFT(\n", + " n_fft=window_size,\n", + " hop_length=hop_size,\n", + " win_length=window_size,\n", + " window=window,\n", + " center=center,\n", + " pad_mode=pad_mode,\n", + " freeze_parameters=True,\n", + " )\n", + " \n", + " self.bn0 = nn.BatchNorm2d(window_size // 2 + 1, momentum=momentum)\n", + " \n", + " self.encoder_block1 = EncoderBlockRes4B(\n", + " in_channels=input_channels,\n", + " out_channels=16,\n", + " kernel_size=(3, 3),\n", + " downsample=(2, 2),\n", + " activation=activation,\n", + " momentum=momentum,\n", + " )\n", + " \n", + " self.encoder_block2 = EncoderBlockRes4B(\n", + " in_channels=16,\n", + " out_channels=32,\n", + " kernel_size=(3, 3),\n", + " downsample=(2, 2),\n", + " activation=activation,\n", + " momentum=momentum,\n", + " )\n", + " \n", + " self.encoder_block3 = EncoderBlockRes4B(\n", + " in_channels=32,\n", + " out_channels=64,\n", + " kernel_size=(3, 3),\n", + " downsample=(2, 2),\n", + " activation=activation,\n", + " momentum=momentum,\n", + " )\n", + " self.encoder_block4 = EncoderBlockRes4B(\n", + " in_channels=64,\n", + " out_channels=128,\n", + " kernel_size=(3, 3),\n", + " downsample=(2, 2),\n", + " activation=activation,\n", + " momentum=momentum,\n", + " )\n", + "# self.encoder_block4 = EncoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=256,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block5 = EncoderBlockRes4B(\n", + "# in_channels=256,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.encoder_block6 = EncoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + " \n", + "# conv_block_in_channels = 128 # 384\n", + " \n", + "# self.conv_block7a = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7b = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7c = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.conv_block7d = EncoderBlockRes4B(\n", + "# in_channels=conv_block_in_channels,\n", + "# out_channels=conv_block_in_channels,\n", + "# kernel_size=(3, 3),\n", + "# downsample=(1, 1),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + " \n", + "# self.decoder_block1 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(1, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block2 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=384,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block3 = DecoderBlockRes4B(\n", + "# in_channels=384,\n", + "# out_channels=256,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block4 = DecoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=128,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block5 = DecoderBlockRes4B(\n", + "# in_channels=128,\n", + "# out_channels=64,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "# self.decoder_block6 = DecoderBlockRes4B(\n", + "# in_channels=64,\n", + "# out_channels=32,\n", + "# kernel_size=(3, 3),\n", + "# upsample=(2, 2),\n", + "# activation=activation,\n", + "# momentum=momentum,\n", + "# )\n", + "\n", + " self.after_conv_block1 = EncoderBlockRes4B(\n", + " in_channels=64,\n", + " out_channels=32,\n", + " kernel_size=(3, 3),\n", + " downsample=(1, 1),\n", + " activation=activation,\n", + " momentum=momentum,\n", + " )\n", + "\n", + " self.after_conv2 = nn.Conv2d(\n", + " in_channels=32,\n", + " out_channels=target_sources_num\n", + " * input_channels\n", + " * self.K\n", + " * self.subbands_num,\n", + " kernel_size=(1, 1),\n", + " stride=(1, 1),\n", + " padding=(0, 0),\n", + " bias=True,\n", + " )\n", + " \n", + " # self.out_conv_block = EncoderBlockRes4B(\n", + " # in_channels=target_sources_num\n", + " # * input_channels\n", + " # * self.subbands_num,\n", + " # out_channels=target_sources_num,\n", + " # kernel_size=(1, signal_length),\n", + " # downsample=(1, 1),\n", + " # activation=activation,\n", + " # momentum=momentum,\n", + " # padding=(0,0),\n", + " # shortcut=False\n", + " # )\n", + " \n", + " self.out_conv_block = Conv2d(\n", + " in_channels=target_sources_num\n", + " * input_channels\n", + " * self.subbands_num,\n", + " out_channels=target_sources_num,\n", + " kernel_size=(1, signal_length),\n", + " stride=(1, 1),\n", + " dilation=(1, 1),\n", + " padding=(0,0),\n", + " )\n", + "\n", + " self.init_weights()\n", + " \n", + " def init_weights(self):\n", + " init_bn(self.bn0)\n", + " init_layer(self.after_conv2)\n", + " \n", + " def feature_maps_to_wav(\n", + " self,\n", + " input_tensor: torch.Tensor,\n", + " sp: torch.Tensor,\n", + " sin_in: torch.Tensor,\n", + " cos_in: torch.Tensor,\n", + " audio_length: int,\n", + " ) -> torch.Tensor:\n", + " r\"\"\"Convert feature maps to waveform.\n", + " Args:\n", + " input_tensor: (batch_size, target_sources_num * input_channels * self.K, time_steps, freq_bins)\n", + " sp: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + " sin_in: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + " cos_in: (batch_size, target_sources_num * input_channels, time_steps, freq_bins)\n", + " Outputs:\n", + " waveform: (batch_size, target_sources_num * input_channels, segment_samples)\n", + " \"\"\"\n", + " batch_size, _, time_steps, freq_bins = input_tensor.shape\n", + "\n", + " x = input_tensor.reshape(\n", + " batch_size,\n", + " self.target_sources_num,\n", + " self.input_channels,\n", + " self.K,\n", + " time_steps,\n", + " freq_bins,\n", + " )\n", + " # x: (batch_size, target_sources_num, input_channles, K, time_steps, freq_bins)\n", + "\n", + " mask_mag = torch.sigmoid(x[:, :, :, 0, :, :])\n", + " _mask_real = torch.tanh(x[:, :, :, 1, :, :])\n", + " _mask_imag = torch.tanh(x[:, :, :, 2, :, :])\n", + " linear_mag = torch.tanh(x[:, :, :, 3, :, :])\n", + " _, mask_cos, mask_sin = magphase(_mask_real, _mask_imag)\n", + " # mask_cos, mask_sin: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + " # Y = |Y|cos∠Y + j|Y|sin∠Y\n", + " # = |Y|cos(∠X + ∠M) + j|Y|sin(∠X + ∠M)\n", + " # = |Y|(cos∠X cos∠M - sin∠X sin∠M) + j|Y|(sin∠X cos∠M + cos∠X sin∠M)\n", + " out_cos = (\n", + " cos_in[:, None, :, :, :] * mask_cos - sin_in[:, None, :, :, :] * mask_sin\n", + " )\n", + " out_sin = (\n", + " sin_in[:, None, :, :, :] * mask_cos + cos_in[:, None, :, :, :] * mask_sin\n", + " )\n", + " # out_cos: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + " # out_sin: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + " # Calculate |Y|.\n", + " out_mag = F.relu_(sp[:, None, :, :, :] * mask_mag + linear_mag)\n", + " # out_mag: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + " # Calculate Y_{real} and Y_{imag} for ISTFT.\n", + " out_real = out_mag * out_cos\n", + " out_imag = out_mag * out_sin\n", + " # out_real, out_imag: (batch_size, target_sources_num, input_channles, time_steps, freq_bins)\n", + "\n", + " # Reformat shape to (n, 1, time_steps, freq_bins) for ISTFT.\n", + " shape = (\n", + " batch_size * self.target_sources_num * self.input_channels,\n", + " 1,\n", + " time_steps,\n", + " freq_bins,\n", + " )\n", + " out_real = out_real.reshape(shape)\n", + " out_imag = out_imag.reshape(shape)\n", + "\n", + " # ISTFT.\n", + " x = self.istft(out_real, out_imag, audio_length)\n", + " # (batch_size * target_sources_num * input_channels, segments_num)\n", + "\n", + " # Reshape.\n", + " waveform = x.reshape(\n", + " batch_size, self.target_sources_num * self.input_channels, audio_length\n", + " )\n", + " # (batch_size, target_sources_num * input_channels, segments_num)\n", + "\n", + " return waveform\n", + " \n", + " def forward(self, x):\n", + " \n", + " subband_x = x\n", + "\n", + " mag, cos_in, sin_in = self.wav_to_spectrogram_phase(subband_x)\n", + " # mag, cos_in, sin_in: (batch_size, input_channels * subbands_num, time_steps, freq_bins)\n", + " \n", + " # Batch normalize on individual frequency bins.\n", + " x = mag.transpose(1, 3)\n", + " x = self.bn0(x)\n", + " x = x.transpose(1, 3)\n", + " # (batch_size, input_channels * subbands_num, time_steps, freq_bins)\n", + " # print(11, x.shape)\n", + " \n", + " # Pad spectrogram to be evenly divided by downsample ratio.\n", + " origin_len = x.shape[2]\n", + " # print(22, origin_len)\n", + " pad_len = (\n", + " int(np.ceil(x.shape[2] / self.downsample_ratio)) * self.downsample_ratio\n", + " - origin_len\n", + " )\n", + " x = F.pad(x, pad=(0, 0, 0, pad_len))\n", + " # print(33, x.shape)\n", + " # x: (batch_size, input_channels * subbands_num, padded_time_steps, freq_bins)\n", + " \n", + " # Let frequency bins be evenly divided by 2, e.g., 257 -> 256\n", + " x = x[..., 0 : x.shape[-1] - 1] # (bs, input_channels, T, F)\n", + " # x: (batch_size, input_channels * subbands_num, padded_time_steps, freq_bins)\n", + " # print(44, x.shape)\n", + " \n", + " (x1_pool, x1) = self.encoder_block1(x)\n", + " (x2_pool, x2) = self.encoder_block2(x1)\n", + " (x3_pool, x3) = self.encoder_block3(x2)\n", + " \n", + " \n", + " (x, _) = self.after_conv_block1(x3) # (bs, 32, T, F)\n", + " \n", + " x = self.after_conv2(x)\n", + " # (batch_size, subbands_num * target_sources_num * input_channles * self.K, T, F')\n", + " # # print(33, \"x.shape\", x.shape)\n", + "\n", + " # Recover shape\n", + " x = F.pad(x, pad=(0, 1)) # Pad frequency, e.g., 256 -> 257.\n", + "\n", + " x = x[:, :, 0:origin_len, :]\n", + " \n", + " # print(55, x.shape)\n", + " \n", + " audio_length = subband_x.shape[2]\n", + " \n", + " # Recover each subband spectrograms to subband waveforms. Then synthesis\n", + " # the subband waveforms to a waveform.\n", + " C1 = x.shape[1] // self.subbands_num\n", + " C2 = mag.shape[1] // self.subbands_num\n", + "\n", + " separated_subband_audio = torch.cat(\n", + " [\n", + " self.feature_maps_to_wav(\n", + " input_tensor=x[:, j * C1 : (j + 1) * C1, :, :],\n", + " sp=mag[:, j * C2 : (j + 1) * C2, :, :],\n", + " sin_in=sin_in[:, j * C2 : (j + 1) * C2, :, :],\n", + " cos_in=cos_in[:, j * C2 : (j + 1) * C2, :, :],\n", + " audio_length=audio_length,\n", + " )\n", + " for j in range(self.subbands_num)\n", + " ],\n", + " dim=1,\n", + " )\n", + " \n", + " \n", + " separated_subband_audio = torch.unsqueeze(separated_subband_audio, 2)\n", + " # print(66, separated_subband_audio.shape)\n", + " \n", + " y = self.out_conv_block(separated_subband_audio)\n", + " \n", + " y = torch.squeeze(y, 2)\n", + " y = torch.squeeze(y, 2)\n", + " \n", + " \n", + "# # UNet\n", + "# # print(\"x\", x.shape)\n", + "# (x1_pool, x1) = self.encoder_block1(x) # x1_pool: (bs, 32, T / 2, F / 2)\n", + "# # print(x1_pool.shape, x1.shape)\n", + "# (x2_pool, x2) = self.encoder_block2(x1_pool) # x2_pool: (bs, 64, T / 4, F / 4)\n", + "# # print(x2_pool.shape, x2.shape)\n", + "# (x3_pool, x3) = self.encoder_block3(x2_pool) # x3_pool: (bs, 128, T / 8, F / 8)\n", + "# # print(x3_pool.shape, x3.shape)\n", + "# # (x4_pool, x4) = self.encoder_block4(x3_pool) # x4_pool: (bs, 256, T / 16, F / 16)\n", + "# # (x5_pool, x5) = self.encoder_block5(x4_pool) # x5_pool: (bs, 384, T / 32, F / 32)\n", + "# # (x6_pool, x6) = self.encoder_block6(x5_pool) # x6_pool: (bs, 384, T / 32, F / 64)\n", + "# (x_center, _) = self.conv_block7a(x3_pool) # (bs, 384, T / 32, F / 64)\n", + "# (x_center, _) = self.conv_block7b(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # (x_center, _) = self.conv_block7c(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # (x_center, _) = self.conv_block7d(x_center) # (bs, 384, T / 32, F / 64)\n", + "# # x7 = self.decoder_block1(x_center, x6) # (bs, 384, T / 32, F / 32)\n", + "# # x8 = self.decoder_block2(x7, x5) # (bs, 384, T / 16, F / 16)\n", + "# # x9 = self.decoder_block3(x8, x4) # (bs, 256, T / 8, F / 8)\n", + "# # print(\"x_center.shape, x3.shape\", x_center.shape, x3.shape)\n", + "# x10 = self.decoder_block4(x_center, x3) # (bs, 128, T / 4, F / 4)\n", + "# x11 = self.decoder_block5(x10, x2) # (bs, 64, T / 2, F / 2)\n", + "# x12 = self.decoder_block6(x11, x1) # (bs, 32, T, F)\n", + " \n", + "# (x, _) = self.after_conv_block1(x12) # (bs, 32, T, F)\n", + " \n", + "# x = self.after_conv2(x)\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles * self.K, T, F')\n", + "# # print(33, \"x.shape\", x.shape)\n", + "\n", + "# # Recover shape\n", + "# x = F.pad(x, pad=(0, 1)) # Pad frequency, e.g., 256 -> 257.\n", + "\n", + "# x = x[:, :, 0:origin_len, :]\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles * self.K, T, F')\n", + "\n", + "# audio_length = subband_x.shape[2]\n", + " \n", + "# # Recover each subband spectrograms to subband waveforms. Then synthesis\n", + "# # the subband waveforms to a waveform.\n", + "# C1 = x.shape[1] // self.subbands_num\n", + "# C2 = mag.shape[1] // self.subbands_num\n", + "\n", + "# separated_subband_audio = torch.cat(\n", + "# [\n", + "# self.feature_maps_to_wav(\n", + "# input_tensor=x[:, j * C1 : (j + 1) * C1, :, :],\n", + "# sp=mag[:, j * C2 : (j + 1) * C2, :, :],\n", + "# sin_in=sin_in[:, j * C2 : (j + 1) * C2, :, :],\n", + "# cos_in=cos_in[:, j * C2 : (j + 1) * C2, :, :],\n", + "# audio_length=audio_length,\n", + "# )\n", + "# for j in range(self.subbands_num)\n", + "# ],\n", + "# dim=1,\n", + "# )\n", + "# # (batch_size, subbands_num * target_sources_num * input_channles, segment_samples)\n", + " \n", + "# separated_subband_audio = torch.unsqueeze(separated_subband_audio, 2)\n", + "# (y, _) = self.out_conv_block(separated_subband_audio)\n", + " \n", + "# y = torch.squeeze(y, 2)\n", + " \n", + " return y\n", + " \n", + " \n", + " \n", + "tmp_x = torch.rand(3, 9, 250)\n", + "# \n", + "# input_dict = {\n", + "# \"waveform\": tmp_x\n", + "# }\n", + "\n", + "tmp_layer = MyModel(input_channels=9, target_sources_num=10)\n", + "tmp_y = tmp_layer(tmp_x)\n", + "tmp_y.shape\n", + "\n", + "# 99 torch.Size([3, 360, 41, 33])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# import torch.nn as nn\n", + "# import torch.optim as optim\n", + "# train_acc = torchmetrics.Accuracy()\n", + "\n", + "# model = nn.Linear(100, 1) # predict logits for 5 classes\n", + "# x = torch.randn(1, 10, 100)\n", + "# y = torch.randint(0, 2, (1, 10, 1)).double()\n", + "# print(y.shape, y)\n", + "\n", + "# criterion = nn.BCEWithLogitsLoss()\n", + "# optimizer = optim.SGD(model.parameters(), lr=1e-1)\n", + "\n", + "# for epoch in range(20):\n", + "# optimizer.zero_grad()\n", + "# output = model(x)\n", + "# print(\"output.shape, y.shape\", output.shape, y.shape)\n", + "# loss = criterion(output, y)\n", + "# loss.backward()\n", + "# optimizer.step()\n", + "# acc = train_acc(output, y.long())\n", + "# print('Loss: {:.3f}, Acc: {:.3f} '.format(loss.item(), acc.item()))\n", + "\n", + "\n", + "# import torch.nn as nn\n", + "# import torch.optim as optim\n", + "# train_acc = torchmetrics.Accuracy()\n", + "\n", + "# model = nn.Conv1d(10, 10, kernel_size=1000, groups=10)\n", + "# x = torch.randn(3, 10, 1000)\n", + "# y = torch.randint(0, 3, (3,))\n", + "# print(y.shape, y)\n", + "\n", + "# criterion = nn.CrossEntropyLoss()\n", + "# optimizer = optim.SGD(model.parameters(), lr=1e-1)\n", + "\n", + "# for epoch in range(20):\n", + "# optimizer.zero_grad()\n", + "# output = model(x)\n", + "# output = torch.squeeze(output)\n", + "# print(\"output.shape, y.shape\", output.shape, y.shape)\n", + "# loss = criterion(output, y)\n", + "# loss.backward()\n", + "# optimizer.step()\n", + "# acc = train_acc(output, y.long())\n", + "# print('Loss: {:.3f}, Acc: {:.3f} '.format(loss.item(), acc.item()))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import torchmetrics\n", + "from splearn.nn.base import LightningModel\n", + "\n", + "\n", + "class LightningModelClassifier(LightningModel):\n", + " def __init__(\n", + " self,\n", + " optimizer=\"adamw\",\n", + " scheduler=\"cosine_with_warmup\",\n", + " optimizer_learning_rate: float=1e-3,\n", + " optimizer_epsilon: float=1e-6,\n", + " optimizer_weight_decay: float=0.0005,\n", + " scheduler_warmup_epochs: int=10,\n", + " criterion=None\n", + " ):\n", + " super().__init__()\n", + " self.save_hyperparameters()\n", + " \n", + " self.train_acc = torchmetrics.Accuracy()\n", + " self.valid_acc = torchmetrics.Accuracy()\n", + " self.test_acc = torchmetrics.Accuracy()\n", + " \n", + " self.criterion_classifier = criterion\n", + " if self.criterion_classifier is None:\n", + " self.criterion_classifier = nn.CrossEntropyLoss()\n", + " \n", + " def build_model(self, model):\n", + " self.model = model\n", + "\n", + " def forward(self, x):\n", + " y_hat = self.model(x)\n", + " return y_hat\n", + " \n", + " def step(self, batch, batch_idx):\n", + " x, y = batch\n", + " y_hat = self.forward(x)\n", + " loss = self.criterion_classifier(y_hat, y)\n", + " return y_hat, y, loss\n", + "\n", + " def training_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.step(batch, batch_idx)\n", + " acc = self.train_acc(y_hat, y)\n", + " self.log('train_loss', loss, on_step=True)\n", + " return loss\n", + "\n", + " def validation_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.step(batch, batch_idx)\n", + " acc = self.valid_acc(y_hat, y)\n", + " self.log('valid_loss', loss, on_step=True)\n", + " return loss\n", + "\n", + " def test_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.step(batch, batch_idx)\n", + " acc = self.test_acc(y_hat, y)\n", + " self.log('test_loss', loss)\n", + " return loss\n", + " \n", + " def training_epoch_end(self, outs):\n", + " self.log('train_acc_epoch', self.train_acc.compute())\n", + " \n", + " def validation_epoch_end(self, outs):\n", + " self.log('valid_acc_epoch', self.valid_acc.compute())\n", + " \n", + " def test_epoch_end(self, outs):\n", + " self.log('test_acc_epoch', self.test_acc.compute())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from splearn.nn.base import LightningModelClassifier\n", + "\n", + "\n", + "class MultilabelLClassifier(LightningModelClassifier):\n", + " def __init__(\n", + " self,\n", + " optimizer=\"adamw\",\n", + " scheduler=\"cosine_with_warmup\",\n", + " optimizer_learning_rate: float=1e-3,\n", + " optimizer_epsilon: float=1e-6,\n", + " optimizer_weight_decay: float=0.0005,\n", + " scheduler_warmup_epochs: int=10,\n", + " ):\n", + " super().__init__()\n", + " self.save_hyperparameters()\n", + " self.criterion_classifier = nn.CrossEntropyLoss() # nn.BCEWithLogitsLoss()\n", + " \n", + " def build_model(self, model, model_output_dim, num_classes, **kwargs):\n", + " self.model = model\n", + " # self.classifier = nn.Linear(model_output_dim*num_classes, num_classes)\n", + " # self.classifier = nn.Conv1d(num_classes, num_classes, kernel_size=model_output_dim, groups=num_classes)\n", + "\n", + " def forward(self, x):\n", + " x = self.model(x)\n", + " # x = torch.flatten(x, 1)\n", + " # y_hat = self.classifier(x)\n", + " # y_hat = torch.squeeze(y_hat, 2)\n", + " return x\n", + " \n", + " def train_val_step(self, batch, batch_idx):\n", + " x, y = batch\n", + " # y = torch.unsqueeze(y, 2).double()\n", + " y_hat = self.forward(x)\n", + " # y_hat = torch.sigmoid(y_hat)\n", + " loss = self.criterion_classifier(y_hat, y.long())\n", + " # loss = F.cross_entropy(y_hat, y)\n", + " return y_hat, y, loss\n", + "\n", + " def training_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.train_val_step(batch, batch_idx)\n", + " acc = self.train_acc(y_hat, y.long())\n", + " self.log('train_loss', loss, on_step=True)\n", + " return loss\n", + "\n", + " def validation_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.train_val_step(batch, batch_idx)\n", + " acc = self.valid_acc(y_hat, y.long())\n", + " self.log('valid_loss', loss, on_step=True)\n", + " return loss\n", + "\n", + " def test_step(self, batch, batch_idx):\n", + " y_hat, y, loss = self.train_val_step(batch, batch_idx)\n", + " acc = self.test_acc(y_hat, y.long())\n", + " self.log('test_loss', loss)\n", + " return loss\n", + "\n", + " \n", + "# x = torch.rand(3, 9, config.data.sample_length)\n", + "# y = torch.randint(0, 2, (3,))\n", + "\n", + "# unet = ResUNet143_Subbandtime(input_channels=config.data.input_channels, target_sources_num=config.data.target_sources_num)\n", + "# model = MultilabelLClassifier(\n", + "# optimizer=config.model.optimizer,\n", + "# scheduler=config.model.scheduler,\n", + "# optimizer_learning_rate=config.training.learning_rate,\n", + "# scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + "# )\n", + "# model.build_model(model=unet, model_output_dim=config.data.sample_length, num_classes=config.data.num_classes)\n", + "\n", + "# tmp_y = model(x)\n", + "# print(\"tmp_y\", tmp_y.shape)\n", + "# print(tmp_y)\n", + "\n", + "# criterion = torch.nn.CrossEntropyLoss()\n", + "# optimizer = torch.optim.SGD(model.parameters(), lr=1e-1)\n", + "\n", + "# for epoch in range(10):\n", + "# optimizer.zero_grad()\n", + "# output = model(x)\n", + "# loss = criterion(output, y)\n", + "# loss.backward()\n", + "# optimizer.step()\n", + "# print('Loss: {:.3f}'.format(loss.item()))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# # x = torch.rand(3, 9, 1000)\n", + "# # y = torch.randint(0, 2, (3,)).double()\n", + "\n", + "# # unet = ResUNet143_Subbandtime(input_channels=config.data.input_channels, target_sources_num=config.data.target_sources_num)\n", + "# # model = MultilabelLClassifier(\n", + "# # optimizer=config.model.optimizer,\n", + "# # scheduler=config.model.scheduler,\n", + "# # optimizer_learning_rate=config.training.learning_rate,\n", + "# # scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + "# # )\n", + "# # model.build_model(model=unet, model_output_dim=config.data.sample_length)\n", + "\n", + "# model = nn.Linear(32,5)\n", + "# x = torch.rand(3, 32)\n", + "# y = torch.randint(0, 2, (3,))\n", + "# print(y)\n", + "\n", + "# tmp_y = model(x)\n", + "# print(\"tmp_y\", tmp_y.shape)\n", + "# print(tmp_y)\n", + "\n", + "# criterion = nn.CrossEntropyLoss() # torch.nn.BCEWithLogitsLoss()\n", + "# optimizer = torch.optim.SGD(model.parameters(), lr=1e-1)\n", + "\n", + "# for epoch in range(20):\n", + "# optimizer.zero_grad()\n", + "# output = model(x)\n", + "# loss = criterion(output, y)\n", + "# # loss = F.cross_entropy(output, y)\n", + "# loss.backward()\n", + "# optimizer.step()\n", + "# print('Loss: {:.3f}'.format(loss.item()))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------------------------------------\n", + "DATALOADER:0 TEST RESULTS\n", + "{'test_acc_epoch': 0.1875, 'test_loss': 16.77286720275879}\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "data": { + "text/plain": [ + "0.1875" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_subject_id = 33\n", + "kfold_k = 0\n", + "\n", + "## init data\n", + "train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + "train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + "val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + "## init model\n", + "unet = MyModel(input_channels=config.data.input_channels, target_sources_num=config.data.target_sources_num)\n", + "model = MultilabelLClassifier(\n", + " optimizer=config.model.optimizer,\n", + " scheduler=config.model.scheduler,\n", + " optimizer_learning_rate=config.training.learning_rate,\n", + " scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + ")\n", + "model.build_model(model=unet, model_output_dim=config.data.sample_length, num_classes=config.data.num_classes)\n", + "\n", + "## init training\n", + "sub_dir = \"sub\"+ str(test_subject_id) +\"_k\"+ str(kfold_k)\n", + "logger_tb = TensorBoardLogger(save_dir=\"tensorboard_logs\", name=config.run_name, sub_dir=sub_dir)\n", + "lr_monitor = LearningRateMonitor(logging_interval='epoch')\n", + "\n", + "trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor])\n", + "trainer.fit(model, train_loader, val_loader)\n", + "\n", + "## test\n", + "\n", + "result = trainer.test(dataloaders=test_loader, verbose=True)\n", + "test_acc = result[0]['test_acc_epoch']\n", + "test_acc\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.1875" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_acc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# test_subject_id = 33\n", + "# kfold_k = 0\n", + "\n", + "# train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + "# train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + "# val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "# test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + "# print(\"train_loader\", train_loader.dataset.data.shape, train_loader.dataset.targets.shape)\n", + "# print(\"val_loader\", val_loader.dataset.data.shape, val_loader.dataset.targets.shape)\n", + "# print(\"test_loader\", test_loader.dataset.data.shape, test_loader.dataset.targets.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# tmp_acc = torchmetrics.Accuracy()\n", + "\n", + "# # index = 0:2\n", + "\n", + "# x = torch.tensor(train_loader.dataset.data[0:10])\n", + "# y = torch.tensor(train_loader.dataset.targets[0:10])\n", + "# # x = torch.unsqueeze(x, 0)\n", + "# # y = torch.unsqueeze(y, 0)\n", + "# # y = torch.unsqueeze(y, 2)\n", + "# print(x.shape, y.shape)\n", + "# y_hat = model(x)\n", + "# y_hat = torch.sigmoid(y_hat)\n", + "\n", + "# acc = tmp_acc(y_hat, y.long())\n", + "# print(\"acc\", acc)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# # trial = pred_y[0]\n", + "# # for i in trial:\n", + "# # print(i)\n", + "\n", + "# N = 2\n", + "# C = 40\n", + "\n", + "# outputs = torch.squeeze(y_hat)\n", + "# labels = torch.squeeze(y)\n", + "\n", + "# outputs = torch.sigmoid(outputs) # torch.Size([N, C]) e.g. tensor([[0., 0.5, 0.]])\n", + "# outputs[outputs < 0.5] = 0\n", + "# outputs[outputs >= 0.5] = 1\n", + "# accuracy = (outputs == labels).sum()/(N*C)*100\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# outputs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/experiments/ssl_classifier/deep4net.py b/experiments/ssl_classifier/deep4net.py new file mode 100644 index 0000000..4def57c --- /dev/null +++ b/experiments/ssl_classifier/deep4net.py @@ -0,0 +1,449 @@ +import torch +import numpy as np +from torch import nn +from torch.nn import init +from torch.nn.functional import elu + + +def np_to_th( + X, requires_grad=False, dtype=None, pin_memory=False, **tensor_kwargs +): + """ + Convenience function to transform numpy array to `torch.Tensor`. + Converts `X` to ndarray using asarray if necessary. + Parameters + ---------- + X: ndarray or list or number + Input arrays + requires_grad: bool + passed on to Variable constructor + dtype: numpy dtype, optional + var_kwargs: + passed on to Variable constructor + Returns + ------- + var: `torch.Tensor` + """ + if not hasattr(X, "__len__"): + X = [X] + X = np.asarray(X) + if dtype is not None: + X = X.astype(dtype) + X_tensor = torch.tensor(X, requires_grad=requires_grad, **tensor_kwargs) + if pin_memory: + X_tensor = X_tensor.pin_memory() + return X_tensor + +def identity(x): + return x + +def transpose_time_to_spat(x): + """Swap time and spatial dimensions. + Returns + ------- + x: torch.Tensor + tensor in which last and first dimensions are swapped + """ + return x.permute(0, 3, 2, 1) + +def squeeze_final_output(x): + """Removes empty dimension at end and potentially removes empty time + dimension. It does not just use squeeze as we never want to remove + first dimension. + Returns + ------- + x: torch.Tensor + squeezed tensor + """ + + assert x.size()[3] == 1 + x = x[:, :, :, 0] + if x.size()[2] == 1: + x = x[:, :, 0] + return x + + +class Expression(nn.Module): + """Compute given expression on forward pass. + Parameters + ---------- + expression_fn : callable + Should accept variable number of objects of type + `torch.autograd.Variable` to compute its output. + """ + + def __init__(self, expression_fn): + super(Expression, self).__init__() + self.expression_fn = expression_fn + + def forward(self, *x): + return self.expression_fn(*x) + + def __repr__(self): + if hasattr(self.expression_fn, "func") and hasattr( + self.expression_fn, "kwargs" + ): + expression_str = "{:s} {:s}".format( + self.expression_fn.func.__name__, str(self.expression_fn.kwargs) + ) + elif hasattr(self.expression_fn, "__name__"): + expression_str = self.expression_fn.__name__ + else: + expression_str = repr(self.expression_fn) + return ( + self.__class__.__name__ + + "(expression=%s) " % expression_str + ) + + +class AvgPool2dWithConv(nn.Module): + """ + Compute average pooling using a convolution, to have the dilation parameter. + Parameters + ---------- + kernel_size: (int,int) + Size of the pooling region. + stride: (int,int) + Stride of the pooling operation. + dilation: int or (int,int) + Dilation applied to the pooling filter. + padding: int or (int,int) + Padding applied before the pooling operation. + """ + + def __init__(self, kernel_size, stride, dilation=1, padding=0): + super(AvgPool2dWithConv, self).__init__() + self.kernel_size = kernel_size + self.stride = stride + self.dilation = dilation + self.padding = padding + # don't name them "weights" to + # make sure these are not accidentally used by some procedure + # that initializes parameters or something + self._pool_weights = None + + def forward(self, x): + # Create weights for the convolution on demand: + # size or type of x changed... + in_channels = x.size()[1] + weight_shape = ( + in_channels, + 1, + self.kernel_size[0], + self.kernel_size[1], + ) + if self._pool_weights is None or ( + (tuple(self._pool_weights.size()) != tuple(weight_shape)) or + (self._pool_weights.is_cuda != x.is_cuda) or + (self._pool_weights.data.type() != x.data.type()) + ): + n_pool = np.prod(self.kernel_size) + weights = np_to_th( + np.ones(weight_shape, dtype=np.float32) / float(n_pool) + ) + weights = weights.type_as(x) + if x.is_cuda: + weights = weights.cuda() + self._pool_weights = weights + + pooled = F.conv2d( + x, + self._pool_weights, + bias=None, + stride=self.stride, + dilation=self.dilation, + padding=self.padding, + groups=in_channels, + ) + return pooled + +class Ensure4d(nn.Module): + def forward(self, x): + while(len(x.shape) < 4): + x = x.unsqueeze(-1) + return + + + +class Deep4Net(nn.Sequential): + """Deep ConvNet model from Schirrmeister et al 2017. + Model described in [Schirrmeister2017]_. + Parameters + ---------- + in_chans : int + XXX + References + ---------- + .. [Schirrmeister2017] Schirrmeister, R. T., Springenberg, J. T., Fiederer, + L. D. J., Glasstetter, M., Eggensperger, K., Tangermann, M., Hutter, F. + & Ball, T. (2017). + Deep learning with convolutional neural networks for EEG decoding and + visualization. + Human Brain Mapping , Aug. 2017. + Online: http://dx.doi.org/10.1002/hbm.23730 + """ + + def __init__( + self, + in_chans, + n_classes, + input_window_samples, + final_conv_length="auto", + n_filters_time=25, + n_filters_spat=25, + filter_time_length=10, + pool_time_length=1, + pool_time_stride=1, + n_filters_2=50, + filter_length_2=10, + n_filters_3=100, + filter_length_3=10, + n_filters_4=200, + filter_length_4=10, + first_nonlin=elu, + first_pool_mode="max", + first_pool_nonlin=identity, + later_nonlin=elu, + later_pool_mode="max", + later_pool_nonlin=identity, + drop_prob=0.5, + double_time_convs=False, + split_first_layer=True, + batch_norm=True, + batch_norm_alpha=0.1, + stride_before_pool=False, + ): + super().__init__() + if final_conv_length == "auto": + assert input_window_samples is not None + self.in_chans = in_chans + self.n_classes = n_classes + self.input_window_samples = input_window_samples + self.final_conv_length = final_conv_length + self.n_filters_time = n_filters_time + self.n_filters_spat = n_filters_spat + self.filter_time_length = filter_time_length + self.pool_time_length = pool_time_length + self.pool_time_stride = pool_time_stride + self.n_filters_2 = n_filters_2 + self.filter_length_2 = filter_length_2 + self.n_filters_3 = n_filters_3 + self.filter_length_3 = filter_length_3 + self.n_filters_4 = n_filters_4 + self.filter_length_4 = filter_length_4 + self.first_nonlin = first_nonlin + self.first_pool_mode = first_pool_mode + self.first_pool_nonlin = first_pool_nonlin + self.later_nonlin = later_nonlin + self.later_pool_mode = later_pool_mode + self.later_pool_nonlin = later_pool_nonlin + self.drop_prob = drop_prob + self.double_time_convs = double_time_convs + self.split_first_layer = split_first_layer + self.batch_norm = batch_norm + self.batch_norm_alpha = batch_norm_alpha + self.stride_before_pool = stride_before_pool + + if self.stride_before_pool: + conv_stride = self.pool_time_stride + pool_stride = 1 + else: + conv_stride = 1 + pool_stride = self.pool_time_stride + self.add_module("ensuredims", Ensure4d()) + pool_class_dict = dict(max=nn.MaxPool2d, mean=AvgPool2dWithConv) + first_pool_class = pool_class_dict[self.first_pool_mode] + later_pool_class = pool_class_dict[self.later_pool_mode] + if self.split_first_layer: + self.add_module("dimshuffle", Expression(transpose_time_to_spat)) + self.add_module( + "conv_time", + nn.Conv2d( + 1, + self.n_filters_time, + (self.filter_time_length, 1), + stride=1, + ), + ) + self.add_module( + "conv_spat", + nn.Conv2d( + self.n_filters_time, + self.n_filters_spat, + (1, self.in_chans), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + n_filters_conv = self.n_filters_spat + else: + self.add_module( + "conv_time", + nn.Conv2d( + self.in_chans, + self.n_filters_time, + (self.filter_time_length, 1), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + n_filters_conv = self.n_filters_time + if self.batch_norm: + self.add_module( + "bnorm", + nn.BatchNorm2d( + n_filters_conv, + momentum=self.batch_norm_alpha, + affine=True, + eps=1e-5, + ), + ) + self.add_module("conv_nonlin", Expression(self.first_nonlin)) + self.add_module( + "pool", + first_pool_class( + kernel_size=(self.pool_time_length, 1), stride=(pool_stride, 1) + ), + ) + self.add_module("pool_nonlin", Expression(self.first_pool_nonlin)) + + def add_conv_pool_block( + model, n_filters_before, n_filters, filter_length, block_nr + ): + suffix = "_{:d}".format(block_nr) + self.add_module("drop" + suffix, nn.Dropout(p=self.drop_prob)) + self.add_module( + "conv" + suffix, + nn.Conv2d( + n_filters_before, + n_filters, + (filter_length, 1), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + if self.batch_norm: + self.add_module( + "bnorm" + suffix, + nn.BatchNorm2d( + n_filters, + momentum=self.batch_norm_alpha, + affine=True, + eps=1e-5, + ), + ) + self.add_module("nonlin" + suffix, Expression(self.later_nonlin)) + + self.add_module( + "pool" + suffix, + later_pool_class( + kernel_size=(self.pool_time_length, 1), + stride=(pool_stride, 1), + ), + ) + self.add_module( + "pool_nonlin" + suffix, Expression(self.later_pool_nonlin) + ) + + add_conv_pool_block( + self, n_filters_conv, self.n_filters_2, self.filter_length_2, 2 + ) + add_conv_pool_block( + self, self.n_filters_2, self.n_filters_3, self.filter_length_3, 3 + ) + add_conv_pool_block( + self, self.n_filters_3, self.n_filters_4, self.filter_length_4, 4 + ) + + # self.add_module('drop_classifier', nn.Dropout(p=self.drop_prob)) +# self.eval() + + if self.final_conv_length == "auto": +# out = self( +# np_to_th( +# np.ones( +# (1, self.in_chans, self.input_window_samples, 1), +# dtype=np.float32, +# ) +# ) +# ) +# n_out_time = out.cpu().data.numpy().shape[2] + n_out_time = 214 + self.final_conv_length = n_out_time + + self.add_module( + "conv_classifier", + nn.Conv2d( + self.n_filters_4, + self.n_classes, + (self.final_conv_length, 1), + bias=True, + ), + ) + self.add_module("softmax", nn.LogSoftmax(dim=1)) + self.add_module("squeeze", Expression(squeeze_final_output)) + + # Initialization, xavier is same as in our paper... + # was default from lasagne + init.xavier_uniform_(self.conv_time.weight, gain=1) + # maybe no bias in case of no split layer and batch norm + if self.split_first_layer or (not self.batch_norm): + init.constant_(self.conv_time.bias, 0) + if self.split_first_layer: + init.xavier_uniform_(self.conv_spat.weight, gain=1) + if not self.batch_norm: + init.constant_(self.conv_spat.bias, 0) + if self.batch_norm: + init.constant_(self.bnorm.weight, 1) + init.constant_(self.bnorm.bias, 0) + param_dict = dict(list(self.named_parameters())) + for block_nr in range(2, 5): + conv_weight = param_dict["conv_{:d}.weight".format(block_nr)] + init.xavier_uniform_(conv_weight, gain=1) + if not self.batch_norm: + conv_bias = param_dict["conv_{:d}.bias".format(block_nr)] + init.constant_(conv_bias, 0) + else: + bnorm_weight = param_dict["bnorm_{:d}.weight".format(block_nr)] + bnorm_bias = param_dict["bnorm_{:d}.bias".format(block_nr)] + init.constant_(bnorm_weight, 1) + init.constant_(bnorm_bias, 0) + + init.xavier_uniform_(self.conv_classifier.weight, gain=1) + init.constant_(self.conv_classifier.bias, 0) + + # Start in eval mode +# self.eval() + + +def get_backbone_and_fc(backbone): + + classifier = nn.Sequential() + classifier.add_module( + "conv_classifier", + backbone.conv_classifier + ) + classifier.add_module("softmax", backbone.softmax) + classifier.add_module("squeeze", backbone.squeeze) + + backbone.conv_classifier = torch.nn.Identity() + backbone.softmax = torch.nn.Identity() + backbone.squeeze = torch.nn.Identity() + return backbone, classifier + +class Deep4NetModel(nn.Module): + def __init__(self, num_channel=10, num_classes=4, signal_length=1000): + super().__init__() + + base_model = Deep4Net( + in_chans=num_channel, + n_classes=num_classes, + input_window_samples=signal_length, + ) + + self.backbone, self.fc = get_backbone_and_fc(base_model) + + def forward(self, x): + x= self.backbone(x) + x = self.fc(x) + return x diff --git a/experiments/ssl_classifier/devt_deep_cnn.ipynb b/experiments/ssl_classifier/devt_deep_cnn.ipynb new file mode 100644 index 0000000..40507ab --- /dev/null +++ b/experiments/ssl_classifier/devt_deep_cnn.ipynb @@ -0,0 +1,1130 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "import numpy as np\n", + "from torch import nn\n", + "from torch.nn import init\n", + "from torch.nn.functional import elu" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def np_to_th(\n", + " X, requires_grad=False, dtype=None, pin_memory=False, **tensor_kwargs\n", + "):\n", + " \"\"\"\n", + " Convenience function to transform numpy array to `torch.Tensor`.\n", + " Converts `X` to ndarray using asarray if necessary.\n", + " Parameters\n", + " ----------\n", + " X: ndarray or list or number\n", + " Input arrays\n", + " requires_grad: bool\n", + " passed on to Variable constructor\n", + " dtype: numpy dtype, optional\n", + " var_kwargs:\n", + " passed on to Variable constructor\n", + " Returns\n", + " -------\n", + " var: `torch.Tensor`\n", + " \"\"\"\n", + " if not hasattr(X, \"__len__\"):\n", + " X = [X]\n", + " X = np.asarray(X)\n", + " if dtype is not None:\n", + " X = X.astype(dtype)\n", + " X_tensor = torch.tensor(X, requires_grad=requires_grad, **tensor_kwargs)\n", + " if pin_memory:\n", + " X_tensor = X_tensor.pin_memory()\n", + " return X_tensor\n", + "\n", + "def identity(x):\n", + " return x\n", + "\n", + "def transpose_time_to_spat(x):\n", + " \"\"\"Swap time and spatial dimensions.\n", + " Returns\n", + " -------\n", + " x: torch.Tensor\n", + " tensor in which last and first dimensions are swapped\n", + " \"\"\"\n", + " return x.permute(0, 3, 2, 1)\n", + "\n", + "def squeeze_final_output(x):\n", + " \"\"\"Removes empty dimension at end and potentially removes empty time\n", + " dimension. It does not just use squeeze as we never want to remove\n", + " first dimension.\n", + " Returns\n", + " -------\n", + " x: torch.Tensor\n", + " squeezed tensor\n", + " \"\"\"\n", + "\n", + " assert x.size()[3] == 1\n", + " x = x[:, :, :, 0]\n", + " if x.size()[2] == 1:\n", + " x = x[:, :, 0]\n", + " return x\n", + "\n", + "\n", + "class Expression(nn.Module):\n", + " \"\"\"Compute given expression on forward pass.\n", + " Parameters\n", + " ----------\n", + " expression_fn : callable\n", + " Should accept variable number of objects of type\n", + " `torch.autograd.Variable` to compute its output.\n", + " \"\"\"\n", + "\n", + " def __init__(self, expression_fn):\n", + " super(Expression, self).__init__()\n", + " self.expression_fn = expression_fn\n", + "\n", + " def forward(self, *x):\n", + " return self.expression_fn(*x)\n", + "\n", + " def __repr__(self):\n", + " if hasattr(self.expression_fn, \"func\") and hasattr(\n", + " self.expression_fn, \"kwargs\"\n", + " ):\n", + " expression_str = \"{:s} {:s}\".format(\n", + " self.expression_fn.func.__name__, str(self.expression_fn.kwargs)\n", + " )\n", + " elif hasattr(self.expression_fn, \"__name__\"):\n", + " expression_str = self.expression_fn.__name__\n", + " else:\n", + " expression_str = repr(self.expression_fn)\n", + " return (\n", + " self.__class__.__name__ +\n", + " \"(expression=%s) \" % expression_str\n", + " )\n", + "\n", + "\n", + "class AvgPool2dWithConv(nn.Module):\n", + " \"\"\"\n", + " Compute average pooling using a convolution, to have the dilation parameter.\n", + " Parameters\n", + " ----------\n", + " kernel_size: (int,int)\n", + " Size of the pooling region.\n", + " stride: (int,int)\n", + " Stride of the pooling operation.\n", + " dilation: int or (int,int)\n", + " Dilation applied to the pooling filter.\n", + " padding: int or (int,int)\n", + " Padding applied before the pooling operation.\n", + " \"\"\"\n", + "\n", + " def __init__(self, kernel_size, stride, dilation=1, padding=0):\n", + " super(AvgPool2dWithConv, self).__init__()\n", + " self.kernel_size = kernel_size\n", + " self.stride = stride\n", + " self.dilation = dilation\n", + " self.padding = padding\n", + " # don't name them \"weights\" to\n", + " # make sure these are not accidentally used by some procedure\n", + " # that initializes parameters or something\n", + " self._pool_weights = None\n", + "\n", + " def forward(self, x):\n", + " # Create weights for the convolution on demand:\n", + " # size or type of x changed...\n", + " in_channels = x.size()[1]\n", + " weight_shape = (\n", + " in_channels,\n", + " 1,\n", + " self.kernel_size[0],\n", + " self.kernel_size[1],\n", + " )\n", + " if self._pool_weights is None or (\n", + " (tuple(self._pool_weights.size()) != tuple(weight_shape)) or\n", + " (self._pool_weights.is_cuda != x.is_cuda) or\n", + " (self._pool_weights.data.type() != x.data.type())\n", + " ):\n", + " n_pool = np.prod(self.kernel_size)\n", + " weights = np_to_th(\n", + " np.ones(weight_shape, dtype=np.float32) / float(n_pool)\n", + " )\n", + " weights = weights.type_as(x)\n", + " if x.is_cuda:\n", + " weights = weights.cuda()\n", + " self._pool_weights = weights\n", + "\n", + " pooled = F.conv2d(\n", + " x,\n", + " self._pool_weights,\n", + " bias=None,\n", + " stride=self.stride,\n", + " dilation=self.dilation,\n", + " padding=self.padding,\n", + " groups=in_channels,\n", + " )\n", + " return pooled\n", + " \n", + "class Ensure4d(nn.Module):\n", + " def forward(self, x):\n", + " while(len(x.shape) < 4):\n", + " x = x.unsqueeze(-1)\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "class Deep4Net(nn.Sequential):\n", + " \"\"\"Deep ConvNet model from Schirrmeister et al 2017.\n", + " Model described in [Schirrmeister2017]_.\n", + " Parameters\n", + " ----------\n", + " in_chans : int\n", + " XXX\n", + " References\n", + " ----------\n", + " .. [Schirrmeister2017] Schirrmeister, R. T., Springenberg, J. T., Fiederer,\n", + " L. D. J., Glasstetter, M., Eggensperger, K., Tangermann, M., Hutter, F.\n", + " & Ball, T. (2017).\n", + " Deep learning with convolutional neural networks for EEG decoding and\n", + " visualization.\n", + " Human Brain Mapping , Aug. 2017.\n", + " Online: http://dx.doi.org/10.1002/hbm.23730\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " in_chans,\n", + " n_classes,\n", + " input_window_samples,\n", + " final_conv_length=\"auto\",\n", + " n_filters_time=25,\n", + " n_filters_spat=25,\n", + " filter_time_length=10,\n", + " pool_time_length=1,\n", + " pool_time_stride=1,\n", + " n_filters_2=50,\n", + " filter_length_2=10,\n", + " n_filters_3=100,\n", + " filter_length_3=10,\n", + " n_filters_4=200,\n", + " filter_length_4=10,\n", + " first_nonlin=elu,\n", + " first_pool_mode=\"max\",\n", + " first_pool_nonlin=identity,\n", + " later_nonlin=elu,\n", + " later_pool_mode=\"max\",\n", + " later_pool_nonlin=identity,\n", + " drop_prob=0.5,\n", + " double_time_convs=False,\n", + " split_first_layer=True,\n", + " batch_norm=True,\n", + " batch_norm_alpha=0.1,\n", + " stride_before_pool=False,\n", + " ):\n", + " super().__init__()\n", + " if final_conv_length == \"auto\":\n", + " assert input_window_samples is not None\n", + " self.in_chans = in_chans\n", + " self.n_classes = n_classes\n", + " self.input_window_samples = input_window_samples\n", + " self.final_conv_length = final_conv_length\n", + " self.n_filters_time = n_filters_time\n", + " self.n_filters_spat = n_filters_spat\n", + " self.filter_time_length = filter_time_length\n", + " self.pool_time_length = pool_time_length\n", + " self.pool_time_stride = pool_time_stride\n", + " self.n_filters_2 = n_filters_2\n", + " self.filter_length_2 = filter_length_2\n", + " self.n_filters_3 = n_filters_3\n", + " self.filter_length_3 = filter_length_3\n", + " self.n_filters_4 = n_filters_4\n", + " self.filter_length_4 = filter_length_4\n", + " self.first_nonlin = first_nonlin\n", + " self.first_pool_mode = first_pool_mode\n", + " self.first_pool_nonlin = first_pool_nonlin\n", + " self.later_nonlin = later_nonlin\n", + " self.later_pool_mode = later_pool_mode\n", + " self.later_pool_nonlin = later_pool_nonlin\n", + " self.drop_prob = drop_prob\n", + " self.double_time_convs = double_time_convs\n", + " self.split_first_layer = split_first_layer\n", + " self.batch_norm = batch_norm\n", + " self.batch_norm_alpha = batch_norm_alpha\n", + " self.stride_before_pool = stride_before_pool\n", + "\n", + " if self.stride_before_pool:\n", + " conv_stride = self.pool_time_stride\n", + " pool_stride = 1\n", + " else:\n", + " conv_stride = 1\n", + " pool_stride = self.pool_time_stride\n", + " self.add_module(\"ensuredims\", Ensure4d())\n", + " pool_class_dict = dict(max=nn.MaxPool2d, mean=AvgPool2dWithConv)\n", + " first_pool_class = pool_class_dict[self.first_pool_mode]\n", + " later_pool_class = pool_class_dict[self.later_pool_mode]\n", + " if self.split_first_layer:\n", + " self.add_module(\"dimshuffle\", Expression(transpose_time_to_spat))\n", + " self.add_module(\n", + " \"conv_time\",\n", + " nn.Conv2d(\n", + " 1,\n", + " self.n_filters_time,\n", + " (self.filter_time_length, 1),\n", + " stride=1,\n", + " ),\n", + " )\n", + " self.add_module(\n", + " \"conv_spat\",\n", + " nn.Conv2d(\n", + " self.n_filters_time,\n", + " self.n_filters_spat,\n", + " (1, self.in_chans),\n", + " stride=(conv_stride, 1),\n", + " bias=not self.batch_norm,\n", + " ),\n", + " )\n", + " n_filters_conv = self.n_filters_spat\n", + " else:\n", + " self.add_module(\n", + " \"conv_time\",\n", + " nn.Conv2d(\n", + " self.in_chans,\n", + " self.n_filters_time,\n", + " (self.filter_time_length, 1),\n", + " stride=(conv_stride, 1),\n", + " bias=not self.batch_norm,\n", + " ),\n", + " )\n", + " n_filters_conv = self.n_filters_time\n", + " if self.batch_norm:\n", + " self.add_module(\n", + " \"bnorm\",\n", + " nn.BatchNorm2d(\n", + " n_filters_conv,\n", + " momentum=self.batch_norm_alpha,\n", + " affine=True,\n", + " eps=1e-5,\n", + " ),\n", + " )\n", + " self.add_module(\"conv_nonlin\", Expression(self.first_nonlin))\n", + " self.add_module(\n", + " \"pool\",\n", + " first_pool_class(\n", + " kernel_size=(self.pool_time_length, 1), stride=(pool_stride, 1)\n", + " ),\n", + " )\n", + " self.add_module(\"pool_nonlin\", Expression(self.first_pool_nonlin))\n", + "\n", + " def add_conv_pool_block(\n", + " model, n_filters_before, n_filters, filter_length, block_nr\n", + " ):\n", + " suffix = \"_{:d}\".format(block_nr)\n", + " self.add_module(\"drop\" + suffix, nn.Dropout(p=self.drop_prob))\n", + " self.add_module(\n", + " \"conv\" + suffix,\n", + " nn.Conv2d(\n", + " n_filters_before,\n", + " n_filters,\n", + " (filter_length, 1),\n", + " stride=(conv_stride, 1),\n", + " bias=not self.batch_norm,\n", + " ),\n", + " )\n", + " if self.batch_norm:\n", + " self.add_module(\n", + " \"bnorm\" + suffix,\n", + " nn.BatchNorm2d(\n", + " n_filters,\n", + " momentum=self.batch_norm_alpha,\n", + " affine=True,\n", + " eps=1e-5,\n", + " ),\n", + " )\n", + " self.add_module(\"nonlin\" + suffix, Expression(self.later_nonlin))\n", + "\n", + " self.add_module(\n", + " \"pool\" + suffix,\n", + " later_pool_class(\n", + " kernel_size=(self.pool_time_length, 1),\n", + " stride=(pool_stride, 1),\n", + " ),\n", + " )\n", + " self.add_module(\n", + " \"pool_nonlin\" + suffix, Expression(self.later_pool_nonlin)\n", + " )\n", + "\n", + " add_conv_pool_block(\n", + " self, n_filters_conv, self.n_filters_2, self.filter_length_2, 2\n", + " )\n", + " add_conv_pool_block(\n", + " self, self.n_filters_2, self.n_filters_3, self.filter_length_3, 3\n", + " )\n", + " add_conv_pool_block(\n", + " self, self.n_filters_3, self.n_filters_4, self.filter_length_4, 4\n", + " )\n", + "\n", + " # self.add_module('drop_classifier', nn.Dropout(p=self.drop_prob))\n", + " self.eval()\n", + " if self.final_conv_length == \"auto\":\n", + " out = self(\n", + " np_to_th(\n", + " np.ones(\n", + " (1, self.in_chans, self.input_window_samples, 1),\n", + " dtype=np.float32,\n", + " )\n", + " )\n", + " )\n", + " n_out_time = out.cpu().data.numpy().shape[2]\n", + " self.final_conv_length = n_out_time\n", + " self.add_module(\n", + " \"conv_classifier\",\n", + " nn.Conv2d(\n", + " self.n_filters_4,\n", + " self.final_conv_length,\n", + " (self.final_conv_length, 1),\n", + " bias=True,\n", + " ),\n", + " )\n", + "\n", + " self.add_module(\"softmax\", nn.LogSoftmax(dim=1))\n", + " self.add_module(\"squeeze\", Expression(squeeze_final_output))\n", + "\n", + " # Initialization, xavier is same as in our paper...\n", + " # was default from lasagne\n", + " init.xavier_uniform_(self.conv_time.weight, gain=1)\n", + " # maybe no bias in case of no split layer and batch norm\n", + " if self.split_first_layer or (not self.batch_norm):\n", + " init.constant_(self.conv_time.bias, 0)\n", + " if self.split_first_layer:\n", + " init.xavier_uniform_(self.conv_spat.weight, gain=1)\n", + " if not self.batch_norm:\n", + " init.constant_(self.conv_spat.bias, 0)\n", + " if self.batch_norm:\n", + " init.constant_(self.bnorm.weight, 1)\n", + " init.constant_(self.bnorm.bias, 0)\n", + " param_dict = dict(list(self.named_parameters()))\n", + " for block_nr in range(2, 5):\n", + " conv_weight = param_dict[\"conv_{:d}.weight\".format(block_nr)]\n", + " init.xavier_uniform_(conv_weight, gain=1)\n", + " if not self.batch_norm:\n", + " conv_bias = param_dict[\"conv_{:d}.bias\".format(block_nr)]\n", + " init.constant_(conv_bias, 0)\n", + " else:\n", + " bnorm_weight = param_dict[\"bnorm_{:d}.weight\".format(block_nr)]\n", + " bnorm_bias = param_dict[\"bnorm_{:d}.bias\".format(block_nr)]\n", + " init.constant_(bnorm_weight, 1)\n", + " init.constant_(bnorm_bias, 0)\n", + "\n", + " init.xavier_uniform_(self.conv_classifier.weight, gain=1)\n", + " init.constant_(self.conv_classifier.bias, 0)\n", + "\n", + " # Start in eval mode\n", + " self.eval()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([3, 214])" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# model = Deep4Net(\n", + "# in_chans=10,\n", + "# n_classes=11,\n", + "# input_window_samples=250,\n", + "# final_conv_length=\"auto\"\n", + "# )\n", + "# # model\n", + "# x = torch.rand(3, 10, 250)\n", + "# y = model(x)\n", + "# y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "214\n", + "torch.Size([3, 200, 214, 1])\n", + "2 torch.Size([3, 214, 214])\n" + ] + }, + { + "data": { + "text/plain": [ + "torch.Size([3, 214, 214])" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def get_backbone_and_fc(backbone):\n", + " \n", + " classifier = nn.Sequential()\n", + " classifier.add_module(\n", + " \"conv_classifier\",\n", + " backbone.conv_classifier\n", + " )\n", + " classifier.add_module(\"softmax\", backbone.softmax)\n", + " classifier.add_module(\"squeeze\", backbone.squeeze)\n", + "\n", + " backbone.conv_classifier = torch.nn.Identity()\n", + " backbone.softmax = torch.nn.Identity()\n", + " backbone.squeeze = torch.nn.Identity()\n", + " return backbone, classifier\n", + "\n", + "class Deep4NetModel(nn.Module):\n", + " def __init__(self, num_channel=10, num_classes=4, signal_length=1000):\n", + " super().__init__()\n", + " \n", + " base_model = Deep4Net(\n", + " in_chans=num_channel,\n", + " n_classes=num_classes,\n", + " input_window_samples=signal_length,\n", + " final_conv_length=\"auto\"\n", + " )\n", + " \n", + " self.backbone, self.fc = get_backbone_and_fc(base_model)\n", + " \n", + " def forward(self, x):\n", + " x = self.backbone(x)\n", + " print(x.shape)\n", + " x = self.fc(x)\n", + " print(2, x.shape)\n", + " return x\n", + "\n", + "\n", + "model = Deep4NetModel(\n", + " num_channel=10,\n", + " num_classes=11,\n", + " signal_length=250,\n", + ")\n", + "\n", + "x = torch.rand(3, 10, 250)\n", + "y = model(x)\n", + "y.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# from deep4net import Deep4NetModel\n", + "# import torch\n", + "# # model = Deep4Net(\n", + "# # in_chans=10,\n", + "# # n_classes=11,\n", + "# # input_window_samples=250,\n", + "# # final_conv_length=\"auto\"\n", + "# # )\n", + "\n", + "# model = Deep4NetModel(\n", + "# num_channel=10,\n", + "# num_classes=11,\n", + "# signal_length=250,\n", + "# )\n", + "# # model\n", + "# x = torch.rand(3, 10, 250)\n", + "# y = model(x)\n", + "# y.shape\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n" + ] + }, + { + "data": { + "text/plain": [ + "1234" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import os\n", + "cwd = os.getcwd()\n", + "import sys\n", + "path = os.path.join(cwd, \"..\\\\..\\\\\")\n", + "sys.path.append(path)\n", + "\n", + "import numpy as np\n", + "import torch\n", + "from torch.utils.data import DataLoader\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "from torch.nn import init\n", + "from torch.nn.functional import elu\n", + "\n", + "from pytorch_lightning import Trainer, seed_everything\n", + "from pytorch_lightning.callbacks import LearningRateMonitor\n", + "from pytorch_lightning.loggers import TensorBoardLogger\n", + "\n", + "import logging\n", + "logging.getLogger('lightning').setLevel(0)\n", + "\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "import pytorch_lightning\n", + "pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR)\n", + "\n", + "from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP\n", + "from splearn.filter.butterworth import butter_bandpass_filter\n", + "from splearn.filter.notch import notch_filter\n", + "from splearn.filter.channels import pick_channels\n", + "from splearn.nn.models import CompactEEGNet\n", + "from splearn.utils import Logger, Config\n", + "from splearn.nn.base import LightningModelClassifier\n", + "\n", + "####\n", + "\n", + "config = {\n", + " \"run_name\": \"deep4net_normal\",\n", + " \"data\": {\n", + " \"load_subject_ids\": np.arange(1,36),\n", + " # \"selected_channels\": [\"PO8\", \"PZ\", \"PO7\", \"PO4\", \"POz\", \"PO3\", \"O2\", \"Oz\", \"O1\"], # AA paper\n", + " \"selected_channels\": [\"PZ\", \"PO5\", \"PO3\", \"POz\", \"PO4\", \"PO6\", \"O1\", \"Oz\", \"O2\"], # hsssvep paper\n", + " },\n", + " \"training\": {\n", + " \"num_epochs\": 500,\n", + " \"num_warmup_epochs\": 50,\n", + " \"learning_rate\": 0.03,\n", + " \"gpus\": [0],\n", + " \"batchsize\": 256,\n", + " },\n", + " \"model\": {\n", + " \"optimizer\": \"adamw\",\n", + " \"scheduler\": \"cosine_with_warmup\",\n", + " },\n", + " \"testing\": {\n", + " \"test_subject_ids\": np.arange(1,36),\n", + " \"kfolds\": np.arange(0,3),\n", + " },\n", + " \"seed\": 1234\n", + "}\n", + "\n", + "main_logger = Logger(filename_postfix=config[\"run_name\"])\n", + "main_logger.write_to_log(\"Config\")\n", + "main_logger.write_to_log(config)\n", + "\n", + "config = Config(config)\n", + "\n", + "seed_everything(config.seed)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Load subject: 1\n", + "Load subject: 2\n", + "Load subject: 3\n", + "Load subject: 4\n", + "Load subject: 5\n", + "Load subject: 6\n", + "Load subject: 7\n", + "Load subject: 8\n", + "Load subject: 9\n", + "Load subject: 10\n", + "Load subject: 11\n", + "Load subject: 12\n", + "Load subject: 13\n", + "Load subject: 14\n", + "Load subject: 15\n", + "Load subject: 16\n", + "Load subject: 17\n", + "Load subject: 18\n", + "Load subject: 19\n", + "Load subject: 20\n", + "Load subject: 21\n", + "Load subject: 22\n", + "Load subject: 23\n", + "Load subject: 24\n", + "Load subject: 25\n", + "Load subject: 26\n", + "Load subject: 27\n", + "Load subject: 28\n", + "Load subject: 29\n", + "Load subject: 30\n", + "Load subject: 31\n", + "Load subject: 32\n", + "Load subject: 33\n", + "Load subject: 34\n", + "Load subject: 35\n", + "Final data shape: (35, 240, 9, 250) (35, 240)\n" + ] + } + ], + "source": [ + "def func_preprocessing(data):\n", + " data_x = data.data\n", + " data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels)\n", + " # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0)\n", + " data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6)\n", + " start_t = 160\n", + " end_t = start_t + 250\n", + " data_x = data_x[:,:,:,start_t:end_t]\n", + " data.set_data(data_x)\n", + " \n", + "\n", + "def leave_one_subject_out(data, **kwargs):\n", + " \n", + " test_subject_id = kwargs[\"test_subject_id\"] if \"test_subject_id\" in kwargs else 1\n", + " \n", + " # get test data\n", + " # test_sub_idx = data.subject_ids.index(test_subject_id)\n", + " test_sub_idx = np.where(data.subject_ids == test_subject_id)[0][0]\n", + " selected_subject_data = data.data[test_sub_idx]\n", + " selected_subject_targets = data.targets[test_sub_idx]\n", + " test_dataset = PyTorchDataset(selected_subject_data, selected_subject_targets)\n", + " \n", + " # get train val data\n", + " indices = np.arange(data.data.shape[0])\n", + " train_val_data = data.data[indices!=test_sub_idx, :, :, :]\n", + " train_val_data = train_val_data.reshape((train_val_data.shape[0]*train_val_data.shape[1], train_val_data.shape[2], train_val_data.shape[3]))\n", + " train_val_targets = data.targets[indices!=test_sub_idx, :]\n", + " train_val_targets = train_val_targets.reshape((train_val_targets.shape[0]*train_val_targets.shape[1]))\n", + "\n", + " train_dataset = PyTorchDataset(train_val_data, train_val_targets)\n", + "\n", + " return train_dataset, test_dataset\n", + "\n", + "data = MultipleSubjects(\n", + " dataset=HSSSVEP, \n", + " root=os.path.join(path, \"../data/hsssvep\"), \n", + " subject_ids=config.data.load_subject_ids, \n", + " func_preprocessing=func_preprocessing,\n", + " func_get_train_val_test_dataset=leave_one_subject_out,\n", + " verbose=True, \n", + ")\n", + "\n", + "print(\"Final data shape:\", data.data.shape, data.targets.shape)\n", + "\n", + "num_channel = data.data.shape[2]\n", + "num_classes = 40\n", + "signal_length = data.data.shape[3]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "running test_subject_id: 30\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 30, 'mean_acc': 0.5, 'acc': []}\n", + "\n", + "running test_subject_id: 31\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 31, 'mean_acc': 0.8333333134651184, 'acc': []}\n", + "\n", + "running test_subject_id: 32\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 32, 'mean_acc': 0.9833333492279053, 'acc': []}\n", + "\n", + "running test_subject_id: 33\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 33, 'mean_acc': 0.28333333134651184, 'acc': []}\n", + "\n", + "running test_subject_id: 34\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 34, 'mean_acc': 0.7875000238418579, 'acc': []}\n", + "\n", + "running test_subject_id: 35\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 35, 'mean_acc': 0.7749999761581421, 'acc': []}\n", + "\n", + "mean all 0.6937499990065893\n" + ] + } + ], + "source": [ + "def train_test_subject(data, config, test_subject_id):\n", + " \n", + " ## init data\n", + " \n", + " train_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id)\n", + " train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + " test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + " ## init model\n", + " base_model = Deep4NetModel(\n", + " num_channel=num_channel,\n", + " num_classes=num_classes,\n", + " signal_length=signal_length,\n", + " )\n", + "\n", + " model = LightningModelClassifier(\n", + " optimizer=config.model.optimizer,\n", + " scheduler=config.model.scheduler,\n", + " optimizer_learning_rate=config.training.learning_rate,\n", + " scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + " )\n", + " \n", + " model.build_model(model=base_model)\n", + "\n", + " ## train\n", + "\n", + " sub_dir = \"sub\"+ str(test_subject_id)\n", + " logger_tb = TensorBoardLogger(save_dir=\"tensorboard_logs\", name=config.run_name, sub_dir=sub_dir)\n", + " lr_monitor = LearningRateMonitor(logging_interval='epoch')\n", + "\n", + " trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor])\n", + " trainer.fit(model, train_loader)\n", + " \n", + " ## test\n", + " \n", + " result = trainer.test(dataloaders=test_loader, verbose=False)\n", + " test_acc = result[0]['test_acc_epoch']\n", + " \n", + " return test_acc\n", + "\n", + "####\n", + "\n", + "main_logger.write_to_log(\"Begin\", break_line=True)\n", + "\n", + "test_results_acc = {}\n", + "means = []\n", + "\n", + "def k_fold_train_test_all_subjects():\n", + " \n", + " for test_subject_id in config.testing.test_subject_ids:\n", + " print()\n", + " print(\"running test_subject_id:\", test_subject_id)\n", + " \n", + " if test_subject_id not in test_results_acc:\n", + " test_results_acc[test_subject_id] = []\n", + " \n", + " mean_acc = train_test_subject(data, config, test_subject_id)\n", + "\n", + " means.append(mean_acc)\n", + " \n", + " this_result = {\n", + " \"test_subject_id\": test_subject_id,\n", + " \"mean_acc\": mean_acc,\n", + " \"acc\": test_results_acc[test_subject_id],\n", + " } \n", + " print(this_result)\n", + " main_logger.write_to_log(this_result)\n", + "\n", + "k_fold_train_test_all_subjects()\n", + "\n", + "mean_acc = np.mean(means)\n", + "print()\n", + "print(\"mean all\", mean_acc)\n", + "main_logger.write_to_log(\"Mean acc: \"+str(mean_acc), break_line=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "running test_subject_id: 1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n" + ] + } + ], + "source": [ + "def train_test_subject(data, config, test_subject_id):\n", + " \n", + " ## init data\n", + " \n", + " train_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id)\n", + " train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + " test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + " ## init model\n", + " base_model = Deep4NetModel(\n", + " num_channel=num_channel,\n", + " num_classes=num_classes,\n", + " signal_length=signal_length,\n", + " )\n", + "\n", + " model = LightningModelClassifier(\n", + " optimizer=config.model.optimizer,\n", + " scheduler=config.model.scheduler,\n", + " optimizer_learning_rate=config.training.learning_rate,\n", + " scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + " )\n", + " \n", + " model.build_model(model=base_model)\n", + "\n", + " ## train\n", + "\n", + " sub_dir = \"sub\"+ str(test_subject_id)\n", + " logger_tb = TensorBoardLogger(save_dir=\"tensorboard_logs\", name=config.run_name, sub_dir=sub_dir)\n", + " lr_monitor = LearningRateMonitor(logging_interval='epoch')\n", + "\n", + " trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor])\n", + " trainer.fit(model, train_loader)\n", + " \n", + " ## test\n", + " \n", + " result = trainer.test(dataloaders=test_loader, verbose=False)\n", + " test_acc = result[0]['test_acc_epoch']\n", + " \n", + " return test_acc\n", + "\n", + "####\n", + "\n", + "main_logger.write_to_log(\"Begin\", break_line=True)\n", + "\n", + "test_results_acc = {}\n", + "means = []\n", + "\n", + "def k_fold_train_test_all_subjects():\n", + " \n", + " for test_subject_id in config.testing.test_subject_ids:\n", + " print()\n", + " print(\"running test_subject_id:\", test_subject_id)\n", + " \n", + " if test_subject_id not in test_results_acc:\n", + " test_results_acc[test_subject_id] = []\n", + " \n", + " mean_acc = train_test_subject(data, config, test_subject_id)\n", + "\n", + " means.append(mean_acc)\n", + " \n", + " this_result = {\n", + " \"test_subject_id\": test_subject_id,\n", + " \"mean_acc\": mean_acc,\n", + " \"acc\": test_results_acc[test_subject_id],\n", + " } \n", + " print(this_result)\n", + " main_logger.write_to_log(this_result)\n", + "\n", + "k_fold_train_test_all_subjects()\n", + "\n", + "mean_acc = np.mean(means)\n", + "print()\n", + "print(\"mean all\", mean_acc)\n", + "main_logger.write_to_log(\"Mean acc: \"+str(mean_acc), break_line=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/experiments/ssl_classifier/main_cca_hsssvep.py b/experiments/ssl_classifier/main_cca_hsssvep.py new file mode 100644 index 0000000..886b6af --- /dev/null +++ b/experiments/ssl_classifier/main_cca_hsssvep.py @@ -0,0 +1,107 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np + +from splearn.data import MultipleSubjects, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.utils import Logger, Config +from splearn.cross_validate.leave_one_out import block_evaluation +from splearn.cross_decomposition.cca import * # https://github.com/jinglescode/python-signal-processing/blob/main/splearn/cross_decomposition/ +from splearn.cross_decomposition.reference_frequencies import * # https://github.com/jinglescode/python-signal-processing/blob/main/splearn/cross_decomposition/ + +#### + +config = { + "run_name": "cca_hsssvep_run2", + "data": { + "load_subject_ids": np.arange(1,36), + # "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], # AA paper + "selected_channels": ["PZ", "PO5", "PO3", "POz", "PO4", "PO6", "O1", "Oz", "O2"], # hsssvep paper + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +#### + +""" +def func_preprocessing(data): + data_x = data.data + # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4'] + selected_channels = config.data.selected_channels + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6) + start_t = 125 + end_t = 125 + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) +""" + +def func_preprocessing(data): + data_x = data.data + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6) + start_t = 160 + end_t = start_t + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + verbose=True, +) + +print("Final data shape:", data.data.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +sampling_rate = data.sampling_rate +signal_duration_seconds = 1 +target_frequencies = data.stimulus_frequencies +reference_frequencies = generate_reference_signals(target_frequencies, size=signal_duration_seconds*sampling_rate, sampling_rate=sampling_rate, num_harmonics=5) +print("reference_frequencies.shape", reference_frequencies.shape) + +#### + + +def test_cca_subject(test_subject_id): + data_subject, labels = data.get_subject(test_subject_id) + predicted_class, accuracy, predicted_probabilities, _, _ = perform_cca(data_subject, reference_frequencies, labels=labels) + return accuracy + +test_results_acc = [] + +for test_subject_id in config.data.load_subject_ids: + test_acc = test_cca_subject(test_subject_id) + test_results_acc.append(test_acc) + + this_result = { + "test_subject_id": test_subject_id, + "acc": test_acc, + } + + main_logger.write_to_log(this_result) + +mean_acc = np.array(test_results_acc).mean().round(3)*100 + +print(f'Mean test accuracy: {mean_acc}%') + +main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) diff --git a/experiments/ssl_classifier/main_deep4net_hsssvep.py b/experiments/ssl_classifier/main_deep4net_hsssvep.py new file mode 100644 index 0000000..e120976 --- /dev/null +++ b/experiments/ssl_classifier/main_deep4net_hsssvep.py @@ -0,0 +1,602 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np +import torch +from torch.utils.data import DataLoader +import torch.nn as nn +import torch.nn.functional as F +from torch.nn import init +from torch.nn.functional import elu + +from pytorch_lightning import Trainer, seed_everything +from pytorch_lightning.callbacks import LearningRateMonitor +from pytorch_lightning.loggers import TensorBoardLogger + +import logging +logging.getLogger('lightning').setLevel(0) + +import warnings +warnings.filterwarnings('ignore') + +import pytorch_lightning +pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR) + +from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.nn.models import CompactEEGNet +from splearn.utils import Logger, Config +from splearn.nn.base import LightningModelClassifier + +#### + +config = { + "run_name": "deep4net_normal", + "data": { + "load_subject_ids": np.arange(1,3), + # "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], # AA paper + "selected_channels": ["PZ", "PO5", "PO3", "POz", "PO4", "PO6", "O1", "Oz", "O2"], # hsssvep paper + }, + "training": { + "num_epochs": 10, + "num_warmup_epochs": 50, + "learning_rate": 0.03, + "gpus": [0], + "batchsize": 256, + }, + "model": { + "optimizer": "adamw", + "scheduler": "cosine_with_warmup", + }, + "testing": { + "test_subject_ids": np.arange(1,2), + "kfolds": np.arange(0,3), + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +seed_everything(config.seed) + +#### + +# def func_preprocessing(data): +# data_x = data.data +# # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4'] +# selected_channels = config.data.selected_channels +# data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels) +# # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) +# data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6) +# start_t = 125 +# end_t = 125 + 250 +# data_x = data_x[:,:,:,start_t:end_t] +# data.set_data(data_x) + +def func_preprocessing(data): + data_x = data.data + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6) + start_t = 160 + end_t = start_t + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + verbose=True, +) + +print("Final data shape:", data.data.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +#### + + +def np_to_th( + X, requires_grad=False, dtype=None, pin_memory=False, **tensor_kwargs +): + """ + Convenience function to transform numpy array to `torch.Tensor`. + Converts `X` to ndarray using asarray if necessary. + Parameters + ---------- + X: ndarray or list or number + Input arrays + requires_grad: bool + passed on to Variable constructor + dtype: numpy dtype, optional + var_kwargs: + passed on to Variable constructor + Returns + ------- + var: `torch.Tensor` + """ + if not hasattr(X, "__len__"): + X = [X] + X = np.asarray(X) + if dtype is not None: + X = X.astype(dtype) + X_tensor = torch.tensor(X, requires_grad=requires_grad, **tensor_kwargs) + if pin_memory: + X_tensor = X_tensor.pin_memory() + return X_tensor + +def identity(x): + return x + +def transpose_time_to_spat(x): + """Swap time and spatial dimensions. + Returns + ------- + x: torch.Tensor + tensor in which last and first dimensions are swapped + """ + return x.permute(0, 3, 2, 1) + +def squeeze_final_output(x): + """Removes empty dimension at end and potentially removes empty time + dimension. It does not just use squeeze as we never want to remove + first dimension. + Returns + ------- + x: torch.Tensor + squeezed tensor + """ + + assert x.size()[3] == 1 + x = x[:, :, :, 0] + if x.size()[2] == 1: + x = x[:, :, 0] + return x + + +class Expression(nn.Module): + """Compute given expression on forward pass. + Parameters + ---------- + expression_fn : callable + Should accept variable number of objects of type + `torch.autograd.Variable` to compute its output. + """ + + def __init__(self, expression_fn): + super(Expression, self).__init__() + self.expression_fn = expression_fn + + def forward(self, *x): + return self.expression_fn(*x) + + def __repr__(self): + if hasattr(self.expression_fn, "func") and hasattr( + self.expression_fn, "kwargs" + ): + expression_str = "{:s} {:s}".format( + self.expression_fn.func.__name__, str(self.expression_fn.kwargs) + ) + elif hasattr(self.expression_fn, "__name__"): + expression_str = self.expression_fn.__name__ + else: + expression_str = repr(self.expression_fn) + return ( + self.__class__.__name__ + + "(expression=%s) " % expression_str + ) + + +class AvgPool2dWithConv(nn.Module): + """ + Compute average pooling using a convolution, to have the dilation parameter. + Parameters + ---------- + kernel_size: (int,int) + Size of the pooling region. + stride: (int,int) + Stride of the pooling operation. + dilation: int or (int,int) + Dilation applied to the pooling filter. + padding: int or (int,int) + Padding applied before the pooling operation. + """ + + def __init__(self, kernel_size, stride, dilation=1, padding=0): + super(AvgPool2dWithConv, self).__init__() + self.kernel_size = kernel_size + self.stride = stride + self.dilation = dilation + self.padding = padding + # don't name them "weights" to + # make sure these are not accidentally used by some procedure + # that initializes parameters or something + self._pool_weights = None + + def forward(self, x): + # Create weights for the convolution on demand: + # size or type of x changed... + in_channels = x.size()[1] + weight_shape = ( + in_channels, + 1, + self.kernel_size[0], + self.kernel_size[1], + ) + if self._pool_weights is None or ( + (tuple(self._pool_weights.size()) != tuple(weight_shape)) or + (self._pool_weights.is_cuda != x.is_cuda) or + (self._pool_weights.data.type() != x.data.type()) + ): + n_pool = np.prod(self.kernel_size) + weights = np_to_th( + np.ones(weight_shape, dtype=np.float32) / float(n_pool) + ) + weights = weights.type_as(x) + if x.is_cuda: + weights = weights.cuda() + self._pool_weights = weights + + pooled = F.conv2d( + x, + self._pool_weights, + bias=None, + stride=self.stride, + dilation=self.dilation, + padding=self.padding, + groups=in_channels, + ) + return pooled + +class Ensure4d(nn.Module): + def forward(self, x): + while(len(x.shape) < 4): + x = x.unsqueeze(-1) + return + + + +class Deep4Net(nn.Sequential): + """Deep ConvNet model from Schirrmeister et al 2017. + Model described in [Schirrmeister2017]_. + Parameters + ---------- + in_chans : int + XXX + References + ---------- + .. [Schirrmeister2017] Schirrmeister, R. T., Springenberg, J. T., Fiederer, + L. D. J., Glasstetter, M., Eggensperger, K., Tangermann, M., Hutter, F. + & Ball, T. (2017). + Deep learning with convolutional neural networks for EEG decoding and + visualization. + Human Brain Mapping , Aug. 2017. + Online: http://dx.doi.org/10.1002/hbm.23730 + """ + + def __init__( + self, + in_chans, + n_classes, + input_window_samples, + final_conv_length, + n_filters_time=25, + n_filters_spat=25, + filter_time_length=10, + pool_time_length=1, + pool_time_stride=1, + n_filters_2=50, + filter_length_2=10, + n_filters_3=100, + filter_length_3=10, + n_filters_4=200, + filter_length_4=10, + first_nonlin=elu, + first_pool_mode="max", + first_pool_nonlin=identity, + later_nonlin=elu, + later_pool_mode="max", + later_pool_nonlin=identity, + drop_prob=0.5, + double_time_convs=False, + split_first_layer=True, + batch_norm=True, + batch_norm_alpha=0.1, + stride_before_pool=False, + ): + super().__init__() + if final_conv_length == "auto": + assert input_window_samples is not None + self.in_chans = in_chans + self.n_classes = n_classes + self.input_window_samples = input_window_samples + self.final_conv_length = final_conv_length + self.n_filters_time = n_filters_time + self.n_filters_spat = n_filters_spat + self.filter_time_length = filter_time_length + self.pool_time_length = pool_time_length + self.pool_time_stride = pool_time_stride + self.n_filters_2 = n_filters_2 + self.filter_length_2 = filter_length_2 + self.n_filters_3 = n_filters_3 + self.filter_length_3 = filter_length_3 + self.n_filters_4 = n_filters_4 + self.filter_length_4 = filter_length_4 + self.first_nonlin = first_nonlin + self.first_pool_mode = first_pool_mode + self.first_pool_nonlin = first_pool_nonlin + self.later_nonlin = later_nonlin + self.later_pool_mode = later_pool_mode + self.later_pool_nonlin = later_pool_nonlin + self.drop_prob = drop_prob + self.double_time_convs = double_time_convs + self.split_first_layer = split_first_layer + self.batch_norm = batch_norm + self.batch_norm_alpha = batch_norm_alpha + self.stride_before_pool = stride_before_pool + + if self.stride_before_pool: + conv_stride = self.pool_time_stride + pool_stride = 1 + else: + conv_stride = 1 + pool_stride = self.pool_time_stride + self.add_module("ensuredims", Ensure4d()) + pool_class_dict = dict(max=nn.MaxPool2d, mean=AvgPool2dWithConv) + first_pool_class = pool_class_dict[self.first_pool_mode] + later_pool_class = pool_class_dict[self.later_pool_mode] + if self.split_first_layer: + self.add_module("dimshuffle", Expression(transpose_time_to_spat)) + self.add_module( + "conv_time", + nn.Conv2d( + 1, + self.n_filters_time, + (self.filter_time_length, 1), + stride=1, + ), + ) + self.add_module( + "conv_spat", + nn.Conv2d( + self.n_filters_time, + self.n_filters_spat, + (1, self.in_chans), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + n_filters_conv = self.n_filters_spat + else: + self.add_module( + "conv_time", + nn.Conv2d( + self.in_chans, + self.n_filters_time, + (self.filter_time_length, 1), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + n_filters_conv = self.n_filters_time + if self.batch_norm: + self.add_module( + "bnorm", + nn.BatchNorm2d( + n_filters_conv, + momentum=self.batch_norm_alpha, + affine=True, + eps=1e-5, + ), + ) + self.add_module("conv_nonlin", Expression(self.first_nonlin)) + self.add_module( + "pool", + first_pool_class( + kernel_size=(self.pool_time_length, 1), stride=(pool_stride, 1) + ), + ) + self.add_module("pool_nonlin", Expression(self.first_pool_nonlin)) + + def add_conv_pool_block( + model, n_filters_before, n_filters, filter_length, block_nr + ): + suffix = "_{:d}".format(block_nr) + self.add_module("drop" + suffix, nn.Dropout(p=self.drop_prob)) + self.add_module( + "conv" + suffix, + nn.Conv2d( + n_filters_before, + n_filters, + (filter_length, 1), + stride=(conv_stride, 1), + bias=not self.batch_norm, + ), + ) + if self.batch_norm: + self.add_module( + "bnorm" + suffix, + nn.BatchNorm2d( + n_filters, + momentum=self.batch_norm_alpha, + affine=True, + eps=1e-5, + ), + ) + self.add_module("nonlin" + suffix, Expression(self.later_nonlin)) + + self.add_module( + "pool" + suffix, + later_pool_class( + kernel_size=(self.pool_time_length, 1), + stride=(pool_stride, 1), + ), + ) + self.add_module( + "pool_nonlin" + suffix, Expression(self.later_pool_nonlin) + ) + + add_conv_pool_block( + self, n_filters_conv, self.n_filters_2, self.filter_length_2, 2 + ) + add_conv_pool_block( + self, self.n_filters_2, self.n_filters_3, self.filter_length_3, 3 + ) + add_conv_pool_block( + self, self.n_filters_3, self.n_filters_4, self.filter_length_4, 4 + ) + + # self.add_module('drop_classifier', nn.Dropout(p=self.drop_prob)) + self.eval() + if self.final_conv_length == "auto": + out = self( + np_to_th( + np.ones( + (1, self.in_chans, self.input_window_samples, 1), + dtype=np.float32, + ) + ) + ) + n_out_time = out.cpu().data.numpy().shape[2] + self.final_conv_length = n_out_time + self.add_module( + "conv_classifier", + nn.Conv2d( + self.n_filters_4, + self.n_classes, + (self.final_conv_length, 1), + bias=True, + ), + ) + self.add_module("softmax", nn.LogSoftmax(dim=1)) + self.add_module("squeeze", Expression(squeeze_final_output)) + + # Initialization, xavier is same as in our paper... + # was default from lasagne + init.xavier_uniform_(self.conv_time.weight, gain=1) + # maybe no bias in case of no split layer and batch norm + if self.split_first_layer or (not self.batch_norm): + init.constant_(self.conv_time.bias, 0) + if self.split_first_layer: + init.xavier_uniform_(self.conv_spat.weight, gain=1) + if not self.batch_norm: + init.constant_(self.conv_spat.bias, 0) + if self.batch_norm: + init.constant_(self.bnorm.weight, 1) + init.constant_(self.bnorm.bias, 0) + param_dict = dict(list(self.named_parameters())) + for block_nr in range(2, 5): + conv_weight = param_dict["conv_{:d}.weight".format(block_nr)] + init.xavier_uniform_(conv_weight, gain=1) + if not self.batch_norm: + conv_bias = param_dict["conv_{:d}.bias".format(block_nr)] + init.constant_(conv_bias, 0) + else: + bnorm_weight = param_dict["bnorm_{:d}.weight".format(block_nr)] + bnorm_bias = param_dict["bnorm_{:d}.bias".format(block_nr)] + init.constant_(bnorm_weight, 1) + init.constant_(bnorm_bias, 0) + + init.xavier_uniform_(self.conv_classifier.weight, gain=1) + init.constant_(self.conv_classifier.bias, 0) + + +#### + +def train_test_subject_kfold(data, config, test_subject_id, kfold_k=0): + + ## init data + + # train_dataset, val_dataset, test_dataset = leave_one_subject_out(data, test_subject_id=test_subject_id, kfold_k=kfold_k) + train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k) + train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True) + val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False) + test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False) + + ## init model + + # eegnet = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length) + base_model = Deep4Net( + in_chans=num_channel, + n_classes=num_classes, + input_window_samples=signal_length, + final_conv_length="auto" + ) + + model = LightningModelClassifier( + optimizer=config.model.optimizer, + scheduler=config.model.scheduler, + optimizer_learning_rate=config.training.learning_rate, + scheduler_warmup_epochs=config.training.num_warmup_epochs, + ) + + model.build_model(model=base_model) + + ## train + + sub_dir = "sub"+ str(test_subject_id) +"_k"+ str(kfold_k) + logger_tb = TensorBoardLogger(save_dir="tensorboard_logs", name=config.run_name, sub_dir=sub_dir) + lr_monitor = LearningRateMonitor(logging_interval='epoch') + + trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor]) + trainer.fit(model, train_loader, val_loader) + + ## test + + result = trainer.test(dataloaders=test_loader, verbose=False) + test_acc = result[0]['test_acc_epoch'] + + return test_acc + +#### + +main_logger.write_to_log("Begin", break_line=True) + +test_results_acc = {} +means = [] + +def k_fold_train_test_all_subjects(): + + for test_subject_id in config.testing.test_subject_ids: + print() + print("running test_subject_id:", test_subject_id) + + if test_subject_id not in test_results_acc: + test_results_acc[test_subject_id] = [] + + for k in config.testing.kfolds: + + test_acc = train_test_subject_kfold(data, config, test_subject_id, kfold_k=k) + + test_results_acc[test_subject_id].append(test_acc) + + mean_acc = np.mean(test_results_acc[test_subject_id]) + means.append(mean_acc) + + this_result = { + "test_subject_id": test_subject_id, + "mean_acc": mean_acc, + "acc": test_results_acc[test_subject_id], + } + print(this_result) + main_logger.write_to_log(this_result) + +k_fold_train_test_all_subjects() + +mean_acc = np.mean(means) +print() +print("mean all", mean_acc) +main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) diff --git a/experiments/ssl_classifier/main_eegnet_hsssvep-run2-all-views.py b/experiments/ssl_classifier/main_eegnet_hsssvep-run2-all-views.py new file mode 100644 index 0000000..773fb9b --- /dev/null +++ b/experiments/ssl_classifier/main_eegnet_hsssvep-run2-all-views.py @@ -0,0 +1,193 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np +import torch +from torch.utils.data import DataLoader +import torch.nn as nn +import torch.nn.functional as F + +from pytorch_lightning import Trainer, seed_everything +from pytorch_lightning.callbacks import LearningRateMonitor +from pytorch_lightning.loggers import TensorBoardLogger + +import logging +logging.getLogger('lightning').setLevel(0) + +import warnings +warnings.filterwarnings('ignore') + +import pytorch_lightning +pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR) + +from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.nn.models import CompactEEGNet +from splearn.utils import Logger, Config +from splearn.nn.base import LightningModelClassifier + +#### + +config = { + "run_name": "main_eegnet_hsssvep-run2-all-views", + "data": { + "load_subject_ids": np.arange(1,36), + # "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], # AA paper + "selected_channels": ["PZ", "PO5", "PO3", "POz", "PO4", "PO6", "O1", "Oz", "O2"], # hsssvep paper + }, + "training": { + "num_epochs": 500, + "num_warmup_epochs": 50, + "learning_rate": 0.03, + "gpus": [0], + "batchsize": 256, + }, + "model": { + "optimizer": "adamw", + "scheduler": "cosine_with_warmup", + }, + "testing": { + "test_subject_ids": np.arange(1,36), + "kfolds": np.arange(0,3), + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +seed_everything(config.seed) + +#### + +def func_preprocessing(data): + data_x = data.data + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6) + start_t = 160 + end_t = start_t + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + + +def leave_one_subject_out(data, **kwargs): + + test_subject_id = kwargs["test_subject_id"] if "test_subject_id" in kwargs else 1 + + # get test data + # test_sub_idx = data.subject_ids.index(test_subject_id) + test_sub_idx = np.where(data.subject_ids == test_subject_id)[0][0] + selected_subject_data = data.data[test_sub_idx] + selected_subject_targets = data.targets[test_sub_idx] + test_dataset = PyTorchDataset(selected_subject_data, selected_subject_targets) + + # get train val data + indices = np.arange(data.data.shape[0]) + train_val_data = data.data[indices!=test_sub_idx, :, :, :] + train_val_data = train_val_data.reshape((train_val_data.shape[0]*train_val_data.shape[1], train_val_data.shape[2], train_val_data.shape[3])) + train_val_targets = data.targets[indices!=test_sub_idx, :] + train_val_targets = train_val_targets.reshape((train_val_targets.shape[0]*train_val_targets.shape[1])) + + train_dataset = PyTorchDataset(train_val_data, train_val_targets) + + return train_dataset, test_dataset + + + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + func_get_train_val_test_dataset=leave_one_subject_out, + verbose=True, +) + +print("Final data shape:", data.data.shape, data.targets.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +#### + +def train_test_subject(data, config, test_subject_id): + + ## init data + + train_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id) + train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True) + test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False) + + ## init model + + eegnet = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length) + + model = LightningModelClassifier( + optimizer=config.model.optimizer, + scheduler=config.model.scheduler, + optimizer_learning_rate=config.training.learning_rate, + scheduler_warmup_epochs=config.training.num_warmup_epochs, + ) + + model.build_model(model=eegnet) + + ## train + + sub_dir = "sub"+ str(test_subject_id) + logger_tb = TensorBoardLogger(save_dir="tensorboard_logs", name=config.run_name, sub_dir=sub_dir) + lr_monitor = LearningRateMonitor(logging_interval='epoch') + + trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor]) + trainer.fit(model, train_loader) + + ## test + + result = trainer.test(dataloaders=test_loader, verbose=False) + test_acc = result[0]['test_acc_epoch'] + + return test_acc + +#### + +main_logger.write_to_log("Begin", break_line=True) + +test_results_acc = {} +means = [] + +def k_fold_train_test_all_subjects(): + + for test_subject_id in config.testing.test_subject_ids: + print() + print("running test_subject_id:", test_subject_id) + + if test_subject_id not in test_results_acc: + test_results_acc[test_subject_id] = [] + + mean_acc = train_test_subject(data, config, test_subject_id) + + means.append(mean_acc) + + this_result = { + "test_subject_id": test_subject_id, + "mean_acc": mean_acc, + "acc": test_results_acc[test_subject_id], + } + print(this_result) + main_logger.write_to_log(this_result) + +k_fold_train_test_all_subjects() + +mean_acc = np.mean(means) +print() +print("mean all", mean_acc) +main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) diff --git a/experiments/ssl_classifier/main_eegnet_hsssvep.py b/experiments/ssl_classifier/main_eegnet_hsssvep.py new file mode 100644 index 0000000..e9ff136 --- /dev/null +++ b/experiments/ssl_classifier/main_eegnet_hsssvep.py @@ -0,0 +1,186 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np +import torch +from torch.utils.data import DataLoader +import torch.nn as nn +import torch.nn.functional as F + +from pytorch_lightning import Trainer, seed_everything +from pytorch_lightning.callbacks import LearningRateMonitor +from pytorch_lightning.loggers import TensorBoardLogger + +import logging +logging.getLogger('lightning').setLevel(0) + +import warnings +warnings.filterwarnings('ignore') + +import pytorch_lightning +pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR) + +from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.nn.models import CompactEEGNet +from splearn.utils import Logger, Config +from splearn.nn.base import LightningModelClassifier + +#### + +config = { + "run_name": "eeg_hsssvep_run2", + "data": { + "load_subject_ids": np.arange(1,36), + # "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], # AA paper + "selected_channels": ["PZ", "PO5", "PO3", "POz", "PO4", "PO6", "O1", "Oz", "O2"], # hsssvep paper + }, + "training": { + "num_epochs": 500, + "num_warmup_epochs": 50, + "learning_rate": 0.03, + "gpus": [0], + "batchsize": 256, + }, + "model": { + "optimizer": "adamw", + "scheduler": "cosine_with_warmup", + }, + "testing": { + "test_subject_ids": np.arange(1,36), + "kfolds": np.arange(0,3), + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +seed_everything(config.seed) + +#### + +# def func_preprocessing(data): +# data_x = data.data +# # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4'] +# selected_channels = config.data.selected_channels +# data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels) +# # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) +# data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6) +# start_t = 125 +# end_t = 125 + 250 +# data_x = data_x[:,:,:,start_t:end_t] +# data.set_data(data_x) + +def func_preprocessing(data): + data_x = data.data + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6) + start_t = 160 + end_t = start_t + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + verbose=True, +) + +print("Final data shape:", data.data.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +#### + +def train_test_subject_kfold(data, config, test_subject_id, kfold_k=0): + + ## init data + + # train_dataset, val_dataset, test_dataset = leave_one_subject_out(data, test_subject_id=test_subject_id, kfold_k=kfold_k) + train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k) + train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True) + val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False) + test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False) + + ## init model + + eegnet = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length) + + model = LightningModelClassifier( + optimizer=config.model.optimizer, + scheduler=config.model.scheduler, + optimizer_learning_rate=config.training.learning_rate, + scheduler_warmup_epochs=config.training.num_warmup_epochs, + ) + + model.build_model(model=eegnet) + + ## train + + sub_dir = "sub"+ str(test_subject_id) +"_k"+ str(kfold_k) + logger_tb = TensorBoardLogger(save_dir="tensorboard_logs", name=config.run_name, sub_dir=sub_dir) + lr_monitor = LearningRateMonitor(logging_interval='epoch') + + trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor]) + trainer.fit(model, train_loader, val_loader) + + ## test + + result = trainer.test(dataloaders=test_loader, verbose=False) + test_acc = result[0]['test_acc_epoch'] + + return test_acc + +#### + +main_logger.write_to_log("Begin", break_line=True) + +test_results_acc = {} +means = [] + +def k_fold_train_test_all_subjects(): + + for test_subject_id in config.testing.test_subject_ids: + print() + print("running test_subject_id:", test_subject_id) + + if test_subject_id not in test_results_acc: + test_results_acc[test_subject_id] = [] + + for k in config.testing.kfolds: + + test_acc = train_test_subject_kfold(data, config, test_subject_id, kfold_k=k) + + test_results_acc[test_subject_id].append(test_acc) + + mean_acc = np.mean(test_results_acc[test_subject_id]) + means.append(mean_acc) + + this_result = { + "test_subject_id": test_subject_id, + "mean_acc": mean_acc, + "acc": test_results_acc[test_subject_id], + } + print(this_result) + main_logger.write_to_log(this_result) + +k_fold_train_test_all_subjects() + +mean_acc = np.mean(means) +print() +print("mean all", mean_acc) +main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) diff --git a/experiments/ssl_classifier/main_ssl_hsssvep.py b/experiments/ssl_classifier/main_ssl_hsssvep.py new file mode 100644 index 0000000..7d9f95d --- /dev/null +++ b/experiments/ssl_classifier/main_ssl_hsssvep.py @@ -0,0 +1,240 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np +import torch +from torch.utils.data import DataLoader +import torch.nn as nn +import torch.nn.functional as F + +from pytorch_lightning import Trainer, seed_everything +from pytorch_lightning.callbacks import LearningRateMonitor +from pytorch_lightning.loggers import TensorBoardLogger + +import logging +logging.getLogger('lightning').setLevel(0) + +import warnings +warnings.filterwarnings('ignore') + +import pytorch_lightning +pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR) + +from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.nn.models import SSLClassifier, CompactEEGNet +from splearn.utils import Logger, Config + +#### + +config = { + "run_name": "ssl_hsssvep", + "data": { + "load_subject_ids": np.arange(1,36), + "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], + "num_views": 2, + }, + "training": { + "num_epochs": 500, + "num_warmup_epochs": 50, + "learning_rate": 0.03, + # "gpus": torch.cuda.device_count(), + "gpus": [0], + "batchsize": 256, + }, + "model": { + "projection_size": 1024, + "optimizer": "adamw", + "scheduler": "cosine_with_warmup", + }, + "testing": { + "test_subject_ids": np.arange(1,36), + "kfolds": np.arange(0,3), + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +seed_everything(config.seed) + +#### + +def func_preprocessing(data): + data_x = data.data + # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4'] + selected_channels = config.data.selected_channels + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6) + start_t = 125 + end_t = 125 + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + +def leave_one_subject_out(data, **kwargs): + + test_subject_id = kwargs["test_subject_id"] if "test_subject_id" in kwargs else 1 + kfold_k = kwargs["kfold_k"] if "kfold_k" in kwargs else 0 + kfold_split = kwargs["kfold_split"] if "kfold_split" in kwargs else 3 + + # get test data + # test_sub_idx = data.subject_ids.index(test_subject_id) + test_sub_idx = np.where(data.subject_ids == test_subject_id)[0][0] + selected_subject_data = data.data[test_sub_idx] + selected_subject_targets = data.targets[test_sub_idx] + test_dataset = PyTorchDataset(selected_subject_data, selected_subject_targets) + + # get train val data + indices = np.arange(data.data.shape[0]) + train_val_data = data.data[indices!=test_sub_idx, :, :, :] + + train_val_data = train_val_data.reshape((train_val_data.shape[0]*train_val_data.shape[1], train_val_data.shape[2], train_val_data.shape[3])) + train_val_targets = data.targets[indices!=test_sub_idx, :] + train_val_targets = train_val_targets.reshape((train_val_targets.shape[0]*train_val_targets.shape[1])) + + # train test split + (X_train, y_train), (X_val, y_val) = data.dataset_split_stratified(train_val_data, train_val_targets, k=kfold_k, n_splits=kfold_split) + # print("X_train.shape, X_val.shape", X_train.shape, X_val.shape, y_train.shape, y_val.shape) + + # ssl + num_views = config.data.num_views + val_num_views = 1 + + X_train_ssl_view1 = X_train + X_train_ssl_view1 = np.tile(X_train_ssl_view1, [num_views,1,1]) + X_val_ssl_view1 = X_val + X_val_ssl_view1 = np.tile(X_val_ssl_view1, [val_num_views,1,1]) + # print("X_train_ssl_view1.shape, X_val_ssl_view1.shape", X_train_ssl_view1.shape, X_val_ssl_view1.shape) + + y_train = np.tile(y_train, [num_views]) + y_val = np.tile(y_val, [val_num_views]) + + # create views + X_train_ssl_view2 = np.zeros((num_views, X_train.shape[0], X_train.shape[1], X_train.shape[2])) + X_val_ssl_view2 = np.zeros((val_num_views, X_val.shape[0], X_val.shape[1], X_val.shape[2])) + # print("X_train_ssl_view2.shape, X_val_ssl_view2.shape", X_train_ssl_view2.shape, X_val_ssl_view2.shape) + + for view_i in range(num_views): + X_train_ssl_view2_subset = np.roll(X_train, (view_i+1), 0) + X_train_ssl_view2[view_i] = X_train_ssl_view2_subset + + if view_i < val_num_views: + X_val_ssl_view2_subset = np.roll(X_val, (view_i+1), 0) + X_val_ssl_view2[view_i] = X_val_ssl_view2_subset + + X_train_ssl_view2 = X_train_ssl_view2.reshape((X_train_ssl_view2.shape[0]*X_train_ssl_view2.shape[1], X_train_ssl_view2.shape[2], X_train_ssl_view2.shape[3])) + X_val_ssl_view2 = X_val_ssl_view2.reshape((X_val_ssl_view2.shape[0]*X_val_ssl_view2.shape[1], X_val_ssl_view2.shape[2], X_val_ssl_view2.shape[3])) + # print("X_train_ssl_view2.shape, X_val_ssl_view2.shape", X_train_ssl_view2.shape, X_val_ssl_view2.shape) + + # create dataset + + train_dataset = PyTorchDataset2Views(X_train_ssl_view1, X_train_ssl_view2, y_train) + val_dataset = PyTorchDataset2Views(X_val_ssl_view1, X_val_ssl_view2, y_val) + + return train_dataset, val_dataset, test_dataset + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + func_get_train_val_test_dataset=leave_one_subject_out, + verbose=True, +) + +print("Final data shape:", data.data.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +#### + +def train_test_subject_kfold(data, config, test_subject_id, kfold_k=0): + + ## init data + + # train_dataset, val_dataset, test_dataset = leave_one_subject_out(data, test_subject_id=test_subject_id, kfold_k=kfold_k) + train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k) + train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True) + val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False) + test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False) + + ## init model + + eegnet = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length) + + model = SSLClassifier( + optimizer=config.model.optimizer, + scheduler=config.model.scheduler, + optimizer_learning_rate=config.training.learning_rate, + scheduler_warmup_epochs=config.training.num_warmup_epochs, + ) + + model.build_model(model=eegnet, projection_size=config.model.projection_size) + + ## train + + sub_dir = "sub"+ str(test_subject_id) +"_k"+ str(kfold_k) + logger_tb = TensorBoardLogger(save_dir="tensorboard_logs", name=config.run_name, sub_dir=sub_dir) + lr_monitor = LearningRateMonitor(logging_interval='epoch') + + trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor]) + trainer.fit(model, train_loader, val_loader) + + ## test + + result = trainer.test(dataloaders=test_loader, verbose=False) + test_acc = result[0]['test_acc_epoch'] + + return test_acc + +#### + +main_logger.write_to_log("Begin", break_line=True) + +test_results_acc = {} +means = [] + +def k_fold_train_test_all_subjects(): + + for test_subject_id in config.testing.test_subject_ids: + print() + print("running test_subject_id:", test_subject_id) + + if test_subject_id not in test_results_acc: + test_results_acc[test_subject_id] = [] + + for k in config.testing.kfolds: + + test_acc = train_test_subject_kfold(data, config, test_subject_id, kfold_k=k) + + test_results_acc[test_subject_id].append(test_acc) + + mean_acc = np.mean(test_results_acc[test_subject_id]) + means.append(mean_acc) + + this_result = { + "test_subject_id": test_subject_id, + "mean_acc": mean_acc, + "acc": test_results_acc[test_subject_id], + } + print(this_result) + main_logger.write_to_log(this_result) + +k_fold_train_test_all_subjects() + +mean_acc = np.mean(means) +print() +print("mean all", mean_acc) +main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) diff --git a/experiments/ssl_classifier/main_trca_hsssvep.py b/experiments/ssl_classifier/main_trca_hsssvep.py new file mode 100644 index 0000000..e05dda9 --- /dev/null +++ b/experiments/ssl_classifier/main_trca_hsssvep.py @@ -0,0 +1,103 @@ +import os +cwd = os.getcwd() +import sys +path = os.path.join(cwd, "..\\..\\") +sys.path.append(path) + +import numpy as np + +from splearn.data import MultipleSubjects, HSSSVEP +from splearn.filter.butterworth import butter_bandpass_filter +from splearn.filter.notch import notch_filter +from splearn.filter.channels import pick_channels +from splearn.utils import Logger, Config +from splearn.cross_decomposition.trca import TRCA +from splearn.cross_validate.leave_one_out import block_evaluation +#### + +config = { + "run_name": "trca_hsssvep_run2", + "data": { + "load_subject_ids": np.arange(1,36), + # "selected_channels": ["PO8", "PZ", "PO7", "PO4", "POz", "PO3", "O2", "Oz", "O1"], # AA paper + "selected_channels": ["PZ", "PO5", "PO3", "POz", "PO4", "PO6", "O1", "Oz", "O2"], # hsssvep paper + }, + "seed": 1234 +} + +main_logger = Logger(filename_postfix=config["run_name"]) +main_logger.write_to_log("Config") +main_logger.write_to_log(config) + +config = Config(config) + +#### + +""" +def func_preprocessing(data): + data_x = data.data + # selected_channels = ['P7','P3','PZ','P4','P8','O1','Oz','O2','P1','P2','POz','PO3','PO4'] + selected_channels = config.data.selected_channels + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=4, highcut=75, sampling_rate=data.sampling_rate, order=6) + start_t = 125 + end_t = 125 + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) +""" + +def func_preprocessing(data): + data_x = data.data + data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels) + # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0) + data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6) + start_t = 160 + end_t = start_t + 250 + data_x = data_x[:,:,:,start_t:end_t] + data.set_data(data_x) + +data = MultipleSubjects( + dataset=HSSSVEP, + root=os.path.join(path, "../data/hsssvep"), + subject_ids=config.data.load_subject_ids, + func_preprocessing=func_preprocessing, + verbose=True, +) + +print("Final data shape:", data.data.shape) + +num_channel = data.data.shape[2] +num_classes = 40 +signal_length = data.data.shape[3] + +#### + +from sklearn.metrics import accuracy_score + +def leave_one_block_evaluation(classifier, X, Y, block_seq_labels=None): + test_results_acc = [] + blocks, targets, channels, samples = X.shape + + main_logger.write_to_log("Begin", break_line=True) + + for block_i in range(blocks): + test_acc = block_evaluation(classifier, X, Y, block_i, block_seq_labels[block_i] if block_seq_labels is not None else None) + test_results_acc.append(test_acc) + + this_result = { + "test_subject_id": block_i+1, + "acc": test_acc, + } + + main_logger.write_to_log(this_result) + + mean_acc = np.array(test_results_acc).mean().round(3)*100 + + print(f'Mean test accuracy: {mean_acc}%') + + main_logger.write_to_log("Mean acc: "+str(mean_acc), break_line=True) + + +trca_classifier = TRCA(sampling_rate=data.sampling_rate) +leave_one_block_evaluation(classifier=trca_classifier, X=data.data, Y=data.targets) diff --git a/experiments/two-pathway/Untitled.ipynb b/experiments/two-pathway/Untitled.ipynb new file mode 100644 index 0000000..2978758 --- /dev/null +++ b/experiments/two-pathway/Untitled.ipynb @@ -0,0 +1,1048 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "cwd = os.getcwd()\n", + "import sys\n", + "path = os.path.join(cwd, \"..\\\\..\\\\\")\n", + "sys.path.append(path)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "from torch.utils.data import DataLoader\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "\n", + "from pytorch_lightning import Trainer, seed_everything\n", + "from pytorch_lightning.callbacks import LearningRateMonitor\n", + "from pytorch_lightning.loggers import TensorBoardLogger\n", + "\n", + "import logging\n", + "logging.getLogger('lightning').setLevel(0)\n", + "\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "import pytorch_lightning\n", + "pytorch_lightning.utilities.distributed.log.setLevel(logging.ERROR)\n", + "\n", + "from splearn.data import MultipleSubjects, PyTorchDataset, PyTorchDataset2Views, HSSSVEP\n", + "from splearn.filter.butterworth import butter_bandpass_filter\n", + "from splearn.filter.notch import notch_filter\n", + "from splearn.filter.channels import pick_channels\n", + "from splearn.nn.models import CompactEEGNet\n", + "from splearn.utils import Logger, Config\n", + "from splearn.nn.base import LightningModelClassifier" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Global seed set to 1234\n" + ] + }, + { + "data": { + "text/plain": [ + "1234" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "config = {\n", + " \"run_name\": \"eeg_hsssvep_run2\",\n", + " \"data\": {\n", + " \"load_subject_ids\": np.arange(1,36),\n", + " # \"selected_channels\": [\"PO8\", \"PZ\", \"PO7\", \"PO4\", \"POz\", \"PO3\", \"O2\", \"Oz\", \"O1\"], # AA paper\n", + " \"selected_channels\": [\"PZ\", \"PO5\", \"PO3\", \"POz\", \"PO4\", \"PO6\", \"O1\", \"Oz\", \"O2\"], # hsssvep paper\n", + " },\n", + " \"training\": {\n", + " \"num_epochs\": 500,\n", + " \"num_warmup_epochs\": 50,\n", + " \"learning_rate\": 0.03,\n", + " \"gpus\": [0],\n", + " \"batchsize\": 256,\n", + " },\n", + " \"model\": {\n", + " \"optimizer\": \"adamw\",\n", + " \"scheduler\": \"cosine_with_warmup\",\n", + " },\n", + " \"testing\": {\n", + " \"test_subject_ids\": np.arange(33,34),\n", + " \"kfolds\": np.arange(0,3),\n", + " },\n", + " \"seed\": 1234\n", + "}\n", + "\n", + "main_logger = Logger(filename_postfix=config[\"run_name\"])\n", + "main_logger.write_to_log(\"Config\")\n", + "main_logger.write_to_log(config)\n", + "\n", + "config = Config(config)\n", + "\n", + "seed_everything(config.seed)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Load subject: 1\n", + "Load subject: 2\n", + "Load subject: 3\n", + "Load subject: 4\n", + "Load subject: 5\n", + "Load subject: 6\n", + "Load subject: 7\n", + "Load subject: 8\n", + "Load subject: 9\n", + "Load subject: 10\n", + "Load subject: 11\n", + "Load subject: 12\n", + "Load subject: 13\n", + "Load subject: 14\n", + "Load subject: 15\n", + "Load subject: 16\n", + "Load subject: 17\n", + "Load subject: 18\n", + "Load subject: 19\n", + "Load subject: 20\n", + "Load subject: 21\n", + "Load subject: 22\n", + "Load subject: 23\n", + "Load subject: 24\n", + "Load subject: 25\n", + "Load subject: 26\n", + "Load subject: 27\n", + "Load subject: 28\n", + "Load subject: 29\n", + "Load subject: 30\n", + "Load subject: 31\n", + "Load subject: 32\n", + "Load subject: 33\n", + "Load subject: 34\n", + "Load subject: 35\n" + ] + } + ], + "source": [ + "def func_preprocessing(data):\n", + " data_x = data.data\n", + " data_x = pick_channels(data_x, channel_names=data.channel_names, selected_channels=config.data.selected_channels)\n", + " # data_x = notch_filter(data_x, sampling_rate=data.sampling_rate, notch_freq=50.0)\n", + " data_x = butter_bandpass_filter(data_x, lowcut=7, highcut=90, sampling_rate=data.sampling_rate, order=6)\n", + " start_t = 160\n", + " end_t = start_t + 250\n", + " data_x = data_x[:,:,:,start_t:end_t]\n", + " data.set_data(data_x)\n", + "\n", + "data = MultipleSubjects(\n", + " dataset=HSSSVEP, \n", + " root=os.path.join(path, \"../data/hsssvep\"), \n", + " subject_ids=config.data.load_subject_ids, \n", + " func_preprocessing=func_preprocessing,\n", + " verbose=True, \n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Final data shape: (35, 240, 9, 250)\n" + ] + } + ], + "source": [ + "print(\"Final data shape:\", data.data.shape)\n", + "\n", + "num_channel = data.data.shape[2]\n", + "num_classes = 40\n", + "signal_length = data.data.shape[3]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "test_subject_id=1\n", + "kfold_k=1\n", + "\n", + "train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(320, 9, 250)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_dataset.data.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [], + "source": [ + "def train_test_subject_kfold(data, config, test_subject_id, kfold_k=0):\n", + " \n", + " ## init data\n", + " \n", + " # train_dataset, val_dataset, test_dataset = leave_one_subject_out(data, test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + " train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + " train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + " val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + " test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + " ## init model\n", + "\n", + " eegnet = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length)\n", + "\n", + " model = LightningModelClassifier(\n", + " optimizer=config.model.optimizer,\n", + " scheduler=config.model.scheduler,\n", + " optimizer_learning_rate=config.training.learning_rate,\n", + " scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + " )\n", + " \n", + " model.build_model(model=eegnet)\n", + "\n", + " ## train\n", + "\n", + " sub_dir = \"sub\"+ str(test_subject_id) +\"_k\"+ str(kfold_k)\n", + " logger_tb = TensorBoardLogger(save_dir=\"tensorboard_logs\", name=config.run_name, sub_dir=sub_dir)\n", + " lr_monitor = LearningRateMonitor(logging_interval='epoch')\n", + "\n", + " trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor])\n", + " trainer.fit(model, train_loader, val_loader)\n", + " \n", + " ## test\n", + " \n", + " result = trainer.test(dataloaders=test_loader, verbose=False)\n", + " test_acc = result[0]['test_acc_epoch']\n", + " \n", + " return test_acc" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "running test_subject_id: 1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 1, 'acc': [0.5916666388511658]}\n", + "\n", + "mean all 0.5916666388511658\n" + ] + } + ], + "source": [ + "main_logger.write_to_log(\"Begin\", break_line=True)\n", + "\n", + "test_results_acc = {}\n", + "means = []\n", + "\n", + "def k_fold_train_test_all_subjects():\n", + " \n", + " for test_subject_id in config.testing.test_subject_ids:\n", + " print()\n", + " print(\"running test_subject_id:\", test_subject_id)\n", + " \n", + " if test_subject_id not in test_results_acc:\n", + " test_results_acc[test_subject_id] = []\n", + " \n", + " test_acc = train_test_subject_kfold(data, config, test_subject_id, kfold_k=0)\n", + " \n", + " test_results_acc[test_subject_id].append(test_acc)\n", + " means.append(test_acc)\n", + " \n", + " this_result = {\n", + " \"test_subject_id\": test_subject_id,\n", + " \"acc\": test_results_acc[test_subject_id],\n", + " } \n", + " print(this_result)\n", + " main_logger.write_to_log(this_result)\n", + "\n", + " \n", + "k_fold_train_test_all_subjects()\n", + "\n", + "mean_acc = np.mean(means)\n", + "print()\n", + "print(\"mean all\", mean_acc)\n", + "main_logger.write_to_log(\"Mean acc: \"+str(mean_acc), break_line=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from splearn.nn.modules.conv2d import Conv2d\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# x = torch.randn(320, 9, 250)\n", + "\n", + "# print(x.shape)\n", + "\n", + "\n", + "# model = CompactEEGNet(num_channel=num_channel, num_classes=num_classes, signal_length=signal_length)\n", + "# y = model(x)\n", + "# print(y.shape)\n", + "\n", + "# model = Model()\n", + "# y = model(x)\n", + "# print(y.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# class SlowFast(nn.Module):\n", + "# def __init__(self, block=None, layers=[3, 4, 6, 3], class_num=10, dropout=0.5):\n", + "# super(SlowFast, self).__init__()\n", + "\n", + "# in_channels = 9\n", + "# filters = [32, 64, 128]\n", + "# kernel_size = (1, 5)\n", + "\n", + "# self.fast_conv1 = Conv2d(\n", + "# in_channels, filters[0], kernel_size=kernel_size, bias=False)\n", + "# self.fast_bn1 = nn.BatchNorm2d(filters[0])\n", + "# self.fast_conv2 = Conv2d(\n", + "# filters[0], filters[1], kernel_size=kernel_size, bias=False)\n", + "# self.fast_bn2 = nn.BatchNorm2d(filters[1])\n", + "# self.fast_conv3 = Conv2d(\n", + "# filters[1], filters[2], kernel_size=kernel_size, bias=False)\n", + "# self.fast_bn3 = nn.BatchNorm2d(filters[2])\n", + "\n", + "# self.fast_relu = nn.ReLU(inplace=True)\n", + "# self.fast_maxpool = nn.MaxPool2d(kernel_size=(1, 2), stride=(1, 1))\n", + "\n", + "# self.lateral_p1 = Conv2d(\n", + "# filters[0], filters[0], kernel_size=(1, 1), stride=(1, 2), bias=False)\n", + "# self.lateral_p2 = Conv2d(\n", + "# filters[1], filters[1], kernel_size=(1, 1), stride=(1, 2), bias=False)\n", + "# self.lateral_p3 = Conv2d(\n", + "# filters[2], filters[2], kernel_size=(1, 1), stride=(1, 2), bias=False)\n", + " \n", + "# self.identity1 = Conv2d(\n", + "# in_channels, filters[0], kernel_size=(1, 1), stride=(1, 1), bias=False)\n", + "# self.identity2 = Conv2d(\n", + "# filters[0], filters[1], kernel_size=(1, 1), stride=(1, 1), bias=False)\n", + "# self.identity3 = Conv2d(\n", + "# filters[1], filters[2], kernel_size=(1, 1), stride=(1, 1), bias=False)\n", + "\n", + "# self.slow_conv1 = Conv2d(\n", + "# in_channels, filters[0], kernel_size=kernel_size, stride=(1, 2), padding=(0, 3), bias=False)\n", + "# self.slow_bn1 = nn.BatchNorm2d(filters[0])\n", + "# self.slow_conv2 = Conv2d(\n", + "# filters[1], filters[1], kernel_size=kernel_size, stride=(1, 2), padding=(0, 3), bias=False)\n", + "# self.slow_bn2 = nn.BatchNorm2d(filters[1])\n", + "# self.slow_conv3 = Conv2d(\n", + "# filters[2], filters[2], kernel_size=kernel_size, stride=(1, 2), padding=(0, 3), bias=False)\n", + "# self.slow_bn3 = nn.BatchNorm2d(filters[2])\n", + "\n", + "# self.slow_relu = nn.ReLU(inplace=True)\n", + "# self.slow_maxpool = nn.MaxPool2d(kernel_size=(1, 2), stride=(1, 1))\n", + "\n", + "# def forward(self, input):\n", + "# input = torch.unsqueeze(input, 2)\n", + "# fast, lateral = self.FastPath(input)\n", + "# slow = self.SlowPath(input, lateral)\n", + "# return fast, slow\n", + "\n", + "# def SlowPath(self, input, lateral):\n", + "# x = self.slow_conv1(input)\n", + "# x = self.slow_bn1(x)\n", + "# x = self.slow_relu(x)\n", + "# x = self.slow_maxpool(x)\n", + "# # print(\"slow x\", x.shape, lateral[0].shape)\n", + "# x = torch.cat([x, lateral[0]], dim=1)\n", + " \n", + "# # print(\"slow x\", x.shape)\n", + "# x = self.slow_conv2(x)\n", + "# x = self.slow_bn2(x)\n", + "# x = self.slow_relu(x)\n", + "# x = self.slow_maxpool(x)\n", + "# # print(\"slow x\", x.shape, lateral[1].shape)\n", + "# x = torch.cat([x, lateral[1]], dim=1)\n", + "\n", + "# # print(\"slow x\", x.shape)\n", + "# x = self.slow_conv3(x)\n", + "# x = self.slow_bn3(x)\n", + "# x = self.slow_relu(x)\n", + "# x = self.slow_maxpool(x)\n", + "# # print(\"slow x\", x.shape, lateral[2].shape)\n", + "# x = torch.cat([x, lateral[2]], dim=1)\n", + "\n", + "# return x\n", + "\n", + "# def FastPath(self, input):\n", + "# lateral = []\n", + "# x1 = self.fast_conv1(input)\n", + "# x1 = self.fast_bn1(x1)\n", + "# x1 = self.fast_relu(x1)\n", + "# x1 = self.identity1(input) + x1\n", + "# # pool1 = self.fast_maxpool(x1)\n", + "# # print(\"pool1\", pool1.shape)\n", + "# # print(\"x1\", x1.shape)\n", + "# lateral_p1 = self.lateral_p1(x1)\n", + "# lateral.append(lateral_p1)\n", + "# # print(\"lateral_p1\", lateral_p1.shape)\n", + "\n", + "# x2 = self.fast_conv2(lateral_p1)\n", + "# x2 = self.fast_bn2(x2)\n", + "# x2 = self.fast_relu(x2)\n", + "# x2 = self.identity2(lateral_p1) + x2\n", + "# # print(lateral_p1.shape, x2.shape)\n", + "# # x2 = lateral_p1 + x2\n", + "# # pool2 = self.fast_maxpool(x2)\n", + "# # print(\"pool2\", pool2.shape)\n", + "# # print(\"x2\", x2.shape)\n", + "# lateral_p2 = self.lateral_p2(x2)\n", + "# # print(\"lateral_p2\", lateral_p2.shape)\n", + "# lateral.append(lateral_p2)\n", + "\n", + "# x3 = self.fast_conv3(lateral_p2)\n", + "# x3 = self.fast_bn3(x3)\n", + "# x3 = self.fast_relu(x3)\n", + "# x3 = self.identity3(lateral_p2) + x3\n", + "# # x3 = lateral_p2 + x3\n", + "# # pool3 = self.fast_maxpool(x3)\n", + "# # print(\"pool3\", pool3.shape)\n", + "# # print(\"x3\", x3.shape)\n", + "# lateral_p3 = self.lateral_p3(x3)\n", + "# # print(\"lateral_p3\", lateral_p3.shape)\n", + "# lateral.append(lateral_p3)\n", + "\n", + "# return lateral_p3, lateral\n", + "\n", + "\n", + "# model = SlowFast()\n", + "# fast, slow = model(x)\n", + "# print(\"fast\", fast.shape)\n", + "# print(\"slow\", slow.shape)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "slow 1 torch.Size([320, 32, 4, 112])\n", + "slow fusion1 torch.Size([320, 32, 4, 112]) torch.Size([320, 8, 32, 112])\n" + ] + }, + { + "ename": "RuntimeError", + "evalue": "Given groups=1, weight of size [32, 4, 1, 3], expected input[320, 8, 32, 112] to have 4 channels, but got 8 channels instead", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 147\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 148\u001b[0m \u001b[0mmodel\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mSlowFast\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 149\u001b[1;33m \u001b[0mfast\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mslow\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mmodel\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 150\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 151\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"fast\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mfast\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Roaming\\Python\\Python38\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 1100\u001b[0m if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks\n\u001b[0;32m 1101\u001b[0m or _global_forward_hooks or _global_forward_pre_hooks):\n\u001b[1;32m-> 1102\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mforward_call\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1103\u001b[0m \u001b[1;31m# Do not call functions when jit is used\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1104\u001b[0m \u001b[0mfull_backward_hooks\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnon_full_backward_hooks\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, input)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[1;31m# print(\"input.shape\", input.shape)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[0mfast\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlateral\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFastPath\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mslow\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mSlowPath\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlateral\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mfast\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mslow\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 104\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36mSlowPath\u001b[1;34m(self, input, lateral)\u001b[0m\n\u001b[0;32m 127\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 128\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"slow fusion1\"\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mx1\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlateral\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 129\u001b[1;33m \u001b[0mx1\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfusion1\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mx1\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlateral\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 130\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 131\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Roaming\\Python\\Python38\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 1100\u001b[0m if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks\n\u001b[0;32m 1101\u001b[0m or _global_forward_hooks or _global_forward_pre_hooks):\n\u001b[1;32m-> 1102\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mforward_call\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1103\u001b[0m \u001b[1;31m# Do not call functions when jit is used\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1104\u001b[0m \u001b[0mfull_backward_hooks\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnon_full_backward_hooks\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, x)\u001b[0m\n\u001b[0;32m 67\u001b[0m \u001b[0mx_f\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 68\u001b[0m \u001b[1;31m# print(888, x_f.shape) # 888 torch.Size([320, 64, 32, 56])\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 69\u001b[1;33m \u001b[0mfuse\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mconv_fast_to_slow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx_f\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 70\u001b[0m \u001b[0mfuse\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbn\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mfuse\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 71\u001b[0m \u001b[0mfuse\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mactivation\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mfuse\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Roaming\\Python\\Python38\\site-packages\\torch\\nn\\modules\\module.py\u001b[0m in \u001b[0;36m_call_impl\u001b[1;34m(self, *input, **kwargs)\u001b[0m\n\u001b[0;32m 1100\u001b[0m if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks\n\u001b[0;32m 1101\u001b[0m or _global_forward_hooks or _global_forward_pre_hooks):\n\u001b[1;32m-> 1102\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mforward_call\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1103\u001b[0m \u001b[1;31m# Do not call functions when jit is used\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1104\u001b[0m \u001b[0mfull_backward_hooks\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnon_full_backward_hooks\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Roaming\\Python\\Python38\\site-packages\\torch\\nn\\modules\\conv.py\u001b[0m in \u001b[0;36mforward\u001b[1;34m(self, input)\u001b[0m\n\u001b[0;32m 444\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 445\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mforward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minput\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mTensor\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m->\u001b[0m \u001b[0mTensor\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 446\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_conv_forward\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mweight\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbias\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 447\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 448\u001b[0m \u001b[1;32mclass\u001b[0m \u001b[0mConv3d\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0m_ConvNd\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\AppData\\Roaming\\Python\\Python38\\site-packages\\torch\\nn\\modules\\conv.py\u001b[0m in \u001b[0;36m_conv_forward\u001b[1;34m(self, input, weight, bias)\u001b[0m\n\u001b[0;32m 440\u001b[0m \u001b[0mweight\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mbias\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstride\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 441\u001b[0m _pair(0), self.dilation, self.groups)\n\u001b[1;32m--> 442\u001b[1;33m return F.conv2d(input, weight, bias, self.stride,\n\u001b[0m\u001b[0;32m 443\u001b[0m self.padding, self.dilation, self.groups)\n\u001b[0;32m 444\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mRuntimeError\u001b[0m: Given groups=1, weight of size [32, 4, 1, 3], expected input[320, 8, 32, 112] to have 4 channels, but got 8 channels instead" + ] + } + ], + "source": [ + "class Block(nn.Module):\n", + " def __init__(self, in_channels, out_channels, kernel_size, stride=1):\n", + " super(Block, self).__init__()\n", + " \n", + " self.conv = Conv2d(\n", + " in_channels, out_channels, kernel_size=kernel_size, stride=stride, bias=False)\n", + " self.bn = nn.BatchNorm2d(out_channels)\n", + " self.relu = nn.ReLU(inplace=True)\n", + " \n", + " def forward(self, input):\n", + " x = self.conv(input)\n", + " x = self.bn(x)\n", + " x = self.relu(x)\n", + " \n", + " return x\n", + "\n", + "class ResBlock(nn.Module):\n", + " def __init__(self, in_channels, hidden_channels, out_channels, kernel_sizes):\n", + " super(ResBlock, self).__init__()\n", + " \n", + " self.conv1 = Block(in_channels=in_channels, out_channels=hidden_channels, kernel_size=kernel_sizes[0])\n", + " self.conv2 = Block(in_channels=hidden_channels, out_channels=hidden_channels, kernel_size=kernel_sizes[1])\n", + " self.conv3 = Block(in_channels=hidden_channels, out_channels=out_channels, kernel_size=kernel_sizes[2])\n", + " self.conv_fusion = Conv2d(\n", + " in_channels=in_channels, out_channels=out_channels, kernel_size=(1,1), bias=False)\n", + " \n", + " def forward(self, input):\n", + " x = self.conv1(input)\n", + " # print(\"ResBlock 1\", x.shape)\n", + " x = self.conv2(x)\n", + " # print(\"ResBlock 2\", x.shape)\n", + " x = self.conv3(x)\n", + " # print(\"ResBlock 3\", x.shape)\n", + " \n", + " shortcut = self.conv_fusion(input)\n", + " # print(\"shortcut\", shortcut.shape)\n", + " \n", + " x = x + shortcut\n", + " return x\n", + "\n", + "class Fusion(nn.Module):\n", + " def __init__(self, fusion_dim_in, conv_kernel_size, conv_stride, slowfast_channel_reduction_ratio=8, conv_fusion_channel_ratio=8):\n", + " super(Fusion, self).__init__()\n", + " \n", + " conv_dim_in = fusion_dim_in // slowfast_channel_reduction_ratio\n", + " norm_eps = 1e-5\n", + " norm_momentum = 0.1\n", + " \n", + " self.conv_fast_to_slow = nn.Conv2d(\n", + " conv_dim_in,\n", + " int(conv_dim_in * conv_fusion_channel_ratio),\n", + " kernel_size=conv_kernel_size,\n", + " stride=conv_stride,\n", + " padding=[k_size // 2 for k_size in conv_kernel_size],\n", + " bias=False,\n", + " )\n", + " \n", + " self.bn = nn.BatchNorm2d(\n", + " num_features=conv_dim_in * conv_fusion_channel_ratio,\n", + " eps=norm_eps,\n", + " momentum=norm_momentum,\n", + " )\n", + " self.activation = nn.ReLU()\n", + " \n", + " def forward(self, x):\n", + " x_s = x[0]\n", + " x_f = x[1]\n", + " # print(888, x_f.shape) # 888 torch.Size([320, 64, 32, 56])\n", + " fuse = self.conv_fast_to_slow(x_f)\n", + " fuse = self.bn(fuse)\n", + " fuse = self.activation(fuse)\n", + " x_s_fuse = torch.cat([x_s, fuse], 1)\n", + " return x_s_fuse\n", + "\n", + "\n", + "class SlowFast(nn.Module):\n", + " def __init__(self):\n", + " super(SlowFast, self).__init__()\n", + " in_channels = 1\n", + " \n", + " self.fast_conv1 = Block(in_channels=in_channels, out_channels=8, kernel_size=(5,7), stride=(2,2))\n", + " self.fast_maxpool = nn.MaxPool2d(kernel_size=(1,3), stride=(1,2), padding=(0,1))\n", + " \n", + " self.fast_conv2 = ResBlock(in_channels=8, hidden_channels=8, out_channels=32, kernel_sizes=[(3,1),(1,3),(1,1)])\n", + " self.fast_conv3 = ResBlock(in_channels=32, hidden_channels=16, out_channels=64, kernel_sizes=[(3,1),(1,3),(1,1)])\n", + " \n", + " self.slow_conv1 = Block(in_channels=in_channels, out_channels=32, kernel_size=(1,7), stride=(16,2))\n", + " self.slow_maxpool = nn.MaxPool2d(kernel_size=(1,3), stride=(1,2), padding=(0,1))\n", + " \n", + " self.slow_conv2 = ResBlock(in_channels=64, hidden_channels=64, out_channels=256, kernel_sizes=[(1,1),(1,3),(1,1)])\n", + " self.slow_conv3 = ResBlock(in_channels=256, hidden_channels=128, out_channels=512, kernel_sizes=[(1,1),(1,3),(1,1)])\n", + " \n", + " self.fusion1 = Fusion(fusion_dim_in=32, conv_kernel_size=(1,3), conv_stride=(8,1))\n", + " self.fusion2 = Fusion(fusion_dim_in=128, conv_kernel_size=(1,3), conv_stride=(8,1))\n", + " self.fusion3 = Fusion(fusion_dim_in=256, conv_kernel_size=(1,3), conv_stride=(8,1))\n", + " \n", + " \n", + " def forward(self, input):\n", + " # input = torch.unsqueeze(input, 3)\n", + " # print(\"input.shape\", input.shape)\n", + " fast, lateral = self.FastPath(input) \n", + " slow = self.SlowPath(input, lateral)\n", + " return fast, slow\n", + "\n", + " \n", + " def FastPath(self, input):\n", + " lateral = []\n", + " x1 = self.fast_conv1(input)\n", + " # x1 = self.fast_maxpool(x1)\n", + " # print(\"fast x1\", x1.shape)\n", + " lateral.append(x1)\n", + " \n", + " x2 = self.fast_conv2(x1)\n", + " # print(\"fast x2\", x2.shape)\n", + " lateral.append(x2)\n", + " \n", + " x3 = self.fast_conv3(x2)\n", + " # print(\"fast x3\", x3.shape)\n", + " lateral.append(x3)\n", + " \n", + " return x3, lateral\n", + " \n", + " def SlowPath(self, input, lateral):\n", + " x1 = self.slow_conv1(input)\n", + " # x1 = self.slow_maxpool(x1)\n", + " print(\"slow 1\", x1.shape)\n", + " \n", + " print(\"slow fusion1\",x1.shape, lateral[0].shape)\n", + " x1 = self.fusion1([x1, lateral[0]])\n", + " \n", + " \n", + " x2 = self.slow_conv2(x1)\n", + " print(\"slow fusion2\", x2.shape, lateral[1].shape)\n", + " x2 = self.fusion2([x2,lateral[1]])\n", + " \n", + " \n", + " x3 = self.slow_conv3(x2)\n", + " print(\"slow fusion3\", x3.shape, lateral[2].shape)\n", + " x3 = self.fusion3([x3,lateral[2]])\n", + " \n", + "\n", + " return x3\n", + "\n", + "\n", + "\n", + "x = torch.randn(320, 1, 64, 224)\n", + "\n", + "model = SlowFast()\n", + "fast, slow = model(x)\n", + "print()\n", + "print(\"fast\", fast.shape)\n", + "print(\"slow\", slow.shape)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([320, 1088, 15, 1])\n", + "torch.Size([320, 40])\n" + ] + } + ], + "source": [ + "# class Detection(nn.Module):\n", + "\n", + "# # def __init__(self, pooler_mode: Pooler.Mode, hidden: nn.Module, num_hidden_out: int, num_classes: int, proposal_smooth_l1_loss_beta: float):\n", + "# def __init__(self):\n", + "# super().__init__()\n", + "# num_hidden_out = 12288\n", + "# num_classes = 40\n", + "# self._proposal_class = nn.Linear(num_hidden_out, num_classes)\n", + "\n", + "# def forward(self, fast_feature, slow_feature):\n", + "# batch_size = fast_feature.shape[0]\n", + " \n", + "# fast_feature = nn.AvgPool2d(kernel_size=(\n", + "# fast_feature.shape[2], 1))(fast_feature).squeeze(2)\n", + "# # print(fast_feature.shape)\n", + "# slow_feature = nn.AvgPool2d(kernel_size=(\n", + "# slow_feature.shape[2], 1))(slow_feature).squeeze(2)\n", + "# # print(slow_feature.shape)\n", + "# feature = torch.cat([fast_feature, slow_feature], dim=1)\n", + "# # print(feature.shape)\n", + "\n", + "# out = feature.view(feature.shape[0],-1)#.cuda()\n", + "# # out = torch.flatten(feature, start_dim=1)\n", + "# # out = torch.reshape(feature,(feature.shape[0],-1))\n", + "# # print(out.shape)\n", + "# proposal_classes = self._proposal_class(out)\n", + "# # print(proposal_classes.shape)\n", + " \n", + "# return proposal_classes\n", + "\n", + "\n", + "# detection = Detection()#.cuda()\n", + "# fast_feature = torch.randn(320, 128, 1, 32)\n", + "# slow_feature = torch.randn(320, 256, 1, 32)\n", + "# y = detection(fast_feature, slow_feature)\n", + "# y.shape\n", + "\n", + "class PoolConcatPathway(nn.Module):\n", + " def __init__(\n", + " self,\n", + " pool,\n", + " dim: int = 1,\n", + " ) -> None:\n", + " super().__init__()\n", + " self.pool = pool\n", + " \n", + " def forward(self, x) -> torch.Tensor:\n", + " output = []\n", + " for ind in range(len(x)):\n", + " if x[ind] is not None:\n", + " if self.pool is not None and self.pool[ind] is not None:\n", + " x[ind] = self.pool[ind](x[ind])\n", + " # print(99, x[ind].shape)\n", + " output.append(x[ind])\n", + " return torch.cat(output, 1)\n", + "\n", + "\n", + "_num_pathway=2\n", + "head_pool_kernel_sizes = ((111, 1), (2, 1))\n", + "pool_model = [\n", + " nn.AvgPool2d(\n", + " kernel_size=head_pool_kernel_sizes[idx],\n", + " stride=(1, 1),\n", + " padding=(0, 0),\n", + " )\n", + " for idx in range(_num_pathway)\n", + "]\n", + "poolconcat = PoolConcatPathway(pool_model)\n", + "fast_feature = torch.randn(320, 64, 125, 1)\n", + "slow_feature = torch.randn(320, 1024, 16, 1)\n", + "# fast_feature = torch.randn(320, 64, 32, 56)\n", + "# slow_feature = torch.randn(320, 1024, 4, 56)\n", + "\n", + "y = poolconcat([fast_feature, slow_feature])\n", + "print(y.shape)\n", + "\n", + "# torch.Size([320, 256, 32, 7])\n", + "\n", + " \n", + "class ResNetBasicHead(nn.Module):\n", + " def __init__(self):\n", + " super().__init__()\n", + " dropout_rate=0.5\n", + " in_features=1088\n", + " out_features=40\n", + " \n", + " self.dropout = nn.Dropout(dropout_rate)\n", + " self.proj = nn.Linear(in_features, out_features)\n", + " self.outputpool = nn.AdaptiveAvgPool2d(1)\n", + "\n", + " def forward(self, x):\n", + " x = self.dropout(x)\n", + " \n", + " x = x.permute((0, 2, 3, 1))\n", + " x = self.proj(x)\n", + " x = x.permute((0, 3, 1, 2))\n", + " \n", + " x = self.outputpool(x)\n", + " x = x.squeeze()\n", + " return x\n", + " \n", + "pooled = torch.randn(320, 1088, 15, 1)\n", + "head = ResNetBasicHead()\n", + "out = head(pooled)\n", + "print(out.shape)\n", + "\n", + "# detection = Detection()\n", + "# fast_feature = torch.randn(320, 64, 125, 1)\n", + "# slow_feature = torch.randn(320, 1024, 16, 1)\n", + "# y = detection(fast_feature, slow_feature)\n", + "# y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([320, 40])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# class Model(nn.Module):\n", + "\n", + "# def __init__(self):\n", + "# super().__init__()\n", + "# self.backbone = SlowFast()\n", + "# self.detection = Detection()\n", + "\n", + "# def forward(self, input):\n", + "# fast_feature, slow_feature = self.backbone(input)\n", + "# # print(99, fast_feature.shape, slow_feature.shape)\n", + "# y = self.detection(fast_feature, slow_feature)\n", + "# return y\n", + "\n", + "class Model(nn.Module):\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.backbone = SlowFast()\n", + " # self.detection = Detection()\n", + " \n", + " _num_pathway=2\n", + " head_pool_kernel_sizes = ((111, 1), (2, 1))\n", + " pool_model = [\n", + " nn.AvgPool2d(\n", + " kernel_size=head_pool_kernel_sizes[idx],\n", + " stride=(1, 1),\n", + " padding=(0, 0),\n", + " )\n", + " for idx in range(_num_pathway)\n", + " ]\n", + " self.poolconcat = PoolConcatPathway(pool_model)\n", + " self.head = ResNetBasicHead()\n", + "\n", + "\n", + " def forward(self, input):\n", + " fast_feature, slow_feature = self.backbone(input)\n", + " # print(99, fast_feature.shape, slow_feature.shape)\n", + " # y = self.detection(fast_feature, slow_feature)\n", + " \n", + " y = self.poolconcat([fast_feature, slow_feature])\n", + " out = self.head(y)\n", + " return out\n", + " \n", + "model = Model()\n", + "y = model(x)\n", + "y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def train_test_subject_kfold(data, config, test_subject_id, kfold_k=0):\n", + " \n", + " ## init data\n", + " \n", + " # train_dataset, val_dataset, test_dataset = leave_one_subject_out(data, test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + " train_dataset, val_dataset, test_dataset = data.get_train_val_test_dataset(test_subject_id=test_subject_id, kfold_k=kfold_k)\n", + " train_loader = DataLoader(train_dataset, batch_size=config.training.batchsize, shuffle=True)\n", + " val_loader = DataLoader(val_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + " test_loader = DataLoader(test_dataset, batch_size=config.training.batchsize, shuffle=False)\n", + "\n", + " ## init model\n", + "\n", + " eegnet = Model()\n", + "\n", + " model = LightningModelClassifier(\n", + " optimizer=config.model.optimizer,\n", + " scheduler=config.model.scheduler,\n", + " optimizer_learning_rate=config.training.learning_rate,\n", + " scheduler_warmup_epochs=config.training.num_warmup_epochs,\n", + " )\n", + " \n", + " model.build_model(model=eegnet)\n", + "\n", + " ## train\n", + "\n", + " sub_dir = \"sub\"+ str(test_subject_id) +\"_k\"+ str(kfold_k)\n", + " logger_tb = TensorBoardLogger(save_dir=\"tensorboard_logs\", name=config.run_name, sub_dir=sub_dir)\n", + " lr_monitor = LearningRateMonitor(logging_interval='epoch')\n", + "\n", + " trainer = Trainer(max_epochs=config.training.num_epochs, gpus=config.training.gpus, logger=logger_tb, progress_bar_refresh_rate=0, weights_summary=None, callbacks=[lr_monitor])\n", + " trainer.fit(model, train_loader, val_loader)\n", + " \n", + " ## test\n", + " \n", + " result = trainer.test(dataloaders=test_loader, verbose=False)\n", + " test_acc = result[0]['test_acc_epoch']\n", + " \n", + " return test_acc" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "running test_subject_id: 33\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n", + "Global seed set to 1234\n", + "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'test_subject_id': 33, 'acc': [0.02916666679084301]}\n", + "\n", + "mean all 0.02916666679084301\n" + ] + } + ], + "source": [ + "main_logger.write_to_log(\"Begin\", break_line=True)\n", + "\n", + "test_results_acc = {}\n", + "means = []\n", + "\n", + "def k_fold_train_test_all_subjects():\n", + " \n", + " for test_subject_id in config.testing.test_subject_ids:\n", + " print()\n", + " print(\"running test_subject_id:\", test_subject_id)\n", + " \n", + " if test_subject_id not in test_results_acc:\n", + " test_results_acc[test_subject_id] = []\n", + " \n", + " test_acc = train_test_subject_kfold(data, config, test_subject_id, kfold_k=0)\n", + " \n", + " test_results_acc[test_subject_id].append(test_acc)\n", + " means.append(test_acc)\n", + " \n", + " this_result = {\n", + " \"test_subject_id\": test_subject_id,\n", + " \"acc\": test_results_acc[test_subject_id],\n", + " } \n", + " print(this_result)\n", + " main_logger.write_to_log(this_result)\n", + "\n", + " \n", + "k_fold_train_test_all_subjects()\n", + "\n", + "mean_acc = np.mean(means)\n", + "print()\n", + "print(\"mean all\", mean_acc)\n", + "main_logger.write_to_log(\"Mean acc: \"+str(mean_acc), break_line=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/requirements.txt b/requirements.txt index 63ee37f..9f46d5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ torch>=1.4.0 numpy scipy matplotlib -sklearn \ No newline at end of file +sklearn +pytorch-lightning +torchmetrics diff --git a/splearn/cross_decomposition/cca.py b/splearn/cross_decomposition/cca.py index f322af8..2406e78 100644 --- a/splearn/cross_decomposition/cca.py +++ b/splearn/cross_decomposition/cca.py @@ -4,11 +4,12 @@ import numpy as np from sklearn.metrics import confusion_matrix import functools -from ..classes.classifier import Classifier +# from ..classes.classifier import Classifier from .reference_frequencies import generate_reference_signals -class CCA(Classifier): +# class CCA(Classifier): +class CCA(): r""" Calculates the canonical correlation coefficient and corresponding weights which maximize a correlation coefficient diff --git a/splearn/classes/classifier.py b/splearn/cross_decomposition/classifier.py similarity index 100% rename from splearn/classes/classifier.py rename to splearn/cross_decomposition/classifier.py diff --git a/splearn/data/__init__.py b/splearn/data/__init__.py index e69de29..fc13c11 100644 --- a/splearn/data/__init__.py +++ b/splearn/data/__init__.py @@ -0,0 +1,6 @@ +from .pytorch_dataset import PyTorchDataset, PyTorchDataset2Views +from .multiple_subjects import MultipleSubjects +from .hsssvep import HSSSVEP +from .openbmi import OPENBMI + +from .generate import generate_signal \ No newline at end of file diff --git a/splearn/data/hsssvep.py b/splearn/data/hsssvep.py new file mode 100644 index 0000000..97d4eb4 --- /dev/null +++ b/splearn/data/hsssvep.py @@ -0,0 +1,75 @@ +import os +import numpy as np +import scipy.io as sio +from typing import Tuple + +from splearn.data.pytorch_dataset import PyTorchDataset + + +class HSSSVEP(PyTorchDataset): + """ + This is a private dataset. + A Benchmark Dataset for SSVEP-Based Brain–Computer Interfaces + Yijun Wang, Xiaogang Chen, Xiaorong Gao, Shangkai Gao + https://ieeexplore.ieee.org/document/7740878 + Sampling rate: 250 Hz + Targets: [8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,8.2,9.2,10.2,11.2,12.2,13.2,14.2,15.2,8.4,9.4,10.4,11.4,12.4,13.4,14.4,15.4,8.6,9.6,10.6,11.6,12.6,13.6,14.6,15.6,8.8,9.8,10.8,11.8,12.8,13.8,14.8,15.8] + + This dataset gathered SSVEP-BCI recordings of 35 healthy subjects (17 females, aged 17-34 years, mean age: 22 years) focusing on 40 characters flickering at different frequencies (8-15.8 Hz with an interval of 0.2 Hz). For each subject, the experiment consisted of 6 blocks. Each block contained 40 trials corresponding to all 40 characters indicated in a random order. Each trial started with a visual cue (a red square) indicating a target stimulus. The cue appeared for 0.5 s on the screen. Subjects were asked to shift their gaze to the target as soon as possible within the cue duration. Following the cue offset, all stimuli started to flicker on the screen concurrently and lasted 5 s. After stimulus offset, the screen was blank for 0.5 s before the next trial began, which allowed the subjects to have short breaks between consecutive trials. Each trial lasted a total of 6 s. To facilitate visual fixation, a red triangle appeared below the flickering target during the stimulation period. In each block, subjects were asked to avoid eye blinks during the stimulation period. To avoid visual fatigue, there was a rest for several minutes between two consecutive blocks. + + EEG data were acquired using a Synamps2 system (Neuroscan, Inc.) with a sampling rate of 1000 Hz. The amplifier frequency passband ranged from 0.15 Hz to 200 Hz. Sixty-four channels covered the whole scalp of the subject and were aligned according to the international 10-20 system. The ground was placed on midway between Fz and FPz. The reference was located on the vertex. Electrode impedances were kept below 10 K". To remove the common power-line noise, a notch filter at 50 Hz was applied in data recording. Event triggers generated by the computer to the amplifier and recorded on an event channel synchronized to the EEG data. + + The continuous EEG data was segmented into 6 s epochs (500 ms pre-stimulus, 5.5 s post-stimulus onset). The epochs were subsequently downsampled to 250 Hz. Thus each trial consisted of 1500 time points. Finally, these data were stored as double-precision floating-point values in MATLAB and were named as subject indices (i.e., S01.mat, ", S35.mat). For each file, the data loaded in MATLAB generate a 4-D matrix named "data" with dimensions of [64, 1500, 40, 6]. The four dimensions indicate "Electrode index", "Time points", "Target index", and "Block index". The electrode positions were saved in a "64-channels.loc" file. Six trials were available for each SSVEP frequency. Frequency and phase values for the 40 target indices were saved in a "Freq_Phase.mat" file. + + Information for all subjects was listed in a "Sub_info.txt" file. For each subject, there are five factors including "Subject Index", "Gender", "Age", "Handedness", and "Group". Subjects were divided into an "experienced" group (eight subjects, S01-S08) and a "naive" group (27 subjects, S09-S35) according to their experience in SSVEP-based BCIs. + """ + + def __init__(self, root: str, subject_id: int, verbose: bool = False) -> None: + + self.root = root + self.sample_rate = 1000 + self.data, self.targets, self.channel_names = _load_data(self.root, subject_id, verbose) + + self.sampling_rate = 250 + self.stimulus_frequencies = np.array([8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,8.2,9.2,10.2,11.2,12.2,13.2,14.2,15.2,8.4,9.4,10.4,11.4,12.4,13.4,14.4,15.4,8.6,9.6,10.6,11.6,12.6,13.6,14.6,15.6,8.8,9.8,10.8,11.8,12.8,13.8,14.8,15.8]) + self.targets_frequencies = self.stimulus_frequencies[self.targets] + + def __getitem__(self, n: int) -> Tuple[np.ndarray, int]: + return (self.data[n], self.targets[n]) + + def __len__(self) -> int: + return len(self.data) + + +def _load_data(root, subject_id, verbose): + + path = os.path.join(root, 'S'+str(subject_id)+'.mat') + data_mat = sio.loadmat(path) + + raw_data = data_mat['data'].copy() + raw_data = np.transpose(raw_data, (2,3,0,1)) + + data = [] + targets = [] + for target_id in np.arange(raw_data.shape[0]): + data.extend(raw_data[target_id]) + + this_target = np.array([target_id]*raw_data.shape[1]) + targets.extend(this_target) + + data = np.array(data) + + # Each trial started with a 0.5-s target cue. Subjects were asked to shift their gaze to the target as soon as possible. After the cue, all stimuli started to flicker on the screen concurrently for 5 s. Then, the screen was blank for 0.5 s before the next trial began. Each trial lasted 6 s in total. + # We cut the signal off after 4 seconds + # We start from 160, because 0.5s Cue + 0.14s (visual latency) as they use phase in stimulus presentation. 0.64*250 = 160 + # data = np.array(data)[:,:,160:1160] + targets = np.array(targets) + + channel_names = ['FP1','FPZ','FP2','AF3','AF4','F7','F5','F3','F1','FZ','F2','F4','F6','F8','FT7','FC5','FC3','FC1','FCz','FC2','FC4','FC6','FT8','T7','C5','C3','C1','Cz','C2','C4','C6','T8','M1','TP7','CP5','CP3','CP1','CPZ','CP2','CP4','CP6','TP8','M2','P7','P5','P3','P1','PZ','P2','P4','P6','P8','PO7','PO5','PO3','POz','PO4','PO6','PO8','CB1','O1','Oz','O2','CB2'] + + if verbose: + print('Load path:', path) + print('Data shape', data.shape) + print('Targets shape', targets.shape) + + return data, targets, channel_names diff --git a/splearn/data/multiple_subjects.py b/splearn/data/multiple_subjects.py new file mode 100644 index 0000000..f6e9c23 --- /dev/null +++ b/splearn/data/multiple_subjects.py @@ -0,0 +1,111 @@ +import numpy as np +from sklearn.model_selection import StratifiedKFold +from splearn.data.pytorch_dataset import PyTorchDataset + + +class MultipleSubjects(PyTorchDataset): + def __init__( + self, + dataset: PyTorchDataset, + root: str, + subject_ids: [], + func_preprocessing=None, + func_get_train_val_test_dataset=None, + verbose: bool = False, + ) -> None: + + self.root = root + self.subject_ids = subject_ids + + self._load_multiple(root, dataset, subject_ids, func_preprocessing, verbose) + self.targets_frequencies = self.stimulus_frequencies[self.targets] + + self.func_get_train_val_test_dataset = func_get_train_val_test_dataset + + def _load_multiple(self, root, dataset: PyTorchDataset, subject_ids: [], func_preprocessing, verbose: bool = False) -> None: + is_first = True + + for subject_i in range(len(subject_ids)): + + subject_id = subject_ids[subject_i] + print('Load subject:', subject_id) + + subject_dataset = dataset(root=root, subject_id=subject_id) + + sub_data = subject_dataset.data + sub_targets = subject_dataset.targets + + if is_first: + self.data = np.zeros((len(subject_ids), sub_data.shape[0], sub_data.shape[1], sub_data.shape[2])) + self.targets = np.zeros((len(subject_ids), sub_targets.shape[0])) + self.sampling_rate = subject_dataset.sampling_rate + self.stimulus_frequencies = subject_dataset.stimulus_frequencies + self.channel_names = subject_dataset.channel_names + is_first = False + + self.data[subject_i, :, :, :] = sub_data + self.targets[subject_i] = sub_targets + + self.targets = self.targets.astype(np.int32) + + if func_preprocessing is not None: + func_preprocessing(self) + + def set_data(self, x): + self.data = x + + def set_targets(self, targets): + self.targets = targets + + def get_subject(self, subject_id): + index = list(self.subject_ids).index(subject_id) + return self.data[index], self.targets[index] + + def dataset_split_stratified(self, X, y, k=0, n_splits=3, seed=71, shuffle=True): + skf = StratifiedKFold(n_splits=n_splits, random_state=seed, shuffle=shuffle) + split_data = skf.split(X, y) + + for idx, value in enumerate(split_data): + + if k != idx: + continue + else: + train_index, test_index = value + + X_train, X_test = X[train_index], X[test_index] + y_train, y_test = y[train_index], y[test_index] + + return (X_train, y_train), (X_test, y_test) + + def get_train_val_test_dataset(self, **kwargs): + if self.func_get_train_val_test_dataset is None: + return self._leave_one_subject_out(**kwargs) + else: + return self.func_get_train_val_test_dataset(self, **kwargs) + + def _leave_one_subject_out(self, **kwargs): + + test_subject_id = kwargs["test_subject_id"] if "test_subject_id" in kwargs else 1 + kfold_k = kwargs["kfold_k"] if "kfold_k" in kwargs else 0 + kfold_split = kwargs["kfold_split"] if "kfold_split" in kwargs else 3 + + # get test data + # test_sub_idx = self.subject_ids.index(test_subject_id) + test_sub_idx = np.where(self.subject_ids == test_subject_id)[0][0] + selected_subject_data = self.data[test_sub_idx] + selected_subject_targets = self.targets[test_sub_idx] + test_dataset = PyTorchDataset(selected_subject_data, selected_subject_targets) + + # get train val data + indices = np.arange(self.data.shape[0]) + train_val_data = self.data[indices!=test_sub_idx, :, :, :] + train_val_data = train_val_data.reshape((train_val_data.shape[0]*train_val_data.shape[1], train_val_data.shape[2], train_val_data.shape[3])) + train_val_targets = self.targets[indices!=test_sub_idx, :] + train_val_targets = train_val_targets.reshape((train_val_targets.shape[0]*train_val_targets.shape[1])) + + # train test split + (X_train, y_train), (X_val, y_val) = self.dataset_split_stratified(train_val_data, train_val_targets, k=kfold_k, n_splits=kfold_split) + train_dataset = PyTorchDataset(X_train, y_train) + val_dataset = PyTorchDataset(X_val, y_val) + + return train_dataset, val_dataset, test_dataset diff --git a/splearn/data/pytorch_dataset.py b/splearn/data/pytorch_dataset.py new file mode 100644 index 0000000..a75b229 --- /dev/null +++ b/splearn/data/pytorch_dataset.py @@ -0,0 +1,65 @@ +from torch.utils.data import Dataset +import numpy as np + + +class PyTorchDataset(Dataset): + def __init__(self, data, targets): + self.data = data + self.data = self.data.astype(np.float32) + self.targets = targets + self.channel_names = None + + def __getitem__(self, index): + return self.data[index], self.targets[index] + + def __len__(self): + return len(self.data) + + def set_data_targets(self, data: [] = None, targets: [] = None) -> None: + if data is not None: + self.data = data.copy() + if targets is not None: + self.targets = targets.copy() + self.targets = self.targets.astype(int) + + def set_channel_names(self,channel_names): + self.channel_names = channel_names + + def get_data(self): + r""" + Data shape: (6, 40, 9, 1250) [# of blocks, # of targets, # of channels, # of sampling points] + """ + return self.data + + def get_targets(self): + r""" + Targets index from 0 to 39. Shape: (6, 40) [# of blocks, # of targets] + """ + return self.targets + + def get_stimulus_frequencies(self): + r""" + A list of frequencies of each stimulus: + [8.0,9.0,10.0,11.0,12.0,13.0,14.0,15.0,8.2,9.2,10.2,11.2,12.2,13.2,14.2,15.2,8.4,9.4,10.4,11.4,12.4,13.4,14.4,15.4,8.6,9.6,10.6,11.6,12.6,13.6,14.6,15.6,8.8,9.8,10.8,11.8,12.8,13.8,14.8,15.8] + """ + return self.stimulus_frequencies + + def get_targets_frequencies(self): + r""" + Targets by frequencies, range between 8.0 Hz to 15.8 Hz. + Shape: (6, 40) [# of blocks, # of targets] + """ + return self.targets_frequencies + + +class PyTorchDataset2Views(Dataset): + def __init__(self, data_view1, data_view2, targets): + self.data_view1 = data_view1.astype(np.float32) + self.data_view2 = data_view2.astype(np.float32) + self.targets = targets + + def __getitem__(self, index): + return self.data_view1[index], self.data_view2[index], self.targets[index] + + def __len__(self): + return len(self.data_view1) diff --git a/splearn/data/utils.py b/splearn/data/utils.py new file mode 100644 index 0000000..ef5f5ae --- /dev/null +++ b/splearn/data/utils.py @@ -0,0 +1,4 @@ +import numpy as np + +def onehot_targets(targets): + return (np.arange(targets.max()+1) == targets[...,None]).astype(int) diff --git a/splearn/filter/butterworth.py b/splearn/filter/butterworth.py new file mode 100644 index 0000000..3443c75 --- /dev/null +++ b/splearn/filter/butterworth.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +"""Digital filter bandpass zero-phase implementation (filtfilt). Apply a digital filter forward and backward to a signal. +""" +import numpy as np +import matplotlib.pyplot as plt +from scipy.signal import butter, filtfilt, sosfiltfilt, freqz +from splearn.fourier import fast_fourier_transform + + +def butter_bandpass_filter(signal, lowcut, highcut, sampling_rate, order=4, verbose=False): + r""" + Digital filter bandpass zero-phase implementation (filtfilt) + Apply a digital filter forward and backward to a signal + + Args: + signal : ndarray, shape (trial,channel,time) + Input signal by trials in time domain + lowcut : int + Lower bound filter + highcut : int + Upper bound filter + sampling_rate : int + Sampling frequency + order : int, default: 4 + Order of the filter + verbose : boolean, default: False + Print and plot details + Returns: + y : ndarray + Filter signal + """ + sos = _butter_bandpass(lowcut, highcut, sampling_rate, order=order, output='sos') + y = sosfiltfilt(sos, signal, axis=-1) + + if verbose: + tmp_x = signal[0, 0] + tmp_y = y[0, 0] + + # time domain + plt.plot(tmp_x, label='signal') + plt.show() + + plt.plot(tmp_y, label='Filtered') + plt.show() + + # freq domain + lower_xlim = lowcut-10 if (lowcut-10) > 0 else 0 + fast_fourier_transform( + tmp_x, sampling_rate, plot=True, plot_xlim=[lower_xlim, highcut+20], plot_label='Signal') + fast_fourier_transform( + tmp_y, sampling_rate, plot=True, plot_xlim=[lower_xlim, highcut+20], plot_label='Filtered') + + plt.xlim([lower_xlim, highcut+20]) + plt.ylim([0, 2]) + plt.legend() + plt.xlabel('Frequency (Hz)') + plt.show() + + print('Input: Signal shape', signal.shape) + print('Output: Signal shape', y.shape) + + return y + +def butter_bandpass_filter_signal_1d(signal, lowcut, highcut, sampling_rate, order=4, verbose=False): + r""" + Digital filter bandpass zero-phase implementation (filtfilt) + Apply a digital filter forward and backward to a signal + + Args: + signal : ndarray, shape (time,) + Single input signal in time domain + lowcut : int + Lower bound filter + highcut : int + Upper bound filter + sampling_rate : int + Sampling frequency + order : int, default: 4 + Order of the filter + verbose : boolean, default: False + Print and plot details + Returns: + y : ndarray + Filter signal + """ + b, a = _butter_bandpass(lowcut, highcut, sampling_rate, order) + y = filtfilt(b, a, signal) + + if verbose: + w, h = freqz(b, a) + plt.plot((sampling_rate * 0.5 / np.pi) * w, + abs(h), label="order = %d" % order) + plt.plot([0, 0.5 * sampling_rate], [np.sqrt(0.5), np.sqrt(0.5)], + '--', label='sqrt(0.5)') + plt.xlabel('Frequency (Hz)') + plt.ylabel('Gain') + plt.grid(True) + plt.legend(loc='best') + low = max(0, lowcut-(sampling_rate/100)) + high = highcut+(sampling_rate/100) + plt.xlim([low, high]) + plt.ylim([0, 1.2]) + plt.title('Frequency response of filter - lowcut:' + + str(lowcut)+', highcut:'+str(highcut)) + plt.show() + + # TIME + plt.plot(signal, label='Signal') + plt.title('Signal') + plt.show() + + plt.plot(y, label='Filtered') + plt.title('Bandpass filtered') + plt.show() + + # FREQ + lower_xlim = lowcut-10 if (lowcut-10) > 0 else 0 + fast_fourier_transform( + signal, sampling_rate, plot=True, plot_xlim=[lower_xlim, highcut+20], plot_label='Signal') + fast_fourier_transform( + y, sampling_rate, plot=True, plot_xlim=[lower_xlim, highcut+20], plot_label='Filtered') + + plt.xlim([lower_xlim, highcut+20]) + plt.ylim([0, 2]) + plt.legend() + plt.xlabel('Frequency (Hz)') + plt.show() + + print('Input: Signal shape', signal.shape) + print('Output: Signal shape', y.shape) + + return y + +def _butter_bandpass(lowcut, highcut, sampling_rate, order=4, output='ba'): + r""" + Create a Butterworth bandpass filter + Design an Nth-order digital or analog Butterworth filter and return the filter coefficients. + + Args: + lowcut : int + Lower bound filter + highcut : int + Upper bound filter + sampling_rate : int + Sampling frequency + order : int, default: 4 + Order of the filter + output : string, default: ba + Type of output {‘ba’, ‘zpk’, ‘sos’} + Returns: + butter : ndarray + Butterworth filter + Dependencies: + butter : scipy.signal.butter + """ + nyq = sampling_rate * 0.5 + low = lowcut / nyq + high = highcut / nyq + return butter(order, [low, high], btype='bandpass', output=output) + + +#### ver 1 + +# def butter_bandpass(signal, lowcut, highcut, sampling_rate, type="sos", order=4, plot=False, **kwargs): +# r""" +# Design a `order`th-order bandpass Butterworth filter with a cutoff frequency between `lowcut`-Hz and `highcut`-Hz, which, for data sampled at `sampling_rate`-Hz. + +# Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html +# https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.filtfilt.html +# https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfiltfilt.html + +# Args: +# signal : ndarray, shape (time,) or (channel,time) or (trial,channel,time) +# Input signal (1D/2D/3D), where last axis is time samples. +# lowcut : int +# Lower bound filter +# highcut : int +# Upper bound filter +# sampling_rate : int +# Sampling frequency +# type: string, optional, default: sos +# Type of output: numerator/denominator (‘ba’), or second-order sections (‘sos’). +# Default is ‘ba’ for backwards compatibility, but ‘sos’ should be used for general-purpose filtering. +# order : int, optional, default: 4 +# Order of the filter +# plot : boolean, optional, default: False +# Plot signal and filtered signal in frequency domain +# plot_xlim : array of shape [lower, upper], optional, default: [lowcut-10 if (lowcut-10) > 0 else 0, highcut+20] +# If `plot=True`, set a limit on the X-axis between lower and upper bound +# plot_ylim : array of shape [lower, upper], optional, default: None +# If `plot=True`, set a limit on the Y-axis between lower and upper bound + +# Returns: +# y : ndarray +# Filtered signal that has same shape in input `signal` + +# Usage: +# >>> from splearn.data.generate import generate_signal +# >>> +# >>> signal_1d = generate_signal( +# >>> length_seconds=4, +# >>> sampling_rate=100, +# >>> frequencies=[4,7,11,17,40, 50], +# >>> plot=True +# >>> ) +# >>> print('signal_1d.shape', signal_1d.shape) +# >>> +# >>> signal_2d = generate_signal( +# >>> length_seconds=4, +# >>> sampling_rate=100, +# >>> frequencies=[[4,7,11,17,40, 50],[1, 3]], +# >>> plot=True +# >>> ) +# >>> print('signal_2d.shape', signal_2d.shape) +# >>> +# >>> signal_3d = np.expand_dims(s1, 0) +# >>> print('signal_3d.shape', signal_3d.shape) +# >>> +# >>> signal_1d_filtered = butter_bandpass( +# >>> signal=signal_1d, +# >>> lowcut=5, +# >>> highcut=20, +# >>> sampling_rate=100, +# >>> plot=True, +# >>> ) +# >>> print('signal_1d_filtered.shape', signal_1d_filtered.shape) +# >>> +# >>> signal_2d_filtered = butter_bandpass( +# >>> signal=signal_2d, +# >>> lowcut=5, +# >>> highcut=20, +# >>> sampling_rate=100, +# >>> type='sos', +# >>> order=4, +# >>> plot=True, +# >>> plot_xlim=[3,20] +# >>> ) +# >>> print('signal_2d_filtered.shape', signal_2d_filtered.shape) +# >>> +# >>> signal_3d_filtered = butter_bandpass( +# >>> signal=signal_3d, +# >>> lowcut=5, +# >>> highcut=20, +# >>> sampling_rate=100, +# >>> type='ba', +# >>> order=4, +# >>> plot=True, +# >>> plot_xlim=[0,40] +# >>> ) +# >>> print('signal_3d_filtered.shape', signal_3d_filtered.shape) +# """ + +# dim = len(signal.shape)-1 + +# if type == 'ba': +# b, a = _butter_bandpass(lowcut, highcut, sampling_rate, order) +# y = filtfilt(b, a, signal) +# else: +# sos = _butter_bandpass(lowcut, highcut, sampling_rate, +# order=order, output='sos') +# y = sosfiltfilt(sos, signal, axis=dim) + +# if plot: +# tmp_x = signal +# tmp_y = y +# if dim == 1: +# tmp_x = signal[0] +# tmp_y = y[0] +# elif dim == 2: +# tmp_x = signal[0, 0] +# tmp_y = y[0, 0] + +# if type == 'ba': +# # plot frequency response of filter +# w, h = freqz(b, a) +# plt.plot((sampling_rate * 0.5 / np.pi) * w, +# abs(h), label="order = %d" % order) +# plt.plot([0, 0.5 * sampling_rate], [np.sqrt(0.5), np.sqrt(0.5)], +# '--', label='sqrt(0.5)') +# plt.xlabel('Frequency (Hz)') +# plt.ylabel('Gain') +# plt.grid(True) +# plt.legend(loc='best') +# low = max(0, lowcut-(sampling_rate/100)) +# high = highcut+(sampling_rate/100) +# plt.xlim([low, high]) +# plt.ylim([0, 1.2]) +# plt.title('Frequency response of filter - lowcut:' + +# str(lowcut)+', highcut:'+str(highcut)) +# plt.show() + +# plot_xlim = kwargs['plot_xlim'] if 'plot_xlim' in kwargs else [lowcut-10 if (lowcut-10) > 0 else 0, highcut+20] +# plot_ylim = kwargs['plot_ylim'] if 'plot_ylim' in kwargs else None + +# # frequency domain +# fast_fourier_transform( +# tmp_x, +# sampling_rate, +# plot=True, +# plot_xlim=plot_xlim, +# plot_ylim=plot_ylim, +# plot_label='Signal' +# ) +# fast_fourier_transform( +# tmp_y, +# sampling_rate, +# plot=True, +# plot_xlim=plot_xlim, +# plot_ylim=plot_ylim, +# plot_label='Filtered' +# ) + +# plt.title('Signal and filtered signal in frequency domain, type:' + type + ',lowcut:' + str(lowcut) + ',highcut:' + str(highcut) + ',order:' + str(order)) +# plt.legend() +# plt.show() + +# return y + + +# def _butter_bandpass(lowcut, highcut, sampling_rate, order=4, output='ba'): +# r""" +# Create a Butterworth bandpass filter. Design an Nth-order digital or analog Butterworth filter and return the filter coefficients. +# Reference: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html + +# Args: +# lowcut : int +# Lower bound filter +# highcut : int +# Upper bound filter +# sampling_rate : int +# Sampling frequency +# order : int, default: 4 +# Order of the filter +# output : string, default: ba +# Type of output {‘ba’, ‘zpk’, ‘sos’}. Type of output: numerator/denominator (‘ba’), pole-zero (‘zpk’), or second-order sections (‘sos’). +# Default is ‘ba’ for backwards compatibility, but ‘sos’ should be used for general-purpose filtering. +# Returns: +# butter : ndarray +# Scipy butterworth filter +# Dependencies: +# butter : scipy.signal.butter +# """ +# nyq = sampling_rate * 0.5 +# low = lowcut / nyq +# high = highcut / nyq +# return butter(order, [low, high], btype='bandpass', output=output) + + +# if __name__ == "__main__": + +# from splearn.data.generate import signal + +# signal_1d = generate_signal( +# length_seconds=4, +# sampling_rate=100, +# frequencies=[4,7,11,17,40, 50], +# plot=True +# ) +# print('signal_1d.shape', signal_1d.shape) + +# signal_2d = generate_signal( +# length_seconds=4, +# sampling_rate=100, +# frequencies=[[4,7,11,17,40, 50],[1, 3]], +# plot=True +# ) +# print('signal_2d.shape', signal_2d.shape) + +# signal_3d = np.expand_dims(s1, 0) +# print('signal_3d.shape', signal_3d.shape) + +# signal_1d_filtered = butter_bandpass( +# signal=signal_1d, +# lowcut=5, +# highcut=20, +# sampling_rate=100, +# plot=True, +# ) +# print('signal_1d_filtered.shape', signal_1d_filtered.shape) + +# signal_2d_filtered = butter_bandpass( +# signal=signal_2d, +# lowcut=5, +# highcut=20, +# sampling_rate=100, +# type='sos', +# order=4, +# plot=True, +# plot_xlim=[3,20] +# ) +# print('signal_2d_filtered.shape', signal_2d_filtered.shape) + +# signal_3d_filtered = butter_bandpass( +# signal=signal_3d, +# lowcut=5, +# highcut=20, +# sampling_rate=100, +# type='ba', +# order=4, +# plot=True, +# plot_xlim=[0,40] +# ) +# print('signal_3d_filtered.shape', signal_3d_filtered.shape) diff --git a/splearn/filter/channels.py b/splearn/filter/channels.py new file mode 100644 index 0000000..cfafa6e --- /dev/null +++ b/splearn/filter/channels.py @@ -0,0 +1,86 @@ +import numpy as np + + +def pick_channels(data: np.ndarray, + channel_names: [str], + selected_channels: [str], + verbose: bool = False) -> np.ndarray: + + picked_ch = pick_channels_mne(channel_names, selected_channels) + + if len(data.shape) == 3: + data = data[:, picked_ch, :] + if len(data.shape) == 4: + data = data[:, :, picked_ch, :] + + if verbose: + print('picking channels: channel_names', + len(channel_names), channel_names) + print('picked_ch', picked_ch) + print() + + del picked_ch + + return data + + +def pick_channels_mne(ch_names, include, exclude=[], ordered=False): + """Pick channels by names. + Returns the indices of ``ch_names`` in ``include`` but not in ``exclude``. + Taken from https://github.com/mne-tools/mne-python/blob/master/mne/io/pick.py + + Parameters + ---------- + ch_names : list of str + List of channels. + include : list of str + List of channels to include (if empty include all available). + .. note:: This is to be treated as a set. The order of this list + is not used or maintained in ``sel``. + exclude : list of str + List of channels to exclude (if empty do not exclude any channel). + Defaults to []. + ordered : bool + If true (default False), treat ``include`` as an ordered list + rather than a set, and any channels from ``include`` are missing + in ``ch_names`` an error will be raised. + .. versionadded:: 0.18 + Returns + ------- + sel : array of int + Indices of good channels. + See Also + -------- + pick_channels_regexp, pick_types + """ + if len(np.unique(ch_names)) != len(ch_names): + raise RuntimeError('ch_names is not a unique list, picking is unsafe') + # _check_excludes_includes(include) + # _check_excludes_includes(exclude) + if not ordered: + if not isinstance(include, set): + include = set(include) + if not isinstance(exclude, set): + exclude = set(exclude) + sel = [] + for k, name in enumerate(ch_names): + if (len(include) == 0 or name in include) and name not in exclude: + sel.append(k) + else: + if not isinstance(include, list): + include = list(include) + if len(include) == 0: + include = list(ch_names) + if not isinstance(exclude, list): + exclude = list(exclude) + sel, missing = list(), list() + for name in include: + if name in ch_names: + if name not in exclude: + sel.append(ch_names.index(name)) + else: + missing.append(name) + if len(missing): + raise ValueError('Missing channels from ch_names required by ' + 'include:\n%s' % (missing,)) + return np.array(sel, int) diff --git a/splearn/nn/base/__init__.py b/splearn/nn/base/__init__.py new file mode 100644 index 0000000..c9c0fc2 --- /dev/null +++ b/splearn/nn/base/__init__.py @@ -0,0 +1,2 @@ +from splearn.nn.base.lightning import LightningModel +from splearn.nn.base.classifier import LightningModelClassifier diff --git a/splearn/nn/base/classifier.py b/splearn/nn/base/classifier.py new file mode 100644 index 0000000..267ae8c --- /dev/null +++ b/splearn/nn/base/classifier.py @@ -0,0 +1,63 @@ +import torchmetrics +from splearn.nn.base import LightningModel +from splearn.nn.loss import LabelSmoothCrossEntropyLoss + + +class LightningModelClassifier(LightningModel): + def __init__( + self, + optimizer="adamw", + scheduler="cosine_with_warmup", + optimizer_learning_rate: float=1e-3, + optimizer_epsilon: float=1e-6, + optimizer_weight_decay: float=0.0005, + scheduler_warmup_epochs: int=10, + ): + super().__init__() + self.save_hyperparameters() + + self.train_acc = torchmetrics.Accuracy() + self.valid_acc = torchmetrics.Accuracy() + self.test_acc = torchmetrics.Accuracy() + + self.criterion_classifier = LabelSmoothCrossEntropyLoss(smoothing=0.3) # F.cross_entropy() + + def build_model(self, model): + self.model = model + + def forward(self, x): + y_hat = self.model(x) + return y_hat + + def step(self, batch, batch_idx): + x, y = batch + y_hat = self.forward(x) + loss = self.criterion_classifier(y_hat, y.long()) # self.criterion_classifier(y_hat, y.long()) # F.cross_entropy(y_hat, y.long()) + return y_hat, y, loss + + def training_step(self, batch, batch_idx): + y_hat, y, loss = self.step(batch, batch_idx) + acc = self.train_acc(y_hat, y.long()) + self.log('train_loss', loss, on_step=True) + return loss + + def validation_step(self, batch, batch_idx): + y_hat, y, loss = self.step(batch, batch_idx) + acc = self.valid_acc(y_hat, y.long()) + self.log('valid_loss', loss, on_step=True) + return loss + + def test_step(self, batch, batch_idx): + y_hat, y, loss = self.step(batch, batch_idx) + acc = self.test_acc(y_hat, y.long()) + self.log('test_loss', loss) + return loss + + def training_epoch_end(self, outs): + self.log('train_acc_epoch', self.train_acc.compute()) + + def validation_epoch_end(self, outs): + self.log('valid_acc_epoch', self.valid_acc.compute()) + + def test_epoch_end(self, outs): + self.log('test_acc_epoch', self.test_acc.compute()) diff --git a/splearn/nn/base/lightning.py b/splearn/nn/base/lightning.py new file mode 100644 index 0000000..c9d2827 --- /dev/null +++ b/splearn/nn/base/lightning.py @@ -0,0 +1,43 @@ +from pytorch_lightning import LightningModule +from splearn.nn.optimization import get_scheduler, get_optimizer, get_num_steps + + +class LightningModel(LightningModule): + def __init__( + self + ): + super().__init__() + + def forward(self, x): + raise NotImplementedError + + def training_step(self, batch, batch_idx): + raise NotImplementedError + + def validation_step(self, batch, batch_idx): + raise NotImplementedError + + def test_step(self, batch, batch_idx): + raise NotImplementedError + + def configure_optimizers(self): + + optimizer = get_optimizer( + name=self.hparams.optimizer, + model=self, + lr=self.hparams.optimizer_learning_rate, + weight_decay=self.hparams.optimizer_weight_decay, + epsilon=self.hparams.optimizer_epsilon + ) + + total_train_steps, num_warmup_steps = get_num_steps(self) + + scheduler = get_scheduler( + name=self.hparams.scheduler, + optimizer=optimizer, + num_warmup_steps=num_warmup_steps, + num_training_steps=total_train_steps, + ) + + scheduler = {'scheduler': scheduler, 'interval': 'step', 'frequency': 1} + return [optimizer], [scheduler] diff --git a/splearn/nn/loss.py b/splearn/nn/loss.py new file mode 100644 index 0000000..22fe9a8 --- /dev/null +++ b/splearn/nn/loss.py @@ -0,0 +1,42 @@ +""" +LabelSmoothCrossEntropyLoss +https://github.com/pytorch/pytorch/issues/7455 +""" +import torch +import torch.nn.functional as F +from torch.nn.modules.loss import _WeightedLoss + + +class LabelSmoothCrossEntropyLoss(_WeightedLoss): + def __init__(self, weight=None, reduction='mean', smoothing=0.0): + super().__init__(weight=weight, reduction=reduction) + self.smoothing = smoothing + self.weight = weight + self.reduction = reduction + + @staticmethod + def _smooth_one_hot(targets: torch.Tensor, n_classes: int, smoothing=0.0): + assert 0 <= smoothing < 1 + with torch.no_grad(): + targets = torch.empty(size=(targets.size(0), n_classes), + device=targets.device) \ + .fill_(smoothing / (n_classes - 1)) \ + .scatter_(1, targets.data.unsqueeze(1), 1. - smoothing) + return targets + + def forward(self, inputs, targets): + targets = LabelSmoothCrossEntropyLoss._smooth_one_hot(targets, inputs.size(-1), + self.smoothing) + lsm = F.log_softmax(inputs, -1) + + if self.weight is not None: + lsm = lsm * self.weight.unsqueeze(0) + + loss = -(targets * lsm).sum(-1) + + if self.reduction == 'sum': + loss = loss.sum() + elif self.reduction == 'mean': + loss = loss.mean() + + return loss diff --git a/splearn/nn/models/EEGNet/CompactEEGNet.py b/splearn/nn/models/EEGNet/CompactEEGNet.py new file mode 100644 index 0000000..aabf2cc --- /dev/null +++ b/splearn/nn/models/EEGNet/CompactEEGNet.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""EEGNet: Compact Convolutional Neural Network (Compact-CNN) https://arxiv.org/pdf/1803.04566.pdf +""" +import torch +from torch import nn +from splearn.nn.modules.conv2d import SeparableConv2d + + +class CompactEEGNet(nn.Module): + """ + EEGNet: Compact Convolutional Neural Network (Compact-CNN) + https://arxiv.org/pdf/1803.04566.pdf + """ + def __init__(self, num_channel=10, num_classes=4, signal_length=1000, f1=96, f2=96, d=1): + super().__init__() + + self.signal_length = signal_length + + # layer 1 + self.conv1 = nn.Conv2d(1, f1, (1, signal_length), padding=(0,signal_length//2)) + self.bn1 = nn.BatchNorm2d(f1) + self.depthwise_conv = nn.Conv2d(f1, d*f1, (num_channel, 1), groups=f1) + self.bn2 = nn.BatchNorm2d(d*f1) + self.avgpool1 = nn.AvgPool2d((1,4)) + + # layer 2 + self.separable_conv = SeparableConv2d( + in_channels=f1, + out_channels=f2, + kernel_size=(1,16) + ) + self.bn3 = nn.BatchNorm2d(f2) + self.avgpool2 = nn.AvgPool2d((1,8)) + + # layer 3 + self.fc = nn.Linear(in_features=f2*(signal_length//32), out_features=num_classes) + + self.dropout = nn.Dropout(p=0.5) + self.elu = nn.ELU() + + def forward(self, x): + + # layer 1 + x = torch.unsqueeze(x,1) + x = self.conv1(x) + x = self.bn1(x) + x = self.depthwise_conv(x) + x = self.bn2(x) + x = self.elu(x) + x = self.avgpool1(x) + x = self.dropout(x) + + # layer 2 + x = self.separable_conv(x) + x = self.bn3(x) + x = self.elu(x) + x = self.avgpool2(x) + x = self.dropout(x) + + # layer 3 + x = torch.flatten(x, start_dim=1) + x = self.fc(x) + + return x diff --git a/splearn/nn/models/EEGNet/__init__.py b/splearn/nn/models/EEGNet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/splearn/nn/models/SSLClassifier/SSLClassifier.py b/splearn/nn/models/SSLClassifier/SSLClassifier.py new file mode 100644 index 0000000..7e151b3 --- /dev/null +++ b/splearn/nn/models/SSLClassifier/SSLClassifier.py @@ -0,0 +1,71 @@ +from splearn.nn.base import LightningModelClassifier +from splearn.nn.models import SimSiam +from splearn.nn.utils import get_backbone_and_fc +from splearn.nn.loss import LabelSmoothCrossEntropyLoss + + +class SSLClassifier(LightningModelClassifier): + def __init__( + self, + optimizer="adamw", + scheduler="cosine_with_warmup", + optimizer_learning_rate: float=1e-3, + optimizer_epsilon: float=1e-6, + optimizer_weight_decay: float=0.0005, + scheduler_warmup_epochs: int=10, + ): + super().__init__() + self.save_hyperparameters() + + self.criterion_classifier = LabelSmoothCrossEntropyLoss(smoothing=0.3) + + def build_model(self, model, **kwargs): + projection_size = kwargs["projection_size"] if "projection_size" in kwargs else 2048 + num_proj_mlp_layers = kwargs["num_proj_mlp_layers"] if "num_proj_mlp_layers" in kwargs else 3 + + backbone, classifier = get_backbone_and_fc(model) + self.ssl_network = SimSiam(backbone=backbone, projection_size=projection_size, num_proj_mlp_layers=num_proj_mlp_layers) + self.classifier_network = classifier + + def forward(self, x): + features = self.ssl_network.backbone(x) + y_hat = self.classifier_network(features) + return y_hat + + def train_val_step(self, batch, batch_idx): + x1, x2, y = batch + + out = self.ssl_network(x1, x2) + loss_recon = out['loss'] + features = out['features'] + + y_hat = self.classifier_network(features) + loss_cross_entropy = self.criterion_classifier(y_hat, y.long()) # self.criterion_classifier(y_hat, y.long()) # F.cross_entropy(y_hat, y.long()) + + loss = loss_recon + loss_cross_entropy + + return y_hat, y, loss, loss_recon, loss_cross_entropy + + def training_step(self, batch, batch_idx): + y_hat, y, loss, loss_recon, loss_cross_entropy = self.train_val_step(batch, batch_idx) + acc = self.train_acc(y_hat, y.long()) + self.log('train_loss', loss, on_step=True) + self.log('train_loss_recon', loss_recon, on_step=True) + self.log('train_loss_cross_entropy', loss_cross_entropy, on_step=True) + return loss + + def validation_step(self, batch, batch_idx): + y_hat, y, loss, loss_recon, loss_cross_entropy = self.train_val_step(batch, batch_idx) + acc = self.valid_acc(y_hat, y.long()) + self.log('valid_loss', loss, on_step=True) + self.log('valid_loss_recon', loss_recon, on_step=True) + self.log('valid_loss_cross_entropy', loss_cross_entropy, on_step=True) + return loss + + def test_step(self, batch, batch_idx): + x, y = batch + y_hat = self.forward(x) + loss = self.criterion_classifier(y_hat, y.long()) # self.criterion_classifier(y_hat, y.long()) # F.cross_entropy(y_hat, y.long()) + acc = self.test_acc(y_hat, y.long()) + self.log('test_loss', loss) + return loss diff --git a/splearn/nn/models/SimSiam/SimSiam.py b/splearn/nn/models/SimSiam/SimSiam.py new file mode 100644 index 0000000..e769763 --- /dev/null +++ b/splearn/nn/models/SimSiam/SimSiam.py @@ -0,0 +1,124 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class SimSiam(nn.Module): + def __init__(self, backbone, projection_size=2048, hidden_dim=None, num_proj_mlp_layers=3): + super().__init__() + + if hidden_dim is None: + hidden_dim = int(projection_size/4) + + self.backbone = backbone + + self.projector = projection_MLP( + in_dim=self.backbone.output_dim, + hidden_dim=projection_size, + out_dim=projection_size, + num_layers=num_proj_mlp_layers, + ) + + self.predictor = prediction_MLP( + in_dim=projection_size, + hidden_dim=hidden_dim, + out_dim=projection_size + ) + + def forward(self, x1, x2): + + latent = self.backbone(x1) + z1 = self.projector(latent) + p1 = self.predictor(z1) + + z2 = self.backbone(x2) + z2 = self.projector(z2) + p2 = self.predictor(z2) + + L = D(p1, z2) / 2 + D(p2, z1) / 2 + return {'loss': L, 'features': latent} + + +def D(p, z, version='simplified'): # negative cosine similarity + if version == 'original': + z = z.detach() + p = F.normalize(p, dim=1) + z = F.normalize(z, dim=1) + return -(p*z).sum(dim=1).mean() + + elif version == 'simplified': + return - F.cosine_similarity(p, z.detach(), dim=-1).mean() + + elif version == 'mse': + return F.mse_loss(p, z.detach(), reduction='mean') + + else: + raise Exception + + +class projection_MLP(nn.Module): + def __init__(self, in_dim, hidden_dim=2048, out_dim=2048, num_layers=3): + super().__init__() + ''' page 3 baseline setting + Projection MLP. The projection MLP (in f) has BN applied to each fully-connected (fc) layer, including its output fc. Its output fc has no ReLU. The hidden fc is 2048-d. + This MLP has 3 layers. + ''' + self.layer1 = nn.Sequential( + nn.Linear(in_dim, hidden_dim), + nn.BatchNorm1d(hidden_dim), + nn.ReLU(inplace=True) + ) + + self.layer2 = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.BatchNorm1d(hidden_dim), + nn.ReLU(inplace=True) + ) + + self.layer3 = nn.Sequential( + nn.Linear(hidden_dim, out_dim), + nn.BatchNorm1d(hidden_dim) + ) + self.num_layers = num_layers + + def set_layers(self, num_layers): + self.num_layers = num_layers + + def forward(self, x): + if self.num_layers == 3: + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + elif self.num_layers == 2: + x = self.layer1(x) + x = self.layer3(x) + else: + raise Exception + return x + + +class prediction_MLP(nn.Module): + def __init__(self, in_dim=2048, hidden_dim=512, out_dim=2048): # bottleneck structure + super().__init__() + ''' page 3 baseline setting + Prediction MLP. The prediction MLP (h) has BN applied to its hidden fc layers. Its output fc does not have BN (ablation in Sec. 4.4) or ReLU. This MLP has 2 layers. + The dimension of h’s input and output (z and p) is d = 2048, and h’s hidden layer’s dimension is 512, making h a bottleneck structure (ablation in supplement). + ''' + self.layer1 = nn.Sequential( + nn.Linear(in_dim, hidden_dim), + nn.BatchNorm1d(hidden_dim), + nn.ReLU(inplace=True) + ) + self.layer2 = nn.Linear(hidden_dim, out_dim) + """ + Adding BN to the output of the prediction MLP h does not work well (Table 3d). We find that this is not about collapsing. + The training is unstable and the loss oscillates. + """ + + def forward(self, x): + x = self.layer1(x) + x = self.layer2(x) + return x + + + diff --git a/splearn/nn/models/SimSiam/__init__.py b/splearn/nn/models/SimSiam/__init__.py new file mode 100644 index 0000000..ce3d777 --- /dev/null +++ b/splearn/nn/models/SimSiam/__init__.py @@ -0,0 +1 @@ +from splearn.nn.models.SimSiam import SimSiam \ No newline at end of file diff --git a/splearn/nn/models/__init__.py b/splearn/nn/models/__init__.py new file mode 100644 index 0000000..16f6e4f --- /dev/null +++ b/splearn/nn/models/__init__.py @@ -0,0 +1,3 @@ +from .EEGNet.CompactEEGNet import CompactEEGNet +from .SimSiam.SimSiam import SimSiam +from .SSLClassifier.SSLClassifier import SSLClassifier diff --git a/splearn/nn/modules/conv1d.py b/splearn/nn/modules/conv1d.py new file mode 100644 index 0000000..f5ef27e --- /dev/null +++ b/splearn/nn/modules/conv1d.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +"""Common 1D convolutions +""" +import torch +from torch import nn +import torch.nn.functional as F +from torch import Tensor +from typing import Optional + + +class DepthWiseConv1d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, padding): + super().__init__() + self.padding = padding + self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, groups=in_channels, bias=bias) + + def forward(self, x): + x = F.pad(x, self.padding) + return self.conv(x) + +#### + + +class BaseConv1d(nn.Module): + """ Base convolution module. """ + def __init__(self): + super(BaseConv1d, self).__init__() + + def _get_sequence_lengths(self, seq_lengths): + return ( + (seq_lengths + 2 * self.conv.padding[0] + - self.conv.dilation[0] * (self.conv.kernel_size[0] - 1) - 1) // self.conv.stride[0] + 1 + ) + + def forward(self, *args, **kwargs): + raise NotImplementedError + + +class PointwiseConv1d(BaseConv1d): + r""" + When kernel size == 1 conv1d, this operation is termed in literature as pointwise convolution. + This operation often used to match dimensions. + Args: + in_channels (int): Number of channels in the input + out_channels (int): Number of channels produced by the convolution + stride (int, optional): Stride of the convolution. Default: 1 + padding (int or tuple, optional): Zero-padding added to both sides of the input. Default: 0 + bias (bool, optional): If True, adds a learnable bias to the output. Default: True + Inputs: inputs + - **inputs** (batch, in_channels, time): Tensor containing input vector + Returns: outputs + - **outputs** (batch, out_channels, time): Tensor produces by pointwise 1-D convolution. + """ + def __init__( + self, + in_channels: int, + out_channels: int, + stride: int = 1, + padding: int = 0, + bias: bool = True, + ) -> None: + super(PointwiseConv1d, self).__init__() + self.conv = nn.Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + padding=padding, + bias=bias, + ) + + def forward(self, inputs: Tensor) -> Tensor: + return self.conv(inputs) + + +class DepthwiseConv1d(BaseConv1d): + r""" + When groups == in_channels and out_channels == K * in_channels, where K is a positive integer, + this operation is termed in literature as depthwise convolution. + Args: + in_channels (int): Number of channels in the input + out_channels (int): Number of channels produced by the convolution + kernel_size (int or tuple): Size of the convolving kernel + stride (int, optional): Stride of the convolution. Default: 1 + padding (int or tuple, optional): Zero-padding added to both sides of the input. Default: 0 + bias (bool, optional): If True, adds a learnable bias to the output. Default: True + Inputs: inputs + - **inputs** (batch, in_channels, time): Tensor containing input vector + Returns: outputs + - **outputs** (batch, out_channels, time): Tensor produces by depthwise 1-D convolution. + """ + def __init__( + self, + in_channels: int, + out_channels: int, + kernel_size: int, + stride: int = 1, + padding: int = 0, + bias: bool = False, + ) -> None: + super(DepthwiseConv1d, self).__init__() + assert out_channels % in_channels == 0, "out_channels should be constant multiple of in_channels" + self.conv = nn.Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + groups=in_channels, + stride=stride, + padding=padding, + bias=bias, + ) + + def forward(self, inputs: Tensor, input_lengths: Optional[Tensor] = None) -> Tensor: + if input_lengths is None: + return self.conv(inputs) + else: + return self.conv(inputs), self._get_sequence_lengths(input_lengths) diff --git a/splearn/nn/modules/conv2d.py b/splearn/nn/modules/conv2d.py new file mode 100644 index 0000000..3022a02 --- /dev/null +++ b/splearn/nn/modules/conv2d.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- +"""Common 2D convolutions +""" + +import math +import torch +import torch.nn as nn +from torch import Tensor +from torch.nn.utils import weight_norm +import torch.nn.functional as F +from typing import Tuple, List + +from splearn.nn.modules.functional import Swish +from splearn.nn.utils import get_class_name + + +class Conv2d(nn.Module): + """ + Input: 4-dim tensor + Shape [batch, in_channels, H, W] + Return: 4-dim tensor + Shape [batch, out_channels, H, W] + + Args: + in_channels : int + Should match input `channel` + out_channels : int + Return tensor with `out_channels` + kernel_size : int or 2-dim tuple + stride : int or 2-dim tuple, default: 1 + padding : int or 2-dim tuple or True + Apply `padding` if given int or 2-dim tuple. Perform TensorFlow-like 'SAME' padding if True + dilation : int or 2-dim tuple, default: 1 + groups : int or 2-dim tuple, default: 1 + w_in: int, optional + The size of `W` axis. If given, `w_out` is available. + + Usage: + x = torch.randn(1, 22, 1, 256) + conv1 = Conv2dSamePadding(22, 64, kernel_size=17, padding=True, w_in=256) + y = conv1(x) + """ + def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding="SAME", dilation=1, groups=1, w_in=None, bias=True): + super().__init__() + + padding = padding + self.kernel_size = kernel_size = kernel_size + self.stride = stride = stride + self.dilation = dilation = dilation + + self.padding_same = False + if padding == "SAME": + self.padding_same = True + padding = (0,0) + + if isinstance(padding, int): + padding = (padding, padding) + + if isinstance(kernel_size, int): + self.kernel_size = kernel_size = (kernel_size, kernel_size) + + if isinstance(stride, int): + self.stride = stride = (stride, stride) + + if isinstance(dilation, int): + self.dilation = dilation = (dilation, dilation) + + self.conv = nn.Conv2d( + in_channels, + out_channels, + kernel_size=kernel_size, + stride=stride, + padding=0 if padding==True else padding, + dilation=dilation, + groups=groups, + bias=bias + ) + + if w_in is not None: + self.w_out = int( ((w_in + 2 * padding[1] - dilation[1] * (kernel_size[1]-1)-1) / 1) + 1 ) + if self.padding_same == "SAME": # if SAME, then replace, w_out = w_in, obviously + self.w_out = w_in + + def forward(self, x): + if self.padding_same == True: + x = self.pad_same(x, self.kernel_size, self.stride, self.dilation) + return self.conv(x) + + # Calculate asymmetric TensorFlow-like 'SAME' padding for a convolution + def get_same_padding(self, x: int, k: int, s: int, d: int): + return max((math.ceil(x / s) - 1) * s + (k - 1) * d + 1 - x, 0) + + # Dynamically pad input x with 'SAME' padding for conv with specified args + def pad_same(self, x, k: List[int], s: List[int], d: List[int] = (1, 1), value: float = 0): + ih, iw = x.size()[-2:] + pad_h, pad_w = self.get_same_padding(ih, k[0], s[0], d[0]), self.get_same_padding(iw, k[1], s[1], d[1]) + if pad_h > 0 or pad_w > 0: + x = F.pad(x, [pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2], value=value) + return x + + +class Conv2dBlockELU(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=(1,1), padding=(0,0), dilation=(1,1), groups=1, activation=nn.ELU, w_in=None): + super(Conv2dBlockELU, self).__init__() + self.conv = nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding, dilation=dilation, groups=groups), + nn.BatchNorm2d(out_channels), + activation(inplace=True) + ) + + if w_in is not None: + self.w_out = int( ((w_in + 2 * padding[1] - dilation[1] * (kernel_size[1]-1)-1) / 1) + 1 ) + + def forward(self, x): + return self.conv(x) + + +class DepthwiseConv2d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, depth=1, padding=0, bias=False): + super(DepthwiseConv2d, self).__init__() + self.depthwise = nn.Conv2d(in_channels, out_channels*depth, kernel_size=kernel_size, padding=padding, groups=in_channels, bias=bias) + + def forward(self, x): + x = self.depthwise(x) + return x + + +class SeparableConv2d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, bias=False): + super(SeparableConv2d, self).__init__() + + if isinstance(kernel_size, int): + padding = kernel_size // 2 + + if isinstance(kernel_size, tuple): + padding = ( + kernel_size[0]//2 if kernel_size[0]-1 != 0 else 0, + kernel_size[1]//2 if kernel_size[1]-1 != 0 else 0 + ) + + self.depthwise = DepthwiseConv2d(in_channels=in_channels, out_channels=in_channels, kernel_size=kernel_size, padding=padding, bias=bias) + self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=bias) + + def forward(self, x): + x = self.depthwise(x) + x = self.pointwise(x) + return x + + +#### + +class Conv2dExtractor(nn.Module): + r""" + Provides inteface of convolutional extractor. + Note: + Do not use this class directly, use one of the sub classes. + Define the 'self.conv' class variable. + Inputs: inputs, input_lengths + - **inputs** (batch, time, dim): Tensor containing input vectors + - **input_lengths**: Tensor containing containing sequence lengths + Returns: outputs, output_lengths + - **outputs**: Tensor produced by the convolution + - **output_lengths**: Tensor containing sequence lengths produced by the convolution + """ + supported_activations = { + 'hardtanh': nn.Hardtanh(0, 20, inplace=True), + 'relu': nn.ReLU(inplace=True), + 'elu': nn.ELU(inplace=True), + 'leaky_relu': nn.LeakyReLU(inplace=True), + 'gelu': nn.GELU(), + 'swish': Swish(), + } + + def __init__(self, input_dim: int, activation: str = 'hardtanh') -> None: + super(Conv2dExtractor, self).__init__() + self.input_dim = input_dim + self.activation = Conv2dExtractor.supported_activations[activation] + self.conv = None + + def get_output_lengths(self, seq_lengths: torch.Tensor): + assert self.conv is not None, "self.conv should be defined" + + for module in self.conv: + if isinstance(module, nn.Conv2d): + numerator = seq_lengths + 2 * module.padding[1] - module.dilation[1] * (module.kernel_size[1] - 1) - 1 + seq_lengths = numerator.float() / float(module.stride[1]) + seq_lengths = seq_lengths.int() + 1 + + elif isinstance(module, nn.MaxPool2d): + seq_lengths >>= 1 + + return seq_lengths.int() + + def get_output_dim(self): + if get_class_name(self) == "VGGExtractor": + output_dim = (self.input_dim - 1) << 5 if self.input_dim % 2 else self.input_dim << 5 + + elif get_class_name(self) == "DeepSpeech2Extractor": + output_dim = int(math.floor(self.input_dim + 2 * 20 - 41) / 2 + 1) + output_dim = int(math.floor(output_dim + 2 * 10 - 21) / 2 + 1) + output_dim <<= 5 + + elif get_class_name(self) == "Conv2dSubsampling": + factor = ((self.input_dim - 1) // 2 - 1) // 2 + output_dim = self.out_channels * factor + + else: + raise ValueError(f"Unsupported Extractor : {self.extractor}") + + return output_dim + + def forward(self, inputs: Tensor, input_lengths: Tensor) -> Tuple[Tensor, Tensor]: + r""" + inputs: torch.FloatTensor (batch, time, dimension) + input_lengths: torch.IntTensor (batch) + """ + outputs, output_lengths = self.conv(inputs.unsqueeze(1).transpose(2, 3), input_lengths) + + batch_size, channels, dimension, seq_lengths = outputs.size() + outputs = outputs.permute(0, 3, 1, 2) + outputs = outputs.view(batch_size, seq_lengths, channels * dimension) + + return outputs, output_lengths + +class Conv2dSubsampling(Conv2dExtractor): + r""" + Convolutional 2D subsampling (to 1/4 length) + Args: + input_dim (int): Dimension of input vector + in_channels (int): Number of channels in the input vector + out_channels (int): Number of channels produced by the convolution + activation (str): Activation function + Inputs: inputs + - **inputs** (batch, time, dim): Tensor containing sequence of inputs + - **input_lengths** (batch): list of sequence input lengths + Returns: outputs, output_lengths + - **outputs** (batch, time, dim): Tensor produced by the convolution + - **output_lengths** (batch): list of sequence output lengths + """ + def __init__( + self, + input_dim: int, + in_channels: int, + out_channels: int, + activation: str = 'relu', + ) -> None: + super(Conv2dSubsampling, self).__init__(input_dim, activation) + self.in_channels = in_channels + self.out_channels = out_channels + self.conv = MaskConv2d( + nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=2), + self.activation, + nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=2), + self.activation, + ) + ) + + def forward(self, inputs: torch.Tensor, input_lengths: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + outputs, input_lengths = super().forward(inputs, input_lengths) + output_lengths = input_lengths >> 2 + output_lengths -= 1 + return outputs, output_lengths + +class MaskConv2d(nn.Module): + r""" + Masking Convolutional Neural Network + Adds padding to the output of the module based on the given lengths. + This is to ensure that the results of the model do not change when batch sizes change during inference. + Input needs to be in the shape of (batch_size, channel, hidden_dim, seq_len) + Refer to https://github.com/SeanNaren/deepspeech.pytorch/blob/master/model.py + Copyright (c) 2017 Sean Naren + MIT License + Args: + sequential (torch.nn): sequential list of convolution layer + Inputs: inputs, seq_lengths + - **inputs** (torch.FloatTensor): The input of size BxCxHxT + - **seq_lengths** (torch.IntTensor): The actual length of each sequence in the batch + Returns: output, seq_lengths + - **output**: Masked output from the sequential + - **seq_lengths**: Sequence length of output from the sequential + """ + def __init__(self, sequential: nn.Sequential) -> None: + super(MaskConv2d, self).__init__() + self.sequential = sequential + + def forward(self, inputs: Tensor, seq_lengths: Tensor) -> Tuple[Tensor, Tensor]: + output = None + + for module in self.sequential: + output = module(inputs) + mask = torch.BoolTensor(output.size()).fill_(0) + + if output.is_cuda: + mask = mask.cuda() + + seq_lengths = self._get_sequence_lengths(module, seq_lengths) + + for idx, length in enumerate(seq_lengths): + length = length.item() + + if (mask[idx].size(2) - length) > 0: + mask[idx].narrow(dim=2, start=length, length=mask[idx].size(2) - length).fill_(1) + + output = output.masked_fill(mask, 0) + inputs = output + + return output, seq_lengths + + def _get_sequence_lengths(self, module: nn.Module, seq_lengths: Tensor) -> Tensor: + r""" + Calculate convolutional neural network receptive formula + Args: + module (torch.nn.Module): module of CNN + seq_lengths (torch.IntTensor): The actual length of each sequence in the batch + Returns: seq_lengths + - **seq_lengths**: Sequence length of output from the module + """ + if isinstance(module, nn.Conv2d): + numerator = seq_lengths + 2 * module.padding[1] - module.dilation[1] * (module.kernel_size[1] - 1) - 1 + seq_lengths = numerator.float() / float(module.stride[1]) + seq_lengths = seq_lengths.int() + 1 + + elif isinstance(module, nn.MaxPool2d): + seq_lengths >>= 1 + + return seq_lengths.int() \ No newline at end of file diff --git a/splearn/nn/modules/functional.py b/splearn/nn/modules/functional.py new file mode 100644 index 0000000..94eecb6 --- /dev/null +++ b/splearn/nn/modules/functional.py @@ -0,0 +1,31 @@ +import torch.nn as nn +from torch import Tensor + + +class GLU(nn.Module): + r""" + The gating mechanism is called Gated Linear Units (GLU), which was first introduced for natural language processing + in the paper “Language Modeling with Gated Convolutional Networks” + """ + def __init__(self, dim: int) -> None: + super(GLU, self).__init__() + self.dim = dim + + def forward(self, inputs: Tensor) -> Tensor: + outputs, gate = inputs.chunk(2, dim=self.dim) + return outputs * gate.sigmoid() + + +class Swish(nn.Module): + r""" + Swish is a smooth, non-monotonic function that consistently matches or outperforms ReLU on deep networks applied + to a variety of challenging domains such as Image classification and Machine translation. + """ + + def __init__(self): + super(Swish, self).__init__() + + def forward(self, inputs: Tensor) -> Tensor: + return inputs * inputs.sigmoid() + + \ No newline at end of file diff --git a/splearn/nn/modules/positional_encoding.py b/splearn/nn/modules/positional_encoding.py new file mode 100644 index 0000000..d2ab0d6 --- /dev/null +++ b/splearn/nn/modules/positional_encoding.py @@ -0,0 +1,27 @@ +import math +import torch +import torch.nn as nn +from torch import Tensor + + +class PositionalEncoding(nn.Module): + r""" + Positional Encoding proposed in "Attention Is All You Need". + Since transformer contains no recurrence and no convolution, in order for the model to make + use of the order of the sequence, we must add some positional information. + "Attention Is All You Need" use sine and cosine functions of different frequencies: + PE_(pos, 2i) = sin(pos / power(10000, 2i / d_model)) + PE_(pos, 2i+1) = cos(pos / power(10000, 2i / d_model)) + """ + def __init__(self, d_model: int = 512, max_len: int = 5000) -> None: + super(PositionalEncoding, self).__init__() + pe = torch.zeros(max_len, d_model, requires_grad=False) + position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + self.register_buffer('pe', pe) + + def forward(self, length: int) -> Tensor: + return self.pe[:, :length] diff --git a/splearn/nn/modules/relative_multi_head_attention.py b/splearn/nn/modules/relative_multi_head_attention.py new file mode 100644 index 0000000..ac25847 --- /dev/null +++ b/splearn/nn/modules/relative_multi_head_attention.py @@ -0,0 +1,96 @@ +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch import Tensor +from typing import Optional + +from splearn.nn.modules.wrapper import Linear + + +class RelativeMultiHeadAttention(nn.Module): + r""" + Multi-head attention with relative positional encoding. + This concept was proposed in the "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" + Args: + dim (int): The dimension of model + num_heads (int): The number of attention heads. + dropout_p (float): probability of dropout + Inputs: query, key, value, pos_embedding, mask + - **query** (batch, time, dim): Tensor containing query vector + - **key** (batch, time, dim): Tensor containing key vector + - **value** (batch, time, dim): Tensor containing value vector + - **pos_embedding** (batch, time, dim): Positional embedding tensor + - **mask** (batch, 1, time2) or (batch, time1, time2): Tensor containing indices to be masked + Returns: + - **outputs**: Tensor produces by relative multi head attention module. + """ + def __init__( + self, + dim: int = 512, + num_heads: int = 16, + dropout_p: float = 0.1, + ) -> None: + super(RelativeMultiHeadAttention, self).__init__() + assert dim % num_heads == 0, "d_model % num_heads should be zero." + + self.dim = dim + self.d_head = int(dim / num_heads) + self.num_heads = num_heads + self.sqrt_dim = math.sqrt(dim) + + self.query_proj = Linear(dim, dim) + self.key_proj = Linear(dim, dim) + self.value_proj = Linear(dim, dim) + self.pos_proj = Linear(dim, dim, bias=False) + + self.dropout = nn.Dropout(p=dropout_p) + self.u_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) + self.v_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) + torch.nn.init.xavier_uniform_(self.u_bias) + torch.nn.init.xavier_uniform_(self.v_bias) + + self.out_proj = Linear(dim, dim) + + def forward( + self, + query: Tensor, + key: Tensor, + value: Tensor, + pos_embedding: Tensor, + mask: Optional[Tensor] = None, + ) -> Tensor: + batch_size = value.size(0) + + query = self.query_proj(query).view(batch_size, -1, self.num_heads, self.d_head) + key = self.key_proj(key).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) + value = self.value_proj(value).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) + pos_embedding = self.pos_proj(pos_embedding).view(batch_size, -1, self.num_heads, self.d_head) + + content_score = torch.matmul((query + self.u_bias).transpose(1, 2), key.transpose(2, 3)) + pos_score = torch.matmul((query + self.v_bias).transpose(1, 2), pos_embedding.permute(0, 2, 3, 1)) + pos_score = self._relative_shift(pos_score) + + score = (content_score + pos_score) / self.sqrt_dim + + if mask is not None: + mask = mask.unsqueeze(1) + score.masked_fill_(mask, -1e4) + + attn = F.softmax(score, -1) + attn = self.dropout(attn) + + context = torch.matmul(attn, value).transpose(1, 2) + context = context.contiguous().view(batch_size, -1, self.dim) + + return self.out_proj(context) + + def _relative_shift(self, pos_score: Tensor) -> Tensor: + batch_size, num_heads, seq_length1, seq_length2 = pos_score.size() + zeros = pos_score.new_zeros(batch_size, num_heads, seq_length1, 1) + padded_pos_score = torch.cat([zeros, pos_score], dim=-1) + + padded_pos_score = padded_pos_score.view(batch_size, num_heads, seq_length2 + 1, seq_length1) + pos_score = padded_pos_score[:, :, 1:].view_as(pos_score) + + return pos_score \ No newline at end of file diff --git a/splearn/nn/modules/residual_connection_module.py b/splearn/nn/modules/residual_connection_module.py new file mode 100644 index 0000000..9351d77 --- /dev/null +++ b/splearn/nn/modules/residual_connection_module.py @@ -0,0 +1,26 @@ +import torch.nn as nn +from torch import Tensor +from typing import Optional + + +class ResidualConnectionModule(nn.Module): + r""" + Residual Connection Module. + outputs = (module(inputs) x module_factor + inputs x input_factor) + """ + def __init__( + self, + module: nn.Module, + module_factor: float = 1.0, + input_factor: float = 1.0, + ) -> None: + super(ResidualConnectionModule, self).__init__() + self.module = module + self.module_factor = module_factor + self.input_factor = input_factor + + def forward(self, inputs: Tensor, mask: Optional[Tensor] = None) -> Tensor: + if mask is None: + return (self.module(inputs) * self.module_factor) + (inputs * self.input_factor) + else: + return (self.module(inputs, mask) * self.module_factor) + (inputs * self.input_factor) diff --git a/splearn/nn/optimization.py b/splearn/nn/optimization.py new file mode 100644 index 0000000..31c8cbe --- /dev/null +++ b/splearn/nn/optimization.py @@ -0,0 +1,359 @@ +import torch +from torch.optim.optimizer import Optimizer +from torch.optim.lr_scheduler import LambdaLR +import math +from typing import Optional, Callable, Iterable, Tuple + +from pytorch_lightning import LightningModule + +############ +# Schedulers +############ + +def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, last_epoch=-1, final_lr=0.1): + """ + Create a schedule with a learning rate that decreases linearly from the initial lr set in the optimizer to 0, after + a warmup period during which it increases linearly from 0 to the initial lr set in the optimizer. + Args: + optimizer (:class:`~torch.optim.Optimizer`): + The optimizer for which to schedule the learning rate. + num_warmup_steps (:obj:`int`): + The number of steps for the warmup phase. + num_training_steps (:obj:`int`): + The total number of training steps. + last_epoch (:obj:`int`, `optional`, defaults to -1): + The index of the last epoch when resuming training. + Return: + :obj:`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step: int): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + return max( + final_lr, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps)) + ) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps: int, num_training_steps: int, num_cycles: float = 0.5, last_epoch: int = -1): + """ + Create a schedule with a learning rate that decreases following the values of the cosine function between the + initial lr set in the optimizer to 0, after a warmup period during which it increases linearly between 0 and the + initial lr set in the optimizer. + + Args: + optimizer (:class:`~torch.optim.Optimizer`): + The optimizer for which to schedule the learning rate. + num_warmup_steps (:obj:`int`): + The number of steps for the warmup phase. + num_training_steps (:obj:`int`): + The total number of training steps. + num_cycles (:obj:`float`, `optional`, defaults to 0.5): + The number of waves in the cosine schedule (the defaults is to just decrease from the max value to 0 + following a half-cosine). + last_epoch (:obj:`int`, `optional`, defaults to -1): + The index of the last epoch when resuming training. + + Return: + :obj:`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps)) + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +TYPE_TO_SCHEDULER_FUNCTION = { + "linear_with_warmup": get_linear_schedule_with_warmup, + "cosine_with_warmup": get_cosine_schedule_with_warmup, +} + +def get_scheduler( + name: str, + optimizer: Optimizer, + num_warmup_steps: Optional[int] = None, + num_training_steps: Optional[int] = None, +): + schedule_func = TYPE_TO_SCHEDULER_FUNCTION[name] + + return schedule_func(optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps) + + +############ +# Optimizers +############ + +""" +Layer-wise adaptive rate scaling for SGD in PyTorch! +Based on https://github.com/noahgolmant/pytorch-lars +""" +class LARS(Optimizer): + r"""Implements layer-wise adaptive rate scaling for SGD. + Args: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float): base learning rate (\gamma_0) + momentum (float, optional): momentum factor (default: 0) ("m") + weight_decay (float, optional): weight decay (L2 penalty) (default: 0) + ("\beta") + eta (float, optional): LARS coefficient + max_epoch: maximum training epoch to determine polynomial LR decay. + Based on Algorithm 1 of the following paper by You, Gitman, and Ginsburg. + Large Batch Training of Convolutional Networks: + https://arxiv.org/abs/1708.03888 + Example: + >>> optimizer = LARS(model.parameters(), lr=0.1, eta=1e-3) + >>> optimizer.zero_grad() + >>> loss_fn(model(input), target).backward() + >>> optimizer.step() + """ + + def __init__(self, params, lr=1.0, momentum=0.9, weight_decay=0.0005, eta=0.001, max_epoch=200, warmup_epochs=1): + if lr < 0.0: + raise ValueError("Invalid learning rate: {}".format(lr)) + if momentum < 0.0: + raise ValueError("Invalid momentum value: {}".format(momentum)) + if weight_decay < 0.0: + raise ValueError("Invalid weight_decay value: {}".format(weight_decay)) + if eta < 0.0: + raise ValueError("Invalid LARS coefficient value: {}".format(eta)) + + self.epoch = 0 + defaults = dict( + lr=lr, + momentum=momentum, + weight_decay=weight_decay, + eta=eta, + max_epoch=max_epoch, + warmup_epochs=warmup_epochs, + use_lars=True, + ) + super().__init__(params, defaults) + + def step(self, epoch=None, closure=None): + """Performs a single optimization step. + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + epoch: current epoch to calculate polynomial LR decay schedule. + if None, uses self.epoch and increments it. + """ + loss = None + if closure is not None: + loss = closure() + + if epoch is None: + epoch = self.epoch + self.epoch += 1 + + for group in self.param_groups: + weight_decay = group["weight_decay"] + momentum = group["momentum"] + eta = group["eta"] + lr = group["lr"] + warmup_epochs = group["warmup_epochs"] + use_lars = group["use_lars"] + group["lars_lrs"] = [] + + for p in group["params"]: + if p.grad is None: + continue + + param_state = self.state[p] + d_p = p.grad.data + + weight_norm = torch.norm(p.data) + grad_norm = torch.norm(d_p) + + # Global LR computed on polynomial decay schedule + warmup = min((1 + float(epoch)) / warmup_epochs, 1) + global_lr = lr * warmup + + # Update the momentum term + if use_lars: + # Compute local learning rate for this layer + local_lr = eta * weight_norm / (grad_norm + weight_decay * weight_norm) + actual_lr = local_lr * global_lr + group["lars_lrs"].append(actual_lr.item()) + else: + actual_lr = global_lr + group["lars_lrs"].append(global_lr) + + if "momentum_buffer" not in param_state: + buf = param_state["momentum_buffer"] = torch.zeros_like(p.data) + else: + buf = param_state["momentum_buffer"] + + buf.mul_(momentum).add_(d_p + weight_decay * p.data, alpha=actual_lr) + p.data.add_(-buf) + + return loss + +class AdamW(Optimizer): + """ + Implements Adam algorithm with weight decay fix as introduced in `Decoupled Weight Decay Regularization + `__. + + Parameters: + params (:obj:`Iterable[nn.parameter.Parameter]`): + Iterable of parameters to optimize or dictionaries defining parameter groups. + lr (:obj:`float`, `optional`, defaults to 1e-3): + The learning rate to use. + betas (:obj:`Tuple[float,float]`, `optional`, defaults to (0.9, 0.999)): + Adam's betas parameters (b1, b2). + eps (:obj:`float`, `optional`, defaults to 1e-6): + Adam's epsilon for numerical stability. + weight_decay (:obj:`float`, `optional`, defaults to 0): + Decoupled weight decay to apply. + correct_bias (:obj:`bool`, `optional`, defaults to `True`): + Whether or not to correct bias in Adam (for instance, in Bert TF repository they use :obj:`False`). + """ + + def __init__( + self, + params, + lr: float = 1e-3, + betas: Tuple[float, float] = (0.9, 0.999), + eps: float = 1e-6, + weight_decay: float = 0.0, + correct_bias: bool = True, + ): + if lr < 0.0: + raise ValueError(f"Invalid learning rate: {lr} - should be >= 0.0") + if not 0.0 <= betas[0] < 1.0: + raise ValueError(f"Invalid beta parameter: {betas[0]} - should be in [0.0, 1.0[") + if not 0.0 <= betas[1] < 1.0: + raise ValueError(f"Invalid beta parameter: {betas[1]} - should be in [0.0, 1.0[") + if not 0.0 <= eps: + raise ValueError(f"Invalid epsilon value: {eps} - should be >= 0.0") + defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, correct_bias=correct_bias) + super().__init__(params, defaults) + + def step(self, closure: Callable = None): + """ + Performs a single optimization step. + + Arguments: + closure (:obj:`Callable`, `optional`): A closure that reevaluates the model and returns the loss. + """ + loss = None + if closure is not None: + loss = closure() + + for group in self.param_groups: + for p in group["params"]: + if p.grad is None: + continue + grad = p.grad.data + if grad.is_sparse: + raise RuntimeError("Adam does not support sparse gradients, please consider SparseAdam instead") + + state = self.state[p] + + # State initialization + if len(state) == 0: + state["step"] = 0 + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like(p.data) + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like(p.data) + + exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"] + beta1, beta2 = group["betas"] + + state["step"] += 1 + + # Decay the first and second moment running average coefficient + # In-place operations to update the averages at the same time + exp_avg.mul_(beta1).add_(grad, alpha=(1.0 - beta1)) + exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2) + denom = exp_avg_sq.sqrt().add_(group["eps"]) + + step_size = group["lr"] + if group["correct_bias"]: # No bias correction for Bert + bias_correction1 = 1.0 - beta1 ** state["step"] + bias_correction2 = 1.0 - beta2 ** state["step"] + step_size = step_size * math.sqrt(bias_correction2) / bias_correction1 + + p.data.addcdiv_(exp_avg, denom, value=-step_size) + + # Just adding the square of the weights to the loss function is *not* + # the correct way of using L2 regularization/weight decay with Adam, + # since that will interact with the m and v parameters in strange ways. + # + # Instead we want to decay the weights in a manner that doesn't interact + # with the m/v parameters. This is equivalent to adding the square + # of the weights to the loss with plain (non-momentum) SGD. + # Add weight decay at the end (fixed version) + if group["weight_decay"] > 0.0: + p.data.add_(p.data, alpha=(-group["lr"] * group["weight_decay"])) + + return loss + + +def get_optimizer(name, model, lr, parameters=None, momentum=0.9, weight_decay=0.0005, epsilon=1e-6, **kwargs): + + if parameters is None: + parameters = model.parameters() + + if name == 'adam': + optimizer = torch.optim.Adam( + parameters, + lr=lr, + eps=epsilon, + weight_decay=weight_decay + ) + elif name == 'adamw': + betas = kwargs["betas"] if "betas" in kwargs else (0.9, 0.999) + correct_bias = kwargs["correct_bias"] if "correct_bias" in kwargs else True + + optimizer = AdamW( + params=parameters, + lr=lr, + eps=epsilon, + weight_decay=weight_decay, + betas=betas, + correct_bias=correct_bias, + ) + elif name == 'sgd': + optimizer = torch.optim.SGD( + parameters, + lr=lr, + momentum=momentum, + weight_decay=weight_decay + ) + elif name == 'lars': + eta = kwargs["eta"] if "eta" in kwargs else 0.001 + max_epoch = kwargs["max_epoch"] if "max_epoch" in kwargs else 100 + warmup_epochs = kwargs["warmup_epochs"] if "warmup_epochs" in kwargs else 10 + + optimizer = LARS( + params=parameters, + lr=lr, + momentum=momentum, + weight_decay=weight_decay, + eta=eta, + max_epoch=max_epoch, + warmup_epochs=warmup_epochs, + ) + else: + raise NotImplementedError + return optimizer + + +#### + +def get_num_steps(litmod: LightningModule): + + dataset_size = len(litmod.train_dataloader()) + train_batches = dataset_size # // litmod.trainer.gpus + total_train_steps = (litmod.trainer.max_epochs * train_batches) // litmod.trainer.accumulate_grad_batches + num_warmup_steps = (litmod.hparams.scheduler_warmup_epochs * train_batches) // litmod.trainer.accumulate_grad_batches + + return total_train_steps, num_warmup_steps diff --git a/splearn/nn/utils.py b/splearn/nn/utils.py new file mode 100644 index 0000000..d12125c --- /dev/null +++ b/splearn/nn/utils.py @@ -0,0 +1,42 @@ +import torch +from itertools import product + +from splearn.utils import Config + + +def get_class_name(obj): + return obj.__class__.__name__ + +def get_backbone_and_fc(backbone): + backbone.output_dim = backbone.fc.in_features + classifier = backbone.fc + backbone.fc = torch.nn.Identity() + return backbone, classifier + + +class HyperParametersTuning(): + ''' + Example usage: + >>> configs = { + >>> 'num_layers': [8,16], + >>> 'dim': [128,256], + >>> 'dropout': [0.5], + >>> } + >>> + >>> all_model_config = HyperParametersTuning(configs) + >>> + >>> for i in range(all_model_config.get_num_configs()): + >>> print(all_model_config.get_config(i)) + ''' + def __init__(self, config): + self.all_model_config = [dict(zip(configs, v)) for v in product(*configs.values())] + + def get_num_configs(self): + return len(self.all_model_config) + + def get_config(self, i, return_config_object=True): + if return_config_object: + config = Config(self.all_model_config[i]) + else: + config = self.all_model_config[i] + return config diff --git a/splearn/utils/__init__.py b/splearn/utils/__init__.py new file mode 100644 index 0000000..611cb4f --- /dev/null +++ b/splearn/utils/__init__.py @@ -0,0 +1,2 @@ +from .config import Config +from .logger import Logger \ No newline at end of file diff --git a/splearn/utils/config.py b/splearn/utils/config.py new file mode 100644 index 0000000..e395aaa --- /dev/null +++ b/splearn/utils/config.py @@ -0,0 +1,17 @@ +from types import SimpleNamespace + + +class Config(SimpleNamespace): + def __init__(self, dictionary, **kwargs): + super().__init__(**kwargs) + for key, value in dictionary.items(): + if isinstance(value, dict): + self.__setattr__(key, Config(value)) + else: + self.__setattr__(key, value) + + def __getattribute__(self, value): + try: + return super().__getattribute__(value) + except AttributeError: + return None diff --git a/splearn/utils/logger.py b/splearn/utils/logger.py new file mode 100644 index 0000000..a44e61a --- /dev/null +++ b/splearn/utils/logger.py @@ -0,0 +1,32 @@ +import json +import os +from datetime import datetime +from pathlib import Path + +class Logger(): + def __init__( + self, + log_dir="run_logs", + filename_postfix=None, + ): + # create dir if does not exist + Path(log_dir).mkdir(parents=True, exist_ok=True) + + # get this log path + now = datetime.now() + date_time = now.strftime("%Y_%m_%d-%H_%M_%S") + + filename = date_time+"-"+filename_postfix if filename_postfix is not None else date_time + + self.log_path = os.path.join(log_dir, filename+".txt") + + def write_to_log(self, content, break_line=False): + + content = str(content) + + with open(self.log_path, 'a') as log_file: + tofile = content + "\n" + if break_line: + tofile = "\n" + tofile + + log_file.write(tofile) diff --git a/tutorials/Butterworth Filter.ipynb b/tutorials/Butterworth Filter.ipynb new file mode 100644 index 0000000..36e242c --- /dev/null +++ b/tutorials/Butterworth Filter.ipynb @@ -0,0 +1,233 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from splearn.data.generate import generate_signal # https://github.com/jinglescode/python-signal-processing/blob/main/splearn/data/generate.py\n", + "from splearn.filter.butterworth import butter_bandpass_filter_signal_1d, butter_bandpass_filter" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "signal_1d = generate_signal(\n", + " length_seconds=4, \n", + " sampling_rate=100, \n", + " frequencies=[4,7,11,17,40, 50],\n", + " plot=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXwAAAEICAYAAABcVE8dAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8GearUAAAgAElEQVR4nOy9ebwtWVUm+O2IOOfc6Y35XpIjJLMFYqKVJohaUq0oqFXYzlpdYotiOrRtlV3VVGFbDmBTrWUrYovQKuIAWlpQqYDMo4yZJENmAmaSA/nyZeabhzudcyJi1x9774i1V6yI2HFv3CHvO+v3e7937z3nxLDPjrW/9a1vra201pjZzGY2s5ntfYt2+gJmNrOZzWxm22Mzhz+zmc1sZpeIzRz+zGY2s5ldIjZz+DOb2cxmdonYzOHPbGYzm9klYjOHP7OZzWxml4jNHP7M9pwppZ6rlDq209fhTCn1PyulHlBKLSulvlopdYdS6rn2tV9WSv3ZDl7b65VSL9+p889se23m8Ge2LaaUuk8ptWad3lml1FuVUtfu9HVtk/0mgJ/VWi9prW/TWj9da/1+/ial1HVKKa2USrb/Emd2KdjM4c9sO+1faK2XAFwJ4BEAv7vD17Nd9jgAd2z1SWYLxczabObwZ7btprVeB/DXAJ7m/qaU+g6l1G1KqQuW/vhl8ppDvi9SSn1ZKXVKKfUy8vq8pSbOKqXuBPC19Hw2uvgPSqk77Xv+WCk1Z187pJT6O6XUSfva3ymlriGf/VGl1D1KqYtKqXuVUv/K/v1JSqkPKKXO2+v5S36fSqmRUmoZQAzgM0qpL5Hr+RZhaD5o/z9nI6Gvs+//MaXU5+31vUMp9ThyDq2U+hml1F0A7rJ/+06l1KeVUueUUh9RSn0Vef9XK6U+Ze/nLwHMNX9bM9tLNnP4M9t2U0otAPgBAB8jf14B8CMADgL4DgA/pZT6LvbRbwDwVADfDOCXlFL/xP79PwF4ov33bQBeJJz2X9nXngjgKQB+0f49AvDHMCj8sQDWALzaXucigFcBeIHWeh+A5wD4tP3crwF4J4BDAK6BEK1orcc2ogGA67XWT6wdFGP/zP5/0NI/H7Vj8B8BfDeAowA+BOCN7HPfBeBZAJ6mlPoaAH8E4CcBXAbgDwDcbBefIYC3APhTAIcB/FcA39NyTTPbQzZz+DPbTnuLUuocgAsAngfgN9wLWuv3a60/p7XOtdafhXFq38Q+/yta6zWt9WcAfAbA9fbv3w/gFVrrM1rrB2CcNLdXa60f0FqfAfAKAD9kz3taa/03WutVrfVF+xo9bw7gK5VS81rrh7TWjpqZwiwSV2mt17XWH97wqDTbTwL4v7XWn9dapwB+HcAzKcq3r5/RWq8B+AkAf6C1/rjWOtNa/wmAMYBn238DAL+ttZ5qrf8awCe36Lpntgtt5vBntp32XVrrgwBGAH4WwAeUUlcAgFLqWUqp91lq5TyAmwAcYZ9/mPy8CsCh56sAPEBeu184N3/9KnveBaXUHyil7ldKXYChVQ4qpWKt9QpMJHITgIdsovkr7DH+PQAF4BNWdfNjXQaigz0OwO9YeuYcgDP2vFfX3NvjAPyCe7/9zLX2fq8C8KD2OyZKYzWzPWozhz+zbTeLPP8bgAyGpgGAvwBwM4BrtdYHALwGxrGF2EMwTs3ZY4X38NeP259/AYYmepbWej9KWkXZa32H1vp5MInmLwB4nf37w1rrn9BaXwWDwv8/pdSTAq+3zqTWtQ8A+Emt9UHyb15r/ZGazz0AE+3Q9y9ord8IM05XK6XouEpjNbM9ajOHP7NtN2XshTD89+ftn/cBOKO1XldK3Qjghzsc8q8A/AebgL0GwP8mvOdnlFLXKKUOw3DiLsm6D4a3P2df+0/kOh+jlPqXlssfA1iGWaSglPo+ktw9C+N0sw7XLNlJGArpCeRvr7H39nR73gNKqe9rOMbrANxkIyallFq0CfF9AD4KIAXwc0qpRCn13QBu3OQ1z+xRZDOHP7PttL+1qpULMFz5iwgn/tMAflUpdRHAL8E48VD7FRhq4l6YROqfCu/5C/vaPfafKzb6bQDzAE7BJJH/nnwmgokAjsNQKd9krxMwSqCP2/u5GcD/rrW+t8M1V0xrvQozLv9g6Zhna63fDOA/A3iTpZxuB/CChmPcAsPjvxpmIbobwI/a1yYwyd8fta/9AID/tplrntmjy9RsA5SZ7XVTSt0H4Me11u/e6WuZ2cx20mYIf2Yzm9nMLhGbOfyZzWxmM7tEbEbpzGxmM5vZJWIzhD+zmc1sZpeIbbrZkjIdD98A4AoYSdlrtda/w96jAPwOgG+HKZj5Ua31p9qOfeTIEX3ddddt9hJnNrOZzeySsVtvvfWU1vqo9Fof3fVSAL+gtf6U1freqpR6l9b6TvKeFwB4sv33LAC/b/9vtOuuuw633HJLD5c4s5nNbGaXhimlaqunN03p2P4in7I/X4QppLmave2FAN6gjX0MpnT9ys2ee2Yzm9nMZhZuvXL4SqnrAHw1gI+zl66G3+/jGKqLwsxmNrOZzWwLrTeHr5RaAvA3AH5ea32Bvyx8RJQHKaVeopS6RSl1y8mTJ/u6vJnNbGYzu+StF4evlBrAOPs/t02xuB2D37zqGpTNqzzTWr9Wa32D1vqGo0fFvMPMZjazmc1sA7Zph28VOH8I4PNa69+qedvNAH7ENnN6NoDzWuuHNnvumc1sZjObWbj1odL5egD/GsDnlFJuN6D/CNt2VWv9GgBvg5Fk3g0jy/xfezjvzGY2s5nNrINt2uHbnX4a+5bbDRd+ZrPnmtnMZjazmW3cZpW2j1K75+QyvnRyeacv41Fjp5bHePvnZixiqGmt8de3HsM43WyL/0vHjp1dxYX16U5fRqPtaYd/anmMB86s7vRlbIn9T//lA/jm//KBnb6MR429+PWfxE/9+ad2/QO5W+y9XziB/+O/fga/9a5/3OlLedTYN/zn9+Ff/O5WbW3cj+1ph3/Dy9+Nb/x/3rfTlzGzXWD3nloBAKxNZog1xM6vmYXxxIXxDl/Jo8vuP727Aeaedvgzm5kzt43ryjjd4St5dNmsm26YPVrGaebwZ3ZJ2cp4hvBn1r+N03ynLyHI9qzDn4XuM6NmAT6WZwg/yKaZcWCPDty68/Zo8Td71uEfP7+205ewZZbn5WOYZv0hizTLcfuD53s73m60vimdf3zkYuEc95Itb1Ek9IWHL+xJIcXqdObwd9SOnysdfpbvLZyyTqRyfVIUv/K3d+I7f/fDOHZ27z2QrlBkZdKfw//SyWV86//7QfzOu+/q7Zi7xVbtwtg3Nf383/7QnhRSrJF5tZv5/EvC4U92mF97xx0PY7VHR0NpieUej/vOOx8GAKxPd3a8Tlxcx/nVrZFP9knpfPHhi+b/Ry72dsyN2OokxYPn+o1oVyxFsd4jct1rwIvaKqF0VnYxvbNnHf7F9fLB7nPSdrUvn17FT/7prfj5N326/c2BtkpQ/WqPDuwRK8Hb6QXyxle8B1/7inf3ekyn0lntMSI6cWEdAHD5vlFvx9yI/exf3Iavf+V7Papvs+aorz4jouM9L0q7yajDv7C2e2s99qzDn2bl5F/fwWrBaW6c58fvPdPbMelDuBVJyN1QXTnZIl68z/F62C6QhxeHvR1zI/bhu04B6Ddv5eZYn1z+buHu7zx+Ade99K295qto0vb8zOFvv9FE2k5SFGN77j4ngRc+9vRAUt5xpykdZ30mpB2l1mfS1uU6KLjYCbvq4ByAsrisD3ORUJ/jdb91+Pvm+ujZuHH7h7vNAvnXtx7r7ZgzhL/DRp3FTiLWrTi3x+H39ECeJZz5TkZE1I6d7QexZrkuFrE+KQqHWHc6IrrywDwA4J6T/Tn8lS1YIL9sx2unI6KjloI7cXG9t2PSHN0M4e+ATbLdgVjpuftyDJSH7uuBPLNSltCPdwnC7wuxrnoUWH/O+dTyBMDOR0T75w1i7hPhu3nVJwV2btWM13SXFCk90mPbiDWSJ9zNtR571uH7lM7uQPjHz/WDKChK7QuxTtJygdxJxEqppftP9+Xw+18ggXKOjXdYg+2S7H2NF1CO2co47U1m6Kivna5Kdf7gkQt9IvxyDuzmuow96/DTXeLwtwbh90/ppDm5zh1ErDRZu96TY6BjtCUOf8cdWO7934c5IJHr/o7rxmsnn0d6/j4bw/kOf/fKT/esw98tlA518tO0n4lAdb59yQy9iGhHcx7ldfQV+nsy1h410u7B3mkH5uZYn8hyxRuzfhbJwuHv8ALp5tgky3urDaCFVzOEvwO2a5K2ZLHpS2q4NsmgFDBMokL2uVmjlM7ORkT9h8aOX10Yxr0+jLsN4fd5b2uTDAvDGACQ9uQU3QKZ5bpXBVZXowCwzzk2TIw7Tfc6wldK/ZFS6oRS6vaa15+rlDqvlPq0/fdLfZy3yaZZjiQyxTY7SVF4CL+nyTXNcwziCINI9Ta5dgulQ8897cnRuHtbGMa9OS9g91AUJcLv796yXBcOv7d5uwV03UaMRrB9zYfVSYb9cwMA6A2EbYX1hfBfD+D5Le/5kNb6mfbfr/Z03lqb5hpLVu+7WyiKvpxzlmkMIoUkjnpDSnuZ0nHjPjeIexuvLNdwvmKnZaxbgfCneY65gUX4Pc1ben07mej2QEVPc2x9mmG/9Td9UbdbYb04fK31BwH0V0rag03THEsj6/D3GEWR5hpxpJBEqjeE4ql0dlTGuhXjZY4zN4h7Q8G+89odnHRf45XlGlqjdPg9IVa/+n13IPz+oheNYRJBqf7GaytsOzn8r1NKfUYp9Xal1NPr3qSUeolS6hal1C0nT57c8MnSXGOfDbF2ltLpn8OfZobSSeKtoXR2C8Kf9OacHcKPenReu2O8gBIt97WYlQuk5aR74/B3B8L3QEVftGHmaNZoptIB8CkAj9NaXw/gdwG8pe6NWuvXaq1v0FrfcPTo0Q2fcJrlmB9EiCO1ow/kliDWTCOJFZKov6TtVrWiWJ9mnSKs8RaMl1NizA/i3lQZ9KHuE1Bkue680TpVnfRhDkTMbyGls7PKuS2gDW3UPYjVTKWjtb6gtV62P78NwEApdWQrzznNciRxhLkk6nVyvf1zD3Uqnd4KDj/NNZIowqBHhO94x2ES9ao6ueHl78bXv/K9we/3x6vfxaxPSsdd2zCOeqUMf+3v7sRX/fI7gzuWZrkuHH1f40VzHkC/QGVklSx9gTCtNd7+uYc6LeRbASrSTGMQ95tX2wrbFoevlLpC2f60Sqkb7XlPb+U5p5nGMI4wN4g7PZDHz63hvpoS9TMrE/zUn38KL379J4OPR8/dGwLLc4Pw4/4oCndt++eSXh3Y8jjF6ZVJcLWmr2rqyzn3n7R147VvLumVj37nHWZPgrtOhPXYpwtDb/kJkvMA+qN0JlneO8367s+fwE/9+afwe++7O/gz61sglc4ICOuLJtoK60uW+UYAHwXwVKXUMaXUi5VSNymlbrJv+V4AtyulPgPgVQB+UG/xtjBp5pxiNxT8nFe+F8/9zfeLrzk99y33nw0+3jjNe5e3pZlGYpO2fSPWpVHSCeGvTzN88r72fH1o3xL3MI6SqNeHEbAIv2dKZ2kuwSTNO7Uf+MS9Z2oR/JMesw8Aglv3usV5q8YL6FeWuW8DyrnffMcXcfNnjouvuZ3MPmw7YIYYBRW9RcgWhA3iaNf0CpKsL5XOD2mtr9RaD7TW12it/1Br/Rqt9Wvs66/WWj9da3291vrZWuuP9HHeJptk2iQ2e+S5N4J816dZoRbqayL4Sdt+FRRLHRH+z73xNnzfaz6KMyuTxveFOjD3MO6bS3qtWwCA+UF/4TZdIIHw4qtP3HsG3/8HH8UffOBL4uuPsZ0cb3/wQtDx3Hl7Ha+sHC+gRyoy0+V4BSL8NMvx6vfdjZ97423i6+7K7j6xHHwd69Mcg9gsFb2DsLg/5dxW2J6ttDVO0XwBofxeW7KMOsLQHjbjNC/qAfqaCFkhy4x6DbcBYN9o0Anhv/PORwDUl9+7B+uuwAfSIfx9c4NeHQ1gkpC5Ri87Q024ww90YLfcb6KhszVbOLrjhlI65QI5gNb9bCPIk7b9JbpzskCGgYp7WjqAumfyTAfakIKw/mhWjTiKDMK/1Dn8nTAnk0o6VKO2oQTK/dXx/NXPZNjX8+Sa5hpJ3G/S1h1ncZRsSDJXlxiP7NaCa4HHdI5gaZT0KmMF+uWkXUTkOOlQisLNsSsPzImvOwe2FriAuHEvosgexixlHH5fDmxCwE/oAnnncRPpHFmSt5GkICy0T5IHwnqkRAexsrLMmcPfdptmLokS/gW0OXzqCEMRCp1cfVXgpVluKm2jfnXlcaSwMAxPctPOk9JntNZFtBA6XtSB9S3L7LOQyFE6+zo6sC/ZTUrqFjN3/6GLLl0gm47bxdLKePWnLuvK4d/5kHH4Vx+aF1+n0WhoZGpAmG2D0GfStufamK2wPezwcwwThTgKp3ROtPTH9iZXBwQ2P0gQR/3pc9NcFwnpPitHB7HCMImCJYH3ny73KJUc+kbGa5xmiCOF+R4bnTmHNV8kz/ujdBZH5pihC5rbyLvOObmFM/Q7KBbIuf7yRAWlswW9dIqFKfA6T140yf6sZpFe3wAIW59m5Xj1mCdKIpu0nXH422+meVqEpMMX4BVkCBPBn1zhDmw0iHotyEjtvQ16lGVOM41BR1kZRWmSQ98I+hpPc8wlPdcYOEqn6Ga4+TFzi8bCsFt+xs2hOgRfRkTh8wsoI41+6Co+Xps/ptYa00wXeYHQRdfdXx1luDFQkROatT9KNHGFV3tdpbMbLbUqHdNRMnwiOJMoCs/BhVI60xyjpN+Sa4fw4x67ZU6zHIMk6hQR0QdMCtE3SoENE7OY9Vk5GilgkPTXKiDleYFgB2Y+V0ebFQtCh/kFoDNybjJOgfWSCGZRVvAzOW0er3FHEOZoxmKB7DXqjnqlWbfC9qzDnxCVTugDPvEcvoTwuyPWNM8Nck76S+aU+Yn+JGCFqqlD0ok6pb7Gy0lO+1Q7FJXJUVScY7NWShctYg14yLXWxRxrQ6yhaNVdh4s0+rk3Run04MA2mjgft4zXuhdFti+S7niLPSa5ARd1KwySqLeoYStszzr8NC91+OEInzowAeFPmykMyTKLxvukdDLLF3a5tzbzFpGOaBWQx2vcQvnUXcfAKpD6THK7nAfQD2KdFJROOML3xqvGOblxDE1qOvqtK1XSZFyl00cU6a6rUM4FLiJuDtVRYF1pVrfQuO+tN0rHPecdGIWdsD3p8PNcF462C8IfT5vRwkb2p50W3F6/FMVWJG2HSdSpXcO4U0QUNl4prVjsFeGbVhRAT06RIfyQh7xtgaTvmWY6aGHKyOYu5nP9JW37lGW6Yww7Vr+35TS6grCs0hiux2cy6l7Zv922Jx2+C0GLStsNIdZ+krapbeI2iKNey7hdK9Y+ZZlJxx77bRx9W1JXsrLrYJ8Ov9whzP2+WSsoig7bAHoRT4ADC+Hji9bPfTp8x+H3mPNw15V0bCHs5k3d/rPjjpROUXXdIwUGOKAyK7zaESvDR+PA6iRd3FopnYAHtnItW0DpbAWacFRKEkXBFZuhC+TiMA5H+JnJeQyTHpPcmVlEYufwe5FlWmqgg7bfS3LX1C3QKtCQMUvZdfQavQx7TNoSSieON/ZMylG32d/ZvB5A6TAqrk8hxaCQZc4c/raam7BdNwlpc2DjaU4KbcIcWJY7uWPPFEXPtIdT6TieO+S41GmJHL4dwwPz4e0ayl7//S2QZV6gv6Stm2NdHEfb/Erttoluq7wwTtp3zr3QL1vQPG1SPJNuH4e+ou682Es2COEzKq4vVZPWQBx1q+zfCduTDn9Cw8cOK+44zbFoH5y6JOTcIMYosGe81rroe2M4/B6TkFE3CWWbTW31rut9E0ZRNCch3d/2d3H4RdsI0yeoj6aqZTvp8HtrswqlE/Ddti2Q7m/758NbCPO+N33kiRz6HsYRItVX0rYEYYMOTf/Wp1kxJ+vG7ECX8bLf/SBRnZLHTVbSVapXNd5W2J50+A5tDa1WPeuA8N3DJjqwaY65QRTs8Cm11GdBhpe07W2LNoOCYytdDBmzolPjKBEfNofI9s8PgiOi1CqQhkmPCVbSbA7oV3Uy34XSseO1f05uQb1OIiLz/gBKhyH8Pu8ttonuPigKSul0jboPzA+Ln+XXB7Wvc3OLWRxFvYkeHOhKIqPSmW1xuM3m0EPXDQnGBC3UcdJzSYzRIO4UbpeItT9Kp0ja9rihx8BWuAJh2utxmmGURJir4egdIutC6ZSqpv7a17q8QBm99IfqumwD6MbowIK8ANLxMu8PBxVbodIpCxf7a0XhKJ0uOvwD87b/ThvCD6J07L25qLvHVhSml85sx6tttyJ8TLrp8CcZQfg1k6ukdMInV9I3pZPnFn2p3tr9OkqnCwp2VcRzA3kbyRLRduHw8y3g28vKZPf7Zs2pmsrq3XCEf2B+IO6SVYxXF4TPIo0+u2W6MeuDNnTRbRcdfmqVOQcankkTlYc3sKPOedgTCEvzcjEzebUZwt9Wo5ROVx1+0+TqSul4oV5PO+G4niQOoQD9VUK6cNv93mbjNMdoEGMukTts+gg/PMnttm8E+uv+mPS8iBSJ4EgVv7cZTWK3jRd9f5M5imKU9JeELBB+j2KDgju3KLhLkrsp6h5PMywOTXPCoIiILGbmmeyvbUSxifleV+kopf5IKXVCKXV7zetKKfUqpdTdSqnPKqW+po/z1llacJDdsuZtfOB6mmGUxBglcSCaoJROP6qTYhGxSMmcpx/ZHKV0wmSZltKp2Td4bCVzZjemsEIiR+kM4x7vzVUm95y0HZCoIXS8gNLh84Q0fd38HuLAdNHlFOipT5DjuTewRWid+ZROWNKWRzyyMCDvFHXTxSxpeCbf98UT+MLDYbuOFYyCXSD72ohmK6wvhP96AM9veP0FAJ5s/70EwO/3dF7RaEjarXLUaKCVqpcZzg0ijAaBlE6BalShOtmsUTRRUBQ97eA0iFSRtA2lKJoonfXi9XD0aZxz/2g8IXRVX5WjDq2GHpNSXLmuRgUbStoWnVP7y3lQKrKvLUI9Sicw6q4sgHV5tQ5Rd7GYRQrDhur3l/7NZ/F775O3oeSWkWeyS4S8E9bXnrYfBNC0k/ULAbxBG/sYgINKqSv7OLdkxRegbG+LDgmi0SCqpSjGBcIPnFxe8rinBBFbROh5NmNF07KOFIWLeCT0NUnNMUcWfYapTsoaA3ddm7UyL9BfL52iG+sGZKx1Dr2ybWJgbxgnBwR6pnR63FWNUjpGbNCNAgPk+WNAh52DAUqw8t5UbfW71hqnlyc4ZXvxt1mxQNodr8zf9rDDD7CrATxAfj9m/1YxpdRLlFK3KKVuOXny5IZORrnzOLBy1HUyHCVx7SYg7uEaJaEqnXIiDJP68PHn33QbXvRHn2g9HuAvIn1SFKXUs0PS1vb6nxtEIvrKrJpoNHAOPyypRlU6E4FjfetnH8LTfunvg3fmcnmBfittrbY/Cl90x0xnz6MiJ4Vd7LBPbjFeUb2M9eTFMa576Vvx0S+dbj0eUOYFIoVOEXKTUa26QfjdktxANep2PbPcHOusnKt5Ji+spUhzjTMrk9bjAaVv8QDALk3cbpfDV8LfxBHRWr9Wa32D1vqGo0ePbuhkGwmxHLoaJU5FUL281HaUHCVRkK68UARErqePfA1v+fRxfOAfwxY3D+H3iCaK7qKdZJmGshkNZITv1EQuoRia92hLsL7irXdidZLh4fPNO5Q5ow3sgH6S3M7RJB1otTYH5pxRl120yh4u9fP8Vrtx+h9++J7W4wFlXkAp1SkH1nidGQVhYVp1vrkLB2EUUJlnMlzG6ugqidI5tWKQ/elAh+/G3NUt0L/tNtsuh38MwLXk92sAHN+qk/GsOf1bnbmHcZTUF4a45N9oEAc2tupf3uYlpDuiifOrU1xYn4qvZblGZB9w93ubuXB6UHNvqVUTdaF0nLMpkqFCpa37W929VK7D5gW6JrnXpxnuPy1vVp/mZlOVKFLB1ahuji3U9Khxc3SxA6XjFFtNyWNaSBViLi8AoJPKDQBOXJQXYfeduarzLr2a3Hjw66CRvIm62+dXRqPuGkrHIfuzq5MgybNPszrAdGkj/JsB/IhV6zwbwHmt9UNbdTI6uUIrRx06GDntvvCFOWogPEFUToSkxilSpUYILeCHxuEJVgC4/lffiRte/u6aa/UpiqAk5NSodOIaTjbNNeJYFUnbUCmraxvhfpfeAyA45OZ0VSj6+rk33oZv+o33i99Lbq8TQHA1qlM11X1vbgwXh+EOP7NjrJRZeKTxOr9mFsZgh2/nOYBOG+J88r4zuPEV78HbP1d9tEvnHAX3SXLPpBsPPsemJAEbXv3uU6LSdZxedvvo6mLsmsyr3u1A8e2E9SXLfCOAjwJ4qlLqmFLqxUqpm5RSN9m3vA3APQDuBvA6AD/dx3nrLMsomgijKBw6GCVxLcfoqIFQCRidCHENUloep8XPIZOLLyL0byFWF5nkOYuIAhHraBDbnbfknIfrfAnU94CnNs3y1s6WznmcXQ10+LmfFwgdr3d9/hEA8sLi2jUACK5GLbe7lK+j2AZwENcqxbhNbRUxgFqgcso6MKVCEb4u5laXpO3tD54HAHzsnmquwF1XFCFYseaesVqET5LLwRw+SdrGNdQtpXJOr7Qnbmn1bp95ta2wpI+DaK1/qOV1DeBn+jhXiFFKJ7Ry1DnC0SCqnQiZLeCJo/ot16jRiRArGeGfWi4n19nVCS5bGjUe0y0iG6EomqzUqneQZRYIv4YCs8572GHjEaN+Kb+3XKJ0CoQfSOnYvIA7Zii1pmASTaeWJ7h8/1zlGpzDT4IpigyjQVy7mKUkegvdMMdFL4CZ79J4OYd/IQBQAGVeAHCLSBhajVQ9DZcThB931OEv1OyD6yP8OBAwlUnbuqj7NHkmTy9P8KTLm4/p18b0169pK2xPVtq6Se85xVaEX1I6dT1qXDn9MAl/GIFycjWhLyDMgfF2De662oxGJJyX1Nq05aUcfjDCT6JaxOZQ8DCxipuQMbPOpqnGwPsqLIIAACAASURBVB3nbGhSLddeRBRKUTgHRr+j8jqJw+9AUbjxcsfgx3THGwZWgbr8hPuc9L2dulhy0iHm8gJANw5/zUYk0ts9ENZRh9/G4ZfNCduP6YGwSF6oaUQXQhv6Sdv++jVthe1Jh19OLgQnNkuHH9cmWB2HP7TcX1vrXlrG7XIJ3NlSrW/I5PIbNYWHj/TY5xgSosmvLsccp2ZbxDrE5pQsxcIUmPcwPX0ch+9/ZmWcFtHVmUAH5vICXRKsAIqNNSSHn+cl7RFajUrHC6giVooUQyuzKd9et7HISXv9ZwMjIhfJAvWLiGRujq1NqlRUGZmqzjr8IoqsyXnEUYRhEgeNl6fgq3nOz69NCxoyZI5RmqiL6GEnbE86fL8FahhF4SidYgNtNiG11hbVhZdPZ1m7I6XOJASB0UWkS4LVD1N9B1YskB2POclykvOQ1CF5ocoIOSbdSKKO9jhJFshghG+vAwhPsAIl503HzpmTnAIIrkYtxqsmeqFIcRjYV53y7bFqjiJDk9wukgXseAVGRG6c6iIigCL88ATrsEYqTSOiQayCIki61WJccx2TNMehBSOdlRYvbuV1lPN2tzZQ26MO3/zv+lMD7YiVTi4J4RfhY1RWNbZ9qSnh2wt+kx333GqJukIeyIxNciAMsdLF5CR7IL3K5MBjmiZuuWlQ15DzoH1e2h5ILmMFqhw+5Wm7qHTcfYUmWPNcFwhccmCUww9NbBbjVVPc5lMUYbThNCv59joO343Z2jQLc2AkLzDoQOmcsQnOU8ICmTNkHeIQJwVytnx7Q85jGLgwUdFDXV5tnOY4aHvwr4zD9ySgFeIzhL+NVlQKtqg9qHnNnYTJQ1Gw+1LbHZikqPE/M8lyKGWUGSGIlcrKujTuos6RI1apP0/bMVOLxt0+uAad8wdSd0raUodXh4LdmC8OY2+xbL5W3ymGjNf5tWnBRUsOjHL4ocd0rSbimrlAv4dhYEdJt6gC9fTLhOzkdm4tgKIgeYE6nluyM/b7aET4SgUnuQsQ5vrMNyL8sNYlXqV6pMQE8yTLMTc0Ffer07TyOje/sl+m63aL7UmHzycC0E4n8H7ddUUxtJNj2wSjioACsbKPTNIcwzjC4ijBaoAMjxaOdFGdUCfPH8hcGq+WkNvfc6BOZtitERqtTK5beNyYH1wYBj2M7rooRRE0Xiv14wX4HH5oC+GiX1FNFEXvP7Sd9tTSjIDj8GWHf3DBINbVQIoioYtIIAXmEP6ZlWrBUpZrKFuo1tSlkl83QDps1nD4xXgFRVm+c5bqc6ZpjlEcYWEYYzUA4U+969jdssw96fC98DHwCyh66NvCGF4pxxuhmc+EOrD67L1L5JnJ1e7AUglNdET4XL5WRi/hUk+niBhYLhSoOmeX5A7t5OgnueXrcE7gwPwgiJ7IbSRSItYwioKOl1TRmxJHG6o6mdgW1HXfW5qbaC+OVG2fl8p12C6ngMzh57nJPbl2DiFjRumqOFIVkFJnZyyoyHKNlYk/l+miOwjc8WqaleMhJcZpG+dQ5ZypKC8XnjoV2CBRWBwmYQukR0V2K4bcbtuTDp+Gj6GUDt1k2SB8Odzu0smRfqYWsWYG4S8M46DJlRE0kdQ4WsmWxyn2zyW2j4msDnHhNhDee2hoZazSZ4oeNoGdHEWEr6vjBRiHH4pWAaPYMseWlSzclsfGyR9ZGorXbZyiO2YgwrfRXCnLrM4xGjVsRIcvzS8AOGiTkGEOjEREgQjfOPkMR5ZMJMHHLBcosDaVm3s2TE+f6iJR5tWiTso5N8ejGg5/6j2THUBY3E3WvBO2Jx1+4cBoC+E2lU7GwseahFpCKkfbET7h21WNMoMi/E4OTNVywZKtTlIsDBPTA5w9jFlRt1Ci8bZFpORX6xczJ4d0HH6bA0sFBVI9pTPAmrCJCLe8aLPRDeG77+LA/ED8nt29AW4RCUzaJu3jBUD8niTzdfhVuooukECplW+yTHfPT6yxTqD8u07JvYXSHtNUF3NHkv7SHkGhyjnX3wlwi5lMgblncqULwu+5g+1W2J52+EkUvuLSBJFUSFQmS0nStqXQgyZtGxF+EmFhmAShCaojjmuUP5KtTjIsDOXWz7QVRShNRCOikrJhY5bn3qLbVhjDy96l66CUjtbt/WYoBQZ0cGD2QT+40ITwu+nwyz0HZJUOlUMGyzJzH+E3jZe5r7A55hVzdRkv5/D5HLNUCoDgFtzTLC+iQ4l+ofmsQaASzFVdAwYESM3RXHJ9YZgEjZdY2T9z+NtnRd8OFf4F0KSthCa8LntJGCcttWmWEGsnSkdXnXOoA5sfxiJVQHcBCm25PPHGS0bjWV52cpSoJG7ldZS5hIx9poJYW8aMLmbu/yAHRvaXrdsboZRlBurwnUqnJp+T5VQOGZaEdC27ATM3OV1Vcfghbb3JvUU1iU1uayQioud1xou5gDBhQJMCqcj5RB1ARc4K5mo4/ALhByRtcxIh1xUM7hbbkw5frBwNlVC69sgVhO+Hj+ZvYUlI6hQlBDbo4vDJJO/C4TuEP0qiSmRCF6bQatQJQfh1CWkjyyzD+NbxImXvUQ3Cd8c44DjpFgdGFzPAjFtIy1tK6Uio0UP4NXJIbpPM3/hcmmNdxst8pnSKkZC0rYxX4Byj9yZJF7k5xZRTA0mUDj0mEDDH7LNhPiNw+CTnE1rr0ZbzcOcdJREWRknwAumONyu82gGjDiy0P3Uoh09bBYRMLsAqamrolwJNjEIVAd3pF8Agu/lhIqoZKIcPhFWjugk9SuqVPa4hGxCWhPSbUMmFV1WKogXhk7lg/g9TiLjvYv9cItJGGUGKocecZnnRJgCQCq+6jZe7Dm8x2+R4mWNufLzqEb5fvQuE5dUchy9V59Kcz7CgFdtBWKHYUjWVtpZ6WxjEWAlQzmUCGJwVXm2jOb5QKZr8C9SVR3KRBy17Lzn8sKRtE/1SJIgGYYqAjerw1yYZFgaxTQaynZYY7SFVNXLzVE01zcAoRRFSCUnzJHWLGd81qtXh88UsmMNPMT+IMRrIPVp8Sies+6ND43XjlXYcr/KYNLG5ufECmHPumPOoc/hprotq89BqbnpvbbUxXaTSA4Lwc41K4t+pqRZGcafWCr4qcEbpbJtRRUBoiOUSZlEkF3nQKtDQylHHFypFub0qwh9ZvnBtmrXSDd7kqlH+SLY6TTFfl7TNqw4/POdRX0XsZJnmfe3dHyV5G194OGJtWyT5YtZFpbMwjGvVMh7tUdMtlJtzYEmNU6ByyGES2i3T56TrVDqLwwRJpDpz+KESytUiyS2rdPKcFnOFJm11QdVI0lfaSDAYhHmtn+ufyUGh0kmDlGCR1fbPCq92wHLtJ9SA7nxhpW8HSSgOApO2vJMhPQ4/78IogdYQ94bl92auQ5GCp3Y0USZtq31MaCIYCKscLSiwBkqHJuoGSXtzK0mHL0VaSaSwYHdBanNgNCICnAMLH69hEiHXgnPO/Q1Q2sbL9B7y9w2WIyLH4Qfq8MlnRA4/Lesl5geheSJKV4XlidzCWzh8AeFTVRPQMWkrLGYSwg/i8CP+TJbHzXPzPQ2tSifXYUowDjBnOvxtNNfDBQBJbLZz0nRy8clYSAYjivADJizhC811VB2YU+kA7Uk1f2N0d8zGjxTHXRg0I3xvH9PADWOGDQ6MygxDHFial1GD27JP4qSdggJopyioYgvopit3MlagBrE2IGtutFCtDuHy8QpqJ5010y/0vPPDOGgXrTTTRdI8NE/kjuuajknFfe4Z6LIpEaWrOFChFa6jpEPUTZQ/7tqcued+mERF/6G2ZzLLNewt1e51sFtsTzp8rjIA2ieCS566z9ShiW4cfvPkcsdwOnwArb07aFFZib7a0WXpwGKMa0JjN2nrtsqjVtYY0J2k6mWGwwAHVs0lVK/DfU/zdp/cYITfsUhqdZJhfhDXftdpTp1iu4SS12Qo1TxeIRFRnpuNa8roRSi8IgvzfKASLNd+pa37W5MVSW5LtXFULCH8tqTtlDyT0sbn0jMZAsLcXIgESpSOV/FMttCGtG4h9JncKevF4Sulnq+U+qJS6m6l1EuF15+rlDqvlPq0/fdLfZy3zjI6YQMVAdPUTxBNM5+3LPllWmnbjibiykRoRqxtDcE2wuGvT3NoDaPSkSptJYQfmOQeEkqHjgfdP8C9LzRpS1FdU90CEIa+3LHc/6GFRPMNCJ/SHnV7+nr3RhwJYOcYj4joeIUkuXN/vESEzyidcA6fURStlE5z0pZy+KFJW6eWcddR5fBdkp8U/wWAigEDYbnk8BOTtKX3VmdcKQXsXlnmpve0VUrFAH4PwPMAHAPwSaXUzVrrO9lbP6S1/s7Nni/EMoK+Qr8ATxFg/881YOcGmVxdFAE+BwkIOnx73nnrwNoKPcrNXUrNfBtidQ+50+FXwm3G4Yfoynk7aXNt5WdoLYR5XzsK5ny7dB0uzJ8LpHT4dQQj/GmKy/fNYVSD8E37AaIRD1U1kSRktVDNp3Ry7TuTunsrFjNh4aH1EgvDMNUJXczqkufc1iYZIgXsm0u88zrzF5FQEEb2MWji8EmlLY9e5Xtz3UWr9AttohhMs5LvTVmlzl6WZd4I4G6t9T1a6wmANwF4YQ/H3bDx5k9ASG8YX65l/lZOHr95Wvekbd0GKK7IY9ElIVsdGLz7CkGsLiR1SVtJI02PGZK0pa0o2sbLHLOdopgWztmG3JGgK7eqplBKh3ZUBGTpomSrHOGTMSs7cJb3Fpzk9qLIZpUO0DzHaKtfAOKGHu66R5bDD0L4WU4WkTBO2qiakrJvkhBFOvA0KOZL+yI5TMz3LNFVdDEv8motCN80T6v3DbSCvKRZ2ymdiCzKcVTNAe4W68PhXw3gAfL7Mfs3bl+nlPqMUurtSqmn1x1MKfUSpdQtSqlbTp48uaEL4s2fgHZdLA0fpQZivPc20J69p0nbuhYPtIwbCOEL86KvuLu/VoRvF5H5mqStJF1sTUKyLSHNtQkUWIdFhEZR7n9J1TS0G4EPYhVO6XRN2pIkN+Aj1pQj6w7jRQuJJMRKWyvw83JLhUWkrruooXTCivtoVBHar2mNyH4BOWlbUoZh9SMTEiEPBCeaEoAQTLNSlU5BiZbHnWRmfLogfBoRuWsNaUexE9aHw5fiTX63nwLwOK319QB+F8Bb6g6mtX6t1voGrfUNR48e3dAF0Qmr7LZ9bZW2rgoSICFnRh1YSaV02aOVLzyb5aRTNrkkaoCbO2bRPI0rTjilE1f3A+BGKQqp1oGWvQNhnHSpq653pDS5Pj9oV53whSeJoqBWAS7JLSVtK3ULVoffpNemSW7z2SrFReV9IZvs8CgqFhwNT9qGqHQyIWnbNmZrE6ZqkhA+l2UGRJG0W2Z1i8Pyu+2UtCXHdNfmzLUdGcal9Jf39udGE9LuuHtZpXMMwLXk92sAHKdv0Fpf0Fov25/fBmCglDrSw7lFk76AdkqnivDpys87OYY0A5tmpGGUGDXkyDULH0MkYKrbvbljzg9jDOO4EvZW0HjUXjlKVSdSOTndzMS8r73dr7sOilglDt85gYVh0lr6njPnHIrwDaWTiIiVV++G7JtcJqRpLqGahKQRET8vt8piJvSAoknIxWFgqwAJqLQgVqdqctdSVTVVqZSQwiuaV6tsSpTnlYKndumvL78GmMMnEdGGEX4cteYnnI3TDL/6t3fixMX1oPdv1vpw+J8E8GSl1OOVUkMAPwjgZvoGpdQVShkvpZS60Z73dA/nFi3nX0DABhUmQVSvTOCoLmTP0TT3G1u5vxXnJAmi+UBKp4rwZdrj5990G+4+sQzAhNuAcZDDJKoktqqItZsOvxyvKgoueqAnYePl7gmAuGUfXZgXR+0yQ46C6zb0eMNH78Pf3/5wce2TNDcUmEDfOefnvtOQdr/UkbjrkQvVmMNvqLalKhVAXsyo8mkhdAen3K8xMH/zx+zj95zGb73rH4vfTa+mGEqp2jkWcR1+Wy+dtL21Ao0g3WeaLGtRzpW5qbjIq4XMMc7hhyZtP/ql0/ijf7gXL3vz7UHv36xt2uFrrVMAPwvgHQA+D+CvtNZ3KKVuUkrdZN/2vQBuV0p9BsCrAPygbqtX3oTRvh1AoAPLyt7bA4Fv5+hTSn5WrqMleexJwAQ0cVHYWo+rNqTJddeJi3jLp4/jJW+4BUCp/FkkrRU8yamofw/IT8R+2wg6xrQvDhC2oceU0UCxEjjptKR0FkdJa7hdLmb11AAA/NJ/vwM3/dmtAMoQfnEkUxQpi17qWkt49ybIMuVCtXKBBJoR65RTYEKlLd0XdnHU3irAJaR54RWfY//LH34cr3rPXThrt4JcGadYGhkHOZKkv9qXsQLtiWC31aC7R0mWySvqw4oh+fcmJW1VCcICokiO8ENlmcfPGWT/uWPng96/WetFh6+1fpvW+ila6ydqrV9h//YarfVr7M+v1lo/XWt9vdb62Vrrj/Rx3jqjSAkI63Vi+ML6RC9vsxuiK6e7EUkPzpgkiAax2RnKOZvXffAePOOX31kJ9Wj5vTsuX8zconHPqRUAKML4xVFCugqS6KXC4Qf00hFkrBLCL8ernQLLmCOVuNAxdfiE0jm7MsFTfvHteP8XT3jvp3ULdcektj7NimMujZIir9PM4bdTFLT9tvlMdVGli3lINXfGQIi0ocfY5jyUUli07TucUud7fv8j+OWb7/DeX9kwpqbWY9+c0dt/+tg5AAZUONAiPRt+9Xt7RGRaUeTeAtmY5A7eha5ZOUdBmHkuVUAL7jJqALoh/HtOmij84QvrIsDr2/Zopa3/BSRBnDR1YNVJXrZWKCv/ukwukS8sUJ95jXbn+813fhEAcOLC2D+mhPAZYju3Wk6cPNdYpg5fQI7VatR2hMLDbXdt9DrpfYdJPTn9Uu1pRJO2i6Nyg4r3ffEEJmmOP/vY/d77pcIrLvWk13XnQxcKh78wSkS1jNROGmjuDePUH035Ca9bZkC/JtrB1VyHjPBdLcEiqfW4sD7Frfefxes/cp/3fmlLSKCK8K89vAAA+PSXjcNfHqdYtAhfytfIxUnNi5nWVNVUrXXwZKzFAtlOGzbl1Tj1tjBMCoR/6/1n8boP3iNeK8FgYjRSZ/daUAYAD5/feh5/7zp8oh0KQ6wkQdSk0vEcWMsxCRqX8gKUwwdg+2+bjpmOM76w5q/6WZ4XqMtcaxVNnFudFD9/+cyqT+kIeuWi34ydDSHtfifeeEkJacbhh+Q82KIqoXGetHU5j099+SwA4PFHFr33i8Vc7JjnyRjf/uB5LNvxWhrJMsOiFYVr9xuQhHTqj8ZmYKy7KD8vt5JmbFA1pSVV6RzyyjjFx+8503hMzuFXcgN2/txx3FARK5OS0qnr11RtrdAhIhJyL7RxXJG0DaANB8XiX6VuuXzWbUz0yIV1fM/vfwSveNvnK5QYR/ihxX2AicIPL5r+QyeXxy3v3rztSYdPqRTAOJBWvjCVEL5QSER4yJB++AMWGssI3yAvs8NOii+fWS3ec545/BAJGP3M6ZUJViYp5gaRbQthzkURa577zjkR+pZwoxSY+9xUWCA9lU4ABcZrDCQ07h7GxVFSOOdP3GscGN90mlNxsRA10AXy1PKkpMCISkeidCj1RP8uGS1Uc9dRVZ343TLNeTskbYUNPeh4UZnhJ+41momj+0be+6tbQsr1I26OnVwuOfxF6vCFam4qjZWOSa1aqGYqj3MPVPgVriHFfTT6b8urAaXD//i95QLJ5xjn8EN6Kzk7fm4N119zAICZe1tte9Lh5zmqTjFE85uUKBBgCD/jDj9sB6eYTfKQyXWKrPTc4dOeJO643IGdJQ7s7MrES6jJSUietG2vFKSNraSOpHy8hnaBbEoYprku0L27HhHhFxx+uWmMC4fPr/KIqIpY+TEpBXZudeJTYJIOn9EeIWh8yqiCgSTLzKuyzMbCK863R9UNPfwkdykMeNhShbyyW9oSEqguZm6RPLc6wTjNMM10OccESodvd2n+FjBeDa1JeK4uRPrLt4Sk90zPO2BR5IkLJd3CufaUNGt09xfSPG19mmGc5njyY/YBAE5dnCH8DRn/ApIQ+kVMQlY5aZq0DZtcFn0F8IXzgxir48xb6dsQfiQ4RerAzqwah+/QnZvsnsyQySFDeunwnZbM3yQO37xnZFshNH0PaZazxUzelJuqdFYnGcZphgvrxklL4wWUD3cktB/wxmtl4iVt3bnGUtJW1TsjblJjuMq+BFkpM5SSxdzoBiBADWLN/PECDN9+ZmVc/OzVGAg5D3Nv5XsmaV6gXDNeJWUIOITP7o3w3CE942klN703zzkTWaY7b5d2JxJd5aKuAaF0ViYZThBnfHHdV+0YCrk56r7z+AVc99K34h8fuVj8zc3Vaw8vIImUB/S2yvakw880Kituu8ywykl7Kh0bPtpyArEJGTep97YoAbOvLY4SrE5TnFkpHf65Codf1eFzp3hubYrH7DehukGsWfGwjyROuqI6CUnaluMlFV7xNgmlnr1e8UB3yHLXU+kZn5fndYjVSdsAOSIy90Q5fH+8XER0ZGmEs6sTT9U0EpLckozVXH9T0tZ3JFK7X9rnZZQEjBdXNQkLD53Xi6QF92kCKmieiCekJbrq3Fo5XhfX02LMFz2E7183bTZXRERNMlZhgeT3RpvNAWbMmhZIrbXfPE3MPfnz1jWce6QB4VciDaGZ3jvuMDUeb77tweJvbtwOzg9w2dJw5vA3anwiBPU6yaqqE979kX6pwyRq3wmHILZSAiZxwRbhDw3Cd+hr31wiItZKpS27tfOrU1x9cB6DWOHMytRSOiX6AqrNwNyxgLAFktYtuKFOG5DiaFBFytyoggKoS2zm5GE0DsblPEZJVDij8piSSsenPdwYP+HIIs6uTEnStlTp0AKoaifQdg6fJwMlmpFSgCObaxlPG5QsLMktJ8/L8XIL5MokxemVCebsd0LnmNRXiR/T0WZPsAnyB8+u2eM3J227dODkdKe0cx2VegJmzJrnF0ucN4kNCuWcqfU4cWFcnOuChPBb/I0b+9MCXXtwYYAjS6MZh79Rozv2AO2VtqXm1w/1pgwp0fDRTK7wtqnl5Cpf5wht0XL4p1cmWBolOLpvFMDhVxH+2dUJDi0McXBhiHOrJmlboq9q0lbSqreV0o+nWYFCXcJMLlTzEWsTAuPRS8QKr/iGH44zdg7/CUeXGjj8eqd4bnWKOFK49vBCgfAjBcwNoqKNhpNVAoTDr3RXrR8zN1fcwsfHC3A5jJIyBNo4fJ9vlzb0oFGmQ/jL6ynOrkzwhCNL5v4pwhciIvp3ADhrx9gpoh44a8a/KWnrdeCMqs8XN+e4y/5WspCiCsLqn8lyMauPGniB5cLAIvyL63jSUTNenNKpVL/H1RzYaRu102jUUYkH5o3DPz1D+BszumMP0F5pazY7IWXvwg5OWe7zy23hI8CKPKLqLkdchrgwNGji9PIEhxeHODA/qDgwqciD39v5tSkOzA9weGGIMysTppG2DxvjpKk6ZhBHrUnbcVo2myuuw3twnDMy75G4cG6UfgCq8ja+4Ycr9HnAOfwji7g4Tn3ExhG+QHucW5tg/1yCw4sDw0lPUiwOk4K+4/sAF3sSqHK8gOYkpEPqPsL3owaty/EqKJ2Goh/azwgonXPuAZWyY6vb0OPhC+tIc43HHzUO20P4LOchOUWXsL3OOvxj1uG7KHJQ0zaCNjRsq40pHP6gPKY7Dr1WH4RFjRFROX/qoxd3Te61gwtmTpy4MMaTLncOvwoqohap9EmbA/jCw1UO/0BB6cwQ/oasa/e6An0VvberiG3KVvEQSocmNoGqBpxXls5bvvDMygSXLQ1xcH5QQfgVDl+gPdZtX5ODC4MCsS5ZdFerdmB5gZBeOm68gCpvyVU6BUXRiMBy9r1FFadIj+kWscLhH12E1v4DmTEULCHWtUmOhWGCQ4tDjNMcp5YnxbFD7k3Kz3CbWHlksTEPq7TlMtZQCswdC4DYu55SkaPE7IdAF0jAVzblBYfPwU95zHV7TVcfmgcAHGOUTiJE1JkEwgKeSSpjBTilI3D4TRFRzfcmJW3da9ceXsA4zbE8TvHEy2WEL1X282jPOfxTy+Pi3koOf4hDNhrfatuTDp9zam3d64pNIopwuzq5sgpfGNb9sUJRMJoIIA5sGCPNNR6+sI7LHMKvcPi+U4yE/ilj64wPLw5xdnWKFZK0lRpX8fEKafc7TrNivABUdluSms0B7e1+q3RVdbxi7vDPriJSwGNt9ed5j6IojwXItMc4NfTU4QVTAPPAmdWCcw25t6LStgXhD5OmxX8D4xXiwNi8XRgmhAITEH6Fw7d/p+Nlo44r9s8BKBfcRQIq+JysPJMtu4SNK8+kTL/QYw5bEH656bm/iPi5Ol+cce2hheK1r7xqP+JIiQi/WnjlX8dJovJxVM751QmUMrm6QwsDrEyyVp+yWbskHH4bYh0LCTUAHn9sEArn8NsdftzgwDhCc0nIB86s4vDiEAujandDLgFLompxkus3c3hxiJMXx5bDj+25qtELD43rtNf8HCMveokqDyNAOPwQxJpVdfj0O+AbfjgZ4P2nV3FoYVj0d6HbRNYh/Nxz+OV4ASYnsEQQPm81IW0Jac7VskAmbLwkGStZROJIBSUhafM083c/cqBR5tKodPjOmdEGdDyKigV6013TlQeMw3fHWyKgQmob4UVvLcKASR2Hz4QB9N7a8mrVvRGERYQtkK6FBAB8xRX7sTRKRA6fVvZLNOupZUMbAqUq7PzaFPtGCaJI4aAFG1uN8vesw6/2p25XBDinVD7A9SjYoImWpC3hT4EqtcQR/lUHTYg8TnNce2hBlrcxFMxpD611sW3iVQfncX5tCq3h9Tkx1+ajS3JrQbrycZp7CJ9zsrxLpaN0mhF+zu6NL5C+g7t8n3E4F9dTXHVwniwq3ZhnlAAAIABJREFUWeUzRUJaoD0maY7RIC7G/8wKo3RYq4mqjDUkaZszh8+iBjYXAANA2igw85kmmaH/LBzdNyocluPgKSrmLUQksYH7DvfPDXDZYsk90zwRjXZcB04fhDVLf3nStq5fE4+62wAFQBF+9TnneaRrLG3lft43V3X4HOFzf5PlGmdWxniKLbA6u2IR/tq0cPSH7P9nZg6/u4kcfkCCyClYpMIQTs+08YVcUQKg0rKAKyJoH5jrjixiNKhOYCl68TpwksWLTtbLbQm9pHbgHTjdItWoK2ccvnHO5evc0ZZJ23AdPneKHKEdWBjg0IJB9Y+7bEHccJwnpCU0Pk4zjGJ5vIBqnoTv5lUsoi2I1SUgAdhe/+XrfBEBzHfYtEBWkrZ1iU0yB90c2z+X4LLFYaUdAS8qk5yiNMf2z5VFanyh5tp+d83NSW6eV5PpFw7CGserJmlb0faT8Zoj31kUKeybG8g6fOZv6HUuj1PkulxgHcI/Z8UVAIp57BaDrbKk/S2PPssrnHS1qpFaXfhIqRI+uUZJjGmmKw7YGVeUAFW+nSO0x5Lw8brLFnHXiWWMbTsCRbhnb3Kx0Lio3mUOzKGLgZT8EsaLv4dbhaJg5eSFtp8XEjXpytnCw+WhfLwA4HGXLeLs6jlcd9miSBtVxktY8MZpjqVRUjx8AIpyd3e+qeDAOKXTNl7D2Ef43nixdg1AO2Ktq4qt6vDpeJk5dt2RRSilDA0yrTr8JtqDJlSvPjSPzxw7X8wvoNoZlUd7gCw2oObmMUf4vsNH5ZkMQvgsIvJUTWy+AMD/9Z1Pw5Elg8CXRnHReqM4LqNueYTjFi9HgVFKx825GaWzCeOc9KBlQ49iAjeFj1mV0gHqKQr+4LifefgIlIhjfhgXk+Kxly2Qqlg/KvCiF5YIdg/vaBDjGpJweqLVEEuN4aQOnPz+qWmtvb70gCTL5Cqddl35lKkuktjn8Hn7ZAC4jjiwosaAFZXx8TL3zCgd2zPe2ZOtIsPdQ1NRWUgzME6BVRZ/Yb6EI9b6eTvNNBsvgzIfZ/83mnma82hfRMap6UcTRaqYY0/i4yXcm9dCWGgeR62QsVZ0+NUEqzMpIqZWbMrTUP3OF0gAePE3PB4vfObVxfVUi8rySmsFT9U0dTkPA8CKpO3aFAcssj+0aBH+6tYi/D3p8LkuVtoqj1pdkQf9TK6rfCFQ7/C5osT97IfxftIWMAjMafCl8voKGmcPVykxjXB0qaQl3O49dUlbrtKh18cttZysp8NnCw9Plg4DEH7KknBc1ZQKUZMLk6+7bKEB4fvI0v3d2ZjRUwBD+ExmV8fhN1EUE4HDl2iijSFW8xlpQ4809/NIdLzMOSLG4bNEsBC9UDrPUR6HbMLbfFaW03bZo6J8JmPvHjld5YGwlpxHNSHdfkxuoySudgKtPJN+hOM2nNk/n2BhGBe7hJ1fpZSOGb+zjwaEr5R6vlLqi0qpu5VSLxVeV0qpV9nXP6uU+po+zltn1d4WgZTOwE0umfbgaAKo56S5ogSo0h4SqvuRr7sOP/3cJwKob4PAC6+44gQwD3IkTFxJVy1V7/L7p8YfRncdYsFT0XsoQIffsphxySAAPOeJR3DdZQt4yhX7xAVSKnt391zeT1aM9TOuNq1qKb3Gux9Wms0Jldnc+KLCQQivmgUCkrY1Cw/PE1G64cmXL+Gpj9mHr3/SEQBV2og7Z4nepHSek2a6CBIQEtJStNvSr4lH3W4uN8mJ2xB+pW6hJpIfxPUOX+oEKve3ogjf3Mv8IMahhSHOrE6gtfYonblBjLlBtOWUzqY5fKVUDOD3ADwPwDEAn1RK3ay1vpO87QUAnmz/PQvA79v/t8R+7bu+0g8x46gFTbAij7oHhz2M5rP1KNic26cTJAdGJ8u3P+PK4ufSgdVz0lzzXFA69rPv/rff5GnKS4TvH5PXLfD3UCsSagO+mAmorpMsM/ceNt67nks9AeDGxx/G+//dPwdQbuXI95/liwg9lrmfEn3/6YtvxPFz642Fe25Y3HvKvEjzHKM5Au4Ucl1dzFodGGvoJ/Wupxt+AEZJ845/88+K3zlFwRdqia6iNQU/+LXX4sjSEM972mPKe4tV0bs+Is7fa3fS0kJ4kpp7a5K+Sr10XAtuSs/RsQBANkCRn3MacXOTKJ2K/Dr26SqH8OcGphjy3OoUK5MMaa5xkMyJwwvDogXDVlkfSdsbAdyttb4HAJRSbwLwQgDU4b8QwBvsxuUfU0odVEpdqbV+qIfzV+x7/+k13u8cKXLjRR7SPp60EZp5b+x9llvhnBr2uuRl3NxKVFyPavgxy2SX+Sxd+Oj1cNmYx+G3NAPjdQvmOqpSNKB8UNsWSKA6xnwxk5K21MLGqxrhTLLS4R+0PYio8V5MnK6SAAI3uqi463DFbYoAAa4Ea9PhS5FZJWnb4MC4dj2Uwx8R5P2tT7/COybthjmKYhHht1e/V1t38OvgLVRonohTdEBVlilFuzyPxK12N6+G1grrxOGbYsiJ11bB2dF9I69AayusD0rnagAPkN+P2b91fQ8AQCn1EqXULUqpW06ePNnD5VUdB7e6witfpaNB/Uxb+1rJmUu0xyBWIhoB6jYr4QnWiKGvksOXTOKbJaknUK8r53UL7jNNPWykaIUbbQ8MVJunSUlbapL0Uwq36fUB1hkPqg6i+AzrDcOdcyljbVadcA4fANxlSHTVsIXDp31ygJoNPdiiwI03OuNSYYne45JcbvwzfBEB2hsamkpuEpkKzpn3lWqbY7x9hbucumZzkkktVXjbCPecu0r10uFHtqHhtGhn8Wh0+NLo8Jkf8h7zR61fq7W+QWt9w9GjRzd9cYCZLPQL4FZXeMX3tOWbLdDPcpOcE6c92hNENZw0d4oShz+ocfii2qG6i5Z5D5F7pnnRza+OwxfbD1gnpJRqVZ20caElUqxD+NICWd0wxhyLyjKz2gUSQNFqonJvLUnb4+fWynNMq6omc325eEx3P63jxeYXPVbxnpY5JhZeuURw3XjVzC//3nyH36WhIU9yyyqdGoSf5lifZviJN9yCu8iGI5xCdU3ceF6tbn65c/CcGm16B1TbQDiVjuHwTTM218bbqXQA4Oi+OW+jla2wPhz+MQDXkt+vAXB8A+/ZMpP6cFArHJiV9UmdLfmmKhxNvOzNn8OH7iojEklRwmkPjtC41e221NQ8jReRcVNKVUq/0xpKhyLWf/tXn8Y/ffm77Qbr1SiCq3TEQiKShPyTj9yH33jHF7xrM91FqxSYW6h5O2luSaQQqZbxKhyYO2eOXPv0lHTcxl46LiIi9/++L57Ac175Xrz3C48AcIuKv0DSY0njRdv9fvbYOfzoH3/CW/xNn5x62oNv+CEZLyCskxNXVU31x+TdQ6V744uo1ho3vPzdeO0Hv1ScQ1ogOV0lSaXHaY5b7z+Ld935CF725tvL9xfJdhIVVYQB7ZTOWKi65tXv9FoppXNoYYgL69OiwIoi/Mv3jXBmZdK6sdJmrA+H/0kAT1ZKPV4pNQTwgwBuZu+5GcCPWLXOswGc3yr+XjKp0x61OoqiUfNL+OIL61P8+ce/jH/9h58oXpcSsrESyt4bwkepHYFURcw15fxeuCWR36+bUzpSu9+/+6z5uk4uj8tFpVJ4RfhVe0xKV9Ek5JtvexB/+clj3nVNWdKWc9K8nTQ3F0XwJHfExsv83bynLSJy1yHLDOuR4se+ZDYJ/+yx88V5JErHHYt3qQR89P2ez5/A+794EnefWC7vrWW8eJ2HZLzhmJvz1U12/Dk2bIyIZIRfVWCV5z25PMap5TF+/W0GBFRzHlXQlldAmNs0ptwXms5zaTySSLGN0VueSavScSAkE8araEFun581T6UzgNZl/yHP4dtd6rZy56tNO3ytdQrgZwG8A8DnAfyV1voOpdRNSqmb7NveBuAeAHcDeB2An97sebtY8QXUqAK4SgeQEqz1OnzXLZCatPI7aom+J25BX/T6ABmxpizcpp+VbCDopJMGpEjt2NlVUpncLMvkdBWVtN13egWnlsde1SIPp4tkaPFwNSN8d01eawWhXQO9N4me4sYrR0sO3x8zOqbuvtzfJqzwqoLwa3T4Dn3fd3oFgGkUR6+D9x6i11fy8eFJW74lpLlPv+JZqlugxltz1EYN5Jh0IQOqiVepv1VaAWElwv+yHSdOy5p7Y99b7r9n0KLSMfdmx1hoG8G/W0fpzA3iol7h3lPmfqlAwPWGOnFh6xx+L60VtNZvg3Hq9G+vIT9rAD/Tx7k2YsVkIV/+v/nLT+Oqg3P4d9/2FZiQysHyM9W+NzKlkxWTi5qkKJFUOm3oC6gifI5Yc11K4EIcmHFOTQjfpyjWSZO4Y2fXsG/O3x/X3GdzRAQYZdM4zXFudVJUG95/egVPv+pAMR5NVbG82ZxkI7brEd1L1d07UDrFkAWyVnLKoiJKgd17yjjo4+fWkGY50lx7NBvPpfBkaXEvduzvs3PMOX732aaFuqDAOqhOJPqFO8W2nAe/Dqnwii+i3OHT2gj/mOV7KlJp8rw4BP3Q+XKHKSnqlsQGbYVX9PqkgrmyXbZ5bY0IKZyD/8LDF7E0SoqOr0DZv2krefw+KJ1dbzGRiQGGL3zzbQ/i995X8oXcQUZKkC6K4WM5uQAUVXR1SdumLdq4SaqDWk7aIg3nIJpC7up2hFyr7lM695wsncyxs2ukfUPDYsbyAu5+xtPMQ6n3nfIR66ABsUoIjRunKPh4FY6DIG/3uTqLI77jlfSQ+9/tXdaBHTu7VkplvfHyed5Cq66Yw7fXd7919PedIg6ft6KoiRraF0iJw6d0nQ9+2jh8ngMSWyuwOXjXI6XDH6dZhdIpE9L1NSmUZr3fPpOnlsdFszOZb692sG3KeXAQJhXMVaJIux1oFKliz4U7jl/AlQfmPMrT9b5y6H8r7JJw+K7Qwj0AD5Md6AGZk6x0tmTSK7rnKHX499gH0n22qVVAcNJ2Wi5UGaOBIja5eNMpyXi/cp6Q5g/sg0RtcuzsqhhFcHlonvtqInc/kyz3UKqHWFnSlkdmwQi/oaisivADKJ2I707leNvyPfT+J2leyOuOnV2tFMPRe3CfyQVqwI0XjYjuI4vltJLkZotI1r5AcgqMF16Z+/SdYhuHz7uHSgifU2D3k2fooXPrFRkr/96cOoZGu7T6/diZ1WIsH7EUSW3U7S3mgVF3xu+t6vAph+9aUBy0qpws10U7bmeXLY3w+COLePNtx/GJe8/UXsNm7NJw+KwH/BfJvpIr41QMUSvcXlaj+Z1meODsWvEluwd9WrPyZ0zbHxQ+2onjLqdJVy45F258k2VOv3DNs6tg3T+XGMQqoOJIQPgVSsei72JLvGGMh86vkc/41+Eis4wlyJo5Vt79UY5eso7jlTKnQCtczXFLmsyN1765BA9dWC82GOHjBZSLmczhR9C6pIcW2Hhx58TnglSZzI0nuYuiMs7hs+6iG9Hh0++WL6IX16dFDu0Bu0h6lA6j9yTuvCjum+Y4tTIpOoOu2vGXk7YR22Sn+ZnkO5FJ18HlsevTDPNCz6GrDs5Vjn/9NQfw+Ycu4CfecEsxj/q0S8PhF6uyoTuow3/w3FpFAgZUJzmnBuhKf+riuGhKtcKSdZUNUIjj4AiN25AsKkBd+GgdWFYi1kg1ozqeMEszPy/AO2q6BOTjLlvEmZWJyHtLmnn+4MwPE6xOUrPByDDG4aUhVu3uVMX+AVH9Q95WmeyuicsMpeilVOlU20Rwq25ogUqfIrooOAd/3WVmj12HMMUkJFvMKN0w73ZAswvkYw8vFONl7qG5xqBcRNplmS5Zy7eELO6NUjrTtroFmYqLmPSXzsGVcVr06j+9PKnIWHl/KylqcA0CT6+MMUnzYrcqtwNaXdLWi7oDk7ZukZQWat7iYn2aY87Or8VhXPx81QEf4QPA855mqpZf+d3P8Dbh6csuCYdfrsrmC3jgbBk+PnBmtVLkAUjZe5+icCHayjjD2dUJrrX8m0MTZTjtT3IfBTeHj5zDr+OOgTKi4A+KZNJGLBx9ASUicovYNYfmcXZlUuku6q6pjcN3e/SeXh7j8NIQi8OkWEwkjT1HrCEyQ5roBFDZ8YmjzyJaaXjIq0nuakJ6QPqnOAfjONlHLIUotwpgWnUyZk6yd6/NoVxzaMFTNXFKsDJebMMPydxC5xZJd59RJXphHH6jjNXPAUmLGU/arowzXHu43HGs0lqBoWapFUUxXqfYFo4OhNWpheizkLUXqgEE4UsJ/OJ7qFI6Sinc+PjLAAALgkP/9mdcgU++7FvwAtJTq0+7NBx+Yr4AN6lXx1mxyh47uyaGqFyfK/W83jeXGAe2MinQxHKBJqrOiXP4IVWQAE0QSRPWpyi4/E+yJPI3aMh0DQpmFMXVB+dxZnVSqHY4Yq10Moy5wy/H6/DiCAvDuNizNxWcOU+wBidtebM5r2Mpo3RYp1TJDAXWrOSglI5zym5OuIrbJg5fchzOgd1jk3iPPbyAcZqXBU2ZLMt085Zv+CEZ73EkzVsKELJcI811M6XDckDSvXGAsDxOccWBOUTKtAimTtLcA4uI7LEj0eGb8XILyMrEj7rp8xOxSD6kFQVAQJjQ9K7IN2QlpUPv5d9/21MRRwr/7MlHKsdXSuEo2W2tb7skHD7v/rg8TvHYwwuIlOHcpSQUr8CTKIqDCwMcP2c47SsPzCNSBOEX9AujPbzeMNXNFqgppWyLXD98lBCru7c2BYUbj6YSdd7ud3mcFRTM+jTHwxfWsTCMK7K56mLmX8fB+aFF+BNctjjE4igpH0YhROf0i7TgcasmIWUly5RTOk1JyCgCl7Hya6Dtft0CeW2hujAInWquuVOQ1DEuwXfvqRUkkSo2x1mdlqBCShZyKqWRAhv4xX1unvP8hJtfIaqmuqStF0WS8dJaY2WcYt/cAAcXzB65F9amxf3Te+CqJnpM12LYqcpKhO9ARU1erWXeUuMqHel7K++/yuEDwFdefQBf+vVv9/Zc2C67JBy+QzFT+yWtTMzkcr2pVydpgfidyROhSlG4h/kyTlFIlbZMHdNW1Qf4vTskhMsTRFI+glvMED7n8Eds56jVSYrFUVJIyu49teK1dXX32bZAHpgfINemyvDwohmvMudRpR94E7twmaFP6TRK5gIcGG33W3dvtAlZSYEZh+OUSNSB8e+tQMHk/guEf3IFhxaHWLL1DyVF4fPNdcVcjZROgfDNmE1Zkttdazle1SJFblU5rUXjZBEZxmYvXbd7WpprLI0SHFoY4IEzq8i1X4VadLBlBU8VEDY/LJ5JF2EVSdu82qywwuFneeMzyXduKzn86v27yIFy+Dttu+Mqtthc0nZcPJAZFoa2VemKaVV6cN5vicv73nAOH/An1+GFIRZGcZFUKx+2+sTmNG9O2gJ+PxWxXUOB8EtKp+lhNNdU7cUuJqTTMiJaGiUFQr335AoOsBbCUcT2nxV4bvcAL4/TEuEzCkxK2haOIzBpy3XlXJUB+OPlPldnEv1SrSJWBaBYJjkPoOTgqcOP+L0JKPigMF4ARax1+/WGU2B8n4KM9c83xy3zE1JNATe+b7JUvUsrVt0CtmifSSki4v2tpKjBfGZQ3L8bf4rwRSqOqfEaC9Xisn2Dfx1VEObmWEhebbvsknD4FYRvHdihxSHOrExwfi3FfgGxtiJ8MrkOL5kHcrmQgFURa5Umam7UBPgOTCqVHzBOepI1qwzMvbFII/clp7wXyMrYInwrKTt+fh2HFtoRfsSTtuQzhxeHWBzFRDJXTdpy2kNCaNxGg2rlqEQTOcfhHsrG5mmMopA6KkoI//DiEPODGMdttae/AYr5fM7aRtAxo3PSRETGadAxa0L4fMMPyaQ8EXeKA8Jzu/c1zTGeA5IWM6pycw55cZTg0MKwqPtoiiIldQxQjvEgVjgwP8DcICpow2mmK9+zSEUGLJAFwhdlrN3zattlu+Mqtth4scTqJMPC0FAUxuFPPPQF1Kh0BNWJs8MLhqJYZbsuDVmizqsUbEET7vPO4RdOUUT45XvaOPyqDl+zKsgIkSrvoYyIyvutjldUqTHgofEB5sAWhiXCl5C2RHs0oXsAXs4DsLRHY0Rkzt/owNhnTBuL6nmL8ZqUDswtkgvDWOyW2aRVd5w0UI4XUEYQkyxn88t3NFKylFtFZijIEmmlccgCyVtzSBJKqmd397NExgsoN/am19F2b25eHloYQimFpVFJGxq600faFak0my/cuA5fFFIwWeo4IOreLtsdV7HFJiVtl0YmCfnguTVMM+05I8BX6eTChAV8BHLIItbCgWVVhy/xhW1ofJiUCUO5hwtHdSEIn+vwq8ljilg5pQMABxgFFtKThG/2sDQyzcEmaV6Ol1Q5qsm9NSTU3HXz/EQTh1+g4I7tfsXxIpROEinbO8XfpLq4jhqZIacNlyyNY8bL/LxKFkk6Xu42UzIXzLmaVDqx915pUR0Qlc5UmNfcyoWnXnI6SMpn0iHwxdY5RtRCDRw+UPL3C8OkUIKZ8eJ0lfJqIXhve26V1gqCEqqU/hIhxQzhb5/RL8kpAhZsEtJNBu7wYyIzlHpw0M9EylShLg5L1Ym0DaDkFNuStlSvLEnmyiIPi/BT3e7wmUon19UHx0eshtI5OD+Ae2aliMg8MOXDI/Grzr76sYcKxLo2ycSIqFp41W28gKosM2YPo1tomhKbUjOwatI29iidxVECpVSBWKX5Za6vmZM+tWx6M93wuMNYsHsTuznG1WVurwNeeNV0bwUaL6LI6vdG6xBCKB0eReXCYkaRcsHhjxJcRhA+n2ORqspY+cLrHOsN1x0CYCKrUkghFVhGZNGt0orcOFsgfYbnUsxCM+Pwt83cpJ5kGpOMKALo5BIeyNDw8SmP2QelFBZI+OjQF1cEVNs1tDuwalUf4dsZNTDO8ka0ao7pFxJJCVbOSS+OEiRxVFQHShw+4CPWCodPxvjA/KDYXH15koqOhNMead68P6v7/DSrX3hc73qOgrtQFNJ4DWJVSXIDwNOu3A/AOB5q0ngBqIyZs+c88TIsDv2k7TTTFQdG55i04Ufl3oTeMHyMabO9kAWStzJp4vDHacnhL40SPOWKUqpYibpJpFHH4buE7/XXHCyOuVqzQLrP87xAUDHktJ7S4ffftkPYdtruuIotNicznJLJtTiMPTQhI3z+4PgTwT3gX2En6dIoLvhbvpmHO2al0raNovAQfjuamAphKzfe6CzLdaVVAEX4jgIDgCccNeXvfLwidh0Sh++0yD9wg9n8bLGgKFKRAqvQHoE5DwCEc5ZlhtThtyWCq5WjVcc8qiyQ5l6/+rHG8dA2vUBVpVOH8L/uCZdhmEQFZQjAc2AcaVMqsi4ypVYIGsh4yQi/nF/0c5Lx+gkpIU2TxSXCj/HMaw8W75FyCW3j9cM3PhaAGTfAVLMuE5pVcvhdWlFUmqcJlA6N3vJcY5rp1rzadln/zRp2oQ1Ipa2bXAujxKtoa1LpZDXoy5VI/9g3PN4cc8gQvtSfhzVqaqUoElVsoBCSIArl8L1KW4F+cZx0lmusT/PCOT/+yCI+dNeponsjPaY7lvufPzhKKXzx5c8vFrkCsdZROu6YunRg7ZRO+V0Pk6giyzTXWqqUJKfJrVo5WtVqexTYOCvG65nXGmqBdhylx2yLIv/sx59VvLZAEL7WutaBccTatEgOCLXiroPfm6EAfVVTY84jCkf406xM2i4OkwqQ8K6DOueaorIXPONK3PfK7yh+XxrFeMiOvSRZpotZuS1p+wJJk9z8Ouj9S0BmJ+3ScPhElun4z6VRgq+65kDxHkl1UpmwbCI89Yp93uRaHJkEUZ7rmvDRdD90m5WY/u/tCP/iOi8NryozHPqUeEputHGVa1omFhKR8XLO+aZveiLuOH4BL3zm1ezefOdc1zaCKlUczbEyLikdnvMwxyrvrS0iot81Ro42YwtvrEBlmSEyVndPgJyQpknblUlajNcVB+bwnV91Jf7l9VexY8p5AR5pxJEqzhVHqpAZ1rXBpkCllAYH0FVZOR6V8RIosEYOv4Lwqxx+sdBkPocPAL/4Hf+kyF14xyXXkdckbbmFgLBc84Wp/t6UUh4lKubVyGIe0n57O21TDl8pdRjAXwK4DsB9AL5fa31WeN99AC4CyACkWusbNnPermZa2fqa34VhjH1zPqfMP+MmQl6DvrgVOulpJqIvisaHNhEWwuFXK20bKJ0QB0aStoXagTkal/xcsxSV60R41cF5/M1PPad6TOfAsnqnyK0sJJIpnYhXVwYgfIoczXVUqbUkUl4hUTjCJ0qWmvECTBL6yFIZPb76h7+mcszqBijt4wWgkBnWNX0TaY8Avr3k8GUqsoiIAnIeXMYq0S80abs6zTCIVfHd/fg3PqH2uNU+QW0OPy5aUYyzHAeG9dStJHuWzBNSCLmEAXkmi8rkXYLwN3sVLwXwHq31kwG8x/5eZ/9ca/3M7Xb2gFuVIw9NuKTaN9oGRu53Z3FMOfzwyQUYjlUKHyOmOmlr1ASY0JmrdHxZph8+hziwAVEgSegLKJO2TsXEk47cXO/60tnkFafIrRyvTEb4Fclp8x7AgO/A6qKXJCq3pgvJeQwYbVbXWiHNNfJc2zqPlo6llZxH+3gBZuFdnWSlHl6gdHIyv9zf6mxUWSClwquS0impt+5JW6+1AuHw1yZ+c7E6i4TFrG2RnB/GBWiRnkmJug1Tgvn3JooN8jxov4XttM1SOi8E8Fz7858AeD+A/3OTx9wSG8URpmlZxu340P//RTfg9PKkGkorSaXT/KW5/uXrk1x0vLzjXxqiKyeTyzkp3mMfgPdAtjkwSlfVJb9cAdNaoMMXKYqW6yhkmdMM7p28UM3cW8mxNvGr7roBszjULdRxVBaeBVFgQrtfrrqgybwQh8/7rWR5O6AAgIVB4slYq3OsjN55idmGAAAgAElEQVSK/kRNKh1WhS4lxuO4G00UR34bhFz4HvzxSlvHy32+UrfQ5vDtPspZrjERNjqKaEK6RvnDzYFHoLxHv9tslcPfLQ5/s1fxGK31QwBg/7+85n0awDuVUrcqpV7SdECl1EuUUrcopW45efLkJi+vtEESYZJlhYrGIfpREle2GgMcb8mRdfM5CsQ6TWt77APGcdehz8p1xwpcpbPZwiv/mPJi5jjptalZIN1iVmdl3xuS/At4GAFYxColbc3PtHlaSLgNmHGQWlGY95SOI6wVBV94qpEGTeaZ7ogt4yUh/JbFDADmLEVR17WSKsEK2qOJ0mGqJqlb5CCilbbtDh/waTPJOXuUjq18bzPa3yoU4btncn2aiTJWSt1mAlqXjPZNkpokJgSEPeoQvlLq3QCuEF56WYfzfL3W+rhS6nIA71JKfUFr/UHpjVrr1wJ4LQDccMMNWnrPRmzIEL6TuNVZHJXNwMpVvAXhWwfmEFg9h5+LyR7JfA5fSBAxrXpo0rYSvbDLcKqTYEpH0JWHhNuAeRi1NtcsIvxOOnyr0knzclOVBpnhJLBQjV5HrnV1vBLqwNoRqzReYQg/xvokK3Zva9bhtzswqmoCTGuBEVusYlKcNBGaAkqWRH6FOE9Ic0pnPoDSEemXwGfS0YbV/ES5iARz+IRmlUAFXcxLhP8oSdpqrb+l7jWl1CNKqSu11g8ppa4EcKLmGMft/yeUUm8GcCMA0eFvlQ0S046VlnE3WRLTkmv7t0AHtjYxSVveEpVy+CGdDAE/fBQTRIQ711oHq07c+0sOv4rwp4TDb3sgxb43LZz0IDYKlNVJCqXM9+G1CuBOUUBolWMSqiCrSewNPJlhe6Ea31VMUv64614ep8h1ORdqjylQYHVFV9TmhzFOXFwXK7kBriuXpYvUyt3N6iMzU3jlKrnbk7aAGTPKc/O50JUCc/fBF7N2UGFp1johhUATdeHwp8IY0xyG66q5V5K2NwN4kf35RQD+O3+DUmpRKbXP/QzgWwHcvsnzdjaatI0j1RpieaFxwEYSQDUJ2cThS6GgZLQ3TFuCqC6Rx41SFLUcvk3ahnL4UvuBtgdHKYWFQYy1Sd4oyywQWB7QTppw0sVG8g3a62kWkLQV9lOVxgsAzq+Z+oQNjVcAwndJSPddN8oyA8QGUWQqjyeEouCLmajSCdhkx11HrqV20uVCszbNWhdIdx10TwKg3TlzhM9bHNC2EVkREYaDsJIGKq/D3WrqyTL3hsN/JYDnKaXuAvA8+zuUUlcppd5m3/MYAB9WSn0GwCcAvFVr/febPG9ncxSF6/zYVFkJ+MmvOqfIraB0ppl1JDKHn2b1jla67gIpSWiCOCOpJbN4bwSB1C1mnNIJRaxNShbJ5ocx1mzOI4mUV/FbKbzKmjsZApTDL8eYf4bKUiXqjRtPsKbCJiHuGOdWjX68PcnNVU3tSW4AdoGsT9r6HH5uJcnhMkOpZbcbL611gfDb5pjXi0rIvXBKJxzh+/mstqioopxryHlMA3IegOXwG5RzVKsfskPYdtqmVDpa69MAvln4+3EA325/vgfA9Zs5Tx/mkPIK6XPSZJTDDw8fwzj8LNdkk/MwpGRooOoiQYtcQhpbmdfLz5QcPgvjk6jgowGjDmkySVfehpSAErEujeRwGwCyhk6O3KgOv67fCk3IT7O82EmqzsriJIdYq8d0Y+4QfpvMUObwA8erIWlLi6RCF90Bo18qlbbkWqdZDqUCxAYk0ZvleaV1hxuvcZpjdZoGc/jO0bpEaxtgmhv4NGtT9XvIHsDu2rkOn3/GLSS7rfBqdyw722CuJ43r/NhmUgFLSFUfYNCEtNUg7Z/iwu3WpG3inI2sOqHIuouCAmCRhtQqIMuLDcvbEL47JR2zIE56ENdSYLw/T6gCCUDRJA+o3httDz0JLFQz11GiyzrE6lpOtKlOeLLdjFfjRwCUOvy6pG3EEGvbvZljxF6eqIrwaRLSHLM1Qo7LpK2UkKYqnbVJ1qoCA+S2EaE064V1871Iyrku3TIB6/DTcjGTFsCBZQjqvqedst1xFdtgA0LpLAaGj219TriVlI7R4Vf51VJmKDVdkoxynVKCiDrvUH7VcdpTivDZvY2SktJJIhVAeziET5xiAEVRINYa9GWOWY8+udHxquNkefO01gRk5CP8TOhyOmIIv42icB/3xisE4VtduZP7NRUShX4HVGYodcvkoCJkMw+zyU49hx9FqpAHhyZtN1Ik5YCKW4il8ap0y2xD+KRR3rQm9xJbSme3yTJ3x1Vsg5kvSRetftuMhnqhErC5QQSlgDXLF0r8KmD59g5oAmCcdE1rhZDdiAC/pbK0OQXgV9qGJNRK52x+z/P2BRIwjnFtkom7AlU5/HbaY+AtkHLYnzApXnvOo7rw1HH4zuG3jZlr00yVYKHjBZSItUmWOQ0YL8CXGUrdMoteQpncI0qyhFKiNfUTRXHftAOH35VmtSDMfS9SROT6W4WKMyiHL9UtAOUc22uFV48aK5K2wUUe5YYeoZNLKYX5QVybtKWItQtfCFhOuiZBlFilQTClE5d8ex3CH8Sm0dvF9bAqSF7xK9EeklFKpy4iKnMp7ccsColSLVJg5rj+hh6hzdPcZyTE6o5RUjrdZIZSB07JnAOrR6xkV6jA74DKDOVumTbCyfOgBdJdBy3C4xw+YL6r/9HetcZIdlTn79x+zOzMvp/e9Xq9XtvAGj/AWRk/AgJswF4exkRI5IGMkshBCZKTCBITKxFJRJSHEiU/UIQDSCghIYiwwgITbAcQyg8ea7DBju1gwJGXtVi/1t6d3ZnpR+VHVd1b93bVvae6q/v2zNQnrban507d6tN1T5065zvnnFrsQDBorIBi6RSMsCrqrx7XpfBN5pxPbkynxAWmx5W0zFUUtF1JaDflrrzc7ae13ctgKufMNeCnwGzWFyAVF/XUfbjlfrt955FTJ1ItMxkUZoVEpw8/tViXWRukKS9uFjEgedKLHYcP36C3AWB2vJK/XzI2QFvilZmZXE0xzLt0rD5pQ15AdZBbz8un2ByQ8cpPllisPvIC8jRDaeE7Nl5lsXLiAmYguCfcFn66QbKCtjbmXPlc5ooKf+AUmX22LDOZE7TNjBAbu0pv5rp42rQEbdeOwm/oRCLZ3rAKDYM2xw0QAdKiWFjqotu3dyMCdICOz8MHdBBSBoiK1lIzkQsw7UbEpBl2++bpJf83bcMnzWFQmGO6KnDasK6VpKUVivLKWvbxg7YmD9+1mZkWGq8efj5o2+tZGsZ4unSAQWIAT17VFmta+rnPDNoa3bpszJ4shiHdZDwffmKc9uybWbuZGDEP/qlbzxMYLPpXxKxStHqDLD4bNtIDp3yHmf1u23R0shrXCJsUpuOcMQHMNKWv+DSblin/7/UFu/Y2IC2KyuNj3y/TFshcOtbjo6rvzs6CNLIrXUdjk3XCC6hl7hfuwwjIB90VtAUGsyt9aum4imEVudc+iWqA3WJtD+nSKdbDr8KAxWplncjXXYs/3oZi32SXS6fXlzx8ziZi+tt7jliCqfBnmXGigT7TFZ8vSWQPgRcdLjCzvhU3EKy9BYBbxu1mgqVOXzYwb1azmiaFNaPwN8+18PzCsuzexCzUBOStYK5Lx318NB6c1D3DZJ0on7Q9QCQZEdxM25SH33P78FMFdrbDslb1tMqyd22YNRKJbBtVsUFF1SZi8vBdx34z7Z8VtC1QKK3F05qZvIDqUhTAYDEwjvtF88pfcqyxfFcoLg+/oPAtGySA9BTZKimNnI2Z36htPvx2w7DwmeWRi/WtuNRffR9XQcNuv29kJlefIss2SEB+T0vdnpWeXSemZyZjxqa5VroAqwqnAdlD3i8JbNqwrt1w+ldzrhSuv9Bw6dgYFHKusr67b9A2p5wdPvwXFpZZG6RJOXW5iWyYa0sO+NlOz23hG5nGVZQ5LZ/lnnmKGnRR6FgDp+tYxq5y15vRivf5hWWsazWsCs42V7P/LFdeQHaSsDYr6RvyYvHwJYNN11YaqJZpBPnZtEyjeFrf5cNvJnh+QcU8mM+kb48KQJ4iT6rYitPNKkSa4MdrgGJs1JbrZ5sNLHb4WcSTwppR+FvmsoblGyoyKwHDqum7XQM2zLWbTuvLtPC5FLBigNVmTTQSWd899eGzLda+M0Vdu726fYGt69uoQs6Hn7qJKv8s55O2KSddDEwHgjn1ebQF5uqKpEsFLDM3SJ0q3+lnSrGo0DXVt9cX2DpfLS/92fI+/Oq/MVknbUsCVD6RiB+07XT7TiWaWfh9VsxDjlkonmZZ53PtRpqJaj6fLpgsnX5fWONZNsy1Gzi5wHGz8lw6Jg+/a8nJAICZVoLFbk/WCWKcXiaFNaTws9ZmOzfOVl6fd3vw/O2A3Ex0P87BxTXICOA39Og7Oz4VWTrcRKKuEZ8oPuTbDCW/lfEwZg9On102Asgsu5MLHStXWVt1Om+BrWxyCsxCy+xnLB4OR1orUjWklaWzURkS2xgbJJDFXgDFjuHISyn8F84sM05EovJEJOdOlf0DAMPC95CX/Dt7K89tRhvIbYxNUpZWUK44RjxHY/NcC6dUWfSy+lapm5XF0ukryrZ9A5xtKQu/w+vmNSmsGYW/aV22oHZtqFb4Gfea728H8pZKcWfPs2P4lfmALHPUtkFozq+vS6es3ozZk5VjsZp+Xp/j9mb1vZxa6joVWF/4udV0IpHbpaNdYLwHHJBUWHNM2zy2b5Ay41ireoxOv9w1UMRmNfapRbu88jGP6p7JQKbA3DTWjKXEpWU2G0mhnPTgPHYYa2wLa42ZOQa8pDIg/30U3bnmqdvWvcqGdkMma2nDzXb9bKuBpU5PNsOJLp3JY8u8aeHPlFwpkWtW4mGxmspRK4DimObxkZPkAWQ8fNvi0kkutq5R9jHNRW4/xpoKn2Oxmn5ebnwCyH8vNldbU7Vj5JaT1nNZzgVtB1knnT4/UU3/TVmQG8hkxrFW9bxMHj5HXvPtRmql2uSVb+jhwcPvustsmEHrTlewKIYt46Th8uFrOa1rNTzcRO6MYBfMZ3LH+ryxZ8tU5zPBhJM5NtNMsNjpRR9+XTB3eZ6LwsLSYSx001IxlSZgFk9zW9ZF6GbRyyWLSysj7dKpmmdGy3SXVjCtEh8Lv9vrG+6XannlH8bBjVi7BroeyllmVZsPsI3Jwk9UAzJeednpZbvaGLk+fDMbtcMo/QzIeILeJG3yKtbS4SnSJF1f8meHwlcuHa6FbxZPK3PpCIjK8eSY/kllQPZMthsJNq7Lb5J5sgG/nDSA1KiwbdSzrQSLqmxE9OHXgE3rMkuSxaAwgqWZS6daXKZ1t6No4Wvmj+C7X8wm067Fpf3cOgBW5TM0Ty+cHAOOAjOzUblZkEB+8y2eiPS8ythErrmYxdOKVE7tC9aVQGeYpXm7PZGyamzy0mwmTpA7m6cfLRPIjJeiQaHn1TUCihwrWNeGyYyQQRorkDX04PikOWWa9QYpePo+delo3znnRARk8buZlj3IDWR1pbguQyAraGh16TQbqYUfffg1wFfo9qCtnw+/WJWzuLg4Y7YLi8tu4UtLkdtdxzyic2r6cBS+mY1qq+rpwuY594lIj9ETgtW9SaOd+vDtsRc917Na4XOKgSnrsszC1ye4DbOtgd/ZxzQtfB4tE8i+j+0bBr8XGfOQr7ksnTYj5gHI09tSt+clL8Adn0gtfKbCbxknDRmQ9tsgXTVv9By73HLSBeac7YQog7Y9WXwwKvzpRzMxFRhf2ZjKsWhN2Hz41bTM4vHRTsvs9oSzz6lrTNNyLtM1LIVvBm2ZDCQgH2/YbrGMtZ87jaNwqj8qn7TLbaZ/Pq2YGxwFpoO2mbwsCl8Pw9RgJuuk6wjI26B997YNMpeNaqmLY0NRXi5aZrcvi4Fx6sI0k3yj71ILn+3SyYgUnCQ8Db1+bRuqmXjV7fEK2GWnbmmI2MadbSXoC+DUIi9xcVJYM7V0AOBjv3IlztlUzdABClawYjtw0qPNIGQRDTItFOXSqXggtTJa7vadi6vVIJXG3WOlcadH9F7foBkOjrttvo3nFpZZD3gr9zDyg9wmnC4KM6DGeCBnFE86O0XZaYZnlviFrYpBW9vmv3/bPABgB4MFJueRpKcMTulnDb2fFF2GwGDZCJ68Grm8hEF5ZQH5pW4fMy1egNXMTLZ9Nv1922IRrjEBo2onl6WjFL5tQ9UKXycMckkBgDbC7Kdu7VFYmDILfySFT0TvBvARAAcBXCWEOOq47kYA/wCgAeATQoi/HOW+w+Ktl+9mX2tmo3J9oUA5JS+3uJgKTCujpa6iZVrm0UgSdPo9ZX3xONJA1uTafM/EPbe/FsdeOFs5HpBv6JFV9eTJTMOtwNxlEmyYaTaw1DHYVQ6a4YJq38hRYJr9kjFZBv/mN197AAd2rMcNB3dWjifHMPztHha+noONDVRM5uKsW/35F9SJx2XhL3ftjX1sKFIobSei+Zkm/vydl+K1F22vHM+cV0+dIrkxj43KxWZnuOXdm9wTEZDRmq0sHUPJryYL/2EA7wLwcdcFRNQA8DHIJufHAHyXiO4WQvzPiPceK4pBW45vD8gWg909YXD7mYlErQaBCFjq9JSfd3BxtZTiWOr2WAHIluGT1aPZFu2ujbPYxUhSA/LZqFx3VRG2OEtm4fPjKDOtBC8sLDvjJFqxerl0VPXD7LMNXtNICG+6ZFflWOaYWYN6XsAQyBrC2JRTI8k39OCcsvTn1w3rB2v9y5/TDZJ14qN0jZdtPO+9+vzKsTTMTm3cHsBARsN+37X7B8c0fPgdbocwo+2oK05irqlVY+ELIR4FBn3VBVwF4AnVzBxE9FkANwOYaoXfMpQzt1Wcxt0fuA7nWBSl2XC85/AvF0FEmGkmysIXmLVYozOtJG17xw2oAfKz6a+Ow1yqHFfVT+k4XAMuvPEVO/G1x05Yf9dSPHxu3gKAVF6ugLSW0Utn+QpMs3Sy5JzRw1+alqmb7HDl9UtX7sU3Hn8GB/dsHPhdy1CKHWZgU39+beEXZax/n8mLscaSxEhO6rOKnFUh36mN/0xunG3hJ39xGLYpFEuC+Lh00nInVh/+6rTwOTgXwFPGz8cAvMZ1MRHdBuA2ANi3b994Z1aCXDDHI40bAC7fu9n6vkld9GGdzDRlzRGXVaMpYLoUaxXMoK1+CDgFsaqgq1D6BG0B4JO3HnL+rtWkgnuG59JZ7PSMioqDvwfgrKBoQzPNRlUVSQPUN9dxARebyIW3X7EHb7t8t9XQMtllXWa5Bv35tQVf3My0kZHKi+ECy52Q+yK1ikdBLmjr4WYF3AZNrr5Vj7fp6udnqetmzs0aa2pF0TKJ6H4ietjy72bmPWySdoblhRB3CSEOCSEO7dixg3mL8DCVM5ftUDmmUYde19HmBIKlxdpTtcjt/sLFNGjr0aykl9XQD9GgoTWQnMSTGZFbDq1GknMT8TZIaeEvq2YdxbG1AtN9Ybm88p7pWgqxHlT5AVcNmzKUyQso759QhFbgpxftFv5sq7hB+nWM4yZrVaEYtPUlBdjHNOXFy95tG0QKFwMpZ+FPkcKvtPCFEDeMeI9jAM4zft4L4PiIY44dZt9XnwBRGZKEFBXPTbG0YaYlmyksOnz0s60ES9rC51hfZtDWo7lLFdKsWI+8hSqklRw9yjVoF9dip2eVx0xRgbEs1gQLqjMXUN1VjIOGopxyOy1xYDJIuDz84omnuAFmLjC/ExGQWeMhFH6+7o1gfW9VyJVJYLOaMgvf5YqbVoU/CR7+dwFcTEQXEFEbwHsA3D2B+46EXFu3fpgFC2Ruj44HDU+7dFw++qrfF5FrfN63W8HDoJXkXTphFBip78AjaNtsqA3QnuU4m/qk+QpMB8Y7nu6q0jF1kNuDcsoZE8isT54LTJ947D56Isp1p+IGuQGkZbtDfDazU5uLouyLVsH1xHUZAkrhu1w6xmY0TT78kSRGRLcQ0TEA1wD4MhF9Vb2/h4juAQAhRBfABwB8FcCjAD4nhHhktGmPH6afm9sqjjvucrfvFXTSLh2nAmtJ3vmZTpfdLFmXVO50wzyMcsxE1djnV6Gsgq7k6GrgboN26bg2wKJPmtej1a8iKQc6yO3KFxgGel66bAQ3yA0gbQNo3yST1AXGK0VhMNKYTVOqUIxP+NJ+7WOaAVjec55a+J1eaccrjWlS+KOydI4AOGJ5/ziAw8bP9wC4Z5R7TRqmn5tbY4ODtvJz9/p8P7BWYIudfmqdmsja3nWxZ9M61pi68XmPWWCLNWZqsQZ26fT4pWsBKY9uX2BhuevYIDMXBrffaLEiaRCFnwZtdRJeOAV2dpnPJtIKXHeFcsnMK8htcPeFCCUvbYT1g7lZs9Ilgp+o1jJcOi4eviEjTg/tSWESLp0ViWLQNpRSlD5pwbYmgCyRaLHTs9IyNSPgxbMdlvUF6FonYd1VLU3LHJKHbx2zwI7xsljPduzyKih8DnRtmLAuHRm05VZO5Y4JAGeW7QFYGwZpqnaZ+bGa5H3PdLq5n0dBKzGeSY/iaaVj5pKoeM+5PkUvKreh7VRtWvUX7Vg/8jxDYXq2nilDLmjrEWCtQsvoeO8TtH3m1BK6fWFdXEMpMGXhy0Ue5rMNlDIO4GNNKzl6lGswFZjtOG1uCLZyDjZkdfnDWfhaXiHHTMtGKJcOt5E84A7ayvcSPPWCB0snPWnIeYRw6eRbhPrRMl0othDlbLopjXWph76A1ajYuWEWH73lUtxwcFeQHJdQiArfgYzJ4le3owqtRPrbZWYq36WTPYzljACuwm81ZMmCUJQ5PWauPHIQrnqSHrcBJi3TUGC22kZaXkL4ycvsChVEOesgd0B5pT58pWh9Nsh0jTksfF3Dh1VLR31PWuGHdOlkJQ3CWfjL6hTpF+Qu3wB/9TX8LOJJISp8B3LFwHqC1ceTO67McOVbKDPNRqX1lb3muXR0hc2Oo7zrMNDlfn1aB1ZhoHuVl4ui44h5JAPXViGzxsO5dLQCSxvXBFRgZz0sfFNejYTsNMOmn1FRLEEdkoGkT1qh3GqArHzJrWdUZC3ZjLBpxcqZ6YShn5PQQVvt0vEZc7aV4JSDMiffG8bClwlNMpkrUNA2pWXyA6xVaDckHdI30xaQfXJt1mheefE2yGLf11BBW8BPOVdBZ7Se8bHwW5m8bNa9vMbcJPmJV2c7AS18o2S5Tx5LGRoJISF4P5MzzcSg9U4PC6cKUeE7kC8GFjZou+wRIALyC8pmwfs+jAAyHn7ggLQZtA2VSNQXSGv9+wQhAVgt/CSh1KfMTd7RHZyyzOQQLp28nzukSydzpXjKy3FCzBkVHqUVzgT04edKkwTKfpfjZsFzn2cyo6muHDW6cmZaA1KetGfdjjKkvHKfTNvcA1nhw+cqsEaSdnAKHbTVDKQQyVzF4B830zZ77VBg6ho+S8e/iFsVtDW5OAYrODs18CiUenm75OHrBmuOYTMbR9AWkJtRp+uXbyPjavzie9OCqPBLkFHxwgY203RzLg+/woL39a8CmYW/3A1DbwOyoG0ojrQeE/D1SVfLQ2+SXicilTUq5xXOYl3shiutoF06Wl4cF4WsyCrl4LLwzfc51nrxewsVzwGMoG2oddv0N8JmW5lLJ/rwVwlahlUXToFl/UP5/sJyC352GJeOsZmFOG4DZnJSGAYFkCXGpD5pjyAkUOai8LXwZa/YpU5IH36RUVOPSwcwTjxOhZ/Ji3Ny02s7JC3TbKbu0zCmCpqB5VNjf6bZiD781YaGsupCUcAA7cMXWO722cyfKgU2FC0zyayacCydLHM05AYJyGYlTQeDpIiZHGupwsJnsprMkgUJhQlIFwObIVk6WZCf9/n0unHKq+IE4JpHSJaOls9yV7bmDEWkaCqq9DKzvDgg19gp1T9gJVn4kZZZgownHa6WTltZ+P0+sReKuQhdWZDp79k+fJIt64K6dBLvbkRV0HI/tchPKqsKcsv3PS38JAtChpQXkPnww+QtZPIC+MpIy6zaBcZfX4CZ8RvQBRbQTQTIU2SnJ1THuGGeyWjhrwqkxcAC+6Q7PX7teiBvhVbx8C/YPs+cR0YzDOXS0dz+kNm7+hR0eqnLtsarNkgAaTmD87fOscbUymVhuRtUXkBYP3fbOBEBw1j45S6wc7fwajXpeehG8SHLI4eksQLymVxULUT58jKfyZWjRqOFX4JxBG0126Mv7O0Kbahy6ZiL7+KdG1hjzrYSLHbCunRaSl4+/Vmrx1QKbNHNES+C48P/6bMLAIBXnjvYJtAGPc7pxW5QeQHAYidc3kKroPDZa0xdZ6OxAtnGeyGzLozJ7ZfzCphF3PFrockZd8Gjv3HxOq4hMg2ICr8Euo9pSAqYycMfypqwLEhTUfDjAo20i1aoB8cs9xuS2w/oJCqevGZb1daX5vW/cs8m5phGLCGgvADDRRHQh3/a04evFb3LpfH0i4sA+Apfr9Osi1YIymnepRPymfRpaA8UTt2BsvAngZUz0xqgWSchKWDt1KXDDxDtM9wOZUqPOx6QWfjLgboRAYWgbcDjNiAVB/fzzRkF06oU3s4NvOJpepzTS+FcOs2CTzpk+QHfgOK+bXKNuSx8vRFcsZe7QWbykvMK59IJKS9Aup/SDXIYt2G08FcHUl55UAUmW/bJUse8hfKyczKrymVNfP791+A8pj8akA/kYldT5gJ+Nk3LDG3hL3axezPPf0xE2DbfxnMLy06Fd+/vvQ7Pnl5iJ4fpcU4FdemEZ7LobmanKgp7FXHZuZvwhe/9LK0PU8Tt11+MK/dtwbUXbWeNN2PEXoAwLh2d/X424IkIkHL33SA5caJpxMqZaQ1oNpKUAhauPHKCsx1ZVnUY1olLkR7avxW7Ns6y5zHTlH1yw7p0sqBtyA0SkErR58G6cKfcJHWFxyJetmsDrr2Qp7yAzGI9tRjOpfrRk2gAAAzsSURBVFPkqoek/mo/N5d1ctm50nL/0YlT1t/PtZt4yyvPYc+h2UhyG0+oU2QjoeAWfquReG+Q+rpGwq96Ow0YaaZE9G4ieoSI+kR0qOS6J4noh0T0IBEdHeWek0QzodQKDtckRCbwAHxOMwDMB26Tpi38TkiXjqo3EzZRLRvHR143XSqVU6j2cpnF2gmadQ0YmbZjkBl3k7xkjwxeX7QzXLOOmWZW9C/YZ0uSoEFuoLBBevDwfa6fFozq0nkYwLsAfJxx7RuEEM+OeL+JotkgvHg2fIBIw6fo0tc/9Ho89fyZIHMA5EIVQtYBD+XS0ZbvUjdg/4DGcEfn9127H4fO34rLmD7nKujNZrETUF5jCNoCWeC+zcyKBaQF/5XbX4u9TNolB7OtBp5bkG0TQ8Y9QtJYgawcBeBPYw1VNn1SGLWn7aMAghTJmkY0kyRlc4Ss26Hho8B2bpjFzg18l00VTGs5mEvHCEKG6uNpPtQ+Fj4RBVP2QP67Goe8zJ9HHjfRFEu/eR7czaOocmF+XyFpzeNg6WhwjbAdKth/8ow95jGtmNT2JADcS0QPENFtZRcS0W1EdJSIjj7zzDMTmp4drQYZ/tXwi8tHgYXGzBgeRrPcb7jjtr97YhzIK6/A8YnQa0xZrHWzR/KbZKjNjIJn2g5zinz1eVuC3HvSqDTDiOh+ALZozZ1CiC8y73OdEOI4Ee0EcB8RPSaE+KbtQiHEXQDuAoBDhw45Qm6TQTNJgjIogDwjpk4FZt47ZC0dQAZYx/Ew1pnRaFp+IWMegJRXI1A5aSCbX93+5ZxREZBRMw4fvgbXCHv5ObwEx2lDpcIXQtww6k2EEMfV/yeI6AiAqwBYFf40oTkWCth08HfHctxOedIBaw9NSc0S897h5JVljoaSF5D5y+s8Qcr7y3k0EwrWyLuVZEXL6gxyh9psJo2x8/CJaB5AIoQ4pV6/GcCfjfu+IbCu1Uj7jc4GYnsMG4QMjbyFH3YzWxqThV+vS2d8J6KQ8jLHrd3CV/cPpZj1WIsBG6MXx/ExKr7/x29Cz8X7nVKMSsu8hYiOAbgGwJeJ6Kvq/T1EdI+6bBeA/yaihwB8B8CXhRD/Ocp9J4V5I/AYihaZtyamw8IP6V8FVBmEQMqmOSQtMzTajQTa4xI6aBtSXkCmwOq38OX9Q25mrUaChYAVOIvj+HwPW+bb2L6el6k9LRiVpXMEwBHL+8cBHFavfwLgilHuUxfmZxrG6zCHofaU+KRNBkcoytxc29ggxyCvOi1W2RUqUbTM6ZUXMD0+fF2mIZS8AFk2Q+exrJ8Js6Hl4morqPLlMFjdn25E5B7IdpgHsjnk8TE0xsHSMTfIuUAP47Swmsz7h3LpzBmfZy5gYl1VqeNJQSvPkC6dcWySw7p0ViKiwi+BySWfD6TAts6309e1WvjGvUM9kDl5BdogJXtFvp4WizWUSydJKHUVhrTwdfLU1MgrEOEByK+xuUBrzMyNWanBWC6iwi+BaXWFSiQ6YDQomRbWyZa5dsmVfJgPYEiLdc8mpcBqPm5rZbB5XSvYmFrRh5SXboKj+ep1QRsV25kVSTnIuVkDySzk9zntiAq/BDlrIpDC32JY+HUqMNPCP38bv8pmGUx5hdogAWDPZplhXPdx+4UzskxAKHkBmcIPKa8D22U9nOMnF4ONOQy023CfRxXXKmh5zTSTYCeti3eFqx807YgKvwTmMXtuDP5QV+3xScC8945ATIO8Dz+kwpcWvi61WxfOKDrgvq28NpIcaJmFck8AwAU75PyOnzwbbMxhoGMd5wWsz6NdhSE3yIt3rcwkqmEQFX4J9MM4324ESxwxESr4NwzM00WozzYOGisA/ML5Mo29bp+0xr6QFr5SYKFiRABw3hY5v/3M/sbjwvMLss7M7k3hakClLrCA8to4u3ZcOrEBSgmyxRVWTJ+89RDuf/TntRadG4d7xFTIIS3W9159Ps7dvA5vePnOYGOOgnM8+g5UYX3qww8nr3Yzwefff03tCv/ES9KltDOovLQRFlXXMIhSK8E4jo8AcP3BXbj+4K6gY/pCByB3BAyomRtYSJkRUe3yMhGSyaFPWqE45RqH9m8NOt4w0B3YLtwR0gXWzP0fCt/+o+trD3JPAlHhl2AcDIppwr/fdjUOMJtS+yLkkXta8I0Pvj7N8gwFgtw8Qlr404I7bnoF3n7FHly0M5yPfFzPpE+3uJWM1bfKAmJuDBzpacJrDmwb29ir8cg9ThdJSB/+tGC21UjjL6EwrlP3WsF0RMGmFDNN2ZczLi5/rEYLfyxQ3qG6s2JXCsbBalpLiAq/BESEuXZj1bp0xonVaOGPA6s7rzM81qd5C/GZHAZR4VfgFbs34mVriKcbCqs9RT2iHoyLObdWEKVWgc/91jV1TyFiFWPDrHwEQ5YQXs1I8xbiqXsoRIUfERRHfvta/PiZhbqnsWJwx00HsX39DN58yfTQTqcZG9c18aG3vByHL9td91RWJEhMcceWQ4cOiaNHj9Y9jYiIiIgVAyJ6QAhxyPa7eI6MiIiIWCMYtcXh3xDRY0T0AyI6QkSbHdfdSESPE9ETRHTHKPeMiIiIiBgOo1r49wG4VAhxOYD/BfDh4gVE1ADwMQA3AbgEwC8T0SUj3jciIiIiwhMjKXwhxL1CCJ1r/i0Aey2XXQXgCSHET4QQywA+C+DmUe4bEREREeGPkD78XwfwFcv75wJ4yvj5mHovIiIiImKCqKRlEtH9AM6x/OpOIcQX1TV3AugC+IxtCMt7TmoQEd0G4DYA2LdvX9X0IiIiIiKYqFT4Qogbyn5PRLcCeBuA64Wd43kMwHnGz3sBHC+5310A7gIkLbNqfhERERERPIzK0rkRwB8CeIcQ4ozjsu8CuJiILiCiNoD3ALh7lPtGRERERPhjpMQrInoCwAyA59Rb3xJCvJ+I9gD4hBDisLruMIC/B9AA8CkhxEeZ4z8D4P+GnN52AM8O+bfjRJyXH+K8/DCt8wKmd26rbV7nCyF22H4x1Zm2o4CIjrqyzepEnJcf4rz8MK3zAqZ3bmtpXjHTNiIiImKNICr8iIiIiDWC1azw76p7Ag7EefkhzssP0zovYHrntmbmtWp9+BEREREReaxmCz8iIiIiwkBU+BERERFrBKtO4U9TKWYiepKIfkhEDxLRUfXeViK6j4h+pP7fMqG5fIqIThDRw8Z7zrkQ0YeVDB8nordMeF4fIaKfKbk9qPI4Jj2v84jo60T0KBE9QkS3q/drlVnJvGqVGRHNEtF3iOghNa8/Ve/XLS/XvGpfY+peDSL6PhF9Sf08XnkJIVbNP8jErh8DOACgDeAhAJfUOJ8nAWwvvPfXAO5Qr+8A8FcTmsvrAFwJ4OGquUCWsX4IMqnuAiXTxgTn9REAH7RcO8l57QZwpXq9AbL89yV1y6xkXrXKDLJm1nr1ugXg2wCungJ5ueZV+xpT9/t9AP8K4Evq57HKa7VZ+CuhFPPNAD6tXn8awDsncVMhxDcBPM+cy80APiuEWBJC/BTAE5CyndS8XJjkvJ4WQnxPvT4F4FHIKq+1yqxkXi5Mal5CCHFa/dhS/wTql5drXi5MbI0R0V4AbwXwicL9xyav1abwp60UswBwLxE9oKqAAsAuIcTTgHx4AeysbXbuuUyDHD9AspPap4xjbS3zIqL9AF4NaR1OjcwK8wJqlplyTzwI4ASA+4QQUyEvx7yA+tfY3wP4AwB9472xymu1KXyvUswTwHVCiCshu339DhG9rsa5+KBuOf4jgAsBvArA0wD+Vr0/8XkR0XoA/wHgd4UQL5VdanlvbHOzzKt2mQkhekKIV0FWxL2KiC4tubzuedUqLyJ6G4ATQogHuH9iec97XqtN4XuVYh43hBDH1f8nAByBPIL9nIh2A4D6/0Rd8yuZS61yFEL8XD2kfQD/hOzoOtF5EVELUql+RgjxBfV27TKzzWtaZKbmchLANwDciCmQl21eUyCv6wC8g4iehHQ9v5GI/gVjltdqU/hTU4qZiOaJaIN+DeDNAB5W87lVXXYrgC/WMT8F11zuBvAeIpohogsAXAzgO5OalF7wCrdAym2i8yIiAvBJAI8KIf7O+FWtMnPNq26ZEdEOItqsXq8DcAOAx1C/vKzzqlteQogPCyH2CiH2Q+qprwkhfg3jlte4os91/QNwGJK58GPIrlx1zeMAZFT9IQCP6LkA2AbgvwD8SP2/dULz+TfIo2sH0lr4jbK5ALhTyfBxADdNeF7/DOCHAH6gFvruGub1i5BH5h8AeFD9O1y3zErmVavMAFwO4Pvq/g8D+JOq9V7zvGpfY8b9Xo+MpTNWecXSChERERFrBKvNpRMRERER4UBU+BERERFrBFHhR0RERKwRRIUfERERsUYQFX5ERETEGkFU+BERERFrBFHhR0RERKwR/D9Mjzi/dXyigQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAEKCAYAAAAcgp5RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8GearUAAAgAElEQVR4nO3dfbQcdZ3n8fenH29IwmOCAuEhjqA8hYAxOjpHgiIDzPC0BwSOo6OoGV3R0d11lmGP6OA4M6uuZ0dFMIsszlkEXDFjZjYK7KwaFQUSjJFnMwHlGgbCQ0JCktvV3d/9o6r61u1053bf9O2q2/19ndPndldVd//qdve3v/39/epXMjOcc84NrlzaDXDOOTe9PNA759yA80DvnHMDzgO9c84NOA/0zjk34DzQO+fcgJs00Es6UtIPJD0i6SFJf95iG0n6kqSNkjZIOi2x7mxJj0Xrrur1DjjnnNu7TjL6KvAfzex44I3AhyWd0LTNOcCx0WU5cD2ApDxwXbT+BODyFvd1zjk3jSYN9Gb2tJk9EF3fDjwCHNG02QXAP1jo58CBkg4DlgIbzWyTmVWA26JtnXPO9Umhm40lHQOcCtzbtOoI4KnE7dFoWavlb2jz2MsJfw0we/bs1732ta/tpmnOOTfU1q1b95yZzW+1ruNAL2kOcAfwMTN7qXl1i7vYXpbvudBsBbACYMmSJbZ27dpOm+acc0NP0m/areso0EsqEgb5W8zsOy02GQWOTNxeAGwGSm2WO+ec65NORt0I+DrwiJl9sc1mq4B3R6Nv3ghsM7OngfuBYyUtlFQCLou2dc451yedZPRvBt4F/ErS+mjZ1cBRAGZ2A7AaOBfYCOwE3hutq0q6ErgTyAM3mdlDPd0D55xzezVpoDezn9C61p7cxoAPt1m3mvCLwDnnJhUEAaOjo+zevTvtpmTSyMgICxYsoFgsdnyfrkbdOOfcdBsdHWXu3Lkcc8wxhJVjFzMznn/+eUZHR1m4cGHH9/MpEJxzmbJ7924OOeQQD/ItSOKQQw7p+teOB3rnXOZ4kG9vKv8bD/TOOTfgPNA751wLn/3sZznxxBNZtGgRixcv5t577+X9738/Dz/8cM+fa86cOT1/zCTvjHXOuSY/+9nP+Od//mceeOAByuUyzz33HJVKhRtvvDHtpk2JZ/TOOdfk6aefZt68eZTLZQDmzZvH4YcfzrJly4inZ/n617/Occcdx7Jly/jABz7AlVdeCcB73vMePvrRj/KmN72JV73qVXz7298GYMeOHbztbW/jtNNO4+STT+a73/1u3/bHM3rnXGb91T89xMObm6fW2jcnHL4/nzrvxL1uc9ZZZ3Httddy3HHHceaZZ3LppZdy+umnN9Zv3ryZz3zmMzzwwAPMnTuXt771rZxyyimN9U8//TQ/+clPePTRRzn//PO5+OKLGRkZYeXKley///4899xzvPGNb+T888/vS8ezZ/TOOddkzpw5rFu3jhUrVjB//nwuvfRSbr755sb6++67j9NPP52DDz6YYrHIJZdcMuH+F154IblcjhNOOIFnnnkGCMfAX3311SxatIgzzzyT3/3ud411080zeudcZk2WeU+nfD7PsmXLWLZsGSeffDLf+MY3GuvCyQDai0s+yW1vueUWtmzZwrp16ygWixxzzDF9O/rXM3rnnGvy2GOP8etf/7pxe/369Rx99NGN20uXLuVHP/oRL774ItVqlTvuuGPSx9y2bRuHHnooxWKRH/zgB/zmN21nFe45z+idc67Jjh07+MhHPsLWrVspFAq8+tWvZsWKFVx88cUAHHHEEVx99dW84Q1v4PDDD+eEE07ggAMO2OtjvvOd7+S8885jyZIlLF68mH6eXEmT/QRJg594xLnh9cgjj3D88cen3YxJ7dixgzlz5lCtVrnooou44ooruOiii/ry3K3+R5LWmdmSVtt76cY556bg05/+NIsXL+akk05i4cKFXHjhhWk3qS0v3Tjn3BR84QtfSLsJHfOM3jnnBpwHeuecG3Ae6J1zbsBNWqOXdBPwx8CzZnZSi/WfAN6ZeLzjgflm9oKkJ4HtQA2otusRds45N306yehvBs5ut9LMPm9mi81sMfCXwI/M7IXEJmdE6z3IO+dmhHw+z+LFixuXJ598kje96U0APPnkk5x0Upjzrl+/ntWrp++U2MlJ1PZFJycHXyPpmA4f73Lg1n1pkHPOpW3WrFmsX79+wrJ77rlnj+3Wr1/P2rVrOffcczt+7Gq1SqHQ3wGPPavRS9qPMPNPHgtswF2S1kla3qvncs65fms+OUilUuGaa67h9ttvZ/Hixdx+++28/PLLXHHFFbz+9a/n1FNPbUxFfPPNN3PJJZdw3nnncdZZZ7XdbteuXVx2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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input: Signal shape (400,)\n", + "Output: Signal shape (400,)\n" + ] + } + ], + "source": [ + "signal_1d_bandpassed = butter_bandpass_filter_signal_1d(signal_1d, lowcut=5, highcut=12, sampling_rate=100, order=4, verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cosine similarity: 0.983947727981058\n" + ] + } + ], + "source": [ + "from numpy import dot\n", + "from numpy.linalg import norm\n", + "\n", + "signal_target = generate_signal(length_seconds=4, sampling_rate=100, frequencies=[7,11])\n", + "cosine_similarity = dot(signal_target, signal_1d_bandpassed)/(norm(signal_target)*norm(signal_1d_bandpassed))\n", + "print(\"Cosine similarity:\", cosine_similarity)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Signal shape: (1, 1, 400)\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAEKCAYAAAAcgp5RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8GearUAAAgAElEQVR4nO3dfbQcdZ3n8fenH29IwmOCAuEhjqA8hYAxOjpHgiIDzPC0BwSOo6OoGV3R0d11lmGP6OA4M6uuZ0dFMIsszlkEXDFjZjYK7KwaFQUSjJFnMwHlGgbCQ0JCktvV3d/9o6r61u1053bf9O2q2/19ndPndldVd//qdve3v/39/epXMjOcc84NrlzaDXDOOTe9PNA759yA80DvnHMDzgO9c84NOA/0zjk34DzQO+fcgJs00Es6UtIPJD0i6SFJf95iG0n6kqSNkjZIOi2x7mxJj0Xrrur1DjjnnNu7TjL6KvAfzex44I3AhyWd0LTNOcCx0WU5cD2ApDxwXbT+BODyFvd1zjk3jSYN9Gb2tJk9EF3fDjwCHNG02QXAP1jo58CBkg4DlgIbzWyTmVWA26JtnXPO9Umhm40lHQOcCtzbtOoI4KnE7dFoWavlb2jz2MsJfw0we/bs1732ta/tpmnOOTfU1q1b95yZzW+1ruNAL2kOcAfwMTN7qXl1i7vYXpbvudBsBbACYMmSJbZ27dpOm+acc0NP0m/areso0EsqEgb5W8zsOy02GQWOTNxeAGwGSm2WO+ec65NORt0I+DrwiJl9sc1mq4B3R6Nv3ghsM7OngfuBYyUtlFQCLou2dc451yedZPRvBt4F/ErS+mjZ1cBRAGZ2A7AaOBfYCOwE3hutq0q6ErgTyAM3mdlDPd0D55xzezVpoDezn9C61p7cxoAPt1m3mvCLwDnnJhUEAaOjo+zevTvtpmTSyMgICxYsoFgsdnyfrkbdOOfcdBsdHWXu3Lkcc8wxhJVjFzMznn/+eUZHR1m4cGHH9/MpEJxzmbJ7924OOeQQD/ItSOKQQw7p+teOB3rnXOZ4kG9vKv8bD/TOOTfgPNA751wLn/3sZznxxBNZtGgRixcv5t577+X9738/Dz/8cM+fa86cOT1/zCTvjHXOuSY/+9nP+Od//mceeOAByuUyzz33HJVKhRtvvDHtpk2JZ/TOOdfk6aefZt68eZTLZQDmzZvH4YcfzrJly4inZ/n617/Occcdx7Jly/jABz7AlVdeCcB73vMePvrRj/KmN72JV73qVXz7298GYMeOHbztbW/jtNNO4+STT+a73/1u3/bHM3rnXGb91T89xMObm6fW2jcnHL4/nzrvxL1uc9ZZZ3Httddy3HHHceaZZ3LppZdy+umnN9Zv3ryZz3zmMzzwwAPMnTuXt771rZxyyimN9U8//TQ/+clPePTRRzn//PO5+OKLGRkZYeXKley///4899xzvPGNb+T888/vS8ezZ/TOOddkzpw5rFu3jhUrVjB//nwuvfRSbr755sb6++67j9NPP52DDz6YYrHIJZdcMuH+F154IblcjhNOOIFnnnkGCMfAX3311SxatIgzzzyT3/3ud411080zeudcZk2WeU+nfD7PsmXLWLZsGSeffDLf+MY3GuvCyQDai0s+yW1vueUWtmzZwrp16ygWixxzzDF9O/rXM3rnnGvy2GOP8etf/7pxe/369Rx99NGN20uXLuVHP/oRL774ItVqlTvuuGPSx9y2bRuHHnooxWKRH/zgB/zmN21nFe45z+idc67Jjh07+MhHPsLWrVspFAq8+tWvZsWKFVx88cUAHHHEEVx99dW84Q1v4PDDD+eEE07ggAMO2OtjvvOd7+S8885jyZIlLF68mH6eXEmT/QRJg594xLnh9cgjj3D88cen3YxJ7dixgzlz5lCtVrnooou44ooruOiii/ry3K3+R5LWmdmSVtt76cY556bg05/+NIsXL+akk05i4cKFXHjhhWk3qS0v3Tjn3BR84QtfSLsJHfOM3jnnBpwHeuecG3Ae6J1zbsBNWqOXdBPwx8CzZnZSi/WfAN6ZeLzjgflm9oKkJ4HtQA2otusRds45N306yehvBs5ut9LMPm9mi81sMfCXwI/M7IXEJmdE6z3IO+dmhHw+z+LFixuXJ598kje96U0APPnkk5x0Upjzrl+/ntWrp++U2MlJ1PZFJycHXyPpmA4f73Lg1n1pkHPOpW3WrFmsX79+wrJ77rlnj+3Wr1/P2rVrOffcczt+7Gq1SqHQ3wGPPavRS9qPMPNPHgtswF2S1kla3qvncs65fms+OUilUuGaa67h9ttvZ/Hixdx+++28/PLLXHHFFbz+9a/n1FNPbUxFfPPNN3PJJZdw3nnncdZZZ7XdbteuXVx2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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input: Signal shape (1, 1, 400)\n", + "Output: Signal shape (1, 1, 400)\n", + "Filtered signal shape: (1, 1, 400)\n" + ] + } + ], + "source": [ + "signal = np.expand_dims(signal_1d, 0)\n", + "signal = np.expand_dims(signal, 0)\n", + "print(\"Signal shape:\", signal.shape)\n", + "\n", + "signal_bandpassed = butter_bandpass_filter(signal, lowcut=5, highcut=12, sampling_rate=100, order=4, verbose=True)\n", + "print(\"Filtered signal shape:\", signal_bandpassed.shape)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 15b9f7411d5a667819fccffd8222439a270c8c0b Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Tue, 15 Feb 2022 10:32:02 +0800 Subject: [PATCH 2/2] add openbmi and notch filter --- splearn/data/openbmi.py | 71 +++++++++++++++++++++++++++++++++++++++++ splearn/filter/notch.py | 7 ++++ 2 files changed, 78 insertions(+) create mode 100644 splearn/data/openbmi.py create mode 100644 splearn/filter/notch.py diff --git a/splearn/data/openbmi.py b/splearn/data/openbmi.py new file mode 100644 index 0000000..39464ba --- /dev/null +++ b/splearn/data/openbmi.py @@ -0,0 +1,71 @@ +import os +import numpy as np +import scipy.io as sio +from typing import Tuple + +from splearn.data.pytorch_dataset import PyTorchDataset + + +class OPENBMI(PyTorchDataset): + """ + EEG dataset and OpenBMI toolbox for three BCI paradigms: an investigation into BCI illiteracy. + Min-Ho Lee, O-Yeon Kwon, Yong-Jeong Kim, Hong-Kyung Kim, Young-Eun Lee, John Williamson, Siamac Fazli, Seong-Whan Lee. + https://academic.oup.com/gigascience/article/8/5/giz002/5304369 + Target frequencies: 5.45, 6.67, 8.57, 12 Hz + Sampling rate: 1000 Hz + """ + + def __init__(self, root: str, subject_id: int, session: int, verbose: bool = False) -> None: + + self.root = root + self.sampling_rate = 1000 + + self.data, self.targets, self.channel_names = _load_data( + self.root, subject_id, session, verbose) + + self.stimulus_frequencies = np.array([12.0,8.57,6.67,5.45]) + self.targets_frequencies = self.stimulus_frequencies[self.targets] + + def __getitem__(self, n: int) -> Tuple[np.ndarray, int]: + return (self.data[n], self.targets[n]) + + def __len__(self) -> int: + return len(self.data) + + +def _load_data(root, subject_id, session, verbose): + + path = os.path.join(root, 'session'+str(session), + 's'+str(subject_id)+'/EEG_SSVEP.mat') + + data_mat = sio.loadmat(path) + + objects_in_mat = [] + for i in data_mat['EEG_SSVEP_train'][0][0]: + objects_in_mat.append(i) + + # data + data = objects_in_mat[0][:, :, :].copy() + data = np.transpose(data, (1, 2, 0)) + data = data.astype(np.float32) + + # label + targets = [] + for i in range(data.shape[0]): + targets.append([objects_in_mat[2][0][i], 0, objects_in_mat[4][0][i]]) + targets = np.array(targets) + targets = targets[:, 2] + targets = targets-1 + + # channel + channel_names = [v[0] for v in objects_in_mat[8][0]] + + if verbose: + print('Load path:', path) + print('Objects in .mat', len(objects_in_mat), + data_mat['EEG_SSVEP_train'].dtype.descr) + print() + print('Data shape', data.shape) + print('Targets shape', targets.shape) + + return data, targets, channel_names \ No newline at end of file diff --git a/splearn/filter/notch.py b/splearn/filter/notch.py new file mode 100644 index 0000000..28fb4f5 --- /dev/null +++ b/splearn/filter/notch.py @@ -0,0 +1,7 @@ +from scipy.signal import filtfilt, iirnotch + + +def notch_filter(data, sampling_rate=1000, notch_freq=50.0, quality_factor=30.0): + b_notch, a_notch = iirnotch(notch_freq, quality_factor, sampling_rate) + data_notched = filtfilt(b_notch, a_notch, data) + return data_notched