{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению\n", "
Автор материала: Michael Kazachok (@miklgr500)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#
Другая сторона tensorflow:KMeans" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Введение" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

\n", "

На сайте tensorflow будет хорошая инструкция по установке под определенную операционную ситему и возможностях использование GPGPU.Я не буду грузить данную работу особенностями \"кухни\" tensorflow (поэтому советую почитать хотябы основы в официальном тьюториале и посмотреть TensorFlow Tutorial and Examples for Beginners with Latest APIs; там же есть примеры, которые помогут в дальнейшем в изучении нейронных сетей), а я пройдусь по уже прошитым в этой либе алгоритмам крастеризации(а их фактически пока только два).

\n", "

При этом будет использоваться набор данных с Kaggel для соревнования Chicago Taxi Rides 2016, который использовался в одной из домашек (рекомендую использовать не более двух месяцев).

\n", "

Применение простейшего алгоритма кластеризации в tensorflow будет сопроваждаться рассмотрением вопросов изящной визуализации (которую я увидел этим летом на соревнований Kaggle New York City Taxi Trip), представленой DrGuillermo и BuryBuryZymon в их работах Dynamics of New York city - Animation и Strength of visualization-python visuals tutorial на соревновании.

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "##
Подключение используемых в работе библиотек и загрузка данных" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "FIG_SIZE = (12,12)\n", "PATH_DATA_JSON = '../../data/column_remapping.json'\n", "PATH_DATA_CSV = '../../data/chicago_taxi_trips_2016_*.csv'\n", "GIF_PATH = '../../img/animation.gif'\n", "KMEANS_GIF_PATH='../../img/kmeans_animation.gif'\n", "NUM_CLUSTERS = 5\n", "BATCH_SIZE = 5\n", "NUM_STEPS = 50\n", "LON_CONST = -87.623177\n", "LAT_CONST = 41.881832\n", "LON_ANI_CENTER = [-87.73, -87.60]\n", "LAT_ANI_CENTER = [41.85, 42.00]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "from glob import glob\n", "\n", "import folium\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import seaborn as sns\n", "from IPython.display import HTML\n", "from joblib import Parallel, delayed\n", "from matplotlib import animation\n", "from matplotlib.patches import Ellipse\n", "\n", "plt.rcParams.update({'figure.max_open_warning': 0})\n", "\n", "import base64\n", "import io\n", "\n", "import numpy as np\n", "import tensorflow as tf\n", "from dateutil import parser\n", "from geopy.geocoders import Nominatim\n", "\n", "%load_ext watermark" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Версии основных библиотек и параметры системы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%watermark -v -m -p numpy,pandas,matplotlib,tensorflow -g" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Открываем данные за два первых месяца. Будте внимательны со ссылками на данные." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#ядро которое будем использовать \n", "#для загруски и преобработки данных за один месяц\n", "def preproc_kernel(path):\n", " with open(PATH_DATA_JSON) as json_file:\n", " column_remapping = json.load(json_file)\n", " df = pd.read_csv(path)\n", " # в дальнейшем понадобяться только геоданные\n", " # и время начала поездки\n", " df = df.loc[:, [\n", " 'trip_start_timestamp',\n", " 'pickup_latitude',\n", " 'pickup_longitude',\n", " 'dropoff_latitude',\n", " 'dropoff_longitude']].dropna()\n", " geo_labels = ['pickup_latitude',\n", " 'pickup_longitude',\n", " 'dropoff_latitude',\n", " 'dropoff_longitude']\n", " for g in geo_labels:\n", " df[g] = df[g].apply(lambda x: float(column_remapping[g].get(str(int(x)))))\n", " return df\n", "\n", "\n", "dataset_files = sorted(glob(PATH_DATA_CSV))\n", "# выполняем загрузку данных параллельно\n", "# на двух ядрах, каждому по одному файлу\n", "dfs = Parallel(n_jobs=2)(delayed(preproc_kernel)(path) for path in dataset_files)\n", "# склеиваем данные\n", "df = pd.concat(dfs, ignore_index=True)\n", "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Визуализация данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Произведем предварительную визуализацию всех гео данных и выявим их границы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# соединяем гео данные для точек посадки и точек высадки\n", "longitude = list(df.pickup_longitude)+list(df.dropoff_longitude)\n", "print('max_long:'+str(max(longitude)))\n", "print('min_long:'+str(min(longitude)))\n", "latitude = list(df.pickup_latitude)+list(df.dropoff_latitude)\n", "print('max_lat:'+str(max(latitude)))\n", "print('min_lat:'+str(min(latitude)))\n", "\n", "loc_df = pd.DataFrame()\n", "loc_df['longitude'] = longitude\n", "loc_df['latitude'] = latitude" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#производим визуализацию объединенных гео данных\n", "fig, ax = plt.subplots(1,1, figsize = FIG_SIZE)\n", "plt.plot(longitude, \n", " latitude, \n", " '.', \n", " color = 'orangered',\n", " markersize = 1.5, \n", " axes = ax, \n", " figure = fig\n", " )\n", "ax.set_axis_off()\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, figsize=FIG_SIZE)\n", "\n", "sns.distplot(loc_df['longitude'], bins=300, kde=False, ax=ax1)\n", "sns.distplot(loc_df['latitude'], bins=300, kde=False, ax=ax2)\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "\n", "##
Kmean в tensorflow" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# формируем массив с данными в нужном формате\n", "# т.е. формируем пары [lon, lat]\n", "# Для правильной работы алгоритма\n", "# неообходимо омязательно избавиться от\n", "# постоянной компаненты\n", "data = [[(lon-LON_CONST), (lat-LAT_CONST)] for lon, lat in zip(longitude, latitude)]\n", "data = np.array(data)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

В качестве основы выберем уже прошитый в tensorflow алгоритм KMeans(люблю открытый код). Те кто разобрал открытый код, мог заметить, что из большого набора функций вызвать можем только training_graph(self). Обратите внимание возвращается ли в вашей версии tensorflow данная функция переменную cluster_centers_var(в 1.3 она не возвращается).

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ " def KMeans_clustering(num_clusters=NUM_CLUSTERS, flag_print=True):\n", " # создаем placeholder X\n", " # подставляя его вместо каких-то знаений\n", " # мы говорим вычислительному графу\n", " # что эти значения будут предоставлены потом: \n", " # в процессе обучения и/или инициализации\n", " X = tf.placeholder(tf.float32, shape=[None, 2])\n", "\n", " # производим построение вычислительного графа для KMeans\n", " kmeans = tf.contrib.factorization.KMeans(\n", " inputs=X,\n", " num_clusters=num_clusters,\n", " initial_clusters=\"kmeans_plus_plus\",\n", " mini_batch_steps_per_iteration=BATCH_SIZE,\n", " random_seed=29,\n", " use_mini_batch=True\n", " )\n", " \n", " (all_scores,cluster_idx, scores,cluster_centers_initialized,\\\n", " cluster_centers_var,init_op,train_op) = kmeans.training_graph()\n", " \n", " # т.к. изначально возвращается tuple\n", " # то берем только первый его член\n", " cluster_idx = cluster_idx[0]\n", " # производим расчет средней дистанции \n", " # точек до своего кластера\n", " avg_distance = tf.reduce_mean(scores)\n", "\n", " # создание сессии и инициальзация\n", " init_vars = tf.global_variables_initializer()\n", " sess = tf.Session()\n", " sess.run(init_vars)\n", " sess.run(init_op, feed_dict={X: data})\n", "\n", " # пошагово обучаем модель\n", " # получая на каждом шаге\n", " # d:среднюю дистанцию от точки \n", " # до центра своего кластера\n", " #----------------------------\n", " # задаем критерии остановки\n", "\n", " for i in range(1,NUM_STEPS+1):\n", " _, d, idx, cl_c = sess.run([train_op, \n", " avg_distance,\n", " cluster_idx,\n", " cluster_centers_var],\n", " feed_dict={X: data}\n", " )\n", " \n", " if (i%10==0)&(flag_print):\n", " print('Step %i, Average Distance %.8f'%(i, d))\n", " sess.close()\n", " return d,idx,cl_c" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Визуализируем работу алгоритма, произведя инициализацию всех кластеров в координате [LON_CONST, LAT_CONST], являющеся центром города.

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# сделаем анимацию обучения\n", "num_clusters = 8\n", "\n", "# массив для инициализации кластеров\n", "# в точке [LON_CONST, LAT_CONST], но \n", "# т.к. у нас все данные смещенны на \n", "# значение данной координаты,\n", "# то инициализацию необходимо провести \n", "# в точке [0, 0]\n", "init_cl = np.array([[0, 0] for i in range(num_clusters)],\n", " dtype=np.float32\n", " )\n", "X = tf.placeholder(tf.float32, shape=[None, 2])\n", "# производим построение вычислительного графа для KMeans\n", "kmeans = tf.contrib.factorization.KMeans(\n", " inputs=X,\n", " num_clusters=num_clusters,\n", " initial_clusters=init_cl,\n", " mini_batch_steps_per_iteration=2,\n", " random_seed=29,\n", " use_mini_batch=False\n", ")\n", " \n", "(all_scores,cluster_idx, scores,cluster_centers_initialized,\\\n", " cluster_centers_var,init_op,train_op) = kmeans.training_graph()\n", "# т.к. изначально возвращается tuple\n", "# то берем только первый его член\n", "cluster_idx = cluster_idx[0]\n", "avg_distance = tf.reduce_mean(scores)\n", "\n", "# создание сессии и инициальзация\n", "init_vars = tf.global_variables_initializer()\n", "sess = tf.Session()\n", "sess.run(init_vars)\n", "sess.run(init_op, feed_dict={X: data})\n", "fig, ax = plt.subplots(1,1, figsize = FIG_SIZE)\n", "# задаем функцию, которую передадим в animation.FuncAnimation\n", "# эта функция будет производить просчет полученого графика\n", "# на каждом шагу, но так как mini_batch_steps_per_iteration=2\n", "# то изменение будут каждые 2 шага, всего шагов будет 10\n", "# их мы непосредственно будем задавать в FuncAnimation\n", "# в виде массива и FuncAnimation пошагово будет передовать\n", "# заданные значения в animate_kmeans\n", "def animate_kmeans(step):\n", " _, d, idx, cl_c = sess.run([train_op, \n", " avg_distance,\n", " cluster_idx,\n", " cluster_centers_var],\n", " feed_dict={X: data}\n", " )\n", " # для упрощения работы с полученными данными после обучения\n", " # создается DataFrame, который в конце кода будет удален\n", " # данное решение может быть не совсем оптимально\n", " # оно просто упрощает жизнь вашему слуге =)\n", " loc_df['labels'] = idx\n", " cl_df = pd.DataFrame()\n", " cl_df['longitude'] = cl_c[:,0]+LON_CONST\n", " cl_df['latitude'] = cl_c[:,1]+LAT_CONST\n", " cl_df['labels'] = cl_df.index\n", " # обязательно чистим предыдущий график\n", " ax.clear()\n", " ax.set_title('Step: '+str(step))\n", " for l in cl_df['labels']:\n", " ax.plot(loc_df.loc[loc_df['labels'] == l, 'longitude'], \n", " loc_df.loc[loc_df['labels'] == l, 'latitude'], \n", " '.',\n", " markersize = 1.5\n", " )\n", " ax.plot(cl_df.loc[cl_df['labels'] == l, 'longitude'], \n", " cl_df.loc[cl_df['labels'] == l, 'latitude'], \n", " 'ro'\n", " )\n", " ax.annotate(s=str(l),\n", " xy=(cl_df.loc[cl_df['labels'] == l, 'longitude'], \n", " cl_df.loc[cl_df['labels'] == l, 'latitude'])\n", " )\n", " \n", " ax.set_axis_off()\n", " del cl_df\n", " \n", "ani = animation.FuncAnimation(fig,\n", " animate_kmeans,\n", " list(range(0, 20)),\n", " interval=500\n", " )\n", "# производим закрытие отрисованных графиков\n", "plt.close()\n", "# дириктори сохранения гифки\n", "gif_path = KMEANS_GIF_PATH\n", "# сохранение гифки\n", "ani.save(gif_path,\n", " writer='imagemagick',\n", " fps=1\n", " )\n", "# открываем сохраненную гифку и производим ее дешифрование\n", "# для дальнейшего URL и подстановки их в HTML\n", "video = io.open(gif_path,\n", " 'r+b'\n", " ).read()\n", "encoded = base64.b64encode(video)\n", "# производим отрисовку анимации в notebook\n", "HTML(data=''''''.format(\n", " encoded.decode('ascii'))) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Видно что обновление происходит каждые 2 шага за счет установки mini_batch_steps_per_iteration=2. Вы можете поиграться с кодом выше! Выставте другую инициализацию(\"kmeans_plus_plus\",\"random\") или поиграйтесь с параметрами для mini_batch, а можно и вовсе изменить количество кластеров!

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_cluster = range(1,15,1)\n", "avg_distance = []\n", "for i in n_cluster:\n", " d,idx,cl_c = KMeans_clustering(num_clusters=i, flag_print=False)\n", " avg_distance.append(d)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.plot([i for i in n_cluster], avg_distance, color = 'seagreen')\n", "plt.xlabel('number of cluster')\n", "plt.ylabel('avg_distance')\n", "plt.title('Optimal Number Of Cluster')\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Из графика видно, что ничего не видно=). Опять гадаем=) Я бы взять 4 кластера, и это довольно неплохо согласуется с предыдущей оценкой, поэтому возмем 5 кластеров(в данном случае лучше взять большее число, т.о. получится более детальная картина трафика).

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "NUM_CLUSTERS = 5\n", " \n", "d,idx,cl_c = KMeans_clustering(num_clusters=NUM_CLUSTERS, flag_print=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Добавим метки кластеров в loc_df, и создадим новый DataFrame с параметрами (широта, долгота и метка кластера для каждого кластера).

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "loc_df['labels'] = idx\n", "cl_df = pd.DataFrame()\n", "cl_df['longitude'] = cl_c[:,0]+LON_CONST\n", "cl_df['latitude'] = cl_c[:,1]+LAT_CONST\n", "cl_df['labels'] = cl_df.index\n", "cl_df.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Визуализация полученых кластеров" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(1,1, figsize = FIG_SIZE)\n", "for l in cl_df['labels']:\n", " plt.plot(loc_df.loc[loc_df['labels'] == l, 'longitude'], \n", " loc_df.loc[loc_df['labels'] == l, 'latitude'], \n", " '.',\n", " markersize = 1.5, \n", " axes = ax, \n", " figure = fig\n", " )\n", " plt.plot(cl_df.loc[cl_df['labels'] == l, 'longitude'], \n", " cl_df.loc[cl_df['labels'] == l, 'latitude'], \n", " 'ro', \n", " axes = ax, \n", " figure = fig\n", " )\n", " ax.annotate(s=str(l),\n", " xy=(cl_df.loc[cl_df['labels'] == l, 'longitude'], \n", " cl_df.loc[cl_df['labels'] == l, 'latitude'])\n", " )\n", " \n", "ax.set_axis_off()\n", "plt.show();" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# посмотрим где наши кластеры расположились на карте\n", "chikago_map = folium.Map(location=[LAT_CONST, LON_CONST], \n", " zoom_start=10,\n", " tiles='OpenStreetMap'\n", " )\n", "# выставляем маркеры на карту Чикаго\n", "for lon, lat in zip(cl_df['longitude'], cl_df['latitude']):\n", " folium.Marker(location=[lat, lon]).add_to(chikago_map)\n", "chikago_map" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Можно заметить, что две самых удаленных от скопления мест посадок и высодок центроид кластеров находяться ровно около аэропортов(1,3), одна принадлежит северным жилым зонам Чикаго(2), а две центроиды можно отнести на деловой и культурный части (4,0) Чикаго.

