Привет! Раньше мы изучали функциональность библиотеки Sklearn на модельных данных, а теперь настало время перейти к прикладной задаче. На этом видео мы потренируемся проходить все шаги от загрузки и предобратки данных до построения финальной модели. Работать мы будем с задачей Bike Sharing. В этой задаче по историческим данным о погодных условиях и аренде велосипедов требуется предсказать, сколько же велосипедов будет занято в заданный день и час. В исходной постановке задачи нам доступно 11 признаков, среди них есть как числовые признаки, так и категориальные, и бинарные данные. Мы будем работать с обучающей выборкой сайта соревнований kaggle. Давайте начнем с импортирования нужных библиотек. И так как мы будем строить графики, нам понадобится pylab. Фактически данные, с которыми мы собираемся работать, представляют собой матрицу «объект–признак», поэтому нам будет удобно работать с ними как с DataFrame. Давайте такой DataFrame получим с помощью функции read_csv. Итак, мы загрузили данные, теперь давайте на них посмотрим. Мы видим, что мы получили некоторый DataFrame. Первые столбцы представляют собой описание каждого объекта, а в последнем столбце мы видим значение целевой метки — количество занятых велосипедов. Также нам доступно описание данных. Мы видим, что каждый объект описывается погодными условиями, например температурой, давлением, скоростью ветра. И в конце нам дана информация о том, сколько же велосипедов было занято. Не будем пока подробно на этом останавливаться, лучше посмотрим размер наших данных. Мы видим, что нам доступно 11 000 объектов практически. И давайте посмотрим, есть ли там пропущенные значения, потому что если они есть, то нам придется задуматься о том, как же их обработать. Делаем это с помощью метода isnull. Ну вот видим, что пропущенных значений нет, теперь можно перейти непосредственно к предобработке данных. Давайте выведем некоторую информацию о нашем DataFrame с помощью метода info. Здесь мы видим, что практически все наши данные представляются цифрами — это либо целочисленный тип, либо тип с плавающей точкой. И только первый столбец, datetime, является объектом типа object. Но на самом деле мы с вами помним, что туда записана дата, поэтому логично было бы использовать эти данные в виде типа datetime. Ну почему это логично? Потому что мы сможем применять специфичные для datetime операции. Вот давайте преобразуем этот тип, сделаем это с помощью комбинации функций apply и to_datetime. И теперь, раз уж мы это сделали, давайте на основе этого столбца рассчитаем два новых признака: первый признак — это месяц, в который происходят события, второй признак — это час. Добавляем их в исходные данные, и теперь давайте посмотрим, как это выглядит. Видим, что появилось два новых столбца: месяц и час — то, что мы хотели. Далее для работы нам удобно разделить наши данные на обучение и тест. Это нужно для того, чтобы мы могли строить нашу модель на обучающей выборке и дальше честно оценить ее качество на отложенном тесте, который в обучении не участвовал. Обратим внимание, что наши данные имеют явную временную привязку — мы знаем час и день, за который мы хотим оценить количество велосипедов. В этом случае нам удобнее разбить данные по времени. Давайте будем строить модель на данных за более ранний период и оценивать ее качество на данных за более поздний период. В данном случае наши данные отсортированы по времени, поэтому для того чтобы получить такое разбиение, достаточно просто отрезать последний кусок данных, допустим последние 1000 объектов, и отправить их в отложенный dataset. Это мы сделаем с помощью метода iloc. Теперь давайте посмотрим на размеры полученных наборов данных. Видим, что действительно 1000 объектов отправляется в отложенный dataset, и остальные почти 10 000 объектов мы будем использовать для обучения. Теперь давайте проверим, что действительно вся обучающая выборка расположена в более раннем периоде времени. И вот, да, мы видим, что обучение заканчивается на час раньше, чем начинается тест. В данном случае нас это вполне устраивает. Для работы с моделями в Sklearn нам требуется отделить целевую метку от остальных данных. Давайте сделаем это для обучающей и для тестовой выборки. Мы знаем, что целевая метка находится в столбце count, поэтому давайте вырежем его из остальных данных. И также вырежем столбец datetime, потому что фактически это просто идентификатор объекта. Делаем это на обоих выборках. И теперь давайте визуализируем целевую переменную, посмотрим на ее распределение на обучении и на тесте. Ну вот, мы видим, что распределение отличается, но, в общем-то, можно сказать, что и во время обучения, и во время теста большую часть времени было занято не более 300 велосипедов. Теперь давайте сделаем следующее. В рамках этого урока мы с вами будем работать только с численными признаками, поэтому давайте сначала их отделим — создадим соответствующий список, — и теперь будем работать только с той частью обучающей и тестовой выборки, которая соответствует этим признакам. Давайте посмотрим, как выглядит получившийся набор данных. Ну да, мы видим, что он состоит только из числовых признаков — как обучение, так и тест. Теперь давайте обучим модель. Так как мы решаем задачу регрессии, давайте обучать SGDRegressor — это регрессия на основе стохастического градиентного спуска. Для начала создадим модель с параметрами по умолчанию и попробуем ее обучить на обучающей части данных. Делаем это с помощью метода fit, а дальше сразу давайте оценим качество с помощью метрики mean_absolute_error — средняя ошибка. Оценивать будем на тестовой части данных. Мы видим, что мы получили какую-то невероятно большую ошибку. Давайте посмотрим, как выглядят наши предсказания, и сравним их с правильным значением целевой функции. Сначала выведем на экран целевую функцию, теперь — наши предсказания, и мы видим, что мы предсказываем невероятно большие числа. Так быть не должно. Давайте посмотрим на коэффициенты регрессии — может быть это прольет свет на ошибку. Так, мы видим, что у нас невероятно большие коэффициенты. Как такое могло произойти? Наверняка из предыдущих уроков вы помните, что многие линейные модели чувствительны к масштабу признаков. В данном случае мы работаем с набором данных, в котором признаки могут сильно отличаться по масштабам. Для того чтобы решить эту проблему, нам нужно сначала отмасштабировать признаки в нашем наборе данных. В Sklearn это можно сделать с помощью модуля preprocessing, нам понадобится объект StandardScaler. Давайте сначала создадим такой объект, и дальше, для того чтобы применить наше преобразование, нам сначала нужно его обучить. Логично предположить, что обучать его можно только на обучающей части данных. Но почему это так? Обычно в жизни на момент, когда мы строим модель, мы еще ничего не знаем про тестовые данные. Поэтому для того чтобы не использовать информацию, которой на самом деле у нас нет — а это может привести к переобучению, — мы будем обучать наши преобразования только на обучающей выборке. Делаем это с помощью метода fit, и после того как мы получим обученное преобразование, мы применим его. Отдельно применим его к обучающей выборке, и отдельно — к тестовой. Это делается с помощью метода transform. Итак, мы получили данные, теперь можно снова обучить модель. Сразу же оцениваем ее качество. И что мы видим? Ошибка стала очень маленькой. Давайте выведем целевую функцию и наши прогнозы. Видим, что мы ошибаемся меньше, чем на один велосипед. Это очень странно. Давайте посмотрим на коэффициенты регрессии, чтобы понять, что же произошло. В таком виде их не очень удобно анализировать, давайте округлим. И что мы видим? Практически все признаки принимают очень маленькие веса, за исключением двух. Давайте посмотрим, что это за признаки. Итак, мы видим, что это признаки casual и registered. Давайте попытаемся вспомнить из описания данных, что они означают. Фактически система аренды велосипедов работает следующим образом: системой может воспользоваться как зарегистрированный пользователь, так и незарегистрированный. В данном случае наши столбцы представляют количество зарегистрированных пользователей, которые используют систему — это столбец registered. И количество пользователей, которые не зарегистрировались, но также хотят арендовать велосипед — это столбец casual. Давайте выведем значения целевой функции и увидим следующую закономерность: фактически, если значение этих двух столбцов сложить, то мы получим нашу целевую метку. Давайте убедимся, что это действительно так на всех данных. Да, это правда так. Фактически два эти столбца в сумме дают нашу целевую метку. Что же тут произошло? Мы с вами совершили стандартную ошибку начинающих data scientist'ов — мы начали анализировать данные и строить модель, детально не разобравшись в значении наших переменных. То есть фактически мы использовали в модели те данные, по которым однозначно восстанавливается целевая функция. Конечно же, так делать не следует. Давайте вырежем эти данные из нашего набора данных. Вырежем их как из обучающей, так и из тестовой выборки. Теперь давайте снова отмасштабируем признаки уже на новом наборе данных. И теперь обучим модель и оценим качество. Видим, что наша ошибка сильно выросла — теперь мы ошибаемся в среднем на 122 велосипеда. Это уже гораздо ближе к истине, эта оценка более реалистична. Для того чтобы до конца убедиться, что мы все делаем правильно, давайте выведем веса и регрессию. Ну да, теперь мы видим, что практически все признаки вносят некоторый вклад в нашу модель — эти веса больше похожи на правильные. Фактически мы с вами получили некоторую базовую модель, некоторый baseline, который мы считаем правильным. Теперь давайте попытаемся его улучшить. Напоминаю, что модель мы обучали с параметрами по умолчанию, поэтому теперь давайте подберем параметры, оптимальные для решаемой задачи. Подбор параметров мы делаем по кросс-валидации. В данном случае с этим может возникнуть проблема, потому что прежде чем обучать модель, мы хотим делать scaling — мы хотим масштабировать признаки. Однако масштабирование мы обучаем только по обучающей части выборки. Таким образом, при кросс-валидации нам придется обучить сразу несколько скейлеров — по одному на каждую итерацию кросс-валидации. Получается, что нам нужно писать неудобные циклы, и запись будет достаточно громоздкой. Однако Sklearn предоставляет нам способ, для того чтобы этого избежать — такой способ называется Pipeline. Вместо одного преобразования мы с вами можем реализовать целую цепочку преобразований. Для этого давайте импортируем Pipeline из модуля pipeline и попытаемся его построить. В данном случае мы хотим делать два шага: первый шаг — это масштабирование признаков, второй шаг — это непосредственно обучение модели. Вот давайте такой Pipeline и создадим. Передаем ему параметр steps — это список наших шагов, и далее каждый шаг представляется тьюплом, где первый элемент — это имя шага, второй элемент — это непосредственно объект, который может преобразовывать данные. Важным условием является то, чтобы у объекта были такие методы, как fit и transform, fit и predict. Вот как наш scaler, так и наш regressor этому интерфейсу удовлетворяют, поэтому мы можем смело вносить их в цепочку. Построили цепочку, и теперь давайте работать с этой цепочкой как с одиночным преобразованием. Фактически это означает, что мы можем эту цепочку обучить с помощью метода fit, а также получить предсказания с помощью метода predict. Давайте это сделаем и посмотрим на ошибку. Формально она не должна измениться, мы ожидаем увидеть ту же самую ошибку, которую мы видели ранее. Итак, да, мы видим, что все получилось — наша ошибка не изменилась. Теперь наша цепочка преобразований готова, мы убедились, что она работает правильно, поэтому давайте перейдем к подбору параметров. Параметры мы будем подбирать по сетке, с помощью перебора различных наборов параметров, поэтому давайте для начала посмотрим, как правильно к ним обращаться. Мы видим, что в случае использования Pipeline нам нужно обращаться к параметрам с помощью расширенного имени. Сначала нам нужно указать имя шага, далее — двойное нижнее подчеркивание и название самого параметра. Вот давайте создадим словарь параметров, которые мы хотим перебирать — довольно просто. Будем перебирать вид функции потерь, количество итераций, вид регуляризации, а также коэффициент перед регуляризацией. И для разнообразия давайте подберем какой-нибудь параметр для скейлинга. Ну, например, давайте подберем среднее. Теперь нужно построить нашу... наш grid, нашу сетку. Строим сетку, передаем туда Pipeline, а также передаем словарь с параметрами. Указываем, что мы будем оценивать метрику «средняя ошибка» и будем делать кросс-валидацию на 4 фолда. Теперь давайте сетку обучим. Так как мы делаем полный перебор по сетке, это может занять существенное время. Итак, сетка обучилась, давайте посмотрим на лучшие значения параметров, а также выведем лучшее значение метрики. И видим, что для обучения нам достаточно трех итераций, нужно использовать квадратичную функцию потерь, scaling нужно делать со средним 0, коэффициент перед регуляризацией должен быть 0,01 и регуляризация принимает вид L2. Теперь давайте оценим лучшую модель на отложенном тесте. Видим, что вроде бы наша ошибка уменьшилась — теперь мы ошибаемся на 120 велосипедов вместо 122, однако это оценка в некоторой точке. Давайте посмотрим, насколько наша ошибка большая относительно среднего значения целевой переменной. Итак, видим, что среднее значение у нас — 232 велосипеда, но при этом мы ошибаемся на 120. Но в общем-то, понятно, что это плохо. И с этой точки зрения 122 от 120 отличаются не очень сильно. Фактически наша оптимизация с помощью подбора параметров не помогла нам улучшить модель, важно это понимать. А для того чтобы в этом убедиться, давайте посмотрим на значения наших предсказаний. Вот для начала их получим, теперь выведем правильные значения целевой метрики, целевой метки, вернее, и выведем наши оценки — видим, что они отличаются очень существенно. Теперь давайте сделаем следующее: отобразим график наших объектов в пространстве правильных значений целевой метки и наших предсказаний. Когда мы строим график в таком пространстве, то для хорошей модели понятно, что мы ожидаем — мы ожидаем облако точек в районе диагонали. Получается, что наши предсказания должны совпадать с целевой меткой, поэтому ну вот диагональ должна получиться. В данном случае мы имеем не очень хорошую модель, давайте посмотрим, как это выглядит. Да, видим, что наши облака точек совсем не похожи на диагональ. Более того, облака точек при использовании модели без подбора параметров и облака точек при использовании модели с подбором параметров не сильно отличаются. То есть фактически наша модель является довольно слабой, и оптимизация по параметрам нам ничего не дала. На этом давайте закончим. В этом уроке мы получили некоторый baseline, некоторую начальную модель, которая работает только на числовых признаках. Также в процессе работы мы с вами научились делать scaling, мы научились строить цепочки преобразований, а на следующем уроке мы постараемся улучшить эту модель, добавив в нее все остальные признаки.