-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnn.py
More file actions
381 lines (308 loc) · 12.3 KB
/
nn.py
File metadata and controls
381 lines (308 loc) · 12.3 KB
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
import numpy as np
def main():
"""
This is sample code for linear regression, which demonstrates how to use the
Graph class.
"""
# This is our data, where x is a 4x2 matrix and y is a 4x1 matrix
x = np.array([[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.]])
y = np.dot(x, np.array([[7.],
[8.]])) + 3
# Let's construct a simple model to approximate a function from 2D
# points to numbers, f(x) = x_0 * m_0 + x_1 * m_1 + b
# Here m and b are variables (trainable parameters):
m = Variable(2,1)
b = Variable(1)
# We train our network using batch gradient descent on our data
for iteration in range(10000):
# At each iteration, we first calculate a loss that measures how
# good our network is. The graph keeps track of all operations used
graph = Graph([m, b])
input_x = Input(graph, x)
input_y = Input(graph, y)
xm = MatrixMultiply(graph, input_x, m)
xm_plus_b = MatrixVectorAdd(graph, xm, b)
loss = SquareLoss(graph, xm_plus_b, input_y)
# Then we use the graph to perform backprop and update our variables
graph.backprop()
graph.step(0.01)
# After training, we should have recovered m=[[7],[8]] and b=[3]
print("Final values are: {}".format([m.data[0,0], m.data[1,0], b.data[0]]))
assert np.isclose(m.data[0,0], 7)
assert np.isclose(m.data[1,0], 8)
assert np.isclose(b.data[0], 3)
print("Success!")
class Graph(object):
"""
A graph that keeps track of the computations performed by a neural network
in order to implement back-propagation.
Each evaluation of the neural network (during both training and test-time)
will create a new Graph. The computation will add nodes to the graph, where
each node is either a DataNode or a FunctionNode.
A DataNode represents a trainable parameter or an input to the computation.
A FunctionNode represents doing a computation based on two previous nodes in
the graph.
The Graph is responsible for keeping track of all nodes and the order they
are added to the graph, for computing gradients using back-propagation, and
for performing updates to the trainable parameters.
For an example of how the Graph can be used, see the function `main` above.
"""
def __init__(self, variables):
"""
Initializes a new computation graph.
variables: a list of Variable objects that store the trainable parameters
for the neural network.
"""
self.nodes = []
for variable in variables:
self.add(variable)
def get_nodes(self):
"""
Returns a list of all nodes that have been added to this Graph, in the
order they were added. This list should include all of the Variable
nodes that were passed to `Graph.__init__`.
Returns: a list of nodes
"""
return self.nodes
def get_inputs(self, node):
"""
Retrieves the inputs to a node in the graph. Assume the `node` has
already been added to the graph.
Returns: a list of numpy arrays
"""
return [parent.output for parent in node.get_parents()]
def get_output(self, node):
"""
Retrieves the output to a node in the graph. Assume the `node` has
already been added to the graph.
Returns: a numpy array or a scalar
"""
return node.output
def get_gradient(self, node):
"""
Retrieves the gradient for a node in the graph. Assume the `node` has
already been added to the graph.
If `Graph.backprop` has already been called, this should return the
gradient of the loss with respect to the output of the node. If
`Graph.backprop` has not been called, it should instead return a numpy
array with correct shape to hold the gradient, but with all entries set
to zero.
Returns: a numpy array
"""
return node.gradient
def add(self, node):
"""
Adds a node to the graph.
This method should calculate and remember the output of the node in the
forwards pass (which can later be retrieved by calling `get_output`)
We compute the output here because we only want to compute it once,
whereas we may wish to call `get_output` multiple times.
Additionally, this method should initialize an all-zero gradient
accumulator for the node, with correct shape.
"""
self.nodes.append(node)
node.output = node.forward(self.get_inputs(node))
node.gradient = np.zeros_like(node.output)
def backprop(self):
"""
Runs back-propagation. Assume that the very last node added to the graph
represents the loss.
After back-propagation completes, `get_gradient(node)` should return the
gradient of the loss with respect to the `node`.
"""
loss_node = self.get_nodes()[-1]
assert np.asarray(self.get_output(loss_node)).ndim == 0
loss_node.gradient = 1.0
for node in reversed(self.get_nodes()):
partialDerivs = node.backward(self.get_inputs(node), self.get_gradient(node))
parents = node.get_parents()
for i in range(len(parents)):
parents[i].gradient += partialDerivs[i]
def step(self, step_size):
"""
Updates the values of all variables based on computed gradients.
Assume that `backprop()` has already been called, and that gradients
have already been computed.
"""
for node in self.get_nodes():
if type(node) is Variable:
node.data -= node.gradient * step_size
class DataNode(object):
"""
DataNode is the parent class for Variable and Input nodes.
Each DataNode must define a `.data` attribute, which represents the data
stored at the node.
"""
@staticmethod
def get_parents():
# A DataNode has no parent nodes, only a `.data` attribute
return []
def forward(self, inputs):
# The forwards pass for a data node simply returns its data
return self.data
@staticmethod
def backward(inputs, gradient):
# A DataNode has no parents or inputs, so there are no gradients to
# compute in the backwards pass
return []
class Variable(DataNode):
"""
A Variable stores parameters used in a neural network.
Variables should be created once and then passed to all future Graph
constructors. Use `.data` to access or modify the numpy array of parameters.
"""
def __init__(self, *shape):
"""
Initializes a Variable with a given shape.
For example, Variable(5) will create 5-dimensional vector variable,
while Variable(10, 10) will create a 10x10 matrix variable.
The initial value of the variable before training starts can have a big
effect on how long the network takes to train. The provided initializer
works well across a wide range of applications.
"""
assert shape
limit = np.sqrt(3.0 / np.mean(shape))
self.data = np.random.uniform(low=-limit, high=limit, size=shape)
class Input(DataNode):
"""
An Input node packages a numpy array into a node in a computation graph.
Use this node for inputs to your neural network.
For trainable parameters, use Variable instead.
"""
def __init__(self, graph, data):
"""
Initializes a new Input and adds it to a graph.
"""
assert isinstance(data, np.ndarray), "data must be a numpy array"
assert data.dtype.kind == "f", "data must have floating-point entries"
self.data = data
graph.add(self)
class FunctionNode(object):
"""
A FunctionNode represents a value that is computed based on other nodes in
the graph. Each function must implement both a forward and backward pass.
"""
def __init__(self, graph, *parents):
self.parents = parents
graph.add(self)
def get_parents(self):
return self.parents
@staticmethod
def forward(inputs):
raise NotImplementedError
@staticmethod
def backward(inputs, gradient):
raise NotImplementedError
class Add(FunctionNode):
"""
Adds two vectors or matrices, element-wise
Inputs: [x, y]
x may represent either a vector or a matrix
y must have the same shape as x
Output: x + y
"""
@staticmethod
def forward(inputs):
return inputs[0] + inputs[1]
@staticmethod
def backward(inputs, gradient):
return [np.array(gradient), np.array(gradient)]
class MatrixMultiply(FunctionNode):
"""
Represents matrix multiplication.
Inputs: [A, B]
A represents a matrix of shape (n x m)
B represents a matrix of shape (m x k)
Output: a matrix of shape (n x k)
"""
@staticmethod
def forward(inputs):
return np.dot(inputs[0], inputs[1])
@staticmethod
def backward(inputs, gradient):
return [np.dot(gradient, inputs[1].T), np.dot(inputs[0].T, gradient)]
class MatrixVectorAdd(FunctionNode):
"""
Adds a vector to each row of a matrix.
Inputs: [A, x]
A represents a matrix of shape (n x m)
x represents a vector (m)
Output: a matrix of shape (n x m)
"""
@staticmethod
def forward(inputs):
return inputs[0] + inputs[1]
@staticmethod
def backward(inputs, gradient):
return [np.array(gradient), np.sum(gradient, axis = 0)]
class ReLU(FunctionNode):
"""
An element-wise Rectified Linear Unit nonlinearity: max(x, 0).
This nonlinearity replaces all negative entries in its input with zeros.
Input: [x]
x represents either a vector or matrix
Output: same shape as x, with no negative entries
"""
@staticmethod
def forward(inputs):
return np.maximum(inputs[0], 0)
@staticmethod
def backward(inputs, gradient):
return [np.where(inputs[0] < 0, 0, gradient)]
class SquareLoss(FunctionNode):
"""
Inputs: [a, b]
a represents a matrix of size (batch_size x dim)
b must have the same shape as a
Output: a number
This node first computes 0.5 * (a[i,j] - b[i,j])**2 at all positions (i,j)
in the inputs, which creates a (batch_size x dim) matrix. It then calculates
and returns the mean of all elements in this matrix.
"""
@staticmethod
def forward(inputs):
diff = np.subtract(inputs[0], inputs[1])
result = 0.5 * diff ** 2
return np.mean(result)
@staticmethod
def backward(inputs, gradient):
partialA = gradient * (inputs[0] - inputs[1]) / len(inputs[0])
partialB = gradient * (inputs[1] - inputs[0]) / len(inputs[1])
return [partialA, partialB]
class SoftmaxLoss(FunctionNode):
"""
A batched softmax loss, used for classification problems.
Inputs: [logits, labels]
logits: a (batch_size x num_classes) matrix of scores, that is typically
calculated based on previous layers. Each score can be an arbitrary
real number.
labels: a (batch_size x num_classes) matrix that encodes the correct
labels for the examples. All entries must be non-negative and the
sum of values along each row should be 1.
Output: a number
"""
@staticmethod
def softmax(input):
exp = np.exp(input - np.max(input, axis=1, keepdims=True))
return exp / np.sum(exp, axis=1, keepdims=True)
@staticmethod
def forward(inputs):
softmax = SoftmaxLoss.softmax(inputs[0])
labels = inputs[1]
assert np.all(labels >= 0), \
"Labels input to SoftmaxLoss must be non-negative. (Did you pass the inputs in the right order?)"
assert np.allclose(np.sum(labels, axis=1), np.ones(labels.shape[0])), \
"Labels input to SoftmaxLoss do not sum to 1 along each row. (Did you pass the inputs in the right order?)"
return np.mean(-np.sum(labels * np.log(softmax), axis=1))
@staticmethod
def backward(inputs, gradient):
softmax = SoftmaxLoss.softmax(inputs[0])
return [
gradient * (softmax - inputs[1]) / inputs[0].shape[0],
gradient * (-np.log(softmax)) / inputs[0].shape[0]
]
if __name__ == '__main__':
main()