|
a |
|
b/deepheart/model.py |
|
|
1 |
import tensorflow as tf |
|
|
2 |
import os |
|
|
3 |
from datetime import datetime |
|
|
4 |
|
|
|
5 |
|
|
|
6 |
class CNN: |
|
|
7 |
def __init__(self, pcg, nclasses=2, learning_rate=0.001, |
|
|
8 |
epochs=5, batch_size=100, dropout=0.75, base_dir="/tmp", |
|
|
9 |
model_name="cnn"): |
|
|
10 |
self.pcg = pcg |
|
|
11 |
self.nclasses = nclasses |
|
|
12 |
self.d_input = self.pcg.train.X.shape[1] |
|
|
13 |
self.learning_rate = learning_rate |
|
|
14 |
self.epochs = epochs |
|
|
15 |
self.batch_size = batch_size |
|
|
16 |
self.dropout = dropout |
|
|
17 |
self.nbatches = int(self.pcg.train.X.shape[0] / float(self.batch_size)) |
|
|
18 |
self.model_name = model_name |
|
|
19 |
self.base_dir = base_dir |
|
|
20 |
|
|
|
21 |
def train(self): |
|
|
22 |
""" |
|
|
23 |
Train a convolutional neural network over the input PCG dataset. |
|
|
24 |
This method is beefy: it is responsible for defining tensorflow |
|
|
25 |
variables, defining the training objective function, defining summary |
|
|
26 |
statistics creating the tensorflow session, running gradient |
|
|
27 |
descent and, ultimately, writing statistics |
|
|
28 |
|
|
|
29 |
In the future this will be refactored into more easily tested |
|
|
30 |
training segments. |
|
|
31 |
|
|
|
32 |
Parameters |
|
|
33 |
---------- |
|
|
34 |
None |
|
|
35 |
|
|
|
36 |
Returns |
|
|
37 |
------- |
|
|
38 |
None |
|
|
39 |
|
|
|
40 |
""" |
|
|
41 |
print('begin train') |
|
|
42 |
print(self.__get_output_name()) |
|
|
43 |
|
|
|
44 |
with tf.name_scope('input'): |
|
|
45 |
X = tf.placeholder(tf.float32, [None, self.d_input], name='X') |
|
|
46 |
y = tf.placeholder(tf.float32, [None, self.nclasses], name='y') |
|
|
47 |
do_drop = tf.placeholder(tf.float32, name='drop') |
|
|
48 |
|
|
|
49 |
with tf.name_scope('weights'): |
|
|
50 |
weights = { |
|
|
51 |
'wc1': tf.Variable(tf.random_normal([5, 1, 1, 32]), name='wc1'), |
|
|
52 |
'wc2': tf.Variable(tf.random_normal([5, 1, 32, 64]), name='wc2'), |
|
|
53 |
# 2 Max pools have taken original 10612 signal down to |
|
|
54 |
# 5306 --> 2653. Each max pool has a ksize=2. |
|
|
55 |
# 'wd1': tf.Variable(tf.random_normal([2653 * 1 * 64, 1024])), |
|
|
56 |
'wd1': tf.Variable(tf.random_normal([int(self.d_input / 4) * 1 * 64, 1024]), name='wd1'), |
|
|
57 |
'out': tf.Variable(tf.random_normal([1024, self.nclasses]), name='outW') |
|
|
58 |
} |
|
|
59 |
with tf.name_scope('biases'): |
|
|
60 |
biases = { |
|
|
61 |
'bc1': tf.Variable(tf.random_normal([32]), name='bc1'), |
|
|
62 |
'bc2': tf.Variable(tf.random_normal([64]), name='bc2'), |
|
|
63 |
'bd1': tf.Variable(tf.random_normal([1024]), name='bd1'), |
|
|
64 |
'out': tf.Variable(tf.random_normal([self.nclasses]), name='outB') |
|
|
65 |
} |
|
|
66 |
|
|
|
67 |
with tf.name_scope('pred'): |
|
|
68 |
pred = self.model1D(X, weights, biases, do_drop) |
|
|
69 |
|
|
|
70 |
with tf.name_scope('cost'): |
|
|
71 |
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y, name='cost')) |
|
|
72 |
optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(cost) |
|
|
73 |
|
|
|
74 |
dim = tf.shape(y)[0] |
|
|
75 |
|
|
|
76 |
with tf.name_scope('sensitivity'): |
|
|
77 |
# sensitivity = correctly predicted abnormal / total number of actual abnormal |
|
|
78 |
abnormal_idxs = tf.cast(tf.equal(tf.argmax(pred, 1), 1), tf.float32) |
|
|
79 |
pred1d = tf.reshape(tf.slice(y, [0, 1], [dim, 1]), [-1]) |
|
|
80 |
abn = tf.mul(pred1d, abnormal_idxs) |
|
|
81 |
sensitivity = tf.reduce_sum(abn) / tf.reduce_sum(pred1d) |
|
|
82 |
tf.scalar_summary('sensitivity', sensitivity) |
|
|
83 |
|
|
|
84 |
with tf.name_scope('specificity'): |
|
|
85 |
# specificity = correctly predicted normal / total number of actual normal |
|
|
86 |
normal_idxs = tf.cast(tf.equal(tf.argmax(pred, 1), 0), tf.float32) |
|
|
87 |
pred1d_n = tf.reshape(tf.slice(y, [0, 0], [dim, 1]), [-1]) |
|
|
88 |
normal = tf.mul(pred1d_n, normal_idxs) |
|
|
89 |
specificity = tf.reduce_sum(normal) / tf.reduce_sum(pred1d_n) |
|
|
90 |
tf.scalar_summary('specificity', sensitivity) |
|
|
91 |
|
|
|
92 |
# Physionet score is the mean of sensitivity and specificity |
|
|
93 |
score = (sensitivity + specificity) / 2.0 |
|
|
94 |
tf.scalar_summary('score', score) |
|
|
95 |
|
|
|
96 |
init = tf.initialize_all_variables() |
|
|
97 |
|
|
|
98 |
saver = tf.train.Saver() |
|
|
99 |
with tf.Session() as sess: |
|
|
100 |
sess.run(init) |
|
|
101 |
|
|
|
102 |
merged = tf.merge_all_summaries() |
|
|
103 |
train_writer = tf.train.SummaryWriter(os.path.join(self.base_dir, 'train'), sess.graph) |
|
|
104 |
|
|
|
105 |
for epoch in range(self.epochs): |
|
|
106 |
avg_cost = 0 |
|
|
107 |
for batch in range(self.nbatches): |
|
|
108 |
batch_x, batch_y = self.pcg.get_mini_batch(self.batch_size) |
|
|
109 |
summary, _, c = sess.run([merged, optimizer, cost], |
|
|
110 |
feed_dict={X: batch_x, |
|
|
111 |
y: batch_y, |
|
|
112 |
do_drop: self.dropout}) |
|
|
113 |
train_writer.add_summary(summary, epoch*batch) |
|
|
114 |
avg_cost += c |
|
|
115 |
avg_cost /= float(self.nbatches) |
|
|
116 |
print('Epoch %s\tcost %s' % (epoch, avg_cost)) |
|
|
117 |
|
|
|
118 |
if epoch % 10 == 0: |
|
|
119 |
acc, sens, spec = sess.run([score, sensitivity, specificity], |
|
|
120 |
feed_dict={X: self.pcg.test.X, |
|
|
121 |
y: self.pcg.test.y, |
|
|
122 |
do_drop: 1.}) |
|
|
123 |
print('Score %s\tSensitivity %s\tSpecificity %s' % (acc, sens, spec)) |
|
|
124 |
|
|
|
125 |
saver.save(sess, self.__get_output_name()) |
|
|
126 |
print('Epoch written') |
|
|
127 |
|
|
|
128 |
def __get_output_name(self): |
|
|
129 |
now = datetime.now() |
|
|
130 |
time_str = "-%s" % (now.date()) # now.hour, now.minute, now.second) |
|
|
131 |
model_path = os.path.join(self.base_dir, self.model_name + time_str + '.tnfl') |
|
|
132 |
return model_path |
|
|
133 |
|
|
|
134 |
def conv2d(self, x, w, b, strides=1): |
|
|
135 |
""" |
|
|
136 |
A small helper function for calcualting a 1D convolution |
|
|
137 |
from tensorflow's conv2d method |
|
|
138 |
|
|
|
139 |
Parameters |
|
|
140 |
---------- |
|
|
141 |
x: tensorflow.placeholder |
|
|
142 |
The feature vector |
|
|
143 |
w: tensorflow.Variable |
|
|
144 |
The unknown weights to learn |
|
|
145 |
b: tensorflow.Variable |
|
|
146 |
The unknown biases to learn |
|
|
147 |
strides: int |
|
|
148 |
The length of the stride to use for convolution |
|
|
149 |
|
|
|
150 |
Returns |
|
|
151 |
------- |
|
|
152 |
tensorflow.Variable |
|
|
153 |
A convolution over the input feature vector |
|
|
154 |
|
|
|
155 |
""" |
|
|
156 |
|
|
|
157 |
|
|
|
158 |
x = tf.nn.conv2d(x, w, strides=[1, strides, strides, 1], padding="SAME") |
|
|
159 |
x = tf.nn.bias_add(x, b) |
|
|
160 |
return tf.nn.relu(x) |
|
|
161 |
|
|
|
162 |
def model1D(self, x, weights, biases, dropout): |
|
|
163 |
""" |
|
|
164 |
A Wrapper to chain several TensorFlow convolutional units together. This 1D model |
|
|
165 |
ultimately calls TensorFlow's conv2d, mapping a 1D feature vector to a collapsed |
|
|
166 |
2D convolution |
|
|
167 |
|
|
|
168 |
Parameters |
|
|
169 |
---------- |
|
|
170 |
x: tensorflow.placeholder |
|
|
171 |
A feature vector of size [None, no_features] |
|
|
172 |
|
|
|
173 |
weights: dict<str, tensorflow.Variable> |
|
|
174 |
Dictionary of Unknown weights to learn |
|
|
175 |
|
|
|
176 |
biases: dict<str, tensorflow.Variable> |
|
|
177 |
Dictionary of unknown biases to learn |
|
|
178 |
|
|
|
179 |
dropout: float |
|
|
180 |
the dropout fraction for convolutional units |
|
|
181 |
|
|
|
182 |
Returns |
|
|
183 |
------- |
|
|
184 |
out: tensorflow.Variable |
|
|
185 |
The result of applying multiple convolutional layers and |
|
|
186 |
a fully connected unit to the input feature vector |
|
|
187 |
|
|
|
188 |
""" |
|
|
189 |
|
|
|
190 |
with tf.name_scope('reshape'): |
|
|
191 |
x = tf.reshape(x, shape=[-1, self.d_input, 1, 1]) # [n_images, width, height, n_channels] |
|
|
192 |
|
|
|
193 |
with tf.name_scope('conv1'): |
|
|
194 |
conv1 = self.conv2d(x, weights['wc1'], biases['bc1']) |
|
|
195 |
conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 1, 1], strides=[1, 2, 1, 1], padding='SAME') |
|
|
196 |
conv1 = tf.nn.relu(conv1) |
|
|
197 |
|
|
|
198 |
with tf.name_scope('conv2'): |
|
|
199 |
conv2 = self.conv2d(conv1, weights['wc2'], biases['bc2']) |
|
|
200 |
conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 1, 1], strides=[1, 2, 1, 1], padding="SAME") |
|
|
201 |
conv2 = tf.nn.relu(conv2) |
|
|
202 |
|
|
|
203 |
with tf.name_scope('fullyConnected'): |
|
|
204 |
d_layer1 = weights['wd1'].get_shape().as_list()[0] |
|
|
205 |
fc1 = tf.reshape(conv2, [-1, d_layer1]) |
|
|
206 |
fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1']) |
|
|
207 |
fc1 = tf.nn.relu(fc1) |
|
|
208 |
fc1 = tf.nn.dropout(fc1, dropout) |
|
|
209 |
|
|
|
210 |
out = tf.add(tf.matmul(fc1, weights['out']), biases['out']) |
|
|
211 |
return out |