ADDI Alzheimers Detection Challenge
F1:0.376 Image pixel representation + CNN Model Baseline
Create Image-Pixel like representation features and Convolution Neural Network based baseline
Convert Clock features to Image Pixel features
Motivation
The digit features are encoded as numbers,distances, angle but it does not represent the actual positions or the spatial information of the actual digits in a clock. Representing the same in a 2-d plane seems a more natural way to represent the digits and the deviations of the drawings from the actual positions. While one option could be to use the hand drawn clocks (this is not directly provided but could be derived from the numerical features). However the purpose of the numerical features is to represent the data in an uniform manner as the drawings can be quiet imperfect and differences less visible. The pixel based representation aims to get the best out of both these attributes -
- Having an uniform measurable numerical representation
- Keeping the spatial and positions in 2-d space intact
Feature Engineering Approach
A clock can be represented as a 7*7
features by representing the clock positions as follows
************************************
* * * 12 * * *
************************************
* * 11 * 1 * *
************************************
* 10 * * * 2 *
************************************
9 * * * * * 3
************************************
* 8 * * * 4 *
************************************
* * 7 * 5 * *
************************************
* * * 6 * * *
************************************
We can replace this based on the features missing_digit_*
features by hot encoding the presence of digits
For e.g. the features
missing_digit_1 = 0
missing_digit_2 = 0
missing_digit_3 = 1
missing_digit_4 = 1
missing_digit_5 = 1
missing_digit_6 = 0
missing_digit_7 = 0
missing_digit_8 = 0
missing_digit_9 = 0
missing_digit_10 = 0
missing_digit_11 = 0
missing_digit_12 = 0
can be represented as
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 1
************************************
0 0 0 0 0 1 0
************************************
0 0 0 0 1 0 0
************************************
0 0 0 0 0 0 0
************************************
further if the feature sequence_flag_ccw
is 1, meaning the clock has been drawn counter clockwise, then the same clock can be perceived as
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
1 0 0 0 0 0 0
************************************
0 1 0 0 0 0 0
************************************
0 0 1 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
now the feature final_rotational_angle
can further shift our clock positions. for e.g. if final_rotational_angle
= 60°
, then the clock shifts by 2 positions and becomes
************************************
0 0 0 1 0 0 0
************************************
0 0 1 0 1 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
0 0 0 0 0 0 0
************************************
We use this feature represented by 7*7
and upscaled scaled by 4 times to get the size 28*28
.
Note that this image like pixel representation which matches with the size of the mnist gray scale images of dimension 1*28*28
We now apply the standard convolution models to this pixel representation on this feature to obtain a benchmark for the cnn model.
We show below a simple missing digit feature with rotation and anti-clockwise is able to get a reasonable learning to achieve a baseline F-Score of 0.376
Future Ideas
Additional digit features like the euc dist digit, distance from the centre, bounding box - area, width and height could be concatenated on the z axis to obtain 3-d image like representation similar to the image used on rgb images. Likewise hour and minute hand features could also be hard coded on the correct pixels based on their orientation and distance via hot encoding. A thinly drawn item could be represented via a transparency number from 0 to 1 (similar to A in RGBA) Centre Dot Feature could just represent the pixel section in the centre of the clock. The eclipse to circle ratio features can be bucketed and be used to shift positions of digits to nearby pixels. Similar techniques could also be applied on the count_defects, percentage inside eclipse, top left, right, bottom area etc.
What is the notebook about?¶
The challenge is to use the features extracted from the Clock Drawing Test to build an automated and algorithm to predict whether each participant is one of three phases:
1) Pre-Alzheimer’s (Early Warning) 2) Post-Alzheimer’s (Detection) 3) Normal (Not an Alzheimer’s patient)
In machine learning terms: this is a 3-class classification task.
How to use this notebook? 📝¶
- Update the config parameters. You can define the common variables here
Variable | Description |
---|---|
AICROWD_DATASET_PATH |
Path to the file containing test data (The data will be available at /ds_shared_drive/ on aridhia workspace). This should be an absolute path. |
AICROWD_PREDICTIONS_PATH |
Path to write the output to. |
AICROWD_ASSETS_DIR |
In case your notebook needs additional files (like model weights, etc.,), you can add them to a directory and specify the path to the directory here (please specify relative path). The contents of this directory will be sent to AIcrowd for evaluation. |
AICROWD_API_KEY |
In order to submit your code to AIcrowd, you need to provide your account's API key. This key is available at https://www.aicrowd.com/participants/me |
- Installing packages. Please use the Install packages 🗃 section to install the packages
- Training your models. All the code within the Training phase ⚙️ section will be skipped during evaluation. Please make sure to save your model weights in the assets directory and load them in the predictions phase section
Setup AIcrowd Utilities 🛠¶
We use this to bundle the files for submission and create a submission on AIcrowd. Do not edit this block.
!pip install -q -U aicrowd-cli
%load_ext aicrowd.magic
!pip install sweetviz
!pip install -U jupyter
import sweetviz as sv
import os
# Please use the absolute for the location of the pip install Shapelydataset.
# Or you can use relative path with `os.getcwd() + "test_data/validation.csv"`
AICROWD_DATASET_PATH = os.getenv("DATASET_PATH", "/ds_shared_drive/validation.csv")
AICROWD_PREDICTIONS_PATH = os.getenv("PREDICTIONS_PATH", "predictions.csv")
AICROWD_ASSETS_DIR = "assets"
#!pip install ipywidgets
#!jupyter nbextension enable --py widgetsnbextension
#!conda install -y jupyterlab_widgets
#!pip install aquirdturtle_collapsible_headings
Install packages 🗃¶
Please add all pacakage installations in this section
!pip install numpy pandas
!pip install -U imbalanced-learn
!pip install xgboost
!pip install lightgbm
!pip install catboost
!pip install tensorflow
!pip install shap
!pip install torch torchvision torchaudio
Define preprocessing code 💻¶
The code that is common between the training and the prediction sections should be defined here. During evaluation, we completely skip the training section. Please make sure to add any common logic between the training and prediction sections here.
Import common packages¶
Please import packages that are common for training and prediction phases here.
import numpy as np
import pandas as pd
import joblib
import matplotlib.pyplot as plt
from collections import Counter
import torch
from tqdm.notebook import tqdm
%matplotlib inline
target_col = "diagnosis"
key_col = "row_id"
cat_cols = ['intersection_pos_rel_centre']
seed = 2021
target_values = ["normal", "post_alzheimer", "pre_alzheimer"]
scale = 4
translator2d = {1: [4,1], 2 : [5,2], 3: [6,3], 4:[5,4] ,5:[4,5] ,6:[3,6] ,7:[2,5] ,8:[1,4] ,9:[0,3],10:[1,2], 11:[2,1] , 12:[3,0]}
ccw_translate = {i: 12 - i for i in range(1,13,1)}
ccw_translate[12] = 12
translator2d_ccw = {ccw_translate[k]:v for k,v in translator2d.items()}
import torchvision
import torch, torch.nn as nn
import torchvision.models as models
from torch.autograd import Variable
import torch.nn.functional as F
import torch.optim as optim
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
np.random.seed(0)
torch.manual_seed(0)
z_dim = 1 # image_repr_features.shape[1]
n_classes = 3
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(z_dim, 10, kernel_size=5)
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
self.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, n_classes)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
return F.softmax(x)
model = Net()
Training phase ⚙️¶
You can define your training code here. This sections will be skipped during evaluation.
train = pd.read_csv('/ds_shared_drive/train.csv')
# valid = pd.read_csv('/ds_shared_drive/validation.csv')
# valid_truth = pd.read_csv('/ds_shared_drive/validation_ground_truth.csv')
# valid_all = valid.merge(valid_truth,how='left')
# train = pd.concat([train, valid_all],axis = 0)
train = train[train[target_col].isin(target_values)].copy().reset_index(drop=True)
# Remove Constant Columns
train = train.loc[:, (train != train.iloc[0]).any()]
features = train.columns[1:-1].to_list()
numeric_features = [c for c in features if c not in cat_cols]
for c in numeric_features:
train[c] = train[c].astype(float)
print(train[target_col].value_counts())
print(train.shape)
df_pos = train[train[target_col].isin(target_values[1:])]
nb_pos = df_pos.shape[0]
nb_neg = nb_pos*2
df_neg = train[train[target_col] == "normal"].sample(n=nb_neg, random_state=seed)
# df_neg = df_normal
df_samples = pd.concat([df_pos, df_neg]).sample(frac=1).reset_index(drop=True)
# df_samples = train
df_samples.shape
df_samples.shape
print(cat_cols)
for c in cat_cols:
df_samples[c].fillna("NA", inplace=True)
df_dummies = pd.get_dummies(df_samples[cat_cols], columns=cat_cols, dummy_na=True).add_prefix('CAT_')
dummy_cols = df_dummies.columns.to_list()
print(dummy_cols)
df_samples = pd.concat([df_samples, df_dummies], axis=1)
df_samples['cnt_NaN'] = df_samples[numeric_features].isna().sum(axis=1)
# df_samples.fillna(-1, inplace=True)
model_features = df_samples.columns.to_list()
model_features = [c for c in model_features if c not in [key_col, target_col] + cat_cols]
print(len(model_features))
X_train = df_samples[model_features]
y_train_all = df_samples[target_col].map(dict(zip(target_values, list(range(len(target_values))))))
df_samples[target_col].value_counts()
image_repr_features = None
for n,row in tqdm(X_train.iterrows()):
image_repr = np.zeros((1,7,7))
centre_repr = np.zeros((1,7,7))
for i in range(1,13):
col = f'missing_digit_{i}'
present = row[col]
if present:
ccw_flag = row["sequence_flag_ccw"] == 1
translator = translator2d
if ccw_flag:
translator = translator2d_ccw
pos = translator[i]
image_repr[0,pos[1],pos[0]] = 1
image_repr = np.kron(image_repr, np.ones((scale,scale)))
# rot_angle_z = image_repr * row["final_rotation_angle"]/360
# centre_dot = row["centre_dot_detect"]
# if centre_dot == 1:
# centre_repr[0,3,3] = 1
# centre_repr = np.kron(centre_repr, np.ones((scale,scale)))
# image_repr = np.vstack([image_repr,rot_angle_z,centre_repr])
image_repr = np.expand_dims(image_repr, axis = 0)
if n > 0:
image_repr_features = np.vstack([image_repr_features,image_repr])
else:
image_repr_features = image_repr
image_repr_features_no_nan = image_repr_features # np.nan_to_num(image_repr_features)
model = Net()
train_x, val_x, train_y, val_y = train_test_split(image_repr_features_no_nan, y_train_all, test_size = 0.1)
(train_x.shape, train_y.shape), (val_x.shape, val_y.shape)
train_x = torch.from_numpy(train_x).float()
val_x = torch.from_numpy(val_x).float()
train_y = torch.from_numpy(train_y.values).long()
val_y = torch.from_numpy(val_y.values).long()
def train(epoch):
tr_loss = 0
# getting the training set
x_train, y_train = Variable(train_x), Variable(train_y)
# getting the validation set
x_val, y_val = Variable(val_x), Variable(val_y)
# clearing the Gradients of the model parameters
optimizer.zero_grad()
# prediction for training and validation set
output_train = model(x_train)
output_val = model(x_val)
# computing the training and validation loss
loss_train = criterion(output_train, y_train)
loss_val = criterion(output_val, y_val)
train_losses.append(loss_train)
val_losses.append(loss_val)
# computing the updated weights of all the model parameters
loss_train.backward()
optimizer.step()
tr_loss = loss_train.item()
if epoch%2 == 0:
# printing the validation loss
print('Epoch : ',epoch+1, '\t', 'loss :', loss_val)
# defining the optimizer
optimizer = optim.Adam(model.parameters(), lr=0.005)
# defining the loss function
criterion = nn.CrossEntropyLoss()
# defining the number of epochs
n_epochs = 15
# empty list to store training losses
train_losses = []
# empty list to store validation losses
val_losses = []
# training the model
for epoch in range(n_epochs):
train(epoch)
# prediction for training set
with torch.no_grad():
output = model(train_x)
softmax = output.cpu()
prob = list(softmax.numpy())
predictions = np.argmax(prob, axis=1)
print(predictions.sum())
# f1 score on training set
f1_score(train_y.numpy(), predictions, average='weighted')
# load your data
Train your model¶
train_x, val_x, train_y, val_y = train_test_split(image_repr_features_no_nan, y_train_all, test_size = 0.1)
(train_x.shape, train_y.shape), (val_x.shape, val_y.shape)
train_x = torch.from_numpy(train_x).float()
val_x = torch.from_numpy(val_x).float()
train_y = torch.from_numpy(train_y.values).long()
val_y = torch.from_numpy(val_y.values).long()
def train(epoch):
tr_loss = 0
# getting the training set
x_train, y_train = Variable(train_x), Variable(train_y)
# getting the validation set
x_val, y_val = Variable(val_x), Variable(val_y)
# clearing the Gradients of the model parameters
optimizer.zero_grad()
# prediction for training and validation set
output_train = model(x_train)
output_val = model(x_val)
# computing the training and validation loss
loss_train = criterion(output_train, y_train)
loss_val = criterion(output_val, y_val)
train_losses.append(loss_train)
val_losses.append(loss_val)
# computing the updated weights of all the model parameters
loss_train.backward()
optimizer.step()
tr_loss = loss_train.item()
if epoch%2 == 0:
# printing the validation loss
print('Epoch : ',epoch+1, '\t', 'loss :', loss_val)
model = Net()
# defining the optimizer
optimizer = optim.Adam(model.parameters(), lr=0.005)
# defining the loss function
criterion = nn.CrossEntropyLoss()
# defining the number of epochs
n_epochs = 15
# empty list to store training losses
train_losses = []
# empty list to store validation losses
val_losses = []
# training the model
for epoch in range(n_epochs):
train(epoch)
# prediction for training set
with torch.no_grad():
output = model(train_x)
softmax = output.cpu()
prob = list(softmax.numpy())
predictions = np.argmax(prob, axis=1)
print(predictions.sum())
# f1 score on training set
f1_score(train_y.numpy(), predictions, average='weighted')
Save your trained model¶
filename = f'{AICROWD_ASSETS_DIR}/model_checkpoint'
check_point = {'params': model.state_dict(),
'optimizer': optimizer.state_dict()}
torch.save(check_point, filename)
Prediction phase 🔎¶
Please make sure to save the weights from the training section in your assets directory and load them in this section
file = f'{AICROWD_ASSETS_DIR}/model_checkpoint'
check_point = torch.load(file)
model.load_state_dict(check_point['params'])
Load test data¶
test_data = pd.read_csv(AICROWD_DATASET_PATH)
test_data.head()
image_repr_features_test = None
for n,row in tqdm(test_data.iterrows()):
image_repr = np.zeros((1,7,7))
centre_repr = np.zeros((1,7,7))
for i in range(1,13):
col = f'missing_digit_{i}'
present = row[col]
if present:
ccw_flag = row["sequence_flag_ccw"] == 1
translator = translator2d
if ccw_flag:
translator = translator2d_ccw
pos = translator[i]
image_repr[0,pos[1],pos[0]] = 1
image_repr = np.kron(image_repr, np.ones((scale,scale)))
# rot_angle_z = image_repr * row["final_rotation_angle"]/360
# centre_dot = row["centre_dot_detect"]
# if centre_dot == 1:
# centre_repr[0,3,3] = 1
# centre_repr = np.kron(centre_repr, np.ones((scale,scale)))
# image_repr = np.vstack([image_repr,rot_angle_z,centre_repr])
image_repr = np.expand_dims(image_repr, axis = 0)
if n > 0:
image_repr_features_test = np.vstack([image_repr_features_test,image_repr])
else:
image_repr_features_test = image_repr
image_repr_features_test_no_nan = image_repr_features_test # np.nan_to_num(image_repr_features)
# prediction for training set
test_x = torch.from_numpy(image_repr_features_test_no_nan).float()
with torch.no_grad():
output = model(test_x)
softmax = output.cpu()
prob = list(softmax.numpy())
predictions = np.argmax(prob, axis=1)
print(predictions.sum())
Generate predictions¶
predictions = {
"row_id": test_data["row_id"].values,
"normal_diagnosis_probability": [x[0] for x in prob],
"post_alzheimer_diagnosis_probability": [x[1] for x in prob],
"pre_alzheimer_diagnosis_probability": [x[2] for x in prob],
}
predictions_df = pd.DataFrame.from_dict(predictions)
predictions_df.head()
Save predictions 📨¶
predictions_df.to_csv(AICROWD_PREDICTIONS_PATH, index=False)
Submit to AIcrowd 🚀¶
NOTE: PLEASE SAVE THE NOTEBOOK BEFORE SUBMITTING IT (Ctrl + S)
!DATASET_PATH=$AICROWD_DATASET_PATH \
aicrowd notebook submit \
--assets-dir $AICROWD_ASSETS_DIR \
--challenge addi-alzheimers-detection-challenge
Content
Comments
You must login before you can post a comment.