Diff of /ClassifyECG.ipynb [000000] .. [c49678]

Switch to unified view

a b/ClassifyECG.ipynb
1
{
2
 "cells": [
3
  {
4
   "cell_type": "markdown",
5
   "metadata": {},
6
   "source": [
7
    "# Classification Analysis for ECG Time-Series\n",
8
    "\n",
9
    "> Copyright 2019 Dave Fernandes. All Rights Reserved.\n",
10
    "> \n",
11
    "> Licensed under the Apache License, Version 2.0 (the \"License\");\n",
12
    "> you may not use this file except in compliance with the License.\n",
13
    "> You may obtain a copy of the License at\n",
14
    ">\n",
15
    "> http://www.apache.org/licenses/LICENSE-2.0\n",
16
    ">  \n",
17
    "> Unless required by applicable law or agreed to in writing, software\n",
18
    "> distributed under the License is distributed on an \"AS IS\" BASIS,\n",
19
    "> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
20
    "> See the License for the specific language governing permissions and\n",
21
    "> limitations under the License."
22
   ]
23
  },
24
  {
25
   "cell_type": "markdown",
26
   "metadata": {},
27
   "source": [
28
    "## Overview\n",
29
    "This notebook classifies time-series for segmented heartbeats from ECG lead II recordings. Either of two models (CNN or RNN) can be selected from training and evaluation.\n",
30
    "- Data for this analysis should be prepared using the `PreprocessECG.ipynb` notebook from this project.\n",
31
    "- Original data is from: https://www.kaggle.com/shayanfazeli/heartbeat"
32
   ]
33
  },
34
  {
35
   "cell_type": "code",
36
   "execution_count": null,
37
   "metadata": {},
38
   "outputs": [],
39
   "source": [
40
    "import numpy as np\n",
41
    "import tensorflow as tf\n",
42
    "import tensorflow.keras.layers as keras\n",
43
    "import matplotlib.pyplot as plt\n",
44
    "import pickle\n",
45
    "\n",
46
    "tf.enable_eager_execution()\n",
47
    "\n",
48
    "TRAIN_SET = './Data/train_set.pickle'\n",
49
    "TEST_SET = './Data/test_set.pickle'\n",
50
    "\n",
51
    "with open(TEST_SET, 'rb') as file:\n",
52
    "    test_set = pickle.load(file)\n",
53
    "    x_test = test_set['x']\n",
54
    "    y_test = test_set['y']\n",
55
    "\n",
56
    "with open(TRAIN_SET, 'rb') as file:\n",
57
    "    train_set = pickle.load(file)\n",
58
    "    x_train = train_set['x']\n",
59
    "    y_train = train_set['y']\n",
60
    "    \n",
61
    "def parameter_count():\n",
62
    "    total = 0\n",
63
    "    for v in tf.trainable_variables():\n",
64
    "        v_elements = 1\n",
65
    "        for dim in v.get_shape():\n",
66
    "            v_elements *= dim.value\n",
67
    "\n",
68
    "        total += v_elements\n",
69
    "    return total"
70
   ]
71
  },
72
  {
73
   "cell_type": "markdown",
74
   "metadata": {},
75
   "source": [
76
    "### Input functions for Estimator"
77
   ]
78
  },
79
  {
80
   "cell_type": "code",
81
   "execution_count": null,
82
   "metadata": {},
83
   "outputs": [],
84
   "source": [
85
    "def combined_dataset(features, labels):\n",
86
    "    assert features.shape[0] == labels.shape[0]\n",
87
    "    dataset = tf.data.Dataset.from_tensor_slices(({'time_series': features}, labels))\n",
88
    "    return dataset\n",
89
    "\n",
90
    "def class_for_element(features, labels):\n",
91
    "    return labels\n",
92
    "\n",
93
    "# For training\n",
94
    "def train_input_fn():\n",
95
    "    dataset = combined_dataset(x_train, y_train)\n",
96
    "    return dataset.repeat().shuffle(500000).batch(200).prefetch(1)\n",
97
    "\n",
98
    "# For evaluation and metrics\n",
99
    "def eval_input_fn():\n",
100
    "    dataset = combined_dataset(x_test, y_test)\n",
101
    "    return dataset.batch(1000).prefetch(1)"
102
   ]
103
  },
104
  {
105
   "cell_type": "markdown",
106
   "metadata": {},
107
   "source": [
108
    "### Define the models\n",
109
    "#### Convolutional Model\n",
110
    "* The convolutional model is taken from: https://arxiv.org/pdf/1805.00794.pdf\n",
111
    "\n",
112
    "Model consists of:\n",
113
    "* An initial 1-D convolutional layer\n",
114
    "* 5 repeated residual blocks (`same` padding)\n",
115
    "* A fully-connected layer\n",
116
    "* A linear layer with softmax output"
117
   ]
118
  },
119
  {
120
   "cell_type": "code",
121
   "execution_count": null,
122
   "metadata": {},
123
   "outputs": [],
124
   "source": [
125
    "CNN_MODEL_DIR = './Models/CNN-Paper'\n",
126
    "\n",
127
    "def conv_unit(unit, input_layer):\n",
128
    "    s = '_' + str(unit)\n",
129
    "    layer = keras.Conv1D(name='Conv1' + s, filters=32, kernel_size=5, strides=1, padding='same', activation='relu')(input_layer)\n",
130
    "    layer = keras.Conv1D(name='Conv2' + s, filters=32, kernel_size=5, strides=1, padding='same', activation=None)(layer )\n",
131
    "    layer = keras.Add(name='ResidualSum' + s)([layer, input_layer])\n",
132
    "    layer = keras.Activation(\"relu\", name='Act' + s)(layer)\n",
133
    "    layer = keras.MaxPooling1D(name='MaxPool' + s, pool_size=5, strides=2)(layer)\n",
134
    "    return layer\n",
135
    "\n",
136
    "def cnn_model(input_layer, mode, params):\n",
137
    "    current_layer = keras.Conv1D(filters=32, kernel_size=5, strides=1)(input_layer)\n",
138
    "\n",
139
    "    for i in range(5):\n",
140
    "        current_layer = conv_unit(i + 1, current_layer)\n",
141
    "\n",
142
    "    current_layer = keras.Flatten()(current_layer)\n",
143
    "    current_layer = keras.Dense(32, name='FC1', activation='relu')(current_layer)\n",
144
    "    logits = keras.Dense(5, name='Output')(current_layer)\n",
145
    "    \n",
146
    "    print('Parameter count:', parameter_count())\n",
147
    "    return logits"
148
   ]
149
  },
150
  {
151
   "cell_type": "markdown",
152
   "metadata": {},
153
   "source": [
154
    "#### Recurrent Model\n",
155
    "\n",
156
    "Model consists of:\n",
157
    "* Two stacked bidirectional GRU layers\n",
158
    "* Two fully-connected layers\n",
159
    "* A linear layer with softmax output\n",
160
    "\n",
161
    "Since the model operates on segmented heartbeat samples, we can use a bidirectional RNN because the whole segment is available for processing at one time. It is also a more \\\"fair\\\" comparison with the CNN."
162
   ]
163
  },
164
  {
165
   "cell_type": "code",
166
   "execution_count": null,
167
   "metadata": {},
168
   "outputs": [],
169
   "source": [
170
    "RNN_MODEL_DIR = './Models/RNN'\n",
171
    "RNN_OUTPUT_UNITS = [64, 128]\n",
172
    "\n",
173
    "def rnn_model(input_layer, mode, params):\n",
174
    "    current_layer = tf.keras.layers.Masking(mask_value=0., input_shape=(187, 1), name='Masked')(input_layer)\n",
175
    "    \n",
176
    "    for i, size in enumerate(RNN_OUTPUT_UNITS):\n",
177
    "        notLast = i + 1 < len(RNN_OUTPUT_UNITS)\n",
178
    "        layer = tf.keras.layers.GRU(size, return_sequences=notLast, dropout=0.2, name = 'GRU' + str(i+1))\n",
179
    "        current_layer = keras.Bidirectional(layer, name = 'BiRNN' + str(i+1))(current_layer)\n",
180
    "\n",
181
    "    current_layer = keras.Dense(64, name='Dense1', activation='relu')(current_layer)\n",
182
    "    current_layer = keras.Dense(16, name='Dense2', activation='relu')(current_layer)\n",
183
    "    logits = keras.Dense(5, name='Output', activation='relu')(current_layer)\n",
184
    "    \n",
185
    "    print('Parameter count:', parameter_count())\n",
186
    "    return logits"
187
   ]
188
  },
189
  {
190
   "cell_type": "markdown",
191
   "metadata": {},
192
   "source": [
193
    "### Estimator setup"
194
   ]
195
  },
196
  {
197
   "cell_type": "code",
198
   "execution_count": null,
199
   "metadata": {},
200
   "outputs": [],
201
   "source": [
202
    "# Initial learning rate\n",
203
    "INITIAL_LEARNING_RATE = 0.001\n",
204
    "\n",
205
    "# Learning rate decay per LR_DECAY_STEPS steps (1.0 = no decay)\n",
206
    "LR_DECAY_RATE = 0.5\n",
207
    "\n",
208
    "# Number of steps for LR to decay by LR_DECAY_RATE\n",
209
    "LR_DECAY_STEPS = 4000\n",
210
    "\n",
211
    "# Threshold for gradient clipping\n",
212
    "GRADIENT_NORM_THRESH = 10.0\n",
213
    "\n",
214
    "# Select model to train\n",
215
    "MODEL_DIR = CNN_MODEL_DIR\n",
216
    "MODEL_FN = cnn_model\n",
217
    "\n",
218
    "def classifier_fn(features, labels, mode, params):\n",
219
    "    is_training = mode == tf.estimator.ModeKeys.TRAIN\n",
220
    "    input_layer = tf.feature_column.input_layer(features, params['feature_columns'])\n",
221
    "    input_layer = tf.expand_dims(input_layer, -1)\n",
222
    "\n",
223
    "    logits = MODEL_FN(input_layer, mode, params)\n",
224
    "\n",
225
    "    # For prediction, exit here\n",
226
    "    predicted_classes = tf.argmax(logits, 1)\n",
227
    "    if mode == tf.estimator.ModeKeys.PREDICT:\n",
228
    "        predictions = {\n",
229
    "            'class_ids': predicted_classes[:, tf.newaxis],\n",
230
    "            'probabilities': tf.nn.softmax(logits),\n",
231
    "            'logits': logits,\n",
232
    "        }\n",
233
    "        return tf.estimator.EstimatorSpec(mode, predictions=predictions)\n",
234
    "\n",
235
    "    # For training and evaluation, compute the loss (MSE)\n",
236
    "    loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)\n",
237
    "\n",
238
    "    accuracy = tf.metrics.accuracy(labels=labels, predictions=predicted_classes, name='acc_op')\n",
239
    "    metrics = {'accuracy': accuracy}\n",
240
    "    tf.summary.scalar('accuracy', accuracy[1])\n",
241
    "\n",
242
    "    if mode == tf.estimator.ModeKeys.EVAL:\n",
243
    "        return tf.estimator.EstimatorSpec(mode, loss=loss, eval_metric_ops=metrics)\n",
244
    "\n",
245
    "    # For training...\n",
246
    "    global_step = tf.train.get_global_step()\n",
247
    "    learning_rate = tf.train.exponential_decay(INITIAL_LEARNING_RATE, global_step, LR_DECAY_STEPS, LR_DECAY_RATE)\n",
248
    "\n",
249
    "    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)\n",
250
    "    #optimizer = tf.contrib.estimator.clip_gradients_by_norm(optimizer, GRADIENT_NORM_THRESH)\n",
251
    "    \n",
252
    "    train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())\n",
253
    "    return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)"
254
   ]
255
  },
256
  {
257
   "cell_type": "markdown",
258
   "metadata": {},
259
   "source": [
260
    "### Train model"
261
   ]
262
  },
263
  {
264
   "cell_type": "code",
265
   "execution_count": null,
266
   "metadata": {},
267
   "outputs": [],
268
   "source": [
269
    "feature_columns = [tf.feature_column.numeric_column('time_series', [187])]\n",
270
    "\n",
271
    "estimator = tf.estimator.Estimator(\n",
272
    "    model_fn=classifier_fn,\n",
273
    "    model_dir=MODEL_DIR,\n",
274
    "    params={\n",
275
    "        'feature_columns': feature_columns,\n",
276
    "    })\n",
277
    "\n",
278
    "estimator.train(train_input_fn, steps=4000)\n",
279
    "info = estimator.evaluate(input_fn=eval_input_fn)"
280
   ]
281
  },
282
  {
283
   "cell_type": "markdown",
284
   "metadata": {},
285
   "source": [
286
    "### Compute metrics"
287
   ]
288
  },
289
  {
290
   "cell_type": "code",
291
   "execution_count": null,
292
   "metadata": {},
293
   "outputs": [],
294
   "source": [
295
    "import sklearn.metrics as skm\n",
296
    "import seaborn\n",
297
    "\n",
298
    "dataset_fn = eval_input_fn\n",
299
    "\n",
300
    "predictions = estimator.predict(input_fn=dataset_fn)\n",
301
    "y_pred = []\n",
302
    "y_prob = []\n",
303
    "\n",
304
    "for i, value in enumerate(predictions):\n",
305
    "    class_id = value['class_ids']\n",
306
    "    y_pred.append(class_id)\n",
307
    "    probabilities = value['probabilities']\n",
308
    "    y_prob.append(probabilities[class_id])\n",
309
    "del predictions\n",
310
    "\n",
311
    "y_pred = np.array(y_pred)\n",
312
    "y_prob = np.array(y_prob)\n",
313
    "y_test = np.reshape(y_test, (len(y_test), 1))\n",
314
    "\n",
315
    "# Classification report\n",
316
    "report = skm.classification_report(y_test, y_pred)\n",
317
    "print(report)\n",
318
    "\n",
319
    "# Confusion matrix\n",
320
    "cm = skm.confusion_matrix(y_test, y_pred)\n",
321
    "seaborn.heatmap(cm, annot=True,annot_kws={\"size\": 16})\n",
322
    "\n",
323
    "y_prob_correct = y_prob[y_pred == y_test]\n",
324
    "y_prob_mis = y_prob[y_pred != y_test]"
325
   ]
326
  },
327
  {
328
   "cell_type": "markdown",
329
   "metadata": {},
330
   "source": [
331
    "### Check probability estimates"
332
   ]
333
  },
334
  {
335
   "cell_type": "code",
336
   "execution_count": null,
337
   "metadata": {},
338
   "outputs": [],
339
   "source": [
340
    "from astropy.stats import binom_conf_interval\n",
341
    "\n",
342
    "_, _, _ = plt.hist(y_prob, 10, (0, 1))\n",
343
    "plt.xlabel('Belief')\n",
344
    "plt.ylabel('Count')\n",
345
    "plt.title('All Predictions')\n",
346
    "plt.show();\n",
347
    "\n",
348
    "n_all, bins = np.histogram(y_prob, 10, (0, 1))\n",
349
    "n_correct, bins = np.histogram(y_prob_correct, 10, (0, 1))\n",
350
    "\n",
351
    "f_correct = n_correct / np.clip(n_all, 1, None)\n",
352
    "f_bins = 0.5 * (bins[:-1] + bins[1:])\n",
353
    "\n",
354
    "n_correct = n_correct[n_all > 0]\n",
355
    "n_total = n_all[n_all > 0]\n",
356
    "f_correct = n_correct / n_total\n",
357
    "f_bins = f_bins[n_all > 0]\n",
358
    "\n",
359
    "lower_bound, upper_bound = binom_conf_interval(n_correct, n_total)\n",
360
    "error_bars = np.array([f_correct - lower_bound, upper_bound - f_correct])\n",
361
    "\n",
362
    "plt.plot([0., 1.], [0., 1.])\n",
363
    "plt.errorbar(f_bins, f_correct, yerr=error_bars, fmt='o')\n",
364
    "plt.xlabel('Softmax Probability')\n",
365
    "plt.ylabel('Frequency')\n",
366
    "plt.title('Correct Predictions')\n",
367
    "plt.show();"
368
   ]
369
  },
370
  {
371
   "cell_type": "code",
372
   "execution_count": null,
373
   "metadata": {},
374
   "outputs": [],
375
   "source": []
376
  }
377
 ],
378
 "metadata": {
379
  "kernelspec": {
380
   "display_name": "Python 3",
381
   "language": "python",
382
   "name": "python3"
383
  },
384
  "language_info": {
385
   "codemirror_mode": {
386
    "name": "ipython",
387
    "version": 3
388
   },
389
   "file_extension": ".py",
390
   "mimetype": "text/x-python",
391
   "name": "python",
392
   "nbconvert_exporter": "python",
393
   "pygments_lexer": "ipython3",
394
   "version": "3.6.7"
395
  }
396
 },
397
 "nbformat": 4,
398
 "nbformat_minor": 2
399
}