Открытый курс по машинному обучению. Сессия № 3

Авторы материала: Ольга Дайховская (@aiho), Юрий Кашницкий (@yorko).

Материал распространяется на условиях лицензии Creative Commons CC BY-NC-SA 4.0. Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала.

Домашнее задание № 7

Обучение без учителя

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

Мы будем работать с набором данных Samsung Human Activity Recognition. Скачайте данные отсюда. Данные поступают с акселерометров и гироскопов мобильных телефонов Samsung Galaxy S3 (подробнее про признаки – по ссылке на UCI выше), также известен вид активности человека с телефоном в кармане – ходил ли он, стоял, лежал, сидел или шел вверх/вниз по лестнице.

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

Заполните код в клетках (где написано "Ваш код здесь") и ответьте на вопросы в веб-форме.


In [1]:
import numpy as np
import pandas as pd
import seaborn as sns
from tqdm import tqdm_notebook

%matplotlib inline
from matplotlib import pyplot as plt
plt.style.use(['seaborn-darkgrid'])
plt.rcParams['figure.figsize'] = (12, 9)
plt.rcParams['font.family'] = 'DejaVu Sans'

from sklearn import metrics
from sklearn.cluster import KMeans, AgglomerativeClustering, SpectralClustering
from sklearn.decomposition import PCA
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC

RANDOM_STATE = 17

In [2]:
X_train = np.loadtxt("../../data/samsung_HAR/samsung_train.txt")
y_train = np.loadtxt("../../data/samsung_HAR/samsung_train_labels.txt").astype(int)

X_test = np.loadtxt("../../data/samsung_HAR/samsung_test.txt")
y_test = np.loadtxt("../../data/samsung_HAR/samsung_test_labels.txt").astype(int)

In [3]:
# Проверим размерности
assert(X_train.shape == (7352, 561) and y_train.shape == (7352,))
assert(X_test.shape == (2947, 561) and y_test.shape == (2947,))

Для кластеризации нам не нужен вектор ответов, поэтому будем работать с объединением обучающей и тестовой выборок. Объедините X_train с X_test, а y_train – с y_test.


In [4]:
X = np.vstack([X_train, X_test])
y = np.hstack([y_train, y_test])

Определим число уникальных значений меток целевого класса.


In [5]:
np.unique(y)


Out[5]:
array([1, 2, 3, 4, 5, 6])

In [6]:
n_classes = np.unique(y).size

Эти метки соответствуют:

  • 1 - ходьбе
  • 2 - подъему вверх по лестнице
  • 3 - спуску по лестнице
  • 4 - сидению
  • 5 - стоянию
  • 6 - лежанию

Отмасштабируйте выборку с помощью StandardScaler с параметрами по умолчанию.


In [7]:
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

Понижаем размерность с помощью PCA, оставляя столько компонент, сколько нужно для того, чтобы объяснить как минимум 90% дисперсии исходных (отмасштабированных) данных. Используйте отмасштабированную выборку и зафиксируйте random_state (константа RANDOM_STATE).


In [8]:
pca = PCA(0.90, random_state=RANDOM_STATE)
X_pca = pca.fit_transform(X_scaled)

Вопрос 1:
Какое минимальное число главных компонент нужно выделить, чтобы объяснить 90% дисперсии исходных (отмасштабированных) данных?


In [9]:
len(pca.explained_variance_ratio_)


Out[9]:
65

Варианты:

  • 56
  • 65
  • 66
  • 193

Вопрос 2:
Сколько процентов дисперсии приходится на первую главную компоненту? Округлите до целых процентов.

Варианты:

  • 45
  • 51
  • 56
  • 61

In [10]:
round(pca.explained_variance_ratio_[0] * 100)


Out[10]:
51.0

Визуализируйте данные в проекции на первые две главные компоненты.


In [11]:
plt.figure(figsize=(13,10))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, s=20, cmap='viridis');
plt.colorbar()


Out[11]:
<matplotlib.colorbar.Colorbar at 0x1f519c01898>

Вопрос 3:
Если все получилось правильно, Вы увидите сколько-то кластеров, почти идеально отделенных друг от друга. Какие виды активности входят в эти кластеры?

