Подробное объяснение математического уравнения для создания практических математических основ для вашего пути к машинному обучению или глубокому обучению.
Большой разрыв между инженером и инженером по машинному обучению - это способность преобразовывать математические уравнения в реальный код. Иногда нам действительно нужно реализовать некоторые базовые концепции с нуля, чтобы лучше понять магию за кулисами, а не просто импортировать библиотеку без дальнейшего понимания.
Поэтому я решил написать несколько статей, чтобы объяснить, как преобразовать математическое уравнение в реальный код. Это часть 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