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

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

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

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

Контент структурирован следующим образом. Это выглядит немного длинным,

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

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

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

x1,x2,y
153,432,0
220,262,0
118,214,0
474,384,1
485,411,1
233,430,0
396,321,1
484,349,1
429,259,1
286,220,1
399,433,0
403,300,1
252,34,1
497,372,1
379,416,0
76,163,0
263,112,1
26,193,0
61,473,0
420,253,1

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

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]
# plot
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 Линейная разделимая задача

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

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

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

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]
theta = np.random.randn(2)
def f(x):
    return theta[0] + theta[1] * x

3 Векторное представление

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

Здесь θ и x - все вектор-столбцы.

Причина, по которой мы используем транспонирование θ, заключается в том, что мы можем использовать матричное умножение.

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

import numpy as np
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(2)
# dot product
def f(x):
    return np.dot(theta, x)

Вы можете спросить, почему мы не пишем np.dot(theta.T, x)? Поскольку в документе говорится: Если оба вектора являются одномерными массивами, это внутреннее произведение векторов (без комплексного сопряжения). Итак, np.dot(theta, x) сделайте то же самое, что и np.dot(theta.T, x).

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

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

  • 𝜇 - среднее значение в каждом столбце
  • 𝜎 - стандартное отклонение в каждом столбце
import numpy as np
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(2)
# 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)
# dot product
def f(x):
    return np.dot(theta, x)

5 Добавить предвзятость

Нам нужно добавить член смещения к нашей функции, чтобы наша модель имела лучшее обобщение. Таким образом, мы увеличиваем параметр с 2 до 3. И добавляем константу x0 = 1, чтобы выровнять векторное представление.

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

import numpy as np
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(3)
# 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)
# get matrix
def to_matrix(std_x):
    return np.array([[1, x1, x2] for x1, x2 in std_x])
mat_x = to_matrix(std_x)
# dot product
def f(x):
    return np.dot(x, theta)

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

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

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

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

Мы используем 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(3)
# 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)
# get matrix
def to_matrix(std_x):
    return np.array([[1, x1, x2] for x1, x2 in std_x])
mat_x = to_matrix(std_x)
# sigmoid function
def f(x):
    return 1 / (1 + np.exp(-np.dot(x, theta)))

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

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

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

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

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

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

  • 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 самое важное уравнение - это. Если вы не можете понять, как это получить, ничего страшного. Все, что нам нужно сделать, это написать его как настоящий код.

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

Шаг 8 немного длиннее, но очень важен. Не паникуйте. Мы его взломаем.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Пишите как одно уравнение.

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

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

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

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

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(3)
# 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)
# get matrix
def to_matrix(std_x):
    return np.array([[1, x1, x2] for x1, x2 in std_x])
mat_x = to_matrix(std_x)
# dot product
def f(x):
    return np.dot(x, theta)
# 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, 3)
    theta: (3,)
    
    dot production: (20,) x (20, 3) -> (3,)
    """
    theta = theta - ETA * np.dot(f(X) - train_y, mat_x)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# plot line
x1 = np.linspace(-2, 2, 100)
x2 = - (theta[0] + x1 * theta[1]) / 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()

10 Резюме

Поздравляю! Я рада, что тебе это удалось. Надеюсь, моя статья будет вам полезна. Вы можете найти весь код ниже. Оставляйте комментарии, чтобы я знал, легко ли понять мою статью. Следите за моей следующей статьей о нелинейной разделимой проблеме.

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