Ответ:

  • 1 кластер: все 6 активностей
  • 2 кластера: (ходьба, подъем вверх по лестнице, спуск по лестнице) и (сидение, стояние, лежание)
  • 3 кластера: (ходьба), (подъем вверх по лестнице, спуск по лестнице) и (сидение, стояние, лежание)
  • 6 кластеров

Сделайте кластеризацию данных методом KMeans, обучив модель на данных со сниженной за счет PCA размерностью. В данном случае мы подскажем, что нужно искать именно 6 кластеров, но в общем случае мы не будем знать, сколько кластеров надо искать.

Параметры:

  • n_clusters = n_classes (число уникальных меток целевого класса)
  • n_init = 100
  • random_state = RANDOM_STATE (для воспроизводимости результата)

Остальные параметры со значениями по умолчанию.


In [12]:
k_means = KMeans(n_clusters=n_classes, n_init=100, random_state=RANDOM_STATE).fit(X_pca)

In [13]:
cluster_labels = k_means.predict(X_pca)

Визуализируйте данные в проекции на первые две главные компоненты. Раскрасьте точки в соответствии с полученными метками кластеров.


In [14]:
plt.figure(figsize=(13,10))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=cluster_labels, s=20,  cmap='viridis')
plt.colorbar()


Out[14]:
<matplotlib.colorbar.Colorbar at 0x1f519fa14e0>

Посмотрите на соответствие между метками кластеров и исходными метками классов и на то, какие виды активностей алгоритм KMeans путает.


In [15]:
tab = pd.crosstab(y, cluster_labels, margins=True)
tab.index = ['ходьба', 'подъем вверх по лестнице', 
             'спуск по лестнице', 'сидение', 'стояние', 'лежание', 'все']
tab.columns = ['cluster' + str(i + 1) for i in range(6)] + ['все']
tab


Out[15]:
cluster1 cluster2 cluster3 cluster4 cluster5 cluster6 все
ходьба 903 0 0 78 741 0 1722
подъем вверх по лестнице 1241 0 0 5 296 2 1544
спуск по лестнице 320 0 0 196 890 0 1406
сидение 1 1235 91 0 0 450 1777
стояние 0 1344 0 0 0 562 1906
лежание 5 52 1558 0 0 329 1944
все 2470 2631 1649 279 1927 1343 10299

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

Пример: если для класса "спуск по лестнице", в котором 1406 объектов, распределение кластеров такое:

  • кластер 1 – 900
  • кластер 3 – 500
  • кластер 6 – 6,

то такая доля будет 900 / 1406 $\approx$ 0.64.

Вопрос 4:
Какой вид активности отделился от остальных лучше всего в терминах простой метрики, описанной выше?

Ответ:

  • ходьба
  • стояние
  • спуск по лестнице
  • нет верного ответа

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


In [18]:
# Ваш код здесь
inertia = []
for k in tqdm_notebook(range(1, n_classes + 1)):
    kmeans = KMeans(n_clusters=k, n_init=100, random_state=RANDOM_STATE).fit(X_pca)
    inertia.append(np.sqrt(kmeans.inertia_))

for i in range(1, len(inertia) - 1):
    D = abs(inertia[i] - inertia[i + 1]) / abs(inertia[i - 1] - inertia[i])
    print(D)


0.1734475356009418
0.41688649539867606
0.9332154094478459
0.629704011371569

Вопрос 5:
Какое количество кластеров оптимально выбрать, согласно методу локтя?

Ответ:

  • 1
  • 2
  • 3
  • 4

Попробуем еще один метод кластеризации, который описывался в статье – агломеративную кластеризацию.


In [220]:
ag = AgglomerativeClustering(n_clusters=n_classes, 
                             linkage='ward').fit_predict(X_pca)

Посчитайте Adjusted Rand Index (sklearn.metrics) для получившегося разбиения на кластеры и для KMeans с параметрами из задания к 4 вопросу.


In [221]:
metrics.adjusted_rand_score(y, ag)


Out[221]:
0.49362763373004886

In [222]:
metrics.adjusted_rand_score(y, cluster_labels)


Out[222]:
0.4198070012602345

Вопрос 6:
Отметьте все верные утверждения.

