Раздел прогнозирования. Проект интернет-магазина

 Раздел прогнозирования. Введение и план

В этой лекции мы продолжим наш проект интернет-магазина, а точнее, рассмотрим процесс обработки данных в Python. Также мы очертим содержание данного раздела. Дело в том, что это большой раздел, и легко забыть, в чём его цель.

Напомним, что в наших более ранних курсах мы рассматривали логистическую регрессию для двоичной классификации, когда мы собирали некоторые данные и пытались спрогнозировать появление одной из двух возможных меток. К примеру, в качестве входных переменных мы могли взять время, проведённое на сайте, и количество просмотренных страниц, а прогнозировали, купит ли пользователь что-то или нет. Как вы помните, если задача имеет только две входные переменные, мы можем изобразить их графически на двухмерной плоскости, а затем провести прямую между двумя классами – скажем, «купит» и «не купит»:

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

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

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

С помощью нейронных сетей мы пойдём ещё дальше. Мы рассмотрим случай, когда данные не являются линейно разделяемыми, и потому логистическая регрессия не работает. К счастью, нейронные сети вполне подходят для решения таких задач.

Помните, что линейная функция имеет вид WTX; всё, что не может быть выражено в виде WTX, является нелинейным. Как вы увидите, нейронные сети также нелинейны. Мы знаем, что выражения вроде x2 или x3 также не являются линейными, но нейронные сети нелинейны очень специфическим образом. Как вы убедитесь, они могут стать нелинейными всего лишь при сочетании нескольких взятых вместе единиц логистической регрессии.

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

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

В моём предыдущем курсе по логистической регрессии мы обсуждали двоичную классификацию. Но что делать, если мы хотим одновременно классифицировать более двух вещей и если вместо того, чтобы просто прогнозировать «купит – не купит», мы хотим предсказать любое количество действий пользователя – покупка, начало оплаты, добавление в корзину, очистка корзины и так далее? Другим примером может быть классификация марки автомашины, основываясь на её изображении, ведь в мире существует куда больше двух марок машины, а потому здесь требуется классификатор, способный классифицировать более двух вещей.

Мы обсудим, как классифицировать более двух вещей в лекции, посвящённой функциям сигмоиды и softmax. Обратите внимание, что вы можете использовать функцию softmax и в логистической регрессии. И наоборот, если в нейронной сети вам нужна лишь двоичная классификация, то вы можете просто использовать сигмоиду.

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

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

Надо отметить, что в этом разделе никакие исходящие результаты нашей нейронной сети не будут иметь смысла. Напомним, что логистическая регрессия включает в себя набор весовых коэффициентов, а поскольку нейронная сеть состоит из множества логистических единиц, она также включает набор весовых коэффициентов. В случае логистической регрессии мы используем градиентный спуск для выяснения значений этих коэффициентов и называем этот процесс «обучением». Напомним также, что мы имеем дело с двумя основными операциями – прогнозированием и обучением. Этот раздел посвящён именно прогнозированию, тогда как следующий – обучению. Лишь после того, как мы научимся обучать нейронную сеть, результаты будут иметь смысл, но не ранее.

Не забывайте, что это типичные стадии машинного обучения. Сначала мы создаём «мозг», но этот мозг не слишком умён, поскольку ещё не обучен, и поэтому прогнозы будут не очень точными. Затем мы его обучаем, и лишь после обучения мозг будет способен давать точные прогнозы, причём именно потому, что мы попытались научить его этому.

Подготовка данных для интернет-магазина

Я уже загрузил библиотеки Numpy и Pandas.

import numpy as np

import pandas as pd

Для загрузки данных используем функцию

pd.read_csv, файл ecommerce_data.csv.

df = pd.read_csv(‘ecommerce_data.csv’)

Если вы хотите увидеть, что в файле, используйте команду

df.head()

Она показывает первые пять строк файла.

