TLDR-›

А вроде, почему?
· Нейронный интерфейс→
· Вызов
· Виды модели (некоторые из множества) →
· Разбираем код →
· Теперь, наконец, контролируемое обучение
· Давайте теперь проверим нейронную сеть
· Набор данных
· ПОСЛЕДНИЕ, ПОДКЛЮЧАЕМ ДАННЫЕ И СЕТЬ!

Но типа, почему?

Реальные проблемы редко бывают простыми, сложные проблемы не совсем пустяки, поэтому ученые часто упрощают их, чтобы понять, что происходит. Но даже с упрощенной моделью выдвижение теорий о реальной сделке по-прежнему остается сложной задачей, полной ошибок, трудностей и стремительного прогресса. Разве наука не велика?

Например, мы можем посмотреть на мировую экономику. Пытаться разобраться в мировой экономике — все равно, что пытаться собрать кубик Рубика с завязанными глазами. Слишком много переменных — от причудливых хитроумных стратегий крупных инвестиционных фирм до того, какой риск готов взять на себя отдельный трейдер. И давайте смотреть правде в глаза, наш мозг примерно так же приспособлен, чтобы справляться со всеми этими сложностями, как хомяк — управлять самолетом. Вот где пригодятся машинные алгоритмы — они подобны суперкомпьютерам, которые могут решить экономический кубик Рубика в мгновение ока, со всей точностью лазерного луча.

Нейронный интерфейс→

Человеческий мозг представляет собой чрезвычайно сложную систему, и ее изучение может быть довольно сложной задачей. Одна из проблем заключается в том, что мы не можем напрямую измерить активность мозга. Вместо этого мы должны полагаться на датчики, которые измеряют уровни напряжения в мозгу. Хотя эти датчики могут предоставить нам некоторую полезную информацию, они являются косвенными показателями мозговой активности и не могут отразить всю сложность мозга.

Другое дело, что структура мозга у всех разная. Это означает, что любое единственное решение, которое мы найдем для изучения мозга или взаимодействия с ним, не будет работать для всех. Нам нужно адаптировать наши решения к каждому человеку, что добавляет дополнительный уровень сложности проблеме.

Кроме того, «проблема», которую мы пытаемся решить, взаимодействуя с мозгом, будет значительно различаться от человека к человеку. Универсального решения проблемы изучения и взаимодействия с человеческим мозгом не существует, и вряд ли когда-либо будет, даже при использовании чрезвычайно точных датчиков.

Соревнование

Для этой модели я сосредоточился на упрощенной задаче и использовал простую нейронную сеть для ее решения.

Набор данных Почему? По сути, программа использует данные, чтобы выяснить, как что-то сделать. Например, если вы хотите научить программу машинного обучения распознавать кошек на картинках, вам нужно предоставить ей множество изображений кошек и указать, какие из них являются кошками. Это называется «набор данных». Программа смотрит на картинки и выясняет, что делает кошку кошкой. Может быть, это форма глаз, размер ушей или цвет меха. Программа ML использует эту информацию для прогнозирования новых изображений. Если он увидит изображение кота, которого раньше не видел, он может сказать «это кот», потому что знает, на какие черты обращать внимание. Короче говоря, набор данных похож на набор примеров, которые программа машинного обучения использует, чтобы узнать, как что-то делать. Без набора данных программа машинного обучения не знала бы, что делать.

  • Для этой программы я использовал набор данных из библиотеки MNE Python. Этот набор данных предлагает несколько образцов ЭЭГ, каждый из которых имеет 226 точек данных. где каждая выборка соответствует определенному испытанию из задачи. Где каждый образец представляет собой одно испытание, описанное в задаче.

Задача →

В этом эксперименте испытуемый сидит перед экраном, и ему предъявляется серия изображений. Изображения включают в себя узоры в виде шахматной доски, которые отображаются либо в левом, либо в правом поле зрения и сопровождаются тонами, которые подаются либо в левое, либо в правое ухо. Интервал между стимулами (т. е. узорами шахматной доски и тонами) составляет 750 миллисекунд (мс).

Иногда в центре поля зрения появляется смайлик. Когда это происходит, испытуемого просят как можно быстрее нажать клавишу правым указательным пальцем. Это задание, вероятно, используется для измерения времени реакции испытуемого на появление смайлика.

В целом, этот эксперимент предназначен для изучения зрительной и слуховой обработки субъекта, а также времени его реакции. Подавая стимулы в разные части поля зрения и в разные уши, экспериментаторы могут изучать, как мозг субъекта обрабатывает и интегрирует эту информацию. Задача нажатия клавиши в ответ на смайлик позволяет экспериментаторам измерить время реакции субъекта, что может дать представление о скорости их когнитивной обработки.

Цель этого эксперимента — найти способ обнаружить удивление или ожидание, наблюдая за электрической активностью мозга. Экспериментаторы надеются, что, создав систему обнаружения компонента P300, они смогут решить проблему обнаружения этих эмоций.

Но что такое P300?

P300 — это специфический паттерн, который проявляется в электрической активности мозга в ответ на определенные раздражители. Обычно это наблюдается как положительное отклонение или всплеск в потоке данных ЭЭГ примерно через 300 миллисекунд (мс) после предъявления редкого или целенаправленного стимула. Например, он покажет, вытягивает ли кто-то карту из колоды, которую он ожидал. Спеллер P300 является прямым приложением P300 к приложениям BCI. С помощью этого приложения пользователи могут печатать, сосредоточив внимание на букве, которую они хотят ввести, и выбрав ее из алфавита, отображаемого на экране.

