Concentration Prediction with EEG#

ตัวอย่างการใช้งาน Neural network จากบทที่แล้วเราได้ทำความรู้จักกับ Neural network(NN)ในเบื้องต้นไปแล้วใน notebook นี้เราจะลองสร้าง NN เพื่อดูว่าผู้เข้าทดสอบคนไหนสับสนกับบทเรียนจากสัญญาณ EEG และข้อมูลอื่นๆของผู้เข้าทดสอบ

Install required library and download dataset#

ใช้ API ของkaggleเพื่อ download dataset

!pip install kaggle
Requirement already satisfied: kaggle in /usr/local/lib/python3.10/dist-packages (1.5.13)
Requirement already satisfied: six>=1.10 in /usr/local/lib/python3.10/dist-packages (from kaggle) (1.16.0)
Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from kaggle) (2023.5.7)
Requirement already satisfied: python-dateutil in /usr/local/lib/python3.10/dist-packages (from kaggle) (2.8.2)
Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from kaggle) (2.27.1)
Requirement already satisfied: tqdm in /usr/local/lib/python3.10/dist-packages (from kaggle) (4.65.0)
Requirement already satisfied: python-slugify in /usr/local/lib/python3.10/dist-packages (from kaggle) (8.0.1)
Requirement already satisfied: urllib3 in /usr/local/lib/python3.10/dist-packages (from kaggle) (1.26.16)
Requirement already satisfied: text-unidecode>=1.3 in /usr/local/lib/python3.10/dist-packages (from python-slugify->kaggle) (1.3)
Requirement already satisfied: charset-normalizer~=2.0.0 in /usr/local/lib/python3.10/dist-packages (from requests->kaggle) (2.0.12)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests->kaggle) (3.4)
# อัพโหลด kaggle.json ที่หาได้จาก https://www.kaggle.com/settings (ไปที่หน้านี้แล้วกด "Create New Token" ใน section API)
!cp kaggle.json /root/.kaggle/
!chmod 600 /root/.kaggle/kaggle.json
!kaggle datasets download -d wanghaohan/confused-eeg
!unzip confused-eeg.zip
cp: cannot stat 'kaggle.json': No such file or directory
chmod: cannot access '/root/.kaggle/kaggle.json': No such file or directory
Traceback (most recent call last):
  File "/usr/local/bin/kaggle", line 5, in <module>
    from kaggle.cli import main
  File "/usr/local/lib/python3.10/dist-packages/kaggle/__init__.py", line 23, in <module>
    api.authenticate()
  File "/usr/local/lib/python3.10/dist-packages/kaggle/api/kaggle_api_extended.py", line 164, in authenticate
    raise IOError('Could not find {}. Make sure it\'s located in'
OSError: Could not find kaggle.json. Make sure it's located in /root/.kaggle. Or use the environment method.
unzip:  cannot find or open confused-eeg.zip, confused-eeg.zip.zip or confused-eeg.zip.ZIP.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader

อ่านข้อมูลในส่วนของ EEG ก่อนโดยในไฟล์นี้จะประกอบไปด้วย

  • SubjectID, VideoID: ID ของวิชาและวิดีโอ

  • Attention: ระดับความใส่ใจ

  • Mediation: ระดับสมาธิ

  • Raw: สัญญาณดิบ

  • EEG ในแต่ละคลื่นความถี่

  • predefinedlabel: ระดับความสับสนที่คาดเดา (ไม่ได้ใช้ในบทนี้) 0 คือไม่เข้าใจ 1 คือเข้าใจ

  • user-definedlabeln: ของระดับความสับสนของนักเรียนหลังจากเรียน 0 คือไม่เข้าใจ 1 คือเข้าใจ

eeg_df = pd.read_csv('/EEG_data.csv')
eeg_df.head()
SubjectID VideoID Attention Mediation Raw Delta Theta Alpha1 Alpha2 Beta1 Beta2 Gamma1 Gamma2 predefinedlabel user-definedlabeln
0 0.0 0.0 56.0 43.0 278.0 301963.0 90612.0 33735.0 23991.0 27946.0 45097.0 33228.0 8293.0 0.0 0.0
1 0.0 0.0 40.0 35.0 -50.0 73787.0 28083.0 1439.0 2240.0 2746.0 3687.0 5293.0 2740.0 0.0 0.0
2 0.0 0.0 47.0 48.0 101.0 758353.0 383745.0 201999.0 62107.0 36293.0 130536.0 57243.0 25354.0 0.0 0.0
3 0.0 0.0 47.0 57.0 -5.0 2012240.0 129350.0 61236.0 17084.0 11488.0 62462.0 49960.0 33932.0 0.0 0.0
4 0.0 0.0 44.0 53.0 -8.0 1005145.0 354328.0 37102.0 88881.0 45307.0 99603.0 44790.0 29749.0 0.0 0.0

Add demographic data#

อีกไฟล์จะเป็นไฟล์ที่เป็นข้อมูลของผู้เข้ารับการทดสอบโดยจะประกอบไปด้วย

  • ID ของนักเรียน

  • เพศ

  • เชื้อชาติ

  • อายุ

dem_df = pd.read_csv('/demographic_info.csv')

Preprocess data#

โดยภาพรวมเราจะนำสองตารางนี้มารวมกันก่อนจะทำการจัดเตรียมในเบื้องต้นเพื่อนำมาลองใช้กับ Neural Network

เปลี่ยนชื่อเพื่อความง่ายในการใช้งานโดยใช้ .rename()

dem_df = dem_df.rename(columns = {'subject ID' : 'SubjectID'})
dem_df['SubjectID'] = dem_df['SubjectID'].astype(np.float64)
dem_df
SubjectID age ethnicity gender
0 0.0 25 Han Chinese M
1 1.0 24 Han Chinese M
2 2.0 31 English M
3 3.0 28 Han Chinese F
4 4.0 24 Bengali M
5 5.0 24 Han Chinese M
6 6.0 24 Han Chinese M
7 7.0 25 Han Chinese M
8 8.0 25 Han Chinese M
9 9.0 24 Han Chinese F

รวม EEG กับ demography#

โดยใช้ .merge(dem_df, how='inner', on='SubjectIDซึ่งการ merge แบบ inner จะเลือกเฉพาะ row มี่มีค่าที่เราต้องการ(SubjectID)ทั้งคู่เท่านั้น

eeg_df = eeg_df.merge(dem_df, how = 'inner', on = 'SubjectID')
eeg_df
SubjectID VideoID Attention Mediation Raw Delta Theta Alpha1 Alpha2 Beta1 Beta2 Gamma1 Gamma2 predefinedlabel user-definedlabeln age ethnicity gender
0 0.0 0.0 56.0 43.0 278.0 301963.0 90612.0 33735.0 23991.0 27946.0 45097.0 33228.0 8293.0 0.0 0.0 25 Han Chinese M
1 0.0 0.0 40.0 35.0 -50.0 73787.0 28083.0 1439.0 2240.0 2746.0 3687.0 5293.0 2740.0 0.0 0.0 25 Han Chinese M
2 0.0 0.0 47.0 48.0 101.0 758353.0 383745.0 201999.0 62107.0 36293.0 130536.0 57243.0 25354.0 0.0 0.0 25 Han Chinese M
3 0.0 0.0 47.0 57.0 -5.0 2012240.0 129350.0 61236.0 17084.0 11488.0 62462.0 49960.0 33932.0 0.0 0.0 25 Han Chinese M
4 0.0 0.0 44.0 53.0 -8.0 1005145.0 354328.0 37102.0 88881.0 45307.0 99603.0 44790.0 29749.0 0.0 0.0 25 Han Chinese M
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
12806 9.0 9.0 64.0 38.0 -39.0 127574.0 9951.0 709.0 21732.0 3872.0 39728.0 2598.0 960.0 1.0 0.0 24 Han Chinese F
12807 9.0 9.0 61.0 35.0 -275.0 323061.0 797464.0 153171.0 145805.0 39829.0 571280.0 36574.0 10010.0 1.0 0.0 24 Han Chinese F
12808 9.0 9.0 60.0 29.0 -426.0 680989.0 154296.0 40068.0 39122.0 10966.0 26975.0 20427.0 2024.0 1.0 0.0 24 Han Chinese F
12809 9.0 9.0 60.0 29.0 -84.0 366269.0 27346.0 11444.0 9932.0 1939.0 3283.0 12323.0 1764.0 1.0 0.0 24 Han Chinese F
12810 9.0 9.0 64.0 29.0 -49.0 1164555.0 1184366.0 50014.0 124208.0 10634.0 445383.0 22133.0 4482.0 1.0 0.0 24 Han Chinese F

12811 rows × 18 columns

Convert to one-hot encoding#

จะเปลี่ยนจากข้อมูลที่เป็น “ประเภท” เป็น ตัวเลข โดยใช้ .get_dummies()
EX: English, Chinese, Other -> (0,0) (0,1), (0,1)
Male/Female -> 1 & 0

eeg_df = pd.get_dummies(eeg_df)
eeg_df.head()
SubjectID VideoID Attention Mediation Raw Delta Theta Alpha1 Alpha2 Beta1 ... Gamma1 Gamma2 predefinedlabel user-definedlabeln age ethnicity_Bengali ethnicity_English ethnicity_Han Chinese gender_F gender_M
0 0.0 0.0 56.0 43.0 278.0 301963.0 90612.0 33735.0 23991.0 27946.0 ... 33228.0 8293.0 0.0 0.0 25 0 0 1 0 1
1 0.0 0.0 40.0 35.0 -50.0 73787.0 28083.0 1439.0 2240.0 2746.0 ... 5293.0 2740.0 0.0 0.0 25 0 0 1 0 1
2 0.0 0.0 47.0 48.0 101.0 758353.0 383745.0 201999.0 62107.0 36293.0 ... 57243.0 25354.0 0.0 0.0 25 0 0 1 0 1
3 0.0 0.0 47.0 57.0 -5.0 2012240.0 129350.0 61236.0 17084.0 11488.0 ... 49960.0 33932.0 0.0 0.0 25 0 0 1 0 1
4 0.0 0.0 44.0 53.0 -8.0 1005145.0 354328.0 37102.0 88881.0 45307.0 ... 44790.0 29749.0 0.0 0.0 25 0 0 1 0 1

5 rows × 21 columns

Data cleaning#

ทำการนำcolumn ที่ไม่ต้องการออกไปเช่น SubjectID, VideoID เพราะเราต้องการจะวัดความเข้าใจจากนักเรียนดังนั้นการที่มี วิชา และ video ที่เป็นสิ่งกระตุ้นให้เกิดความไม่เข้าใจนั้นอาจจะทำให้โมเดลของเราคาดเดาผลลัพธ์จากทั้งสอง features แทนที่จะใช้ข้อมูลของตัวนักเรียนเอง

รวมถึง predefinedlabel ที่ไม่จำเป็นและ gender_F ที่เป็น columnsที่เกินมาจากการทำ one hot encoding โดยใช่ .drop()

eeg_df = eeg_df.drop(['SubjectID', 'VideoID', 'predefinedlabel', ' gender_F'], axis = 1)

Mediation and Attention มีค่าเป็น 0 ซึ่งเป็นข้อผิดพลาดตามที่ผู้เขียนกล่าวในการอภิปรายดังนั้นเราจะเลือกข้อมูลที่ > 0 เท่านั้น

eeg_df = eeg_df[eeg_df['Attention'] > 0.0]
eeg_df = eeg_df[eeg_df['Mediation'] > 0.0]

label มีเพียง 0,1

eeg_df['user-definedlabeln'].unique()
array([0., 1.])

Get the arrays from dataset#

แยก column ที่เราต้องการทำนายออกมา

X = np.array(eeg_df.drop(['user-definedlabeln'], axis = 1))
y = np.array(eeg_df['user-definedlabeln'])

Data preprocessing#

print(X.min())
print(X.max())
-2048.0
3964663.0

เราจะเห็นว่าค่าแต่ละค่าของ feature นั้นแตกต่างกันมากเนื่องจากแต่ละ featureใช่คนละ scale ดังนั้นเราจึงต้องใช้ StandardScalerเข้ามาช่วยให้ข้อมูลนั้นอยู่ใน scale เดียวกัน

from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(X)
print(X.min())
print(X.max())
-15.829161155386538
29.216116594451524

Split data#

แยกข้อมูลเป็น train-tests โดย train_test_split

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 1)

เช็คขนาดนของ X, y

print(X_train.shape)
print(y_train.shape)
(9110, 16)
(9110,)
print(X_test.shape)
print(y_test.shape)
(2278, 16)
(2278,)

สร้าง NN ของเราขึ้นมาตาที่เคยเรียนในบทก่อนหน้า

class StudentDataset(Dataset):
    def __init__(self, X, y):
        # เปลี่ยนให้อยู่ในรูป tensor
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.float32).reshape(-1, 1)

    def __len__(self):
        return self.y.shape[0]

    def __getitem__(self, index):
        features = self.X[index]
        label = self.y[index]

        return features, label
