Машинное обучение с нуля: часть 1

В этой статье мы построим самую базовую модель машинного обучения, называемую линейной регрессией, и реализуем ее, используя только python NumPy. Сначала мы рассмотрим наш набор данных, а затем поговорим об общем процессе контролируемого обучения, за которым следует представление гипотез, функция потерь и алгоритм градиентного спуска.
После этого мы напишем LinReg класс и протестируем его на наших данных.
Серия "Машинное обучение с нуля" -
- Часть 1: Линейная регрессия с нуля в Python
- Часть 2: Локально взвешенная линейная регрессия в Python
- Часть 3: Нормальное уравнение с использованием Python: решение в закрытой форме для линейной регрессии
- Часть 4: Полиномиальная регрессия с нуля в Python
Линейная регрессия
Проще говоря, у нас есть набор данных с X (функции) и y (метки), и мы хотим провести к нему прямую линию. Давайте возьмем следующие данные в качестве мотивирующего примера:
Предположим, вы изучаете физику и хотите знать, сколько вы можете набрать на тесте (из 100), учитывая количество часов, которые вы занимаетесь.
# Imoprting required libraries.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# Reading the csv file.
df = pd.read_csv('data.csv')
# Displayinng the first five elements of the dataframe.
df.head(10)

В этих данных Часы - это X (функция) (помните, что у нас есть только одна функция в этих данных, то есть часы, хотя может быть несколько функций, например, сколько проблем вы решили для практики и т. Д.). Оценки равны y (метки / цель) для каждого имеющегося у нас примера. Вот как выглядят данные, когда мы их строим.
# Taking the Hours and Scores column of the dataframe as X and y
# respectively and coverting them to numpy arrays.
X = np.array(df['Hours']).reshape(-1,1)
y = np.array(df['Scores'])
# Plotting the data X(Hours) on x-axis and y(Scores) on y-axis
plt.figure(figsize=(8,6)) # figure size
plt.scatter(X, y)
plt.title('Hours vs Scores')
plt.xlabel('X (Input) : Hours')
plt.ylabel('y (Target) : Scores')

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

Во-первых, у нас есть обучающий набор (данные), такой как в таблице 1. Мы передаем этот обучающий набор в алгоритм обучения. Задача алгоритма обучения состоит в том, чтобы вывести функцию h(x) или функцию гипотезы, которая затем используется для прогнозирования (y: оценка) для заданных входных данных X (количество часов).
Первый вопрос: как представить гипотезу h (x)?
Итак, поскольку нам нужна прямая линия, мы скажем, что наша гипотеза будет:
h(x) = wX + b
w → веса, b → смещение
В коде я буду называть h(x) как y_hat.
y_hat = np.dot(X, weights) + bias
В приведенном выше примере мы можем представить данные в двух измерениях, поэтому мы можем сказать, что weights будет наклоном линии, а bias будет точкой пересечения по оси Y. Но если бы у нас было две функции в наших данных, а не только одна, то мы бы представляли данные в 3-х измерениях, и нам потребовалась бы плоскость для размещения данных в 3-х мерном пространстве. Итак, наша гипотеза будет представлять собой плоскость, а не прямую линию. По мере увеличения количества функций размеры наших weights и bias увеличиваются.
weights и bias - векторы, а размеры w и b равны количеству объектов. weights и bias также называются параметрами алгоритма обучения.
Наша цель - найти такие значения weights и bias, чтобы h(x) было как можно ближе к y.
Обозначения -
n → количество функций (1 в нашем примере, это часы)
m → количество обучающих примеров (в нашем примере 25)
X → Возможности
y → метки / цель
(X(i), y(i)) → i-й пример в Обучающем наборе
Представление X, y, w и b
X - матрица размера (m, n); строки представляют собой обучающие примеры, а столбцы представляют функции.
y - матрица размера (m, 1); каждая строка является меткой для соответствующего набора функций в X.
w - это вектор размера (n, 1), а параметр b - это скаляр, который может транслироваться. Ознакомьтесь с Трансляция в NumPy
Если бы у нас было 3 функции и 3 обучающих примера, то представление выглядело бы так (m = 3, n = 3) -

