Если вы новичок в науке о данных или машинном обучении, у вас может возникнуть соблазн сбежать в Kaggle и сразу же приступить к работе над большими проектами, и я вас не виню; Я тоже люблю работать над проектами и бросать себе вызов. Хотя лучший способ научиться программировать — это работать над проектами и учиться на своих ошибках, есть один важный фундаментальный шаг, на который вы должны обратить внимание. Благодаря таким фреймворкам, как TensorFlow или PyTorch, вам осталось всего несколько строк кода, чтобы построить модель нейронной сети с глубоким обучением и получить хорошую точность, но вам не кажется, что чего-то не хватает? Или вам может быть интересно, что происходит под капотом и как работают эти модели нейронной сети. Я пишу эту статью, чтобы сделать несколько шагов назад к регрессии и рассмотреть основы, чтобы иметь более прочную основу, на которой вы можете строить.

Основная идея…

Давайте начнем рисовать некоторые точки данных.

import numpy as np
import matplotlib.pyplot as plt

X = np.array([0,1,2,3,4,5,6,7,8,9,10])
y = np.array([1,3,5,4,7,5,8,9,11,10,13])
plt.scatter(X,y)

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

Следующее уравнение может представлять нашу линию:

w0: значение смещения, которое увеличит или уменьшит значение y при x=0.
w1: вес нашей переменной x, которая увеличит или уменьшит наше значение x переменная.

На данный момент мы хотим найти оптимальные веса (w0 и w1), чтобы сумма наших ошибок (смещений вершин) была небольшой. Мы используем функцию стоимости Сумма S в квадрате Error(SSE) сделать это:

Эта функция принимает веса — в данном случае (w0,w1) — затем вычисляет разницу между нашим предсказанным значением y и истинным значением y, возводит результаты в квадрат, а затем суммирует их. Вот что я хотел бы, чтобы вы вынесли из этого прекрасного уравнения. Это функция стоимости, обозначаемая как J(w), где w — вектор [w0,w1]. Поскольку мы работаем только с двумя переменными, мы можем представить это как J(w0,w1). Эта функция стоимости создаст трехмерную поверхность с осью w0 и осью w1; выход — это наше значение стоимости по оси z (третье измерение). Конечно, мы могли бы посмотреть на эту трехмерную поверхность и увидеть самую нижнюю точку, но при работе с моделями глубокого обучения функция стоимости имеет миллионы, а иногда и миллиарды параметров или весов. Итак, поскольку мы не можем визуализировать миллионное измерение или даже четвертое, нам нужен лучший способ найти наилучшую комбинацию значений веса для достижения глобального минимума. Благодаря исчислению это можно сделать простым способом; мы называем это градиентным спуском.

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

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

Где n — скорость обучения, определяющая, насколько малы шаги по направлению к спуску; если мы делаем слишком большие шаги, мы можем промахнуться и пропустить глобальный минимум стоимости, а если наши шаги слишком малы, обучение модели займет целую вечность, что также не идеально. Прежде чем мы углубимся в частную производную (градиент), давайте визуализируем нашу функцию стоимости в 3D.

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

Частные производные

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

Если вы не знакомы с исчислением, не переживайте слишком сильно. Постарайтесь понять основные идеи, которые мы здесь обсуждаем. Тогда я предлагаю вам посмотреть уроки по производным, частным производным и градиентному спуску. Я очень рекомендую репетитора Youtube под названием «Репетитор по органической химии». Вот составленный им плейлист по исчислению.

Теперь, когда мы получили функцию стоимости:

Мы можем реализовать это на Python:

import numpy as np
from numpy import random 
import math
import time 
import pandas as pd
import matplotlib.pyplot as plt
random.seed(seed=1234)

