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

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

Введение

Логистическая регрессия (ЛогР) — статистическая модель, используемая для прогнозирования вероятности возникновения некоторого события, она выдаёт ответ в виде числа в промежутке от 0 до 1. Бинарная логистическая регрессия применяется в случае, когда зависимая переменная является бинарной (может принимать только два значения). С помощью ЛогР можно оценивать вероятность наступления события.

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

ЛогР является одним из фундаментальных методов МО. ЛогР выполняется быстро и относительно несложно, и ее результаты удобно интерпретировать. По сути это метод двоичной классификации, но его также можно применять к мультиклассовым задачам.

Логистическая функция или логистическая кривая — это S-образная кривая (сигмоидальная кривая) с уравнением:

f(x) = \frac {L}{1 + e^{-kx}}

k — это коэффициент для переменной x, он отвечает за логистическую скорость роста, крутизна кривой; x — это переменная функции; L — это супремум значений функции, то есть максимальное и минимальное значение, если будет 1, то максимум будет 1, если 2 — то 2 и так далее, нижний супремум равен 0, чтобы его изменить, нужно вычесть или прибавить к этой функции определенное число.

 График логистической функции
График логистической функции

ЛогР является моделью, которая основана на вычислении вероятности, эта логистическая функция выводится через формулу для вычисления вероятности. ЛогР это не применение сигмоидальной функции активации к линейной регрессии, а вычисление вероятности.

Формулировка задачи

Допустим, что у нас есть некоторый набор данных, состоящих из элементов вида x = (x_1,...,x_m), m — число различных признаков, каждый из которых описывает определенную характеристику рассматриваемых объектов или наблюдений.

Нам нужно создать модель, основанную на логистической регрессии, некоторой зависимой переменной y на множестве независимых переменных x = (x_1,...,x_m). У нас есть известные входные значения   x_i и соответствующее фактическое значение результата y для каждого наблюдения i=1,…,n, n — число наблюдений.

Цель — найти функцию логистической регрессии   P такую, чтобы прогнозируемые ответы P(x_i) были как можно ближе к фактическому ответу y_i для каждого наблюдения i=1,…,n. Нужно помнить, что фактический ответ может быть только 0 или 1 в задачах двоичной классификации. Это означает, что каждое значение P(x_i) должно быть близко к 0 или 1.

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

Подготовка данных

Условия, которым должны удовлетворять данные, почти такие же, как и для линейной регрессии:

  • Наблюдения (входные данные) должны быть независимы друг от друга;

  • Между признаками (независимыми входными переменными) не должно быть мультиколлинеарности;

  • Зависимая переменная должна иметь бинарное значение, то есть 0 или 1;

  • Достаточное количество обучающих примеров;

  • В обучающих данных не должно быть много выбросов.

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

x_{ij} = \frac {{x_{ij} - mean(X_j)}} {{std(X_j)}}

Каждый признак масштабируется относительно всех признаков той же категории (см. линейная регрессия).

Обучение

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

L(O, T) = \frac{1}{n}\sum_{i = 1}^{n} −(t_i log(o_i) − (1 − t_i) log(1 − o_i))

O — список ответов модели для каждого входного вектора признаков, T — список целевых значений. Если ответ модели будет равен единице, то в выражении   \log(1 - o_i) будет ошибка, так как логарифм нуля не определен. Чтобы предотвратить ошибку, можно просто изменить значение 1 на 0.99 — логарифм будет определен. Если ответ будет равен нулю, то ошибка будет в выражении \log(o_i), можно изменить 0 на 0.01.

Ответ модели:

o = \frac {1}{(1 + e^z)}

z - линейная комбинация входных данных и коэффициентов регрессии z = x_1 w_1 + ... + x_m w_m + b. Эта функция преобразует любое вещественное число z в значение между 0 и 1, что интерпретируется как вероятность.

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

L'_{wj} = x_{ij} (o_i - t_i) \frac{1}{n}, L'_b = (o_i - t_i) \frac{1}{n}

