Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NN v1.14.0 Update matrix maths #35

Merged
merged 8 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ This library uses Pytest for the unit tests.
These tests are located in the `tests` directory.
To run the tests:

python -m pytest tests -vx --cov --cov-report term-missing
python -m pytest tests

## Linting and Formatting
This library uses `ruff` for linting and formatting.
Expand Down
4 changes: 2 additions & 2 deletions neural_network/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ def backpropagate_error(self, errors: Matrix, learning_rate: float) -> None:
activation=self._activation, layer_vals=self._layer_output, errors=errors, lr=learning_rate
)
delta = nn_math.calculate_delta(layer_vals=self._layer_input, gradients=gradient)
self.weights = Matrix.add(self.weights, delta)
self.bias = Matrix.add(self.bias, gradient)
self.weights = self.weights + delta
self.bias = self.bias + gradient

def feedforward(self, vals: Matrix) -> Matrix:
"""
Expand Down
115 changes: 24 additions & 91 deletions neural_network/math/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,31 @@ class Matrix:
This class handles the matrix mathematics required to pass data through neural networks.
"""

def __init__(self, rows: int, cols: int, vals: NDArray | None = None) -> None:
def __init__(self, vals: NDArray) -> None:
"""
Initialise Matrix with number of rows and columns, and optionally the matrix values.

Parameters:
rows (int): Number of rows in matrix
cols (int): Number of columns in matrix
vals (NDArray | None): Matrix values if specified
vals (NDArray): Matrix values
"""
self._rows = rows
self._cols = cols
self._vals = vals
self.vals = vals

def __str__(self) -> str:
return str(self.vals)

@property
def vals(self) -> NDArray:
if self._vals is None:
self._vals = np.zeros(shape=self.shape)
return self._vals
def __add__(self, other: Matrix) -> Matrix:
return Matrix.from_array(self.vals + other.vals)

def __sub__(self, other: Matrix) -> Matrix:
return Matrix.from_array(self.vals - other.vals)

def __mul__(self, other: float | int | Matrix) -> Matrix:
if isinstance(other, Matrix):
return Matrix.from_array(self.vals * other.vals)
return Matrix.from_array(self.vals * other)

def __matmul__(self, other: Matrix) -> Matrix:
return Matrix.from_array(self.vals @ other.vals)

@property
def as_list(self) -> list[float]:
Expand All @@ -44,7 +48,7 @@ def as_list(self) -> list[float]:

@property
def shape(self) -> tuple:
return (self._rows, self._cols)
return self.vals.shape

@classmethod
def from_array(cls, matrix_array: NDArray | list[list[float]] | list[float]) -> Matrix:
Expand All @@ -58,13 +62,9 @@ def from_array(cls, matrix_array: NDArray | list[list[float]] | list[float]) ->
matrix (Matrix): Matrix with assigned values
"""
matrix_array = np.array(matrix_array, dtype=object)
try:
_rows, _cols = matrix_array.shape
except ValueError:
if matrix_array.ndim == 1:
matrix_array = np.expand_dims(matrix_array, axis=1)
_rows, _cols = matrix_array.shape

return cls(_rows, _cols, matrix_array)
return cls(matrix_array)

@classmethod
def random_matrix(cls, rows: int, cols: int, low: float, high: float) -> Matrix:
Expand All @@ -80,8 +80,7 @@ def random_matrix(cls, rows: int, cols: int, low: float, high: float) -> Matrix:
Returns:
matrix (Matrix): Matrix with random values
"""
_vals = rng.uniform(low=low, high=high, size=(rows, cols))
return cls.from_array(_vals)
return cls.from_array(rng.uniform(low=low, high=high, size=(rows, cols)))

@classmethod
def random_column(cls, rows: int, low: float, high: float) -> Matrix:
Expand All @@ -98,68 +97,6 @@ def random_column(cls, rows: int, low: float, high: float) -> Matrix:
"""
return cls.random_matrix(rows=rows, cols=1, low=low, high=high)

@staticmethod
def add(matrix: Matrix, other_matrix: Matrix) -> Matrix:
"""
Add two Matrix objects.

Parameters:
matrix (Matrix): Matrix to use in sum
other_matrix (Matrix): Other Matrix to use in sum

Returns:
new_matrix (Matrix): Sum of both matrices
"""
new_matrix = matrix.vals + other_matrix.vals
return Matrix.from_array(new_matrix)

@staticmethod
def subtract(matrix: Matrix, other_matrix: Matrix) -> Matrix:
"""
Subtract two Matrix objects.

