-
Notifications
You must be signed in to change notification settings - Fork 73
/
wtte-rnn.py
159 lines (119 loc) · 5.53 KB
/
wtte-rnn.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import numpy as np
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.layers import Activation
from keras.layers import Masking
from keras.optimizers import RMSprop
from keras import backend as k
from sklearn.preprocessing import normalize
"""
Discrete log-likelihood for Weibull hazard function on censored survival data
y_true is a (samples, 2) tensor containing time-to-event (y), and an event indicator (u)
ab_pred is a (samples, 2) tensor containing predicted Weibull alpha (a) and beta (b) parameters
For math, see https://ragulpr.github.io/assets/draft_master_thesis_martinsson_egil_wtte_rnn_2016.pdf (Page 35)
"""
def weibull_loglik_discrete(y_true, ab_pred, name=None):
y_ = y_true[:, 0]
u_ = y_true[:, 1]
a_ = ab_pred[:, 0]
b_ = ab_pred[:, 1]
hazard0 = k.pow((y_ + 1e-35) / a_, b_)
hazard1 = k.pow((y_ + 1) / a_, b_)
return -1 * k.mean(u_ * k.log(k.exp(hazard1 - hazard0) - 1.0) - hazard1)
"""
Not used for this model, but included in case somebody needs it
For math, see https://ragulpr.github.io/assets/draft_master_thesis_martinsson_egil_wtte_rnn_2016.pdf (Page 35)
"""
def weibull_loglik_continuous(y_true, ab_pred, name=None):
y_ = y_true[:, 0]
u_ = y_true[:, 1]
a_ = ab_pred[:, 0]
b_ = ab_pred[:, 1]
ya = (y_ + 1e-35) / a_
return -1 * k.mean(u_ * (k.log(b_) + b_ * k.log(ya)) - k.pow(ya, b_))
"""
Custom Keras activation function, outputs alpha neuron using exponentiation and beta using softplus
"""
def activate(ab):
a = k.exp(ab[:, 0])
b = k.softplus(ab[:, 1])
a = k.reshape(a, (k.shape(a)[0], 1))
b = k.reshape(b, (k.shape(b)[0], 1))
return k.concatenate((a, b), axis=1)
"""
Load and parse engine data files into:
- an (engine/day, observed history, sensor readings) x tensor, where observed history is 100 days, zero-padded
for days that don't have a full 100 days of observed history (e.g., first observed day for an engine)
- an (engine/day, 2) tensor containing time-to-event and 1 (since all engines failed)
There are probably MUCH better ways of doing this, but I don't use Numpy that much, and the data parsing isn't the
point of this demo anyway.
"""
def load_file(name):
with open(name, 'r') as file:
return np.loadtxt(file, delimiter=',')
np.set_printoptions(suppress=True, threshold=10000)
train = load_file('train.csv')
test_x = load_file('test_x.csv')
test_y = load_file('test_y.csv')
# Combine the X values to normalize them, then split them back out
all_x = np.concatenate((train[:, 2:26], test_x[:, 2:26]))
all_x = normalize(all_x, axis=0)
train[:, 2:26] = all_x[0:train.shape[0], :]
test_x[:, 2:26] = all_x[train.shape[0]:, :]
# Make engine numbers and days zero-indexed, for everybody's sanity
train[:, 0:2] -= 1
test_x[:, 0:2] -= 1
# Configurable observation look-back period for each engine/day
max_time = 100
def build_data(engine, time, x, max_time, is_test):
# y[0] will be days remaining, y[1] will be event indicator, always 1 for this data
out_y = np.empty((0, 2), dtype=np.float32)
# A full history of sensor readings to date for each x
out_x = np.empty((0, max_time, 24), dtype=np.float32)
for i in range(100):
print("Engine: " + str(i))
# When did the engine fail? (Last day + 1 for train data, irrelevant for test.)
max_engine_time = int(np.max(time[engine == i])) + 1
if is_test:
start = max_engine_time - 1
else:
start = 0
this_x = np.empty((0, max_time, 24), dtype=np.float32)
for j in range(start, max_engine_time):
engine_x = x[engine == i]
out_y = np.append(out_y, np.array((max_engine_time - j, 1), ndmin=2), axis=0)
xtemp = np.zeros((1, max_time, 24))
xtemp[:, max_time-min(j, 99)-1:max_time, :] = engine_x[max(0, j-max_time+1):j+1, :]
this_x = np.concatenate((this_x, xtemp))
out_x = np.concatenate((out_x, this_x))
return out_x, out_y
train_x, train_y = build_data(train[:, 0], train[:, 1], train[:, 2:26], max_time, False)
test_x = build_data(test_x[:, 0], test_x[:, 1], test_x[:, 2:26], max_time, True)[0]
train_u = np.zeros((100, 1), dtype=np.float32)
train_u += 1
test_y = np.append(np.reshape(test_y, (100, 1)), train_u, axis=1)
"""
Here's the rest of the meat of the demo... actually fitting and training the model.
We'll also make some test predictions so we can evaluate model performance.
"""
# Start building our model
model = Sequential()
# Mask parts of the lookback period that are all zeros (i.e., unobserved) so they don't skew the model
model.add(Masking(mask_value=0., input_shape=(max_time, 24)))
# LSTM is just a common type of RNN. You could also try anything else (e.g., GRU).
model.add(LSTM(20, input_dim=24))
# We need 2 neurons to output Alpha and Beta parameters for our Weibull distribution
model.add(Dense(2))
# Apply the custom activation function mentioned above
model.add(Activation(activate))
# Use the discrete log-likelihood for Weibull survival data as our loss function
model.compile(loss=weibull_loglik_discrete, optimizer=RMSprop(lr=.001))
# Fit!
model.fit(train_x, train_y, nb_epoch=250, batch_size=2000, verbose=2, validation_data=(test_x, test_y))
# Make some predictions and put them alongside the real TTE and event indicator values
test_predict = model.predict(test_x)
test_predict = np.resize(test_predict, (100, 2))
test_result = np.concatenate((test_y, test_predict), axis=1)
# TTE, Event Indicator, Alpha, Beta
print(test_result)