Тази статия представя дълбоко гмуркане в логистичния регресионен модел, който е един от основните модели, използвани за класифициране на данни.

Проблемът с класификацията

Има два широки вида модели на машинно обучение: регресионни модели и класификационни модели. Регресионните модели са тези, които помагат при прогнозирането на някаква непрекъсната променлива, т.е. нещо, което има диапазон от непрекъснати стойности, например: цени на определен продукт или количеството очаквани продажби за влагане в рекламата и маркетинга или колко продажби на продукта могат да се очакват, дадени пазарните условия.

Класификационните модели от друга страна имат за цел да групират данните в отделни класове въз основа на определени характеристики в набора от данни. Не е необходимо групите да бъдат подредени, но най-често трябва да са уникални и за предпочитане да не се припокриват в пространството на характеристиките. Може да има произволен брой характеристики, които могат да повлияят на класа на наблюдение. Проблемът с класификацията често се свежда до прогнозиране на вероятността определен субект да принадлежи към определена група. Тази вероятност е това, което определя към кой клас е присвоен обектът по време на прогнозите

Как ще помогне логистичната регресия

Логистичната регресия се основава на специална функция в математиката, наречена функция на логистичния растеж. Изразява се под формата на уравнение и има съответна графика за две стойности x и y, както следва:

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

Дясната страна на уравнението е член на линейна регресия. Това ще бъде уравнението на параметъра, като бетите са параметрите на модела.

Забележете, че има две плато или повече формално стабилни състояния на функцията. Една близо до ниски стойности на x и ниски стойности на y и след това функцията се изкачва до друго стабилно състояние при някаква стойност на x и след това остава на стабилна стойност за по-нататъшно увеличаване на стойността на x. Това се нарича двуфазна крива поради двете стабилни състояния и въз основа на някаква прагова стойност на x, тя може да промени състоянието си.

Двуфазното свойство на тази логистична крива се използва за правене на прогнози за набори от данни, които имат два различни етикета на клас. Въпреки че логистичната регресия може да се използва за набори от данни, които имат множество класове, ние ще се съсредоточим върху двата класа от модела. Нека се потопим в детайлите на математическата рамка на модела.

Работа на логистична регресия

Функцията logit е логаритмична трансформация на сигмоидната функция, която изчислява съотношението на логаритъма на вероятността за наблюдение, принадлежащо към клас 0 или клас 1. Тя се дава от следната формула:

Както всеки модел на машинно обучение, има цена, свързана и с този модел. Функцията на разходите на този модел основно изчислява сумата от логаритмичните стойности на вероятностите на грешно класифицираните точки. Тя се дава от следното уравнение.

С функцията на разходите виждаме, че с малко опростяване получаваме трансцендентно уравнение, което не може да бъде решено с нормални оценки на най-малкия квадрат, тъй като уравнението няма решение в затворена форма. Така че трябва да използваме числен метод, за да намерим глобалния минимум на функцията на разходите в пространството на параметрите. Ще използваме градиентно спускане, за да направим точно това.

Градиентно спускане е метод за оптимизация за намиране на най-близкия минимум на функция при даден набор от начални променливи. Минимумът, който намира градиентното спускане, може да бъде или локалният минимум, или глобалният минимум. работи при ходене в обратна посока на възходящия градиент на функцията. за дадена функция, новата стойност на x ще бъде дадена от следното уравнение.

Графиката по-долу показва какво прави градиентното спускане. Започва с произволна стойност на аргументите за функцията, след това изчислява наклона на функцията при тези параметри, като използва първата производна и след това прави отрицателна стъпка към минимума на функцията. Размерът на „стъпката“ или колко параметрите се движат към минималната стойност се диктува от алфа параметъра, който се нарича скорост на обучение, тъй като той определя колко големи стъпки отнема спускането и следователно колко време отнема на модела "уча". Ако стойността на алфа е твърде малка, отнема много време, докато параметрите се сближат, ако е твърде голяма, оптимизацията може да надхвърли минимума и да излезе извън контрол.

