Skip to content

Commit

Permalink
better error checking in loss_
Browse files Browse the repository at this point in the history
  • Loading branch information
pde-bakk committed Oct 18, 2022
1 parent fffc438 commit fa13aa7
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 2 deletions.
6 changes: 4 additions & 2 deletions module03/ex06/my_logistic_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def loss_elem_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> np.ndarr

@staticmethod
@accepts(np.ndarray, np.ndarray, float)
def loss_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> float:
def loss_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> float | None:
"""
Computes the logistic loss value.
Args:
Expand All @@ -106,7 +106,9 @@ def loss_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> float:
This function should not raise any Exception.
"""
loss_elem = MyLogisticRegression.loss_elem_(y, y_hat, eps)
return -loss_elem.sum() / y.shape[0]
if loss_elem is None:
return None
return -np.sum(loss_elem) / y.shape[0]

def __predict_(self, x: np.ndarray) -> np.ndarray:
"""Computes the vector of prediction y_hat from two non-empty numpy.ndarray.
Expand Down
31 changes: 31 additions & 0 deletions module03/ex06/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,37 @@ def main():
print(f'Example 4: {loss = }\n')
assert np.isclose(loss, 1.4779126923052268)

# Extra tests
y = np.array([[0], [0]])
y_hat = np.array([[0], [0]])
result = mylr.loss_(y, y_hat)
print(f'Correction test 1: {result}')
assert np.isclose(result, 1e-15)

y = np.array([[0], [1]])
y_hat = np.array([[0], [1]])
result = mylr.loss_(y, y_hat)
print(f'Correction test 2: {result}')
assert np.isclose(result, 1e-15)

y = np.array([[0], [0], [0]])
y_hat = np.array([[1], [0], [0]])
result = mylr.loss_(y, y_hat)
print(f'Correction test 3: {result}')
assert np.isclose(result, 11.51292546)

y = np.array([[0], [0], [0]])
y_hat = np.array([[1], [0], [1]])
result = mylr.loss_(y, y_hat)
print(f'Correction test 4: {result}')
assert np.isclose(result, 23.02585093)

y = np.array([[0], [1], [0]])
y_hat = np.array([[1], [0], [1]])
result = mylr.loss_(y, y_hat)
print(f'Correction test 5: {result}')
assert np.isclose(result, 34.53877639)


if __name__ == '__main__':
main()
143 changes: 143 additions & 0 deletions module03/ex08/my_logistic_regression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import numpy as np


def accepts(*types):
def check_accepts(f):
if len(types) != f.__code__.co_argcount:
return None

def new_f(*args, **kwargs):
if any(not isinstance(arg, t) for arg, t in zip(args, types)):
return None
return f(*args, **kwargs)

# new_f.__name__ = f.__name__
return new_f

return check_accepts


class MyLogisticRegression:
"""
Description: My personal logistic regression to classify things.
If a function has the __ prefix, it means that it assumes the x value has a column of ones already...
"""

def __init__(self, theta: np.ndarray, alpha: float = 0.001, max_iter: int = 1000):
if not isinstance(theta, np.ndarray) or not isinstance(alpha, float) or not isinstance(max_iter, int):
raise TypeError('Bad arguments given to MyLogisticRegression')
self.alpha = alpha
self.max_iter = max_iter
self.theta = theta

@staticmethod
@accepts(np.ndarray)
def sigmoid_(x: np.ndarray) -> np.ndarray:
"""
Compute the sigmoid of a vector.
Args:
x: has to be a numpy.ndarray of shape (m, 1).
Returns:
The sigmoid value as a numpy.ndarray of shape (m, 1).
None if x is an empty numpy.ndarray.
Raises:
This function should not raise any Exception.
"""
return 1 / (1 + np.exp(-x))

def gradient_(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
"""Computes a gradient vector from three non-empty numpy.ndarray, with a for-loop. The three arrays must have compatiblArgs:
x: has to be an numpy.ndarray, a matrix of shape m * n.
y: has to be an numpy.ndarray, a vector of shape m * 1.
theta: has to be an numpy.ndarray, a vector of shape (n + 1) * 1.
Returns:
The gradient as a numpy.ndarray, a vector of shape n * 1, containing the result of the formula for all j.
None if x, y, or theta are empty numpy.ndarray.
None if x, y and theta do not have compatible dimensions.
Raises:
This function should not raise any Exception.
"""
ones = np.ones(shape=(x.shape[0], 1))
x = np.hstack((ones, x))
y_hat = self.__predict_(x)
return x.T.dot(y_hat - y) / y.shape[0]

def __gradient_(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
y_hat = self.__predict_(x)
return x.T.dot(y_hat - y) / y.shape[0]

def fit_(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
"""
:param x: np.ndarray
:param y: np.ndarray
:return: new theta values
"""
x = np.column_stack((np.ones(shape=(x.shape[0], 1)), x))
for idx in range(self.max_iter):
self.theta -= (self.alpha * self.__gradient_(x, y))
return self.theta

@staticmethod
@accepts(np.ndarray, np.ndarray, float)
def loss_elem_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> np.ndarray | None:
"""
:param y: Actual values as an np.ndarray
:param y_hat: Predicted values as an np.ndarray
:param eps: very small value
:return: np.ndarray of the losses
"""
if y.shape != y_hat.shape:
return None
return y * np.log(y_hat + eps) + (1 - y) * np.log(1 - y_hat + eps)

@staticmethod
@accepts(np.ndarray, np.ndarray, float)
def loss_(y: np.ndarray, y_hat: np.ndarray, eps: float = 1e-15) -> float | None:
"""
Computes the logistic loss value.
Args:
y: has to be an numpy.ndarray, a vector of shape m * 1.
y_hat: has to be an numpy.ndarray, a vector of shape m * 1.
eps: has to be a float, epsilon (default=1e-15)
Returns:
The logistic loss value as a float.
None on any error.
Raises:
This function should not raise any Exception.
"""
loss_elem = MyLogisticRegression.loss_elem_(y, y_hat, eps)
if loss_elem is None:
return None
return -np.sum(loss_elem) / y.shape[0]

def __predict_(self, x: np.ndarray) -> np.ndarray:
"""Computes the vector of prediction y_hat from two non-empty numpy.ndarray.
Args:
x: has to be an numpy.ndarray, a vector of dimension m * n.
theta: has to be an numpy.ndarray, a vector of dimension (n + 1) * 1.
Returns:
y_hat as a numpy.ndarray, a vector of dimension m * 1.
None if x or theta are empty numpy.ndarray.
None if x or theta dimensions are not appropriate.
Raises:
This function should not raise any Exception.
"""
return MyLogisticRegression.sigmoid_(x.dot(self.theta))

def predict_(self, x: np.ndarray) -> np.ndarray | None:
"""Computes the vector of prediction y_hat from two non-empty numpy.ndarray.
Args:
x: has to be an numpy.ndarray, a vector of dimension m * n.
theta: has to be an numpy.ndarray, a vector of dimension (n + 1) * 1.
Returns:
y_hat as a numpy.ndarray, a vector of dimension m * 1.
None if x or theta are empty numpy.ndarray.
None if x or theta dimensions are not appropriate.
Raises:
This function should not raise any Exception.
"""
if not isinstance(x, np.ndarray) or x.size == 0:
return None
ones = np.ones(shape=(x.shape[0], 1))
x = np.hstack((ones, x))
return MyLogisticRegression.sigmoid_(x.dot(self.theta))

0 comments on commit fa13aa7

Please sign in to comment.