Варианты:

  • Согласно ARI, KMeans справился с кластеризацией хуже, чем Agglomerative Clustering
  • Для ARI не имеет значения какие именно метки присвоены кластерам, имеет значение только разбиение объектов на кластеры
  • В случае случайного разбиения на кластеры ARI будет близок к нулю

Можно заметить, что задача не очень хорошо решается именно как задача кластеризации, если выделять несколько кластеров (> 2). Давайте теперь решим задачу классификации, вспомнив, что данные у нас размечены.

Для классификации используйте метод опорных векторов – класс sklearn.svm.LinearSVC. Мы в курсе отдельно не рассматривали этот алгоритм, но он очень известен, почитать про него можно, например, в материалах Евгения Соколова – тут.

Настройте для LinearSVC гиперпараметр C с помощью GridSearchCV.

  • Обучите новый StandardScaler на обучающей выборке (со всеми исходными признаками), примените масштабирование к тестовой выборке
  • В GridSearchCV укажите cv=3.

In [223]:
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

In [224]:
svc = LinearSVC(random_state=RANDOM_STATE)
svc_params = {'C': [0.001, 0.01, 0.1, 1, 10]}

In [225]:
grid = GridSearchCV(svc, param_grid=svc_params, cv=3).fit(X_train_scaled, y_train)
best_svc = grid.best_estimator_

In [231]:
best_svc.C


Out[231]:
0.1

Вопрос 7
Какое значение гиперпараметра C было выбрано лучшим по итогам кросс-валидации?

Ответ:

  • 0.001
  • 0.01
  • 0.1
  • 1
  • 10

In [227]:
y_predicted = best_svc.predict(X_test_scaled)

In [229]:
print(metrics.classification_report(y_test, y_predicted, target_names=tab.index[:6]))


                          precision    recall  f1-score   support

                  ходьба       0.97      1.00      0.98       496
подъем вверх по лестнице       0.98      0.97      0.98       471
       спуск по лестнице       1.00      0.98      0.99       420
                 сидение       0.96      0.87      0.91       491
                 стояние       0.88      0.97      0.92       532
                 лежание       1.00      0.98      0.99       537

             avg / total       0.96      0.96      0.96      2947

Вопрос 8:
Какой вид активности SVM определяет хуже всего в терминах точности? Полноты?

Ответ:

  • по точности – подъем вверх по лестнице, по полноте – лежание
  • по точности – лежание, по полноте – сидение
  • по точности – ходьба, по полноте – ходьба
  • по точности – стояние, по полноте – сидение

Наконец, проделайте то же самое, что в 7 вопросе, только добавив PCA.

  • Используйте выборки X_train_scaled и X_test_scaled
  • Обучите тот же PCA, что раньше, на отмасшабированной обучающей выборке, примените преобразование к тестовой
  • Настройте гиперпараметр C на кросс-валидации по обучающей выборке с PCA-преобразованием. Вы заметите, насколько это проходит быстрее, чем раньше.

Вопрос 9:
Какова разность между лучшим качеством (долей верных ответов) на кросс-валидации в случае всех 561 исходных признаков и во втором случае, когда применялся метод главных компонент? Округлите до целых процентов.

Варианты:

  • Качество одинаковое
  • 2%
  • 4%
  • 10%
  • 20%

In [233]:
X_train_scaled_pca = pca.fit_transform(X_train_scaled)
X_test_scaled_pca = pca.transform(X_test_scaled)

In [234]:
svc = LinearSVC(random_state=RANDOM_STATE)
svc_params = {'C': [0.001, 0.01, 0.1, 1, 10]}
grid_2 = GridSearchCV(svc, param_grid=svc_params, cv=3).fit(X_train_scaled_pca, y_train)

In [235]:
round((grid.best_score_ - grid_2.best_score_) * 100)


Out[235]:
4.0

Вопрос 10:
Выберите все верные утверждения:

Варианты:

  • Метод главных компонент в данном случае позволил уменьшить время обучения модели, при этом качество (доля верных ответов на кросс-валидации) очень пострадало, более чем на 10%
  • PCA можно использовать для визуализации данных, однако для этой задачи есть и лучше подходящие методы, например, tSNE. Зато PCA имеет меньшую вычислительную сложность
  • PCA строит линейные комбинации исходных признаков, и в некоторых задачах они могут плохо интерпретироваться человеком