pytinytensor 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.
- pytinytensor-0.1.0/PKG-INFO +13 -0
- pytinytensor-0.1.0/README.md +203 -0
- pytinytensor-0.1.0/pyproject.toml +3 -0
- pytinytensor-0.1.0/pytinytensor.egg-info/PKG-INFO +13 -0
- pytinytensor-0.1.0/pytinytensor.egg-info/SOURCES.txt +37 -0
- pytinytensor-0.1.0/pytinytensor.egg-info/dependency_links.txt +1 -0
- pytinytensor-0.1.0/pytinytensor.egg-info/requires.txt +5 -0
- pytinytensor-0.1.0/pytinytensor.egg-info/top_level.txt +1 -0
- pytinytensor-0.1.0/setup.cfg +4 -0
- pytinytensor-0.1.0/setup.py +81 -0
- pytinytensor-0.1.0/tests/test_autograd.py +59 -0
- pytinytensor-0.1.0/tests/test_data.py +48 -0
- pytinytensor-0.1.0/tests/test_losses.py +31 -0
- pytinytensor-0.1.0/tests/test_nn.py +91 -0
- pytinytensor-0.1.0/tests/test_optim.py +63 -0
- pytinytensor-0.1.0/tests/test_tensor_math.py +140 -0
- pytinytensor-0.1.0/tinytensor/__init__.py +0 -0
- pytinytensor-0.1.0/tinytensor/backends/__init__.py +0 -0
- pytinytensor-0.1.0/tinytensor/backends/cpu_numpy.py +0 -0
- pytinytensor-0.1.0/tinytensor/backends/cuda_binding.cpp +41 -0
- pytinytensor-0.1.0/tinytensor/backends/cuda_gpu.cu +41 -0
- pytinytensor-0.1.0/tinytensor/config.py +20 -0
- pytinytensor-0.1.0/tinytensor/core/__init__.py +0 -0
- pytinytensor-0.1.0/tinytensor/core/autograd.py +22 -0
- pytinytensor-0.1.0/tinytensor/core/ops.py +1 -0
- pytinytensor-0.1.0/tinytensor/core/tensor.py +288 -0
- pytinytensor-0.1.0/tinytensor/data/__init__.py +5 -0
- pytinytensor-0.1.0/tinytensor/data/dataloader.py +41 -0
- pytinytensor-0.1.0/tinytensor/data/dataset.py +19 -0
- pytinytensor-0.1.0/tinytensor/nn/__init__.py +0 -0
- pytinytensor-0.1.0/tinytensor/nn/activations.py +36 -0
- pytinytensor-0.1.0/tinytensor/nn/linear.py +18 -0
- pytinytensor-0.1.0/tinytensor/nn/losses.py +12 -0
- pytinytensor-0.1.0/tinytensor/nn/modules.py +63 -0
- pytinytensor-0.1.0/tinytensor/optim/__init__.py +11 -0
- pytinytensor-0.1.0/tinytensor/optim/optimizer.py +72 -0
- pytinytensor-0.1.0/tinytensor/utils/__init__.py +4 -0
- pytinytensor-0.1.0/tinytensor/utils/bar.py +26 -0
- pytinytensor-0.1.0/tinytensor/utils/early_stopping.py +64 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytinytensor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: numpy>=1.23
|
|
7
|
+
Requires-Dist: pybind11>=2.10
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
10
|
+
Dynamic: provides-extra
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# tinytensor
|
|
2
|
+
|
|
3
|
+
Маленький самописный autograd на numpy. Тензоры, backprop, пара слоев,
|
|
4
|
+
лоссы, оптимизаторы и даталоадер. По сути свой мини-pytorch, только без
|
|
5
|
+
плюшек и без cuda (пока).
|
|
6
|
+
|
|
7
|
+
Никакой производительности тут не ищите, тут просто видно как все
|
|
8
|
+
устроено внутри, без магии.
|
|
9
|
+
|
|
10
|
+
## Содержание
|
|
11
|
+
|
|
12
|
+
- [Установка](#установка)
|
|
13
|
+
- [Быстрый старт](#быстрый-старт)
|
|
14
|
+
- [Как работает autograd](#как-работает-autograd)
|
|
15
|
+
- [API](#api)
|
|
16
|
+
- [Примеры](#примеры)
|
|
17
|
+
- [Тесты](#тесты)
|
|
18
|
+
- [Структура](#структура)
|
|
19
|
+
- [Чего нет](#чего-нет)
|
|
20
|
+
- [Ссылки](#ссылки)
|
|
21
|
+
|
|
22
|
+
## Установка
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/IbrokhimN/tinytensor
|
|
26
|
+
cd tinytensor
|
|
27
|
+
pip install -e . # обычная установка
|
|
28
|
+
pip install -e .[dev] # плюс pytest, если хотите гонять тесты
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Или руками:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -r requirements.txt
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Зависимость одна - numpy.
|
|
38
|
+
|
|
39
|
+
## Быстрый старт
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from tinytensor.core.tensor import Tensor
|
|
43
|
+
from tinytensor.nn.linear import Linear
|
|
44
|
+
from tinytensor.nn.losses import MSELoss
|
|
45
|
+
from tinytensor.optim import SGD
|
|
46
|
+
|
|
47
|
+
model = Linear(in_features=1, out_features=1)
|
|
48
|
+
loss_fn = MSELoss()
|
|
49
|
+
optimizer = SGD(model.parameters(), lr=0.01)
|
|
50
|
+
|
|
51
|
+
x = Tensor([[1.0], [2.0], [3.0]])
|
|
52
|
+
y = Tensor([[3.0], [5.0], [7.0]]) # y = 2x + 1
|
|
53
|
+
|
|
54
|
+
for epoch in range(100):
|
|
55
|
+
optimizer.zero_grad()
|
|
56
|
+
pred = model(x)
|
|
57
|
+
loss = loss_fn(pred, y)
|
|
58
|
+
loss.backward()
|
|
59
|
+
optimizer.step()
|
|
60
|
+
|
|
61
|
+
print(model.weight.data, model.bias.data) # ~2.0, ~1.0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Рабочие примеры целиком лежат в [`examples/`](examples/).
|
|
65
|
+
|
|
66
|
+
## Как работает autograd
|
|
67
|
+
|
|
68
|
+
У каждого `Tensor` есть:
|
|
69
|
+
|
|
70
|
+
- `data` - сами значения (numpy-массив, всегда float32);
|
|
71
|
+
- `grad` - градиент, пока не посчитан - None;
|
|
72
|
+
- `_prev` - от каких тензоров он произошел (родители в графе);
|
|
73
|
+
- `_backward` - функция которая знает как раскидать градиент на родителей.
|
|
74
|
+
|
|
75
|
+
Когда считаете `z = f(x, y)`, по цепному правилу:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
dL/dx = dL/dz * dz/dx
|
|
79
|
+
dL/dy = dL/dz * dz/dy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`backward()` строит топологический порядок графа (`tinytensor/core/autograd.py`),
|
|
83
|
+
ставит корню градиент = 1 и идет в обратном порядке, вызывая `_backward()`
|
|
84
|
+
у каждого узла. Ровно так же устроен micrograd Карпатова, только у него
|
|
85
|
+
даже покомпактнее:
|
|
86
|
+
|
|
87
|
+
- [Karpathy - micrograd](https://github.com/karpathy/micrograd) - реализация того же самого на ~100 строк, must see
|
|
88
|
+
- [CS231n: Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/) - если нужно разложить backprop по полочкам
|
|
89
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
90
|
+
|
|
91
|
+
Если совсем в тему - есть видос Карпатова где он с нуля пишет micrograd и
|
|
92
|
+
объясняет каждую строчку: ["The spelled-out intro to neural networks and backpropagation"](https://www.youtube.com/watch?v=VMj-3S1tku0).
|
|
93
|
+
Собственно тут все то же самое, только на numpy и чуть пошире.
|
|
94
|
+
|
|
95
|
+
### Формулы, которые реализованы в Tensor
|
|
96
|
+
|
|
97
|
+
| операция | вперед | производная |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `a + b` | `a + b` | `dL/da += dL/dz`, `dL/db += dL/dz` (плюс схлопывание по broadcast-осям) |
|
|
100
|
+
| `a * b` | `a * b` | `dL/da += dL/dz * b`, `dL/db += dL/dz * a` |
|
|
101
|
+
| `a @ b` | matmul | `dL/da += dL/dz @ bᵀ`, `dL/db += aᵀ @ dL/dz` |
|
|
102
|
+
| `a ** p` | `aᵖ` | `dL/da += dL/dz * p * a^(p-1)` |
|
|
103
|
+
| ReLU | `max(0, x)` | 1 при x>0, иначе 0 |
|
|
104
|
+
| LeakyReLU | x или αx | 1 при x>0, иначе α |
|
|
105
|
+
| sigmoid | `1/(1+e⁻ˣ)` | `σ(x)*(1-σ(x))` |
|
|
106
|
+
| tanh | `tanh(x)` | `1 - tanh²(x)` |
|
|
107
|
+
| GELU | `0.5x(1+tanh(√(2/π)(x+0.044715x³)))` | см. [статью по GELU](https://arxiv.org/abs/1606.08415), формула там не самая короткая |
|
|
108
|
+
|
|
109
|
+
Про broadcasting и почему градиент иногда надо досуммировать обратно
|
|
110
|
+
(`_unbroadcast`) норм объясняют [правила broadcasting в numpy](https://numpy.org/doc/stable/user/basics.broadcasting.html).
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
### Tensor
|
|
115
|
+
|
|
116
|
+
`tinytensor.core.tensor.Tensor` - главный класс. Есть `+ - * @ **`, `.sum()`,
|
|
117
|
+
активации `relu / leaky_relu / sigmoid / tanh / gelu`, ну и `.backward()`.
|
|
118
|
+
|
|
119
|
+
### nn
|
|
120
|
+
|
|
121
|
+
- `Module` - от него наследуются все слои, `forward / parameters() / zero_grad()`, по духу как [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html);
|
|
122
|
+
- `Linear(in_features, out_features)` - обычный `y = xW + b`, веса инициализируются по [He/Kaiming init](https://arxiv.org/abs/1502.01852) (`std = sqrt(2/in_features)`);
|
|
123
|
+
- `ReLU, LeReLU, Sigmod, Tanh, GELU` - тонкие обертки над методами Tensor;
|
|
124
|
+
- `MSELoss` - `mean((pred - target)^2)`, банальщина.
|
|
125
|
+
|
|
126
|
+
### optim
|
|
127
|
+
|
|
128
|
+
- `SGD(params, lr, momentum=0.0)` - обычный градиентный спуск, можно с моментом;
|
|
129
|
+
- `AdamW(params, lr, betas, eps, weight_decay)` - Adam с отдельным weight decay, см. [Loshchilov & Hutter](https://arxiv.org/abs/1711.05101) (в отличие от обычного [Adam](https://arxiv.org/abs/1412.6980), decay тут не лезет в градиент, а сразу режет веса).
|
|
130
|
+
|
|
131
|
+
### data
|
|
132
|
+
|
|
133
|
+
- `Dataset / TensorDataset` - обертка над (x, y);
|
|
134
|
+
- `DataLoader` - бьет на батчи, можно с shuffle, мини-версия [`torch.utils.data.DataLoader`](https://pytorch.org/docs/stable/data.html).
|
|
135
|
+
|
|
136
|
+
## Примеры
|
|
137
|
+
|
|
138
|
+
- [`examples/01_linear_regression.py`](examples/01_linear_regression.py) - линейная регрессия, Linear + MSELoss + SGD на `y = 3x + 2 + шум`.
|
|
139
|
+
- [`examples/02_mnist_mlp.py`](examples/02_mnist_mlp.py) - MLP (Linear -> ReLU -> Linear) с AdamW и DataLoader на синтетике в формате mnist (784 фичи, 10 классов). Настоящего mnist и загрузчиков датасетов тут нет, лень было тащить.
|
|
140
|
+
|
|
141
|
+
Как выглядит запуск вживую:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
$ python3 01_linear_regression.py
|
|
145
|
+
epoch 0 | loss 30.8016
|
|
146
|
+
epoch 180 | loss 0.2365
|
|
147
|
+
выученные параметры: weight ~ 2.995, bias ~ 1.996
|
|
148
|
+
|
|
149
|
+
$ python3 02_mnist_mlp.py
|
|
150
|
+
epoch 1/5 | avg loss 2.0998
|
|
151
|
+
epoch 5/5 | avg loss 0.0481
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Тесты
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pip install -e .[dev]
|
|
158
|
+
python3 -m pytest tests/ -v
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
47 штук, гоняют арифметику тензоров и broadcasting, autograd (накопление
|
|
162
|
+
градиента, топология, повторный backward), лоссы, оптимизаторы и
|
|
163
|
+
Dataset/DataLoader.
|
|
164
|
+
|
|
165
|
+
> Важно: запускать через pytest из корня репы (или после `pip install -e .`),
|
|
166
|
+
> а не `python3 test_x.py` из папки tests - иначе tinytensor просто не найдется.
|
|
167
|
+
|
|
168
|
+
## Структура
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
tinytensor/
|
|
172
|
+
├── tinytensor/
|
|
173
|
+
│ ├── core/ # Tensor, autograd, ops
|
|
174
|
+
│ ├── nn/ # Module, Linear, активации, MSELoss
|
|
175
|
+
│ ├── optim/ # SGD, AdamW
|
|
176
|
+
│ ├── data/ # Dataset, DataLoader
|
|
177
|
+
│ ├── backends/ # заготовка под cuda, пока пусто
|
|
178
|
+
│ └── config.py # сид рандома
|
|
179
|
+
├── examples/
|
|
180
|
+
├── tests/
|
|
181
|
+
├── setup.py
|
|
182
|
+
└── requirements.txt
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Чего нет
|
|
186
|
+
|
|
187
|
+
- Только numpy-backend, cuda_gpu.py пустой файл-заглушка.
|
|
188
|
+
- Нет кросс-энтропии, сверток, рекуррентных слоев, сохранения модели (см. ToDo в исходном плане проекта).
|
|
189
|
+
- backward не кэширует граф между вызовами, каждый раз строит топологию заново, как и в micrograd - никакой лени в духе pytorch тут нет.
|
|
190
|
+
|
|
191
|
+
## Ссылки
|
|
192
|
+
|
|
193
|
+
- [Andrej Karpathy - micrograd (GitHub)](https://github.com/karpathy/micrograd) - основной референс для всего autograd
|
|
194
|
+
- [Andrej Karpathy - видео про backprop и micrograd](https://www.youtube.com/watch?v=VMj-3S1tku0)
|
|
195
|
+
- [CS231n - Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/)
|
|
196
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
197
|
+
- [NumPy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html)
|
|
198
|
+
- [He et al. - инициализация весов](https://arxiv.org/abs/1502.01852)
|
|
199
|
+
- [Kingma & Ba - Adam](https://arxiv.org/abs/1412.6980)
|
|
200
|
+
- [Loshchilov & Hutter - AdamW](https://arxiv.org/abs/1711.05101)
|
|
201
|
+
- [Hendrycks & Gimpel - GELU](https://arxiv.org/abs/1606.08415)
|
|
202
|
+
- [PyTorch - torch.nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html)
|
|
203
|
+
- [PyTorch - torch.utils.data.DataLoader](https://pytorch.org/docs/stable/data.html)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytinytensor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: numpy>=1.23
|
|
7
|
+
Requires-Dist: pybind11>=2.10
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
10
|
+
Dynamic: provides-extra
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
pytinytensor.egg-info/PKG-INFO
|
|
5
|
+
pytinytensor.egg-info/SOURCES.txt
|
|
6
|
+
pytinytensor.egg-info/dependency_links.txt
|
|
7
|
+
pytinytensor.egg-info/requires.txt
|
|
8
|
+
pytinytensor.egg-info/top_level.txt
|
|
9
|
+
tests/test_autograd.py
|
|
10
|
+
tests/test_data.py
|
|
11
|
+
tests/test_losses.py
|
|
12
|
+
tests/test_nn.py
|
|
13
|
+
tests/test_optim.py
|
|
14
|
+
tests/test_tensor_math.py
|
|
15
|
+
tinytensor/__init__.py
|
|
16
|
+
tinytensor/config.py
|
|
17
|
+
tinytensor/backends/__init__.py
|
|
18
|
+
tinytensor/backends/cpu_numpy.py
|
|
19
|
+
tinytensor/backends/cuda_binding.cpp
|
|
20
|
+
tinytensor/backends/cuda_gpu.cu
|
|
21
|
+
tinytensor/core/__init__.py
|
|
22
|
+
tinytensor/core/autograd.py
|
|
23
|
+
tinytensor/core/ops.py
|
|
24
|
+
tinytensor/core/tensor.py
|
|
25
|
+
tinytensor/data/__init__.py
|
|
26
|
+
tinytensor/data/dataloader.py
|
|
27
|
+
tinytensor/data/dataset.py
|
|
28
|
+
tinytensor/nn/__init__.py
|
|
29
|
+
tinytensor/nn/activations.py
|
|
30
|
+
tinytensor/nn/linear.py
|
|
31
|
+
tinytensor/nn/losses.py
|
|
32
|
+
tinytensor/nn/modules.py
|
|
33
|
+
tinytensor/optim/__init__.py
|
|
34
|
+
tinytensor/optim/optimizer.py
|
|
35
|
+
tinytensor/utils/__init__.py
|
|
36
|
+
tinytensor/utils/bar.py
|
|
37
|
+
tinytensor/utils/early_stopping.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tinytensor
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import pybind11
|
|
4
|
+
from setuptools import setup, find_packages, Extension
|
|
5
|
+
from setuptools.command.build_ext import build_ext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_cuda():
|
|
9
|
+
try:
|
|
10
|
+
subprocess.check_output(["nvcc", "--version"])
|
|
11
|
+
return True
|
|
12
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CUDABuildExt(build_ext):
|
|
17
|
+
def build_extensions(self):
|
|
18
|
+
self.compiler.src_extensions.append('.cu')
|
|
19
|
+
original_compile = self.compiler._compile
|
|
20
|
+
|
|
21
|
+
def custom_compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
|
|
22
|
+
if isinstance(extra_postargs, dict):
|
|
23
|
+
gcc_postargs = extra_postargs.get('gcc', [])
|
|
24
|
+
nvcc_postargs = extra_postargs.get('nvcc', [])
|
|
25
|
+
else:
|
|
26
|
+
gcc_postargs = extra_postargs or []
|
|
27
|
+
nvcc_postargs = []
|
|
28
|
+
|
|
29
|
+
if os.path.splitext(src)[1] == '.cu':
|
|
30
|
+
inc_flags = [f"-I{inc}" for inc in self.compiler.include_dirs]
|
|
31
|
+
cmd = ['nvcc', '-c', src, '-o', obj] + inc_flags + nvcc_postargs
|
|
32
|
+
self.spawn(cmd)
|
|
33
|
+
else:
|
|
34
|
+
original_compile(obj, src, ext, cc_args, gcc_postargs, pp_opts)
|
|
35
|
+
|
|
36
|
+
self.compiler._compile = custom_compile
|
|
37
|
+
super().build_extensions()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ext_modules = []
|
|
41
|
+
|
|
42
|
+
if check_cuda():
|
|
43
|
+
cuda_module = Extension(
|
|
44
|
+
'tinytensor.cuda_ops',
|
|
45
|
+
sources=[
|
|
46
|
+
'tinytensor/backends/cuda_gpu.cu',
|
|
47
|
+
'tinytensor/backends/cuda_binding.cpp'
|
|
48
|
+
],
|
|
49
|
+
include_dirs=[
|
|
50
|
+
pybind11.get_include(),
|
|
51
|
+
'/usr/local/cuda/include',
|
|
52
|
+
os.path.expanduser('~/.local/include')
|
|
53
|
+
],
|
|
54
|
+
library_dirs=['/usr/local/cuda/lib64'],
|
|
55
|
+
libraries=['cudart', 'cublas'],
|
|
56
|
+
extra_compile_args={
|
|
57
|
+
'gcc': ['-O3', '-fPIC', '-std=c++17'],
|
|
58
|
+
'nvcc': ['-O3', '-Xcompiler', '-fPIC']
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
ext_modules.append(cuda_module)
|
|
62
|
+
print("CUDA найдена. Собираем cuBLAS бэкенд")
|
|
63
|
+
else:
|
|
64
|
+
print("CUDA не найдена. Сборка продолжится на CPU (NumPy)")
|
|
65
|
+
|
|
66
|
+
setup(
|
|
67
|
+
name="pytinytensor",
|
|
68
|
+
version="0.1.0",
|
|
69
|
+
description="мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )",
|
|
70
|
+
packages=find_packages(include=["tinytensor", "tinytensor.*"]),
|
|
71
|
+
ext_modules=ext_modules,
|
|
72
|
+
cmdclass={'build_ext': CUDABuildExt},
|
|
73
|
+
install_requires=[
|
|
74
|
+
"numpy>=1.23",
|
|
75
|
+
"pybind11>=2.10",
|
|
76
|
+
],
|
|
77
|
+
extras_require={
|
|
78
|
+
"dev": ["pytest>=7.0"],
|
|
79
|
+
},
|
|
80
|
+
python_requires=">=3.9",
|
|
81
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from tinytensor.core.tensor import Tensor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_backward_sets_grad_of_ones_on_root():
|
|
7
|
+
a = Tensor([1.0, 2.0], requires_grad=True)
|
|
8
|
+
out = a.sum()
|
|
9
|
+
out.backward()
|
|
10
|
+
assert out.grad == np.float32(1.0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_no_grad_when_requires_grad_false():
|
|
14
|
+
a = Tensor([1.0, 2.0], requires_grad=False)
|
|
15
|
+
b = Tensor([3.0, 4.0], requires_grad=False)
|
|
16
|
+
c = a + b
|
|
17
|
+
assert c.requires_grad is False
|
|
18
|
+
assert c.grad is None # backward не должен считаться вообще
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_diamond_graph_accumulates_gradients():
|
|
22
|
+
x = Tensor([3.0], requires_grad=True)
|
|
23
|
+
y = x + x
|
|
24
|
+
y.backward()
|
|
25
|
+
assert np.allclose(x.grad, [2.0])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_chain_of_relu_and_sum():
|
|
29
|
+
x = Tensor([-1.0, 2.0, -3.0, 4.0], requires_grad=True)
|
|
30
|
+
out = x.relu().sum()
|
|
31
|
+
out.backward()
|
|
32
|
+
# градиент 1 там где x>0, иначе 0
|
|
33
|
+
assert np.allclose(x.grad, [0.0, 1.0, 0.0, 1.0])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_gradients_accumulate_across_multiple_backward_calls():
|
|
37
|
+
x = Tensor([2.0], requires_grad=True)
|
|
38
|
+
y = x.relu()
|
|
39
|
+
y.backward()
|
|
40
|
+
y2 = x.relu()
|
|
41
|
+
y2.backward()
|
|
42
|
+
assert np.allclose(x.grad, [2.0])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_backward_only_computed_once_per_node():
|
|
46
|
+
calls = {"n": 0}
|
|
47
|
+
x = Tensor([1.0, 2.0], requires_grad=True)
|
|
48
|
+
mid = x.relu()
|
|
49
|
+
|
|
50
|
+
original = mid._backward
|
|
51
|
+
|
|
52
|
+
def counted():
|
|
53
|
+
calls["n"] += 1
|
|
54
|
+
original()
|
|
55
|
+
|
|
56
|
+
mid._backward = counted
|
|
57
|
+
out = mid.sum()
|
|
58
|
+
out.backward()
|
|
59
|
+
assert calls["n"] == 1
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from tinytensor.data import TensorDataset, DataLoader
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_dataset(n=10):
|
|
7
|
+
x = np.arange(n).reshape(n, 1).astype(np.float32)
|
|
8
|
+
y = (np.arange(n) * 2).astype(np.float32)
|
|
9
|
+
return TensorDataset(x, y)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_tensor_dataset_len_and_getitem():
|
|
13
|
+
ds = make_dataset(5)
|
|
14
|
+
assert len(ds) == 5
|
|
15
|
+
x, y = ds[2]
|
|
16
|
+
assert x[0] == 2.0
|
|
17
|
+
assert y == 4.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_dataloader_batches_cover_full_dataset():
|
|
21
|
+
ds = make_dataset(10)
|
|
22
|
+
loader = DataLoader(ds, batch_size=3, shuffle=False)
|
|
23
|
+
seen = 0
|
|
24
|
+
for xb, yb in loader:
|
|
25
|
+
seen += xb.shape[0]
|
|
26
|
+
assert seen == 10
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_dataloader_len_is_ceil_division():
|
|
30
|
+
ds = make_dataset(10)
|
|
31
|
+
loader = DataLoader(ds, batch_size=3)
|
|
32
|
+
assert len(loader) == 4 # ceil(10/3)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_dataloader_last_batch_smaller():
|
|
36
|
+
ds = make_dataset(10)
|
|
37
|
+
loader = DataLoader(ds, batch_size=3, shuffle=False)
|
|
38
|
+
batches = list(loader)
|
|
39
|
+
assert batches[-1][0].shape[0] == 1 # 10 = 3+3+3+1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dataloader_shuffle_changes_order_with_fixed_seed():
|
|
43
|
+
ds = make_dataset(20)
|
|
44
|
+
np.random.seed(0)
|
|
45
|
+
loader = DataLoader(ds, batch_size=20, shuffle=True)
|
|
46
|
+
xb, _ = next(iter(loader))
|
|
47
|
+
order = xb.data.flatten()
|
|
48
|
+
assert not np.array_equal(order, np.arange(20)) # почти наверняка перемешано
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from tinytensor.core.tensor import Tensor
|
|
5
|
+
from tinytensor.nn.losses import MSELoss
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_mse_forward_matches_manual_formula():
|
|
9
|
+
y_pred = Tensor([1.0, 2.0, 3.0])
|
|
10
|
+
y_true = Tensor([1.5, 2.5, 2.0])
|
|
11
|
+
loss = MSELoss()(y_pred, y_true)
|
|
12
|
+
|
|
13
|
+
expected = np.mean((y_pred.data - y_true.data) ** 2)
|
|
14
|
+
assert loss.data == pytest.approx(expected, abs=1e-5)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_mse_zero_when_predictions_match():
|
|
18
|
+
y_pred = Tensor([1.0, 2.0, 3.0])
|
|
19
|
+
y_true = Tensor([1.0, 2.0, 3.0])
|
|
20
|
+
loss = MSELoss()(y_pred, y_true)
|
|
21
|
+
assert loss.data == pytest.approx(0.0, abs=1e-6)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_mse_backward_gives_gradient_wrt_prediction():
|
|
25
|
+
y_pred = Tensor([2.0, 4.0], requires_grad=True)
|
|
26
|
+
y_true = Tensor([0.0, 0.0])
|
|
27
|
+
loss = MSELoss()(y_pred, y_true)
|
|
28
|
+
loss.backward()
|
|
29
|
+
# d(mean((p-t)^2))/dp = 2*(p-t)/n
|
|
30
|
+
expected_grad = 2 * (y_pred.data - y_true.data) / y_pred.data.size
|
|
31
|
+
assert np.allclose(y_pred.grad, expected_grad, atol=1e-5)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from tinytensor.core.tensor import Tensor
|
|
4
|
+
from tinytensor.nn.modules import Module
|
|
5
|
+
from tinytensor.nn.linear import Linear
|
|
6
|
+
from tinytensor.nn.activations import ReLU, LeReLU, Sigmod, Tanh, GELU
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_module_parameters_collects_only_requires_grad_tensors():
|
|
10
|
+
class Dummy(Module):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.w = Tensor([1.0], requires_grad=True)
|
|
14
|
+
self.const = Tensor([2.0], requires_grad=False)
|
|
15
|
+
|
|
16
|
+
m = Dummy()
|
|
17
|
+
params = m.parameters()
|
|
18
|
+
assert len(params) == 1
|
|
19
|
+
assert params[0] is m.w
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_module_parameters_recurse_into_submodules():
|
|
23
|
+
class Inner(Module):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.w = Tensor([1.0], requires_grad=True)
|
|
27
|
+
|
|
28
|
+
class Outer(Module):
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.inner = Inner()
|
|
32
|
+
self.b = Tensor([2.0], requires_grad=True)
|
|
33
|
+
|
|
34
|
+
m = Outer()
|
|
35
|
+
assert len(m.parameters()) == 2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_module_zero_grad():
|
|
39
|
+
class Dummy(Module):
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.w = Tensor([1.0], requires_grad=True)
|
|
43
|
+
|
|
44
|
+
m = Dummy()
|
|
45
|
+
m.w.grad = np.array([9.0], dtype=np.float32)
|
|
46
|
+
m.zero_grad()
|
|
47
|
+
assert m.w.grad is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_linear_forward_shape():
|
|
51
|
+
layer = Linear(in_features=4, out_features=3)
|
|
52
|
+
x = Tensor(np.random.randn(5, 4))
|
|
53
|
+
out = layer(x)
|
|
54
|
+
assert out.shape == (5, 3)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_linear_has_weight_and_bias_as_params():
|
|
58
|
+
layer = Linear(3, 2)
|
|
59
|
+
params = layer.parameters()
|
|
60
|
+
assert len(params) == 2 # weight + bias
|
|
61
|
+
assert layer.weight.shape == (3, 2)
|
|
62
|
+
assert layer.bias.shape == (1, 2)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_relu_module_matches_tensor_method():
|
|
66
|
+
x = Tensor([-1.0, 2.0])
|
|
67
|
+
assert np.allclose(ReLU()(x).data, x.relu().data)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_leaky_relu_module():
|
|
71
|
+
x = Tensor([-2.0, 3.0])
|
|
72
|
+
out = LeReLU(alpha=0.2)(x)
|
|
73
|
+
assert np.allclose(out.data, [-0.4, 3.0])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_sigmoid_module():
|
|
77
|
+
x = Tensor([0.0])
|
|
78
|
+
out = Sigmod()(x)
|
|
79
|
+
assert np.allclose(out.data, [0.5])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_tanh_module():
|
|
83
|
+
x = Tensor([0.0])
|
|
84
|
+
out = Tanh()(x)
|
|
85
|
+
assert np.allclose(out.data, [0.0])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_gelu_module():
|
|
89
|
+
x = Tensor([0.0])
|
|
90
|
+
out = GELU()(x)
|
|
91
|
+
assert np.allclose(out.data, [0.0], atol=1e-5)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from tinytensor.core.tensor import Tensor
|
|
4
|
+
from tinytensor.optim import SGD, AdamW
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_sgd_step_moves_against_gradient():
|
|
8
|
+
p = Tensor([1.0, 1.0], requires_grad=True)
|
|
9
|
+
p.grad = np.array([1.0, 2.0], dtype=np.float32)
|
|
10
|
+
opt = SGD([p], lr=0.1)
|
|
11
|
+
opt.step()
|
|
12
|
+
assert np.allclose(p.data, [0.9, 0.8])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_sgd_zero_grad_resets_to_none():
|
|
16
|
+
p = Tensor([1.0], requires_grad=True)
|
|
17
|
+
p.grad = np.array([5.0], dtype=np.float32)
|
|
18
|
+
opt = SGD([p], lr=0.1)
|
|
19
|
+
opt.zero_grad()
|
|
20
|
+
assert p.grad is None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_sgd_momentum_accumulates_velocity():
|
|
24
|
+
p = Tensor([0.0], requires_grad=True)
|
|
25
|
+
opt = SGD([p], lr=1.0, momentum=0.9)
|
|
26
|
+
|
|
27
|
+
p.grad = np.array([1.0], dtype=np.float32)
|
|
28
|
+
opt.step()
|
|
29
|
+
first = p.data.copy()
|
|
30
|
+
|
|
31
|
+
p.grad = np.array([1.0], dtype=np.float32)
|
|
32
|
+
opt.step()
|
|
33
|
+
#второй шаг должен быть больше первого изза накопленной скорости
|
|
34
|
+
assert abs(p.data[0] - first[0]) > 1.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_sgd_skips_params_without_grad():
|
|
38
|
+
p1 = Tensor([1.0], requires_grad=True)
|
|
39
|
+
p2 = Tensor([1.0], requires_grad=True)
|
|
40
|
+
p1.grad = np.array([1.0], dtype=np.float32)
|
|
41
|
+
p2.grad = None # градиент не считался
|
|
42
|
+
opt = SGD([p1, p2], lr=0.1)
|
|
43
|
+
opt.step()
|
|
44
|
+
assert p2.data[0] == 1.0 # не изменился
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_adamw_step_changes_parameters():
|
|
48
|
+
p = Tensor([1.0, -1.0], requires_grad=True)
|
|
49
|
+
p.grad = np.array([0.5, -0.5], dtype=np.float32)
|
|
50
|
+
opt = AdamW([p], lr=0.1)
|
|
51
|
+
before = p.data.copy()
|
|
52
|
+
opt.step()
|
|
53
|
+
assert not np.allclose(p.data, before)
|
|
54
|
+
assert opt.t == 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_adamw_multiple_steps_reduce_simple_quadratic():
|
|
58
|
+
p = Tensor([5.0], requires_grad=True)
|
|
59
|
+
opt = AdamW([p], lr=0.5)
|
|
60
|
+
for _ in range(50):
|
|
61
|
+
p.grad = 2.0 * p.data
|
|
62
|
+
opt.step()
|
|
63
|
+
assert abs(p.data[0]) < abs(5.0)
|