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

Большой разрыв между инженером и инженером по машинному обучению - это способность преобразовывать математические уравнения в реальный код. Иногда нам действительно нужно реализовать некоторые базовые концепции с нуля, чтобы лучше понять магию за кулисами, а не просто импортировать библиотеку без дальнейшего понимания.
Поэтому я решил написать несколько статей, чтобы объяснить, как преобразовать математическое уравнение в реальный код. Это часть 1, и я приведу пример классификации с использованием логистической регрессии для линейной разделяемой задачи. И я постараюсь сделать объяснение максимально простым.
Контент структурирован следующим образом. Это выглядит немного длинным,
- Посмотрите на данные
- Линейная разделимая задача
- Векторное представление
- Стандартизация
- Добавить предвзятость
- Сигмовидная функция
- Функция правдоподобия
- Обновить параметр θ
- Постройте линию
- Резюме
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