Skip to content

Coding Linear & Logistic Regression

Посмотрим на некий обзор главных моментов которые дадут нам возможность реализовать линейные модели в python и numpy. Посмотрим как отличается линейная регрессия от логистической, и как можно добавлять регуляризацию для этих моделей, чтобы можно было контролировать обобщающию способность модели

Run in Google Colab

Linear Regression

В этом разделе посмотрим на реализацию линейных моделей в python и numpy. Есть различные подходя для решения задачи линейной регрессии, мы посмотрим на реализацию с помощью матричных операции и с градиентном спуском. Так же посмотрим как добавлять гегуляризацию для этой модели

Задача оптимизации гиперпараметров

Существует несколько вариантов для выбора гипераметров линейной регрессии

  • Через прямые матричные операции
  • Через градиентны спуск

Я думаю важно знать о обоих подходов и как они реализуются

Реализация Матричных Операции

Линейная регрессия выражается следующей зависимостью:

\[y=X\theta+\epsilon\]
  • \(X\) — матрица объекты-признаки
  • \(y\) — вектор целевых значений

Cоответствующих:

  • \(X\), \(\theta\) — параметр линейной регрессии, \(\epsilon\) — некоторый шум

Наша задача, минимизировать среднеквадратическую ошибку между \(y\) и \(X\theta\) (используюя Least Squares Method)

Используя матричную формулеровку, следует выражение для \(\theta\) как:

\[X^Ty=X^TX\theta \rightarrow \theta=(X^TX)^{-1}X^Ty\]

Который нам дает оптимальный \(\theta\) в этой постоновке задачи, решая \(\theta\) мы можем делать предсказание \(y=X\theta+\epsilon\)

def linreg(X,y):
    lsm = inv(np.dot(X.T,X))
    Xt = np.dot(X.T,y)
    theta = np.dot(lsm,Xt)
    return theta

Реализация Градиентного Спуска

Алтернативный подход для решения этой задачи (оптимизации гиперпараметров) является с методом градиентного спуска

Для реализации линейной регрессии с помощью методов оптимизации будем использовать функцию ошибки среднего квадратичного, которая является выпуклой функцией в n-мерном пространстве \(\mathbb{R}^n\) и в общем виде выглядит следующим образом:

\[MSE = \frac{1}{n} * \sum_{i=1}^{n}{(y_i - a(x_i))^2}\]
  • \(x_i\) — вектор-признак \(i\)-го объекта обучающей выборки
  • \(y_i\) — истинное значение для \(i\)-го объекта,
  • \(a(x)\) — алгоритм, предсказывающий для данного объекта \(x\) целевое значение
  • \(n\) — кол-во объектов в выборке

В случае линейной регрессии \(MSE\) представляется как:

\[MSE(X, y, \theta) = \frac{1}{2n} * \sum_{i=1}^{n}{(y_i - \theta^Tx_i)^2} = \frac{1}{2n} \lVert{y - X\theta}\rVert_{2}^{2}=\frac{1}{2n} (y - X\theta)^T(y - X\theta)\]
  • \(\theta\) — параметр модели линейной регрессии
  • \(X\) — матрица объекты-признаки
  • \(y\) - вектор истинных значений, соответствующих \(X\)

Возьмем первый вариант представления функции ошибки и посчитаем ее градиент по параметру \(\theta\), предварительно переименовав \(MSE\) в \(L\):

\[L=\frac{1}{2n} * \sum_{i=1}^{n}{(y_i - \theta^Tx_i)^2}\]
\[\nabla L = \frac{1}{n}\sum_{i=1}^{n}{(\theta^Tx_i - y_i) \cdot x_i} = \frac{1}{n}X^T(X\theta - y)\]
def mse_grad(X,theta):
    n = X.shape[0]
    grad = (1/n) * X.T.dot(X.dot(theta) - y)
    return grad

Нам еще нужны функции для шага градиентного спуска и оптимизационный цикл

# шаг градиентного спуска
def grad_step(theta,theta_grad,alpha):
    return theta - alpha*theta_grad