Примечание → обнаружение P300 может быть сложной задачей, поскольку P300 представляет собой относительно небольшой сигнал в данных ЭЭГ, и его бывает трудно отличить от других фоновых шумов или артефактов. Однако с помощью таких методов, как усреднение или обработка сигналов, можно улучшить отношение сигнал/шум и повысить точность обнаружения P300.

Использование машинного обучения

Во-первых, важно понимать, что машины могут работать во многих измерениях, потому что они могут быстро и точно обрабатывать и анализировать большие объемы данных. Это особенно полезно в контексте интерфейсов мозг-компьютер (BCI), где алгоритмы машинного обучения могут использоваться для анализа моделей активности мозга и прогнозирования того, что пытается сделать пользователь.

Классификатор — это тип алгоритма машинного обучения, который предназначен для классификации данных по различным классам или категориям. В контексте BCI можно использовать классификатор для анализа сигналов мозга и присвоения им меток, таких как «Движение вправо» или «Движение влево». Это может быть полезно в различных приложениях, таких как управление протезом конечности или компьютерным курсором.

Одно из преимуществ использования алгоритмов машинного обучения в BCI заключается в том, что их можно научить распознавать закономерности в активности мозга, которые людям может быть трудно различить. Например, алгоритм может идентифицировать тонкие различия в активности мозга, связанные с различными действиями или намерениями, даже если эти различия не сразу заметны человеку-наблюдателю. Это может позволить BCI более точно и эффективно реагировать на намерения пользователя.

Таким образом, использование алгоритмов машинного обучения в BCI может стать мощным инструментом, помогающим пользователям взаимодействовать с окружающей средой новыми и инновационными способами. Анализируя закономерности активности мозга и делая прогнозы на основе этих закономерностей, алгоритмы машинного обучения могут позволить пользователям управлять устройствами и выполнять задачи, которые в противном случае были бы трудными или невозможными.

Типы моделей (некоторые из многих) →

Машина опорных векторов

Проще говоря, SVM (Support Vector Machine) — это тип алгоритма машинного обучения, который используется для классификации данных по разным категориям. Он делает это, находя линию или кривую, которая как можно точнее разделяет данные на разные категории. Эта линия или кривая называется «гиперплоскостью».

Чтобы найти гиперплоскость, которая лучше всего подходит для заданного набора данных, алгоритм SVM проходит процесс оптимизации, в ходе которого он пытается свести к минимуму количество точек данных, которые неправильно классифицированы или помещены в неправильную категорию («обучение алгоритма»). Например, если вы используете SVM для классификации баскетболистов как защитников или нападающих, алгоритм попытается найти гиперплоскость, которая максимально правильно отделяет защитников от нападающих.

В частности, в модели, которую я построил здесь, цель состоит в том, чтобы использовать сигналы ЭЭГ (электроэнцефалография), чтобы определить, испытывает ли человек удивление. Чтобы обучить алгоритм SVM (машина опорных векторов) для выполнения этой задачи, мне понадобились примеры данных ЭЭГ с компонентом P300 и без него (два разных класса данных). Входными данными для SVM будут значения, записанные для каждого канала устройства ЭЭГ.

Процесс оптимизации SVM преследовал 3 разные цели с убывающей релевантностью:

  • Минимизация ошибочно классифицированных экземпляров → Основная цель алгоритма SVM — найти линию или кривую (называемую «гиперплоскостью»), которая максимально правильно разделяет разные категории данных. Однако не всегда возможно найти гиперплоскость, которая идеально разделяет все точки данных на их правильные категории. В этом случае алгоритм SVM попытается свести к минимуму количество неправильно классифицированных точек данных или тех, которые помещены в неправильную категорию.

  • «Граница» гиперплоскости → Положение гиперплоскости в алгоритме SVM выбирается таким образом, чтобы максимально увеличить расстояние между гиперплоскостью и ближайшими точками данных любого класса. Максимальное увеличение запаса помогает улучшить производительность обобщения модели SVM, а это означает, что она с большей вероятностью правильно классифицирует новые, невидимые данные. Другими словами, цель состоит в том, чтобы найти гиперплоскость, которая не только правильно разделяет различные классы данных, но и имеет максимально возможное расстояние между гиперплоскостью и ближайшими точками данных. Это помогает гарантировать, что модель не будет слишком сложной и сможет хорошо обобщать новые данные.

Ядро → Если невозможно разделить два класса данных с помощью прямой линии или кривой (гиперплоскости), алгоритм SVM (Машина опорных векторов) может использовать метод, называемый «трюком ядра», для преобразования данных таким образом, который позволяет для разлуки. Хитрость ядра заключается в использовании математических преобразований для сопоставления точек данных с многомерным пространством, где становится возможным найти гиперплоскость, разделяющую два класса. Существуют различные ядра, которые можно использовать в этом процессе, но наиболее распространенным является ядро ​​радиальной базовой функции (RBF). Ядро RBF помогает размещать данные из двух классов на разных уровнях, упрощая поиск гиперплоскости, удовлетворяющей требуемому условию.

Нейронная сеть

Нейронные сети предназначены для имитации того, как работает человеческий мозг для решения проблем. В человеческом мозге нейроны взаимодействуют друг с другом посредством процесса, называемого «срабатыванием», при котором один нейрон посылает сигнал другим нейронам, которые, в свою очередь, посылают сигналы другим нейронам, создавая цепную реакцию. Эта цепная реакция, или «каскадный эффект», позволяет мозгу выполнять широкий спектр задач и достигать сложных целей.

Точно так же нейронные сети состоят из взаимосвязанных «нейронов» или единиц, которые обрабатывают и передают информацию. Эти нейроны организованы в слои и работают вместе, чтобы анализировать и понимать сложные закономерности в данных. Воспроизводя то, как человеческий мозг обрабатывает и передает информацию, нейронные сети могут научиться решать проблемы и делать точные прогнозы или решения.

