Построение логистической регрессии с нуля в NumPy. Это проще, чем вы думаете!

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

Здесь мы расскажем о математике модели, о том, как ее обучить и как построить на Python с помощью numpy.

Математика, лежащая в основе модели

Давайте сначала представим, что у нас есть набор данных, подобный приведенному выше, где желтые точки соответствуют классу 0, а фиолетовые - классу 1. Чтобы предсказать метку класса, нам нужна модель, которая будет принимать два измерения данных в качестве входных и предсказывать число от 0 до 1. Считайте, что это вероятность метки с учетом данных и параметров модели: ŷ = p (y = 1 | X, w).

Модели логистической регрессии стремятся подобрать прямую линию (или плоскость / гиперплоскость) между двумя классами так, чтобы точки на линии имели вероятность 0,5, а точки, удаленные от линии, имели вероятность 0 или 1 в зависимости от того, с какой стороны линия, на которой они лежат.

Итак, теперь мы понимаем концепцию, давайте попробуем ее немного формализовать.

Наша линейная линия / плоскость / гиперплоскость может быть выражена как: x̂ = wx + b, где w - вектор весов, а b - предвзятость.

«Это все очень хорошо, но как мне получить закрытую форму для оценки вероятности, присвоенной ярлыку?», - спросите вы.

Отличный вопрос!

Что ж, все, что нам нужно, это способ сопоставить нашу линейную модель с допустимой вероятностью. Это означает, что функция должна иметь следующие свойства:

  1. Он может выводить только значения в диапазоне [0,1] при непрерывном вводе
  2. Он должен быть гладким и дифференцируемым (причина этого станет более понятной позже).

Здесь в игру вступает сигмовидная или логистическая функция. График сигмовидной функции показан ниже вместе с ее функциональной формой. Это означает, что у нас есть выражение для вероятности того, что точка принадлежит определенному классу.

Таким образом, всю нашу модель можно выразить как: ŷ = σ (wx + b).

Обучение нашей модели

Мы знаем, что модель может выводить вероятность для каждого из двух классов, но как выбрать веса модели (w, b)?

Для этого нам нужно определить функцию стоимости для модели и минимизировать эту функцию по отношению к параметрам модели.

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

Эта потеря также называется бинарной кросс-энтропией.

Итак, хорошая новость в том, что мы почти у цели! Теперь нам нужно использовать градиентный спуск для оптимизации параметров на основе функции стоимости. Этот процесс оптимизации является наиболее распространенным способом обучения сетевых моделей машинного обучения. Он включает в себя вычисление производной функции стоимости по каждому из параметров модели - поэтому важно, чтобы сигмоидальная функция была гладкой и дифференцируемой.

Те из вас, кто немного разбирается в вычислениях, могут знать это как применение правила цепочки, но благодаря Джеффу Хинтону [1] в мире машинного обучения мы называем это обратным распространением.

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

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

От логистической регрессии к глубоким нейронным сетям

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

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

Код

Итак, это теория, но как мы можем применить это на практике?

Давайте реализуем прямой проход, чтобы делать прогнозы с помощью модели.

import numpy as np
def _sigmoid(x):
    return 1/(1 + np.exp(-x))
def predict(X,W):
        X = np.asarray(X)
        if len(X.shape) != 2:
            raise ValueError("Shape must be (dims,n_data_points)")
        X = np.concatenate([X,np.ones([1,X.shape[-1]])],axis=0)
        X_hat = np.matmul(W,X)
        y_hat = _sigmoid(X_hat)
        
        return y_hat

В реализации следует отметить две особенности:

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

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

Теперь давайте определим функцию стоимости ...

cost = -np.sum(y*np.log(y_hat) + (1-y)*np.log(1-y_hat))

… И шаг градиентного спуска:

dc_dw = -np.sum((y-y_hat)*X,axis=-1)[np.newaxis,:]
self.W = self.W - dc_dw * lr

Теперь мы можем обернуть это в классе с методами fit и predict, чтобы имитировать API типа sk-learn.

class LogisticRegressor():
    def __init__(self):
        self.loss = []
        self.W = None
    
    def _sigmoid(self,x):
        return 1/(1 + np.exp(-x))
def fit(self,X:np.ndarray,y:np.ndarray,epochs: int=100,lr: float=0.01):
        self.epochs = epochs
        self.lr = lr
        X = np.asarray(X)
        y = np.asarray(y)
        if len(X.shape) != 2:
            raise ValueError("Shape must be (dims,n_data_points)")
        X = np.concatenate([X,np.ones([1,X.shape[-1]])],axis=0)
        dims, n_data_points = X.shape
        
        if self.W is None:
            self.W = np.random.randn(1,dims)
        
        for i in range(epochs):
            X_hat = np.matmul(self.W,X)
            y_hat = self._sigmoid(X_hat)
            
            cost = -np.sum(y*np.log(y_hat) + (1-y)*np.log(1-y_hat))
            self.loss.append(cost)
            
            dc_dw = -np.sum((y-y_hat)*X,axis=-1)[np.newaxis,:]
            self.W = self.W - dc_dw * self.lr
        
    def plot_loss(self):
        plt.scatter(list(range(len(self.loss))),self.loss)
        
    def predict(self,X:np.ndarray)-> np.ndarray:
        X = np.asarray(X)
        if len(X.shape) != 2:
            raise ValueError("Shape must be (dims,n_data_points)")
        X = np.concatenate([X,np.ones([1,X.shape[-1]])],axis=0)
        X_hat = np.matmul(self.W,X)
        y_hat = self._sigmoid(X_hat)
        
        return y_hat

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

В этом случае мы визуализировали границу принятия решений между двумя классами. Это можно найти там, где классификатор предсказывает вероятность 0,5. Другими словами, это строка, в которой wx + b = 0. Таким образом, наша тяжелая работа выглядит так, как будто она окупилась, наша модель может правильно определить 0,89% данных. точек и может выражать неопределенность при классификации точек, близких к границе решения!

Итак, что мы узнали?

Если вы зашли так далеко, то, надеюсь, вы лучше понимаете, как работает логистическая регрессия и почему двоичная кросс-энтропия - это просто другой способ выразить логарифмическую вероятность предсказаний модели.

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

[1]: Rumelhart, D., Hinton, G. и Williams, R., 1986. Изучение представлений с помощью ошибок обратного распространения. Природа, 323 (6088), стр. 533–536.