Machine learning
NewTechAudit
320

Машинное обучение: с чего начать, или как построить первую модель

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

В закладки

В качестве первой задачи для машинного обучения возьмем что-то понятное и простое, например прогноз стоимости жилья. Готовый датасет можно найти на сайте kaggle. На первых шагах обучения не стоит брать датасеты с большим количеством переменных, например «House Prices: Advanced Regression Techniques» состоит из 80 переменных и advanced regression, остановимся на «House Sales in King County, USA» с 21 параметром.

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

Итак, мы выбрали задачу и готовы приступить к ее решению. Решение будет включать два этапа: анализ данных и построение моделей.

1. Работа с данными

Сделаем отступление и отдельно отметим важность анализа данных. В настоящий момент все более-менее популярные алгоритмы уже написаны в виде библиотек и непосредственное построение модели сводится к нескольким строкам кода, например, k-ближайших соседей из sklearn в python:

from sklearn .neighbors import KNeighborsClassifier clf_KNN = KNeighborsClassifier() #Создаем модель clf_KNN.fit(X_train, Y_train) #Обучаем модель Y_KNN = clf_KNN.predict(X_test) #Предсказываем значения для выборки

Всего четыре строчки кода для получения результата. Так в чем же сложность? Сложность заключается в получении того самого X_train — данных, которые подаются на вход модели. Известный принцип «мусор на входе» = «мусор на выходе» (англ. garbage in — garbage out (GIGO)) в моделировании работает более чем на 100%, и именно от работы с данными во многом будет зависеть качество полученного решения задачи машинного обучения.

А теперь в бой!

Для анализа данных мы будем использовать pandas, для понимания и оценки «на глаз» используем простые графики из seaborn.

Импортируем библиотеки, читаем данные, выведем несколько записей из массива данных, посмотрим на типы данных и пропуски в них.

Код и Out

import pandas as pd import matplotlib.pyplot as plt import seaborn as sns df = pd.read_csv('…/train.csv') df.head(5)
df.info()

Массив данных состоит из 21 613 записей без пропусков в данных и содержит только одно текстовое поле date.

С каждым признаком поработаем подробнее и начнем с самого простого — откинем id (не несет полезной информации), zipcode (код зоны, где расположен дом) и координаты (lat & long), так как мы только знакомимся c machine learning, а корректное преобразование географических данных слишком специфично для начинающего специалиста.

df=df.drop(['id','zipcode','lat','long'], axis=1)

Теперь посмотрим на дату объявления. Формат даты задан yyyymmddt000000, в целом ее тоже можно было бы удалить из датасета, но у нас есть поля год постройки (yr_built) и год последнего ремонта (yr_renovated), которые заданы в в формате года (YYYY), что не очень информативно.

Оперируя датой объявления, можно преобразовать год в возраст вычитанием (год объявления — год постройки / год ремонта). Отметим по части домов год ремонта стоит 0, и, предположив, что это означает отсутствие ремонта с постройки, заменим нули в году ремонта на год постройки, предварительно убедившись, что в данных отсутствуют некорректные записи, где год ремонта меньше года постройки:

df[(df['yr_renovated']<df['yr_built'])&df['yr_renovated']!=0]
df.loc[df['yr_renovated']==0, ['yr_renovated']]=df['yr_built'] df['yr_built']=df['date'].str[0:4].astype(int)-df['yr_built'] df['yr_renovated']=df['date'].str[0:4].astype(int)-df['yr_renovated'] df=df.drop('date', axis=1) df.head(5)

Следующим параметром проанализируем цену и воспользуемся для этого «Ящиком с усами» (Box plot). Ящик с усами — простой и удобный график, показывающий одномерное распределение вероятностей, или, проще говоря, концентрацию данных.

Отрисовывает медиану (линия в центре), верхний и нижний квартили (стороны ящика), края статистически значимой выборки («усы») и выбросы (точки за «усами»). Легко понять по картинке на нормальном распределении (справа).

График позволяет быстро оценить где располагается большая часть данных (50% находятся внутри ящика), их симметричность (смещение медианы к одной из сторон ящика и/или длина «усов») и степень разброса — дисперсию (размеры ящика, размеры усов и количество точек-выбросов).

