Аннотация. Логистическая регрессия — это фундаментальный алгоритм машинного обучения, в котором представлены линейные модели, функция активации, градиентный спуск и обратное распространение. В этой статье мы сосредоточимся на реализации логистической регрессии, градиентного спуска, оптимизатора Адама и бинарной функции перекрестной энтропийной потери с нуля с использованием Python и хорошо разберемся в алгоритмах. Для этой реализации используется общедоступный набор данных, называемый данными банковского маркетинга из UCI. Лучшая модель достигла 86% точности на тестовом наборе без какой-либо обработки дисбаланса данных.

Полный код на GitHub: https://github.com/chaitanyamanem/Bank-Marketing
Набор данных: https://archive.ics.uci.edu/ml/datasets/bank+marketing

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

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

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

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

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

x1..xn : входные признаки
w1 …wn : важность или веса признаков соответственно
w0 : смещение
x0 : всегда 1

Прежде чем мы рассмотрим, как мы определяем веса w1, w2 … wn, нам нужно решить еще одну проблему, а именно: z в приведенном выше уравнении не является вероятностью в диапазоне от 0 до 1 (0 — ноль процентов, а 1 — 100 %). ), который можно увидеть ниже. Мы дали несколько чисел в качестве весов (просто взяв в качестве примера 2 функции), и мы получили некоторое произвольное число.

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

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

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

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

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

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

Во-первых, давайте воспользуемся наивным визуальным подходом, чтобы узнать W1, который дает минимальный убыток. График потерь для различных значений w1 при сохранении постоянного члена смещения w0 (или b) выглядит следующим образом.

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

Теперь давайте перейдем к алгоритмическому подходу. Когда у нас есть потеря для всего набора данных по отношению к начальным случайным весам, которые мы взяли, наш следующий шаг — выяснить, как изменить веса. Нам нужно увеличить или уменьшить вес? (это называется направлением изменения) и сколько нам нужно изменить (это называется наклоном). Здесь нам пригодятся производные, которые дают нам градиенты, которые могут задавать как наклон, так и направление для изменения весов. Если мы пересмотрим тот же пример данных, который мы использовали выше, для начального веса 0,5 градиент, который мы получим с помощью цепного правила, составит 14,63.

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

Некоторые популярные варианты скорости обучения: 0,1, 0,01, 0,001.

Если вы принимаете скорость обучения равной 0,001, ваш w после обновления в соответствии с приведенным выше уравнением будет равен 0,4853. Мы немного опустились к -0,5 с 0,5. Если вы повторите этот процесс несколько раз (обычно называемых эпохами), вы в конечном итоге достигнете весов, дающих минимальные потери (называемых оптимальными весами). Этот процесс называется оптимизацией.

Процесс обучения:

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

X1 – входной признак (у нас могут быть x1, x2….xn функции)
весовой параметр w1 для x1
X0 – фиктивный вход, всегда 1
W0 – коэффициент смещения
a1 — это активация или также можно сказать, что y_hat
L — это проигрыш

Прямое распространение: мы берем входные данные в виде x1, x2 … xn и соответствующие веса как w1, w2 … wn . вместо того, чтобы добавлять член смещения отдельно, мы делаем его w0. Таким образом, функция (x0), которая умножает w0, всегда равна 1. Первая операция в прямом распространении — применить линейное преобразование к входным данным с их соответствующим весом. Это означает, что все функции и веса помещаются в линейное уравнение ниже.

Затем мы отправляем результирующее значение z в нелинейную функцию активации, которая сжимает z в диапазоне от 0 до 1. Это также наше предсказанное значение y_hat.

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

Обратное распространение:

После определения убытка и если он не равен 0, это означает, что нам нужно улучшить, для этого нам нужно скорректировать веса. Как мы уже говорили, мы делаем это систематически, а не случайным образом. Мы используем производные, чтобы выяснить, какими должны быть градиенты весов, чтобы двигаться к минимальным потерям. Например, как мы обновляем вес w1 (вес для признака x1), как показано ниже.

Производную потерь по w1 можно вычислить по цепному правилу, так как между w1 и потерями есть промежуточные функции (операции). Уравнение цепного правила:

Здесь производная функции потерь по y_hat (или a) определяется как

Производная y_hat по z равна

Производная z по w1 равна

Вот и все, мы вычислили одну итерацию логистической регрессии или нейронной сети одного слоя одного нейрона.

Реализация. Теперь пришло время реализовать эти концепции в коде. Мы собираемся перенести данные в массивы numpy и реализовать процесс обучения, используя объясненное выше уравнение. Мы не собираемся использовать готовые пакеты машинного обучения (например, sklearn).

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

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

Шаг 1: Инициализируйте веса. Инициализация нормализованного веса Ксавье лучше всего подходит для инициализации случайных весов, когда у нас есть сигмовидная функция активации.