В мозге или нейронной сети нейроны — это клетки, которые получают и передают информацию. У них есть входы, называемые дендритами, которые получают сигналы от других нейронов, и выходы, называемые аксонами, которые передают сигналы другим нейронам. Когда нейрон получает достаточно входных данных, он «срабатывает» и посылает сигнал по своему аксону другим нейронам. Сила связи между двумя нейронами называется весом связи. В нейронной сети у каждого нейрона также есть «алгоритм активации», который определяет выходные данные нейрона на основе полученных им входных данных.

И нейронные сети, и машины опорных векторов (SVM) — это типы алгоритмов машинного обучения, которые можно использовать для классификации данных. Нейронная сеть создает многомерное пространство решений, чтобы найти лучший способ классификации данных, в то время как SVM начинает с заранее определенного алгоритма. Нейронные сети требуют больше времени на настройку, но могут быть более гибкими и эффективными, чем SVM. С другой стороны, SVM требуют меньше времени на настройку, но могут быть менее гибкими и эффективными, чем нейронные сети.

нейронная сеть, которая может решать только линейные задачи

Построенная модель не может точно идентифицировать компоненты p300 во всех случаях, поскольку эти компоненты могут различаться в зависимости от изучаемого человека, и может присутствовать нелинейный шум. Чтобы решить эти проблемы, я использую нейронную сеть, которая может адаптироваться к этим вариациям и потенциально другим проблемам, которые могут возникнуть. Эта нейронная сеть будет обучена идентифицировать компоненты p300 и в конечном итоге использоваться для определения того, когда человек удивлен изображением.

Разбираем код →

Первый шаг — настроить среду, загрузить библиотеки и пакеты.

Импорт библиотек:

!pip install mne #EEG Data Package
!pip install sklearn # Machine Learning Library
!pip install matplotlib # Plotting Library
!pip install torch # Machine Learning Library
!pip install torchvision
!pip install tensorboardX

MNE → помогает обрабатывать, анализировать и визуализировать данные ЭЭГ

sklearn → пакет ML с инструментами для анализа данных и моделирования

matplotlib → Это библиотека 2D-графики, которая позволяет пользователям создавать широкий спектр статических, анимированных и интерактивных визуализаций.

факел → работает и работает с многомерными массивами (тензорами)

torchvision → работает с обработкой изображений и видео, обнаружением объектов и классификацией изображений.

tensorboardX → работает с логированием и визуализацией процесса обучения моделей PyTorch

Импорт модулей

from collections import OrderedDict
from pylab import rcParams
import torch
import torch.nn as nn
import torchvision.transforms
import matplotlib.pyplot as plt
import numpy as np
import mne
from sklearn.preprocessing import RobustScaler
  • Модуль OrderedDict из библиотеки collections импортируется для использования упорядоченной структуры данных словаря. Это может быть полезно для хранения данных в определенном порядке или для создания сопоставления между ключами и значениями, поддерживающего порядок добавления элементов.
  • Функция rcParams из библиотеки pylab импортирована для установки значений по умолчанию для различных параметров форматирования графика в matplotlib. Это может быть полезно для настройки единообразного внешнего вида графиков в проекте.
  • Библиотека torch импортируется для обеспечения доступа к тензорной библиотеке PyTorch и различным функциям машинного обучения.
  • Модуль nn из библиотеки torch импортируется для использования функций нейронной сети PyTorch.
  • Модуль torchvision.transforms импортируется для использования функций преобразования изображений PyTorch. Это может быть полезно для предварительной обработки изображений перед их использованием в качестве входных данных для модели.
  • Модуль matplotlib.pyplot импортирован для использования различных функций построения графиков. Это может быть полезно для визуализации данных или результатов моделей машинного обучения.
  • Библиотека numpy импортирована для использования различных числовых и научных вычислительных функций.
  • Библиотека mne импортируется для использования различных функций обработки и анализа электрофизиологических данных, таких как ЭЭГ и МЭГ.
  • Класс RobustScaler из модуля sklearn.preprocessing импортируется для использования масштабатора, устойчивого к воздействию выбросов в данных. Это может быть полезно для нормализации данных, когда есть потенциальные выбросы, которые могут повлиять на процесс масштабирования.

Инициализировать параметры

eeg_sample_count = 240 # How many samples are we training
learning_rate = 1e-3 # How hard the network will correct its mistakes while learning
eeg_sample_length = 226 # Number of eeg data points per sample
number_of_classes = 1 # We want to answer the "is this a P300?" question
hidden1 = 500 # Number of neurons in our first hidden layer
hidden2 = 1000 # Number of neurons in our second hidden layer
hidden3 = 100 # Number of neurons in our third hidden layer
output = 10 # Number of neurons in our output layer
  • Переменная eeg_sample_count представляет количество выборок, которые используются для обучения.
  • Переменная learning_rate указывает силу коррекции, которую делает сеть, когда она учится на своих ошибках. Меньшая скорость обучения может привести к более медленному обучению, но также может привести к более точной модели.
  • Переменная eeg_sample_length представляет количество точек данных ЭЭГ в каждом образце.
  • Переменная number_of_classes указывает, что модель обучается классифицировать события одного типа (в данном случае «это P300?»).
  • Переменные hidden1, hidden2 и hidden3 представляют количество нейронов в каждом из трех скрытых слоев модели. Эти слои отвечают за извлечение признаков из входных данных и передачу их в выходной слой.
  • Переменная output представляет количество нейронов в выходном слое модели. Этот слой отвечает за создание окончательного прогноза на основе признаков, извлеченных скрытыми слоями.

Настройка нейронной сети

Обзор:

Граф сети представляет собой набор линейных узлов и узлов активации. Это изображение примерно соответствует заданному графику.

До тех пор, пока они не пройдут через конечный выходной узел, входные данные будут двигаться по этому графику, собирая смещения, масштабируясь по весам и трансформируясь функциями активации. Затем конечное значение преобразуется в число от 0 до 1 с помощью сигмоидальной функции → конечный # — это предиктор p300.

**

## Define the network
tutorial_model = nn.Sequential()
# Input Layer (Size 226 -> 500)
tutorial_model.add_module('Input Linear', nn.Linear(eeg_sample_length, hidden1))
tutorial_model.add_module('Input Activation', nn.CELU()) 
# Hidden Layer (Size 500 -> 1000)
tutorial_model.add_module('Hidden Linear', nn.Linear(hidden1, hidden2))
tutorial_model.add_module('Hidden Activation', nn.ReLU())
# Hidden Layer (Size 1000 -> 100)
tutorial_model.add_module('Hidden Linear2', nn.Linear(hidden2, hidden3))
tutorial_model.add_module('Hidden Activation2', nn.ReLU())
# Hidden Layer (Size 100 -> 10)
tutorial_model.add_module('Hidden Linear3', nn.Linear(hidden3, 10))
tutorial_model.add_module('Hidden Activation3', nn.ReLU())
# Output Layer (Size 10 -> 1)
tutorial_model.add_module('Output Linear', nn.Linear(10, number_of_classes))
tutorial_model.add_module('Output Activation', nn.Sigmoid())

Здесь я определил нейронную сеть с помощью библиотеки PyTorch. Он создал последовательную модель, которая представляет собой сеть с прямой связью, состоящую из ряда слоев, которые обрабатывают входные данные и передают их на следующий уровень.

  • Сеть имеет входной слой размером 226 и скрытый слой размером 500. Входной слой определяется с помощью модуля nn.Linear, который применяет линейное преобразование к входным данным. Входной слой также включает функцию активации, которая представляет собой нелинейную функцию, которая вводит нелинейность в сеть. В этом случае функцией активации является функция CELU (Concurrent Exponential Linear Unit).
  • В сети также есть два дополнительных скрытых слоя, каждый размером 1000 и 100 соответственно. Эти скрытые слои также определяются с помощью модуля nn.Linear и функции активации, которой в данном случае является функция ReLU (выпрямленная линейная единица).
  • Наконец, сеть имеет выходной слой размером 1. Этот слой также определяется с помощью модуля nn.Linear и функции активации, которая в данном случае является сигмовидной функцией. Сигмовидная функция часто используется в качестве функции активации для выходного слоя модели классификации, потому что она отображает результат в значение от 0 до 1, которое можно интерпретировать как вероятность.

В целом, этот код определяет нейронную сеть с рядом полностью связанных слоев, которые обрабатывают входные данные и производят выходные данные. Сеть имеет в общей сложности пять скрытых слоев и выходной слой, и каждый слой включает функцию активации для введения нелинейности в модель.

Функции обучения и потерь

Обзор:

Далее нам понадобятся функции потерь и обучения. По сути, мы получаем обратную связь от функций потерь о том, насколько хорошо сеть обучается. Потеря нуля или почти нуля указывает на то, что сеть успешно прогнозирует обучающий набор данных. Это значение можно использовать для оценки завершения обучения сети.

Ниже я определяю базовую процедуру обучения, которая обновляет веса сети и вычисляет потери для каждой итерации, а также широко используемую функцию потерь, которую можно использовать в PyTorch.

# Define a loss function
loss_function = torch.nn.MSELoss()
# Define a training procedure
def train_network(train_data, actual_class, iterations):
  # Keep track of loss at every training iteration
  loss_data = []
  # Begin training for a certain amount of iterations
  for i in range(iterations):
    # Begin with a classification
    classification = tutorial_model(train_data)
    # Find out how wrong the network was
    loss = loss_function(classification, actual_class)
    loss_data.append(loss)
    # Zero out the optimizer gradients every iteration
    optimizer.zero_grad()
    # Teach the network how to do better next time
    loss.backward()
    optimizer.step()
  # Plot a nice loss graph at the end of training
  rcParams['figure.figsize'] = 10, 5
  plt.title("Loss vs Iterations")
  plt.plot(list(range(0, len(loss_data))), loss_data)
  plt.show()
# Save the network's default state so we can retrain from the default weights
torch.save(tutorial_model, "/home/tutorial_model_default_state")

Этот код определяет процедуру обучения нейронной сети с использованием библиотеки PyTorch.

  • Процедура обучения принимает три входных данных: train_data, фактический_класс и итерации. train_data — это пакет входных данных, которые сеть будет использовать для изучения и обновления своих весов. Фактический_класс — это правильная метка класса для входных данных, с которыми сеть будет сравнивать свои прогнозы, чтобы измерить свою производительность. Параметр iterations указывает, сколько раз должна выполняться процедура обучения.
  • Процедура обучения начинается с определения функции потерь с использованием модуля nn.MSELoss. Эта функция потерь измеряет среднеквадратичную ошибку между прогнозами сети и фактическими метками классов.
  • Затем процедура обучения запускает цикл для заданного количества итераций. На каждой итерации сеть делает прогноз, используя входные данные, и вычисляет потери, используя функцию потерь. Затем с помощью функции loss.backward() вычисляются градиенты потерь относительно весов сети, а оптимизатор обновляет веса с помощью функции optimizer.step().
  • В конце цикла обучения процедура обучения строит график потерь в зависимости от количества итераций для визуализации процесса обучения. Состояние сети по умолчанию также сохраняется с помощью функции torch.save, поэтому ее можно переобучить из весов по умолчанию позже.