Можно построить распределение только этого признака по всему массиву, но информативнее будет использовать две оси, например цену и количество спален, что в свою очередь также покажет наличие связи между признаками:

sns.boxplot(y='price', data=df) #только price sns.boxplot(y='price', x='bedrooms', data=df) #price & bedrooms

Out price & bedrooms:

Из графика сразу видно наличие экстремальных значений price и bedrooms (только представьте дом с 33 спальнями! J). Наличие таких значений (иначе называемых как выбросы) в целевом признаке price часто приводит к переобучению модели, так именно они будут давать большую ошибку, которую алгоритмы стараются минимизировать.

Из графика видно, что большая часть (если посчитать — 93,22%) лежит в диапазоне 0-1млн, а свыше 2 млн — всего 198 значений (0,92%). От 1% датасета можно избавиться практически безболезненно, поэтому вызвав простой просмотр 217 записей предварительно отсортировав по цене, увидим искомую отметку price в 1 965 000 и удалим все, что выше этой цены.

df.sort_values (by='price', ascending=False).head(217) df=df[df['price']<=1965000]

Подумаем немного над признаком bedrooms. Мы видим 13 домов с bedrooms = 0, а также странную запись о доме с 33 bedrooms. Поступим также как и с price, удалив нули из bedroms (а заодно и bathrooms):

df=df[(df['bedrooms']!=0)&(df['bathrooms']!=0)]

Касательно дома с 33 спальнями — учитывая цену, можно предположить что это опечатка и спален на самом деле 3. Сравним жилую площадь этого дома (1620) со средней жилой площадью домов с 3 спальнями (1798,2), что ж, вероятно, наша догадка верна, поэтому просто изменим это значение на 3 и еще раз построим предыдущий box plot:

df.loc[df['bedrooms']==33,['bedrooms']]=3 sns.boxplot(y='price', x='bedrooms', data=df)

Что ж, значительно лучше. Аналогично bedrooms посмотрим и на bathrooms. Нулевые значения мы удалили, другие экстремальные значения в поле отсутствуют:

sns.boxplot(y='bathrooms', x='bedrooms', data=df)

В полях sqft_living, floors, waterfront, view, condition, grade, sqft_living15 также все значения более-менее реальны, их трогать не будем:

plt.rcParams['figure.figsize']=2,3 #размер картинки sns.boxplot(y='sqft_living', data=df) sns.boxplot(y='floors',color='#2ecc71', data=df) sns.boxplot(y='sqft_living15',color='#9b59b6', data=df) plt.rcParams['figure.figsize']=4,4 sns.boxplot(y='price', x='waterfront', data=df) sns.boxplot(y='price', x='view' , data=df) sns.boxplot(y='price', x='condition' , data=df) sns.boxplot(y='price', x='grade' , data=df)

А вот с sqft_lot и sqft_lot15 нужно что-то придумать, и из-за больших значений вполне подойдет логарифмирование:

df['sqft_lot']=np.log(df['sqft_lot']) df['sqft_lot15']=np.log(df['sqft_lot15'])

sqft_lot до и после:

sqft_above и sqft_basement — составные части sqft_living, поэтому также трогать их не будем.

На этом с предварительным анализом мы закончим и посмотрим на тепловую карту корреляций:

sns.heatmap(df.corr(), cmap = 'viridis',annot = True)

Изучив карту корреляций видим, что иногда признаки сильно коррелированы между собой, поэтому удалим часть признаков с высокой корреляцией: sqft_lot15 (оставим sqft_lot), yr_built (оставим yr_renovated), sqft_above (sqft_living)

На этом закончим работу с данными и перейдем к созданию модели.

2. Моделирование

В данной части мы построим две модели: линейную регрессию и дерево решений.

Все необходимые нам модели содержаться в библиотеке sklearn.

Для начала отделим целевую переменную от остальных данных для обучения, а также разделим выборки на обучающую (70%) и тестовую (30%, на которой мы проверим как работает модель):

Y=df['price'] X=df.drop ('price',axis=1) from sklearn.model_selection import train_test_split X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.3, shuffle = True)

Также из sklearn для оценки модели загрузим три метрики — mean_absolute_error (средняя абсолютная ошибка), mean_squared_error (Среднеквадратическое отклонение), r2_score (коэффициент детерминации):

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