Итак, выйдем из оболочки и начнём работу с файлом процесса. Если вы не хотите писать код, а сразу его просмотреть, зайдите на github, соответствующий файл называется process.py. Прежде всего загрузим библиотеки Numpy и Pandas.

import numpy as np

import pandas as pd

Далее мы напишем функцию get_data. Она, во-первых, считывает данные из файла, как мы это сделали чуть ранее, а во-вторых, преобразует их в матрицы Numpy, поскольку так легче работать.

def get_data():

df = pd.read_csv(‘ecommerce_data.csv’)

data = df.as_matrix()

Далее нам необходимо разделить наши x и y. Y – это последний столбец, поэтому x у нас будет всеми остальными столбцами, за исключением последнего.

X = data[:, :-1]

Y = data[:, -1]

Далее, как мы говорили, необходимо нормализовать числовые столбцы. Для х1 это значение х1 минус среднее значение, поделённое на среднее отклонение. То же самое и для х2.

X[:,1] = (X[:,1] – X[:,1].mean()) / X[:,1].std()

X[:,2] = (X[:,2] – X[:,2].mean()) / X[:,2].std()

Перейдём теперь к работе над столбцом категорий, которым является время суток. Для этого возьмём форму оригинального х и на его основе создадим новый Х2 размерности Nx(D+3), поскольку у нас четыре категории. Обратите внимание, что большинство значений x будут совпадать.

N, D = X.shape

X2 = np.zeros((N, D+3))

X2[:,0:(D-1)] = X[:,0:(D-1)]

Теперь напишем код прямого кодирования для остальных четырёх столбцов. Сначала сделаем это простым способом. Для каждого наблюдения считываем значение времени суток, которое, как вы помните, принимает значение 0, 1, 2 и 3, и записываем это значение в Х2.

for n in xrange(N):

t = int(X[n,D-1])

X2[n,t+D-1] = 1

Есть и другой способ. Мы можем создать новую матрицу размерности Nx4 для четырёх столбцов, а затем проиндексировать её напрямую.

Z = np.zeros((N, 4))

Z[np.arange(N), X[:,D-1].astype(np.int32)] = 1

В таком случае нужно будет дописать ещё одну строку

X2[:,-4:] = Z

assert(np.abs(X2[:,-4:] – Z).sum() < 10e-10)

И конец функции.

return X2, Y

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

def get_binary_data():

X, Y = get_data()

X2 = X[Y <= 1]

Y2 = Y[Y <= 1]

return X2, Y2

Это всё, что касается подготовки данных.

Проект интернет-магазина. Создание прогнозов

 Соответствующий файл называется ann_predict.py.

Итак, прежде всего импортируем библиотеку numpy и, конечно же, загрузим наши данные.

import numpy as np

from process import get_data

Центральная задача этого файла – показать, как использовать прямое распространение в нейронной сети при обработке данных интернет-магазина и как делать прогнозы.

Итак, для начала загрузим данные – это всего лишь функция get_data.

X, Y = get_data()

Теперь случайным образом инициируем весовые коэффициенты нашей нейронной сети, поскольку мы ещё не знаем, как их обучать. Итак, создадим M = 5 единиц скрытого слоя, D у нас соответствует количеству признаков x, K – это количество уникальных значений Y и потому пробегает значения от 0 до K-1. W1 – матрица размерности DxM, b1 – вектор размерности M, W2 – матрица размерности MxK, b2 – вектор размерности K.

M = 5

D = X.shape[1]

K = len(set(Y))

W1 = np.random.randn(D, M)

b1 = np.zeros(M)

W2 = np.random.randn(M, K)

b2 = np.zeros(K)

Разумеется, нам понадобится функция софтмакс.

def softmax(a):

expA = np.exp(a)

return expA / expA.sum(axis=1, keepdims=True)

Далее идёт функция прямого распространения, в качестве аргументов использующие X, W1, b1, W2, b2. В качестве функции активации используем гиперболический тангенс.

def forward(X, W1, b1, W2, b2):