В горното изображение вземаме една стойност на функцията при определена стойност на x, след това изчисляваме производната на функцията в тази точка и правим стъпка в отрицателната посока на тази производна. Докато се приближаваме към минимума, размерът на стъпката ни става все по-малък и по-малък. правим това, докато или достигнем минимума и не можем да продължим повече, или докато не бъде достигнат наложен от потребителя лимит на итерация.

Така че за нашата функция на разходите уравнението за градиентно спускане става следното:

Първата производна на функцията на разходите се оказва следната:

Сега, след като имаме всичко на място, нека ги приложим и да ги видим в действие!!

Внедряване

Първо започваме, като дефинираме клас за логистична регресия с inti функция.

import numpy as np 

class LogisticRegression:
  def __init__(self, weights = None, bias = None, random_seed = None):
    self.weights = weights 
    self.bias = bias 
    self.random_seed = random_seed 
    self.errors = [] 

  def fit(): 
    return 

  def predict(): 
    return 
  
  def sigmoid():
    return 
  
  def linear_equation():
    return 
  
  def cost():
    return 
  

Дефинирахме няколко функции за контролиране на различни аспекти на модела, функцията за приспособяване ще приспособи модела към данните, методът за прогнозиране ще направи нови прогнози

сигмоидната функция ще приложи логистичното уравнение, методът на линейното уравнение ще приложи регресионното уравнение и накрая функцията на разходите ще изчисли цената на параметрите на модела.

Нека попълним всеки метод един по един.

def sigmoid(self, x):
  z = self.linear_equation(x) 
  return 1 / (1 + np.exp(-z)) 

Сигмоидната функция извиква линейното уравнение, за да изчисли регресионната стойност за дадено x и след това връща сигмоидния резултат за регресионния резултат z.

def linear_equation(self, x): 
  y = self.bias + (self.weights * x) 
  return y 

Функцията linear_equation просто умножава дадената стойност x с тегла и добавя члена на отклонението, ако има такъв.

def predict(self, x):
  y = self.sigmoid(x) 
  return np.where(y >= 0.5, 1, 0) 

Функцията за прогнозиране приема стойност на характеристика като аргумент и получава прогнозата от сигмоидната функция. След това проверява дали стойността на y е по-голяма или равна на определен праг (в нашия случай е 0,5), ако е равна или по-голяма от 0,5, тогава връща 1 като резултат, в противен случай връща 0 като резултат. Тази граница на прага на прогнозиране е напълно произволна за нашия случай и може да бъде променена от потребителя.

def cost(self, y, X): 
        
        rig_result = self.linear_equation(X) 

        cost1 = np.dot(y.T, np.log(np.exp(rig_result))) 
        cost2 = np.dot((1 - y).T, np.log(1 + np.exp(rig_result)))

        return sum((cost1 + cost2))

функцията на разходите първо получава регресионния резултат от линейната функция, след това изчислява до стойности на функцията на разходите и връща сумата от тези стойности.

Стойността на cost1 е стойността, която ни казва цената, свързана с грешната класификация в наблюдения с клас 1, а стойността на цена 2 ни казва грешната класификация в наблюденията с клас 0

Сега идва функцията за приспособяване, която ще приспособи модела към данните:

def fit(self, X, y, n_iters = 10, alpha = 0.01):

        # apply the gradient descent method to update weights. 

        #initialize parameters 
        if self.weights == None: 
            rgen = np.random.RandomState(self.random_seed) 
            self.weights  = rgen.normal(0, 0.1, X.shape[1]) 
        
        if self.bias == None: 
            self.bias = 0
        
        # iteration 
        for _ in range(n_iters): 

            cost = self.cost(y , X)
            self.errors.append(cost)

            predictions = self.predict(X) 

            mis_classifications = y - predictions

            self.weights -= alpha * sum(np.dot(X.T, mis_classifications))
            self.bias -= alpha * sum(mis_classifications) 

        return self 

