--- a +++ b/eeg_learn_functions.py @@ -0,0 +1,496 @@ +from __future__ import print_function +import time + +import numpy as np +np.random.seed(1234) +from functools import reduce +import math as m + +import scipy.io +#import theano +#import theano.tensor as T + +from scipy.interpolate import griddata +from sklearn.preprocessing import scale +#from utils import augment_EEG, cart2sph, pol2cart + +#import lasagne +# from lasagne.layers.dnn import Conv2DDNNLayer as ConvLayer +#from lasagne.layers import Conv2DLayer, MaxPool2DLayer, InputLayer +#from lasagne.layers import DenseLayer, ElemwiseMergeLayer, FlattenLayer +#from lasagne.layers import ConcatLayer, ReshapeLayer, get_output_shape +#from lasagne.layers import Conv1DLayer, DimshuffleLayer, LSTMLayer, SliceLayer + + +def azim_proj(pos): + """ + Computes the Azimuthal Equidistant Projection of input point in 3D Cartesian Coordinates. + Imagine a plane being placed against (tangent to) a globe. If + a light source inside the globe projects the graticule onto + the plane the result would be a planar, or azimuthal, map + projection. + + :param pos: position in 3D Cartesian coordinates + :return: projected coordinates using Azimuthal Equidistant Projection + """ + [r, elev, az] = cart2sph(pos[0], pos[1], pos[2]) + return pol2cart(az, m.pi / 2 - elev) + + +def gen_images(locs, features, n_gridpoints, normalize=True, + augment=False, pca=False, std_mult=0.1, n_components=2, edgeless=False): + """ + Generates EEG images given electrode locations in 2D space and multiple feature values for each electrode + + :param locs: An array with shape [n_electrodes, 2] containing X, Y + coordinates for each electrode. + :param features: Feature matrix as [n_samples, n_features] + Features are as columns. + Features corresponding to each frequency band are concatenated. + (alpha1, alpha2, ..., beta1, beta2,...) + :param n_gridpoints: Number of pixels in the output images + :param normalize: Flag for whether to normalize each band over all samples + :param augment: Flag for generating augmented images + :param pca: Flag for PCA based data augmentation + :param std_mult Multiplier for std of added noise + :param n_components: Number of components in PCA to retain for augmentation + :param edgeless: If True generates edgeless images by adding artificial channels + at four corners of the image with value = 0 (default=False). + :return: Tensor of size [samples, colors, W, H] containing generated + images. + """ + feat_array_temp = [] + nElectrodes = locs.shape[0] # Number of electrodes + # Test whether the feature vector length is divisible by number of electrodes + assert features.shape[1] % nElectrodes == 0 + n_colors = features.shape[1] // nElectrodes + for c in range(int(n_colors)): + feat_array_temp.append(features[:, c * nElectrodes : nElectrodes * (c+1)]) + if augment: + if pca: + for c in range(n_colors): + feat_array_temp[c] = augment_EEG(feat_array_temp[c], std_mult, pca=True, n_components=n_components) + else: + for c in range(n_colors): + feat_array_temp[c] = augment_EEG(feat_array_temp[c], std_mult, pca=False, n_components=n_components) + nSamples = features.shape[0] + # Interpolate the values + grid_x, grid_y = np.mgrid[ + min(locs[:, 0]):max(locs[:, 0]):n_gridpoints*1j, + min(locs[:, 1]):max(locs[:, 1]):n_gridpoints*1j + ] + temp_interp = [] + for c in range(n_colors): + temp_interp.append(np.zeros([nSamples, n_gridpoints, n_gridpoints])) + # Generate edgeless images + if edgeless: + min_x, min_y = np.min(locs, axis=0) + max_x, max_y = np.max(locs, axis=0) + locs = np.append(locs, np.array([[min_x, min_y], [min_x, max_y],[max_x, min_y],[max_x, max_y]]),axis=0) + for c in range(n_colors): + feat_array_temp[c] = np.append(feat_array_temp[c], np.zeros((nSamples, 4)), axis=1) + # Interpolating + for i in range(nSamples): + for c in range(n_colors): + temp_interp[c][i, :, :] = griddata(locs, feat_array_temp[c][i, :], (grid_x, grid_y), + method='cubic', fill_value=np.nan) + print('Interpolating {0}/{1}\r'.format(i+1, nSamples), end='\r') + # Normalizing + for c in range(n_colors): + if normalize: + temp_interp[c][~np.isnan(temp_interp[c])] = \ + scale(temp_interp[c][~np.isnan(temp_interp[c])]) + temp_interp[c] = np.nan_to_num(temp_interp[c]) + return np.swapaxes(np.asarray(temp_interp), 0, 1) # swap axes to have [samples, colors, W, H] + + +def build_cnn(input_var=None, w_init=None, n_layers=(4, 2, 1), n_filters_first=32, imsize=32, n_colors=3): + """ + Builds a VGG style CNN network followed by a fully-connected layer and a softmax layer. + Stacks are separated by a maxpool layer. Number of kernels in each layer is twice + the number in previous stack. + input_var: Theano variable for input to the network + outputs: pointer to the output of the last layer of network (softmax) + + :param input_var: theano variable as input to the network + :param w_init: Initial weight values + :param n_layers: number of layers in each stack. An array of integers with each + value corresponding to the number of layers in each stack. + (e.g. [4, 2, 1] == 3 stacks with 4, 2, and 1 layers in each. + :param n_filters_first: number of filters in the first layer + :param imSize: Size of the image + :param n_colors: Number of color channels (depth) + :return: a pointer to the output of last layer + """ + weights = [] # Keeps the weights for all layers + count = 0 + # If no initial weight is given, initialize with GlorotUniform + if w_init is None: + w_init = [lasagne.init.GlorotUniform()] * sum(n_layers) + # Input layer + network = InputLayer(shape=(None, n_colors, imsize, imsize), + input_var=input_var) + for i, s in enumerate(n_layers): + for l in range(s): + network = Conv2DLayer(network, num_filters=n_filters_first * (2 ** i), filter_size=(3, 3), + W=w_init[count], pad='same') + count += 1 + weights.append(network.W) + network = MaxPool2DLayer(network, pool_size=(2, 2)) + return network, weights + + +def build_convpool_max(input_vars, nb_classes, imsize=32, n_colors=3, n_timewin=3): + """ + Builds the complete network with maxpooling layer in time. + + :param input_vars: list of EEG images (one image per time window) + :param nb_classes: number of classes + :param imsize: size of the input image (assumes a square input) + :param n_colors: number of color channels in the image + :param n_timewin: number of time windows in the snippet + :return: a pointer to the output of last layer + """ + convnets = [] + w_init = None + # Build 7 parallel CNNs with shared weights + for i in range(n_timewin): + if i == 0: + convnet, w_init = build_cnn(input_vars[i], imsize=imsize, n_colors=n_colors) + else: + convnet, _ = build_cnn(input_vars[i], w_init=w_init, imsize=imsize, n_colors=n_colors) + convnets.append(convnet) + # convpooling using Max pooling over frames + convpool = ElemwiseMergeLayer(convnets, theano.tensor.maximum) + # A fully-connected layer of 512 units with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=512, nonlinearity=lasagne.nonlinearities.rectify) + # And, finally, the output layer with 50% dropout on its inputs: + convpool = lasagne.layers.DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=nb_classes, nonlinearity=lasagne.nonlinearities.softmax) + return convpool + + +def build_convpool_conv1d(input_vars, nb_classes, imsize=32, n_colors=3, n_timewin=3): + """ + Builds the complete network with 1D-conv layer to integrate time from sequences of EEG images. + + :param input_vars: list of EEG images (one image per time window) + :param nb_classes: number of classes + :param imsize: size of the input image (assumes a square input) + :param n_colors: number of color channels in the image + :param n_timewin: number of time windows in the snippet + :return: a pointer to the output of last layer + """ + convnets = [] + w_init = None + # Build 7 parallel CNNs with shared weights + for i in range(n_timewin): + if i == 0: + convnet, w_init = build_cnn(input_vars[i], imsize=imsize, n_colors=n_colors) + else: + convnet, _ = build_cnn(input_vars[i], w_init=w_init, imsize=imsize, n_colors=n_colors) + convnets.append(FlattenLayer(convnet)) + # at this point convnets shape is [numTimeWin][n_samples, features] + # we want the shape to be [n_samples, features, numTimeWin] + convpool = ConcatLayer(convnets) + convpool = ReshapeLayer(convpool, ([0], n_timewin, get_output_shape(convnets[0])[1])) + convpool = DimshuffleLayer(convpool, (0, 2, 1)) + # input to 1D convlayer should be in (batch_size, num_input_channels, input_length) + convpool = Conv1DLayer(convpool, 64, 3) + # A fully-connected layer of 512 units with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=512, nonlinearity=lasagne.nonlinearities.rectify) + # And, finally, the output layer with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=nb_classes, nonlinearity=lasagne.nonlinearities.softmax) + return convpool + + +def build_convpool_lstm(input_vars, nb_classes, grad_clip=110, imsize=32, n_colors=3, n_timewin=3): + """ + Builds the complete network with LSTM layer to integrate time from sequences of EEG images. + + :param input_vars: list of EEG images (one image per time window) + :param nb_classes: number of classes + :param grad_clip: the gradient messages are clipped to the given value during + the backward pass. + :param imsize: size of the input image (assumes a square input) + :param n_colors: number of color channels in the image + :param n_timewin: number of time windows in the snippet + :return: a pointer to the output of last layer + """ + convnets = [] + w_init = None + # Build 7 parallel CNNs with shared weights + for i in range(n_timewin): + if i == 0: + convnet, w_init = build_cnn(input_vars[i], imsize=imsize, n_colors=n_colors) + else: + convnet, _ = build_cnn(input_vars[i], w_init=w_init, imsize=imsize, n_colors=n_colors) + convnets.append(FlattenLayer(convnet)) + # at this point convnets shape is [numTimeWin][n_samples, features] + # we want the shape to be [n_samples, features, numTimeWin] + convpool = ConcatLayer(convnets) + convpool = ReshapeLayer(convpool, ([0], n_timewin, get_output_shape(convnets[0])[1])) + # Input to LSTM should have the shape as (batch size, SEQ_LENGTH, num_features) + convpool = LSTMLayer(convpool, num_units=128, grad_clipping=grad_clip, + nonlinearity=lasagne.nonlinearities.tanh) + # We only need the final prediction, we isolate that quantity and feed it + # to the next layer. + convpool = SliceLayer(convpool, -1, 1) # Selecting the last prediction + # A fully-connected layer of 256 units with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=256, nonlinearity=lasagne.nonlinearities.rectify) + # And, finally, the output layer with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(convpool, p=.5), + num_units=nb_classes, nonlinearity=lasagne.nonlinearities.softmax) + return convpool + + +def build_convpool_mix(input_vars, nb_classes, grad_clip=110, imsize=32, n_colors=3, n_timewin=3): + """ + Builds the complete network with LSTM and 1D-conv layers combined + + :param input_vars: list of EEG images (one image per time window) + :param nb_classes: number of classes + :param grad_clip: the gradient messages are clipped to the given value during + the backward pass. + :param imsize: size of the input image (assumes a square input) + :param n_colors: number of color channels in the image + :param n_timewin: number of time windows in the snippet + :return: a pointer to the output of last layer + """ + convnets = [] + w_init = None + # Build 7 parallel CNNs with shared weights + for i in range(n_timewin): + if i == 0: + convnet, w_init = build_cnn(input_vars[i], imsize=imsize, n_colors=n_colors) + else: + convnet, _ = build_cnn(input_vars[i], w_init=w_init, imsize=imsize, n_colors=n_colors) + convnets.append(FlattenLayer(convnet)) + # at this point convnets shape is [numTimeWin][n_samples, features] + # we want the shape to be [n_samples, features, numTimeWin] + convpool = ConcatLayer(convnets) + convpool = ReshapeLayer(convpool, ([0], n_timewin, get_output_shape(convnets[0])[1])) + reformConvpool = DimshuffleLayer(convpool, (0, 2, 1)) + # input to 1D convlayer should be in (batch_size, num_input_channels, input_length) + conv_out = Conv1DLayer(reformConvpool, 64, 3) + conv_out = FlattenLayer(conv_out) + # Input to LSTM should have the shape as (batch size, SEQ_LENGTH, num_features) + lstm = LSTMLayer(convpool, num_units=128, grad_clipping=grad_clip, + nonlinearity=lasagne.nonlinearities.tanh) + lstm_out = SliceLayer(lstm, -1, 1) + # Merge 1D-Conv and LSTM outputs + dense_input = ConcatLayer([conv_out, lstm_out]) + # A fully-connected layer of 256 units with 50% dropout on its inputs: + convpool = DenseLayer(lasagne.layers.dropout(dense_input, p=.5), + num_units=512, nonlinearity=lasagne.nonlinearities.rectify) + # And, finally, the 10-unit output layer with 50% dropout on its inputs: + convpool = DenseLayer(convpool, + num_units=nb_classes, nonlinearity=lasagne.nonlinearities.softmax) + return convpool + + +def iterate_minibatches(inputs, targets, batchsize, shuffle=False): + """ + Iterates over the samples returing batches of size batchsize. + :param inputs: input data array. It should be a 4D numpy array for images [n_samples, n_colors, W, H] and 5D numpy + array if working with sequence of images [n_timewindows, n_samples, n_colors, W, H]. + :param targets: vector of target labels. + :param batchsize: Batch size + :param shuffle: Flag whether to shuffle the samples before iterating or not. + :return: images and labels for a batch + """ + if inputs.ndim == 4: + input_len = inputs.shape[0] + elif inputs.ndim == 5: + input_len = inputs.shape[1] + assert input_len == len(targets) + if shuffle: + indices = np.arange(input_len) + np.random.shuffle(indices) + for start_idx in range(0, input_len, batchsize): + if shuffle: + excerpt = indices[start_idx:start_idx + batchsize] + else: + excerpt = slice(start_idx, start_idx + batchsize) + if inputs.ndim == 4: + yield inputs[excerpt], targets[excerpt] + elif inputs.ndim == 5: + yield inputs[:, excerpt], targets[excerpt] + + +def train(images, labels, fold, model_type, batch_size=32, num_epochs=5): + """ + A sample training function which loops over the training set and evaluates the network + on the validation set after each epoch. Evaluates the network on the training set + whenever the + :param images: input images + :param labels: target labels + :param fold: tuple of (train, test) index numbers + :param model_type: model type ('cnn', '1dconv', 'maxpool', 'lstm', 'mix') + :param batch_size: batch size for training + :param num_epochs: number of epochs of dataset to go over for training + :return: none + """ + num_classes = len(np.unique(labels)) + (X_train, y_train), (X_val, y_val), (X_test, y_test) = reformatInput(images, labels, fold) + X_train = X_train.astype("float32", casting='unsafe') + X_val = X_val.astype("float32", casting='unsafe') + X_test = X_test.astype("float32", casting='unsafe') + # Prepare Theano variables for inputs and targets + input_var = T.TensorType('floatX', ((False,) * 5))() + target_var = T.ivector('targets') + # Create neural network model (depending on first command line parameter) + print("Building model and compiling functions...") + # Building the appropriate model + if model_type == '1dconv': + network = build_convpool_conv1d(input_var, num_classes) + elif model_type == 'maxpool': + network = build_convpool_max(input_var, num_classes) + elif model_type == 'lstm': + network = build_convpool_lstm(input_var, num_classes, 100) + elif model_type == 'mix': + network = build_convpool_mix(input_var, num_classes, 100) + elif model_type == 'cnn': + input_var = T.tensor4('inputs') + network, _ = build_cnn(input_var) + network = DenseLayer(lasagne.layers.dropout(network, p=.5), + num_units=256, + nonlinearity=lasagne.nonlinearities.rectify) + network = DenseLayer(lasagne.layers.dropout(network, p=.5), + num_units=num_classes, + nonlinearity=lasagne.nonlinearities.softmax) + else: + raise ValueError("Model not supported ['1dconv', 'maxpool', 'lstm', 'mix', 'cnn']") + # Create a loss expression for training, i.e., a scalar objective we want + # to minimize (for our multi-class problem, it is the cross-entropy loss): + prediction = lasagne.layers.get_output(network) + loss = lasagne.objectives.categorical_crossentropy(prediction, target_var) + loss = loss.mean() + params = lasagne.layers.get_all_params(network, trainable=True) + updates = lasagne.updates.adam(loss, params, learning_rate=0.001) + # Create a loss expression for validation/testing. The crucial difference + # here is that we do a deterministic forward pass through the network, + # disabling dropout layers. + test_prediction = lasagne.layers.get_output(network, deterministic=True) + test_loss = lasagne.objectives.categorical_crossentropy(test_prediction, + target_var) + test_loss = test_loss.mean() + # As a bonus, also create an expression for the classification accuracy: + test_acc = T.mean(T.eq(T.argmax(test_prediction, axis=1), target_var), + dtype=theano.config.floatX) + # Compile a function performing a training step on a mini-batch (by giving + # the updates dictionary) and returning the corresponding training loss: + train_fn = theano.function([input_var, target_var], loss, updates=updates) + # Compile a second function computing the validation loss and accuracy: + val_fn = theano.function([input_var, target_var], [test_loss, test_acc]) + # Finally, launch the training loop. + print("Starting training...") + best_validation_accu = 0 + # We iterate over epochs: + for epoch in range(num_epochs): + # In each epoch, we do a full pass over the training data: + train_err = 0 + train_batches = 0 + start_time = time.time() + for batch in iterate_minibatches(X_train, y_train, batch_size, shuffle=False): + inputs, targets = batch + train_err += train_fn(inputs, targets) + train_batches += 1 + # And a full pass over the validation data: + val_err = 0 + val_acc = 0 + val_batches = 0 + for batch in iterate_minibatches(X_val, y_val, batch_size, shuffle=False): + inputs, targets = batch + err, acc = val_fn(inputs, targets) + val_err += err + val_acc += acc + val_batches += 1 + av_train_err = train_err / train_batches + av_val_err = val_err / val_batches + av_val_acc = val_acc / val_batches + # Then we print the results for this epoch: + print("Epoch {} of {} took {:.3f}s".format( + epoch + 1, num_epochs, time.time() - start_time)) + print(" training loss:\t\t{:.6f}".format(av_train_err)) + print(" validation loss:\t\t{:.6f}".format(av_val_err)) + print(" validation accuracy:\t\t{:.2f} %".format(av_val_acc * 100)) + if av_val_acc > best_validation_accu: + best_validation_accu = av_val_acc + # After training, we compute and print the test error: + test_err = 0 + test_acc = 0 + test_batches = 0 + for batch in iterate_minibatches(X_test, y_test, batch_size, shuffle=False): + inputs, targets = batch + err, acc = val_fn(inputs, targets) + test_err += err + test_acc += acc + test_batches += 1 + av_test_err = test_err / test_batches + av_test_acc = test_acc / test_batches + print("Final results:") + print(" test loss:\t\t\t{:.6f}".format(av_test_err)) + print(" test accuracy:\t\t{:.2f} %".format(av_test_acc * 100)) + # Dump the network weights to a file like this: + np.savez('weights_lasg_{0}'.format(model_type), *lasagne.layers.get_all_param_values(network)) + print('-'*50) + print("Best validation accuracy:\t\t{:.2f} %".format(best_validation_accu * 100)) + print("Best test accuracy:\t\t{:.2f} %".format(av_test_acc * 100)) + +''' +if __name__ == '__main__': + from utils import reformatInput + + # Load electrode locations + print('Loading data...') + locs = scipy.io.loadmat('../Sample data/Neuroscan_locs_orig.mat') + locs_3d = locs['A'] + locs_2d = [] + # Convert to 2D + for e in locs_3d: + locs_2d.append(azim_proj(e)) + + feats = scipy.io.loadmat('../Sample data/FeatureMat_timeWin.mat')['features'] + print ('Feats Shape: ',feats.shape) + subj_nums = np.squeeze(scipy.io.loadmat('../Sample data/trials_subNums.mat')['subjectNum']) + # Leave-Subject-Out cross validation + fold_pairs = [] + for i in np.unique(subj_nums): + ts = subj_nums == i + tr = np.squeeze(np.nonzero(np.bitwise_not(ts))) + ts = np.squeeze(np.nonzero(ts)) + np.random.shuffle(tr) # Shuffle indices + np.random.shuffle(ts) + fold_pairs.append((tr, ts)) + + # CNN Mode + print('Generating images...') + # Find the average response over time windows + av_feats = reduce(lambda x, y: x+y, [feats[:, i*192:(i+1)*192] for i in range(feats.shape[1] / 192)]) + av_feats = av_feats / (feats.shape[1] / 192) + images = gen_images(np.array(locs_2d), + av_feats, + 32, normalize=False) + print('\n') + + # Class labels should start from 0 + print('Training the CNN Model...') + train(images, np.squeeze(feats[:, -1]) - 1, fold_pairs[2], 'cnn') + + # Conv-LSTM Mode + print('Generating images for all time windows...') + images_timewin = np.array([gen_images(np.array(locs_2d), + feats[:, i * 192:(i + 1) * 192], 32, normalize=False) for i in + range(feats.shape[1] / 192) + ]) + print('\n') + print('Training the LSTM-CONV Model...') + train(images_timewin, np.squeeze(feats[:, -1]) - 1, fold_pairs[2], 'mix') + + print('Done!') +'''