Аннотацией
В этой статье показан простой способ создания собственного класса линейной регрессии с использованием стохастического градиентного спуска. Будет представлен легкий и понятный код с реализацией основных методов: fit, predict и score. Статья будет полезна тем, кто хочет вкратце разобраться, как работает класс LinearRegression из библиотеки sklearn. Также материал подходит для участников курса программирования "Школа 21".
Немного теории
Для разработки собственного класса линейной регрессии, использующего стохастический градиентный спуск (SGD), нужно реализовать итерационный алгоритм оптимизации, который обновляет веса модели на каждом шаге, используя градиент функции ошибки, вычисленный на одном случайном примере из обучающего набора.
Основные шаги алгоритма:
Инициализация весов и смещения (обычно нулями или малыми случайными значениями).
-
Для количества эпох или до сходимости:
Перемешать обучающий набор для случайного порядка доступа.
-
Для каждого обучающего примера:
Сделать предсказание на основе текущих весов.
Вычислить ошибку и градиент функции потерь (например, среднеквадратичной ошибки).
-
Обновить веса и смещение по формуле:
w:=w−α⋅∇Li(w),
где:
- ? — вектор весов,
- ? — скорость обучения (learning rate),
-∇??(?) — градиент функции ошибки на одном примере с индексом ?.Градиент равен вектору частных производных функции потерь (например, среднеквадратичной ошибки) по параметрам модели (весам и смещению) для этого примера. То есть
∇L(w)=−(yi−wxi)xi
Обновление параметра:
w:=w−α(−(yi−wxi)xi)=w+α(yi−wxi)xi
По окончании обучения использовать финальные веса для предсказания.
1 этап, инициализация значений
В конструкторе init нужно задать параметры модели: скорость обучения (learning rate), количество итераций (эпох), а также инициализировать веса модели.
Какие параметры модели вообще есть, почему мы задаем именно такие?
В модели линейной регрессии с использованием стохастического градиентного спуска есть несколько важных параметров, которые влияют на процесс обучения и качество модели:
Параметр |
Описание |
---|---|
Веса (weights) и смещение (bias) |
Веса — численные коэффициенты для каждого признака, умножаются на значения признаков для предсказания. Смещение — свободный параметр, сдвигает линию регрессии вверх или вниз. Оба параметра оптимизируются в процессе обучения. |
Скорость обучения (learning rate) |
Регулирует размер шага обновления весов на каждом шаге обучения. Большое значение приводит к нестабильности, маленькое — к медленному или неэффективному обучению. |
Количество эпох (epochs) |
Количество проходов по всему набору данных для обновления весов. Большее число может повысить точность, но рискует переобучением и увеличивает время обучения. |
Инициализация весов |
Начальное задание весов чаще всего нулями или малыми случайными числами. Важно избегать слишком больших значений для стабильности градиентов. |
Функция потерь (loss function) |
Мера качества модели. В классической линейной регрессии часто используется среднеквадратичная ошибка (MSE), показывающая средний квадрат ошибки предсказаний. |
Используем три основных параметра: скорость, количество эпох и веса. Регулировка этих параметров важна для успешного обучения модели и хорошей обобщающей способности.
Обычно параметры модели линейной регрессии с SGD принимают следующие стандартные значения:
Скорость обучения (learning rate, lr): от 0.1 до 0.0001. Значение зависит от масштаба данных и задачи. При больших значениях градиентный спуск может расходиться.
Количество эпох (epochs): от нескольких сотен до тысяч, например, 500, 1000, 5000. Чем больше эпох, тем дольше обучение, но лучше сходимость.
Инициализация весов. Обычно веса инициализируют нулями или малыми случайными числами близкими к нулю (например, нормальным распределением с малой дисперсией).
def __init__(self):
self.lr_speed = 0.1
self.lr_epochs = 500
self.weights = None
Метод fit
Метод fit принимает обучающую выборку с признаками (Х) и целевыми значениями (у), после чего находит оптимальные значения весов и смещения, минимизируя ошибку предсказания модели. Алгоритм действий:
Передаем матрицу признаков X и вектор целей y.
Инициализируем веса нулями (или мелкими случайными числами).
Для каждого эпоха (итерации) случайно проходимся по всем объектам (или берем случайный объект) и обновляем веса согласно формуле стохастического градиентного спуска, которую уже рассматривали выше: w:=w−α⋅∇Li(w)
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features + 1)
for _ in range(self.lr_epochs):
# Создаем случайную перестановку индексов образцов для стохастического градиентного спуска
indices = np.random.permutation(n_samples)
# Проходим по каждому объекту в случайном порядке
for i in indices:
xi = X[i] # Вектор признаков текущего объекта
yi = y[i] # Истинное значение целевой переменной
# Предсказание модели: y_pred = w^T * x_i + b
y_pred = np.dot(self.weights[1:], xi) + self.weights[0]
# Вычисляем ошибку предсказания (yi−wxi)
error = yi - y_pred
# Формулы обновления параметров по шагу стохастического градиентного спуска (SGD):
# w := w + α * (yi−wxi) * xi
# Обновляем веса признаков
self.weights[1:] += self.lr_speed * error * xi
# Обновляем смещение (bias)
self.weights[0] += self.lr_speed * error
-
*В выражении n_samples,n_features=X.shape
n_samples— это количество образцов (строк) в матрице признаков X То есть число объектов, на которых обучается модель.
n_features — количество признаков (столбцов) в X. То есть число переменных, которыми описывается каждый объект.
Например, если X — это таблица с 100 строками и 5 столбцами, означает, что у вас 100 объектов и 5 признаков для каждого.
Метод predict
Забираем способ вычисления предсказания из прошлого метода, не забыв передать Х
def predict(self, X):
return np.dot(X, self.weights[1:]) + self.weights[0]
Метод score
Данный метод и способ его реализации хорошо описан в документации sklearn.
Коэффициент детерминации, ?2, определяется как где ? — сумма квадратов остатков ошибки ((y_true - y_pred)** 2).sum(), а ? — общая сумма квадратов отклонений ((y_true - y_true.mean()) ** 2).sum().
def r_squared (self, y, y_pred):
u = ((y - y_pred) ** 2).sum()
v = ((y - y.mean()) ** 2).sum()
return 1 - (u / v)
Первый запуск и сравнение с оригиналом