функцията за прилягане инициализира теглата и пристрастието съответно към произволни стойности и 0 стойност. По време на итерационната процедура той генерира набор от прогнози и след това проверява степента на грешна класификация. След това използва формулата за градиентно спускане, за да актуализира съответно теглата и отклонението. ще предадем краен брой итерации на функцията за прилягане и тя ще прекрати цикъла, след като тези итерации изтекат. Алфа параметърът отново се избира въз основа на попадение и опит. Докато избираме алфа, ние обикновено търсим стойност, която е достатъчно голяма, за да накара модела да се конвертира в рамките на зададени итерации, но не е твърде голяма, за да накара функцията да превиши минимума.

Взети заедно, нашата програма изглежда като тази по-долу:

import numpy as np 

# logistic regression model for binary classification. 

class LogisticRegression(): 

    def __init__(self, weights = None, bias = None , random_seed = None):

        self.weights = weights
        self.bias = bias 
        self.random_seed = random_seed
        self.errors = [] 


    def fit(self, X, y, n_iters = 10, alpha = 0.01):

        # apply the gradient descent method to update weights. 

        #initialize parameters 
        if self.weights == None: 
            rgen = np.random.RandomState(self.random_seed) 
            self.weights  = rgen.normal(0, 0.1, X.shape[1]) 
        
        if self.bias == None: 
            self.bias = 0
        
        # iteration 
        for _ in range(n_iters): 

            cost = self.cost(y , X)
            self.errors.append(cost)

            predictions = self.predict(X) 

            mis_classifications = y - predictions

            self.weights -= alpha * sum(np.dot(X.T, mis_classifications))
            self.bias -= alpha * sum(mis_classifications) 

        return self 
    
    def predict(self, X):

        y = self.sigmoid(X)
        
        return np.where(y>=0.5, 0, 1) 
    
    def sigmoid(self, x):

        z = self.linear_equation(x)

        return  1/(1 + np.exp(-z)) 
    
    def linear_equation(self, x):

        y = (self.weights * x) + self.bias 
        return y 

    def cost(self, y, X): 
        
        rig_result = self.linear_equation(X) 

        cost1 = np.dot(y.T, np.log(np.exp(rig_result))) 
        cost2 = np.dot((1 - y).T, np.log(1 + np.exp(rig_result)))

        return sum((cost1 + cost2)) 

Нека напишем тестов скрипт, за да тестваме модела...

Тестване на данните от ириса

Ще тестваме този модел в набора от данни на Iris, който е де факто за много предварителни модели. Наборът от данни за ирис съдържа 150 проби от цветя на ирис от 3 различни вида: versicolor, setosa и virginica. Записват се 4 параметъра: SepalLength, PetalLength, Petal Width и Petal Width. Можете да намерите и изтеглите набора от данни тук.

Първо ще започнем със зареждането на набора от данни:

import pandas as pd 
import numpy as np 
from logistic_regression import LogisticRegression 
from sklearn.preprocessing import Standard Scalar 
from sklearn.model_selection import train_test_split 

data = pd.read_csv(path_to_dataset)
data.head() 

Резултатът изглежда нещо подобно:

output: 
  Id  SepalLengthCm  SepalWidthCm  PetalLengthCm  PetalWidthCm      Species
   1            5.1           3.5            1.4           0.2  Iris-setosa
   2            4.9           3.0            1.4           0.2  Iris-setosa
   3            4.7           3.2            1.3           0.2  Iris-setosa
   4            4.6           3.1            1.5           0.2  Iris-setosa
   5            5.0           3.6            1.4           0.2  Iris-setosa

Ще изберем PetalWidthCm като колона с характеристики и ще вземем първите 100 записа в набора от данни. Тези първи 100 записа съответстват на Iris-setosa и Iris-versicolor, по 50 всеки. Нека извлечем функциите и класовете от набора от данни:

import pandas as pd 
import numpy as np 
from logistic_regression import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split 


data = pd.read_csv("./datasets/Iris.csv") 
print(data.head())

X = np.array( data.iloc[0:100, [4]]).reshape(100, 1)
standardize = StandardScaler() 
X = standardize.fit_transform(X) 

y = np.where(data.Species[0:100] == "Iris-setosa", 0,1).reshape(100, 1) 

След като получим колоните с функции, ние използваме стандартния скаларен метод за стандартизиране на данните. Стандартизацията насочва данните към означаване на 0 и прави стандартното отклонение равно на 1 за разпределението на набора от данни.

За стойностите на класа използваме 0 като един клас за Iris-setosa и 1 като етикет на класа за Iris-versicolor.

Преоформяме векторите на етикетите на характеристиките и класовете, за да направим вектор на колона от 100 записа.

Нека обучим модела на тези данни:

import pandas as pd 
import numpy as np 
from logistic_regression import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split 


data = pd.read_csv("./datasets/Iris.csv") 
print(data.head())

X = np.array( data.iloc[0:100, [4]]).reshape(100, 1)
standardize = StandardScaler() 
X = standardize.fit_transform(X) 

y = np.where(data.Species[0:100] == "Iris-setosa", 0,1).reshape(100, 1) 

# train test split and model fitting. 
train_x, test_x, train_y, test_y = train_test_split(X, y, test_size = 0.5) 


model = LogisticRegression() 
n_iters = 20
model = model.fit(train_x, train_y, n_iters = n_iters, alpha = 0.001) 

Първо изваждаме 50% от набора от данни и го оставяме настрана като набор за тестване, а останалите 50 процента използваме като данни за обучение. Ще обучим модела за 20 итерации и с алфа стойност 0,001 за градиентно спускане.

Сега, когато моделът е монтиран, нека да видим как се представя за тестване на данни.

import pandas as pd 
import numpy as np 
from logistic_regression import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split 


data = pd.read_csv("./datasets/Iris.csv") 
print(data.head())

X = np.array( data.iloc[0:100, [4]]).reshape(100, 1)
standardize = StandardScaler() 
X = standardize.fit_transform(X) 

y = np.where(data.Species[0:100] == "Iris-setosa", 0,1).reshape(100, 1) 


train_x, test_x, train_y, test_y = train_test_split(X, y, test_size = 0.5) 


model = LogisticRegression() 
n_iters = 20
model = model.fit(train_x, train_y, n_iters = n_iters, alpha = 0.001) 


predictions = model.predict(test_x) 

def misclassifications(actuals, predictions): 

    misclassifications = actuals - predictions 

    return sum(misclassifications) 


print(f"no of misclassifications: {misclassifications(test_y, predictions)}")
output: 
no of misclassifications: [0]

Дефинираме нов метод, който взема прогнози, направени от модела и действителните стойности на теста, и вижда колко от прогнозите са грешно класифицирани. В нашия случай за текущото изпълнение броят на грешните класификации е 0. Това означава, че моделът е успял да предвиди перфектно всички тестови стойности. В повечето случаи това няма да е така, тъй като ще има известно припокриване в двата класа, но за набора от данни за ириса има ясно разделение между класовете, така че моделът да може да приспособи перфектен класификатор към данните.

Заключение

В тази публикация видяхме подробностите за изпълнението на логистичната регресия и я внедрихме с помощта на numpy в python. Обърнете внимание, че тази конкретна реализация е само за данни от двоичен клас. Друга модификация на логистичната регресия също би могла да класифицира многокласови данни. Един от тях е класификационен модел един срещу всички.

справка

Основната препратка, която използвах за тази публикация, е книгата „Elements of Statistical learning“ от Travis Hastie et. ал. Можете да намерите книгата тук.