Z = np.tanh(X.dot(W1) + b1)

return softmax(Z.dot(W2) + b2)

Теперь мы можем получить собственно выход нейронной сети.

P_Y_given_X = forward(X, W1, b1, W2, b2)

predictions = np.argmax(P_Y_given_X, axis=1)

Запишем также функцию для вычисления точности – коэффициента классификации. Функция использует в качестве аргументов значения целевых переменных и полученных прогнозных значений. Сразу же выведем её на экран.

def classification_rate(Y, P):

return np.mean(Y == P)

print ‘’Score:’’, classification_rate(Y, predictions)

Запустим программу.

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

Проект интернет-магазина. Обучение нейронной сети

В данном разделе мы продолжим наш пример с интернет-магазином и рассмотрим файл ann_train.py. Впрочем, большую часть материала мы можем скопировать с предыдущего файла. Начало идентично, разве что количество скрытых единиц установим равным 5.

import numpy as np

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from process import get_data

def y2indicator(y, K)

N = len(y)

ind = np.zeros((N, K))

for i in xrange(N):

ind[i, y[i]] = 1

return ind

X, Y = get_data()

X, Y = shuffle(X, Y)

Y = Y.astype(np.int32)

M = 5

D = X.shape[1]

K = len(set(Y))

Преобразуем наши данные в учебный и проверочный наборы – это тоже можно скопировать из предыдущего файла. Как и тогда, первые 100 примеров будут учебным набором, остальные – проверочным.

Xtrain = X[:-100]

Ytrain = Y[:-100]

Ytrain_ind = y2indicator(Ytrain, K)

Xtest = X[-100:]

Ytest = Y[-100:]

Ytest_ind = y2indicator(Ytest, K)

Теперь случайным образом инициируем весовые коэффициенты. W1 будет размерности DxM, b1 – ряд нулей размерности M, у W2 размерность MxK, а b2 – это нули размерности K.

W1 = np.random.randn(D, M)

b1 = np.zeros(M)

W2 = np.random.randn(M, K)

b2 = np.zeros(K)

Функцию softmax мы можем скопировать полностью, а вот функцию forward надо немного дописать. Функции predict, classification_rate и cross_entropy можно также скопировать с предыдущего файла.

def softmax(a):

expA – np.exp(a)

return expA / expA.sum(axis=1, keepdims=True)

def forward(X, W1, b1, W2, b2):

Z = np.tanh(X.dot(W1) + b1)

return softmax(Z.dot(W2) + b2)

def predict(P_Y_given_X):

return np.argmax(P_Y_given_X, axis=1)

def classification_rate(Y, P):

return np.mean(Y == P)

def cross_entropy(T, pY):

return –np.mean(T*np.log(pY))

Теперь мы готовы к основному учебному циклу. Коэффициент обучения установим равным 0,001, количество циклов – 10 000.

train_costs = []

test_costs = []

learning_rate = 0.001

for i in xrange(10000):

pYtrain, Ztrain = forward(Xtrain, W1, b1, W2, b2)

pYtest, Ztest = forward(Xtest, W1, b1, W2, b2)

ctrain = cross_entropy(Ytrain_ind, pYtrain)

ctest = cross_entropy(Ytest_ind, pYtest)

train_costs.append(ctrain)

ctest.append(ctest)

Затем можем приступать к градиентному спуску.

W2 -= leraning_rate*Ztrain.T.dot(pYtrain – Ytrain_ind)

b2 -= learning_rate*(pYtrain – Ytrain_ind).sum()

dZ = (pYtrain – Ytrain_ind).dot(W2.T) * (1 – Ztrain*Ztrain)

W1 -= leraning_rate*Xtrain.T.dot(dZ)

b1 -= learning_rate*dZ.sum(axis=0)

if i % 1000 == 0:

print i, ctrain, ctest

После этого мы можем вывести на экран коэффициент классификации и значение функции затрат. Эту часть кода тоже можно просто скопировать из предыдущего примера.