# สร้าง dataloader สำหรับเทรน
train_dataset = StudentDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=100, shuffle=True)

Create a model#

class EEGNet(nn.Module):
    # สร้าง Neural Network ที่มี 2 Linear layer
    def __init__(self, input_size):
        super().__init__()
        self.fc1 = nn.Linear(input_size, 4)
        self.fc2 = nn.Linear(4, 1)

    # สร้าง forward porpagation
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        x = F.sigmoid(x)
        return x
model = EEGNet(16)
model
EEGNet(
  (fc1): Linear(in_features=16, out_features=4, bias=True)
  (fc2): Linear(in_features=4, out_features=1, bias=True)
)

เรากำหนดให้โมเดลเทรนข้อมูลไป 10 epoch โดยใช้ for loop
จากนั้นในแต่ละ batch จะทำการ

  1. ล้าง gradient ของ optimizer ใน iteration ก่อนหน้าด้วย (optimizer.zero_grad())

  2. ผ่านข้อมูลเข้าไปในโมเดล

  3. คำนวน loss โดย (criterion(outputs, labels)) จะเป็นการเทียบระหว่าง output & labels

  4. และจำหา gradient และ ปรับ parameter โดย (loss.backward()) และ (optimizer.step()) ตามลำดับ

criterion = nn.BCELoss()  # binary cross entropy
optimizer = optim.Adam(model.parameters(), lr=0.001)
for epoch in range(10):
    running_loss = 0.0
    for i, data in enumerate(train_loader, 0):
        inputs, labels = data
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        if i % 1000 == 0:
            print(f"[epoch: {epoch + 1} ] loss: {running_loss / 1:.3f}")
            running_loss = 0.0