Parameters:
matrix (Matrix): Matrix to use in subtraction
other_matrix (Matrix): Other Matrix to use in subtraction

Returns:
new_matrix (Matrix): Difference between both matrices
"""
new_matrix = matrix.vals - other_matrix.vals
return Matrix.from_array(new_matrix)

@staticmethod
def multiply(matrix: Matrix, val: Matrix | float) -> Matrix:
"""
Multiply Matrix with scalar or Matrix.

Parameters:
matrix (Matrix): Matrix to to use for multiplication
val (Matrix | float): Matrix or scalar to use for multiplication

Returns:
new_matrix (Matrix): Multiplied Matrix
"""
if isinstance(val, Matrix):
val = val.vals
new_matrix = matrix.vals.dot(val)
return Matrix.from_array(new_matrix)

@staticmethod
def multiply_element_wise(matrix: Matrix, other_matrix: Matrix) -> Matrix:
"""
Multiply Matrix element wise with Matrix.

Parameters:
matrix (Matrix): Matrix to use for multiplication
other_matrix (Matrix): Other Matrix to use for multiplication

Returns:
new_matrix (Matrix): Multiplied Matrix
"""
new_matrix = matrix.vals * other_matrix.vals
return Matrix.from_array(new_matrix)

@staticmethod
def transpose(matrix: Matrix) -> Matrix:
"""
Expand All @@ -171,8 +108,7 @@ def transpose(matrix: Matrix) -> Matrix:
Returns:
new_matrix (Matrix): Transposed Matrix
"""
new_matrix = matrix.vals.transpose()
return Matrix.from_array(new_matrix)
return Matrix.from_array(matrix.vals.transpose())

@staticmethod
def map(matrix: Matrix, activation: ActivationFunction) -> Matrix:
Expand All @@ -186,8 +122,7 @@ def map(matrix: Matrix, activation: ActivationFunction) -> Matrix:
Returns:
new_matrix (Matrix): Matrix with mapped values
"""
new_matrix = np.vectorize(activation.func)(matrix.vals)
return Matrix.from_array(new_matrix)
return Matrix.from_array(np.vectorize(activation.func)(matrix.vals))

