-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCNN_functions.py
More file actions
296 lines (218 loc) · 11 KB
/
Copy pathCNN_functions.py
File metadata and controls
296 lines (218 loc) · 11 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Feb 19 09:58:26 2021
@author: stephanhavermans
"""
# The following script contains the code for a convolutional neural network (CNN) that uses backpropagation written without the use of functions from traditional
# Deep learning packages such as TensorFlow or PyTorch. The code was originally created as part of an assignment in a deep learning course that would reward extra
# credits if completed successfully.
from tensorflow import keras
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
import random
from tqdm import tqdm
## CNN functions
def convolution(inputs, kernels):
spread = math.floor(kernels.shape[0]/2)
assert kernels.shape[2] == inputs.shape[2]
feature_maps = np.zeros((inputs.shape[0] - 2 * spread, inputs.shape[0] - 2 * spread,
kernels.shape[3]))
# Apply each convolution in turn
for k in range(kernels.shape[3]):
# Iterate over each image
for fm_number in range(inputs.shape[2]):
# Iterate over each potential focus cell in the input
for i in range(inputs.shape[0]):
for j in range(inputs.shape[1]):
# Ignore the cell if the kernel cannot be applied (too close to border)
if ((i-spread)<0 or (i+spread)>inputs.shape[0]-1
) or ((j-spread)<0 or (j+spread)>inputs.shape[1]-1):
continue
else:
# Update the output cell for the ouput feature map
# corresponding to that convolution and location
feature_maps[i-spread, j-spread, k] += np.sum(
kernels[:, :, :, k][:, :, fm_number] *
inputs[i-spread:i+spread+1, j-spread:j+spread+1, fm_number])
return feature_maps
#Rectified Linear Unit (ReLU) activation function
def relu(feature_maps):
return np.maximum(0,feature_maps)
def max_pooling(feature_maps, pool_size):
fm_max_pooled = np.zeros((math.floor(feature_maps.shape[0]/pool_size[0]),
math.floor(feature_maps.shape[0]/pool_size[0]),
feature_maps.shape[2]))
# Iterate over the input feature maps
for z in range(feature_maps.shape[2]):
curr_x = output_x = 0
# Iterate over each location in the map, set x coordinate
while curr_x + pool_size[0] <= feature_maps.shape[0]:
curr_y = output_y = 0
# Then iterate over the y coordinates
while curr_y + pool_size[0] <= feature_maps.shape[0]:
# Pick up the maximum value within the window and update the output
fm_max_pooled[output_x, output_y, z] = np.max(feature_maps[curr_x:curr_x+pool_size[0],
curr_y:curr_y+pool_size[0],
z])
curr_y += pool_size[0]
output_y += 1
curr_x += pool_size[0]
output_x += 1
return fm_max_pooled
def normalise(feature_maps):
for fm_number in range(feature_maps.shape[2]):
if np.std(feature_maps[:, :, fm_number]) == 0:
feature_maps[:, :, fm_number] = feature_maps[:, :, fm_number] - np.mean(feature_maps[:, :, fm_number])
else:
feature_maps[:, :, fm_number] = (feature_maps[:, :, fm_number] - np.mean(feature_maps[:, :, fm_number])) / np.std(feature_maps[:, :, fm_number])
return feature_maps
#Weights have to be in order (# of output nodes, weight values) --> (10, 450)
def fully_connected_layer(feature_maps, weights):
return weights.dot(feature_maps.reshape((np.prod(feature_maps.shape),1)))
def softmax(activation_layer):
return np.exp(activation_layer) / np.sum(np.exp(activation_layer))
def convolutionBackward(dconv_prev, conv_input, f):
# Dimension information
(f_dim, _, f_per_input, f_count) = f.shape
(fm_dim, _, _) = conv_input.shape
# Initialise variables to contain gradients/derivatives
dout = np.zeros(conv_input.shape)
dfilt = np.zeros(filters.shape)
# Iterate over each filter and image location
for f in range(f_count):
x = 0
while x + f_dim <= fm_dim:
y = 0
while y + f_dim <= fm_dim:
# Loss gradient of filter
dfilt[:, :, :, f] += dconv_prev[x, y, f] * conv_input[x:x+f_dim, y:y+f_dim, :]
# Loss gradient of the input to the convolution operation
dout[x:x+f_dim, y:y+f_dim, :] += dconv_prev[x, y, f] * filters[:, :, :, f]
y += 1
x += 1
return dout, dfilt
def nanargmax(arr):
# Return index of the largest non-nan value in the array.
idx = np.nanargmax(arr)
idxs = np.unravel_index(idx, arr.shape)
return idxs
def maxpoolBackward(dpool, orig, pool_dim, s=2):
# Dimensions and output
(fm_dim, _, fm_count) = orig.shape
dout = np.zeros(orig.shape)
# Iterate over the feature_maps (and locations)
for fm in range(fm_count):
curr_x = output_x = 0
while curr_x + pool_dim <= fm_dim:
curr_y = output_y = 0
while curr_y + pool_dim <= fm_dim:
# Find the index of max value from the input for focus area
(a, b) = nanargmax(orig[curr_x:curr_x+pool_dim, curr_y:curr_y+pool_dim, fm])
# Update the reverse max pool output at that location
dout[curr_x+a, curr_y+b, fm] = dpool[output_x, output_y, fm]
curr_y += s
output_y += 1
curr_x += s
output_x += 1
return dout
# A convolutional neural network with two convolutional layers, a fully connected layer,
# and an output layer with pooling, thresholding and normalisation. The function gives
# the accuracy of the labels as an output.
def cnn(images, labels, f1, f2, w, pool_size, lr, decay, batch_size):
# Set variable to assess accuracy
acc_count = 0
# Number of batches
batches = math.ceil(images.shape[0] / batch_size)
batch_count = 0
# Shuffle images/labels
p = np.random.permutation(len(images))
images = images[p]
labels = labels[p]
for n in tqdm(range(batches)):
# Images and labels for the current batch
images_batch = images[n * batch_size:(n * batch_size) + batch_size]
labels_batch = labels[n * batch_size:(n * batch_size) + batch_size]
# Initialise gradient totals for parameters
df1_batch = np.zeros((f1.shape))
df2_batch = np.zeros((f2.shape))
dw_batch = np.zeros((w.shape))
for i in range(len(images_batch)):
image = images_batch[i]
label = labels_batch[i]
#### Feed-forward
conv1 = convolution(image, f1) # first convolutional layer
conv1 = relu(conv1) # ReLU
conv2 = convolution(conv1, f2) # second convolutional layer
conv2 = relu(conv2) # ReLU
pooled = max_pooling(conv2, pool_size) # max pooling
norm = normalise(pooled) # normalisation
fc = fully_connected_layer(norm, w) # fully connected layer
probs = softmax(fc) # output probs
# Check prediction
if np.argmax(probs) == np.argmax(label):
acc_count += 1
#### Backpropagation
dout = probs - label.reshape(len(label), 1) # derivs of CCE and softmax
# Gradients of fc layer weights, product of output derivs and (flat) activations
dw = (dout.reshape(len(dout),1)).dot(norm.reshape(np.prod(norm.shape),1).T)
dfc = w.T.dot(dout) # derivative of the activations of the fully connected layer
dpool = dfc.reshape(pooled.shape) # un-flatten
dconv2 = maxpoolBackward(dpool, conv2, 2, s=2) # reverse max pooling
dconv2[conv2<=0] = 0 # ReLU
dconv1, df2 = convolutionBackward(dconv2, conv1, f2) # backpropagation conv2
dconv1[conv1<=0] = 0 # ReLU
dimage, df1 = convolutionBackward(dconv1, image, f1) # backpropagation conv1
# Add image gradients to batch gradient totals
df1_batch += df1
df2_batch += df2
dw_batch += dw
# Calculate mean update for mini-batch gradient descent
df1_update = df1_batch / len(images_batch)
df2_update = df2_batch / len(images_batch)
dw_update = dw_batch / len(images_batch)
# Update parameters
f1 -= lr * df1_update
f2 -= lr * df2_update
w -= lr * dw_update
# Update learning rate
batch_count += 1
lr = lr * (1.0 / (1.0 + decay * batch_count))
# Calculate accuracy
accuracy = round((acc_count / len(images)) * 100, 2)
return [f1, f2, w, accuracy]
# Define functions to initialise parameters
def initialiseFilter(size, scale = 1.0):
# Initialise filter: normal dist. with sd inversely proportional the square root of the number of units
stddev = scale/np.sqrt(np.prod(size))
return np.random.normal(loc = 0, scale = stddev, size = size)
def initialiseWeights(size):
# Initialize weights with a random normal distribution
return np.random.standard_normal(size=size) * 0.01
# Define a function to predict categories and output test accuracy
def predict(images, labels, f1, f2, w, pool_size):
# Set variables to assess accuracy and store predictions
acc_count = 0
predictions = []
for i in range(len(images)):
image = images[i]
label = labels[i]
#### Feed-forward
conv1 = convolution(image, f1) # first convolutional layer
conv1 = relu(conv1) # ReLU
conv2 = convolution(conv1, f2) # second convolutional layer
conv2 = relu(conv2) # ReLU
pooled = max_pooling(conv2, pool_size) # max pooling
norm = normalise(pooled) # normalisation
fc = fully_connected_layer(norm, w) # fully connected layer
probs = softmax(fc) # output probs
# Check prediction
if np.argmax(probs) == np.argmax(label):
acc_count += 1
# Add prediction to list
predictions.append(np.argmax(probs))
# Calculate accuracy
accuracy = round((acc_count / len(images)) * 100, 2)
return [accuracy, predictions]