Начнем с линейной регрессии:

from sklearn.linear_model import LinearRegression LR = LinearRegression() #Создаем модель LR.fit(X_train, Y_train) #Обучаем модель Y_LR = LR.predict(X_test) #Предсказываем значения для выборки print ('MAE:', round (mean_absolute_error(Y_test, Y_LR),3)) #Метрики print ('√MSE:', round (mean_squared_error(Y_test, Y_LR)**(1/2),3)) print ('R2_score:', round (r2_score(Y_test, Y_LR),3))

MAE: 124477.452

√MSE 175205.645

R2_score: 0.627

Дерево решений:

from sklearn.tree import DecisionTreeRegressor TR = DecisionTreeRegressor() #Создаем модель TR.fit(X_train, Y_train) #Обучаем модель Y_TR=TR.predict(X_test) #Предсказываем значения для выборк print ('MAE:', round (mean_absolute_error(Y_test, Y_TR),3)) #Метрики print ('√MSE:', round (mean_squared_error(Y_test, Y_TR)**(1/2),3)) print ('R2_score:', round (r2_score(Y_test, Y_TR),3))

MAE: 151734.906

√MSE 220856.721

R2_score: 0.407

Исходя из метрик можно сделать вывод о том, что Линейная регрессия показала лучший результат, поэтому логичнее выбрать ее. Однако мы не задавались вопросами, из чего состоит ошибка модели, не является ли модель переобученной.

Вполне вероятно, что к ухудшению результата DecisionTreeRegressor приводит именно переобучение, так как мы даже не ограничивали глубину дерева в параметрах модели. Можем легко проверить это перебирая глубину деревьев в коротком цикле:

dep,score=[],[] for i in range(3,16): TR = DecisionTreeRegressor(max_depth=i) TR.fit(X_train, Y_train) Y_TR=TR.predict(X_test) dep.append(i) score.append(mean_squared_error(Y_test, Y_TR)**(1/2)) #Массив значений √MSE plt.rcParams['figure.figsize']=6,3 plt.plot(dep, score)

Очевидно, что лучший показатель при max_depth=7, и, посмотрев, на метрики (MAE: 124861.441, √MSE 175322.737, R2_score: 0.626) становиться понятно, что модель с таким ограничением аналогична линейной регрессии по качеству.

Также мы можем попробовать оценить какие признаки оказались наиболее важны для модели для прогноза стоимости:

Исходя из графика видно, что на стоимость больше всего влияет grade — общая субъективная оценка дома риелторской компанией (что, кстати, говорит о компетентности оценки), на втором месте — площадь дома, а на третьем — год последнего ремонта. Показатели количества спален, ванных комнат, этажей же модель посчитала незначимыми для прогноза.

Для лучшего понимания результатов, посчитаем среднюю ошибку в процентах — по линейной регрессии средняя ошибка 27,5%, то есть модель ошибается чуть больше, чем на четверть при прогнозе стоимости дома, что довольно много.

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

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

Лайфхаки IT, проверенные AI-решения для стандартных задач
{ "author_name": "NewTechAudit", "author_type": "editor", "tags": ["\u0441\u043e\u0437\u0434\u0430\u0435\u043c","\u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c","\u043e\u0431\u0443\u0447\u0430\u0435\u043c","\u043c\u0430\u0448\u0438\u043d\u043d\u043e\u0435\u043e\u0431\u0443\u0447\u0435\u043d\u0438\u0435"], "comments": 2, "likes": 1, "favorites": 10, "is_advertisement": false, "subsite_label": "ml", "id": 131153, "is_wide": true, "is_ugc": false, "date": "Mon, 01 Jun 2020 14:35:38 +0300", "is_special": false }
Курс «Enterprise fullstack-разработка на JavaScript с применением технологий React, Redux и Node.js»
13 июля Онлайн 64 000 ₽
Объявление на vc.ru
0
2 комментария
Популярные
По порядку
1

А не могли бы вы в статье указать ссылки на базы данных?

Ответить
0

Кирилл, добрый день! Спасибо за совет, добавим ссылки в статьи

Ответить

Комментарии