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 จะทำการ
ล้าง gradient ของ optimizer ใน iteration ก่อนหน้าด้วย (
optimizer.zero_grad()
)ผ่านข้อมูลเข้าไปในโมเดล
คำนวน loss โดย (
criterion(outputs, labels)
) จะเป็นการเทียบระหว่าง output & labelsและจำหา 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: นาย กรวิชญ์ โชตยาภา