o — ответ модели на данный входной вектор признаков, t — целевое значение для данного входа, n — число примеров. Эта производная похожа на производную функции MSE, но эти две производные получаются от разных функций. Но по факту, если бы использовалась MSE для оптимизации, то особо ничего бы не поменялось, результат будет почти тот же самый.

Обучение происходит методом стохастического градиентного спуска — изменение весов на каждом примере.

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

L_2: L(t_i, o_i) = − (t_i log(o_i) − (1 − t_i) log(1 − o_i)) + \lambda \sum_{j=1}^{m} w_j^2

m — число весов, λ — коэффициент регуляризации. Другие дополнительные члены для целевой функции оптимизации выглядят и применяются при обучении аналогично, как и для линейной регрессии.

Оценка качества модели

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

Можно было бы использовать MSE для измерения расстояния между ответами и целевыми значениями, но делать так будет некорректно, так как ЛогР решает задачу классификации, где нужно получить значение вероятности, а не задачу регрессии, где нужно получить число. Дело не в типе задачи, а в функции ошибки, в том, что такое MSE.

Оценить качество обученной модели можно с помощью метрики accuracy и precision.

Точность (accuracy). Представляет собой долю правильно классифицированных объектов из общего числа объектов в наборе данных (тестовом). Однако, точность может быть обманчива в случае несбалансированных классов, поскольку она не учитывает соотношения ложных срабатываний. Она рассчитывается по формуле:

accuracy = \frac{TP + TN}{TP + TN + FP + FN}

TP — true positive, истинный положительный, то есть ответ, который классифицирован как 1 и на самом деле ответ должен быть 1. Например, если модель предсказала, что пациент болен, и пациент действительно болен, это считается истинно положительным результатом.

TN — true negative, истинный отрицательный, то есть ответ, который классифицирован как 0 и на самом деле ответ должен быть 0. Например, если модель предсказала, что пациент здоров, и пациент действительно здоров, это считается истинно отрицательным результатом.

FP — false positive, ложный положительный, то есть ответ, который классифицирован как 1, но на самом деле ответ должен быть 0. Например, если модель предсказала, что пациент болен, но пациент на самом деле здоров, это считается ложно положительным результатом.

FN — false negative, ложный отрицательный, то есть ответ, который классифицирован как 0, но на самом деле ответ должен быть 1. Например, если модель предсказала, что пациент здоров, но пациент на самом деле болен, это считается ложно отрицательным результатом.

Если пациент болен, то это положительный класс 1, если здоров, то это отрицательный класс 0.

Точность (precision). Точность показывает долю объектов, названных моделью положительными, которые действительно являются положительными. Эта метрика важна, когда необходимо минимизировать количество ложных положительных результатов. Она рассчитывается по формуле:

precision = \frac{TP}{TP + FP}

Полнота (recall) или чувствительность. Полнота показывает долю объектов положительного класса, которые были правильно выявлены моделью. Эта метрика важна, когда ошибка нераспознания положительного класса имеет большие последствия. Она рассчитывается по формуле:

recall = \frac{TP}{TP + FN}

Эти метрики являются фундаментальными для оценки качества и производительности моделей бинарной классификации.

F1-мера (F1-score). F1-мера представляет собой среднее гармоническое точности и полноты, что позволяет объединить эти две метрики в одну. Эта метрика полезна, когда точность и полнота имеют одинаковую важность. Она рассчитывается по формуле:

F_1 = 2 \cdot \frac{precision \cdot recall}{precision + recall}

Интерпретируемость

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

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

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

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

Особенности логистической регрессии

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

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

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

Примеры использования ЛогР. Оценка риска заболевания и улучшение медицинских решений. Прогнозирование, откроет ли клиент срочный вклад или нажмет ли на кнопку оформления заказа. Анализ банковских транзакций.

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

Создание набора данных для задачи классификации

Делаем учебный набор данных с помощью этого генератора точек. Функция data_points возвращает 2 списка значений. Создаются точки данных в области данной точки.

from random import randint
from matplotlib import pyplot as plt