print ‘’Final train classification_rate:’’, classification_rate(Ytrain, predict(pYtrain))

print ‘’Final test classification_rate:’’, classification_rate(Ytest, predict(pYtest))

legend1, = plt.plot(train_costs, label=’train cost’)

legend2, = plt.plot(test_costs, label=’test cost’)

plt.legend([legend1, legend2])

plt.show()

Запустим программу и посмотрим, что получится. Как и ожидалось, функция затрат для учебного набора несколько ниже, чем для проверочного. Коэффициент классификации равен 97% для учебного набора и 92% для проверочного. Это несколько лучше, чем в случае логистической регрессии.

Софтмакс

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

Ранее мы обсуждали только двоичную классификацию, для которой в окружающем мире также есть множество приложений. Например, мы могли взять в качестве входных данных влажность воздуха и земли, месяц года и географические координаты, и пытаться предсказать, будет ли дождь. В другом случае в качестве входных переменных мы могли бы взять данные о том, насколько кто-то занимается физическими упражнениями, его возраст и индекс массы тела, а также что он ест, и пытаться предсказать, заболеет ли человек определённой болезнью. Таким образом, как правило, мы отвечаем на вопросы в стиле «да/нет».

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

Другим примером является известная база данных MNIST, в которой необходимо категоризировать каждое изображение в качестве определённой цифры от 0 до 9.

Поговорим теперь о том, как нам расширить логистическую единицу, чтобы обрабатывать более двух классов. Напомним, что когда у нас два класса, нам нужна лишь одна исходящая переменная, поскольку она показывает вероятность появления одного из классов, а вероятность появления второго класса равна единице минус полученная вероятность:

P(Y= 0|X)=1-P(Y=1|X).

Но то же самое можно сделать и по-другому. Мы можем иметь два исходящих узла, результаты которых мы бы просто нормализовали, чтобы их сумма была равна единице.

На самом деле это именно то, чем мы и будем заниматься. Мы также должны подставить их значения в экспоненту, чтобы удостовериться, что результаты будут положительными.

В таком случае обнаружение результата с принадлежностью к классу 1, рассчитывается по формуле:

P(Y= 1|X)= \frac{e^a__1}{e^a__1 + e^a__2},

a_1 = w^T_1 x, a_2 = w^T_2 x,

а с принадлежностью к классу 2 – по формуле:

P(Y= 2|X)= \frac{e^a__2}{e^a__1 + e^a__2},

Обратите внимание, что весовые коэффициенты уже больше не векторы, как они были представлены в логистической регрессии, поскольку каждый входной узел должен быть подключён к каждому исходящему узлу. Тогда если у вас D входных узлов и два исходящих, то общее количество весовых коэффициентов будет равно 2*D, а сами значения весовых коэффициентов будут представлены матрицей размерности Dx2.

Заметьте, что такой способ представления очень легко расширить до K классов. В таком случае у нас получается новая матрица

размерности DxK. Мы вычисляем результат, возводя в экспоненту каждую исходящую переменную, а затем нормализуя их:

P(Y= k|X)= \frac{e^a__k}{Z},

Z = e^a__1+ e^a__2 + \; ....\; + e^a__k.

Получившаяся исходящая переменная, которую я обозначил через A, обычно называется активацией:

A_N_x_K = X_N_x_K W_N_x_K \rightarrow Y_N_x_K = softmax (A_N_x_K).

Обучение логистической регрессии с помощью софтмакс

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

import numpy as np

import matplotlib.pyplot as plt

Итак, у нас уже импортированы библиотеки Nympy и Matplotlib. Теперь нам нужно перемешать наши данные и, естественно, загрузить сами данные.

from sklearn.utils import shuffle

from process import get_data

Нам также нужна функция, преобразующая целевые переменные в матрицу показателей. Назовём её y2indicator.

def y2indicator(y, K)

N = len(y)

ind = np.zeros((N, K))

for i in xrange(N):

