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

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

После этого мы напишем LinReg класс и протестируем его на наших данных.

Серия "Машинное обучение с нуля" -

Линейная регрессия

Проще говоря, у нас есть набор данных с 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')

Мы видим, что по мере того, как мы делаем каждый шаг, потери уменьшаются и уменьшаются, что означает, что мы делаем все хорошо и идем в правильном направлении (то есть вниз) в ландшафте функции потерь.

Это все, что касается линейной регрессии. Немного поиграйте с кодом и попробуйте его с более крупными наборами данных. Спасибо за прочтение.

Скоро появятся новые статьи о машинном обучении с нуля.

По вопросам, комментариям, проблемам обращайтесь ко мне в разделе ответов.

Серия "Машинное обучение с нуля" -