Подробное объяснение математического уравнения для создания практических математических основ для вашего пути к машинному обучению или глубокому обучению.

Всем привет! Это пошаговая инструкция «Уравнение-код», часть 3. На этот раз

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

На этот раз мы займемся нелинейной разделимой проблемой. Если вы не видели первую часть, ничего страшного. Часть 2 является самодостаточной. Но если вы хотите лучше понять часть 2, лучше сначала прочитать часть 1.



Пошаговое руководство по проекту машинного обучения« Уравнение в код на Python - Часть 1, разделимая линейно…
Подробное объяснение математического уравнения для построения практических математических основ для машинного обучения или… в сторонуdatascience.com »



Вот данные и код.

Контент структурирован следующим образом. * означает, что вы можете пропустить этот шаг, если вы уже часть 1.

  1. Посмотрите на данные
  2. Нелинейная разделимая задача
  3. Стандартизация*
  4. Добавьте смещение и полиномиальный член
  5. Сигмовидная функция *
  6. Функция правдоподобия *
  7. Обновить параметр θ *
  8. Постройте линию
  9. Точность
  10. Резюме

1 Посмотрите на данные

Вот данные non_linear_data.csv

x1,x2,y
0.54508775,2.34541183,0
0.32769134,13.43066561,0
4.42748117,14.74150395,0
2.98189041,-1.81818172,1
4.02286274,8.90695686,1
2.26722613,-6.61287392,1
-2.66447221,5.05453871,1
-1.03482441,-1.95643469,1
4.06331548,1.70892541,1
2.89053966,6.07174283,0
2.26929206,10.59789814,0
4.68096051,13.01153161,1
1.27884366,-9.83826738,1
-0.1485496,12.99605136,0
-0.65113893,10.59417745,0
3.69145079,3.25209182,1
-0.63429623,11.6135625,0
0.17589959,5.84139826,0
0.98204409,-9.41271559,1
-0.11094911,6.27900499,0

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

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("non_linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# plot data points
plt.plot(train_x[train_y == 1, 0], train_x[train_y == 1, 1], 'o')
plt.plot(train_x[train_y == 0, 0], train_x[train_y == 0, 1], 'x')
plt.show()

После запуска приведенного выше сценария вы должны увидеть рисунок ниже.

Кажется, мы не можем использовать одну прямую линию для разделения X и O. Мы называем такую ​​проблему проблемой нелинейной разделимости, когда данные не разделимы линейно.

2 Нелинейная разделимая задача

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

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

Мы используем θ для представления параметра. Знак θ в левой части означает, что функция f (x) имеет параметр theta. θ в правой части означает, что есть два параметра. Последний член - это полиномиальный член, который делает модель обобщенной для нелинейных разделяемых данных.

Обратите внимание, что у нас есть две функции x1 и x2 в non_linear_data.csv. Мы выбираем x1 как полиномиальный член. Таким образом, функция должна стать ниже формы.

Инициализируем 4 параметра

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)

3 Стандартизация

Чтобы обучение происходило быстро, мы используем стандартизацию, также называемую z - оценкой. Мы делаем это по столбцам.

  • 𝜇 - среднее значение в каждом столбце
  • 𝜎 - стандартное отклонение в каждом столбце
import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)
# standardization
mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)
def standardizer(x):
    return (x - mu) / sigma
std_x = standardizer(train_x)

4 добавить смещение и полиномиальный член

Нам нужно добавить смещение и полиномиальный член, чтобы построить матрицу данных. Мы добавляем константу x0 = 1, чтобы выровнять векторное представление.

Более подробную информацию о векторном представлении вы можете найти в части 1: 3 Векторное представление.

Чтобы упростить расчет, мы преобразуем x в матрицу.

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)
# standardization
mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)
def standardizer(x):
    return (x - mu) / sigma
std_x = standardizer(train_x)
# add x0 and x1^2 to get matrix
def to_matrix(x):
    x0 = np.ones([x.shape[0], 1]) 
    x3 = x[:, 0, np.newaxis] ** 2
    return np.hstack([x0, x, x3])
mat_x = to_matrix(std_x) 
# dot product
def f(x):
    return np.dot(x, theta)

Мы используем x3 для представления x1*x1.

Размер std_x равен (20, 2). После to_matrix(std_x) размер mat_x равен (20, 4). Что касается скалярного произведения, размер результата равен (4,). Таким образом, результат производства точек должен быть (20, 4) x (4,) -> (20,), который представляет собой одномерный массив, содержащий прогнозы для 20 выборок.

5 сигмовидная функция

Ниже представлено векторное представление

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