Теперь, наконец, контролируемое обучение

Алгоритмы обучения с учителем — это тип алгоритма машинного обучения, который опирается на размеченные обучающие данные для обучения и прогнозирования. В этих алгоритмах четко определены вход и выход для каждого примера в обучающих данных.

Чтобы понять, как работают эти алгоритмы, рассмотрим пример сдачи итогового экзамена онлайн-курса. Каждый вопрос на экзамене включает некоторую информацию, которую вам нужно использовать для его решения (входные данные), и вы даете ответ (выходные данные). Без определенного ответа на каждый вопрос ваш учитель не сможет точно оценить вашу успеваемость, потому что у него не будет возможности сравнить ваши ответы с правильными ответами.

Точно так же алгоритмы обучения с учителем используют помеченные обучающие данные для изучения взаимосвязи между входными и выходными данными. Они используют эти знания, чтобы делать прогнозы на основе новых, невидимых данных. Поскольку ввод и вывод для каждого примера в обучающих данных четко определены, алгоритм может обучаться и делать точные прогнозы.

Короче говоря, алгоритмы обучения с учителем подобны маленьким джиннам, которые могут предсказывать будущее на основе прошлого опыта. У них есть возможность принимать некоторые входные данные, обрабатывать их с помощью своей волшебной лампы и выдавать предсказание того, что должно произойти. Но они не берут эти предсказания из воздуха — их нужно сначала обучить.

В процессе обучения этим алгоритмам подается серия примеров с правильным выводом, и они используют эти примеры, чтобы научиться делать прогнозы. Разные алгоритмы имеют разные процессы обучения, но все они используют метрику для измерения того, насколько хорошо они работают на обучающих примерах. Этот показатель может варьироваться в зависимости от типа модели и цели алгоритма.

Для моделей классификации, которые предсказывают метку дискретного класса, обычно используется метрика перекрестной энтропии. А для регрессионных моделей, которые предсказывают непрерывное значение, обычно используется среднеквадратическая ошибка (RMSE). Итак, если вы когда-нибудь окажетесь в ситуации, когда вам нужно предсказать будущее и вы не знаете, куда обратиться, просто помните, что алгоритмы обучения с учителем прикроют вашу спину (конечно, если вы их правильно обучили).

При обучении модели контролируемого обучения данные обычно делятся на три разных набора: обучающий набор, проверочный набор и тестовый набор. Учебный набор используется для подачи модели и корректировки весов и смещений модели. Набор проверки используется для оценки производительности модели и ее настройки на основе результатов. Набор тестов используется для оценки окончательной производительности модели и просмотра того, как она работает с невидимыми данными.

Чтобы проиллюстрировать эту концепцию, рассмотрим пример обучения модели распознаванию мозговых волн P300 от других мозговых волн. У вас может быть набор данных с примерами мозговых волн P300 и примерами других мозговых волн. Затем вы разделите этот набор данных на обучающий набор, проверочный набор и тестовый набор. Модель будет обучаться на обучающем наборе, а ее производительность будет оцениваться на проверочном наборе. Как только модель будет завершена, она будет оценена на тестовом наборе, чтобы увидеть, насколько хорошо она работает с невидимыми данными.

Использование трех отдельных наборов данных позволяет уменьшить погрешность модели и улучшить ее способность к обобщению. Когда модель оценивается только на тренировочном наборе, она может пересоответствовать тренировочным данным и плохо работать на новых примерах. Однако, используя отдельный набор проверки и набор тестов, мы можем гарантировать, что модель способна обобщать новые данные и делать точные прогнозы.

Давайте теперь проверим нейронную сеть

После всего этого я проверил данные, чтобы определить, было ли обучение эффективным. Полное раскрытие → после этого все становится сложнее, но я разобью!

# Classify our positive test dataset
predicted_positives = tutorial_model(test_positives).data.tolist()

Здесь tutorial_model применяется к каждому примеру в наборе данных test_positives, который представляет собой список "положительных" примеров. Результатом модели является список прогнозов, который затем преобразуется в список Python с помощью метода tolist(). Этот список прогнозов хранится в переменной predicted_positives.

# Print the results
for index, value in enumerate(predicted_positives):
  print("Positive Test {1} Value scored: {0:.2f}%".format(value[0] * 100, index + 1))

Этот цикл перебирает список прогнозов в predicted_positives и печатает значение каждого прогноза в процентах. Функция enumerate() возвращает как индекс, так и значение каждого элемента в списке, что позволяет нам включить индекс в напечатанное сообщение. Метод format() используется для вставки значений в строку сообщения.

#Classify the negative test dataset
predicted_negatives = tutorial_model(test_negatives).data.tolist()

Эта строка похожа на первую строку кода, но она применяет tutorial_model к набору данных test_negatives, который представляет собой список «отрицательных» примеров. Результатом является список прогнозов, который хранится в переменной predicted_negatives.

Наконец, остальная часть кода представляет собой цикл, аналогичный предыдущему, но он перебирает список прогнозов в **predicted_negatives** и выводит значение каждого прогноза в процентах. Функция enumerate() возвращает как индекс, так и значение каждого элемента в списке, что позволяет нам включить индекс в напечатанное сообщение. Метод format() используется для вставки значений в строку сообщения.

В целом, этот код выполняет классификацию двух отдельных наборов данных: «положительного» тестового набора данных и «отрицательного» тестового набора данных. Модель, используемая для классификации, — tutorial_model, которая уже обучена на некоторых данных.

Во-первых, код классифицирует положительный тестовый набор данных, применяя tutorial_model к каждому примеру в наборе данных и сохраняя результаты в списке с именем predicted_positives. Затем результаты распечатываются со значением каждого прогноза, выраженным в процентах.

