Машинное обучение: Линейная регрессия. Теория и реализация. С нуля. На чистом Python

от автора

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

Введение

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

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

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

Линейная регрессия представляет собой линейное уравнение, состоящее из определенного числа зависимых переменных (входных признаков) и их коэффициентов:   y = w_1 x_1 + ... + w_m x_m + b, m — число признаков, b — смещение (смещение выходного значения функции). При обучении ЛинР нужно оценивать степень соответствия получаемой функции и точек данных, это соответствие представляется в виде остаточного члена.

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

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

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

Линейность. Независимые и зависимые переменные должны иметь линейную связь друг с другом. То есть изменения зависимой переменной приводят к изменению независимой переменной линейно.

Независимость. Наблюдения в наборе данных должны быть независимы друг от друга — значения переменных для одного наблюдения (объекта) не должны зависеть от значений переменных для другого наблюдения (объекта).

Дисперсия остатков. У всех точек данных относительно линии регрессии должна быть примерно одинаковая дисперсия (отклонение). Это указывает на то, что используемые признаки не влияют на дисперсию остатков.

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

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

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

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

x_{ij} = {\frac {x_{ij} - mean(X_j)} {std(X_j)}}, {i=1,...,n, j=1,...,m}std(X_j) = \sqrt{\frac {1}{n} \sum_{i=1}^{n} (x_{ij} - mean(X_j)}mean(X_j) = \frac {1}{n} \sum_{i=1}^{n} x_{ij}, X_j = (x_{1j}, ...,x_{nj})

n — число наблюдений (объектов), m — число признаков,   X_j— список всех значений из набора данных для данного признака (j-я координата вектора), mean — среднее значение, std — стандартное отклонение.

Обучение

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

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

E = MSE(Y, T) = \frac{1}{n} \sum_{i=1}^{n} (y_i - t_i)^2, y_i = \sum_{j=1}^{m} (x_{ij} w_j) + b

E — функция ошибки, n — число примеров, m — число признаков, b — смещение,   x_{ij}— j-й признак i-го объекта,   w_j— i-й вес (изменяемый параметр).

Производные по весам и смещению:

E'_b = \frac{\partial E}{\partial b} = \frac{2}{n} \sum_{i=1}^{n} (y_i - t_i), E'_{w_j} = \frac{\partial E}{\partial w_j} = \frac{2}{n} \sum_{i=1}^{n} (y_i - t_i) x_{ij}

Изменения весов и смещения: b = b - lr \cdot E'_b, w_j = w_j - lr \cdot E'_{w_j}, lr — скорость обучения.

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

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

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

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

Ниже представлены дополнительные члены для функции ошибки, они используют не обучаемый параметр \lambda, этот параметр (коэффициент) регулирует степень влияния члена регуляризации. Во всех трех членах смещение b учитывается как последний вес, а m = m + 1.

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

L_1 = \lambda \sum_{j=1}^{m} |w_j|, E =  \frac{1}{n} \sum_{i=1}^{n} (y_i - t_i)^2 + L_1

L2-регуляризация (квадрат L2-нормы вектора весов). Цель состоит в том, чтобы предотвратить переобучение модели ЛинР путем уменьшения больших, по абсолютному значению, весов. Это полезно, когда набор данных имеет мультиколлинеарность — входные признаки сильно коррелируют. Этот член в функции ошибки уменьшает сумму квадратов значений весов.

L_2 = \lambda \sum_{j=1}^{m} |w_j|^2, E =  \frac{1}{n} \sum_{i=1}^{n} (y_i - t_i)^2 + L_2

L1- и L2-регуляризация вектора весов (назовем этот член L3) — это гибридный метод регуляризации, который сочетает в себе возможности L1- и L2-регуляризации для предотвращения переобучения модели. Здесь используется дополнительный не обучаемый параметр   \alpha, с помощью которого можно управлять сочетанием L1- и L2-регуляризацией.

L_3 = \lambda \alpha \sum_{j=1}^{m} |w_j| + \lambda \frac{1-\alpha}{2} \sum_{j=1}^{m} |w_j|^2, E =  \frac{1}{n} \sum_{i=1}^{n} (y_i - t_i)^2 + L_3

Обучение модели при использовании дополнительных членов в функции ошибки

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

Производные от дополнительных членов по j-тому весу:

L_1: \lambda \cdot sign(w_j)
L_2: 2 \cdot \lambda \cdot w_j
L_3: \alpha \cdot \lambda \cdot simg(w_j) + (1 - \alpha) \cdot \lambda \cdot w_j

Для смещения вместо w_jнужно вставить b.

Сигнальная функция (производная выражения под модулем):

sign(x) =   \begin{cases} 1, & \quad x > 1\\ 0, & \quad x = 0 \\ -1, & \quad x < 0 \end{cases}

Производные по весам и смещению:

E'_b = \frac{2}{n} \sum_{i=1}^{n} (y_i - t_i) + L, E'_{w_j} = \frac{2}{n} \sum_{i=1}^{n} (y_i - t_i) x_{ij} + L

Вместо L нужно вставить выражение для производной L1, L2 или L3 для веса или для смещения.

Изменение весов и смещения:b = b - lr \cdot E'_b, w_j = w_j - lr \cdot E'_{w_j}

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

Наиболее распространенные метрики качества для ЛинР: MSE, MAE, RMSE, коэффициент детерминации R-квадрат, скорректированный коэффициент детерминации R-квадрата.

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

R_2 = 1 - \frac {ESS}{TSS}

Остаточная сумма квадратов (ESS, errors square sum) — сумма квадратов остатков (ошибок) для каждой точки данных из обучающего (тестового) набора. Это измерение разницы между наблюдаемым результатом и ожидаемым.

ESS = \sum_{i=1}^{n} (y_i - t_i)^2

Общая сумма квадратов (TSS, total square sum) — сумма ошибок точек данных относительно среднего значения целевой переменной.

TSS = \sum_{i=1}^{n} (y_i - mean(T)), mean(T) = \frac{1}{n} \sum_{i=1}^{n} t_i

 t_i — целевое значение (target),   y_i— ответ модели.

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

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

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

Преимущества и недостатки

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

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

Далее рассмотрим реализацию.

Создание набора данных

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

from random import randint from matplotlib import pyplot as plt  def linear_data_points(n_samples, noise=1):     k = randint(-100, 100) / 100     b = randint(0, 100) / 100     print(k, b)      def f(input_value):         offset_x = randint(-100 * noise, noise * 100) / 100         offset_y = randint(-100 * noise, noise * 100) / 100         x = input_value + offset_x         y = k * x + b + offset_y         return x, y      points = [f(i) for i 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   x_list, y_list = linear_data_points(n_samples=5, noise=0.5)  print(x_list, y_list)  plt.scatter(x=x_list, y=y_list) plt.show()

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

В этом генераторе создаются точки данных со смещением от истинной зависимости, которая описывается с помощью двух случайно сгенерированных параметров k и b. Здесь b — это смещение (по оси у, это смещение зависимости), k — это обычный вес, они оба являются обучаемыми параметрами. Переменные offset_x и offset_y отвечают за случайное смещение точки от истинной зависимости. Таким образом у нас получается набор точек, который мы можем аппроксимировать с помощью модели линейной регрессии.

Выводим значения параметров b и k, их нужно запомнить. В результате обучения модели ЛинР, должны получиться веса со значениями, очень близкими к этим. Также выводим сами точки данных x_list и y_list, они будут нашим обучающим набором данных, их нужно скопировать и объявить в другом файле.

Должны получиться несколько точек данных, как на рисунке ниже.

 Точки данных на графике

Точки данных на графике

Создание простой модели ЛинР

Под словом «простая» я имею в виду простую реализацию кода (костыльная реализация). Создаем новый файл и объявляем в нем все выше описанные данные и инициализируем параметры b и k случайным значением от -1 до 1.

from random import randint  n_samples = 5  # target: k = -0.41, b = 0.81 x_values = [0.12, 1.4, 1.9, 3.5, 4.44]  # inputs y_values = [0.7708, -0.004, 0.051, -0.245, -1.0704]  # targets lr = 0.1 b, k = randint(-100, 100) / 100, randint(-100, 100) / 100  # bias, w

Делаем статическую функцию для зависимости y = kx + b.

def f(x):     return k * x + b

Наша задача состоит в том, чтобы аппроксимировать набор данных и оптимизировать модель ЛинР. Оптимизировать модель будем с помощью градиента. Для оптимизации нам нужна какая-то целевая функция. В качестве такой функции будем использовать функцию среднего квадратичного отклонения (средней квадратичной ошибки). Оптимизируем значение этой функции к минимуму, то есть в сторону нуля, поэтому нужно использовать вычетание градиента, другими словами это будет градиентный спуск. Здесь слова «отклонение», «ошибка» и «остаток» можно рассматривать как взаимозаменяемые, так как речь идет о расхождении между ответом (output) модели и целевым значением (target).

Функция MSE выглядит таким образом.

def mse():     errors = []      for i in range(n_samples):         target = y_values[i]         output = f(x_values[i])          errors.append((target - output) ** 2)      return sum(errors) / n_samples

errors — это список для хранения значений отклонений (остатков) для каждого входного значения из списка x_values.

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

for epoch in range(100):     for i in range(n_samples):         target = y_values[i]         output = f(x_values[i])          b -= lr * (2 / n_samples) * (output - target)         k -= lr * (2 / n_samples) * (output - target) * x_values[i]      print(f"epoch: {epoch}, error: {mse()}")  print(k, b)

Процесс обучения производится 100 раз (100 итераций), по каждому входному значению из набора данных. Вычисляем ответ модели для каждого входного значения и сохраняем его в переменную output. Затем используем формулу (которая дана в теоретической части) для вычисления значения двух частных производных (получается вектор-градиент) и вычитаем градиент из текущего значения вектора весов, состоящего из двух параметров (k, b). Здесь представлен стохастический градиентный спуск — изменение весов после вычисления производной каждого примера.

Выводим получившиеся после обучения значения параметров b и k. Они должны получиться близкими по значению с теми, которые использовались при генерации набора данных. В этом примере это k = -0.41 и b = 0.81.

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

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

Создаем новый файл и в нем немного изменяем инициализацию набора данных.

inputs = [[0.12], [1.4], [1.9], [3.5], [4.44]]  # inputs

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

def mse(outputs, targets):     error = 0      for i, output in enumerate(outputs):         error += (output - targets[i]) ** 2      return error / len(outputs)

Тут сразу суммируем все значения отклонений.

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

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

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

Далее создаем метод для вычисления ответа модели.

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

Перемножаем все признаки и их веса и суммируем, у веса-смещения значение признака равно 1, поэтому просто прибавляем. Обращаемся к этому элементу через индекс «-1» (первый с конца).

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

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

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

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: {mse(outputs, targets)}")

Метод называется «fit», это глагол, на английском означает «произвести соответствие», то есть тут производится процесс «подгонки» входных данных к выходным.

Далее создаем экземпляр этого класса и проводим обучение модели.

targets = [0.7708, -0.004, 0.051, -0.245, -1.0704]  # targets lr_model = LinearRegression(features_num=1) lr_model.fit(inputs, targets, epochs=100, lr=0.1) print(lr_model.weights)

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

Проверка качества модели

Создаем функцию для вычисления R-квадрата.

def r2_score(outputs, targets):     mean_target = sum(targets) / len(targets)      ess = sum([(outputs[i] - targets[i]) ** 2 for i in range(len(outputs))])     tss = sum([(targets[i] - mean_target) ** 2 for i in range(len(outputs))])      return 1 - ess / tss

Обучаем модель и создаем список ответов модели на входные данные.

outputs = [lr_model.forward(inp) for inp in inputs]

После этого мы можем вычислять различные метрики для проверки качества модели.

print(f"mse: {mse(outputs, targets)}") print(f"r2: {r2_score(outputs, targets)}")

Ошибка MSE довольно мала (0.04), это свидетельствует о том, что модель описывает зависимость достаточно близко к истинной зависимости. Значение R-квадрата довольно близко к 1 (0.88), это свидетельствует о том, что дисперсия остатков модели довольно мала, это означает, что модель хорошо описывает зависимость целевых значений от независимых переменных (входных признаков), что точки данных распределены хорошо.

Задания для самостоятельного решения

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

  • Попробовать использовать нормировку данных для каждого признака.

  • Проверить качество модели.

  • Обучить модель и проанализировать результаты работы модели, качество модели на новых данных.

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

  • Попробовать регуляризацию.

Заключение

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

Эта статья будет очень полезна для начинающих. А для тех, кто уже «в теме», эта статья может послужить в качестве источника для новых идей, ведь новые идеи не появляются из неоткуда, они могут появляться в тот момент, когда вы что-то читаете или изучаете.

Ссылка на папку с кодом на ГитХабе.


ссылка на оригинал статьи https://habr.com/ru/articles/863194/