def data_points(n_samples, class_point, noise=1):
    def offset_point():
        offset_x = randint(-100 * noise, noise * 100) / 100
        offset_y = randint(-100 * noise, noise * 100) / 100
        x = class_point[0] + offset_x
        y = class_point[1] + offset_y
        return x, y

    points = [offset_point() for _ in range(n_samples)]
    x_list = [points[i][0] for i in range(n_samples)]
    y_list = [points[i][1] for i in range(n_samples)]

    return x_list, y_list


x1_list, y1_list = data_points(n_samples=5, class_point=(1, 1), noise=0.9)
x2_list, y2_list = data_points(n_samples=5, class_point=(4, 2.5), noise=2.5)

print(x1_list, y1_list)
print(x2_list, y2_list)

plt.scatter(x=x1_list, y=y1_list, color='red')
plt.scatter(x=x2_list, y=y2_list, color='green')
plt.show()

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

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

 Изображение точек данных на графике
Изображение точек данных на графике

Создавать искусственные точки данных можно и другими зависимостями. В этом примере в качестве зависимости выступает точка. Данные создаются в прямоугольной области, в центре которой лежит данная точка. Длины сторон области задаются переменными offset_x и offset_y Зависимость можно создать, например, в виде небольшого отрезка, который можно получить из уравнения прямой y = kx + b.

Создание простой модели логистической регрессии

Простой с точки зрения кода.

 Сгенерированный набор данных
Сгенерированный набор данных

Сначала нужно сделать предобработку данных, сделать набор входных и выходных данных, делаем списки точек (векторов признаков).

inputs = [(x1[i], y1[i]) for i in range(len(x1))]
targets = [0 for i in range(len(x1))]
inputs += [(x2[i], y2[i]) for i in range(len(x2))]
targets += [1 for i in range(len(x2))]

Создаем веса и функции, которые будут обрабатывать ответ модели.

weights = [randint(-100, 100) / 100 for _ in range(3)]

def weighted_z(point):
    z = [item * weights[i] for i, item in enumerate(point)]
    return sum(z) + weights[-1]

def logistic_function(z):
    return 1 / (1 + e ** (-z))

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

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

def logistic_error():
    errors = []

    for i, point in enumerate(inputs):
        z = weighted_z(point)
        output = logistic_function(z)
        target = targets[i]

        if output == 1:
            output = 0.99999

        if output == 0:
            output = 0.00001

        error = -(target * log(output, e) - (1 - target) * log(1 - output, e))
        errors.append(error)

    return sum(errors) / len(errors)

Далее делаем процедуру обучения весов на всем наборе данных.

lr = 0.1

for epoch in range(100):
    for i, point in enumerate(inputs):
        z = weighted_z(point)
        output = logistic_function(z)
        target = targets[i]

        for j in range(len(weights) - 1):
            weights[j] -= lr * point[j] * (output - target) * (1 / len(inputs))

        weights[-1] -= lr * (output - target) * (1 / len(inputs))

    print(f"epoch: {epoch}, error: {logistic_error()}")

print(weights)

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

Ответы модели на все входные данные можно посмотреть таким образом:

def test():
    for i, point in enumerate(inputs):
        z = weighted_z(point)
        output = logistic_function(z)
        target = targets[i]
        print(f"output: {round(output, 2)}, target: {target}")
test()

Здесь вычисляется ответ модели на каждый элемент данных и выводится вместе с целевым значением.

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

def accuracy():
    true_outputs = 0

    for i, point in enumerate(inputs):
        z = weighted_z(point)
        output = logistic_function(z)
        target = targets[i]

        if round(output) == target:
            true_outputs += 1

    return true_outputs, len(inputs)

print("accuracy:", accuracy())

Улучшение кода

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

def logistic_error(outputs, targets):
    error = 0

    for i, point in enumerate(inputs):
        if outputs[i] == 1:
            outputs[i] = 0.99999
            
        if outputs[i] == 0:
            outputs[i] = 0.00001
            
        error -= targets[i] * log(outputs[i], e) - (1 - targets[i]) * log(1 - outputs[i], e)

    return error / len(targets)

Создаем класс для модели логистической регрессии.