Затем код делает то же самое для отрицательного тестового набора данных, применяя tutorial_model к каждому примеру и сохраняя результаты в списке с именем predicted_negatives. Результаты снова печатаются в процентах.

Наконец, код генерирует точечную диаграмму с некоторыми выборками из положительных и отрицательных тестовых наборов данных. На графике показаны отдельные области красных и зеленых точек, при этом один положительный образец отмечен зеленым цветом, а два отрицательных образца - красным. Ось Y ограничена диапазоном [0, 1], а ось X представляет длину выборки ЭЭГ. График предназначен для иллюстрации «пространства решений» сети или областей, в которых модель может делать точные прогнозы.

На графике видны отдельные области красных и зеленых точек. Если бы входными данными для сети были простые координаты x и y, квадратная полоса в центре представляла бы «пространство решений» сети. Однако реальная выборка сигнала ЭЭГ представляет собой массив из нескольких точек и в любой момент может пересечь границу. Один положительный образец показан зеленым цветом, а два отрицательных образца показаны красным.

Похоже, что наша сеть правильно классифицирует простой набор фиктивных данных ЭЭГ, и теперь она готова к тестированию на реальных образцах. Мы почти готовы решить проблему с P300.

Набор данных

Мне нужно указать путь к конкретному набору данных, который я собираюсь использовать в Python, чтобы получить этот набор данных. В этом случае мозговые волны были отфильтрованы от 0 до 40 Гц в образце аудиовизуальной базы данных.

#getting the from a MNE library, here im using a p300 dataset 
data_path = mne.datasets.sample.data_path()
data_path

Я получил только сырые данные. Это означает, что это не сигналы, которые были вырезаны или сфокусированы на интересном событии, а просто набор потоковых образцов сигналов мозга из многочисленных каналов ЭЭГ.

from pathlib import Path
import mne
# Set the path to the data directory
data_path = Path('/path/to/data/directory')
# Construct the file paths using the data_path variable
raw_fname = data_path / 'MEG/sample/sample_audvis_filt-0-40_raw.fif'
event_fname = data_path / 'MEG/sample/sample_audvis_filt-0-40_raw-eve.fif'
# Read the raw data file and preload it into RAM
raw_data = mne.io.read_raw_fif(raw_fname, preload=True) 
# Set the EEG reference to the average of all voltages
raw_data.set_eeg_reference()

Здесь, в этом коде, я загрузил необработанные данные из файла и сохранил их в переменной с именем raw_data. Затем отбираются данные с электродов ЭЭГ и электродов ЭОГ, которые используются для измерения активности мозга и движения глаз соответственно. Этот набор данных также включает данные МЭГ, метода визуализации мозга, который использует сверхпроводники и требует большого устройства. Однако MEG не является широко доступным из-за его стоимости и технических требований.

Далее →

raw_data = raw_data.pick(picks=["eeg","eog"])
picks_eeg_only = mne.pick_types(raw_data.info, 
                                eeg=True, 
                                eog=True, 
                                meg=False, 
                                exclude='bads')

Таким образом, код выбирает определенные данные из набора данных raw_data и сохраняет их в новой переменной с именем raw_data. Функция pick позволяет пользователю выбирать, какие источники данных ему интересны. В этом случае пользователь выбирает данные с электродов ЭЭГ и ЭОГ.

Вторая строка кода использует функцию pick_types для выбора данных, соответствующих определенным критериям. В этом случае пользователь выбирает данные с пометкой "ЭЭГ" или "ЭОГ" (eeg=True, eog=True) и исключает данные с пометкой "МЭГ" или "Плохо" (meg=False, exclude='bads'). >). Полученные данные сохраняются в переменной picks_eeg_only.

Для упрощения этот код просматривает определенные события, которые произошли во время записи ЭЭГ, например, когда участнику был представлен смайлик. Затем данные нарезаются или «эпохируются», чтобы включить определенный период времени до и после того, как произошло событие. Это позволяет нам анализировать данные ЭЭГ именно во время этих событий и потенциально идентифицировать закономерности, такие как мозговая волна P300. Выбранный период времени может быть скорректирован, с более короткими периодами, потенциально позволяющими более точно анализировать паттерны мозговых волн.

events = mne.read_events(event_fname)
event_id = 5
tmin = -0.5 
tmax = 1
epochs = mne.Epochs(raw_data, events, event_id, tmin, tmax, proj=True,
                    picks=picks_eeg_only, baseline=(None, 0), preload=True,
                    reject=dict(eeg=100e-6, eog=150e-6), verbose = False)
print(epochs)

Этот код считывается в файле событий, который содержит информацию о том, когда произошли определенные события во время записи ЭЭГ. Затем он определяет идентификатор интересующего события (в данном случае идентификатор события 5 соответствует моменту, когда участникам был представлен смайлик) и временное окно вокруг события, которое мы хотим извлечь. Переменные tmin и tmax определяют время до и после события, которое мы хотим включить в эпоху.

Я использую функцию mne.Epochs() для создания эпох или небольших сегментов данных ЭЭГ вокруг этих событий. Эпохи создаются путем выбора только данных с электродов ЭЭГ и каналов ЭОГ (с использованием переменной picks_eeg_only) и применения базовой коррекции (вычитание средней активности в базовом периоде из активности в каждую эпоху). Затем эпохи загружаются в память для более быстрой обработки. Наконец, объект эпох выводится на консоль.