print("Finished Training")
[epoch: 1 ] loss: 0.720
[epoch: 2 ] loss: 0.688
[epoch: 3 ] loss: 0.674
[epoch: 4 ] loss: 0.687
[epoch: 5 ] loss: 0.654
[epoch: 6 ] loss: 0.698
[epoch: 7 ] loss: 0.648
[epoch: 8 ] loss: 0.665
[epoch: 9 ] loss: 0.656
[epoch: 10 ] loss: 0.622
Finished Training
from sklearn.metrics import accuracy_score

เช็ค accuracy ของผลลัพธ์ที่โมเดลทำนายได้ torch.no_grad() จะเป็นการบอกว่าไม่ต้องเก็บ gradient ระหว่างทำงาน ก่อนที่จะนำมาเปรียบเทียบจะต้องนำมา round() เสียก่อนเพื่อให้ค่าความน่าจะเป็นที่ออกมาเป็นค่า 0,1

with torch.no_grad():
    y_pred = model(torch.tensor(X_test, dtype=torch.float32))
    accuracy = accuracy_score(y_test, y_pred.round())
    print(f"Accuracy {accuracy}")
Accuracy 0.6198419666374012

ในตัวอย่างนี้เราค้องการที่จะแสดงให้เห็นที่ process ในการเขียนและใช้งาน Neural networkในเบื้องต้น แต่ในการใช้งานจริงนั้นจะต้องมีการ design และปรับปรุง hyperparameters ต่างๆเพื่อให้เหมาะสมกับการใช่งานรวมถึงการจัดการกับข้อมูลก่อนนำมาเทรน (preprocessing) ก็เป็นขั้นตอนที่จำเป็นเช่นกัน



ผู้จัดเตรียม code ใน tutorial: นาย กรวิชญ์ โชตยาภา