class LinearRegression():
    def __init__(self, eta=0.01, n_iter=20):
        self.eta = eta #Learning Rate
        self.n_iter = n_iter #Number of times we update our weights.
    def fit(self, X, y):
        self.w_ = np.random.randn(1 + X.shape[1]) #Initial Weights
        self.cost_ = [] 

        for i in range(self.n_iter):
            output = self.net_input(X)
            errors = (y - output)
            self.w_[1:] += self.eta * X.T.dot(errors) #X.T.dot(errors) is the Gradient Descent of the Cost function. 
            self.w_[0] += self.eta * errors.sum()
            cost = (errors**2).sum() / 2.0
            self.cost_.append(cost)
        return self

    def net_input(self, X):
        return np.dot(X, self.w_[1:]) + self.w_[0] 

    def predict(self, X):
        return self.net_input(X)

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

from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_std = sc.fit_transform(X)
y_std = sc.fit_transform(y[:, np.newaxis]).flatten()
lr = LinearRegression()
lr.fit(X_std, y_std)

def lin_regplot(X, y, model):
    plt.scatter(X, y, c='steelblue', edgecolor='white', s=70)
    plt.plot(X, model.predict(X), color='black', lw=2)
    return None 

lin_regplot(X_std, y_std, lr)
plt.xlabel('X (standardized)')
plt.ylabel('y (standardized)')
plt.show()

График SSE для каждой эпохи (n_iter):

plt.plot(range(1, lr.n_iter+1), lr.cost_)
plt.ylabel('SSE')
plt.xlabel('Epoch')
plt.show()
print(f"SSE: {lr.cost_[-1]}")

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

random.seed(seed=1234)

# Let's define the range for w0 and w1
w0_range = np.linspace(-2, 2, 100)
w1_range = np.linspace(-2, 2, 100)
w0, w1 = np.meshgrid(w0_range, w1_range)

# Calculating the cost function for each combination of w0 and w1
cost_f = np.zeros_like(w0)
for i in range(len(w0_range)):
    for j in range(len(w1_range)):
        errors = (y  - w1[i, j] * X) + w0[i, j] 
        cost_f[i, j] = (errors ** 2).sum() / 2.0

w_i = np.random.randn(1 + X.shape[1]) #initial random weights used in our model.
w0_i = int(w_i[0])
w1_i = int(w_i[1])

w0_f = int(lr.w_[0]) #Our final weights from the model
w1_f = int(lr.w_[1])

fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')

ax.plot_surface(w0, w1, cost_f, cmap='viridis')
ax.plot([w0_i, w0_i], [w1_i, w1_i], [0, cost_f[w0_i, w1_i]], color='red', linewidth=2, label='Initial Weights', zorder=10)
ax.plot([w0_f, w0_f], [w1_f, w1_f], [0, cost_f[w0_f, w1_f]], color='blue', linewidth=2, label='Final Weights', zorder=10)

ax.set_xlabel('w0')
ax.set_ylabel('w1')
ax.set_zlabel('Cost')
ax.set_title('Cost Function')
ax.view_init(elev=20, azim=155)  # Set the elevation and azimuth angles



ax.legend()

# Show the plot
plt.show()

Множественная линейная регрессия

Вы можете спросить себя, а что, если у нас есть несколько переменных, будет ли математика такой же? Ответ положительный. Начнем с работы с двумя независимыми переменными, которые мы называем x1 и x2. Создадим массив формы (50,2); то есть 50 строк и два столбца.

n = 50  # Number of data points
X = np.random.randint(0,10,(n,2))  # Independent variables
y = 2 * X[:,0] + 3 * X[:,1] + np.random.randn(n)* 0.5  # Dependent variable (target)

В общем, мы можем определить плоскость множественной линейной регрессии следующим образом:

x0 = 1, что дает наш вес смещения w0.

Используя тот же код и функцию стоимости, которые мы использовали ранее, вот подобранная гиперплоскость и стандартизированные точки данных X1 и X2:

Множественная линейная регрессия против. Адалин

Наконец, я хотел бы показать, насколько процесс обучения, используемый в Adaline (Adaptive Linear Neuron), похож на нашу модель регрессии. Вот схематическая иллюстрация простой Adaline:

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

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

Хорошо, на этом все заканчивается. Я надеюсь, что вы узнали кое-что из этого. В будущем я хотел бы подробно остановиться на архитектурах MLP, RNN и Transformers. Спасибо за прочтение!

P.S. Ниже приведена ссылка на полный код, размещенный на моем GitHub :)