ind[i, y[i]] = 1

return ind

Теперь загружаем данные и перемешиваем их.

X, Y = get_data()

X, Y = shuffle(X, Y)

Y = Y.astype(np.int32)

D = X.shape[1]

K = len(set(Y))

Теперь разделим наши данные на учебный и проверочный наборы. Учебным набором будем считать первые 100 примеров.

Xtrain = X[:-100]

Ytrain = Y[:-100]

Ytrain_ind = y2indicator(Ytrain, K)

Xtest = X[-100:]

Ytest = Y[-100:]

Ytest_ind = y2indicator(Ytest, K)

Следующее – инициализация весовых коэффициентов. W пусть будет иметь гауссово распределение с размерностью DxK, а b – набором нулей размерности K.

W = np.random.randn(D, K)

b = np.zeros(K)

Далее определим функции softmax, forward, predict, classification_rate и cross_entropy.

def softmax(a):

expA – np.exp(a)

return expA / expA.sum(axis=1, keepdims=True)

def forward(X, W, b):

return softmax(X.dot(W) + b)

def predict(P_Y_given_X):

return np.argmax(P_Y_given_X, axis=1)

def classification_rate(Y, P):

return np.mean(Y == P)

def cross_entropy(T, pY):

return –np.mean(T*np.log(pY))

Справившись со всем этим, мы можем заняться нашим учебным циклом и запустить градиентный спуск. Коэффициент обучения установим равным 0,001, количество циклов – равным 10 000, причём после каждых 1 000 циклов будем выводить на экран значение функции затрат.

train_costs = []

test_costs = []

learning_rate = 0.001

for i in xrange(10000):

pYtrain = forward(Xtrain, W, b)

pYtest = forward(Xtest, W, b)

ctrain = cross_entropy(Ytrain_ind, pYtrain)

ctest = cross_entropy(Ytest_ind, pYtest)

train_costs.append(ctrain)

test_costs.append(ctest)

W -= leraning_rate*Xtrain.T.dot(pYtrain – Ytrain_ind)

b -= learning_rate*(pYtrain – Ytrain_ind).sum(axis=0)

if i % 1000 == 0:

print i, ctrain, ctest

И наконец мы можем вывести на экран учебный и проверочный коэффициенты классификации.

print ‘’Final train classification_rate:’’, classification_rate(Ytrain, predict(pYtrain))

print ‘’Final test classification_rate:’’, classification_rate(Ytest, predict(pYtest))

Закончив всё это, подпишем для удобства графики.

legend1, = plt.plot(train_costs, label=’train cost’)

legend2, = plt.plot(test_costs, label=’test cost’)

plt.legend([legend1, legend2])

plt.show()

Запустим программу. Итак, как мы видим, наша функция затрат учебного набора несколько меньше функции затрат проверочного набора. Точность у нас получилась равной 93% для учебного набора и равной 86% – для проверочного набора. Это несколько хуже, чем в случае двоичной классификации.

Софтмакс в коде

В этой лекции я покажу вам, как реализовать функцию софтмакс в коде.

Итак, прежде всего надо импортировать библиотеку Numpy.

import numpy as np

Давайте теперь представим, что у нас есть исходящие переменные нейронной сети или функция активации последнего слоя.

a = np.random.randn(5)

Итак, теперь у нас есть 5 различных чисел, представляющих функцию активации в пяти разных узлах исходящего слоя, и теперь нам надо использовать софтмакс на этих числах. Как вы помните, первое, что нужно сделать, – подставить их в экспоненту. Так и сделаем.

expa = np.exp(a)

Не забывайте, что все функции Numpy обрабатываются поэлементно, так что у нас есть сразу все экспоненты.

Теперь нам нужно полученные числа поделить на их сумму.

answer = expa / expa.sum()

Это и есть результат функции софтмакс. Вы можете убедиться, что все эти числа являются вероятностями, поскольку их сумма равна единице.

answer.sum()

К