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

В этой части 2 мы реализуем линейную регрессию с несколькими переменными для прогнозирования цен на дома. У нас уже есть набор данных, содержащий цены на жилье в Портленде, штат Орегон. В наборе данных всего три столбца: первый — размер дома (в квадратных футах), второй — количество спален, а третий — цена дома. Мы будем использовать этот набор данных для обучения нашей модели линейной регрессии.

Шаг 1. Знакомство с данными

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

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
data = pd.read_table('ex1data2.txt', header = None, sep=',')
plotdata = [data.iloc[:,0], data.iloc[:,1] , data.iloc[:,2]]
fig = plt.figure()
titles = {221: 'House_Size', 222: 'Bedroom', 223: 'Price'}
for i in range(3):
    ax = fig.add_subplot(list(titles.keys())[i])
    ax.boxplot(plotdata[i])
    ax.title.set_text(list(titles.values())[i])

Ниже приведен вывод. Мы можем видеть разные диапазоны каждой характеристики: размер дома (в диапазоне от 1000 до 3000) и нет. спален (в диапазоне от 2 до 5). Размеры дома примерно в 1000 раз превышают количество спален. В таком случае нам нужно выполнить нормализацию признаков, чтобы помочь градиентному спуску сходиться намного быстрее.

Шаг 2: Нормализация признаков

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

def FeatureNorm(x):
    x_norm = x
    mu    = np.zeros((1,x.shape[1])) # 2D array of shape(1,2)
    sigma = np.zeros((1,x.shape[1])) # std.deviation
# for each feature dimension, we will compute mean and std.dev.
# next, we will subtract mean from each entry of that feature and divide by std. dev.
for i in range(x.shape[1]):
        mu[0][i] = x[:,i].mean()
        sigma[0][i] = x[:,i].std()
        x_norm[:,i] = (x[:,i] - mu[0][i])/sigma[0][i]
    
    return x_norm, mu, sigma

Теперь давайте нормализуем наши два столбца характеристик (размер дома и количество спален).

m  = len(data.iloc[:,2])   # number of training examples
x_ = data.iloc[:,:2].to_numpy(dtype=float)
y  = data.iloc[:,2].to_numpy().reshape(m,1)
x_, mu, sigma = FeatureNorm(x_)
# let's see first 5 entries of our normalized feature columns.
print(x_[:5])

# add the bias term
x = np.concatenate((np.ones((m,1)), x_), axis = 1)

Шаг 3: Функции стоимости и градиентного спуска

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

# function to calculate the cost J while we perform the gradient descent
def J(x,y,theta):
    m = len(y)
    x_prime = np.transpose(x)
    theta_prime = np.transpose(theta)
    y_prime = np.transpose(y)
    
    value = np.sum(np.power(np.dot(theta_prime, x_prime) - y_prime,2))
    J = value/ (2 * m)
    return J

Приведенная ниже функция градиентного спуска будет принимать значения ϴ, x, y, скорость обучения α и количество итераций и обновлять ϴ небольшими шагами. При вызове эта функция выводит каждое значение J и ϴ для каждой итерации (это делается исключительно для проверки, чтобы убедиться, что J уменьшается на каждой итерации и может быть опущена) и возвращает окончательные значения ϴ.

def gradient_des(x,y, theta, alpha, num_iters):
    m = len(y) 
    J_history = np.zeros((num_iters,1)) 
    x_prime = np.transpose(x)
    y_prime = np.transpose(y)
    
    for i in range(num_iters):
        theta_p = np.transpose(theta)
        
        term1 = np.dot(theta_p, x_prime) - y_prime
        term2 = np.dot(term1, x)
        term3 = np.transpose(term2)
        theta = theta - (alpha/m) * term3
        
        J_history[i] = J(x,y,theta)
        print("Theta = {}, J = {}, for iteration {}".format(theta, J_history[i],i))
    return theta, J_history

Давайте проверим наши функции, установив начальные тета равными нулю со скоростью обучения 0,1 и повторив ее 1000 раз.

alpha = 0.1
iteration = 1000
theta_start = np.zeros((3,1))
theta, J_history = gradient_des(x,y,theta_start,alpha,iteration)
print("Theta computed from gradient descent: {}, {} and {}".format(theta[0], theta[1], theta[2]))

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

# Let's predict the price of a 1650 sq-ft, 3 br house
x1 = np.array([1, 1650, 3], dtype = float).reshape(1,3)
# feature normalization
x1[0][1] = (x1[0][1] - mu[0][0])/sigma[0][0]
x1[0][2] = (x1[0][2] - mu[0][1])/sigma[0][1]
price = float(np.dot(x1, theta))
print("Predicted price of a 1650 sq-ft, 3 br house (using gradient descent): {}".format(price))

Выбор скорости обучения α

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

Приведенный ниже код вызовет нашу функцию градиента_des() и запустит ее на 50 итераций со скоростью обучения, выбранной пользователем. В конце цикла он будет отображать значения J в зависимости от количества итераций. Мы можем попробовать значения альфа на логарифмической шкале с мультипликативными шагами, примерно в 3 раза превышающими предыдущее значение (т. е. 0,3, 1, 0,03, 0,01 и т. д.).

ПРИМЕЧАНИЕ. При небольшой скорости обучения градиентному спуску требуется очень много времени, чтобы достичь оптимального значения. И наоборот, если скорость обучения слишком велика, J(theta) может расходиться и увеличиваться, что приводит к слишком большим значениям для компьютерных вычислений.

alpha = float(input("Please enter the learning rate"))
iteration = 50
theta_start = np.zeros((3,1))
theta, J_history = gradient_des(x,y,theta_start,alpha,iteration)
plt.plot(range(iteration), J_history, '-b', linewidth = 2 )
plt.xlabel("Number of iterations")
plt.ylabel("Cost J")

Чтобы получить представление, ниже приведены разные графики с разными значениями альфы.

Итак, вот как мы можем выбрать скорость обучения, чтобы помочь градиентному спуску быстро сойтись.

Использование нормального уравнения

Альтернативный способ найти оптимальные тета-значения — использовать нормальное уравнение в закрытой форме, которое не требует масштабирования признаков и циклов до сходимости. Уравнение:

def normalEqn(x,y):
    theta = np.zeros((x.shape[1], 1)) 
    x_prime = np.transpose(x)
    term1 = np.linalg.inv(np.dot(x_prime,x))
    term2 = np.dot(term1, x_prime)
    theta = np.dot(term2, y)
    return theta
data = pd.read_table('ex1data2.txt', header = None, sep=',')
m = len(data)
x_ = data.iloc[:,:2].to_numpy(dtype=float)
x = np.concatenate((np.ones((m,1)), x_), axis = 1)
y  = data.iloc[:,2].to_numpy().reshape(m,1)
theta = normalEqn(x,y)
print("Theta computed from the normal equations: {}, {}, and {}".format(theta[0], theta[1], theta[2]))

Да, это все шаги по построению модели линейной регрессии для данных с более чем одним признаком (здесь мы попробовали как градиентный спуск, так и нормальное уравнение). Кроме того, мы узнали, как выбрать подходящую скорость обучения. Часть 3 будет посвящена модели логистической регрессии.

До встречи. :)

Продолжай учиться. Наслаждайся путешествием!