leanpass 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leanpass-0.1.0/PKG-INFO +64 -0
- leanpass-0.1.0/README.md +55 -0
- leanpass-0.1.0/leanpass/__init__.py +5 -0
- leanpass-0.1.0/leanpass/nn.py +72 -0
- leanpass-0.1.0/leanpass/optim.py +53 -0
- leanpass-0.1.0/leanpass/tensor.py +530 -0
- leanpass-0.1.0/leanpass.egg-info/PKG-INFO +64 -0
- leanpass-0.1.0/leanpass.egg-info/SOURCES.txt +15 -0
- leanpass-0.1.0/leanpass.egg-info/dependency_links.txt +1 -0
- leanpass-0.1.0/leanpass.egg-info/entry_points.txt +2 -0
- leanpass-0.1.0/leanpass.egg-info/requires.txt +1 -0
- leanpass-0.1.0/leanpass.egg-info/top_level.txt +1 -0
- leanpass-0.1.0/pyproject.toml +11 -0
- leanpass-0.1.0/setup.cfg +13 -0
- leanpass-0.1.0/tests/test_gradcheck.py +81 -0
- leanpass-0.1.0/tests/test_leanpass.py +43 -0
leanpass-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leanpass
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, transparent alternative to PyTorch/TensorFlow built on NumPy.
|
|
5
|
+
Author: LeanPass
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
|
|
10
|
+
# leanpass
|
|
11
|
+
|
|
12
|
+
A minimal, transparent NumPy-based autograd library for small models.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- `Tensor` with reverse-mode autodiff
|
|
17
|
+
- Operators: `+`, `-`, `*`, `/`, `**`, `@`
|
|
18
|
+
- Activations: `relu`, `sigmoid`, `softmax`
|
|
19
|
+
- `Linear` and `MLP` modules
|
|
20
|
+
- `SGD` and `Adam` optimizers
|
|
21
|
+
- Graph visualization via `Tensor.visualize_dot()`
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from leanpass import Tensor, nn, optim
|
|
33
|
+
|
|
34
|
+
x = Tensor([[1.0, 2.0]], requires_grad=False)
|
|
35
|
+
model = nn.MLP([2, 16, 3])
|
|
36
|
+
logits = model(x)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run demo
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python demo.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Publish to PyPI
|
|
46
|
+
|
|
47
|
+
1. Update `version` in `pyproject.toml`.
|
|
48
|
+
2. Install build tools:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install --upgrade build twine
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Build distribution files:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. Upload to PyPI:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m twine upload dist/*
|
|
64
|
+
```
|
leanpass-0.1.0/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# leanpass
|
|
2
|
+
|
|
3
|
+
A minimal, transparent NumPy-based autograd library for small models.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- `Tensor` with reverse-mode autodiff
|
|
8
|
+
- Operators: `+`, `-`, `*`, `/`, `**`, `@`
|
|
9
|
+
- Activations: `relu`, `sigmoid`, `softmax`
|
|
10
|
+
- `Linear` and `MLP` modules
|
|
11
|
+
- `SGD` and `Adam` optimizers
|
|
12
|
+
- Graph visualization via `Tensor.visualize_dot()`
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from leanpass import Tensor, nn, optim
|
|
24
|
+
|
|
25
|
+
x = Tensor([[1.0, 2.0]], requires_grad=False)
|
|
26
|
+
model = nn.MLP([2, 16, 3])
|
|
27
|
+
logits = model(x)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Run demo
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python demo.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Publish to PyPI
|
|
37
|
+
|
|
38
|
+
1. Update `version` in `pyproject.toml`.
|
|
39
|
+
2. Install build tools:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m pip install --upgrade build twine
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Build distribution files:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
4. Upload to PyPI:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m twine upload dist/*
|
|
55
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .tensor import Tensor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Module:
|
|
7
|
+
"""A base class for layers and model containers."""
|
|
8
|
+
|
|
9
|
+
def parameters(self):
|
|
10
|
+
"""Collect all trainable Tensor parameters recursively."""
|
|
11
|
+
params = []
|
|
12
|
+
for value in self.__dict__.values():
|
|
13
|
+
if isinstance(value, Tensor) and value.requires_grad:
|
|
14
|
+
params.append(value)
|
|
15
|
+
elif isinstance(value, Module):
|
|
16
|
+
params.extend(value.parameters())
|
|
17
|
+
return params
|
|
18
|
+
|
|
19
|
+
def zero_grad(self):
|
|
20
|
+
"""Reset gradients before each optimization step."""
|
|
21
|
+
for param in self.parameters():
|
|
22
|
+
param.grad = np.zeros_like(param.data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Linear(Module):
|
|
26
|
+
"""A single fully connected layer: y = x @ W + b."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, in_features, out_features):
|
|
29
|
+
scale = 1.0 / np.sqrt(in_features)
|
|
30
|
+
self.weight = Tensor(
|
|
31
|
+
np.random.uniform(-scale, scale, size=(in_features, out_features)),
|
|
32
|
+
requires_grad=True,
|
|
33
|
+
)
|
|
34
|
+
self.bias = Tensor(np.zeros(out_features), requires_grad=True)
|
|
35
|
+
|
|
36
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
37
|
+
return x @ self.weight + self.bias
|
|
38
|
+
|
|
39
|
+
def __call__(self, x: Tensor) -> Tensor:
|
|
40
|
+
return self.forward(x)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MLP(Module):
|
|
44
|
+
"""A simple feedforward network with ReLU activations."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, layer_sizes):
|
|
47
|
+
self.layers = []
|
|
48
|
+
for in_dim, out_dim in zip(layer_sizes[:-1], layer_sizes[1:]):
|
|
49
|
+
self.layers.append(Linear(in_dim, out_dim))
|
|
50
|
+
|
|
51
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
52
|
+
"""Propagate the input through each layer and apply ReLU except last."""
|
|
53
|
+
for index, layer in enumerate(self.layers):
|
|
54
|
+
x = layer(x)
|
|
55
|
+
if index < len(self.layers) - 1:
|
|
56
|
+
x = x.relu()
|
|
57
|
+
return x
|
|
58
|
+
|
|
59
|
+
def __call__(self, x: Tensor) -> Tensor:
|
|
60
|
+
return self.forward(x)
|
|
61
|
+
|
|
62
|
+
def parameters(self):
|
|
63
|
+
"""Return parameters from every Linear layer in the network."""
|
|
64
|
+
params = []
|
|
65
|
+
for layer in self.layers:
|
|
66
|
+
params.extend(layer.parameters())
|
|
67
|
+
return params
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def mse_loss(prediction: Tensor, target: Tensor) -> Tensor:
|
|
71
|
+
"""Mean squared error loss used for regression training."""
|
|
72
|
+
return ((prediction - target) ** 2).mean()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SGD:
|
|
5
|
+
"""Simple stochastic gradient descent optimizer."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, params, lr=1e-2):
|
|
8
|
+
self.params = list(params)
|
|
9
|
+
self.lr = lr
|
|
10
|
+
|
|
11
|
+
def step(self):
|
|
12
|
+
"""Apply a plain gradient descent update to each parameter."""
|
|
13
|
+
for param in self.params:
|
|
14
|
+
if param.grad is None:
|
|
15
|
+
continue
|
|
16
|
+
param.data = param.data - self.lr * param.grad
|
|
17
|
+
|
|
18
|
+
def zero_grad(self):
|
|
19
|
+
"""Zero out gradients so the next backward pass starts clean."""
|
|
20
|
+
for param in self.params:
|
|
21
|
+
param.grad = np.zeros_like(param.data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Adam:
|
|
25
|
+
"""Adam optimizer with bias-corrected moment estimates."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8):
|
|
28
|
+
self.params = list(params)
|
|
29
|
+
self.lr = lr
|
|
30
|
+
self.b1, self.b2 = betas
|
|
31
|
+
self.eps = eps
|
|
32
|
+
self.m = [np.zeros_like(p.data) for p in self.params]
|
|
33
|
+
self.v = [np.zeros_like(p.data) for p in self.params]
|
|
34
|
+
self.t = 0
|
|
35
|
+
|
|
36
|
+
def step(self):
|
|
37
|
+
"""Update each parameter using Adam's adaptive moment estimates."""
|
|
38
|
+
self.t += 1
|
|
39
|
+
for i, param in enumerate(self.params):
|
|
40
|
+
if param.grad is None:
|
|
41
|
+
continue
|
|
42
|
+
g = param.grad
|
|
43
|
+
self.m[i] = self.b1 * self.m[i] + (1 - self.b1) * g
|
|
44
|
+
self.v[i] = self.b2 * self.v[i] + (1 - self.b2) * (g ** 2)
|
|
45
|
+
|
|
46
|
+
m_hat = self.m[i] / (1 - self.b1 ** self.t)
|
|
47
|
+
v_hat = self.v[i] / (1 - self.b2 ** self.t)
|
|
48
|
+
param.data = param.data - self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
|
|
49
|
+
|
|
50
|
+
def zero_grad(self):
|
|
51
|
+
"""Zero out gradients for all tracked parameters."""
|
|
52
|
+
for param in self.params:
|
|
53
|
+
param.grad = np.zeros_like(param.data)
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _ensure_array(data):
|
|
5
|
+
"""Convert inputs to NumPy arrays so math is always numeric."""
|
|
6
|
+
if isinstance(data, Tensor):
|
|
7
|
+
return data.data
|
|
8
|
+
return np.array(data, dtype=np.float64)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sum_to_shape(grad, shape):
|
|
12
|
+
"""Reduce broadcasted gradients back to the shape of the original tensor."""
|
|
13
|
+
if grad.shape == shape:
|
|
14
|
+
return grad
|
|
15
|
+
|
|
16
|
+
while grad.ndim > len(shape):
|
|
17
|
+
grad = grad.sum(axis=0)
|
|
18
|
+
|
|
19
|
+
for axis, size in enumerate(shape):
|
|
20
|
+
if size == 1 and grad.shape[axis] != 1:
|
|
21
|
+
grad = grad.sum(axis=axis, keepdims=True)
|
|
22
|
+
|
|
23
|
+
return grad.reshape(shape)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Tensor:
|
|
27
|
+
"""A tiny automatic differentiation tensor backed by NumPy."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, data, requires_grad=False, name=None):
|
|
30
|
+
self.data = np.array(data, dtype=np.float64)
|
|
31
|
+
self.grad = np.zeros_like(self.data) if requires_grad else None
|
|
32
|
+
self.requires_grad = requires_grad
|
|
33
|
+
self._backward = lambda: None
|
|
34
|
+
self._prev = ()
|
|
35
|
+
self._op = ""
|
|
36
|
+
self._meta = {}
|
|
37
|
+
self.name = name
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
name = f" name={self.name}" if self.name else ""
|
|
41
|
+
return f"Tensor(shape={self.data.shape}, requires_grad={self.requires_grad}{name})"
|
|
42
|
+
|
|
43
|
+
def _create_child(self, data, op, prev, name=None, meta=None):
|
|
44
|
+
out = Tensor(data, requires_grad=any(node.requires_grad for node in prev), name=name)
|
|
45
|
+
out._prev = tuple(prev)
|
|
46
|
+
out._op = op
|
|
47
|
+
out._meta = meta or {}
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
def __add__(self, other):
|
|
51
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
52
|
+
out = self._create_child(self.data + other.data, "+", (self, other))
|
|
53
|
+
|
|
54
|
+
def _backward():
|
|
55
|
+
if self.requires_grad:
|
|
56
|
+
self.grad += _sum_to_shape(out.grad, self.data.shape)
|
|
57
|
+
if other.requires_grad:
|
|
58
|
+
other.grad += _sum_to_shape(out.grad, other.data.shape)
|
|
59
|
+
|
|
60
|
+
out._backward = _backward
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
def __radd__(self, other):
|
|
64
|
+
return self + other
|
|
65
|
+
|
|
66
|
+
def __neg__(self):
|
|
67
|
+
out = self._create_child(-self.data, "neg", (self,))
|
|
68
|
+
|
|
69
|
+
def _backward():
|
|
70
|
+
if self.requires_grad:
|
|
71
|
+
self.grad += _sum_to_shape(-out.grad, self.data.shape)
|
|
72
|
+
|
|
73
|
+
out._backward = _backward
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
def __sub__(self, other):
|
|
77
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
78
|
+
out = self._create_child(self.data - other.data, "-", (self, other))
|
|
79
|
+
|
|
80
|
+
def _backward():
|
|
81
|
+
if self.requires_grad:
|
|
82
|
+
self.grad += _sum_to_shape(out.grad, self.data.shape)
|
|
83
|
+
if other.requires_grad:
|
|
84
|
+
other.grad += _sum_to_shape(-out.grad, other.data.shape)
|
|
85
|
+
|
|
86
|
+
out._backward = _backward
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
def __rsub__(self, other):
|
|
90
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
91
|
+
return other - self
|
|
92
|
+
|
|
93
|
+
def __mul__(self, other):
|
|
94
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
95
|
+
out = self._create_child(self.data * other.data, "*", (self, other))
|
|
96
|
+
|
|
97
|
+
def _backward():
|
|
98
|
+
if self.requires_grad:
|
|
99
|
+
self.grad += _sum_to_shape(out.grad * other.data, self.data.shape)
|
|
100
|
+
if other.requires_grad:
|
|
101
|
+
other.grad += _sum_to_shape(out.grad * self.data, other.data.shape)
|
|
102
|
+
|
|
103
|
+
out._backward = _backward
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
def __rmul__(self, other):
|
|
107
|
+
return self * other
|
|
108
|
+
|
|
109
|
+
def __truediv__(self, other):
|
|
110
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
111
|
+
out = self._create_child(self.data / other.data, "/", (self, other))
|
|
112
|
+
|
|
113
|
+
def _backward():
|
|
114
|
+
if self.requires_grad:
|
|
115
|
+
self.grad += _sum_to_shape(out.grad / other.data, self.data.shape)
|
|
116
|
+
if other.requires_grad:
|
|
117
|
+
other.grad += _sum_to_shape(-out.grad * self.data / (other.data ** 2), other.data.shape)
|
|
118
|
+
|
|
119
|
+
out._backward = _backward
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
def __pow__(self, exponent):
|
|
123
|
+
exponent = float(exponent)
|
|
124
|
+
out = self._create_child(self.data ** exponent, "**", (self,), meta={"exponent": exponent})
|
|
125
|
+
|
|
126
|
+
def _backward():
|
|
127
|
+
if self.requires_grad:
|
|
128
|
+
self.grad += _sum_to_shape(out.grad * exponent * self.data ** (exponent - 1), self.data.shape)
|
|
129
|
+
|
|
130
|
+
out._backward = _backward
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
def exp(self):
|
|
134
|
+
out = self._create_child(np.exp(self.data), "exp", {self})
|
|
135
|
+
|
|
136
|
+
def _backward():
|
|
137
|
+
if self.requires_grad:
|
|
138
|
+
self.grad += _sum_to_shape(out.grad * out.data, self.data.shape)
|
|
139
|
+
|
|
140
|
+
out._backward = _backward
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
def log(self):
|
|
144
|
+
out = self._create_child(np.log(self.data), "log", {self})
|
|
145
|
+
|
|
146
|
+
def _backward():
|
|
147
|
+
if self.requires_grad:
|
|
148
|
+
self.grad += _sum_to_shape(out.grad / self.data, self.data.shape)
|
|
149
|
+
|
|
150
|
+
out._backward = _backward
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
def sigmoid(self):
|
|
154
|
+
out = self._create_child(1 / (1 + np.exp(-self.data)), "sigmoid", {self})
|
|
155
|
+
|
|
156
|
+
def _backward():
|
|
157
|
+
if self.requires_grad:
|
|
158
|
+
sigmoid_grad = out.data * (1 - out.data)
|
|
159
|
+
self.grad += _sum_to_shape(out.grad * sigmoid_grad, self.data.shape)
|
|
160
|
+
|
|
161
|
+
out._backward = _backward
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
def softmax(self, axis=-1):
|
|
165
|
+
shifted = self.data - self.data.max(axis=axis, keepdims=True)
|
|
166
|
+
exp_values = np.exp(shifted)
|
|
167
|
+
probabilities = exp_values / exp_values.sum(axis=axis, keepdims=True)
|
|
168
|
+
out = self._create_child(probabilities, "softmax", {self})
|
|
169
|
+
|
|
170
|
+
def _backward():
|
|
171
|
+
if self.requires_grad:
|
|
172
|
+
grad = out.grad
|
|
173
|
+
sum_grad = (grad * out.data).sum(axis=axis, keepdims=True)
|
|
174
|
+
self.grad += _sum_to_shape(out.data * (grad - sum_grad), self.data.shape)
|
|
175
|
+
|
|
176
|
+
out._backward = _backward
|
|
177
|
+
return out
|
|
178
|
+
|
|
179
|
+
def __matmul__(self, other):
|
|
180
|
+
other = other if isinstance(other, Tensor) else Tensor(other)
|
|
181
|
+
out = self._create_child(self.data @ other.data, "@", (self, other))
|
|
182
|
+
|
|
183
|
+
def _backward():
|
|
184
|
+
if self.requires_grad:
|
|
185
|
+
self.grad += _sum_to_shape(out.grad @ other.data.T, self.data.shape)
|
|
186
|
+
if other.requires_grad:
|
|
187
|
+
other.grad += _sum_to_shape(self.data.T @ out.grad, other.data.shape)
|
|
188
|
+
|
|
189
|
+
out._backward = _backward
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
def relu(self):
|
|
193
|
+
out = self._create_child(np.maximum(0, self.data), "relu", {self})
|
|
194
|
+
|
|
195
|
+
def _backward():
|
|
196
|
+
if self.requires_grad:
|
|
197
|
+
grad_input = out.grad * (self.data > 0).astype(np.float64)
|
|
198
|
+
self.grad += _sum_to_shape(grad_input, self.data.shape)
|
|
199
|
+
|
|
200
|
+
out._backward = _backward
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
def sum(self):
|
|
204
|
+
out = Tensor(self.data.sum(), requires_grad=self.requires_grad, name="sum")
|
|
205
|
+
out._prev = {self}
|
|
206
|
+
out._op = "sum"
|
|
207
|
+
|
|
208
|
+
def _backward():
|
|
209
|
+
if self.requires_grad:
|
|
210
|
+
self.grad += np.ones_like(self.data) * out.grad
|
|
211
|
+
|
|
212
|
+
out._backward = _backward
|
|
213
|
+
return out
|
|
214
|
+
|
|
215
|
+
def mean(self):
|
|
216
|
+
out = Tensor(self.data.mean(), requires_grad=self.requires_grad, name="mean")
|
|
217
|
+
out._prev = {self}
|
|
218
|
+
out._op = "mean"
|
|
219
|
+
|
|
220
|
+
def _backward():
|
|
221
|
+
if self.requires_grad:
|
|
222
|
+
scale = 1.0 / self.data.size
|
|
223
|
+
self.grad += np.ones_like(self.data) * out.grad * scale
|
|
224
|
+
|
|
225
|
+
out._backward = _backward
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
def visualize(self):
|
|
229
|
+
"""Return a simple text representation of the computation graph."""
|
|
230
|
+
nodes = []
|
|
231
|
+
edges = []
|
|
232
|
+
visited = set()
|
|
233
|
+
|
|
234
|
+
def build(v):
|
|
235
|
+
if v in visited:
|
|
236
|
+
return
|
|
237
|
+
visited.add(v)
|
|
238
|
+
label = v._op or "leaf"
|
|
239
|
+
if v.name:
|
|
240
|
+
label += f" ({v.name})"
|
|
241
|
+
nodes.append((id(v), label, v.data.shape))
|
|
242
|
+
for child in v._prev:
|
|
243
|
+
edges.append((id(child), id(v)))
|
|
244
|
+
build(child)
|
|
245
|
+
|
|
246
|
+
build(self)
|
|
247
|
+
|
|
248
|
+
lines = [f"Node {nid}: {label} shape={shape}" for nid, label, shape in nodes]
|
|
249
|
+
lines += [f"Edge {src} -> {dst}" for src, dst in edges]
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
def visualize_dot(self):
|
|
253
|
+
"""Return a Graphviz DOT representation of the computation graph."""
|
|
254
|
+
nodes, edges = self._graph_nodes()
|
|
255
|
+
lines = ["digraph computation_graph {", " rankdir=LR;", " node [shape=box, style=filled, fillcolor=lightgray];"]
|
|
256
|
+
|
|
257
|
+
for node in nodes:
|
|
258
|
+
label = node._op or "leaf"
|
|
259
|
+
if node.name:
|
|
260
|
+
label += f"\n{node.name}"
|
|
261
|
+
shape = "ellipse" if len(node._prev) == 0 else "box"
|
|
262
|
+
lines.append(f" n{ id(node) } [label=\"{label}\", shape={shape}];")
|
|
263
|
+
|
|
264
|
+
for src, dst in edges:
|
|
265
|
+
lines.append(f" n{src} -> n{dst};")
|
|
266
|
+
|
|
267
|
+
lines.append("}")
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
|
|
270
|
+
def backward(self, gradient=None):
|
|
271
|
+
if gradient is None:
|
|
272
|
+
gradient = np.ones_like(self.data)
|
|
273
|
+
if self.grad is None:
|
|
274
|
+
self.grad = np.zeros_like(self.data)
|
|
275
|
+
self.grad = self.grad + np.array(gradient, dtype=np.float64)
|
|
276
|
+
|
|
277
|
+
topo = []
|
|
278
|
+
visited = set()
|
|
279
|
+
|
|
280
|
+
def build(v):
|
|
281
|
+
if v not in visited:
|
|
282
|
+
visited.add(v)
|
|
283
|
+
for child in v._prev:
|
|
284
|
+
build(child)
|
|
285
|
+
topo.append(v)
|
|
286
|
+
|
|
287
|
+
build(self)
|
|
288
|
+
|
|
289
|
+
for node in topo:
|
|
290
|
+
if node.requires_grad and node.grad is None:
|
|
291
|
+
node.grad = np.zeros_like(node.data)
|
|
292
|
+
|
|
293
|
+
for node in reversed(topo):
|
|
294
|
+
node._backward()
|
|
295
|
+
|
|
296
|
+
def _graph_nodes(self):
|
|
297
|
+
nodes = []
|
|
298
|
+
edges = []
|
|
299
|
+
visited = set()
|
|
300
|
+
|
|
301
|
+
def build(v):
|
|
302
|
+
if v in visited:
|
|
303
|
+
return
|
|
304
|
+
visited.add(v)
|
|
305
|
+
nodes.append(v)
|
|
306
|
+
for child in v._prev:
|
|
307
|
+
edges.append((id(child), id(v)))
|
|
308
|
+
build(child)
|
|
309
|
+
|
|
310
|
+
build(self)
|
|
311
|
+
return nodes, edges
|
|
312
|
+
|
|
313
|
+
def grad_check(self, eps=1e-6, tol=1e-4):
|
|
314
|
+
"""Compare backward gradients against finite differences."""
|
|
315
|
+
if not self.requires_grad:
|
|
316
|
+
raise ValueError("grad_check requires the output tensor to require gradients")
|
|
317
|
+
|
|
318
|
+
nodes, _ = self._graph_nodes()
|
|
319
|
+
numeric = {}
|
|
320
|
+
|
|
321
|
+
for node in nodes:
|
|
322
|
+
if not node.requires_grad:
|
|
323
|
+
continue
|
|
324
|
+
original = node.data.copy()
|
|
325
|
+
numeric_grad = np.zeros_like(node.data)
|
|
326
|
+
for idx in np.ndindex(node.data.shape):
|
|
327
|
+
node.data[idx] = original[idx] + eps
|
|
328
|
+
plus = self._eval_forward()
|
|
329
|
+
node.data[idx] = original[idx] - eps
|
|
330
|
+
minus = self._eval_forward()
|
|
331
|
+
node.data[idx] = original[idx]
|
|
332
|
+
numeric_grad[idx] = (np.sum(plus) - np.sum(minus)) / (2 * eps)
|
|
333
|
+
|
|
334
|
+
self.zero_grad_all()
|
|
335
|
+
self.backward()
|
|
336
|
+
numeric[node] = numeric_grad
|
|
337
|
+
node.data = original
|
|
338
|
+
|
|
339
|
+
errors = []
|
|
340
|
+
for node in nodes:
|
|
341
|
+
if not node.requires_grad:
|
|
342
|
+
continue
|
|
343
|
+
diff = np.max(np.abs(node.grad - numeric[node]))
|
|
344
|
+
if diff > tol:
|
|
345
|
+
errors.append((node, diff, node.grad, numeric[node]))
|
|
346
|
+
|
|
347
|
+
return errors
|
|
348
|
+
|
|
349
|
+
def _eval_forward(self):
|
|
350
|
+
"""Evaluate the graph forward using current leaf values without mutating nodes."""
|
|
351
|
+
topo = []
|
|
352
|
+
visited = set()
|
|
353
|
+
values = {}
|
|
354
|
+
|
|
355
|
+
def build(v):
|
|
356
|
+
if v not in visited:
|
|
357
|
+
visited.add(v)
|
|
358
|
+
for child in v._prev:
|
|
359
|
+
build(child)
|
|
360
|
+
topo.append(v)
|
|
361
|
+
|
|
362
|
+
build(self)
|
|
363
|
+
|
|
364
|
+
for node in topo:
|
|
365
|
+
if len(node._prev) == 0:
|
|
366
|
+
values[node] = node.data
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
if node._op == "+":
|
|
370
|
+
values[node] = values[node._prev[0]] + values[node._prev[1]]
|
|
371
|
+
elif node._op == "-":
|
|
372
|
+
values[node] = values[node._prev[0]] - values[node._prev[1]]
|
|
373
|
+
elif node._op == "*":
|
|
374
|
+
values[node] = values[node._prev[0]] * values[node._prev[1]]
|
|
375
|
+
elif node._op == "/":
|
|
376
|
+
values[node] = values[node._prev[0]] / values[node._prev[1]]
|
|
377
|
+
elif node._op == "**":
|
|
378
|
+
exponent = node._meta.get("exponent", 2.0)
|
|
379
|
+
values[node] = values[node._prev[0]] ** exponent
|
|
380
|
+
elif node._op == "@":
|
|
381
|
+
values[node] = values[node._prev[0]] @ values[node._prev[1]]
|
|
382
|
+
elif node._op == "relu":
|
|
383
|
+
values[node] = np.maximum(0, values[node._prev[0]])
|
|
384
|
+
elif node._op == "exp":
|
|
385
|
+
values[node] = np.exp(values[node._prev[0]])
|
|
386
|
+
elif node._op == "log":
|
|
387
|
+
values[node] = np.log(values[node._prev[0]])
|
|
388
|
+
elif node._op == "sigmoid":
|
|
389
|
+
x = values[node._prev[0]]
|
|
390
|
+
values[node] = 1 / (1 + np.exp(-x))
|
|
391
|
+
elif node._op == "softmax":
|
|
392
|
+
x = values[node._prev[0]]
|
|
393
|
+
shifted = x - x.max(axis=-1, keepdims=True)
|
|
394
|
+
exp_values = np.exp(shifted)
|
|
395
|
+
values[node] = exp_values / exp_values.sum(axis=-1, keepdims=True)
|
|
396
|
+
elif node._op == "sum":
|
|
397
|
+
values[node] = values[node._prev[0]].sum()
|
|
398
|
+
elif node._op == "mean":
|
|
399
|
+
values[node] = values[node._prev[0]].mean()
|
|
400
|
+
elif node._op == "neg":
|
|
401
|
+
values[node] = -values[node._prev[0]]
|
|
402
|
+
else:
|
|
403
|
+
values[node] = node.data
|
|
404
|
+
|
|
405
|
+
return values[self]
|
|
406
|
+
|
|
407
|
+
def zero_grad_all(self):
|
|
408
|
+
"""Zero all gradients in the current graph."""
|
|
409
|
+
nodes, _ = self._graph_nodes()
|
|
410
|
+
for node in nodes:
|
|
411
|
+
if node.requires_grad:
|
|
412
|
+
node.grad = np.zeros_like(node.data)
|
|
413
|
+
|
|
414
|
+
def _graph_nodes(self):
|
|
415
|
+
nodes = []
|
|
416
|
+
edges = []
|
|
417
|
+
visited = set()
|
|
418
|
+
|
|
419
|
+
def build(v):
|
|
420
|
+
if v in visited:
|
|
421
|
+
return
|
|
422
|
+
visited.add(v)
|
|
423
|
+
nodes.append(v)
|
|
424
|
+
for child in v._prev:
|
|
425
|
+
edges.append((id(child), id(v)))
|
|
426
|
+
build(child)
|
|
427
|
+
|
|
428
|
+
build(self)
|
|
429
|
+
return nodes, edges
|
|
430
|
+
|
|
431
|
+
def grad_check(self, eps=1e-6, tol=1e-4):
|
|
432
|
+
"""Compare analytical gradients to numerical finite differences."""
|
|
433
|
+
if not self.requires_grad:
|
|
434
|
+
raise ValueError("grad_check requires the output tensor to require gradients")
|
|
435
|
+
|
|
436
|
+
nodes, _ = self._graph_nodes()
|
|
437
|
+
numeric = {}
|
|
438
|
+
|
|
439
|
+
def scalar_forward():
|
|
440
|
+
return float(self.data.sum()) if self.data.size == 1 else None
|
|
441
|
+
|
|
442
|
+
for node in nodes:
|
|
443
|
+
if not node.requires_grad:
|
|
444
|
+
continue
|
|
445
|
+
analytic = np.zeros_like(node.data)
|
|
446
|
+
numeric_grad = np.zeros_like(node.data)
|
|
447
|
+
original = node.data.copy()
|
|
448
|
+
|
|
449
|
+
it = np.nditer(node.data, flags=["multi_index"], op_flags=["readwrite"])
|
|
450
|
+
while not it.finished:
|
|
451
|
+
idx = it.multi_index
|
|
452
|
+
node.data[idx] = original[idx] + eps
|
|
453
|
+
plus = self._eval_forward()
|
|
454
|
+
node.data[idx] = original[idx] - eps
|
|
455
|
+
minus = self._eval_forward()
|
|
456
|
+
node.data[idx] = original[idx]
|
|
457
|
+
numeric_grad[idx] = (plus - minus) / (2 * eps)
|
|
458
|
+
it.iternext()
|
|
459
|
+
|
|
460
|
+
node.zero_grad_all()
|
|
461
|
+
self.backward()
|
|
462
|
+
analytic = node.grad.copy()
|
|
463
|
+
numeric[node] = numeric_grad
|
|
464
|
+
node.data = original
|
|
465
|
+
|
|
466
|
+
errors = []
|
|
467
|
+
for node in nodes:
|
|
468
|
+
if not node.requires_grad:
|
|
469
|
+
continue
|
|
470
|
+
diff = np.max(np.abs(node.grad - numeric[node]))
|
|
471
|
+
if diff > tol:
|
|
472
|
+
errors.append((node, diff, node.grad, numeric[node]))
|
|
473
|
+
|
|
474
|
+
return errors
|
|
475
|
+
|
|
476
|
+
def _eval_forward(self):
|
|
477
|
+
"""Evaluate current graph data forward to this tensor using NumPy semantics."""
|
|
478
|
+
topo = []
|
|
479
|
+
visited = set()
|
|
480
|
+
|
|
481
|
+
def build(v):
|
|
482
|
+
if v not in visited:
|
|
483
|
+
visited.add(v)
|
|
484
|
+
for child in v._prev:
|
|
485
|
+
build(child)
|
|
486
|
+
topo.append(v)
|
|
487
|
+
|
|
488
|
+
build(self)
|
|
489
|
+
|
|
490
|
+
for node in topo:
|
|
491
|
+
if node._op == "leaf":
|
|
492
|
+
continue
|
|
493
|
+
if node._op == "+":
|
|
494
|
+
node.data = list(node._prev)[0].data + list(node._prev)[1].data
|
|
495
|
+
elif node._op == "-":
|
|
496
|
+
node.data = list(node._prev)[0].data - list(node._prev)[1].data
|
|
497
|
+
elif node._op == "*":
|
|
498
|
+
node.data = list(node._prev)[0].data * list(node._prev)[1].data
|
|
499
|
+
elif node._op == "/":
|
|
500
|
+
node.data = list(node._prev)[0].data / list(node._prev)[1].data
|
|
501
|
+
elif node._op == "**":
|
|
502
|
+
node.data = list(node._prev)[0].data ** float(2)
|
|
503
|
+
elif node._op == "@":
|
|
504
|
+
node.data = list(node._prev)[0].data @ list(node._prev)[1].data
|
|
505
|
+
elif node._op == "relu":
|
|
506
|
+
node.data = np.maximum(0, list(node._prev)[0].data)
|
|
507
|
+
elif node._op == "exp":
|
|
508
|
+
node.data = np.exp(list(node._prev)[0].data)
|
|
509
|
+
elif node._op == "log":
|
|
510
|
+
node.data = np.log(list(node._prev)[0].data)
|
|
511
|
+
elif node._op == "sigmoid":
|
|
512
|
+
x = list(node._prev)[0].data
|
|
513
|
+
node.data = 1 / (1 + np.exp(-x))
|
|
514
|
+
elif node._op == "softmax":
|
|
515
|
+
x = list(node._prev)[0].data
|
|
516
|
+
shifted = x - x.max(axis=-1, keepdims=True)
|
|
517
|
+
exp_values = np.exp(shifted)
|
|
518
|
+
node.data = exp_values / exp_values.sum(axis=-1, keepdims=True)
|
|
519
|
+
elif node._op == "sum":
|
|
520
|
+
node.data = list(node._prev)[0].data.sum()
|
|
521
|
+
elif node._op == "mean":
|
|
522
|
+
node.data = list(node._prev)[0].data.mean()
|
|
523
|
+
return float(self.data) if np.isscalar(self.data) else self.data
|
|
524
|
+
|
|
525
|
+
def zero_grad_all(self):
|
|
526
|
+
"""Zero all gradients in the current graph."""
|
|
527
|
+
nodes, _ = self._graph_nodes()
|
|
528
|
+
for node in nodes:
|
|
529
|
+
if node.requires_grad:
|
|
530
|
+
node.grad = np.zeros_like(node.data)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leanpass
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, transparent alternative to PyTorch/TensorFlow built on NumPy.
|
|
5
|
+
Author: LeanPass
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
|
|
10
|
+
# leanpass
|
|
11
|
+
|
|
12
|
+
A minimal, transparent NumPy-based autograd library for small models.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- `Tensor` with reverse-mode autodiff
|
|
17
|
+
- Operators: `+`, `-`, `*`, `/`, `**`, `@`
|
|
18
|
+
- Activations: `relu`, `sigmoid`, `softmax`
|
|
19
|
+
- `Linear` and `MLP` modules
|
|
20
|
+
- `SGD` and `Adam` optimizers
|
|
21
|
+
- Graph visualization via `Tensor.visualize_dot()`
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from leanpass import Tensor, nn, optim
|
|
33
|
+
|
|
34
|
+
x = Tensor([[1.0, 2.0]], requires_grad=False)
|
|
35
|
+
model = nn.MLP([2, 16, 3])
|
|
36
|
+
logits = model(x)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run demo
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python demo.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Publish to PyPI
|
|
46
|
+
|
|
47
|
+
1. Update `version` in `pyproject.toml`.
|
|
48
|
+
2. Install build tools:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install --upgrade build twine
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Build distribution files:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. Upload to PyPI:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m twine upload dist/*
|
|
64
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.cfg
|
|
4
|
+
leanpass/__init__.py
|
|
5
|
+
leanpass/nn.py
|
|
6
|
+
leanpass/optim.py
|
|
7
|
+
leanpass/tensor.py
|
|
8
|
+
leanpass.egg-info/PKG-INFO
|
|
9
|
+
leanpass.egg-info/SOURCES.txt
|
|
10
|
+
leanpass.egg-info/dependency_links.txt
|
|
11
|
+
leanpass.egg-info/entry_points.txt
|
|
12
|
+
leanpass.egg-info/requires.txt
|
|
13
|
+
leanpass.egg-info/top_level.txt
|
|
14
|
+
tests/test_gradcheck.py
|
|
15
|
+
tests/test_leanpass.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
leanpass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "leanpass"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lightweight, transparent alternative to PyTorch/TensorFlow built on NumPy."
|
|
5
|
+
authors = [{ name = "LeanPass" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.8"
|
|
8
|
+
dependencies = ["numpy"]
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
leanpass-demo = "demo:main"
|
leanpass-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from leanpass import Tensor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def finite_difference_grad(f, x, eps=1e-6):
|
|
7
|
+
orig = x.data.copy()
|
|
8
|
+
grad = np.zeros_like(x.data)
|
|
9
|
+
for idx in np.ndindex(x.data.shape):
|
|
10
|
+
x.data[idx] = orig[idx] + eps
|
|
11
|
+
plus = f().data.copy()
|
|
12
|
+
x.data[idx] = orig[idx] - eps
|
|
13
|
+
minus = f().data.copy()
|
|
14
|
+
x.data[idx] = orig[idx]
|
|
15
|
+
grad[idx] = (plus - minus).sum() / (2 * eps)
|
|
16
|
+
return grad
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_grad_check_add():
|
|
20
|
+
x = Tensor([1.0, 2.0], requires_grad=True)
|
|
21
|
+
y = Tensor([3.0, 4.0], requires_grad=True)
|
|
22
|
+
z = (x + y).sum()
|
|
23
|
+
z.backward()
|
|
24
|
+
|
|
25
|
+
assert np.allclose(x.grad, [1.0, 1.0])
|
|
26
|
+
assert np.allclose(y.grad, [1.0, 1.0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_grad_check_power():
|
|
30
|
+
x = Tensor([2.0, 3.0], requires_grad=True)
|
|
31
|
+
z = (x ** 3).sum()
|
|
32
|
+
z.backward()
|
|
33
|
+
|
|
34
|
+
assert np.allclose(x.grad, [12.0, 27.0])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_grad_check_sigmoid():
|
|
38
|
+
x = Tensor([0.5, -0.5], requires_grad=True)
|
|
39
|
+
z = x.sigmoid().sum()
|
|
40
|
+
z.backward()
|
|
41
|
+
|
|
42
|
+
numeric = finite_difference_grad(lambda: x.sigmoid(), x)
|
|
43
|
+
assert np.allclose(x.grad, numeric, atol=1e-4)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_grad_check_exp_log():
|
|
47
|
+
x = Tensor([0.5, 1.0], requires_grad=True)
|
|
48
|
+
z = x.exp().log().sum()
|
|
49
|
+
z.backward()
|
|
50
|
+
|
|
51
|
+
numeric = finite_difference_grad(lambda: x.exp().log(), x)
|
|
52
|
+
assert np.allclose(x.grad, numeric, atol=1e-4)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_grad_check_softmax():
|
|
56
|
+
x = Tensor([[1.0, 2.0, 3.0]], requires_grad=True)
|
|
57
|
+
z = x.softmax().sum()
|
|
58
|
+
z.backward()
|
|
59
|
+
|
|
60
|
+
numeric = finite_difference_grad(lambda: x.softmax(), x)
|
|
61
|
+
assert np.allclose(x.grad, numeric, atol=1e-4)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_grad_check_graph_visualizer():
|
|
65
|
+
x = Tensor([1.0, 2.0], requires_grad=True, name="input")
|
|
66
|
+
y = (x * x + x.sigmoid()).sum()
|
|
67
|
+
graph = y.visualize()
|
|
68
|
+
|
|
69
|
+
assert "softmax" not in graph
|
|
70
|
+
assert "relu" not in graph
|
|
71
|
+
assert "input" in graph
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_visualize_dot():
|
|
75
|
+
x = Tensor([1.0, 2.0], requires_grad=True, name="input")
|
|
76
|
+
y = (x * x + x.sigmoid()).sum()
|
|
77
|
+
dot = y.visualize_dot()
|
|
78
|
+
|
|
79
|
+
assert dot.startswith("digraph")
|
|
80
|
+
assert "->" in dot
|
|
81
|
+
assert "input" in dot
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from leanpass import Tensor, nn, optim
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_tensor_basic_arithmetic():
|
|
7
|
+
x = Tensor([1.0, 2.0], requires_grad=True)
|
|
8
|
+
y = Tensor([3.0, 4.0], requires_grad=True)
|
|
9
|
+
|
|
10
|
+
z = x * y + x
|
|
11
|
+
z_sum = z.sum()
|
|
12
|
+
z_sum.backward()
|
|
13
|
+
|
|
14
|
+
assert np.allclose(x.grad, [4.0, 5.0])
|
|
15
|
+
assert np.allclose(y.grad, [1.0, 2.0])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_linear_forward_backward():
|
|
19
|
+
layer = nn.Linear(2, 1)
|
|
20
|
+
x = Tensor([[1.0, 2.0]], requires_grad=False)
|
|
21
|
+
y = layer(x)
|
|
22
|
+
loss = y.sum()
|
|
23
|
+
loss.backward()
|
|
24
|
+
|
|
25
|
+
assert layer.weight.grad.shape == layer.weight.data.shape
|
|
26
|
+
assert layer.bias.grad.shape == layer.bias.data.shape
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_mlp_training_step():
|
|
30
|
+
model = nn.MLP([2, 4, 1])
|
|
31
|
+
optimizer = optim.SGD(model.parameters(), lr=0.1)
|
|
32
|
+
|
|
33
|
+
x = Tensor([[1.0, 1.0]], requires_grad=False)
|
|
34
|
+
y_true = Tensor([[2.0]], requires_grad=False)
|
|
35
|
+
|
|
36
|
+
pred = model(x)
|
|
37
|
+
loss = nn.mse_loss(pred, y_true)
|
|
38
|
+
model.zero_grad()
|
|
39
|
+
loss.backward()
|
|
40
|
+
optimizer.step()
|
|
41
|
+
|
|
42
|
+
assert all(param.grad is not None for param in model.parameters())
|
|
43
|
+
assert any(np.any(param.data != 0) for param in model.parameters())
|