init commit: загрузка и предобработка данных
This commit is contained in:
commit
0a55543ae2
22 files changed
+1986
No files matched your search
@@ -0,0 +1,6 @@
|
||||
Записи/OpenBCI GUI
|
||||
Записи/OpenVibe Signals
|
||||
__pycache__
|
||||
.ipynb_checkpoints
|
||||
.pytest_cache
|
||||
runs
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
g_offset = nil
|
||||
g_duration = nil
|
||||
|
||||
-- this function is called when the box is initialized
|
||||
function initialize(box)
|
||||
|
||||
dofile(box:get_config("${Path_Data}") .. "/plugins/stimulation/lua-stimulator-stim-codes.lua")
|
||||
|
||||
g_offset = box:get_setting(2)
|
||||
g_duration = box:get_setting(3)
|
||||
end
|
||||
|
||||
-- this function is called when the box is uninitialized
|
||||
function uninitialize(box)
|
||||
|
||||
end
|
||||
|
||||
function wait_until(box, time)
|
||||
while box:get_current_time() < time do
|
||||
box:sleep()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function wait_for(box, duration)
|
||||
wait_until(box, box:get_current_time() + duration)
|
||||
end
|
||||
|
||||
|
||||
|
||||
function process(box)
|
||||
-- loops on every received stimulation for a given input
|
||||
while box:keep_processing() do
|
||||
for stimulation = 1, box:get_stimulation_count(1) do
|
||||
|
||||
-- gets the received stimulation
|
||||
identifier, date, duration = box:get_stimulation(1, 1)
|
||||
-- discards it
|
||||
box:remove_stimulation(1, 1)
|
||||
|
||||
-- delay the OVTK_GDF_Left and Right
|
||||
if identifier == OVTK_GDF_Left or identifier == OVTK_GDF_Right then
|
||||
box:send_stimulation(1, OVTK_GDF_Correct, date+g_offset, 0)
|
||||
box:send_stimulation(1, OVTK_GDF_Incorrect, date+g_offset+g_duration, 0)
|
||||
end
|
||||
end
|
||||
box:sleep()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import torch\n",
|
||||
"\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"from matplotlib import pylab as plt\n",
|
||||
"\n",
|
||||
"from sklearn.model_selection import train_test_split\n",
|
||||
"from sklearn.metrics import balanced_accuracy_score\n",
|
||||
"\n",
|
||||
"import pytorch_lightning as pl\n",
|
||||
"from pytorch_lightning.callbacks import ModelCheckpoint\n",
|
||||
"\n",
|
||||
"from core.io import filter_events, get_ranges, split_epochs_binary\n",
|
||||
"from core.signal_processing import filter_signal, __create_constants, CSPFilter\n",
|
||||
"from core.nn.provider import NetProvider\n",
|
||||
"from core.nn.data import TableDataset\n",
|
||||
"\n",
|
||||
"%matplotlib inline\n",
|
||||
"plt.style.use('bmh')\n",
|
||||
"plt.rcParams.update({'font.size': 14})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Необходимо проверить точность классификации с помощью полносвязной сети:\n",
|
||||
"\n",
|
||||
"1. Подобрать архитектуру (поиск по сетке) - возможно полносвязные слои заменить на свёртку\n",
|
||||
"2. Выбрать электроды\n",
|
||||
"3. Исследовать влияние фильтрации\n",
|
||||
"4. Исследовать влияние использования CSP-фильтра\n",
|
||||
"\n",
|
||||
"> По числовым значениям - **обсудить с Андреем Николаевичем**\n",
|
||||
"\n",
|
||||
"> **Окружение TacticENV**\n",
|
||||
"\n",
|
||||
"> **Сохранять все результаты - ребятам понадабятся графики/таблицы в статье, необзодимые визуализации обсудить с Андреем Николаевичем**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Примеры использования библеотеки `core`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Данные"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Загрузка"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"openvibe_config = json.load(open(\"./config/openVibe.json\", \"r\"))\n",
|
||||
"raw_data = pd.read_csv(\"./Записи/OpenVibe signals/motor-imagery-csp-1-acquisition-[2024.04.18-10.37.21].csv\")\n",
|
||||
"data = filter_events(raw_data, openvibe_config)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Фильтрация"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"constants = __create_constants(low=8.0, high=30.0, sample_rate=250.0, order=5) \n",
|
||||
"filtered_data = filter_signal(df=data, cols=[x for x in data.columns if \"Channel\" in x], constants=constants)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Получение отрезков и разметки"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x, y = split_epochs_binary(\n",
|
||||
" df=filtered_data, \n",
|
||||
" epoch_duration=4 * 250, \n",
|
||||
" init_pause=int(0.5 * 250), \n",
|
||||
" final_pause=0, \n",
|
||||
" step=50000, # берем только 1 отрезок на действие (обсудить с А.Н.), подробнее см. доку \n",
|
||||
" map_labels={\"left\": 0, \"right\": 1},\n",
|
||||
" cols=[x for x in data.columns if \"Channel\" in x]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### CSP-фильтр"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(40, 1000)"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"csp_filter = CSPFilter(4)\n",
|
||||
"csp_filter.fit(x, y)\n",
|
||||
"csp_filter.transform(x).shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Нейронки\n",
|
||||
"\n",
|
||||
"> Используем `pytorch-lightning`, если будешь добавлять - придерживайся стиля 🤖\n",
|
||||
"\n",
|
||||
"Ниже пример полного пайплайна обучения (без фильтрации, без сколбзящего окна, без CSP-фильтра) для 3 случайных электродов - `Channel 1`, `Channel 2`, `Channel 3`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"(40, 3000) (40, 2)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"raw_data = pd.read_csv(\"./Записи/OpenVibe signals/motor-imagery-csp-1-acquisition-[2024.04.18-10.37.21].csv\")\n",
|
||||
"data = filter_events(raw_data, openvibe_config)\n",
|
||||
"x, y = split_epochs_binary(\n",
|
||||
" df=data, \n",
|
||||
" epoch_duration=4 * 250, \n",
|
||||
" init_pause=int(0.5 * 250), \n",
|
||||
" final_pause=0, \n",
|
||||
" step=50000, # берем только 1 отрезок на действие (обсудить с А.Н.), подробнее см. доку \n",
|
||||
" map_labels={\"left\": 0, \"right\": 1},\n",
|
||||
" cols=[\"Channel 1\", \"Channel 2\", \"Channel 3\"]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Делаем сигналы плоскими, лейблы в OneHot\n",
|
||||
"x = x.reshape((-1, 3 * 1000))\n",
|
||||
"y = np.eye(2)[y]\n",
|
||||
"print(x.shape, y.shape)\n",
|
||||
"\n",
|
||||
"# бъем train/test\n",
|
||||
"X_train, X_test, y_train, y_test = train_test_split(x, y, train_size=0.8)\n",
|
||||
"\n",
|
||||
"# конвертим в объекты торча\n",
|
||||
"train_loader = torch.utils.data.DataLoader(TableDataset(X_train, y_train), batch_size=32)\n",
|
||||
"val_loader = torch.utils.data.DataLoader(TableDataset(X_test, y_test), batch_size=8)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"net = torch.nn.Sequential(\n",
|
||||
" torch.nn.Linear(3000, 1024),\n",
|
||||
" torch.nn.Sigmoid(),\n",
|
||||
" torch.nn.Linear(1024, 2),\n",
|
||||
" torch.nn.Softmax(dim=1),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"wrapped_net = NetProvider(\n",
|
||||
" net=net, \n",
|
||||
" lr=1e-4, \n",
|
||||
" criteria=torch.nn.BCELoss(), \n",
|
||||
" metrics=[\n",
|
||||
" lambda pred, labels: balanced_accuracy_score(np.argmax(labels, axis=1), np.argmax(pred, axis=1))\n",
|
||||
" ],\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"GPU available: True (cuda), used: True\n",
|
||||
"TPU available: False, using: 0 TPU cores\n",
|
||||
"IPU available: False, using: 0 IPUs\n",
|
||||
"HPU available: False, using: 0 HPUs\n",
|
||||
"Missing logger folder: runs\\example\\lightning_logs\n",
|
||||
"LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
|
||||
"\n",
|
||||
" | Name | Type | Params\n",
|
||||
"----------------------------------------\n",
|
||||
"0 | criteria | BCELoss | 0 \n",
|
||||
"1 | net | Sequential | 3.1 M \n",
|
||||
"----------------------------------------\n",
|
||||
"3.1 M Trainable params\n",
|
||||
"0 Non-trainable params\n",
|
||||
"3.1 M Total params\n",
|
||||
"12.300 Total estimated model params size (MB)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "dc2a3deccc5b4690a005b7309e3f9322",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Sanity Checking: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"c:\\Users\\user\\anaconda3\\envs\\TacticENV\\lib\\site-packages\\pytorch_lightning\\trainer\\connectors\\data_connector.py:441: The 'val_dataloader' does not have many workers which may be a bottleneck. Consider increasing the value of the `num_workers` argument` to `num_workers=11` in the `DataLoader` to improve performance.\n",
|
||||
"c:\\Users\\user\\anaconda3\\envs\\TacticENV\\lib\\site-packages\\pytorch_lightning\\trainer\\connectors\\data_connector.py:441: The 'train_dataloader' does not have many workers which may be a bottleneck. Consider increasing the value of the `num_workers` argument` to `num_workers=11` in the `DataLoader` to improve performance.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "97215ced575c46d99b8e3cd20ab809e9",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Training: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "2d57332f35bf49ab8ff89e3f4215180b",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "64ab7eb3058c4e518478349348fcb950",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "7c621ba6368341d68765f570aaa2b8db",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "4fc7906c46b44fc8814452719f69a6cb",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "dcdd8be5bf65421db1af4cf9303da835",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "cb3f5bc1991b4addb12cd99b9213a57b",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "734438becdbc443e87942898e64199a4",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "0baf18f624a341efba714be21cf42c5f",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "d39784d032f84f5cbc06b1694beb1acc",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "b1998c91254e493893112c63a0e0b680",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Validation: | | 0/? [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"`Trainer.fit` stopped: `max_epochs=10` reached.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"torch.set_float32_matmul_precision('medium')\n",
|
||||
"trainer = pl.Trainer(\n",
|
||||
" default_root_dir=\"./runs/example\",\n",
|
||||
" accelerator=\"auto\",\n",
|
||||
" max_epochs=10,\n",
|
||||
" callbacks=[\n",
|
||||
" ModelCheckpoint(save_weights_only=True, monitor=\"val/loss\", mode=\"min\"),\n",
|
||||
" ],\n",
|
||||
" log_every_n_steps=1,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"trainer.fit(wrapped_net, train_loader, val_loader)\n",
|
||||
"__best_path = list(Path(\"./runs/example/lightning_logs/version_0/checkpoints/\").glob(\"*\"))[0]\n",
|
||||
"wrapped_net.load_state_dict(torch.load(str(__best_path))['state_dict'])\n",
|
||||
"del __best_path"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"map_actions": {
|
||||
"769": "OVTK_GDF_Left",
|
||||
"768": "OVTK_GDF_Start_Of_Trial",
|
||||
"33281": "OVTK_StimulationId_Train",
|
||||
"786": "OVTK_GDF_Cross_On_Screen",
|
||||
"32769": "OVTK_StimulationId_ExperimentStart",
|
||||
"32775": "OVTK_StimulationId_BaselineStart",
|
||||
"33553": "OVTK_StimulationId_AddedSamplesBegin",
|
||||
"32776": "OVTK_StimulationId_BaselineStop",
|
||||
"1010": "OVTK_GDF_End_Of_Session",
|
||||
"770": "OVTK_GDF_Right",
|
||||
"781": "OVTK_GDF_Feedback_Continuous",
|
||||
"33282": "OVTK_StimulationId_Beep",
|
||||
"800": "OVTK_GDF_End_Of_Trial",
|
||||
"33552": "OVTK_StimulationId_RemovedSamples",
|
||||
"32770": "OVTK_StimulationId_ExperimentStop",
|
||||
"33554": "OVTK_StimulationId_AddedSamplesEnd"
|
||||
},
|
||||
"main_actions": {
|
||||
"left": "769",
|
||||
"right": "770",
|
||||
"stop": "800"
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
+135
@@ -0,0 +1,135 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def filter_events(df: pd.DataFrame, config: dict[str, any]) -> pd.DataFrame:
|
||||
"""Получение табличной нотации для экспериментов из файлов OpenVibe (.csv)
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): сырой файл .ov сконвертированный в .csv и загруженный в pandas
|
||||
config (dict[str, any]): словарь с расшифровкой кодов событий OpenVibe
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: фрейм (таблица) в нашей собственной нотации
|
||||
"""
|
||||
|
||||
events = []
|
||||
__temp_event = {}
|
||||
|
||||
new_frame = df.copy()
|
||||
|
||||
__ov_events = df[~df["Event Id"].isna()][["Event Id"]]
|
||||
for idx in range(__ov_events.shape[0]):
|
||||
__row_events = __ov_events.iloc[idx, 0].split(":")
|
||||
|
||||
if config["main_actions"]["left"] in __row_events or config["main_actions"]["right"] in __row_events:
|
||||
__temp_event = {"start": __ov_events.index[idx]}
|
||||
if config["main_actions"]["left"] in __row_events:
|
||||
__temp_event["action"] = "left"
|
||||
if config["main_actions"]["right"] in __row_events:
|
||||
__temp_event["action"] = "right"
|
||||
|
||||
# В записи stop повторяется дважды (как и любые другие действия)
|
||||
if config["main_actions"]["stop"] in __row_events and len(__temp_event) > 0:
|
||||
__temp_event["stop"] = __ov_events.index[idx + 1]
|
||||
events.append(__temp_event.copy())
|
||||
__temp_event = {}
|
||||
|
||||
new_frame = new_frame.drop(columns=["Epoch", "Event Id", "Event Date", "Event Duration"])
|
||||
new_frame["action"] = None
|
||||
|
||||
for event in events:
|
||||
new_frame.loc[event["start"] : event["stop"], "action"] = event["action"]
|
||||
|
||||
new_frame["action"] = new_frame["action"].fillna("relax")
|
||||
return new_frame
|
||||
|
||||
|
||||
def get_ranges(labels: np.ndarray) -> list[list[int, int, int]]:
|
||||
"""Преобразование сырых лейблов временного ряда в отрезки для визуализации
|
||||
|
||||
Args:
|
||||
labels (np.ndarray): лейблы
|
||||
|
||||
Returns:
|
||||
list[list[int, int, int]]: список отрезков для визуализации, каждый отрезок определен как
|
||||
тройка "начало", "конец", "тип действия"
|
||||
"""
|
||||
|
||||
actions = []
|
||||
__temp_action = [0]
|
||||
__prev_action = labels[0]
|
||||
idx = 1
|
||||
|
||||
while True:
|
||||
if idx + 1 > len(labels):
|
||||
__temp_action.extend([idx, __prev_action])
|
||||
actions.append(__temp_action)
|
||||
break
|
||||
|
||||
if labels[idx] != __prev_action:
|
||||
__temp_action.extend([idx - 1, __prev_action])
|
||||
actions.append(__temp_action)
|
||||
__temp_action = [idx]
|
||||
__prev_action = labels[idx]
|
||||
|
||||
idx += 1
|
||||
|
||||
return actions
|
||||
|
||||
|
||||
def split_epochs_binary(
|
||||
df: pd.DataFrame,
|
||||
epoch_duration: int,
|
||||
init_pause: int,
|
||||
final_pause: int,
|
||||
step: int,
|
||||
map_labels: dict[str, int],
|
||||
cols: list[str],
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Формирование бинарного датасета (из отрезков, когда есть действие!)
|
||||
|
||||
Notes:
|
||||
Если нужно использовать только один отрезок на действие - установть step > [отсчеты в отрезке]
|
||||
|
||||
Например, если отрезок действия 4 с (1000 отсчётов для частоты дискретизации 250 Гц), то поставив step=1001
|
||||
будет сформирован только 1 обучающий (валидационный) объект из данного действия.
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): фрейм с данными
|
||||
epoch_duration (int): длительность эпохи (в отсчетах сигнала, t*f)
|
||||
init_pause (int): начальная пауза - сколько отсчетов выбросить с момента предъявления стимула
|
||||
(в отсчетах сигнала, t*f)
|
||||
final_pause (int): конечная пауза - сколько отсчетов выкинуть в конце (в отсчетах сигнала, t*f)
|
||||
step (int): шаг смещения внутри времени прдъявления стимула (в отсчетах сигнала, t*f)
|
||||
map_labels (dict[str, int]): словарь соотносящий действие и его числовое значение,
|
||||
например, {'left': 0, 'right': 1}
|
||||
cols (list[str]): имена колонок, которые будут добавлены в датасет,
|
||||
например: ['Channel 1', 'Channel 2', 'Channel 3']
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, np.ndarray]: кортеж с 2 массивами:
|
||||
|
||||
* обучающие данные, размерность [количество образцов X длительность эпохи X количество каналов]
|
||||
|
||||
* метки классов (в соответсвии с map_labels), размерность [количество образцов X 1]
|
||||
|
||||
Raise:
|
||||
ValueError: словарь map_labels содержит информацию не о 2х классах
|
||||
"""
|
||||
|
||||
if len(map_labels) != 2:
|
||||
raise ValueError(f"Словарь map_labels должен содержать 2 класса, получено: {len(map_labels)}!")
|
||||
|
||||
actions = get_ranges(df["action"])
|
||||
x = []
|
||||
y = []
|
||||
|
||||
for action in actions:
|
||||
if action[2] in map_labels.keys():
|
||||
__low_bound = action[0] + init_pause
|
||||
__upper_bound = action[1] - final_pause - epoch_duration + 1
|
||||
for inner_idx in range(__low_bound, __upper_bound, step):
|
||||
x.append(df.loc[inner_idx : inner_idx + epoch_duration - 1, cols].values)
|
||||
y.append(map_labels[action[2]])
|
||||
return np.array(x), np.array(y)
|
||||
Whitespace-only changes.
@@ -0,0 +1,23 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
class TableDataset(torch.utils.data.Dataset):
|
||||
|
||||
def __init__(self, x: np.ndarray, y: np.ndarray) -> None:
|
||||
"""Простой датасет из объектов numpy
|
||||
|
||||
Args:
|
||||
x (np.ndarray): обучающие объекты
|
||||
y (np.ndarray): метки
|
||||
"""
|
||||
super().__init__()
|
||||
self.map_labels = None
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __len__(self):
|
||||
return len(self.x)
|
||||
|
||||
def __getitem__(self, idx) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return torch.FloatTensor(self.x[idx]), torch.FloatTensor(self.y[idx])
|
||||
@@ -0,0 +1,53 @@
|
||||
import pytorch_lightning as pl
|
||||
import torch
|
||||
|
||||
|
||||
class NetProvider(pl.LightningModule):
|
||||
def __init__(self, net: torch.nn.Module, lr: float, criteria: torch.nn.Module, metrics: list[callable]) -> None:
|
||||
"""Базовая обертка для сетей
|
||||
|
||||
Args:
|
||||
net (torch.nn.Module): сеть - модуль pyTorch
|
||||
lr (float): темп обучения
|
||||
criteria (torch.nn.Module): функция ошибки
|
||||
metrics (list[callable]): список метрик - функций, принимающих (preds: np.ndarray, true_labels: np.ndarray)
|
||||
"""
|
||||
super().__init__()
|
||||
self.lr = lr
|
||||
self.criteria = criteria
|
||||
self.net = net
|
||||
self.metrics = metrics
|
||||
|
||||
def forward(self, x):
|
||||
return self.net(x)
|
||||
|
||||
def configure_optimizers(self):
|
||||
return torch.optim.AdamW(self.parameters(), lr=self.lr)
|
||||
|
||||
def training_step(self, batch, batch_idx):
|
||||
data, labels = batch
|
||||
preds = self.forward(data)
|
||||
loss = self.criteria(preds, labels)
|
||||
self.log("train/loss", loss, on_step=True, on_epoch=True)
|
||||
for metric in self.metrics:
|
||||
self.log(
|
||||
f"train/{metric.__name__}",
|
||||
metric(preds.cpu().detach().numpy(), labels.cpu().detach().numpy()),
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
)
|
||||
return loss
|
||||
|
||||
def validation_step(self, batch, batch_idx):
|
||||
data, labels = batch
|
||||
preds = self.forward(data)
|
||||
loss = self.criteria(preds, labels)
|
||||
self.log("val/loss", loss, on_step=True, on_epoch=True)
|
||||
for metric in self.metrics:
|
||||
self.log(
|
||||
f"val/{metric.__name__}",
|
||||
metric(preds.cpu().detach().numpy(), labels.cpu().detach().numpy()),
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
)
|
||||
return loss
|
||||
@@ -0,0 +1,134 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy
|
||||
from scipy.signal import butter, lfilter
|
||||
|
||||
|
||||
def __create_constants(low: float, high: float, sample_rate: float, order: int) -> tuple[float, float]:
|
||||
"""Вычисление констан полосового фильтра (Баттерворта)
|
||||
|
||||
Args:
|
||||
low (float): нижняя чатсота среза
|
||||
high (float): верхняя частота среза
|
||||
sample_rate (float): частота дискретизации
|
||||
order (int): порядок фильтра
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: константы фильтра, необходимые для вычислений
|
||||
"""
|
||||
return butter(order, [low, high], fs=sample_rate, btype="band")
|
||||
|
||||
|
||||
def filter_signal(df: pd.DataFrame, cols: list[str], constants: tuple[float, float]) -> pd.DataFrame:
|
||||
"""Фильтрация датасета (сигналов с датчиков)
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): датафрейм с данными из OpenVibe
|
||||
cols (list[str]): список колонок, данные в которых нужно отфильтровать
|
||||
constants (tuple[float, float]): константы для вычисления фильтра (см. __create_constants)
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: входной фрейм с данными, с примененным фильтром
|
||||
"""
|
||||
new_df = df.copy()
|
||||
for col in cols:
|
||||
new_df[col] = lfilter(*constants, df[col].values)
|
||||
|
||||
return new_df
|
||||
|
||||
|
||||
class CSPFilter:
|
||||
"""
|
||||
Реализация CSP фильтра в стиле sklearn.
|
||||
Основано на реализации github.co/Hiroaki-K4/total_perspective_vortex/main/srcs/csp.py
|
||||
"""
|
||||
|
||||
def __init__(self, n_components: int) -> None:
|
||||
self.n_components = n_components
|
||||
self.classes = None
|
||||
self.filters = None
|
||||
self.patterns = None
|
||||
self.mean = None
|
||||
self.std = None
|
||||
|
||||
def __get_covariate(self, x: np.ndarray, y: np.ndarray, cls: any) -> np.ndarray:
|
||||
"""Получение ковариаций
|
||||
|
||||
Args:
|
||||
x (np.ndarray): отсчеты сигнала
|
||||
y (np.ndarray): метки класса
|
||||
cls (any): текущий класс
|
||||
|
||||
Returns:
|
||||
np.ndarray: матрица ковариаций
|
||||
"""
|
||||
|
||||
x_class = x[y == cls]
|
||||
_, _, n_channels = x_class.shape
|
||||
# x_class = np.transpose(x_class, [1, 0, 2])
|
||||
x_class = x_class.reshape(n_channels, -1)
|
||||
cov = np.dot(x_class, x_class.T)
|
||||
return cov
|
||||
|
||||
def fit(self, x: np.ndarray, y: np.ndarray) -> None:
|
||||
"""Обучение фильтра
|
||||
|
||||
Args:
|
||||
x (np.ndarray): отсчеты сигнала (числа)
|
||||
y (np.ndarray): метки класса (любые данные)
|
||||
"""
|
||||
self.classes = np.unique(y)
|
||||
n_classes = len(self.classes)
|
||||
if n_classes != 2:
|
||||
raise ValueError(f"Количество классов для CSP должно быть = 2 (получено {n_classes})")
|
||||
|
||||
cov_neg = self.__get_covariate(x, y, self.classes[0])
|
||||
cov_pos = self.__get_covariate(x, y, self.classes[1])
|
||||
|
||||
eig_vals, eig_vecs = scipy.linalg.eigh(cov_neg, cov_pos)
|
||||
for i in range(len(eig_vecs)):
|
||||
eig_vecs[i] = eig_vecs[i] / np.linalg.norm(eig_vecs[i])
|
||||
|
||||
sorted_vals = np.argsort(eig_vals)
|
||||
idxs = np.empty_like(sorted_vals)
|
||||
idxs[0::2] = sorted_vals[len(sorted_vals) // 2 :][::-1]
|
||||
idxs[1::2] = sorted_vals[: len(sorted_vals) // 2]
|
||||
|
||||
eig_vecs = eig_vecs[:, idxs]
|
||||
self.filters = eig_vecs.T
|
||||
self.patterns = np.linalg.inv(eig_vecs)
|
||||
pick_filters = self.filters[:, self.n_components]
|
||||
|
||||
x = np.asarray([np.dot(pick_filters, epoch.T) for epoch in x])
|
||||
x = (x**2).mean(axis=1)
|
||||
self.mean = x.mean(axis=0)
|
||||
self.std = x.std(axis=0)
|
||||
|
||||
def transform(self, x: np.ndarray) -> np.ndarray:
|
||||
"""Применение фильтра
|
||||
|
||||
Args:
|
||||
x (np.ndarray): отсчеты сигнала (числа)
|
||||
|
||||
Returns:
|
||||
np.ndarray: вторичные параметры (фичи)
|
||||
"""
|
||||
pick_filters = self.filters[: self.n_components]
|
||||
x = np.asarray([np.dot(pick_filters, epoch.T) for epoch in x])
|
||||
x = (x**2).mean(axis=1)
|
||||
x -= self.mean
|
||||
x /= self.std
|
||||
return x
|
||||
|
||||
def fit_transform(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
||||
"""Обучение с последующим применением ко входным данным
|
||||
|
||||
Args:
|
||||
x (np.ndarray): отсчеты сигнала (числа)
|
||||
y (np.ndarray): метки класса (любые данные)
|
||||
|
||||
Returns:
|
||||
np.ndarray: вторичные параметры (фичи)
|
||||
"""
|
||||
self.fit(x, y)
|
||||
return self.transform(x)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def get_ranges(labels: np.ndarray) -> list[list[int, int, int]]:
|
||||
"""Преобразование сырых лейблов временного ряда в отрезки для визуализации
|
||||
|
||||
Args:
|
||||
labels (np.ndarray): лейблы
|
||||
|
||||
Returns:
|
||||
list[list[int, int, int]]: список отрезков для визуализации, каждый отрезок определен как
|
||||
тройка "начало", "конец", "тип действия"
|
||||
"""
|
||||
|
||||
actions = []
|
||||
__temp_action = [0]
|
||||
__prev_action = labels[0]
|
||||
idx = 1
|
||||
|
||||
while True:
|
||||
if idx + 1 > len(labels):
|
||||
__temp_action.extend([idx, __prev_action])
|
||||
actions.append(__temp_action)
|
||||
break
|
||||
|
||||
if labels[idx] != __prev_action:
|
||||
__temp_action.extend([idx - 1, __prev_action])
|
||||
actions.append(__temp_action)
|
||||
__temp_action = [idx]
|
||||
__prev_action = labels[idx]
|
||||
|
||||
idx += 1
|
||||
|
||||
return actions
|
||||
@@ -0,0 +1,911 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"from matplotlib import pylab as plt\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import seaborn as sns\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"%matplotlib inline\n",
|
||||
"plt.style.use('bmh')\n",
|
||||
"plt.rcParams.update({'font.size': 14})\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Загрузка 🐱👤"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Описание формата csv файла:\n",
|
||||
">http://openvibe.inria.fr/csv-file-format-description/\n",
|
||||
"\n",
|
||||
"Коды событий:\n",
|
||||
">http://openvibe.inria.fr/stimulation-codes/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"openvibe_config = json.load(open(\"./config/openVibe.json\", \"r\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"raw_data = pd.read_csv(\"./Записи/OpenVibe signals/motor-imagery-csp-1-acquisition-[2024.04.18-10.37.21].csv\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>Time:250Hz</th>\n",
|
||||
" <th>Epoch</th>\n",
|
||||
" <th>Channel 1</th>\n",
|
||||
" <th>Channel 2</th>\n",
|
||||
" <th>Channel 3</th>\n",
|
||||
" <th>Channel 4</th>\n",
|
||||
" <th>Channel 5</th>\n",
|
||||
" <th>Channel 6</th>\n",
|
||||
" <th>Channel 7</th>\n",
|
||||
" <th>Channel 8</th>\n",
|
||||
" <th>...</th>\n",
|
||||
" <th>Channel 10</th>\n",
|
||||
" <th>Channel 11</th>\n",
|
||||
" <th>Channel 12</th>\n",
|
||||
" <th>Channel 13</th>\n",
|
||||
" <th>Channel 14</th>\n",
|
||||
" <th>Channel 15</th>\n",
|
||||
" <th>Channel 16</th>\n",
|
||||
" <th>Event Id</th>\n",
|
||||
" <th>Event Date</th>\n",
|
||||
" <th>Event Duration</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0.000</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171386.796875</td>\n",
|
||||
" <td>165426.734375</td>\n",
|
||||
" <td>154487.453125</td>\n",
|
||||
" <td>162283.515625</td>\n",
|
||||
" <td>181144.156250</td>\n",
|
||||
" <td>166410.906250</td>\n",
|
||||
" <td>158936.093750</td>\n",
|
||||
" <td>149495.546875</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179601.828125</td>\n",
|
||||
" <td>142757.171875</td>\n",
|
||||
" <td>123782.796875</td>\n",
|
||||
" <td>155169.875000</td>\n",
|
||||
" <td>146074.296875</td>\n",
|
||||
" <td>151097.203125</td>\n",
|
||||
" <td>132544.546875</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>0.004</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171392.593750</td>\n",
|
||||
" <td>165444.875000</td>\n",
|
||||
" <td>154490.781250</td>\n",
|
||||
" <td>162296.968750</td>\n",
|
||||
" <td>181143.578125</td>\n",
|
||||
" <td>166415.734375</td>\n",
|
||||
" <td>158936.906250</td>\n",
|
||||
" <td>149496.156250</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179604.187500</td>\n",
|
||||
" <td>142784.218750</td>\n",
|
||||
" <td>123787.976562</td>\n",
|
||||
" <td>155203.531250</td>\n",
|
||||
" <td>146079.062500</td>\n",
|
||||
" <td>151096.406250</td>\n",
|
||||
" <td>132547.890625</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>0.008</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171387.046875</td>\n",
|
||||
" <td>165428.828125</td>\n",
|
||||
" <td>154489.281250</td>\n",
|
||||
" <td>162286.640625</td>\n",
|
||||
" <td>181143.687500</td>\n",
|
||||
" <td>166412.078125</td>\n",
|
||||
" <td>158937.578125</td>\n",
|
||||
" <td>149494.859375</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179602.234375</td>\n",
|
||||
" <td>142767.937500</td>\n",
|
||||
" <td>123788.117188</td>\n",
|
||||
" <td>155178.015625</td>\n",
|
||||
" <td>146076.218750</td>\n",
|
||||
" <td>151096.375000</td>\n",
|
||||
" <td>132545.265625</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>0.012</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171377.921875</td>\n",
|
||||
" <td>165400.750000</td>\n",
|
||||
" <td>154485.890625</td>\n",
|
||||
" <td>162266.593750</td>\n",
|
||||
" <td>181143.359375</td>\n",
|
||||
" <td>166404.328125</td>\n",
|
||||
" <td>158937.078125</td>\n",
|
||||
" <td>149492.484375</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179598.078125</td>\n",
|
||||
" <td>142729.265625</td>\n",
|
||||
" <td>123783.914062</td>\n",
|
||||
" <td>155126.984375</td>\n",
|
||||
" <td>146069.140625</td>\n",
|
||||
" <td>151094.765625</td>\n",
|
||||
" <td>132538.375000</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>0.016</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171376.703125</td>\n",
|
||||
" <td>165396.484375</td>\n",
|
||||
" <td>154484.875000</td>\n",
|
||||
" <td>162261.906250</td>\n",
|
||||
" <td>181142.125000</td>\n",
|
||||
" <td>166401.875000</td>\n",
|
||||
" <td>158935.968750</td>\n",
|
||||
" <td>149490.734375</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179596.703125</td>\n",
|
||||
" <td>142719.171875</td>\n",
|
||||
" <td>123783.015625</td>\n",
|
||||
" <td>155120.093750</td>\n",
|
||||
" <td>146067.218750</td>\n",
|
||||
" <td>151092.062500</td>\n",
|
||||
" <td>132535.734375</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>0.020</td>\n",
|
||||
" <td>0</td>\n",
|
||||
" <td>171385.546875</td>\n",
|
||||
" <td>165420.531250</td>\n",
|
||||
" <td>154489.937500</td>\n",
|
||||
" <td>162277.234375</td>\n",
|
||||
" <td>181143.109375</td>\n",
|
||||
" <td>166408.687500</td>\n",
|
||||
" <td>158938.078125</td>\n",
|
||||
" <td>149492.656250</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>179601.718750</td>\n",
|
||||
" <td>142751.265625</td>\n",
|
||||
" <td>123791.242188</td>\n",
|
||||
" <td>155167.218750</td>\n",
|
||||
" <td>146075.421875</td>\n",
|
||||
" <td>151093.078125</td>\n",
|
||||
" <td>132542.531250</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"<p>6 rows × 21 columns</p>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" Time:250Hz Epoch Channel 1 Channel 2 Channel 3 \\\n",
|
||||
"0 0.000 0 171386.796875 165426.734375 154487.453125 \n",
|
||||
"1 0.004 0 171392.593750 165444.875000 154490.781250 \n",
|
||||
"2 0.008 0 171387.046875 165428.828125 154489.281250 \n",
|
||||
"3 0.012 0 171377.921875 165400.750000 154485.890625 \n",
|
||||
"4 0.016 0 171376.703125 165396.484375 154484.875000 \n",
|
||||
"5 0.020 0 171385.546875 165420.531250 154489.937500 \n",
|
||||
"\n",
|
||||
" Channel 4 Channel 5 Channel 6 Channel 7 Channel 8 \\\n",
|
||||
"0 162283.515625 181144.156250 166410.906250 158936.093750 149495.546875 \n",
|
||||
"1 162296.968750 181143.578125 166415.734375 158936.906250 149496.156250 \n",
|
||||
"2 162286.640625 181143.687500 166412.078125 158937.578125 149494.859375 \n",
|
||||
"3 162266.593750 181143.359375 166404.328125 158937.078125 149492.484375 \n",
|
||||
"4 162261.906250 181142.125000 166401.875000 158935.968750 149490.734375 \n",
|
||||
"5 162277.234375 181143.109375 166408.687500 158938.078125 149492.656250 \n",
|
||||
"\n",
|
||||
" ... Channel 10 Channel 11 Channel 12 Channel 13 \\\n",
|
||||
"0 ... 179601.828125 142757.171875 123782.796875 155169.875000 \n",
|
||||
"1 ... 179604.187500 142784.218750 123787.976562 155203.531250 \n",
|
||||
"2 ... 179602.234375 142767.937500 123788.117188 155178.015625 \n",
|
||||
"3 ... 179598.078125 142729.265625 123783.914062 155126.984375 \n",
|
||||
"4 ... 179596.703125 142719.171875 123783.015625 155120.093750 \n",
|
||||
"5 ... 179601.718750 142751.265625 123791.242188 155167.218750 \n",
|
||||
"\n",
|
||||
" Channel 14 Channel 15 Channel 16 Event Id Event Date \\\n",
|
||||
"0 146074.296875 151097.203125 132544.546875 NaN NaN \n",
|
||||
"1 146079.062500 151096.406250 132547.890625 NaN NaN \n",
|
||||
"2 146076.218750 151096.375000 132545.265625 NaN NaN \n",
|
||||
"3 146069.140625 151094.765625 132538.375000 NaN NaN \n",
|
||||
"4 146067.218750 151092.062500 132535.734375 NaN NaN \n",
|
||||
"5 146075.421875 151093.078125 132542.531250 NaN NaN \n",
|
||||
"\n",
|
||||
" Event Duration \n",
|
||||
"0 NaN \n",
|
||||
"1 NaN \n",
|
||||
"2 NaN \n",
|
||||
"3 NaN \n",
|
||||
"4 NaN \n",
|
||||
"5 NaN \n",
|
||||
"\n",
|
||||
"[6 rows x 21 columns]"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"raw_data.head(6)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Фильтруем события, для нас интересны стрелки (лево/право)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"----------------------"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def filter_events(df: pd.DataFrame, config: dict[str, any]) -> pd.DataFrame:\n",
|
||||
" \"\"\"Получение табличной нотации для экспериментов из файлов OpenVibe (.csv)\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" df (pd.DataFrame): сырой файл .ov сконвертированный в .csv и загруженный в pandas\n",
|
||||
" config (dict[str, any]): словарь с расшифровкой кодов событий OpenVibe\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" pd.DataFrame: фрейм (таблица) в нашей собственной нотации\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" events = []\n",
|
||||
" __temp_event = {}\n",
|
||||
"\n",
|
||||
" new_frame = df.copy()\n",
|
||||
"\n",
|
||||
" __ov_events = df[~df['Event Id'].isna()][['Event Id']]\n",
|
||||
" for idx in range(__ov_events.shape[0]):\n",
|
||||
" __row_events = __ov_events.iloc[idx, 0].split(\":\")\n",
|
||||
" \n",
|
||||
" if config['main_actions']['left'] in __row_events or config['main_actions']['right'] in __row_events:\n",
|
||||
" __temp_event = {\"start\": __ov_events.index[idx]}\n",
|
||||
" if config['main_actions']['left'] in __row_events:\n",
|
||||
" __temp_event['action'] = 'left'\n",
|
||||
" if config['main_actions']['right'] in __row_events:\n",
|
||||
" __temp_event['action'] = 'right'\n",
|
||||
"\n",
|
||||
" # В записи stop повторяется дважды (как и любые другие действия)\n",
|
||||
" if config['main_actions']['stop'] in __row_events and len(__temp_event) > 0:\n",
|
||||
" __temp_event['stop'] = __ov_events.index[idx + 1]\n",
|
||||
" events.append(__temp_event.copy())\n",
|
||||
" __temp_event = {}\n",
|
||||
"\n",
|
||||
" new_frame = new_frame.drop(columns=['Epoch', 'Event Id', 'Event Date', 'Event Duration'])\n",
|
||||
" new_frame['action'] = None\n",
|
||||
"\n",
|
||||
" for event in events:\n",
|
||||
" new_frame.loc[event['start']: event['stop'], 'action'] = event['action']\n",
|
||||
"\n",
|
||||
" new_frame['action'] = new_frame['action'].fillna('relax')\n",
|
||||
" return new_frame"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def get_ranges(labels: np.ndarray) -> list[list[int, int, int]]:\n",
|
||||
" \"\"\"Преобразование сырых лейблов временного ряда в отрезки для визуализации\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" labels (np.ndarray): лейблы\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" list[list[int, int, int]]: список отрезков для визуализации, каждый отрезок определен как\n",
|
||||
" тройка \"начало\", \"конец\", \"тип действия\"\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" actions = []\n",
|
||||
" __temp_action = [0]\n",
|
||||
" __prev_action = labels[0]\n",
|
||||
" idx = 1\n",
|
||||
"\n",
|
||||
" while True:\n",
|
||||
" if idx + 1 > len(labels):\n",
|
||||
" __temp_action.extend([idx, __prev_action])\n",
|
||||
" actions.append(__temp_action)\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" if labels[idx] != __prev_action:\n",
|
||||
" __temp_action.extend([idx - 1, __prev_action])\n",
|
||||
" actions.append(__temp_action)\n",
|
||||
" __temp_action = [idx]\n",
|
||||
" __prev_action = labels[idx]\n",
|
||||
"\n",
|
||||
" idx += 1\n",
|
||||
"\n",
|
||||
" return actions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"data = filter_events(raw_data, openvibe_config)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1500x500 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plot_data = data.copy()\n",
|
||||
"actions = get_ranges(plot_data['action'])\n",
|
||||
"action_colors = {\"relax\": \"#C0E986\", \"left\": \"#CA86E9\", \"right\": \"#E98B86\"}\n",
|
||||
"action_names = {\"relax\": \"отдых\", \"left\": \"левая рука\", \"right\": \"правая рука\"}\n",
|
||||
"index = 2\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots(figsize=(15, 5))\n",
|
||||
"__main = ax.plot(plot_data.iloc[:, index], color=\"black\", linewidth=.4, label=\"Данные\")\n",
|
||||
"ax.set_xticks(ax.get_xticks())\n",
|
||||
"ax.set_xticklabels([x * 0.004 for x in ax.get_xticks()])\n",
|
||||
"ax.set_xlim(0, 107_776)\n",
|
||||
"\n",
|
||||
"__patches = []\n",
|
||||
"for act in actions:\n",
|
||||
" __patches.append(plt.axvspan(act[0], act[1], color=action_colors[act[2]], label=action_names[act[2]]))\n",
|
||||
"\n",
|
||||
"ax.set_title(f\"Визуализация активности в сессии 1\\nс датчика \\\"{data.columns[index]}\\\"\")\n",
|
||||
"ax.set_xlabel(\"Время, с\")\n",
|
||||
"ax.set_ylabel(\"Значение сигнала\\n(формат OpenVibe)\")\n",
|
||||
"ax.legend(loc=\"best\", handles=[__main[0]] + __patches[7:10])\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"del __main, __patches"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABN4AAAHnCAYAAABnvJDkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAACa1UlEQVR4nOzdd3hUZfr/8c+kTXojhZCeUJKAIhZUEEJRUbF30RVQRNwVO6KCAq6su7afIKIICn5ZWdcVUawgSBFEUUFWhISWhJIQCJDeM+f3R66cJaRPMoTA+3Vdc0HmtPs8c84zM/c8xWIYhiEAAAAAAAAAbcqpvQMAAAAAAAAATkck3gAAAAAAAAAHIPEGAAAAAAAAOACJNwAAAAAAAMABSLwBAAAAAAAADkDiDQAAAAAAAHAAEm8AAAAAAACAA5B4AwAAAAAAAByAxBsAAAAAAADgACTeAAAAAADN9vnnn+uvf/2rqqqq6ix7/fXXtXDhwnaICgBOTSTeAHRIq1evlsViqffh4eGhyMhIXX311Zo3b55KS0vbO1wAAIDTxs8//6znnntO48ePV3p6uioqKnTw4EH99a9/1aOPPqqVK1e2d4gAcMqwGIZhtHcQANBSq1ev1uDBg5u1bo8ePfTZZ5+pR48eDo4KAADg9Ldjxw6dffbZKisrq7PMYrHou+++06BBg05+YABwCqLFG4AO74EHHtDvv/9uPn766SfNmTNHiYmJkqTU1FRdeeWVKikpaedIAQAAOr7u3btrxYoVuvDCC2s9n5SUpE8//ZSkGwAcx6W9AwCA1goJCVGvXr1qPde3b1/96U9/0qBBg7Rx40alpaXp3Xff1YMPPthOUQIAAJw+LrnkEv34448qKSlRZmamQkJC5OPj095hAcAphxZvAE5bHh4emj59uvn3119/3Y7RAAAAnH48PDwUHx9P0g0AGkDiDcBp7aKLLjL/n5GRUWf5jz/+qMmTJ2vQoEHq3Lmz3Nzc5Ovrq6SkJD3wwAPatm1bs4+1YsUKjRw5Ut26dZOPj498fHyUmJioG2+8Uf/85z+Vn59fa/3GJoho7FGfmmVTp06VJH333Xe64YYbFB4eLnd3d0VHR+u+++7T7t27m3UuWVlZmjx5ss4//3wFBgbKarUqPDxcN954o5YuXdrsMomJiWnWOS1YsKDJfa1atUojR45UfHy8PD095ePjo6SkJD300EPas2dPs+IZNWpUs+JpqovMrl279Nhjj+nss8+Wv7+/WcYjRozQ2rVrG9wuPT292ee8YMECc9309PQ6y2vKdtSoUQ3uY/ny5bXOa/Xq1Y0e89NPP9Wtt96qqKgoubu7y8/PT3369NEzzzyj7OzsRrdtSkNl7+zsrMDAQPXr10//+Mc/VFhYWO/2x98vq1evlmEYevfddzVgwAAFBQXJ09NTPXv21HPPPaeCgoJmxbRp0yaNGzdOCQkJ8vHxkaenp7p27aoxY8Zoy5YtDW534r173333NXqcrKwsubq6Nvv6as39d2Jd0JxzqO+6GDRoUJOxbtu2rdZ5Nec+bkpz79HGrvuW7quxc8zOztbkyZPVt29fBQQEyNnZ2e54GosxJiamwXWmTJliHufPf/5zneU2m03fffednnjiCfXv319BQUFydXWVv7+/zjnnHD3xxBPau3dvg/tPS0tT586dZbFYdPbZZysvL6/e9Zqqc7Zs2SJfX19ZLBYlJyfXO/ZXjePrt8YejZVLjR9//FGjRo1SXFycPD095evrq169eumxxx5r9LyPr4+belx//fX17qOiokLvvPOOLr30UoWGhsrNzU0hISEaOnSo3n77bVVUVDQZf3NNnTq1Td6/JMfXMe+8806tmOp7D6tRUVGh+fPn67rrrlNkZKTc3d0VFBSkc845R2PGjNGyZctUWVkpqWWv2YmP4+NtbD81ScS77rpL69atazDutq4jt2zZovvvv189e/aUj4+PnJyc7P68BOAUZABAB7Rq1SpDkiHJmDJlSoPrlZaWmuslJCTUWjZ//nxzWUMPZ2dn480332w0lmPHjhlXXXVVk/s6Mc7jz6Elj/ocf4znn3/esFgs9W7r4eFhfPbZZ42ez4IFCwxPT89GY7juuuuMwsLCRvdjGIYRHR3drHOaP39+g/soLi42brnllka3d3NzM959990m4xk5cmSz4klOTm5wHy+++KLh6ura6Pbjxo0zKisr62yblpbWrHM2jNrXZ1paWp3lNWU7cuTIerevqKgwkpKSasW1atWqetc9fPiwkZyc3Og5+fr6Gl999VWjMTemuWUfHx9v7N+/v872x98vy5YtM6688soG9xEdHW3s2rWrwVgqKyuN8ePHN3ifSDIsFovx/PPP17v9ifeul5eXkZub2+Dxnn/++WZfX629/xqq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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1500x500 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"fig, ax = plt.subplots(1, figsize=(15, 5))\n",
|
||||
"__temp = pd.DataFrame({'Длительность, с': [(x[1] - x[0]) / 250 for x in actions[1:-1]], 'Действие': [x[2] for x in actions[1:-1]]})\n",
|
||||
"sns.boxplot(data=__temp, x='Действие', y='Длительность, с', ax=ax)\n",
|
||||
"ax.set_title('Распределение времени для каждого действия')\n",
|
||||
"del __temp\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"> Что делаем с выбросами? Это так и задуманно? (вопрос 1 из сообщения)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Формирование бинарного датасета 👥"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Параметры датасета\n",
|
||||
"\n",
|
||||
"* Полосовой фильтр ✔\n",
|
||||
" * частота $8-30 Гц$\n",
|
||||
" * порядок $5$\n",
|
||||
"* Эпохи ✔\n",
|
||||
" * $0,5 с$ после предъявления стимула\n",
|
||||
" * $4 c$ - длительность эпохи\n",
|
||||
" * Насколько смещаемся?\n",
|
||||
"* CSP-фильтр по эпохам ✔\n",
|
||||
" * количество признаков $1024$"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Полосовой фильтр"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from scipy.signal import butter, lfilter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def __create_constants(low: float, high: float, sample_rate: float, order: int) -> tuple[float, float]:\n",
|
||||
" \"\"\"Вычисление констан полосового фильтра (Баттерворта)\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" low (float): нижняя чатсота среза\n",
|
||||
" high (float): верхняя частота среза\n",
|
||||
" sample_rate (float): частота дискретизации\n",
|
||||
" order (int): порядок фильтра\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" tuple[float, float]: константы фильтра, необходимые для вычислений\n",
|
||||
" \"\"\"\n",
|
||||
" return butter(order, [low, high], fs=sample_rate, btype='band')\n",
|
||||
"\n",
|
||||
"def filter_signal(df: pd.DataFrame, cols: list[str], constants: tuple[float, float]) -> pd.DataFrame:\n",
|
||||
" \"\"\"Фильтрация датасета (сигналов с датчиков)\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" df (pd.DataFrame): датафрейм с данными из OpenVibe\n",
|
||||
" cols (list[str]): список колонок, данные в которых нужно отфильтровать\n",
|
||||
" constants (tuple[float, float]): константы для вычисления фильтра (см. __create_constants)\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" pd.DataFrame: входной фрейм с данными, с примененным фильтром\n",
|
||||
" \"\"\"\n",
|
||||
" new_df = df.copy()\n",
|
||||
" for col in cols:\n",
|
||||
" new_df[col] = lfilter(*constants, df[col].values)\n",
|
||||
"\n",
|
||||
" return new_df\n",
|
||||
" "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAABQMAAAIICAYAAADaJ/gXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3QU1dsH8O/2zab3kJAECF1A/EkVpCnSRBAVpAhEQaXYkKKANGkK0lQERYqAr9IUEOliCb1XgQDpjfS2Lbs77x/rDrvJ9uxmUp7POTmE7MydZ+7O3Jm5cwuPYRgGhBBCCCGEEEIIIYSQWo/PdQCEEEIIIYQQQgghhJCqQZWBhBBCCCGEEEIIIYTUEVQZSAghhBBCCCGEEEJIHUGVgYQQQgghhBBCCCGE1BFUGUgIIYQQQgghhBBCSB1BlYGEEEIIIYQQQgghhNQRVBlICCGEEEIIIYQQQkgdQZWBhBBCCCGEEEIIIYTUEVQZSAghhBBCCCGEEEJIHUGVgYQQQgghhBBCCCGE1BFUGUgIIaTa+PPPP8Hj8Sz+eHp6IiYmBsOGDcP+/fu5DpcQQggh1YhKpcKZM2fw5Zdf4rXXXkOzZs3A5/PZ+whCCCF6PIZhGK6DIIQQQgB9ZWDPnj3tXr5fv37YtWsXZDKZG6MihBBCSE0QGxuLzZs3W/ycHn0JIUSPWgYSQgipliZMmIDr16+zP9euXcM///yDlStXokGDBgCAgwcP4u233+Y2UEIIIYRUC8aVfd7e3ujevTvCwsI4jIgQQqonIdcBEEIIIeaEhISgVatWFf7etWtXDBs2DG3atEFOTg62bduGpUuXIjw8nIMoCSGEEFJd9OvXDz169ED79u3RokUL8Pl89OjRA5mZmVyHRggh1Qq1DCSEEFLj1KtXD2PGjAGgbwVw4cIFjiMihBBCCNeGDRuGsWPH4rHHHgOfT4+6hBBiCZWQhBBCaqSGDRuyv6tUqgqfG09G8ueff1pMZ8SIEexyhu7HxlJTU7FgwQIMHjwYzZs3R2BgIEQiEfz9/dGxY0fMmzcPOTk5FdYbMmQIeDwe/P39oVQqbe7PY489Bh6PhzZt2lT47MyZM5g9ezZ69OiBsLAwiMVi+Pj4oGXLlpgwYQJu3bplM32DBg0aWJ2kxfAzb968CuuOHTvWYj4ZZGdnw8/Pz2o6R44cwXvvvYdu3bqhQYMG8PLygkQiQf369TFw4ED89NNP0Ol0FrdR2fywZz8MDPk1duxYp9OyJ08MHjx4gA8++ABt27aFr68vBAKB3d+PPXQ6Hf744w9MnToVXbp0QVBQEEQiEfz8/NC2bVtMnToVycnJVtPo0aMHeDweevToYfZzhmEQGxvLxvr555+zn82bN8+u48/cT2Jiotl0LFGpVGjUqBG7nLnvcPPmzWa3xefz4e3tjdatW+P999832XZ59n4n9pZJeXl5mDdvHtq1awd/f39IpVJERkbilVdewe+//251GwYMw2D37t0YNmwYGjRoAJlMBj8/P7Rq1QojRozAnj17TMolZ7+T8nlqT158++23Fr9XR1iKSSKRICoqCkOGDMFvv/3mVNrlOZqf5d27dw9TpkxBmzZt4OfnB6lUiujoaIwYMQJ///23XTHk5eVh8eLF6NatG0JDQyEWixEREYFOnTrh448/xqVLl1wav/G5Ye076tKlC7ucuTLBVddiQgghLsYQQggh1cSJEycYAAwAZu7cuVaXnTp1KrvsmTNnrKZ14sQJs2mcPHmSXQYAEx0dXWGZ/fv3myxj7icgIIA5deqUyXq///47+/n27dut7svp06fZZVetWmXy2aZNm2xuXyAQMF9//bXVbRhER0fbTM9S/o8ZM8ZiPhmMHz/eZjovvfSSze0/88wzjFwur7CuK/LDnv0on19jxoxxOi178oRhGObgwYOMVCp1+vuxx9y5c22mLZPJmD179lhMo3v37gwApnv37mY/f/fdd9m0Zs2a5fD2Lf0kJCSYTceSRYsWmaxv7ju053gy5Mnx48fNbsfe78SeMumPP/5g/P39rcby0ksvMQqFwuJ2UlJSmA4dOtjcp02bNlXYB0d/yueprbwoKChggoODLX6vjrA3xrfeesup9A2cyU9jS5YsYUQikdV13377bUaj0ViMYefOnYyPj4/NGFwZv/G5Yek72r59u0ka5soEV12LK8NQZlkrLwghpK6hMQMJIYTUOFlZWfjhhx8AAJGRkfjf//7ncBoMw+C9996zuZyvry+GDBmCDh06oGXLlggMDIRYLEZaWhp2796Nbdu2IS8vD6+++ioePHgAgUAAAOjTpw+ioqKQnJyMjRs3YsSIERa3sXHjRgCAWCzGqFGjTD7TaDTw9/fHCy+8gO7du6NJkybw9PREeno6Ll26hDVr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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1500x500 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"index = 1\n",
|
||||
"\n",
|
||||
"constants = __create_constants(8.0, 30.0, 250.0, 5) \n",
|
||||
"plot_data = filter_signal(plot_data, [data.columns[index]], constants)\n",
|
||||
"actions = get_ranges(plot_data['action'])\n",
|
||||
"action_colors = {\"relax\": \"#C0E986\", \"left\": \"#CA86E9\", \"right\": \"#E98B86\"}\n",
|
||||
"action_names = {\"relax\": \"отдых\", \"left\": \"левая рука\", \"right\": \"правая рука\"}\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots(figsize=(15, 5))\n",
|
||||
"__filtered = ax.plot(plot_data.iloc[1000:, index], color=\"black\", linewidth=.4, label=\"Фильтрованные данные\")\n",
|
||||
"ax.set_xticks(ax.get_xticks())\n",
|
||||
"ax.set_xticklabels([x * 0.004 for x in ax.get_xticks()])\n",
|
||||
"ax.set_xlim(0, 107_776)\n",
|
||||
"\n",
|
||||
"__patches = []\n",
|
||||
"for act in actions:\n",
|
||||
" __patches.append(plt.axvspan(act[0], act[1], color=action_colors[act[2]], label=action_names[act[2]]))\n",
|
||||
"\n",
|
||||
"ax.set_title(f\"Визуализация активности в сессии 1\\nс датчика \\\"{data.columns[index]}\\\" (пропущена 1000 отсчетов, артефакты фильтрации)\")\n",
|
||||
"ax.set_xlabel(\"Время, с\")\n",
|
||||
"ax.set_ylabel(\"Значение сигнала\\n(формат OpenVibe)\")\n",
|
||||
"ax.legend(loc=\"best\", handles=[__filtered[0]] + __patches[7:10])\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"del __patches, __filtered"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"constants = __create_constants(low=8.0, high=30.0, sample_rate=250.0, order=5) \n",
|
||||
"filtered_data = filter_signal(df=data, cols=[x for x in data.columns if \"Channel\" in x], constants=constants)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Скользящее окно\n",
|
||||
"\n",
|
||||
"> Пояснение к вопросу 3 из сообщения\n",
|
||||
"\n",
|
||||
"Разбиение:\n",
|
||||
"\n",
|
||||
"|----------| Стимул ($5 с$)\n",
|
||||
"\n",
|
||||
"|-xxxxxxxx-| Эпоха 1 ($0,5 с$ паузы + $4 с$ данных)\n",
|
||||
"\n",
|
||||
"|--xxxxxxxx| Эпоха 2 ($0,5 с$ паузы + $0,5 с$ смещение окна + $4 с$ данных)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def split_epochs_binary(\n",
|
||||
" df: pd.DataFrame, \n",
|
||||
" epoch_duration: int, \n",
|
||||
" init_pause: int, \n",
|
||||
" final_pause: int,\n",
|
||||
" step: int,\n",
|
||||
" map_labels: dict[str, int],\n",
|
||||
" cols: list[str]\n",
|
||||
") -> tuple[np.ndarray, np.ndarray]:\n",
|
||||
" \"\"\"Формирование бинарного датасета (из отрезков, когда есть действие!)\n",
|
||||
"\n",
|
||||
" Notes:\n",
|
||||
" Если нужно использовать только один отрезок на действие - установть step > [отсчеты в отрезке]\n",
|
||||
"\n",
|
||||
" Например, если отрезок действия 4 с (1000 отсчётов для частоты дискретизации 250 Гц), то поставив step=1001\n",
|
||||
" будет сформирован только 1 обучающий (валидационный) объект из данного действия.\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" df (pd.DataFrame): фрейм с данными \n",
|
||||
" epoch_duration (int): длительность эпохи (в отсчетах сигнала, t*f)\n",
|
||||
" init_pause (int): начальная пауза - сколько отсчетов выбросить с момента предъявления стимула (в отсчетах сигнала, t*f)\n",
|
||||
" final_pause (int): конечная пауза - сколько отсчетов выкинуть в конце (в отсчетах сигнала, t*f)\n",
|
||||
" step (int): шаг смещения внутри времени прдъявления стимула (в отсчетах сигнала, t*f)\n",
|
||||
" map_labels (dict[str, int]): словарь соотносящий действие и его числовое значение, например, {'left': 0, 'right': 1}\n",
|
||||
" cols (list[str]): имена колонок, которые будут добавлены в датасет, например: ['Channel 1', 'Channel 2', 'Channel 3']\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" tuple[np.ndarray, np.ndarray]: кортеж с 2 массивами: \n",
|
||||
" \n",
|
||||
" * обучающие данные, размерность [количество образцов X длительность эпохи X количество каналов]\n",
|
||||
"\n",
|
||||
" * метки классов (в соответсвии с map_labels), размерность [количество образцов X 1]\n",
|
||||
"\n",
|
||||
" Raise:\n",
|
||||
" ValueError: словарь map_labels содержит информацию не о 2х классах\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" if len(map_labels) != 2:\n",
|
||||
" raise ValueError(f\"Словарь map_labels должен содержать 2 класса, получено: {len(map_labels)}!\")\n",
|
||||
" \n",
|
||||
" actions = get_ranges(df['action'])\n",
|
||||
" x = []\n",
|
||||
" y = []\n",
|
||||
" \n",
|
||||
" for action in actions:\n",
|
||||
" if action[2] in map_labels.keys():\n",
|
||||
" __low_bound = action[0] + init_pause\n",
|
||||
" __upper_bound = action[1] - final_pause - epoch_duration + 1\n",
|
||||
" for inner_idx in range(__low_bound, __upper_bound, step):\n",
|
||||
" x.append(df.loc[inner_idx: inner_idx + epoch_duration - 1, cols].values)\n",
|
||||
" y.append(map_labels[action[2]])\n",
|
||||
" return np.array(x), np.array(y)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"x, y = split_epochs_binary(\n",
|
||||
" df=filtered_data, \n",
|
||||
" epoch_duration=4 * 250, \n",
|
||||
" init_pause=int(0.5 * 250), \n",
|
||||
" final_pause=0, \n",
|
||||
" step=int(5 * 250), \n",
|
||||
" map_labels={\"left\": 0, \"right\": 1},\n",
|
||||
" cols=[x for x in data.columns if \"Channel\" in x]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"((40, 1000, 16), (40,))"
|
||||
]
|
||||
},
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"x.shape, y.shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### CSP фильтр"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import scipy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class CSPFilter:\n",
|
||||
" \"\"\"\n",
|
||||
" Реализация CSP фильтра в стиле sklearn. \n",
|
||||
" Основано на реализации github.co/Hiroaki-K4/total_perspective_vortex/main/srcs/csp.py\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" def __init__(self, n_components: int) -> None:\n",
|
||||
" self.n_components = n_components\n",
|
||||
" self.classes = None\n",
|
||||
" self.filters = None\n",
|
||||
" self.patterns = None\n",
|
||||
" self.mean = None\n",
|
||||
" self.std = None\n",
|
||||
"\n",
|
||||
" def __get_covariate(self, x: np.ndarray, y: np.ndarray, cls: any) -> np.ndarray:\n",
|
||||
" \"\"\"Получение ковариаций\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" x (np.ndarray): отсчеты сигнала\n",
|
||||
" y (np.ndarray): метки класса\n",
|
||||
" cls (any): текущий класс\n",
|
||||
" \n",
|
||||
" Returns:\n",
|
||||
" np.ndarray: матрица ковариаций\n",
|
||||
" \"\"\"\n",
|
||||
" \n",
|
||||
" x_class = x[y == cls]\n",
|
||||
" _, _, n_channels = x_class.shape\n",
|
||||
" # x_class = np.transpose(x_class, [1, 0, 2])\n",
|
||||
" x_class = x_class.reshape(n_channels, -1)\n",
|
||||
" cov = np.dot(x_class, x_class.T)\n",
|
||||
" return cov\n",
|
||||
" \n",
|
||||
" def fit(self, x: np.ndarray, y: np.ndarray) -> None:\n",
|
||||
" \"\"\"Обучение фильтра\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" x (np.ndarray): отсчеты сигнала (числа)\n",
|
||||
" y (np.ndarray): метки класса (любые данные)\n",
|
||||
" \"\"\"\n",
|
||||
" self.classes = np.unique(y)\n",
|
||||
" n_classes = len(self.classes)\n",
|
||||
" if n_classes != 2:\n",
|
||||
" raise ValueError(f\"Количество классов для CSP должно быть = 2 (получено {n_classes})\")\n",
|
||||
"\n",
|
||||
" cov_neg = self.__get_covariate(x, y, self.classes[0])\n",
|
||||
" cov_pos = self.__get_covariate(x, y, self.classes[1])\n",
|
||||
"\n",
|
||||
" eig_vals, eig_vecs = scipy.linalg.eigh(cov_neg, cov_pos)\n",
|
||||
" for i in range(len(eig_vecs)):\n",
|
||||
" eig_vecs[i] = eig_vecs[i] / np.linalg.norm(eig_vecs[i])\n",
|
||||
"\n",
|
||||
" sorted_vals = np.argsort(eig_vals)\n",
|
||||
" idxs = np.empty_like(sorted_vals)\n",
|
||||
" idxs[0::2] = sorted_vals[len(sorted_vals) // 2 :][::-1]\n",
|
||||
" idxs[1::2] = sorted_vals[: len(sorted_vals) // 2]\n",
|
||||
" \n",
|
||||
" eig_vecs = eig_vecs[:, idxs]\n",
|
||||
" self.filters = eig_vecs.T\n",
|
||||
" self.patterns = np.linalg.inv(eig_vecs)\n",
|
||||
" pick_filters = self.filters[:, self.n_components]\n",
|
||||
"\n",
|
||||
" x = np.asarray([np.dot(pick_filters, epoch.T) for epoch in x])\n",
|
||||
" x = (x ** 2).mean(axis=1)\n",
|
||||
" self.mean = x.mean(axis=0)\n",
|
||||
" self.std = x.std(axis=0)\n",
|
||||
"\n",
|
||||
" def transform(self, x: np.ndarray) -> np.ndarray:\n",
|
||||
" \"\"\"Применение фильтра\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" x (np.ndarray): отсчеты сигнала (числа)\n",
|
||||
" \n",
|
||||
" Returns:\n",
|
||||
" np.ndarray: вторичные параметры (фичи)\n",
|
||||
" \"\"\"\n",
|
||||
" pick_filters = self.filters[: self.n_components]\n",
|
||||
" x = np.asarray([np.dot(pick_filters, epoch.T) for epoch in x])\n",
|
||||
" x = (x ** 2).mean(axis=1)\n",
|
||||
" x -= self.mean\n",
|
||||
" x /= self.std\n",
|
||||
" return x\n",
|
||||
"\n",
|
||||
" def fit_transform(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:\n",
|
||||
" \"\"\"Обучение с последующим применением ко входным данным\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" x (np.ndarray): отсчеты сигнала (числа)\n",
|
||||
" y (np.ndarray): метки класса (любые данные)\n",
|
||||
" \n",
|
||||
" Returns:\n",
|
||||
" np.ndarray: вторичные параметры (фичи)\n",
|
||||
" \"\"\"\n",
|
||||
" self.fit(x, y)\n",
|
||||
" return self.transform(x)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(71, 1000)"
|
||||
]
|
||||
},
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"csp_filter = CSPFilter(4)\n",
|
||||
"csp_filter.fit(x, y)\n",
|
||||
"csp_filter.transform(x).shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"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.10.13"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
|
||||
=================================================
|
||||
Îïèñàíèå ôîðìàòà csv ôàéëà
|
||||
http://openvibe.inria.fr/csv-file-format-description/
|
||||
Êîäû ñîáûòèé:
|
||||
http://openvibe.inria.fr/stimulation-codes/
|
||||
|
||||
|
||||
|
||||
Ôîðìàò âûâîäà:
|
||||
|
||||
Time:250Hz,
|
||||
Epoch,
|
||||
Channel 1,
|
||||
Channel 2,
|
||||
Channel 3,
|
||||
Channel 4,
|
||||
Channel 5,
|
||||
Channel 6,
|
||||
Channel 7,
|
||||
Channel 8,
|
||||
Channel 9,
|
||||
Channel 10,
|
||||
Channel 11,
|
||||
Channel 12,
|
||||
Channel 13,
|
||||
Channel 14,
|
||||
Channel 15,
|
||||
Channel 16,
|
||||
Event Id,
|
||||
Event Date,
|
||||
|
||||
|
||||
|
||||
Ïðèìåðû ôðàãìåíòîâ çàïèñè îòíîñÿùèåñÿ ê ñîáûòèÿì ïîêàçà ñòðåëîê:
|
||||
|
||||
Ëåâî
|
||||
32.9839999469,257,175321.6875000000,161476.5781250000,157384.8906250000,165382.3125000000,182353.1093750000,163875.0781250000,160708.5000000000,162043.2968750000,150507.6562500000,177546.5781250000,156140.7187500000,132061.4531250000,152687.8437500000,153717.4062500000,163318.2500000000,154924.4218750000,
|
||||
770:770,
|
||||
32.9874541578:32.9876184834,
|
||||
0.0000000000:0.0000000000
|
||||
32.9879999468,257,175311.5468750000,161454.5156250000,157379.3906250000,165363.1093750000,182351.3593750000,163863.5000000000,160706.2656250000,162040.2187500000,150504.3281250000,177536.3281250000,156103.6406250000,132053.7187500000,152655.0156250000,153707.7500000000,163314.8906250000,154920.7187500000,,,
|
||||
32.9919999468,257,175305.4843750000,161444.4375000000,157375.6718750000,165352.2968750000,182349.0781250000,163856.1250000000,160703.7343750000,162037.6875000000,150501.0468750000,177530.1250000000,156083.9531250000,132048.3281250000,152644.7812500000,153701.3906250000,163310.8281250000,154917.0468750000,,,
|
||||
32.9959999467,257,175315.4062500000,161463.2812500000,157381.4062500000,165367.3437500000,182351.8906250000,163865.5000000000,160707.1562500000,162041.9375000000,150504.0156250000,177539.1562500000,156111.0312500000,132055.3593750000,152673.0468750000,153708.9375000000,163314.3750000000,154920.6718750000,
|
||||
770,
|
||||
32.9962524336,
|
||||
0.0000000000
|
||||
|
||||
Ïðàâî
|
||||
60.9639999014,476,175538.3437500000,162268.3906250000,157672.1093750000,165778.6562500000,182674.5156250000,165550.2656250000,161696.2656250000,162263.4062500000,151121.2812500000,178085.1250000000,156655.3437500000,132842.8750000000,153574.0000000000,154055.8593750000,163721.6718750000,154574.9375000000,
|
||||
769:769,
|
||||
60.9675073745:60.9675452565,
|
||||
0.0000000000:0.0000000000
|
||||
60.9679999014,476,175526.0312500000,162246.7031250000,157664.8125000000,165758.0937500000,182672.2187500000,165538.8906250000,161693.2500000000,162259.3750000000,151118.2187500000,178074.6406250000,156616.5000000000,132834.6093750000,153542.0781250000,154045.5781250000,163716.9375000000,154570.5625000000,
|
||||
769,
|
||||
60.9707477945,
|
||||
0.0000000000
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
|
||||
=================================================
|
||||
Îïèñàíèå ôîðìàòà csv ôàéëà
|
||||
http://openvibe.inria.fr/csv-file-format-description/
|
||||
Êîäû ñîáûòèé:
|
||||
http://openvibe.inria.fr/stimulation-codes/
|
||||
|
||||
|
||||
|
||||
Ôîðìàò âûâîäà:
|
||||
|
||||
Time:250Hz,
|
||||
Epoch,
|
||||
Channel 1,
|
||||
Channel 2,
|
||||
Channel 3,
|
||||
Channel 4,
|
||||
Channel 5,
|
||||
Channel 6,
|
||||
Channel 7,
|
||||
Channel 8,
|
||||
Channel 9,
|
||||
Channel 10,
|
||||
Channel 11,
|
||||
Channel 12,
|
||||
Channel 13,
|
||||
Channel 14,
|
||||
Channel 15,
|
||||
Channel 16,
|
||||
Event Id,
|
||||
Event Date,
|
||||
|
||||
|
||||
|
||||
Ïðèìåðû ôðàãìåíòîâ çàïèñè îòíîñÿùèåñÿ ê ñîáûòèÿì ïîêàçà ñòðåëîê:
|
||||
|
||||
Ëåâî
|
||||
32.9839999469,257,175321.6875000000,161476.5781250000,157384.8906250000,165382.3125000000,182353.1093750000,163875.0781250000,160708.5000000000,162043.2968750000,150507.6562500000,177546.5781250000,156140.7187500000,132061.4531250000,152687.8437500000,153717.4062500000,163318.2500000000,154924.4218750000,
|
||||
770:770,
|
||||
32.9874541578:32.9876184834,
|
||||
0.0000000000:0.0000000000
|
||||
32.9879999468,257,175311.5468750000,161454.5156250000,157379.3906250000,165363.1093750000,182351.3593750000,163863.5000000000,160706.2656250000,162040.2187500000,150504.3281250000,177536.3281250000,156103.6406250000,132053.7187500000,152655.0156250000,153707.7500000000,163314.8906250000,154920.7187500000,,,
|
||||
32.9919999468,257,175305.4843750000,161444.4375000000,157375.6718750000,165352.2968750000,182349.0781250000,163856.1250000000,160703.7343750000,162037.6875000000,150501.0468750000,177530.1250000000,156083.9531250000,132048.3281250000,152644.7812500000,153701.3906250000,163310.8281250000,154917.0468750000,,,
|
||||
32.9959999467,257,175315.4062500000,161463.2812500000,157381.4062500000,165367.3437500000,182351.8906250000,163865.5000000000,160707.1562500000,162041.9375000000,150504.0156250000,177539.1562500000,156111.0312500000,132055.3593750000,152673.0468750000,153708.9375000000,163314.3750000000,154920.6718750000,
|
||||
770,
|
||||
32.9962524336,
|
||||
0.0000000000
|
||||
|
||||
Ïðàâî
|
||||
60.9639999014,476,175538.3437500000,162268.3906250000,157672.1093750000,165778.6562500000,182674.5156250000,165550.2656250000,161696.2656250000,162263.4062500000,151121.2812500000,178085.1250000000,156655.3437500000,132842.8750000000,153574.0000000000,154055.8593750000,163721.6718750000,154574.9375000000,
|
||||
769:769,
|
||||
60.9675073745:60.9675452565,
|
||||
0.0000000000:0.0000000000
|
||||
60.9679999014,476,175526.0312500000,162246.7031250000,157664.8125000000,165758.0937500000,182672.2187500000,165538.8906250000,161693.2500000000,162259.3750000000,151118.2187500000,178074.6406250000,156616.5000000000,132834.6093750000,153542.0781250000,154045.5781250000,163716.9375000000,154570.5625000000,
|
||||
769,
|
||||
60.9707477945,
|
||||
0.0000000000
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
1. В данных существуюет небольшие выбросы по времени предъявления стимула (лево/право) см. pdf. В программе есть вариации по длительности предъявления или это ошибка чтения? >90% стимулов - 5 с. однако, 1-3 стимула (в среднем) длиннее - до 7 с. (см. pdf).
|
||||
2. Какие электроды берем для эксперемента с 3мя? Номера в нотации 'Channel #'
|
||||
3. Если эпохи по 4 с после предъявления сигнала (с учётом паузы в 0,5 с), то через какой шаг смещаемся внутри времени предъявления стимула? Через каждый отсчет - не будет онлайна (не успеем вычислить фильтры и сеть со скоростью 1/250с). Пока сделал через 0,5 с (принцип скользящего окна - когда праый край касается границы предъявления стимула прекращаем скольжение, см. pdf).
|
||||
4. Реализация CSP может отличаться.
|
||||
Reference in new issue
Block a user