# оптимизационный цикл
def optimise(X,theta,n_iters):

    # theta0
    theta = start_theta.copy()

    # оптимизационный цикл
    for i in range(n_iters):
        theta_grad = mse_grad(X,theta)
        theta = grad_step(theta,theta_grad,alpha)

    return theta

Мы начинаем с начального условия theta итеративно меняем его с помощью градиента, мы используем градиентный спуск для обновления параметров модели (весов) в направлении, противоположном градиенту функции потерь, чтобы минимизировать ошибку предсказания

Регуляризация

Далее посмотрим как можно реализовать регуляризацию в линейную регрессию, это важное понятие потому что оно даст нам возможность контролировать обобщаюшию способность модели, давая нам контроль над влиянием изменения гиперпараметров модели при ее оптимизации. Существет несколько подходов, L1 и L2 регуляризация:

  • Для L1 нормалтзации: Регуляризация также штрафует большие коэффициенты, и может свести их к нулю!
  • Для L2 нормалтзации: Регуляризация штрафует большие коэффициенты, но не обнуляет их, а лишь делает их ближе к нулю. Это помогает уменьшить вариативность модели и сделать её более стабильной

Рассмотрим как это можно реализовать при использования градиентного спуска

После добавления регуляризации функция ошибки линейной регрессии будет выглядеть следующим образом:

\[L=\frac{1}{2n} * \sum_{i=1}^{n}{(y_i - \theta^Tx_i)^2} + \frac{\lambda}{2m}\sum_{j}^{m}{\theta_j^2}\]

А ее градиент по параметру \(\theta\):

\[\nabla L = \frac{1}{n}\sum_{i=1}^{n}{(\theta^Tx_i - y_i) \cdot x_i} + \frac{\lambda}{m}\sum_{j=1}^{m}{\theta_j} = \frac{1}{n}X^T(X\theta - y) + \frac{\lambda}{m}\sum_{j=1}^{m}{\theta_j}\]

Что мы делаем, мы добавляем дополнительный термин который пропорционален квадрату величины коэффициентов в квадратную функцию потерь (на подобие RidgeRegressor)

# градиент квадратной функции потерь
def mse_grad_reg(X,theta):
    n = X.shape[0]
    grad = (1/n) * X.T.dot(X.dot(theta) - y) # стандартный градиент функции потерь
    grad_temp = lambd * np.mean(theta)  # дополнительный термин
    return grad + grad_temp

Предсказание с LinearRegression

Имея оптимизированные параметры \(\theta\), мы можем делаить предсказания используя функцию

def predict(X,theta):
     y_pred = X.dot(self.theta)

Logistic Regression

Задача оптимизации гиперпараметров

Теперь посмотрим на логистическую регрессию в этом разделе, ее реализацию в python и numpy. Задача классификации с линейной модели немного отличается от регрессии. Посмотрим как реализовать ее с помощью градиетного спуска, и так же посмотрим на пример регуляризации.

Реализация Градиентного Спуска

В случае логистической регрессией, мы можем использовать только градиентный спуск, потому что нет явного матричного способа найти оптимальные коэффициенты

Начнем с функции сигмойда, sigmoid функция:

\[h_{\theta}(x)=\frac{1}{1+\exp^{-\theta x}},\]
  • \(\theta\) — вектор параметров логистической регрессии
  • \(x\) - вектор признаков объекта из выборки

Функция ошибки для логистической регрессии в случае бинарной классификации называется бинарной кросс-энтропией (log loss) и записывается следующим образом:

\[L=-\frac{1}{n}(y_i \log h_{\theta}(x_i) + (1-y_i) \log(1-h_{\theta}(x_i)))\]

Для геализации градиентного спуска, нам нудем градиент функции потерь

Соответствующий градиент функции ошибки равен:

\[\nabla L=\frac{1}{n}\sum_{i=1}^{n}{(h_{\theta}(x_i)-y_i)x_i}\]

Реализуем функцию сигмойда

# Функция Сигмойда
def sigmoid(X, theta):
    return 1.0 / (1.0 + np.exp(-X.dot(theta)))

Функция ошибки logloss, разница с регрессии только в sigmoid(X,theta), в регресии используем X.theta

