I created my first implementation of an arbitrary feed forward neural network with a simple back-propagation training implementation.
class NeuralNet(object):
learning_rate = .1
# Error function
@staticmethod
def J(guess, target):
return 0.5 * np.linalg.norm(guess - target, 2) ** 2
def __init__(self, structure=tuple(), activation_functions=tuple(), activation_derivatives=tuple()):
self.w = []
self.b = []
self.f = list(activation_functions)
self.df = list(activation_derivatives)
li = len(structure) - 1
while li > 0:
self.w.insert(0, np.random.uniform(low=-1.0, high=1.0, size=(structure[li - 1], structure[li])))
self.b.insert(0, np.random.uniform(low=-1.0, high=1.0, size=structure[li]))
li -= 1
def forward(self, x):
z = [0] * len(self.w)
a = [x]
for i, (wi, bi, fi) in enumerate(zip(self.w, self.b, self.f)):
z[i] = a[i] @ wi + bi
a.append(fi(z[i]))
return a[-1]
def train(self, training_data, labels):
# Get a permutation vector for shuffling the inputs and labels in each epoch:
permutation = np.random.permutation(len(inputs))
# Keeping track of all MSE values:
errors = []
# Training loop:
for epoch in range(10000):
# Shuffling the inputs and labels for each epoch:
X = training_data[permutation]
Y = labels[permutation]
# n
# Keeping track of the error: MSE = 1/n * SUM ||Activation - Target|| ** 2
# i=1
error = 0.0
for xi, yi in zip(X, Y):
# Forward pass:
z = [0] * len(self.w)
a = [xi]
for i, (wi, bi, fi) in enumerate(zip(self.w, self.b, self.f)):
z[i] = a[i] @ wi + bi
a.append(fi(z[i]))
# Calculate error for (xi, yi) according to 0.5 * ||xi - yi|| ** 2
error += self.J(a[-1], yi)
# Backwards pass:
# - Calculate deltas:
layer_delta = []
iter_zi = iter(zip(reversed(range(len(z))), reversed(z)))
layer_delta.insert(0, (a[-1] - yi) * self.df[1](next(iter_zi)[1]))
for i, zi in iter_zi:
delta_i = np.multiply(layer_delta[0] @ self.w[i+1].T, self.df[i](z[i]))
layer_delta.insert(0, delta_i)
# - Calculate weight deltas:
weight_delta = []
for i, di in zip(reversed(range(len(layer_delta))), reversed(layer_delta)):
delta_wi = a[i].reshape(-1, 1) * layer_delta[i]
weight_delta.insert(0, delta_wi)
# w[i](new) := w[i](old) - LR * dJ/dw[i]
# b[i](new) := b[i](old) - LR * dJ/db[i]
for i in range(len(self.w)):
self.w[i] = self.w[i] - self.learning_rate * weight_delta[i]
self.b[i] = self.b[i] - self.learning_rate * layer_delta[i]
errors.append(error / len(X))
# Convergence testing, according to the last N errors:
error_delta = sum(reversed(errors[-5:]))
if error_delta < 1.0e-6:
print("Error delta reached, ", epoch, " exiting.")
break
return errors
This seem to work just fine, at least it successfully learnt the XOR problem:
# Inputs and their respective labels:
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
labels = np.array([[0], [1], [1], [0]])
nn = NeuralNet(structure=(2, 2, 1),
activation_functions=(lambda x: np.tanh(x),
lambda x: .25 * x if x < 0 else x),
activation_derivatives=(lambda x: 1.0 - np.tanh(x) ** 2,
lambda x: .25 if x < 0 else 1))
nn.learning_rate = 0.1
print("Before training:")
print(nn.forward(np.array([0, 0])))
print(nn.forward(np.array([0, 1])))
print(nn.forward(np.array([1, 0])))
print(nn.forward(np.array([1, 1])))
errors = nn.train(inputs, labels)
print("After training:")
print(nn.forward(np.array([0, 0])))
print(nn.forward(np.array([0, 1])))
print(nn.forward(np.array([1, 0])))
print(nn.forward(np.array([1, 1])))
plt.plot(errors)
plt.show()
The calculations for layer_deltas seem particularly complicated, but whenever I try to simplify it, I break something. Any tips I could simplify that or any tips for the above code in general?