Программа, в том виде в котором есть сейчас явно не справляется и работает достаточно долго (примерно 3 минут на датасет)
Рефакторинг
Можно вернуться в инициализацию и методом подбора задать количество эпох, в моем случае - 650, больше приводили к переобучению и тормозили скорость выполнения еще примерно минуты на 2.
Из простейшего, необходимо преобразовать все массивы в numpy-массивы. Так как numpy использует оптимизированные операции для матриц и векторов, что ускоряет вычисления и позволяет избежать ошибок при математических операциях.
Но модель продолжает обучается медленно. Почему?
Вместо того чтобы итерироваться по всем отдельным образцам и обновлять веса по одному, можно использовать векторизованный подход для вычисления ошибок и градиентов по всему набору данных.
Также, для воспроизводимости можно добавить np.random.seed()
class linear_regression():
def __init__(self):
self.lr_speed = 0.00001 #Уменьшаем скорость
self.lr_epochs = 500
self.weights = None
def fit(self, X, y):
X = np.array(X) # Преобразуем X в numpy-массивы
y = np.array(y) # Преобразуем y в numpy-массивы
n_samples, n_features = X.shape
self.weights = np.zeros(n_features + 1)
np.random.seed(21) # фиксируем генератор случайных чисел
for _ in range(self.lr_epochs):
indices = np.random.permutation(n_samples)
xi = X[indices] # переставляем X в соответствии с индексами
yi = y[indices] # переставляем y
y_pred = np.dot(xi, self.weights[1:]) + self.weights[0]
error = yi - y_pred
# обновление весов с учетом ошибок и признаков
self.weights[1:] += self.lr_speed * np.dot(error, xi)
self.weights[0] += self.lr_speed * error.sum()
def predict(self, X):
X = np.array(X)
return np.dot(X, self.weights[1:]) + self.weights[0]
def r_squared (self, y, y_pred):
y = np.array(y)
y_pred = np.array(y_pred)
u = ((y - y_pred) ** 2).sum()
v = ((y - y.mean()) ** 2).sum()
return 1 - (u / v)
Уменьшаем скорость обучения, чтобы обновления весов происходили плавно и не вызывали больших перепадов значений, что предотвращает числовое переполнение и нестабильность. Кроме того, можно уменьшить количество эпох для ускорения вычислений, при этом программа отрабатывает примерно за 10 секунд и даёт результаты, идентичные оригинальным.

Не знаю поможет ли это кому-то, но надеюсь вам было также интересно впервые разобраться в этой теме, как и мне.