@staticmethod
def average_matrix(matrix: Matrix, other_matrix: Matrix) -> Matrix:
Expand All @@ -201,8 +136,7 @@ def average_matrix(matrix: Matrix, other_matrix: Matrix) -> Matrix:
Returns:
new_matrix (Matrix): Average of both matrices
"""
new_matrix = np.average([matrix.vals, other_matrix.vals], axis=0)
return Matrix.from_array(new_matrix)
return Matrix.from_array(np.average([matrix.vals, other_matrix.vals], axis=0))

@staticmethod
def mutated_matrix(matrix: Matrix, mutation_rate: float, random_range: list[float]) -> Matrix:
Expand Down Expand Up @@ -283,5 +217,4 @@ def shift_vals(self, shift: float) -> None:
Parameters:
shift (float): Factor to shift values by
"""
_mult_array = rng.uniform(low=(1 - shift), high=(1 + shift), size=self.shape)
self._vals = _mult_array
self.vals *= rng.uniform(low=(1 - shift), high=(1 + shift), size=self.shape)
15 changes: 6 additions & 9 deletions neural_network/math/nn_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def feedforward_through_layer(
Returns:
output_vals (Matrix): Output values
"""
output_vals = Matrix.multiply(weights, input_vals)
output_vals = Matrix.add(output_vals, bias)
output_vals = weights @ input_vals
output_vals = output_vals + bias
return Matrix.map(output_vals, activation)


Expand All @@ -38,8 +38,7 @@ def calculate_gradient(layer_vals: Matrix, errors: Matrix, activation: Activatio
gradient (Matrix): Gradient values
"""
gradient = Matrix.from_array(np.vectorize(activation.derivative)(layer_vals.vals))
gradient = Matrix.multiply_element_wise(gradient, errors)
return Matrix.multiply(gradient, lr)
return gradient * errors * lr


def calculate_delta(layer_vals: Matrix, gradients: Matrix) -> Matrix:
Expand All @@ -53,8 +52,7 @@ def calculate_delta(layer_vals: Matrix, gradients: Matrix) -> Matrix:
Returns:
delta (Matrix): Delta factors
"""
incoming_transposed = Matrix.transpose(layer_vals)
return Matrix.multiply(gradients, incoming_transposed)
return gradients @ Matrix.transpose(layer_vals)


def calculate_error_from_expected(expected_outputs: Matrix, actual_outputs: Matrix) -> Matrix:
Expand All @@ -68,7 +66,7 @@ def calculate_error_from_expected(expected_outputs: Matrix, actual_outputs: Matr
Returns:
errors (Matrix): Difference between expected and actual outputs
"""
return Matrix.subtract(expected_outputs, actual_outputs)
return expected_outputs - actual_outputs


def calculate_next_errors(weights: Matrix, calculated_errors: Matrix) -> Matrix:
Expand All @@ -82,5 +80,4 @@ def calculate_next_errors(weights: Matrix, calculated_errors: Matrix) -> Matrix:
Returns:
errors (Matrix): Next errors
"""
weights_t = Matrix.transpose(weights)
return Matrix.multiply(weights_t, calculated_errors)
return Matrix.transpose(weights) @ calculated_errors
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "neural-network"
version = "1.13.1"
version = "1.14.0"
description = "An artificial neural network library in Python."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
18 changes: 7 additions & 11 deletions tests/math/test_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@


class TestMatrix:
def test_given_no_vals_when_creating_matrix_then_check_matrix_has_zero_vals(
self, mock_len_inputs: int, mock_len_outputs: int
) -> None:
test_matrix = Matrix(rows=mock_len_inputs, cols=mock_len_outputs)
assert not np.any(test_matrix.vals)

def test_given_shape_when_creating_random_matrix_then_check_matrix_has_correct_shape(
self, mock_weights_range: list[float], mock_len_inputs: int, mock_len_outputs: int
) -> None:
Expand Down Expand Up @@ -65,7 +59,7 @@ def test_given_two_matrices_when_adding_then_check_new_matrix_correctly_calculat

matrix_1 = Matrix.from_array(array_1)
matrix_2 = Matrix.from_array(array_2)
new_matrix = Matrix.add(matrix_1, matrix_2)
new_matrix = matrix_1 + matrix_2

expected_vals = np.array([[0, 3], [6, -2]])
actual_vals = new_matrix.vals
Expand All @@ -77,7 +71,7 @@ def test_given_two_matrices_when_subtracting_then_check_new_matrix_correctly_cal

matrix_1 = Matrix.from_array(array_1)
matrix_2 = Matrix.from_array(array_2)
new_matrix = Matrix.subtract(matrix_1, matrix_2)
new_matrix = matrix_1 - matrix_2

expected_vals = np.array([[2, 1], [2, 8]])
actual_vals = new_matrix.vals
Expand All @@ -89,7 +83,7 @@ def test_given_two_matrices_when_multiplying_then_check_new_matrix_correctly_cal

matrix_1 = Matrix.from_array(array_1)
matrix_2 = Matrix.from_array(array_2)
new_matrix = Matrix.multiply(matrix_1, matrix_2)
new_matrix = matrix_1 @ matrix_2

expected_vals = np.array([[3, -9], [2, -11], [6, -18]])
actual_vals = new_matrix.vals
Expand All @@ -101,7 +95,7 @@ def test_given_two_matrices_when_multiplying_element_wise_then_check_new_matrix_

matrix_1 = Matrix.from_array(array_1)
matrix_2 = Matrix.from_array(array_2)
new_matrix = Matrix.multiply_element_wise(matrix_1, matrix_2)
new_matrix = matrix_1 * matrix_2

expected_vals = np.array([[-1, 2], [8, -15], [6, 8]])
actual_vals = new_matrix.vals
Expand All @@ -112,7 +106,7 @@ def test_given_matrix_and_scalar_when_multiplying_then_check_new_matrix_correctl
multiplier = 3

matrix = Matrix.from_array(array)
new_matrix = Matrix.multiply(matrix, multiplier)
new_matrix = matrix * multiplier

expected_vals = array * multiplier
actual_vals = new_matrix.vals
Expand Down Expand Up @@ -180,3 +174,5 @@ def test_given_matrix_when_shifting_vals_then_check_vals_are_different(self) ->

matrix_1.shift_vals(0.5)
assert not np.all(matrix_1.vals == matrix_2.vals)
assert np.all(array * 0.5 <= matrix_1.vals)
assert np.all(matrix_1.vals <= array * 1.5)