Нижний индекс - это i-й обучающий пример, а верхний индекс - это j-я функция.

Следующий вопрос: как выбрать параметры w и b?
Итак, давайте выберем w и b так, чтобы h(x) было как можно ближе к y. Итак, мы хотим выбрать такие параметры, чтобы, по крайней мере, для часов, оценка которых нам известна, алгоритм обучения выдавал оценки, близкие к тем, которые мы знаем в обучающем наборе.
Функция потерь
Итак, мы определяем функцию потерь как среднеквадратичную ошибку -

loss = np.mean((y_hat - y)**2)
И мы хотим найти значения w и b, которые минимизируют функцию потерь. h(x) или y_hat - это значение, предсказываемое алгоритмом, а y - истинное значение. Функция потерь - это мера того, насколько мы близки к истинному / целевому значению или, в целом, это мера того, насколько хорошо работает алгоритм. Чем меньше убыток, тем лучше.
Снова цель состоит в том, чтобы найти weights и bias так, чтобы минимизировать функцию потерь.
Буква J или L может использоваться для обозначения функции потерь. Функция потерь также называется функцией затрат, эти термины могут использоваться как синонимы.
Последний вопрос: как минимизировать функцию потерь?
Это делается с помощью алгоритма, называемого градиентным спуском.
Интуиция для градиентного спуска

Здесь theta0 то же самое, что bias, и theta1 то же самое, что weights.
Допустим, у нас есть функция затрат / потерь J(theta0, theta1), и мы хотим найти значения для theta0 и theta1, которые минимизируют J. Итак, при градиентном спуске мы начинаем в какой-то момент с инициализации theta0 и theta1 случайным образом или значением всех нулей, и что мы делаем, перед каждым шагом мы оглядываемся на все 360 градусов вокруг нас и смотрим, должны ли мы сделать небольшой шаг. , в каком направлении мы должны сделать этот шаг, чтобы спуститься как можно быстрее, потому что это самая низкая точка функции, где мы найдем минимум J и, таким образом, получим оптимальные значения для theta0 и theta1. На каждом этапе градиентный спуск будет искать самый крутой спуск.
Алгоритм градиентного спуска
Сначала мы инициализируем weights и bias случайным образом или вектором всех нулей.
# Initializing weights as a matrix of zeros of size: (number of # features: n, 1) and bias as 0 weights = np.zeros((n,1)) bias = 0
Затем мы продолжим изменять / обновлять weights и bias, чтобы уменьшить функцию потерь L.
Правило обновления для градиентного спуска

# Updating the parameters: parameter := parameter - lr*(derivative # of loss/cost w.r.t parameter) weights -= lr*dw bias -= lr*db
lr or alpha → Скорость обучения. Он определяет, насколько большой или маленький шаг вы сделаете при спуске. Более низкая скорость обучения означает меньшие шаги и наоборот.

На приведенном выше рисунке показан параметр w, который спускается вниз, чтобы достичь минимального значения J. w, где J минимально, является оптимальным w.
Градиент / производная weights w.r.t Loss
Ограничение «y» в приведенной ниже формуле такое же, как y_hat или h(x).

dw = (1/m)*np.dot(X.T, (y_hat - y))
Градиент / производная bias w.r.t Loss

