init commit: загрузка и предобработка данных
This commit is contained in:
+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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user