I'm trying to learn LSTMs and I thought a nice way of doing it would be identifying onset-and-offset of QRS complexes on ECGs.
I have 300 x 200 x 2 numpy array of ECGs (300 ECGs, each of 200 data points, each data point being [x=time,y=voltage]. Sometimes it's 0 padded at the start (like the example below) as ECGs can be of different durations.
I also have labels in the form 300 x 2, 300 labels of [qrs_onset,qrs_offset].
This is an example of an ECG plotted and labels superimposed.
I've done some pre-processing, shuffling the location of the ECG forwards and backwards by ~ 10% of the cycle length. I then feed it into an LSTM model with Python/Keras/Tensorflow backend:
self.model = Sequential() self.model.add(LSTM(input_length=199, input_dim=2, dropout=0.1, output_dim=64, return_sequences=True)) self.model.add(LSTM(256, dropout=0.1, return_sequences=True)) self.model.add(LSTM(100, dropout=0.1, return_sequences=False)) self.model.add(Dense(2)) self.model.add(Activation("linear")) self.model.compile(loss="mean_squared_error", optimizer="rmsprop")
However, all the LSTM does is fine a location that fits the entire dataset best, and gives that exact location regardless of the ECG fed to it.
It takes about 4000 epochs to get to this point, but at this stage the loss completely plateaus and makes 0 further progress.
It's strange because I thought an LSTM would be great for this task.
Things I have tried:
- Reducing the epoch number to a smaller proportion of the dataset to get out of local minima
- Adding between 1 and 3 LSTM layers of between 64 and 300 units.
- Using the Adam optimiser instead of RMSProp
- Adding and removing dropout between 0.0 and 0.2
Adding a 1D Conv layer to try and identify the peaks using spatial information before feeding this into an LSTM
self.model = Sequential() self.model.add(Conv1D(filters=32, kernel_size=3, padding='same', activation='relu')) self.model.add(MaxPooling1D(pool_size=2)) self.model.add( LSTM(300, dropout=0.2, return_sequences=True, input_shape=(None, 300))) self.model.add( LSTM(300, dropout=0.2, return_sequences=True, input_shape=(None, 300))) self.model.add( LSTM(300, dropout=0.2, return_sequences=False, input_shape=(None, 300))) self.model.add(Dense(2, input_dim=300)) self.model.add(Activation("linear")) self.model.compile(loss="mean_squared_error", optimizer="rmsprop")
But the model seems completely incapable of learning to identify the QRS complex.
Does anyone have any advice of how I might improve things. I'm expecting to get more data, but actually I'm not sure that will help here as I'm certainly not over-fitting the data I've already got.
Do you think resampling my data series at regular intervals so it's just voltage in a linear time series might help, so my input is 300 x 200 x 1, rather than 300 x 200 x 1? It's not ideal as the sampling frequency changes, but I am willing to try if people think it will help.
Just in case anyone is interested, I made a few changes to my network and now it works beautifully:
Red curve = Probability of being QRS onset
Green curve = Probability of being QRS offset
Dotted lines are maximum probability for each
Basically the changes I made were
- I got rid of the time series array, so the input data was just an array of voltage data
- I instead made two networks, one for QRS onset and one for QRS offset; this appears to have reduced the tendency for the network to find one of the parameters, and then just place the other based of the average QRS duration.
- I changed the task for a regression task to a classification task, so each data point had a probability of being QRS onset (thanks @shimao)