Мы используем z для представления линейной функции и передаем ее сигмоидной функции. Сигмоидальная функция даст вероятность для каждой выборки данных. У нас есть два класса в наших данных, один - 1, а другой - 0.

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

Мы можем написать код ниже

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)
# standardization
mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)
def standardizer(x):
    return (x - mu) / sigma
std_x = standardizer(train_x)
# add x0 and x1^2 to get matrix
def to_matrix(x):
    x0 = np.ones([x.shape[0], 1]) 
    x3 = x[:, 0, np.newaxis] ** 2
    return np.hstack([x0, x, x3])
mat_x = to_matrix(std_x)
# change dot production to sigmoid function
def f(x):
    return 1 / (1 + np.exp(-np.dot(x, theta)))

6 Функция правдоподобия

Вы можете пропустить этот шаг, если вас не интересует объяснение уравнения или если вы уже читали это в части 1.

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

Подожди, подожди ... какого черта в этих штуках!

Не паникуйте. Успокойся.

Давайте разбираться.

  • 1- ›2 (как перенести строку 1 в строку 2): log(ab) = log a + log b
  • 2->3: log(a)^b = b * log a
  • 3- ›4: Поскольку у нас есть только два класса, y = 0 и y = 1, мы можем использовать следующее уравнение:

  • 4- ›5: мы используем приведенное ниже преобразование, чтобы сделать уравнение более читаемым.

Итак, мы получили финальную часть.

Не забывай, почему мы это начали. Целевая функция может помочь нам в правильном обновлении параметра.

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

На шаге 6 самое важное уравнение - это. Если вы не можете понять, как это получить, ничего страшного. Все, что нам нужно сделать, это написать его как настоящий код.

7 Обновить параметр θ

Вы можете пропустить этот шаг, если уже читали его в части 1.

Этот шаг очень важен. Не паникуйте. Мы его взломаем.

θj - j-й параметр.

  • η - скорость обучения, мы установили ее равной 0,001 (1e-3).
  • n - количество выборок данных, в нашем случае их 20.
  • i - i-я выборка данных

Поскольку у нас есть три параметра, мы можем записать его в виде трех уравнений. Мы используем x3 для представления x1*x1.

Обозначение := такое же, как =. Вы можете найти объяснение здесь.

Самая сложная часть - это Σ (символ суммирования), поэтому я расширяю Σ для лучшего понимания.

Смотри внимательно.

Я раскрасил три части уравнения, потому что мы можем представить их в виде матриц. Посмотрите на красную и синюю часть в первом ряду, где мы обновляем theta 0.

Запишем красную часть и синюю часть как векторы-столбцы.

Поскольку у нас есть 20 выборок данных, размер f равен (20,1). Размер x0 равен (20,1). Мы можем написать матричное умножение с транспонированием.

Таким образом, размер должен быть (1, 20) x (20, 1) -> (1,). Получаем одну шкалу для обновления тета 0.

x1 и x2 также являются вектор-столбцом. И мы можем записать в них матрицу X.

А тета - это вектор-строка

Вернемся к уравнению.

Мы можем написать это как

Запишите это как одно уравнение.

Версия, подобная массиву Numpy, может быть простой для понимания.

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

θ: (1, 4) 
f^T: (1, 20) 
x: (20, 4)
dot production: (1, 20) x (20, 4) -> (1, 4)

Все кажется таким правильным. Напишем код. Собственно, всего две строчки.

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)
# standardization
mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)
def standardizer(x):
    return (x - mu) / sigma
std_x = standardizer(train_x)
# add x0 and x1^2 to get matrix
def to_matrix(x):
    x0 = np.ones([x.shape[0], 1]) 
    x3 = x[:, 0, np.newaxis] ** 2
    return np.hstack([x0, x, x3])
mat_x = to_matrix(std_x)
# sigmoid function
def f(x):
    return 1 / (1 + np.exp(-np.dot(x, theta)))
# update times
epoch = 2000
# learning rate
ETA = 1e-3
# update parameter
for _ in range(epoch):
    """
    f(mat_x) - train_y: (20,)
    mat_x: (20, 4)
    theta: (4,)
    
    dot production: (20,) x (20, 4) -> (4,)
    """
    theta = theta - ETA * np.dot(f(mat_x) - train_y, mat_x)

Что-то странное? Помните, что мы пишем перед кодом?

dot production: (1, 20) x (20, 4) -> (1, 4)
The dimension changes make sense here.

Но почему, когда мы пишем код, мы используем (20,) x (20, 4) -> (4,)?

На самом деле это не настоящая математическая нотация, это нотация Numpy. И если вы используете TensorFlow или PyTroch, вы должны быть с ними знакомы.