## Initialize the weights
n_params = X.shape[1]+1 ## Number of parameters (or weights) + 1 for bias
lower, upper = -(1.0 / math.sqrt(n_params)), (1.0 / math.sqrt(n_params))
self.W = np.random.uniform(lower, upper, size=n_params).reshape(n_params,1)
self.W[0,0] = 1 ## bias

Шаг 2: прямое распространение

  
## Training loop for number of epochs
for e in range(epochs):
    ## Loss and gradients of this iteration set to zeros
    L = 0
    grads = np.zeros((n_params,1))
    
    ## Forword propagation
    ## For each training example calculate loss and gradient
    for i in range(n):
        x = X[i,:].reshape(X.shape[1],1)
        x = np.vstack((np.ones((1,1)),x)) ## adding x0 = 1
        z = np.dot(self.W.T,x) 
        y_hat = 1 / (1 + np.exp(-z))
        error = logisticLoss(y[i],y_hat)
        L += error
        
        ## Find gradients                
        dy_hat = y_hat * (1-y[i]) - y[i] * (1-y_hat)/ (y_hat * (1-y_hat))
        dz = np.exp(-z)/(1+np.exp(-z))**2
        grads +=  dy_hat * dz * x
          

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

шаг 3: Обратное распространение.

## Continuation code in the same "Training loop for number of epochs"
    
    ## For each epoch calculate metrics and update weights
    loss = L/n
    val_accuracy = accuracy_score(y_test,self.predict(X_test))
    accuracy = accuracy_score(y_train,self.predict(X_train))
    
    ### Log history
    self.history['loss'].append(loss[0][0])
    self.history['weights'] += (np.copy(self.W),)            
    self.history['accuracy'].append(accuracy)
    self.history['val_accuracy'].append(val_accuracy)            
    print(f"Epoch {e+1} / {epochs} \n loss: {loss[0][0]} Accuracy: {accuracy} val_accuracy: {val_accuracy}")
    
    ### Update parameters    
    
    self.W -= lr * grads / n

Это код продолжения в том же цикле for, что и выше (внутри эпох for цикла, снаружи внутреннего цикла прямого распространения).

Сначала мы вычисляем потери и точность в эту эпоху и регистрируем их, что полезно для наблюдения за ходом обучения. Затем мы обновляем параметры (веса) в соответствии с потерей.

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

Обучение и результаты

Мы создаем модель, созданную выше, и запускаем ее на 100 эпох со скоростью обучения 0,1.

lr = LogisticRegression()
grads = lr.train(X_train,y_train,100,10**-1)

Первоначально точность проверки была очень низкой и составляла примерно 11%. Однако в конечном итоге он улучшается с увеличением количества эпох или, другими словами, после многократного обновления весов. Он достигает примерно 81%, как показано ниже.

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

Учитывая, что набор данных несбалансирован и имеет примерно 89% экземпляров «Нет», точность модели хуже, чем наивная модель, которая все время просто предсказывает «Нет».

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

С оптимизатором Adam: остальная часть кода остается прежней, меняется только часть обновления весов. Теперь вместо того, чтобы обновлять веса непосредственно в тренировочном цикле с текущими градиентами, мы отправляем объект модели и градиенты алгоритму оптимизатора Адама для обновления весов на основе импульса и члена RMSprop. Реализация алгоритма оптимизатора Адама показана ниже.

class Adam:
    def __init__(self,lr=10**-1):
        self.lr = lr
        self.m = None
        self.v = None
        self.beta1 = 0.9
        self.beta2 = 0.999
        self.epsilon = 10**-8
        
    def optimize(self, model, grads):
        if self.m is None and self.v is None:
            self.m = np.zeros_like(grads)
            self.v = np.zeros_like(grads)
        self.m = self.beta1 * self.m + (1-self.beta1) * grads
        self.v = self.beta2 * self.v + (1-self.beta2) * grads ** 2
        
        m_hat = self.m / (1-self.beta1)
        v_hat = self.v / (1-self.beta2)
        
        model.W -= self.lr * m_hat / (np.sqrt(v_hat+self.epsilon))

Теперь изменение в основном коде

### Update parameters    
optim.optimize(self,grads) ## call the optimizer with model object and grads

Изменение функции обучения вызову

lr = LogisticRegression()
grads = lr.train(X_train,y_train,40,Adam(10**-2))

Теперь результаты выглядят так, как показано ниже.

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

Большое спасибо за чтение.

Ссылки
https://arxiv.org/abs/1412.6980
https://machinelearningmastery.com/weight-initialization-for-deep-learning-neural- network/
Д-р Синь Чен, Ноттингемский университет, конспект лекций, 2022 г.