Skip to content

Commit ca40ccd

Browse files
authored
Merge branch 'main' into dev
2 parents a94e2c8 + 432fa91 commit ca40ccd

27 files changed

+537
-223
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ jobs:
2525
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
2626
pip install pytest pytest-cov flake8 black
2727
pip install -e .
28+
- name: Format with black
29+
run: |
30+
# Format the code with black before checking
31+
black .
32+
# Check if any changes were made
33+
git diff --exit-code || { echo "Code was not properly formatted before commit. Black has fixed the formatting issues."; exit 0; }
2834
- name: Lint with flake8
2935
run: |
3036
# stop the build if there are Python syntax errors or undefined names
3137
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
3238
# exit-zero treats all errors as warnings
3339
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
34-
- name: Format with black
35-
run: |
36-
black --check .
3740
- name: Test with pytest
3841
run: |
3942
pytest --cov=./ --cov-report=xml
4043
- name: Upload coverage to Codecov
4144
uses: codecov/codecov-action@v3
4245
with:
4346
file: ./coverage.xml
44-
fail_ci_if_error: false
47+
fail_ci_if_error: false

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ COPY . .
99
RUN pip install -e .
1010

1111
# Default command runs tests
12-
CMD ["python", "-m", "pytest", "tests/"]
12+
CMD ["python", "-m", "pytest", "tests/"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Łukasz Bielaszewski
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,3 @@ trainer.fit(X, y, epochs=100)
7171
# Make predictions
7272
predictions = model(X)
7373
```
74-

core/checks.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
"""
2+
This module provides gradient checking utilities to verify backpropagation calculations.
3+
"""
4+
15
import numpy as np
26

7+
38
def check_gradients(model, x, y, loss_fn, epsilon=1e-5, threshold=1e-3):
4-
"""
5-
Compares analytical gradients from backprop to numerical gradients.
9+
"""Compare analytical gradients from backpropagation to numerical gradients.
10+
11+
Args:
12+
model: The neural network model to check
13+
x: Input tensor
14+
y: Target tensor
15+
loss_fn: Loss function
16+
epsilon: Small perturbation for numerical gradient calculation
17+
threshold: Maximum allowable relative error
18+
19+
Raises:
20+
AssertionError: If the relative error between analytical and numerical gradients exceeds threshold
621
"""
722
# Forward and backward to compute analytical gradients
823
preds = model(x)
@@ -41,4 +56,6 @@ def check_gradients(model, x, y, loss_fn, epsilon=1e-5, threshold=1e-3):
4156
print(f" Param shape: {param.data.shape}")
4257
print(f" Mean relative error: {relative_error:.6e}")
4358

44-
assert relative_error < threshold, f"Gradient check failed! Relative error: {relative_error}"
59+
assert (
60+
relative_error < threshold
61+
), f"Gradient check failed! Relative error: {relative_error}"

core/tensor.py

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
1+
"""
2+
This module implements the Tensor class for automatic differentiation in the ML framework.
3+
"""
4+
15
import numpy as np
26

7+
38
class Tensor:
9+
"""
10+
Core tensor class with automatic differentiation capabilities.
11+
12+
Implements a computational graph with automatic gradient calculation
13+
for building and training neural networks.
14+
"""
15+
416
def __init__(self, data, requires_grad=False):
17+
"""
18+
Initialize a new Tensor.
19+
20+
Args:
21+
data: Input data (numpy array or convertible to numpy array)
22+
requires_grad: Whether the tensor requires gradient computation
23+
"""
524
if not isinstance(data, np.ndarray):
625
data = np.array(data, dtype=np.float32)
726

@@ -12,11 +31,24 @@ def __init__(self, data, requires_grad=False):
1231
self._prev = set()
1332

1433
def __repr__(self):
34+
"""Return string representation of the tensor."""
1535
return f"Tensor(data={self.data}, grad={self.grad})"
1636

1737
def __add__(self, other):
38+
"""
39+
Add two tensors or a tensor and a scalar.
40+
41+
Args:
42+
other: Another tensor or scalar value
43+
44+
Returns:
45+
A new tensor containing the result
46+
"""
1847
other = other if isinstance(other, Tensor) else Tensor(other)
19-
out = Tensor(self.data + other.data, requires_grad=self.requires_grad or other.requires_grad)
48+
out = Tensor(
49+
self.data + other.data,
50+
requires_grad=self.requires_grad or other.requires_grad,
51+
)
2052

2153
def _backward():
2254
if self.requires_grad:
@@ -36,8 +68,20 @@ def _backward():
3668
return out
3769

3870
def __mul__(self, other):
71+
"""
72+
Multiply two tensors or a tensor and a scalar.
73+
74+
Args:
75+
other: Another tensor or scalar value
76+
77+
Returns:
78+
A new tensor containing the result
79+
"""
3980
other = other if isinstance(other, Tensor) else Tensor(other)
40-
out = Tensor(self.data * other.data, requires_grad=self.requires_grad or other.requires_grad)
81+
out = Tensor(
82+
self.data * other.data,
83+
requires_grad=self.requires_grad or other.requires_grad,
84+
)
4185

4286
def _backward():
4387
if self.requires_grad:
@@ -57,18 +101,51 @@ def _backward():
57101
return out
58102

59103
def __sub__(self, other):
104+
"""
105+
Subtract a tensor or scalar from this tensor.
106+
107+
Args:
108+
other: Another tensor or scalar value
109+
110+
Returns:
111+
A new tensor containing the result
112+
"""
60113
other = other if isinstance(other, Tensor) else Tensor(other)
61114
return self + (-other)
62115

63116
def __neg__(self):
117+
"""
118+
Negate this tensor.
119+
120+
Returns:
121+
A new tensor with negated values
122+
"""
64123
return self * -1
65124

66125
def __truediv__(self, other):
126+
"""
127+
Divide this tensor by another tensor or scalar.
128+
129+
Args:
130+
other: Another tensor or scalar value
131+
132+
Returns:
133+
A new tensor containing the result
134+
"""
67135
other = other if isinstance(other, Tensor) else Tensor(other)
68136
return self * other**-1
69137

70138
def __pow__(self, power):
71-
out = Tensor(self.data ** power, requires_grad=self.requires_grad)
139+
"""
140+
Raise this tensor to a power.
141+
142+
Args:
143+
power: Exponent value
144+
145+
Returns:
146+
A new tensor containing the result
147+
"""
148+
out = Tensor(self.data**power, requires_grad=self.requires_grad)
72149

73150
def _backward():
74151
if self.requires_grad:
@@ -80,8 +157,20 @@ def _backward():
80157
return out
81158

82159
def __matmul__(self, other):
160+
"""
161+
Perform matrix multiplication with another tensor.
162+
163+
Args:
164+
other: Another tensor for matrix multiplication
165+
166+
Returns:
167+
A new tensor containing the result
168+
"""
83169
other = other if isinstance(other, Tensor) else Tensor(other)
84-
out = Tensor(self.data @ other.data, requires_grad=self.requires_grad or other.requires_grad)
170+
out = Tensor(
171+
self.data @ other.data,
172+
requires_grad=self.requires_grad or other.requires_grad,
173+
)
85174

86175
def _backward():
87176
if self.requires_grad:
@@ -101,6 +190,15 @@ def _backward():
101190
return out
102191

103192
def __getitem__(self, idx):
193+
"""
194+
Get items from the tensor at specified indices.
195+
196+
Args:
197+
idx: Index or slice to retrieve
198+
199+
Returns:
200+
A new tensor with the selected items
201+
"""
104202
out = Tensor(self.data[idx], requires_grad=self.requires_grad)
105203

106204
def _backward():
@@ -114,12 +212,24 @@ def _backward():
114212
return out
115213

116214
def __hash__(self):
215+
"""Hash function for tensor objects."""
117216
return id(self)
118217

119218
def __eq__(self, other):
219+
"""Check if two tensor objects are the same."""
120220
return id(self) == id(other)
121221

122222
def sum(self, axis=None, keepdims=False):
223+
"""
224+
Sum tensor elements along specified axis.
225+
226+
Args:
227+
axis: Axis along which to sum
228+
keepdims: Whether to keep the summed dimensions
229+
230+
Returns:
231+
A new tensor with summed values
232+
"""
123233
out_data = np.sum(self.data, axis=axis, keepdims=keepdims)
124234
out = Tensor(out_data, requires_grad=self.requires_grad)
125235

@@ -136,6 +246,12 @@ def _backward():
136246
return out
137247

138248
def mean(self):
249+
"""
250+
Calculate the mean of all tensor elements.
251+
252+
Returns:
253+
A new tensor containing the mean value
254+
"""
139255
out = Tensor(self.data.mean(), requires_grad=self.requires_grad)
140256

141257
def _backward():
@@ -148,6 +264,12 @@ def _backward():
148264
return out
149265

150266
def exp(self):
267+
"""
268+
Calculate the exponential of all tensor elements.
269+
270+
Returns:
271+
A new tensor with exponential values
272+
"""
151273
out = Tensor(np.exp(self.data), requires_grad=self.requires_grad)
152274

153275
def _backward():
@@ -160,6 +282,12 @@ def _backward():
160282
return out
161283

162284
def log(self):
285+
"""
286+
Calculate the natural logarithm of all tensor elements.
287+
288+
Returns:
289+
A new tensor with logarithm values
290+
"""
163291
out = Tensor(np.log(self.data), requires_grad=self.requires_grad)
164292

165293
def _backward():
@@ -172,6 +300,16 @@ def _backward():
172300
return out
173301

174302
def max(self, axis=None, keepdims=False):
303+
"""
304+
Find maximum values along specified axis.
305+
306+
Args:
307+
axis: Axis along which to find maximum values
308+
keepdims: Whether to keep the dimensions
309+
310+
Returns:
311+
A new tensor with maximum values
312+
"""
175313
data = self.data
176314
max_data = np.max(data, axis=axis, keepdims=keepdims)
177315
out = Tensor(max_data)
@@ -192,6 +330,12 @@ def _backward():
192330
return out
193331

194332
def backward(self):
333+
"""
334+
Perform backpropagation starting from this tensor.
335+
336+
Computes gradients for all tensors in the computational graph
337+
that require gradients.
338+
"""
195339
if self.grad is None:
196340
self.grad = np.ones_like(self.data)
197341
print("Backward on:", self.data)
@@ -215,7 +359,10 @@ def topo(tensor):
215359

216360
def clip_gradients(self, max_norm):
217361
"""
218-
Clips gradients to prevent exploding gradients
362+
Clip gradients to prevent exploding gradients.
363+
364+
Args:
365+
max_norm: Maximum gradient norm
219366
"""
220367
if self.grad is None:
221368
return
@@ -226,4 +373,3 @@ def clip_gradients(self, max_norm):
226373
# Clip if necessary
227374
if grad_norm > max_norm:
228375
self.grad = self.grad * (max_norm / (grad_norm + 1e-12))
229-

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ services:
1313
- .:/app
1414
ports:
1515
- "8888:8888"
16-
command: jupyter notebook --ip=0.0.0.0 --port=8888 --no-browser --allow-root --NotebookApp.token='' --NotebookApp.password=''
16+
command: jupyter notebook --ip=0.0.0.0 --port=8888 --no-browser --allow-root --NotebookApp.token='' --NotebookApp.password=''

0 commit comments

Comments
 (0)