\n", "

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

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Визуализация трафика между центрами" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['pickup_hour'] = df['trip_start_timestamp'].apply(lambda x: parser.parse(x).hour)\n", "df['pickup_cluster'] = loc_df.loc[:len(df)-1,'labels'].values\n", "df['dropoff_cluster'] = loc_df.loc[len(df):, 'labels'].values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

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

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def trafic_animation(lon_ani_lim=None, lat_ani_lim=None, strong=6):\n", " # передовая пределы возможно ограничить зону \n", " # изображения анимации\n", " # так же немаловажен параметр strong\n", " # который является маштабирующим коэффициентом\n", " # и влияет на ширину стрелок\n", " if (lon_ani_lim==None)|(lat_ani_lim==None):\n", " lim_cl_df = cl_df\n", " elif (len(lon_ani_lim)!=2)|(len(lat_ani_lim)!=2):\n", " lim_cl_df = cl_df\n", " else:\n", " lim_cl_df = cl_df[\n", " ((cl_df['longitude']>lon_ani_lim[0])&(cl_df['longitude']lat_ani_lim[0])&(cl_df['latitude']'''.format(\n", " encoded.decode('ascii')))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# присмотримся к центру города\n", "encoded = trafic_animation(lon_ani_lim=LON_ANI_CENTER, \n", " lat_ani_lim=LAT_ANI_CENTER, \n", " strong=2\n", " )\n", "HTML(data=''''''.format(\n", " encoded.decode('ascii')))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Прелесть такого рода визуализации в том, что ее может проинтерпритировать даже ребенок.

" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "##
Заключение" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Tensorflow довольно мощное API, которое хорошо подходит не только для обучения нейронных сетей. Хотя стоит отметит скудность документации(по сравнению с sklearn) по некоторым частям библиотеки. Одна из таких частей и была рассмотренна в данном тьюториале. Я так же надеюсь вам понравилась визуализации и вы влюбились в нее так же как и я когда впервые ее увидел. Если такого рода тьюториал вам понравится, то я подумаю о переносе его в виде статьи на хабрахабр и создании цикла такого рода статей.

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Спасибо за внимание!

" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.1" } }, "nbformat": 4, "nbformat_minor": 2 }