pytinytensor 0.1.0__tar.gz → 0.1.2__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.2/LICENSE +21 -0
- pytinytensor-0.1.2/PKG-INFO +226 -0
- pytinytensor-0.1.2/pytinytensor.egg-info/PKG-INFO +226 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/pytinytensor.egg-info/SOURCES.txt +24 -0
- pytinytensor-0.1.2/setup.py +133 -0
- pytinytensor-0.1.2/tinytensor/nn/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2/tinytensor}/setup.py +9 -9
- pytinytensor-0.1.2/tinytensor/tinytensor/__init__.py +0 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/backends/__init__.py +0 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/backends/cpu_numpy.py +0 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/config.py +20 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/core/__init__.py +0 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/core/autograd.py +22 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/core/ops.py +1 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/core/tensor.py +288 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/data/__init__.py +5 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/data/dataloader.py +41 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/data/dataset.py +19 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/nn/__init__.py +0 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/nn/activations.py +36 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/nn/linear.py +18 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/nn/losses.py +12 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/nn/modules.py +64 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/optim/__init__.py +11 -0
- pytinytensor-0.1.2/tinytensor/tinytensor/optim/optimizer.py +72 -0
- pytinytensor-0.1.2/tinytensor/utils/__init__.py +4 -0
- pytinytensor-0.1.2/tinytensor/utils/bar.py +26 -0
- pytinytensor-0.1.2/tinytensor/utils/early_stopping.py +64 -0
- pytinytensor-0.1.0/PKG-INFO +0 -13
- pytinytensor-0.1.0/pytinytensor.egg-info/PKG-INFO +0 -13
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/README.md +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/pyproject.toml +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/pytinytensor.egg-info/dependency_links.txt +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/pytinytensor.egg-info/requires.txt +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/pytinytensor.egg-info/top_level.txt +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/setup.cfg +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_autograd.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_data.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_losses.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_nn.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_optim.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tests/test_tensor_math.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/backends/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/backends/cpu_numpy.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/backends/cuda_binding.cpp +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/backends/cuda_gpu.cu +0 -0
- /pytinytensor-0.1.0/tinytensor/core/__init__.py → /pytinytensor-0.1.2/tinytensor/backends/cuda_gpu.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/config.py +0 -0
- {pytinytensor-0.1.0/tinytensor/nn → pytinytensor-0.1.2/tinytensor/core}/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/core/autograd.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/core/ops.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/core/tensor.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/data/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/data/dataloader.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/data/dataset.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/nn/activations.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/nn/linear.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/nn/losses.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/nn/modules.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/optim/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2}/tinytensor/optim/optimizer.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2/tinytensor}/tinytensor/utils/__init__.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2/tinytensor}/tinytensor/utils/bar.py +0 -0
- {pytinytensor-0.1.0 → pytinytensor-0.1.2/tinytensor}/tinytensor/utils/early_stopping.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IbrokhimN
|
|
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.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytinytensor
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )
|
|
5
|
+
Home-page: https://github.com/IbrokhimN/tinytensor
|
|
6
|
+
Author: IbrokhimN
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=1.23
|
|
11
|
+
Requires-Dist: pybind11>=2.10
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: provides-extra
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
# tinytensor
|
|
25
|
+
|
|
26
|
+
Маленький самописный autograd на numpy. Тензоры, backprop, пара слоев,
|
|
27
|
+
лоссы, оптимизаторы и даталоадер. По сути свой мини-pytorch, только без
|
|
28
|
+
плюшек и без cuda (пока).
|
|
29
|
+
|
|
30
|
+
Никакой производительности тут не ищите, тут просто видно как все
|
|
31
|
+
устроено внутри, без магии.
|
|
32
|
+
|
|
33
|
+
## Содержание
|
|
34
|
+
|
|
35
|
+
- [Установка](#установка)
|
|
36
|
+
- [Быстрый старт](#быстрый-старт)
|
|
37
|
+
- [Как работает autograd](#как-работает-autograd)
|
|
38
|
+
- [API](#api)
|
|
39
|
+
- [Примеры](#примеры)
|
|
40
|
+
- [Тесты](#тесты)
|
|
41
|
+
- [Структура](#структура)
|
|
42
|
+
- [Чего нет](#чего-нет)
|
|
43
|
+
- [Ссылки](#ссылки)
|
|
44
|
+
|
|
45
|
+
## Установка
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/IbrokhimN/tinytensor
|
|
49
|
+
cd tinytensor
|
|
50
|
+
pip install -e . # обычная установка
|
|
51
|
+
pip install -e .[dev] # плюс pytest, если хотите гонять тесты
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Или руками:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -r requirements.txt
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Зависимость одна - numpy.
|
|
61
|
+
|
|
62
|
+
## Быстрый старт
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from tinytensor.core.tensor import Tensor
|
|
66
|
+
from tinytensor.nn.linear import Linear
|
|
67
|
+
from tinytensor.nn.losses import MSELoss
|
|
68
|
+
from tinytensor.optim import SGD
|
|
69
|
+
|
|
70
|
+
model = Linear(in_features=1, out_features=1)
|
|
71
|
+
loss_fn = MSELoss()
|
|
72
|
+
optimizer = SGD(model.parameters(), lr=0.01)
|
|
73
|
+
|
|
74
|
+
x = Tensor([[1.0], [2.0], [3.0]])
|
|
75
|
+
y = Tensor([[3.0], [5.0], [7.0]]) # y = 2x + 1
|
|
76
|
+
|
|
77
|
+
for epoch in range(100):
|
|
78
|
+
optimizer.zero_grad()
|
|
79
|
+
pred = model(x)
|
|
80
|
+
loss = loss_fn(pred, y)
|
|
81
|
+
loss.backward()
|
|
82
|
+
optimizer.step()
|
|
83
|
+
|
|
84
|
+
print(model.weight.data, model.bias.data) # ~2.0, ~1.0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Рабочие примеры целиком лежат в [`examples/`](examples/).
|
|
88
|
+
|
|
89
|
+
## Как работает autograd
|
|
90
|
+
|
|
91
|
+
У каждого `Tensor` есть:
|
|
92
|
+
|
|
93
|
+
- `data` - сами значения (numpy-массив, всегда float32);
|
|
94
|
+
- `grad` - градиент, пока не посчитан - None;
|
|
95
|
+
- `_prev` - от каких тензоров он произошел (родители в графе);
|
|
96
|
+
- `_backward` - функция которая знает как раскидать градиент на родителей.
|
|
97
|
+
|
|
98
|
+
Когда считаете `z = f(x, y)`, по цепному правилу:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
dL/dx = dL/dz * dz/dx
|
|
102
|
+
dL/dy = dL/dz * dz/dy
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`backward()` строит топологический порядок графа (`tinytensor/core/autograd.py`),
|
|
106
|
+
ставит корню градиент = 1 и идет в обратном порядке, вызывая `_backward()`
|
|
107
|
+
у каждого узла. Ровно так же устроен micrograd Карпатова, только у него
|
|
108
|
+
даже покомпактнее:
|
|
109
|
+
|
|
110
|
+
- [Karpathy - micrograd](https://github.com/karpathy/micrograd) - реализация того же самого на ~100 строк, must see
|
|
111
|
+
- [CS231n: Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/) - если нужно разложить backprop по полочкам
|
|
112
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
113
|
+
|
|
114
|
+
Если совсем в тему - есть видос Карпатова где он с нуля пишет micrograd и
|
|
115
|
+
объясняет каждую строчку: ["The spelled-out intro to neural networks and backpropagation"](https://www.youtube.com/watch?v=VMj-3S1tku0).
|
|
116
|
+
Собственно тут все то же самое, только на numpy и чуть пошире.
|
|
117
|
+
|
|
118
|
+
### Формулы, которые реализованы в Tensor
|
|
119
|
+
|
|
120
|
+
| операция | вперед | производная |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `a + b` | `a + b` | `dL/da += dL/dz`, `dL/db += dL/dz` (плюс схлопывание по broadcast-осям) |
|
|
123
|
+
| `a * b` | `a * b` | `dL/da += dL/dz * b`, `dL/db += dL/dz * a` |
|
|
124
|
+
| `a @ b` | matmul | `dL/da += dL/dz @ bᵀ`, `dL/db += aᵀ @ dL/dz` |
|
|
125
|
+
| `a ** p` | `aᵖ` | `dL/da += dL/dz * p * a^(p-1)` |
|
|
126
|
+
| ReLU | `max(0, x)` | 1 при x>0, иначе 0 |
|
|
127
|
+
| LeakyReLU | x или αx | 1 при x>0, иначе α |
|
|
128
|
+
| sigmoid | `1/(1+e⁻ˣ)` | `σ(x)*(1-σ(x))` |
|
|
129
|
+
| tanh | `tanh(x)` | `1 - tanh²(x)` |
|
|
130
|
+
| GELU | `0.5x(1+tanh(√(2/π)(x+0.044715x³)))` | см. [статью по GELU](https://arxiv.org/abs/1606.08415), формула там не самая короткая |
|
|
131
|
+
|
|
132
|
+
Про broadcasting и почему градиент иногда надо досуммировать обратно
|
|
133
|
+
(`_unbroadcast`) норм объясняют [правила broadcasting в numpy](https://numpy.org/doc/stable/user/basics.broadcasting.html).
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
### Tensor
|
|
138
|
+
|
|
139
|
+
`tinytensor.core.tensor.Tensor` - главный класс. Есть `+ - * @ **`, `.sum()`,
|
|
140
|
+
активации `relu / leaky_relu / sigmoid / tanh / gelu`, ну и `.backward()`.
|
|
141
|
+
|
|
142
|
+
### nn
|
|
143
|
+
|
|
144
|
+
- `Module` - от него наследуются все слои, `forward / parameters() / zero_grad()`, по духу как [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html);
|
|
145
|
+
- `Linear(in_features, out_features)` - обычный `y = xW + b`, веса инициализируются по [He/Kaiming init](https://arxiv.org/abs/1502.01852) (`std = sqrt(2/in_features)`);
|
|
146
|
+
- `ReLU, LeReLU, Sigmod, Tanh, GELU` - тонкие обертки над методами Tensor;
|
|
147
|
+
- `MSELoss` - `mean((pred - target)^2)`, банальщина.
|
|
148
|
+
|
|
149
|
+
### optim
|
|
150
|
+
|
|
151
|
+
- `SGD(params, lr, momentum=0.0)` - обычный градиентный спуск, можно с моментом;
|
|
152
|
+
- `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 тут не лезет в градиент, а сразу режет веса).
|
|
153
|
+
|
|
154
|
+
### data
|
|
155
|
+
|
|
156
|
+
- `Dataset / TensorDataset` - обертка над (x, y);
|
|
157
|
+
- `DataLoader` - бьет на батчи, можно с shuffle, мини-версия [`torch.utils.data.DataLoader`](https://pytorch.org/docs/stable/data.html).
|
|
158
|
+
|
|
159
|
+
## Примеры
|
|
160
|
+
|
|
161
|
+
- [`examples/01_linear_regression.py`](examples/01_linear_regression.py) - линейная регрессия, Linear + MSELoss + SGD на `y = 3x + 2 + шум`.
|
|
162
|
+
- [`examples/02_mnist_mlp.py`](examples/02_mnist_mlp.py) - MLP (Linear -> ReLU -> Linear) с AdamW и DataLoader на синтетике в формате mnist (784 фичи, 10 классов). Настоящего mnist и загрузчиков датасетов тут нет, лень было тащить.
|
|
163
|
+
|
|
164
|
+
Как выглядит запуск вживую:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
$ python3 01_linear_regression.py
|
|
168
|
+
epoch 0 | loss 30.8016
|
|
169
|
+
epoch 180 | loss 0.2365
|
|
170
|
+
выученные параметры: weight ~ 2.995, bias ~ 1.996
|
|
171
|
+
|
|
172
|
+
$ python3 02_mnist_mlp.py
|
|
173
|
+
epoch 1/5 | avg loss 2.0998
|
|
174
|
+
epoch 5/5 | avg loss 0.0481
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Тесты
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e .[dev]
|
|
181
|
+
python3 -m pytest tests/ -v
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
47 штук, гоняют арифметику тензоров и broadcasting, autograd (накопление
|
|
185
|
+
градиента, топология, повторный backward), лоссы, оптимизаторы и
|
|
186
|
+
Dataset/DataLoader.
|
|
187
|
+
|
|
188
|
+
> Важно: запускать через pytest из корня репы (или после `pip install -e .`),
|
|
189
|
+
> а не `python3 test_x.py` из папки tests - иначе tinytensor просто не найдется.
|
|
190
|
+
|
|
191
|
+
## Структура
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
tinytensor/
|
|
195
|
+
├── tinytensor/
|
|
196
|
+
│ ├── core/ # Tensor, autograd, ops
|
|
197
|
+
│ ├── nn/ # Module, Linear, активации, MSELoss
|
|
198
|
+
│ ├── optim/ # SGD, AdamW
|
|
199
|
+
│ ├── data/ # Dataset, DataLoader
|
|
200
|
+
│ ├── backends/ # заготовка под cuda, пока пусто
|
|
201
|
+
│ └── config.py # сид рандома
|
|
202
|
+
├── examples/
|
|
203
|
+
├── tests/
|
|
204
|
+
├── setup.py
|
|
205
|
+
└── requirements.txt
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Чего нет
|
|
209
|
+
|
|
210
|
+
- Только numpy-backend, cuda_gpu.py пустой файл-заглушка.
|
|
211
|
+
- Нет кросс-энтропии, сверток, рекуррентных слоев, сохранения модели (см. ToDo в исходном плане проекта).
|
|
212
|
+
- backward не кэширует граф между вызовами, каждый раз строит топологию заново, как и в micrograd - никакой лени в духе pytorch тут нет.
|
|
213
|
+
|
|
214
|
+
## Ссылки
|
|
215
|
+
|
|
216
|
+
- [Andrej Karpathy - micrograd (GitHub)](https://github.com/karpathy/micrograd) - основной референс для всего autograd
|
|
217
|
+
- [Andrej Karpathy - видео про backprop и micrograd](https://www.youtube.com/watch?v=VMj-3S1tku0)
|
|
218
|
+
- [CS231n - Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/)
|
|
219
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
220
|
+
- [NumPy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html)
|
|
221
|
+
- [He et al. - инициализация весов](https://arxiv.org/abs/1502.01852)
|
|
222
|
+
- [Kingma & Ba - Adam](https://arxiv.org/abs/1412.6980)
|
|
223
|
+
- [Loshchilov & Hutter - AdamW](https://arxiv.org/abs/1711.05101)
|
|
224
|
+
- [Hendrycks & Gimpel - GELU](https://arxiv.org/abs/1606.08415)
|
|
225
|
+
- [PyTorch - torch.nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html)
|
|
226
|
+
- [PyTorch - torch.utils.data.DataLoader](https://pytorch.org/docs/stable/data.html)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytinytensor
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )
|
|
5
|
+
Home-page: https://github.com/IbrokhimN/tinytensor
|
|
6
|
+
Author: IbrokhimN
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=1.23
|
|
11
|
+
Requires-Dist: pybind11>=2.10
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: provides-extra
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
# tinytensor
|
|
25
|
+
|
|
26
|
+
Маленький самописный autograd на numpy. Тензоры, backprop, пара слоев,
|
|
27
|
+
лоссы, оптимизаторы и даталоадер. По сути свой мини-pytorch, только без
|
|
28
|
+
плюшек и без cuda (пока).
|
|
29
|
+
|
|
30
|
+
Никакой производительности тут не ищите, тут просто видно как все
|
|
31
|
+
устроено внутри, без магии.
|
|
32
|
+
|
|
33
|
+
## Содержание
|
|
34
|
+
|
|
35
|
+
- [Установка](#установка)
|
|
36
|
+
- [Быстрый старт](#быстрый-старт)
|
|
37
|
+
- [Как работает autograd](#как-работает-autograd)
|
|
38
|
+
- [API](#api)
|
|
39
|
+
- [Примеры](#примеры)
|
|
40
|
+
- [Тесты](#тесты)
|
|
41
|
+
- [Структура](#структура)
|
|
42
|
+
- [Чего нет](#чего-нет)
|
|
43
|
+
- [Ссылки](#ссылки)
|
|
44
|
+
|
|
45
|
+
## Установка
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/IbrokhimN/tinytensor
|
|
49
|
+
cd tinytensor
|
|
50
|
+
pip install -e . # обычная установка
|
|
51
|
+
pip install -e .[dev] # плюс pytest, если хотите гонять тесты
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Или руками:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -r requirements.txt
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Зависимость одна - numpy.
|
|
61
|
+
|
|
62
|
+
## Быстрый старт
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from tinytensor.core.tensor import Tensor
|
|
66
|
+
from tinytensor.nn.linear import Linear
|
|
67
|
+
from tinytensor.nn.losses import MSELoss
|
|
68
|
+
from tinytensor.optim import SGD
|
|
69
|
+
|
|
70
|
+
model = Linear(in_features=1, out_features=1)
|
|
71
|
+
loss_fn = MSELoss()
|
|
72
|
+
optimizer = SGD(model.parameters(), lr=0.01)
|
|
73
|
+
|
|
74
|
+
x = Tensor([[1.0], [2.0], [3.0]])
|
|
75
|
+
y = Tensor([[3.0], [5.0], [7.0]]) # y = 2x + 1
|
|
76
|
+
|
|
77
|
+
for epoch in range(100):
|
|
78
|
+
optimizer.zero_grad()
|
|
79
|
+
pred = model(x)
|
|
80
|
+
loss = loss_fn(pred, y)
|
|
81
|
+
loss.backward()
|
|
82
|
+
optimizer.step()
|
|
83
|
+
|
|
84
|
+
print(model.weight.data, model.bias.data) # ~2.0, ~1.0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Рабочие примеры целиком лежат в [`examples/`](examples/).
|
|
88
|
+
|
|
89
|
+
## Как работает autograd
|
|
90
|
+
|
|
91
|
+
У каждого `Tensor` есть:
|
|
92
|
+
|
|
93
|
+
- `data` - сами значения (numpy-массив, всегда float32);
|
|
94
|
+
- `grad` - градиент, пока не посчитан - None;
|
|
95
|
+
- `_prev` - от каких тензоров он произошел (родители в графе);
|
|
96
|
+
- `_backward` - функция которая знает как раскидать градиент на родителей.
|
|
97
|
+
|
|
98
|
+
Когда считаете `z = f(x, y)`, по цепному правилу:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
dL/dx = dL/dz * dz/dx
|
|
102
|
+
dL/dy = dL/dz * dz/dy
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`backward()` строит топологический порядок графа (`tinytensor/core/autograd.py`),
|
|
106
|
+
ставит корню градиент = 1 и идет в обратном порядке, вызывая `_backward()`
|
|
107
|
+
у каждого узла. Ровно так же устроен micrograd Карпатова, только у него
|
|
108
|
+
даже покомпактнее:
|
|
109
|
+
|
|
110
|
+
- [Karpathy - micrograd](https://github.com/karpathy/micrograd) - реализация того же самого на ~100 строк, must see
|
|
111
|
+
- [CS231n: Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/) - если нужно разложить backprop по полочкам
|
|
112
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
113
|
+
|
|
114
|
+
Если совсем в тему - есть видос Карпатова где он с нуля пишет micrograd и
|
|
115
|
+
объясняет каждую строчку: ["The spelled-out intro to neural networks and backpropagation"](https://www.youtube.com/watch?v=VMj-3S1tku0).
|
|
116
|
+
Собственно тут все то же самое, только на numpy и чуть пошире.
|
|
117
|
+
|
|
118
|
+
### Формулы, которые реализованы в Tensor
|
|
119
|
+
|
|
120
|
+
| операция | вперед | производная |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `a + b` | `a + b` | `dL/da += dL/dz`, `dL/db += dL/dz` (плюс схлопывание по broadcast-осям) |
|
|
123
|
+
| `a * b` | `a * b` | `dL/da += dL/dz * b`, `dL/db += dL/dz * a` |
|
|
124
|
+
| `a @ b` | matmul | `dL/da += dL/dz @ bᵀ`, `dL/db += aᵀ @ dL/dz` |
|
|
125
|
+
| `a ** p` | `aᵖ` | `dL/da += dL/dz * p * a^(p-1)` |
|
|
126
|
+
| ReLU | `max(0, x)` | 1 при x>0, иначе 0 |
|
|
127
|
+
| LeakyReLU | x или αx | 1 при x>0, иначе α |
|
|
128
|
+
| sigmoid | `1/(1+e⁻ˣ)` | `σ(x)*(1-σ(x))` |
|
|
129
|
+
| tanh | `tanh(x)` | `1 - tanh²(x)` |
|
|
130
|
+
| GELU | `0.5x(1+tanh(√(2/π)(x+0.044715x³)))` | см. [статью по GELU](https://arxiv.org/abs/1606.08415), формула там не самая короткая |
|
|
131
|
+
|
|
132
|
+
Про broadcasting и почему градиент иногда надо досуммировать обратно
|
|
133
|
+
(`_unbroadcast`) норм объясняют [правила broadcasting в numpy](https://numpy.org/doc/stable/user/basics.broadcasting.html).
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
### Tensor
|
|
138
|
+
|
|
139
|
+
`tinytensor.core.tensor.Tensor` - главный класс. Есть `+ - * @ **`, `.sum()`,
|
|
140
|
+
активации `relu / leaky_relu / sigmoid / tanh / gelu`, ну и `.backward()`.
|
|
141
|
+
|
|
142
|
+
### nn
|
|
143
|
+
|
|
144
|
+
- `Module` - от него наследуются все слои, `forward / parameters() / zero_grad()`, по духу как [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html);
|
|
145
|
+
- `Linear(in_features, out_features)` - обычный `y = xW + b`, веса инициализируются по [He/Kaiming init](https://arxiv.org/abs/1502.01852) (`std = sqrt(2/in_features)`);
|
|
146
|
+
- `ReLU, LeReLU, Sigmod, Tanh, GELU` - тонкие обертки над методами Tensor;
|
|
147
|
+
- `MSELoss` - `mean((pred - target)^2)`, банальщина.
|
|
148
|
+
|
|
149
|
+
### optim
|
|
150
|
+
|
|
151
|
+
- `SGD(params, lr, momentum=0.0)` - обычный градиентный спуск, можно с моментом;
|
|
152
|
+
- `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 тут не лезет в градиент, а сразу режет веса).
|
|
153
|
+
|
|
154
|
+
### data
|
|
155
|
+
|
|
156
|
+
- `Dataset / TensorDataset` - обертка над (x, y);
|
|
157
|
+
- `DataLoader` - бьет на батчи, можно с shuffle, мини-версия [`torch.utils.data.DataLoader`](https://pytorch.org/docs/stable/data.html).
|
|
158
|
+
|
|
159
|
+
## Примеры
|
|
160
|
+
|
|
161
|
+
- [`examples/01_linear_regression.py`](examples/01_linear_regression.py) - линейная регрессия, Linear + MSELoss + SGD на `y = 3x + 2 + шум`.
|
|
162
|
+
- [`examples/02_mnist_mlp.py`](examples/02_mnist_mlp.py) - MLP (Linear -> ReLU -> Linear) с AdamW и DataLoader на синтетике в формате mnist (784 фичи, 10 классов). Настоящего mnist и загрузчиков датасетов тут нет, лень было тащить.
|
|
163
|
+
|
|
164
|
+
Как выглядит запуск вживую:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
$ python3 01_linear_regression.py
|
|
168
|
+
epoch 0 | loss 30.8016
|
|
169
|
+
epoch 180 | loss 0.2365
|
|
170
|
+
выученные параметры: weight ~ 2.995, bias ~ 1.996
|
|
171
|
+
|
|
172
|
+
$ python3 02_mnist_mlp.py
|
|
173
|
+
epoch 1/5 | avg loss 2.0998
|
|
174
|
+
epoch 5/5 | avg loss 0.0481
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Тесты
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e .[dev]
|
|
181
|
+
python3 -m pytest tests/ -v
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
47 штук, гоняют арифметику тензоров и broadcasting, autograd (накопление
|
|
185
|
+
градиента, топология, повторный backward), лоссы, оптимизаторы и
|
|
186
|
+
Dataset/DataLoader.
|
|
187
|
+
|
|
188
|
+
> Важно: запускать через pytest из корня репы (или после `pip install -e .`),
|
|
189
|
+
> а не `python3 test_x.py` из папки tests - иначе tinytensor просто не найдется.
|
|
190
|
+
|
|
191
|
+
## Структура
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
tinytensor/
|
|
195
|
+
├── tinytensor/
|
|
196
|
+
│ ├── core/ # Tensor, autograd, ops
|
|
197
|
+
│ ├── nn/ # Module, Linear, активации, MSELoss
|
|
198
|
+
│ ├── optim/ # SGD, AdamW
|
|
199
|
+
│ ├── data/ # Dataset, DataLoader
|
|
200
|
+
│ ├── backends/ # заготовка под cuda, пока пусто
|
|
201
|
+
│ └── config.py # сид рандома
|
|
202
|
+
├── examples/
|
|
203
|
+
├── tests/
|
|
204
|
+
├── setup.py
|
|
205
|
+
└── requirements.txt
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Чего нет
|
|
209
|
+
|
|
210
|
+
- Только numpy-backend, cuda_gpu.py пустой файл-заглушка.
|
|
211
|
+
- Нет кросс-энтропии, сверток, рекуррентных слоев, сохранения модели (см. ToDo в исходном плане проекта).
|
|
212
|
+
- backward не кэширует граф между вызовами, каждый раз строит топологию заново, как и в micrograd - никакой лени в духе pytorch тут нет.
|
|
213
|
+
|
|
214
|
+
## Ссылки
|
|
215
|
+
|
|
216
|
+
- [Andrej Karpathy - micrograd (GitHub)](https://github.com/karpathy/micrograd) - основной референс для всего autograd
|
|
217
|
+
- [Andrej Karpathy - видео про backprop и micrograd](https://www.youtube.com/watch?v=VMj-3S1tku0)
|
|
218
|
+
- [CS231n - Backpropagation, Intuitions](https://cs231n.github.io/optimization-2/)
|
|
219
|
+
- [colah - Calculus on Computational Graphs](https://colah.github.io/posts/2015-08-Backprop/)
|
|
220
|
+
- [NumPy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html)
|
|
221
|
+
- [He et al. - инициализация весов](https://arxiv.org/abs/1502.01852)
|
|
222
|
+
- [Kingma & Ba - Adam](https://arxiv.org/abs/1412.6980)
|
|
223
|
+
- [Loshchilov & Hutter - AdamW](https://arxiv.org/abs/1711.05101)
|
|
224
|
+
- [Hendrycks & Gimpel - GELU](https://arxiv.org/abs/1606.08415)
|
|
225
|
+
- [PyTorch - torch.nn.Module](https://pytorch.org/docs/stable/generated/torch.nn.Module.html)
|
|
226
|
+
- [PyTorch - torch.utils.data.DataLoader](https://pytorch.org/docs/stable/data.html)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
setup.py
|
|
@@ -14,10 +15,12 @@ tests/test_optim.py
|
|
|
14
15
|
tests/test_tensor_math.py
|
|
15
16
|
tinytensor/__init__.py
|
|
16
17
|
tinytensor/config.py
|
|
18
|
+
tinytensor/setup.py
|
|
17
19
|
tinytensor/backends/__init__.py
|
|
18
20
|
tinytensor/backends/cpu_numpy.py
|
|
19
21
|
tinytensor/backends/cuda_binding.cpp
|
|
20
22
|
tinytensor/backends/cuda_gpu.cu
|
|
23
|
+
tinytensor/backends/cuda_gpu.py
|
|
21
24
|
tinytensor/core/__init__.py
|
|
22
25
|
tinytensor/core/autograd.py
|
|
23
26
|
tinytensor/core/ops.py
|
|
@@ -32,6 +35,27 @@ tinytensor/nn/losses.py
|
|
|
32
35
|
tinytensor/nn/modules.py
|
|
33
36
|
tinytensor/optim/__init__.py
|
|
34
37
|
tinytensor/optim/optimizer.py
|
|
38
|
+
tinytensor/tinytensor/__init__.py
|
|
39
|
+
tinytensor/tinytensor/config.py
|
|
40
|
+
tinytensor/tinytensor/backends/__init__.py
|
|
41
|
+
tinytensor/tinytensor/backends/cpu_numpy.py
|
|
42
|
+
tinytensor/tinytensor/core/__init__.py
|
|
43
|
+
tinytensor/tinytensor/core/autograd.py
|
|
44
|
+
tinytensor/tinytensor/core/ops.py
|
|
45
|
+
tinytensor/tinytensor/core/tensor.py
|
|
46
|
+
tinytensor/tinytensor/data/__init__.py
|
|
47
|
+
tinytensor/tinytensor/data/dataloader.py
|
|
48
|
+
tinytensor/tinytensor/data/dataset.py
|
|
49
|
+
tinytensor/tinytensor/nn/__init__.py
|
|
50
|
+
tinytensor/tinytensor/nn/activations.py
|
|
51
|
+
tinytensor/tinytensor/nn/linear.py
|
|
52
|
+
tinytensor/tinytensor/nn/losses.py
|
|
53
|
+
tinytensor/tinytensor/nn/modules.py
|
|
54
|
+
tinytensor/tinytensor/optim/__init__.py
|
|
55
|
+
tinytensor/tinytensor/optim/optimizer.py
|
|
56
|
+
tinytensor/tinytensor/utils/__init__.py
|
|
57
|
+
tinytensor/tinytensor/utils/bar.py
|
|
58
|
+
tinytensor/tinytensor/utils/early_stopping.py
|
|
35
59
|
tinytensor/utils/__init__.py
|
|
36
60
|
tinytensor/utils/bar.py
|
|
37
61
|
tinytensor/utils/early_stopping.py
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import pybind11
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from setuptools import setup, find_packages, Extension
|
|
6
|
+
from setuptools.command.build_ext import build_ext
|
|
7
|
+
|
|
8
|
+
long_description = Path(__file__).parent / "README.md"
|
|
9
|
+
long_description = long_description.read_text(encoding="utf-8") if long_description.exists() else ""
|
|
10
|
+
|
|
11
|
+
def find_cuda_lib_dirs():
|
|
12
|
+
candidates = [
|
|
13
|
+
os.environ.get("CUDA_HOME"),
|
|
14
|
+
os.environ.get("CUDA_PATH"),
|
|
15
|
+
"/usr/local/cuda",
|
|
16
|
+
]
|
|
17
|
+
conda_prefix = os.environ.get("CONDA_PREFIX")
|
|
18
|
+
if conda_prefix:
|
|
19
|
+
candidates.append(conda_prefix)
|
|
20
|
+
|
|
21
|
+
found = []
|
|
22
|
+
for base in candidates:
|
|
23
|
+
if not base:
|
|
24
|
+
continue
|
|
25
|
+
lib_dir = os.path.join(base, "lib64")
|
|
26
|
+
if not os.path.isdir(lib_dir):
|
|
27
|
+
lib_dir = os.path.join(base, "lib")
|
|
28
|
+
has_cudart = any(
|
|
29
|
+
f.startswith("libcudart.so") for f in os.listdir(lib_dir)
|
|
30
|
+
) if os.path.isdir(lib_dir) else False
|
|
31
|
+
has_cublas = any(
|
|
32
|
+
f.startswith("libcublas.so") for f in os.listdir(lib_dir)
|
|
33
|
+
) if os.path.isdir(lib_dir) else False
|
|
34
|
+
if has_cudart and has_cublas:
|
|
35
|
+
found.append(lib_dir)
|
|
36
|
+
return found
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_cuda():
|
|
40
|
+
try:
|
|
41
|
+
subprocess.check_output(["nvcc", "--version"])
|
|
42
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
43
|
+
return False, []
|
|
44
|
+
|
|
45
|
+
lib_dirs = find_cuda_lib_dirs()
|
|
46
|
+
if not lib_dirs:
|
|
47
|
+
print("nvcc есть но libcudart/libcublas не найдены рядом. Сборка пойдет на CPU.")
|
|
48
|
+
return False, []
|
|
49
|
+
return True, lib_dirs
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CUDABuildExt(build_ext):
|
|
53
|
+
# если cuda-сборка все равно упадет (например линковка) - не роняем весь pip install,
|
|
54
|
+
# а просто откатываемся на чистый cpu-пакет
|
|
55
|
+
def build_extensions(self):
|
|
56
|
+
self.compiler.src_extensions.append('.cu')
|
|
57
|
+
original_compile = self.compiler._compile
|
|
58
|
+
|
|
59
|
+
def custom_compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
|
|
60
|
+
# extra_postargs может приходить как список или словарь — обрабатываем оба случая
|
|
61
|
+
if isinstance(extra_postargs, dict):
|
|
62
|
+
gcc_postargs = extra_postargs.get('gcc', [])
|
|
63
|
+
nvcc_postargs = extra_postargs.get('nvcc', [])
|
|
64
|
+
else:
|
|
65
|
+
gcc_postargs = extra_postargs or []
|
|
66
|
+
nvcc_postargs = []
|
|
67
|
+
|
|
68
|
+
if os.path.splitext(src)[1] == '.cu':
|
|
69
|
+
# nvcc собирает .cu файл
|
|
70
|
+
inc_flags = [f"-I{inc}" for inc in self.compiler.include_dirs]
|
|
71
|
+
cmd = ['nvcc', '-c', src, '-o', obj] + inc_flags + nvcc_postargs
|
|
72
|
+
self.spawn(cmd)
|
|
73
|
+
else:
|
|
74
|
+
# g++ собирает .cpp файл (с pybind11)
|
|
75
|
+
original_compile(obj, src, ext, cc_args, gcc_postargs, pp_opts)
|
|
76
|
+
|
|
77
|
+
self.compiler._compile = custom_compile
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
super().build_extensions()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"сборка cuda-расширения не удалась ({e}), продолжаем без него (cpu-only).")
|
|
83
|
+
self.extensions = []
|
|
84
|
+
|
|
85
|
+
ext_modules = []
|
|
86
|
+
|
|
87
|
+
cuda_ok, cuda_lib_dirs = check_cuda()
|
|
88
|
+
|
|
89
|
+
if cuda_ok:
|
|
90
|
+
cuda_module = Extension(
|
|
91
|
+
'tinytensor.cuda_ops',
|
|
92
|
+
sources=[
|
|
93
|
+
'tinytensor/backends/cuda_gpu.cu', # nvcc
|
|
94
|
+
'tinytensor/backends/cuda_binding.cpp' # g++
|
|
95
|
+
],
|
|
96
|
+
include_dirs=[
|
|
97
|
+
pybind11.get_include(),
|
|
98
|
+
'/usr/local/cuda/include',
|
|
99
|
+
os.path.expanduser('~/.local/include')
|
|
100
|
+
],
|
|
101
|
+
library_dirs=cuda_lib_dirs,
|
|
102
|
+
libraries=['cudart', 'cublas'],
|
|
103
|
+
extra_compile_args={
|
|
104
|
+
'gcc': ['-O3', '-fPIC', '-std=c++17'],
|
|
105
|
+
'nvcc': ['-O3', '-Xcompiler', '-fPIC']
|
|
106
|
+
},
|
|
107
|
+
optional=True,
|
|
108
|
+
)
|
|
109
|
+
ext_modules.append(cuda_module)
|
|
110
|
+
print("cublas")
|
|
111
|
+
else:
|
|
112
|
+
print("cpu")
|
|
113
|
+
|
|
114
|
+
setup(
|
|
115
|
+
name="pytinytensor",
|
|
116
|
+
version="0.1.2",
|
|
117
|
+
description="мини ИИ фреймворк от IbrokimN ( github/IbrokhimN )",
|
|
118
|
+
long_description=long_description,
|
|
119
|
+
long_description_content_type="text/markdown",
|
|
120
|
+
author="IbrokhimN",
|
|
121
|
+
url="https://github.com/IbrokhimN/tinytensor",
|
|
122
|
+
packages=find_packages(include=["tinytensor", "tinytensor.*"]),
|
|
123
|
+
ext_modules=ext_modules,
|
|
124
|
+
cmdclass={'build_ext': CUDABuildExt},
|
|
125
|
+
install_requires=[
|
|
126
|
+
"numpy>=1.23",
|
|
127
|
+
"pybind11>=2.10",
|
|
128
|
+
],
|
|
129
|
+
extras_require={
|
|
130
|
+
"dev": ["pytest>=7.0"],
|
|
131
|
+
},
|
|
132
|
+
python_requires=">=3.9",
|
|
133
|
+
)
|
|
File without changes
|