# This is the channel used to monitor the P300 response
channel = "EEG 058"
# Display a graph of the sensor position we're using
sensor_position_figure = epochs.plot_sensors(show_names=[channel]

Чтобы найти лучший канал ЭЭГ для обнаружения компонента P300, рассмотрите следующее:

  • Расположение электрода относительно мозга: компонент Р300 обычно связан с активностью в префронтальной коре, поэтому электрод, помещенный над этой областью, может иметь более высокие показатели компонента Р300.
  • Соотношение сигнал/шум. Высококачественная система записи ЭЭГ и шаги по минимизации шума и артефактов могут помочь улучшить соотношение сигнал/шум, облегчая обнаружение компонента P300.
  • Поэкспериментируйте с несколькими каналами: может быть полезно попробовать использовать несколько каналов и выбрать тот, который обеспечивает наиболее четкий и надежный сигнал P300.

Короче говоря, лучший канал ЭЭГ для обнаружения компонента P300 будет зависеть от конкретного вопроса исследования и экспериментальной установки.

epochs.plot_image(picks=channel)

Нам нужны контрпримеры, чтобы сопоставить наши данные о компонентах P300 с тем, что у нас есть. Мы также соберем все дополнительные случайные события из этого набора данных.

event_id=[1,2,3,4]
epochsNoP300 = mne.Epochs(raw_data, events, event_id, tmin, tmax, proj=True,
                    picks=picks_eeg_only, baseline=(None, 0), preload=True,
                    reject=dict(eeg=100e-6, eog=150e-6), verbose = False)
print(epochsNoP300)

По сути, здесь класс Epochs используется для представления сегментов непрерывных данных, привязанных по времени к какому-либо событию или триггеру. В этом случае переменная events содержит список событий или триггеров, а список event_id указывает, какие события мы хотим включить в эпохи. Переменные tmin и tmax определяют диапазон времени относительно каждого события, которое должно быть включено в эпохи.

Аргумент picks используется для указания того, какие каналы или датчики необработанных данных должны быть включены в эпохи. Аргумент baseline задает временной диапазон для использования в качестве эталона при вычислении эпох. Аргумент preload указывает, следует ли загружать данные в память немедленно, а аргумент reject указывает критерии отклонения эпох на основе данных в них. Наконец, аргумент verbose определяет, должна ли функция выводить дополнительную информацию во время работы.

После создания объекта Epochs он выводится на консоль с помощью функции print().

Далее

(это становится довольно сложно, но я попытался разобрать это с помощью аналогии) Представьте, что вы пытаетесь определить, чувствует ли человек себя счастливым или грустным, основываясь на выражении его лица. У вас есть набор данных изображений лиц людей, и на некоторых изображениях изображены счастливые лица, а на других — грустные лица. В наборе данных значительно больше изображений счастливых лиц, поэтому вы решаете использовать только подмножество изображений, чтобы сохранить баланс между двумя классами данных (счастливые и грустные лица).

Когда вы рисуете среднее выражение лица для счастливого и грустного лиц, вы замечаете, что нет существенной разницы между ними в области рта. Это означает, что вы не можете надежно использовать область рта, чтобы определить, счастлив человек или опечален, основываясь на выражении его лица.

еще много дел

Чтобы сделать данные более удобоваримыми, мне все еще нужно →

  • Масштабируйте данные, чтобы не допустить, чтобы выбросы управляли шоу.
  • Назначьте метки образцам, чтобы мы знали, к какому классу они принадлежат.
  • Объедините данные в одну большую счастливую семью.
  • Преобразуйте данные в правильный тип данных для использования PyTorch.
eeg_data_scaler = RobustScaler()
# We have 12 p300 samples
p300s = np.squeeze(epochs.get_data(picks=channel))
# We have 208 non-p300 samples
others = np.squeeze(epochsNoP300.get_data(picks=channel))
# Scale the p300 data using the RobustScaler
p300s = p300s.transpose()
p300s = eeg_data_scaler.fit_transform(p300s)
p300s = p300s.transpose()
# Scale the non-p300 data using the RobustScaler
others = others.transpose()
others = eeg_data_scaler.fit_transform(others)
others = others.transpose()
## Prepare the train and test tensors
# Specify Positive P300 train and test samples
p300s_train = p300s[0:9]
p300s_test = p300s[9:12]
p300s_test = torch.tensor(p300s_test).float()
# Specify Negative P300 train and test samples
others_train = others[30:39]
others_test = others[39:42]
others_test = torch.tensor(others_test).float()
# Combine everything into their final structures
training_data = torch.tensor(np.concatenate((p300s_train, others_train), axis = 0)).float()
positive_testing_data = torch.tensor(p300s_test).float()
negative_testing_data = torch.tensor(others_test).float()
# Print the size of each of our data structures
print("training data count: " + str(training_data.shape[0]))
print("positive testing data count: " + str(positive_testing_data.shape[0]))
print("negative testing data count: " + str(negative_testing_data.shape[0]))
# Generate training labels
labels = torch.tensor(np.zeros((training_data.shape[0],1))).float()
labels[0:10] = 1.0
print("training labels count: " + str(labels.shape[0]))

ХОРОШО, РАЗБИВКА КОДА СЕЙЧАС

  • Он создает объект RobustScaler с именем eeg_data_scaler, который является типом масштабатора, используемого для преобразования данных путем их масштабирования до определенного диапазона.
  • Он извлекает данные P300 из объекта epochs и данные, отличные от P300, из объекта epochsNoP300, используя метод get_data(), и сохраняет их в переменных p300s и others соответственно. Переменная channel используется для указания того, какие каналы или датчики должны быть включены в данные.
  • Он масштабирует данные P300 и не-P300 с помощью объекта RobustScaler и сохраняет масштабированные данные в переменных p300s и others.
  • Он разделяет масштабированные данные P300 и данные, отличные от P300, на обучающие и тестовые наборы. Для данных P300 он использует первые 9 выборок для обучения и оставшиеся 3 выборки для тестирования. Для данных, отличных от P300, он использует первые 9 выборок для обучения и оставшиеся 3 выборки для тестирования.
  • Он объединяет обучающие данные в один тензор с именем training_data, а тестовые данные — в отдельные тензоры с именами positive_testing_data и negative_testing_data.
  • Он генерирует метки для обучающих данных с меткой 1,0 для данных P300 и меткой 0,0 для данных, отличных от P300.
  • Он печатает размер каждой из структур данных.

ПОСЛЕДНИЕ, МЫ ПОДКЛЮЧАЕМ ДАННЫЕ И СЕТЬ!

Во-первых, на этот раз вместо тренировки с образцами данных, которые мы сделали, я буду тренироваться на реальных данных ЭЭГ, начиная с весов по умолчанию.

tutorial_model = torch.load("/home/tutorial_model_default_state")
## Define a learning function, needs to be reinitialized every load
optimizer = torch.optim.Adam(tutorial_model.parameters(), lr = learning_rate)
## Use our training procedure with the sample data
print("Below is the loss graph for dataset training session")
train_network(training_data, labels, iterations = 50)

Этот код загружает предварительно обученную модель PyTorch из файла, определяет функцию обучения (optimizer) для модели, а затем использует обучающие данные и метки для обучения модели с использованием определенной функции обучения.

Вот более подробное объяснение того, что делает каждая строка кода:

  1. tutorial_model = torch.load("/home/tutorial_model_default_state"): эта строка загружает предварительно обученную модель PyTorch из файла, расположенного по указанному пути. Модель будет храниться в переменной tutorial_model.
  2. optimizer = torch.optim.Adam(tutorial_model.parameters(), lr = learning_rate): Эта строка создает функцию обучения под названием optimizer с использованием алгоритма оптимизации Адама. optimizer будет использоваться для обновления параметров модели во время обучения. Переменная learning_rate указывает размер шага, который следует использовать при обновлении параметров модели.
  3. train_network(training_data, labels, iterations = 50): эта строка вызывает функцию с именем train_network() и передает ей обучающие данные, метки и количество итераций для обучения. Функция train_network(), скорее всего, обучает модель, используя optimizer и предоставленные данные. Оператор print перед этой строкой указывает, что потеря (мера того, насколько хорошо модель соответствует данным) будет отображаться в процессе обучения.

ПОСЛЕДНИЙ шаг, который я обещаю → классификация набора данных

# Classify our positive test dataset and print the results
classification_1 = tutorial_model(positive_testing_data)
for index, value in enumerate(classification_1.data.tolist()):
  print("P300 Positive Classification {1}: {0:.2f}%".format(value[0] * 100, index + 1))
print()
# Classify our negative test dataset and print the results
classification_2 = tutorial_model(negative_testing_data)
for index, value in enumerate(classification_2.data.tolist()):
  print("P300 Negative Classification {1}: {0:.2f}%".format(value[0] * 100, index + 1))
P300 Positive Classification 1: 100.00%
P300 Positive Classification 2: 99.94%
P300 Positive Classification 3: 100.00%
P300 Negative Classification 1: 99.92%
P300 Negative Classification 2: 0.04%
P300 Negative Classification 3: 0.00%
  • classification_1 = tutorial_model(positive_testing_data): в этой строке используется tutorial_model для классификации выборок в тензоре positive_testing_data как событий P300 или не-P300. Результаты классификации сохраняются в тензоре classification_1.
  • for index, value in enumerate(classification_1.data.tolist()):: Эта строка запускает цикл, который перебирает каждый элемент тензора classification_1.data и выводит результаты классификации для каждой выборки. Функция enumerate() используется для циклического перебора элементов тензора и отслеживания текущего индекса. Метод tolist() используется для преобразования тензора в список.
  • print("P300 Positive Classification {1}: {0:.2f}%".format(value[0] * 100, index + 1)): В этой строке печатаются результаты классификации для текущего образца. Метод format() используется для включения в строку значения классификации (value[0]) и индекса текущей выборки (index + 1). Значение классификации умножается на 100, чтобы выразить его в процентах.
  • classification_2 = tutorial_model(negative_testing_data): эта строка похожа на первую классификацию, но использует тензор negative_testing_data вместо тензора positive_testing_data.
  • Остальная часть кода аналогична предыдущему циклу, но он перебирает тензор classification_2 и печатает результаты классификации для отрицательного тестового набора данных.

В целом этот код использует обученную модель для классификации образцов в тестовых наборах данных и печати результатов классификации. Результаты выражаются в процентах, причем более высокий процент указывает на более высокий уровень достоверности классификации.

Вот и все!

Еда на вынос -›

  1. Благодаря написанию и исследованию этой статьи я получил более глубокое понимание машинного обучения и его приложений для анализа и классификации биологических данных, таких как ЭЭГ.
  2. И машинное обучение в исследованиях в области неврологии и то, как оно может дать представление об активности и поведении мозга.
  3. Кроме того, я также приобрел практические навыки в реализации алгоритмов машинного обучения для анализа данных ЭЭГ и пришел к пониманию проблем и ограничений таких подходов + я обнаружил потенциальные приложения и последствия машинного обучения для обнаружения и понимания реакции удивления у испытуемых.
  4. Проходя через этот процесс, мое личное понимание и знание машинного обучения и его приложений в нейробиологии значительно улучшилось. Мое критическое мышление и аналитические навыки улучшились, поскольку я оценил сильные и слабые стороны различных подходов к машинному обучению и рассмотрел их потенциальное влияние на исследования и практику.

В целом, пока я писал и исследовал эту статью, я был на диком пути личного роста и развития. Я смог придумать свои собственные идеи и учиться на чужих работах, и это был настоящий взрыв!