# Градиент бинарной кросс энтропии
# sigmoid(X,theta) - классификации
# X.theta - регрессии
def bin_crossentropy_grad(X, y, theta):
    n = X.shape[0]
    grad = (1.0/n) * X.T.dot(sigmoid(X,theta) - y )
    return grad

Как и ранее, нам нужна функция которая выполняет один шаг градиентного спуска, а так же вес цикл оптимизации гиперпараметров \(\theta\)

# Шаг градиентного спуска
def gradient_step(theta, theta_grad, alpha):
    return theta - alpha * theta_grad

# Главная функция 
def optimize(X, y, grad_func, start_theta, alpha, n_iters):

    theta = start_theta.copy()

    for i in range(n_iters):
        theta_grad = grad_func(X, y, theta)
        theta = gradient_step(theta, theta_grad, alpha)

    return theta

Регуляризация

Процесс идентичный, как и в линейной регрессии, мы добавляем дополнительный термин в функцию потерь, в этот раз это бинарная кросс энтропия (logloss)

Функция ошибки для логистической регрессии в случае бинарной классификации с регуляризатором записывается следующим образом:

\[L=-\frac{1}{n}(y_i \log h_{\theta}(x_i) + (1-y_i) \log(1-h_{\theta}(x_i)))+\frac{\lambda}{2m}\sum_{j}^{m}{\theta_j^2}\]
  • \(x_i\) — вектор признаков
  • \(i\)-го примера из обучающей выборки
  • \(y_i\) — истинный класс для соответствующего примера (0 или 1),- \(n\) — число примеров в обучающей выборке
  • \(m\) — количество нефиктивных признаков
  • \(\lambda\) — параметр регуляризации
  • \(h_{\theta}(x)\) — sigmoid функция, равная:
\[h_{\theta}(x)=\frac{1}{1+\exp^{-\theta x}}\]
  • \(\theta\) — вектор параметров логистической регрессии
  • \(x\) - вектор признаков объекта из выборки

Соответствующий градиент функции ошибки равен:

\[\nabla L=\frac{1}{n}\sum_{i=1}^{n}{(h_{\theta}(x_i)-y_i)x_i}+\frac{\lambda}{m}\sum_{j}^{m}{\theta_j}\]

И ее реализация:

# градиент бинарной кросс энтропии
def logloss_reg(X,y,theta):
   n = X.shape[0]
   grad = (1.0/n) * X.T.dot(sigmoid(X,theta) - y)
   grad_term = lambd * np.mean(theta)
   return grad + grad_term

Предсказание с LogisticRegression

Найдя оптимальные гиперпараметры \(\theta\) , мы можем делать предсказания с помошью функции сигмойды. Она дает нам вероятности принадлежности к положительному классу в диапазоне [0,1]. Мы можем как и в sklearn анолагично именовать их predict & predict_proba

# Вероятность принадлежности класса (0/1)
def predict_proba(X,theta):
   return self.sigmoid(X,theta)

# Предсказание с порогом 0.5
def predict(X):
   y_pred = self.predict_proba(X) > 0.5

Подведем Итоги

В этом посте мы рассмотрели как можно реализовать линейную и логистическую регрессию в простом виде. Так же отметили как можно добавить регуляризацию для обоих вариантов. Для линейной регресии мы представили два варианта решения оптимизации гиперпараметров линейных моделей. Подход градиетного спуска используется чаще всего на практике, так как вычисления обратной матрицы является трудозатратным процессом.

Для решения задачи выбора гиперпараметров линейной регресии с помошью градиентного спуска нам нужно знать градиент среднеквадратической ошибки, так же и для логистической регресиии нам нужно знать градиент бинарной кросс энтропии, это пожалуй остновной момент. Зная формулы для \(\frac{dL}{d\theta}\), мы можем воспользоваться градиентном спуском для оптимизации гиперпараметров.

Так же мы увидили как можно добавлять регуляризацию, собственно добавляя дополнительный термин в функцию потерь, дает нам возможность штрафовать большие значения весов, чем больше значение \(\alpha\), тем больше эффект регуляризации и веса будут уменьшатся для больших значении.