class LogisticRegression:
    def __init__(self, features_num):
        # +1 for bias, bias is last weight
        self.weights = [randint(-100, 100) / 100 for _ in range(features_num + 1)]

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

В методе forward вычисляется взвешенная сумма весов и входных признаков и вычисляется ответ модели.

def forward(self, input_features):
    output = 0
    
    for i, feature in enumerate(input_features):
        output += self.weights[i] * feature
    
    return logistic_function(output + self.weights[-1])

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

def train(self, inp, output, target, samples_num, lr):
    for j in range(len(self.weights) - 1):
        self.weights[j] += lr * (1 / samples_num) * (target - output) * inp[j]

    self.weights[-1] += lr * (1 / samples_num) * (target - output)

В методе fit происходит обучение модели на наборе данных.

def fit(self, inputs, targets, epochs=100, lr=0.1):
    for epoch in range(epochs):
        outputs = []

        for i, inp in enumerate(inputs):
            output = self.forward(inp)
            outputs.append(output)

            self.train(inp, output, targets[i], len(inputs), lr)

        print(f"epoch: {epoch}, error: {logistic_error(outputs, targets)}")

Нужно будет задать число итераций обучения epoch и скорость изменения весов lr.

Создаем модель, указываем число признаков и обучаем

logr_model = LogisticRegression(features_num=2)
logr_model.fit(inputs, targets, epochs=100, lr=0.1)

Создаем метод для получения ответа модели для списка входных значений.

def forward_list(self, inputs):
    outputs = []

    for inp in inputs:
        output = self.forward(inp)
        outputs.append(output)

    return outputs

Функция для оценки accuracy.

def accuracy(outputs, targets):
    true_outputs = 0

    for i, output in enumerate(outputs):
        if round(output) == targets[i]:
            true_outputs += 1

    return true_outputs, len(inputs)

Смотрим какие получились веса и точность модели.

outputs = logr_model.forward_list(inputs)
print(logr_model.weights)
print("accuracy:", accuracy(outputs, targets))

Заключение

Мы рассмотрели основные теоретические моменты и сделали модель логистической регрессии с нуля на чистом Python. Сделали свой собственный генератор данных для задачи классификации, сделали набор данных, обучили на нем модель и оценили качество с помощью метрики accyracy. Потом мы улучшили код, сделали класс для модели логистической регрессии, чтобы ее можно было легко использовать. Немного рассмотрели процесс обучения, как происходит изменение весов, как расстановка переменных в формуле функции ошибки влияет на изменение весов.

Папка с кодом на ГитХабе.

Мои другие статьи на Хабре.

Комментарии (2)


  1. CrazyElf
    10.12.2024 09:07

    Хотелось бы видеть, что выводит ваш код, так гораздо нагляднее было бы )


    1. neuromancertdi Автор
      10.12.2024 09:07

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

      data_generating.py

      [1.44, 0.84, 0.8200000000000001, 1.51, 0.48, 1.5, 1.23, 1.45,
      1.6, 0.41000000000000003] [1.22, 0.85, 1.29, 0.52, 1.0, 1.48,
      0.43999999999999995, 1.08, 1.75, 1.78]
      [3.61, 2.2, 5.85, 4.7, 5.47, 5.21, 3.27]
      [2.0300000000000002, 4.05, 3.11, 2.7800000000000002,
      0.3599999999999999, 1.6600000000000001, 4.41]

      logr_simple_code.py

      ...
      epoch: 98, error: -0.13584311999647203
      epoch: 99, error: -0.1348438388254259
      [0.6294302897862611, 0.16071481253428724, -1.3718277053956087]
      output: 0.3, target: 0
      output: 0.4, target: 0
      ...
      output: 0.6, target: 1
      output: 0.46, target: 1
      accuracy: (16, 17)

      logr_improve_code.py

      ...
      epoch: 98, error: -0.13977499883338454
      epoch: 99, error: -0.1386041623631271
      [0.56564154849143, 0.1414637390798315, -1.1827345023671545]
      accuracy: (15, 17)

      Тогда могу в следующих статьях добавлять вывод