db = (1/m)*np.sum((y_hat - y))
Если вы знаете достаточно вычислений, вы можете взять частную производную от убытка (заменить y_hat в убытке) с параметрами weights и bias, вы получите тот же результат.
Резюме линейной регрессии
Сначала получите данные и разделите их на X (функции) и y (метки). Затем инициализируйте параметры случайным образом или все нули, как это -
weights = np.zeros((n,1)) # n: number of features bias = 0
После этого,
1. Вычислите y_hat или h (x).
y_hat = np.dot(X, weights) + bias
2. Рассчитайте градиенты потерь по параметрам (weights и bias)
dw = (1/m)*np.dot(X.T, (y_hat - y)) db = (1/m)*np.sum((y_hat - y))
3. Обновите параметры weights и bias.
weights -= lr*dw bias -= lr*db
4. Повторите вышеуказанные шаги. Количество повторений - это количество шагов, которые вы делаете при спуске. В машинном обучении это также называется epochs (или количество итераций).
Класс линейной регрессии
Объясняется комментариями (#).
# Linear Regression class
class LinReg:
# Initializing lr: learning rate, epochs: no. of iterations,
# weights & bias: parameters as None
# default lr: 0.01, epochs: 800
def __init__(self, lr=0.01, epochs=800):
self.lr = lr
self.epochs = epochs
self.weights = None
self.bias = None
# Training function: fit
def fit(self, X, y):
# shape of X: (number of training examples: m, number of
# features: n)
m, n = X.shape
# Initializing weights as a matrix of zeros of size: (number
# of features: n, 1) and bias as 0
self.weights = np.zeros((n,1))
self.bias = 0
# reshaping y as (m,1) in case your dataset initialized as
# (m,) which can cause problems
y = y.reshape(m,1)
# empty lsit to store losses so we can plot them later
# against epochs
losses = []
# Gradient Descent loop/ Training loop
for epoch in range(self.epochs):
# Calculating prediction: y_hat or h(x)
y_hat = np.dot(X, self.weights) + self.bias
# Calculting loss
loss = np.mean((y_hat - y)**2)
# Appending loss in list: losses
losses.append(loss)
# Calculating derivatives of parameters(weights, and
# bias)
dw = (1/m)*np.dot(X.T, (y_hat - y))
db = (1/m)*np.sum((y_hat - y))
# Updating the parameters: parameter := parameter - lr*derivative
# of loss/cost w.r.t parameter)
self.weights -= self.lr*dw
self.bias -= self.lr*db
# returning the parameter so we can look at them later
return self.weights, self.bias, losses
# Predicting(calculating y_hat with our updated weights) for the
# testing/validation
def predict(self, X):
return np.dot(X, self.weights) + self.bias
Теперь давайте протестируем класс LinReg в нашем наборе данных.
Обучение
X_train, X_test, y_train, y_test = X[:20], X[20:], y[:20], y[20:] model = LinReg(epochs=100) w, b, l = model.fit(X_train,y_train)
Построение результатов
# Plotting our predictions.
fig = plt.figure(figsize=(8,6))
plt.scatter(X, y)
plt.plot(X, model.predict(X)) # X and predictions.
plt.title('Hours vs Percentage')
plt.xlabel('X (Input) : Hours')
plt.ylabel('y (Target) : Scores')

# Predicting on the test set.
X_test_preds = model.predict(X_test)
X_test_preds
>> array([[28.05459243],
[48.44728266],
[38.73647779],
[68.83997288],
[77.57969726]])
Сравнение истинных значений с прогнозами модели.
# Comparing True values to our predictions.
Compare_df = pd.DataFrame({'Actual':y_test,Predicted':X_test_preds})
Compare_df

Потери по сравнению с эпохами (количество итераций).
fig = plt.figure(figsize=(8,6))
plt.plot([i for i in range(100)], l, 'r-')
plt.xlabel('Number of iterations')
plt.ylabel('Loss / Cost')

Мы видим, что по мере того, как мы делаем каждый шаг, потери уменьшаются и уменьшаются, что означает, что мы делаем все хорошо и идем в правильном направлении (то есть вниз) в ландшафте функции потерь.
Это все, что касается линейной регрессии. Немного поиграйте с кодом и попробуйте его с более крупными наборами данных. Спасибо за прочтение.
Скоро появятся новые статьи о машинном обучении с нуля.
По вопросам, комментариям, проблемам обращайтесь ко мне в разделе ответов.
Серия "Машинное обучение с нуля" -
- Часть 1: Линейная регрессия с нуля в Python
- Часть 2: Локально взвешенная линейная регрессия в Python
- Часть 3: Нормальное уравнение с использованием Python: решение в закрытой форме для линейной регрессии
- Часть 4: Полиномиальная регрессия с нуля в Python