(20,) означает, что это одномерный массив с 20 числами. Это может быть вектор-строка или вектор-столбец, потому что он имеет только одно измерение. Если мы установим это как двумерный массив, например (20, 1) или (1, 20), мы можем легко определить, что(20, 1) - вектор-столбец, а (1, 20) - вектор-строка.

Но почему бы не задать размер явно, чтобы исключить двусмысленность?

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

Потому что это может сэкономить наше время!

Возьмем для примера (20,) x (20, 4) -> (4,). Если мы хотим получить (1, 20) x (20, 4) -> (1, 4), что нам нужно делать с (20,) x (20, 4) -> (4,)?

  • Преобразовать (20,) в (1, 20)
  • Вычислить (1, 20) x (20, 4) - ›(1, 4)
  • Поскольку (1, 4) - это двумерный вектор-столбец, нам нужно преобразовать его в одномерный массив. (1,4) - ›(4,)

Честно говоря, это расстраивает. Почему мы не можем выполнить их за один шаг?

Да, поэтому мы можем написать(20,) x (20, 4) -> (4,).

Хорошо, давайте посмотрим, как написано в документе numpy.dot ().

Numpy.dot (): если a является массивом ND, а b - одномерным массивом, это произведение суммы по последней оси а и б.

Хм, вообще-то я не понимаю сути. Но np.matmul () описывает аналогичные вычисления с преобразованием в (20,1) или (1,20) для выполнения стандартного двумерного матричного произведения. Может, нам удастся получить вдохновение.

Np.matmul (): Если первый аргумент - 1-D, он перемещается в матрицу, добавляя 1 к его размерам. После матричного умножения добавленная 1 удаляется.

Ха, это недостающая часть! В нашем случае (20,) становится (1, 20), потому что первое измерение (20,4) равно 20. И (1, 20) * (20, 4) -> (1, 4). Затем добавленная 1 удаляется, поэтому мы получаем (4,). Один шаг для всех.

8 Постройте линию

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

Мы сделаем некоторые точки данных как x1 и вычислим x2 на основе изученных нами параметров.

# plot line
x1 = np.linspace(-2, 2, 100)
x2 = - (theta[0] + x1 * theta[1] + theta[3] * x1**2) / theta[2]
plt.plot(std_x[train_y == 1, 0], std_x[train_y == 1, 1], 'o') # train data of class 1
plt.plot(std_x[train_y == 0, 0], std_x[train_y == 0, 1], 'x') # train data of class 0
plt.plot(x1, x2, linestyle='dashed') # plot the line we learned
plt.show()

9 Точность

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

import numpy as np
import matplotlib.pyplot as plt
# read data
data = np.loadtxt("linear_data.csv", delimiter=',', skiprows=1)
train_x = data[:, 0:2]
train_y = data[:, 2]
# initialize parameter
theta = np.random.randn(4)
# standardization
mu = train_x.mean(axis=0)
sigma = train_x.std(axis=0)
def standardizer(x):
    return (x - mu) / sigma
std_x = standardizer(train_x)
# add x0 and x1^2 to get matrix
def to_matrix(x):
    x0 = np.ones([x.shape[0], 1]) 
    x3 = x[:, 0, np.newaxis] ** 2
    return np.hstack([x0, x, x3])
mat_x = to_matrix(std_x)
# sigmoid function
def f(x):
    return 1 / (1 + np.exp(-np.dot(x, theta)))
# classify sample to 0 or 1
def classify(x): 
    return (f(x) >= 0.5).astype(np.int)
# update times
epoch = 2000
# learning rate
ETA = 1e-3
# accuracy log
accuracies = []
# update parameter
for _ in range(epoch):
    theta = theta - ETA * np.dot(f(mat_x) - train_y, mat_x)
    result = classify(mat_x) == train_y 
    accuracy = sum(result) / len(result) 
    accuracies.append(accuracy)
# plot accuracy line
x = np.arange(len(accuracies))
plt.plot(x, accuracies)
plt.show()
  • classify(x): если вероятность больше 0,5, мы классифицируем ее как True
  • result: содержит прогноз в виде списка [Ture, False,…]
  • accuracy = sum(result) / len(result): вычислить, сколько правильных выборок в прогнозе в текущую эпоху.

Наконец, мы строим линию точности.

Мы можем видеть, что линия становится устойчивой после 1000 эпох.

10 Резюме

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

Просмотрите другие мои сообщения на Medium с категориальным представлением!
GitHub: BrambleXu
LinkedIn: Xu Liang
Блог: BrambleXu