ML-алгоритмы с нуля на NumPy: LinearRegression, LogisticRegression, MNISTNeuralNetwork
Project description
numl — ML-алгоритмы с нуля на NumPy
Учебная реализация трёх фундаментальных алгоритмов машинного обучения без использования sklearn, PyTorch или TensorFlow.
Написана с целью закрепить теорию на практике.
LinearRegression · LogisticRegression · MNISTNeuralNetwork
Структура проекта
numl/
├── numl/
│ ├── base.py # BaseModel: цикл SGD, ранняя остановка
│ ├── linear.py # LinearRegression (SGD / GD / normal equation)
│ ├── logistic.py # LogisticRegression (SGD / GD)
│ ├── neural.py # MNISTNeuralNetwork 784→128→64→10 (backprop)
│ └── __init__.py
├── tests/
│ ├── test_linear.py # 22 теста
│ ├── test_logistic.py # 21 тест
│ └── test_neural.py # 34 теста
├── examples/
│ ├── california_housing.ipynb # LinearRegression на реальных данных
│ ├── breast_cancer.ipynb # LogisticRegression + анализ порога
│ └── mnist_neural.ipynb # NeuralNetwork на MNIST
└── requirements.txt
Математика
Ниже — формулы, которые реализованы в коде. Каждый раздел содержит ссылку на соответствующий файл.
Линейная регрессия linear.py
Модель: $\hat{y} = \mathbf{w}^\top \mathbf{x} + b$
Функция потерь (MSE):
$$L = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$
Градиент и шаг обновления:
$$\frac{\partial L}{\partial \mathbf{w}} = \frac{1}{m} \mathbf{X}^\top (\hat{\mathbf{y}} - \mathbf{y}), \qquad \mathbf{w} \leftarrow \mathbf{w} - \alpha \cdot \frac{\partial L}{\partial \mathbf{w}}$$
Аналитическое решение (нормальное уравнение):
$$\boldsymbol{\theta} = (\mathbf{X}^\top \mathbf{X})^{-1} \mathbf{X}^\top \mathbf{y}$$
Реализовано через np.linalg.lstsq для численной устойчивости при вырожденных матрицах.
Логистическая регрессия logistic.py
Функция активации — сигмоид:
$$\hat{y} = \sigma(z) = \frac{1}{1 + e^{-z}}, \quad z = \mathbf{w}^\top \mathbf{x} + b$$
Функция потерь (Binary Cross-Entropy, выводится из MLE):
$$L = -\frac{1}{n} \sum_{i=1}^{n} \left[ y_i \log \hat{y}_i + (1 - y_i) \log (1 - \hat{y}_i) \right]$$
После упрощения через цепное правило градиент принимает ту же форму, что и в линейной регрессии — это следствие выпуклости BCE:
$$\frac{\partial L}{\partial \mathbf{w}} = \frac{1}{m} \mathbf{X}^\top (\hat{\mathbf{y}} - \mathbf{y})$$
Благодаря этому LogisticRegression и LinearRegression используют один и тот же цикл обучения в BaseModel, переопределяя только _activation() и _compute_loss().
Почему
norm_eqзапрещён: BCE невыпукла относительно $\mathbf{w}$ в замкнутой форме — аналитического решения не существует, только итеративная оптимизация.
Нейронная сеть neural.py
Архитектура:
Вход (784) → Dense(128, ReLU) → Dense(64, ReLU) → Dense(10, Softmax)
Прямой проход для слоя $l$:
$$\mathbf{z}^{[l]} = \mathbf{W}^{[l]} \mathbf{a}^{[l-1]} + \mathbf{b}^{[l]}, \qquad \mathbf{a}^{[l]} = g^{[l]}(\mathbf{z}^{[l]})$$
Функция потерь (Categorical Cross-Entropy):
$$L = -\frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K} y_{ik} \log \hat{y}_{ik}$$
Обратный проход (цепное правило):
$$\boldsymbol{\delta}^{[3]} = \hat{\mathbf{Y}} - \mathbf{Y} \quad \text{(softmax + CCE упрощаются)}$$
$$\boldsymbol{\delta}^{[l]} = \left(\mathbf{W}^{[l+1]\top} \boldsymbol{\delta}^{[l+1]}\right) \odot \text{ReLU}'(\mathbf{z}^{[l]})$$
$$\frac{\partial L}{\partial \mathbf{W}^{[l]}} = \frac{1}{m} \boldsymbol{\delta}^{[l]} \mathbf{a}^{[l-1]\top}, \qquad \frac{\partial L}{\partial \mathbf{b}^{[l]}} = \frac{1}{m} \sum \boldsymbol{\delta}^{[l]}$$
Инициализация весов по He (для ReLU-сетей):
$$\mathbf{W}^{[l]} \sim \mathcal{N}!\left(0,\ \frac{2}{n^{[l-1]}}\right)$$
Быстрый старт
git clone https://github.com/KarIes-ss/numl.git
cd numl
pip install -r requirements.txt
from numl import LinearRegression, LogisticRegression, MNISTNeuralNetwork
LinearRegression
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from numl import LinearRegression
X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# Три режима обучения
model = LinearRegression(method="norm_eq").fit(X_train, y_train)
print(f"R² = {model.score(X_test, y_test):.4f}")
model = LinearRegression(method="sgd", lr=0.1, epochs=300).fit(X_train, y_train)
model.plot_loss()
LogisticRegression
from sklearn.datasets import load_breast_cancer
from numl import LogisticRegression
X, y = load_breast_cancer(return_X_y=True)
# ... предобработка ...
model = LogisticRegression(lr=0.1, epochs=500, threshold=0.5)
model.fit(X_train, y_train)
proba = model.predict(X_test) # вероятности ∈ (0, 1)
labels = model.predict_class(X_test) # метки {0, 1}
print(f"Accuracy = {model.score(X_test, y_test):.4f}")
MNISTNeuralNetwork
from numl import MNISTNeuralNetwork
# Ожидается X нормализованный в [0, 1], y ∈ {0, …, 9}
model = MNISTNeuralNetwork(hidden1=128, hidden2=64, lr=0.01, epochs=20)
model.fit(X_train, y_train)
model.plot_history()
preds = model.predict(X_test)
print(f"Accuracy = {model.score(X_test, y_test):.4f}")
Запуск тестов
# Установка зависимостей (только numpy и pytest)
pip install -r requirements.txt
# Все тесты
python -m pytest tests/ -v
# Один файл
python -m pytest tests/test_linear.py -v
# Конкретный класс или тест
python -m pytest tests/test_logistic.py::TestLogisticNormEqForbidden -v
python -m pytest tests/test_neural.py::TestActivations::test_softmax_numerical_stability -v
Покрытие: 77 тестов, ~1.6 сек.
| Файл | Тестов | Что проверяется |
|---|---|---|
test_linear.py |
22 | Три метода обучения, R², ранняя остановка, нормальное уравнение |
test_logistic.py |
21 | BCE, сигмоид, порог классификации, запрет norm_eq |
test_neural.py |
34 | Формы тензоров, softmax, backprop, сходимость, predict_proba |
Тесты написаны на pytest с использованием фикстур и параметризации (@pytest.mark.parametrize). Покрывается как поведение публичного API, так и внутренняя математика (_activation, _compute_loss, _relu_grad).
Архитектурные решения
Шаблонный метод (Template Method) в BaseModel
Весь цикл обучения (SGD, ранняя остановка, история потерь) реализован единожды в BaseModel.fit(). Подклассы переопределяют только два метода:
def _activation(self, z): ... # sigmoid / identity
def _compute_loss(self, y, y_pred): ... # BCE / MSE
Это устраняет дублирование и позволяет добавить новую модель (например, SVMRegression) без изменения цикла обучения.
Численная стабильность
- Сигмоид:
np.clip(z, -500, 500)предотвращаетexp(-z)overflow - Softmax: вычитание
max(z)по столбцам передexp(стандартный трюк log-sum-exp) - BCE и CCE:
eps = 1e-9защищает отlog(0)на граничных предсказаниях
Нормальное уравнение через lstsq
Вместо явного обращения $(\mathbf{X}^\top\mathbf{X})^{-1}$ используется np.linalg.lstsq, которая применяет SVD-разложение — устойчива при мультиколлинеарности и вырожденных матрицах.
Результаты на реальных данных
| Датасет | Модель | Метрика | Значение |
|---|---|---|---|
| California Housing | LinearRegression (norm_eq) |
R² | ~0.606 |
| Breast Cancer Wisconsin | LogisticRegression (sgd) |
Accuracy | ~0.982 |
| MNIST | MNISTNeuralNetwork (128→64) |
Accuracy | ~0.970 |
Зависимости
numpy>=1.24
matplotlib>=3.7
pytest>=7.0 # только для тестов
scikit-learn>=1.3 # только для примеров (загрузка датасетов)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file numl-0.1.0.tar.gz.
File metadata
- Download URL: numl-0.1.0.tar.gz
- Upload date:
- Size: 25.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82b33e122e9e1163832908b4d58ec404add2f31bf05bf58950dcd5ebfedf376e
|
|
| MD5 |
c90261451c907e3e2d00fca1dcb8b17d
|
|
| BLAKE2b-256 |
b602beb9ecc801d2d96e51a6dd4c9112909d3e0dab37af16965fe7cd44ee5a67
|
File details
Details for the file numl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: numl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bb983f79d22d12072e440c4998fc7960b85b1944fa669247b22dd56299ed8bc
|
|
| MD5 |
a977e021c262b5441e3de6cc10109cd6
|
|
| BLAKE2b-256 |
2c91b11bfc0fbfbf8eb9a5d483b93a14fce2ebd0481bdf1be5c5b492e3280ff3
|