#!/usr/bin/env python # coding: utf-8 # # Семинар 13. Понижение размерности данных (отбор признаков) # # ![](http://i.imgur.com/9HiVD0m.png) # # # Основная цель: каким-либо образом "уменьшить" число признаков. # # Почему это может быть важно: # ### Проклятие размерности # # Рассмотрим пример (аналогично пончикам с лекции): пусть есть треугольники, круги и квадратики. Будем считать что признаки принимают значения только из какого-то интервала. Если есть всего один признак, то объекты лежат "примерно" рядом. В случае двух признаков они находятся дальше друг от друга. При увеличении размерности объекты будут все дальше друг от друга. То есть чем больше пространство, тем объекты становятся более "непохожими" (лежат далеко друг от друга). Получается, нужно много данных, чтобы алгоритм мог с ними работать (что порой затруднительно). # # ![](http://nikhilbuduma.com/img/dimension_sparsity.png) # ### Шумовые и просто плохие признаки # # Присутствие шума в признаках может ухудшать модель. Будем решать задачу классификации. # In[8]: import numpy as np import pandas as pd import pylab as plt get_ipython().run_line_magic('precision', '3') get_ipython().run_line_magic('matplotlib', 'inline') # In[9]: import sklearn.datasets from sklearn.cross_validation import train_test_split # Рассмотрим некоторую случайную выборку, где все признаки "хорошие". Разделим ее на обучающую и тестовую: # In[94]: X, y = sklearn.datasets.make_classification(n_redundant=0, random_state=42) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42) # В качестве модели будем использовать логистическую регрессию. Для оценки качества воспользуемся метрикой *Accuracy*. # In[10]: from sklearn.linear_model import LogisticRegressionCV # In[11]: from sklearn.metrics import accuracy_score # Обучим нашу модель и посчитаем ее качество: # In[97]: lr = LogisticRegressionCV(Cs=[0.01, 0.1, 1.0], random_state=42) lr.fit(X_train, y_train) accuracy_score(y_test, lr.predict(X_test)) # Теперь к обучающим и тестовым данным добавим признак, не несущий никакой информации, а являющийся равномерным шумом на интервале от 0 до 1. # In[98]: X_train_noise = np.concatenate([X_train, np.random.uniform(size=(X_train.shape[0], 1))], axis=1) X_test_noise = np.concatenate([X_test, np.random.uniform(size=(X_test.shape[0], 1))], axis=1) # In[99]: lr = LogisticRegressionCV(Cs=[0.01, 0.1, 1.0], random_state=42) lr.fit(X_train_noise, y_train) accuracy_score(y_test, lr.predict(X_test_noise)) # Можно заметить, что качество стало чуть хуже. # # Аналогично и для "плохих" признаков (в данном случае — коррелирующих). Добавим к исходным данным первый признак умноженый на 2. # In[100]: X_train_corr = np.concatenate([X_train, (X_train[:,0] * 2)[:, np.newaxis]], axis=1) X_test_corr = np.concatenate([X_test, (X_test[:, 0] * 2)[:, np.newaxis]], axis=1) # In[101]: lr = LogisticRegressionCV(Cs=[0.01, 0.1, 1.0], random_state=42) lr.fit(X_train_corr, y_train) accuracy_score(y_test, lr.predict(X_test_corr)) # Качество снова стало чуть ниже. # # К сожалению, сходу не всегда понятно какие именно из признаков являются шумовыми. # ### Переобучение # # Чем больше признаков, тем сложнее становится модель (например, в случае линейных — нужно находить больше коэффициентов). Таким образом, если признаков очень много, а данных не совсем — можно столкнуться с переобучением. # *Какие еще проблемы возникают при большом числе признаков?* # ## Отбор признаков # - Одномерные методы — оценивают важность признаков по отдельности # # Например, использую оценку дисперсии признака: # In[4]: from sklearn.feature_selection import VarianceThreshold from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import f_classif # In[41]: X = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], [0, 1, 1]] sel = VarianceThreshold(0.2) sel.fit(X) X_new = sel.fit_transform(X) sel.variances_ # In[42]: sel = VarianceThreshold(0.2) X_new = sel.fit_transform(X) X_new # Теперь попробуем поработать с реальными данными. # Рассмотрим датасет Титаник: # In[12]: data = pd.read_csv('titanic.csv') data.head() # Оставим только числовые признаки (а пол перекодируем в 0 и 1), выкинем объекты с пропусками: # In[14]: X = data[['Pclass', 'Age', 'SibSp', 'Parch', 'Fare']] X['Sex'] = (data.Sex == 'male').astype('int') X = X.dropna(axis=0) y = data.Survived[X.index] X.head() # In[15]: X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # Теперь посмотрим какие признаки с точки зрения одномерных методов важны. # # Функция *SelectKBest* позволяет выбрать *k* наиболее важных признаков из модели с помощью заданного метода, после чего оставить только их. Так как в данном случае нам интересно скорее посмотреть на сами признаки, просто рассмотрим какие "важности" мы получаем, используя этот метод. # In[16]: selector = SelectKBest(f_classif, k=2) selector.fit(X_train, y_train) # Посмотрим на полученные значения: # In[17]: scores = selector.scores_ scores # In[18]: idx = np.arange(scores.size) width = 0.5 p1 = plt.bar(idx, scores, width, color='g') plt.xticks(idx + width/2, X.columns)[1] # Итак, получили что пол, класс и цена билета хорошо связаны с целевой меткой (с точки зрения этого метода), в то время как SibSp имеет относительно малую значимость. Посмотрим, найдутся ли эти закономерности, если мы попробуем воспользоваться другой группой методов. # - Отбор с помощью моделей # # Некоторые изученные модели позволяют оценить "важность" признаков. # # Например: # - коэффициенты в линейных моделях можно интерпретировать как "важности" признаков в случае смасштабированных данных (а в случае *L1*-регуляризации коэффициенты при *неважных* признаках зануляются, например, в *Lasso*) # - RandomForest (и просто решающее дерево) умеют оценивать насколько важен был признак # # Попробуем понять, являются ли эти празнаки также важными с точки зрения моделей. # Для начала рассмотрим знакомую нам линейную модель (логистическую регрессию): # In[19]: from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) lr = LogisticRegressionCV(Cs=[0.1, 1.0, 10], random_state=42) lr.fit(X_train_scaled, y_train) # In[20]: coef = lr.coef_ coef # Построим коэффициенты на диаграмме для более удобного сравнения (в качестве "важности" считаем модуль коэффициента): # In[21]: idx = np.arange(coef.size) width = 0.5 p1 = plt.bar(idx, np.abs(coef[0]), width, color='g') plt.xticks(idx + width/2, X.columns)[1] # И посмотрим на качество модели: # In[22]: X_test_scaled = scaler.transform(X_test) accuracy_score(y_test, lr.predict(X_test_scaled)) # А теперь случайный лес: # In[23]: from sklearn.ensemble import RandomForestClassifier # In[24]: rf = RandomForestClassifier(n_estimators=1000, random_state=42) rf.fit(X_train, y_train) # Теперь посчитаем важности признаков: # In[25]: importance = rf.feature_importances_ importance # В данном случае само значение важности не несет какой-то глубокой информации (кроме как чем больше — тем лучше), оно скорее важно для сравнения признаков между собой. # In[26]: idx = np.arange(importance.size) width = 0.5 p1 = plt.bar(idx, importance, width, color='g') plt.xticks(idx + width/2, X.columns)[1] # Вполне логично, что возраст, пол и цена билета (которая неявно указывает на класс пассажира) важны в этой задаче. # In[27]: accuracy_score(y_test, rf.predict(X_test)) # Если сравнивать с предыдущим методом отбора признаков, то можно сказать что модели также находят указанные закономерности в данных. # # Обе модели показали сравнимое качество, но в тоже время выбрали разные "важные признаки". Так как модели имеют разную природу, следовательно влияние признаков вычисляется по-разному. Это необходимо помнить, но также уметь сравнивать результаты: по обеим моделям явно видно, что возраст, как и пол, достаточно хорошие признаки. В то же время цена билета явно зависела от класса, поэтому это нормально что одна модель обратила внимание на цену, а другая — на класс. # Давайте посмотрим какое качество будет иметь случайный лес, если мы хотим оставить всего три признака: # - Pclass, Fare, Sex (от одномерного метода) # - Pclass, Age, Sex (от линейного метода) # - Age, Fare, Sex (от случайного леса) # In[37]: rf = RandomForestClassifier(n_estimators=1000, random_state=42) rf.fit(X_train[['Pclass', 'Fare', 'Sex']], y_train) accuracy_score(y_test, rf.predict(X_test[['Pclass', 'Fare', 'Sex']])) # In[38]: rf = RandomForestClassifier(n_estimators=1000, random_state=42) rf.fit(X_train[['Pclass', 'Age', 'Sex']], y_train) accuracy_score(y_test, rf.predict(X_test[['Pclass', 'Age', 'Sex']])) # In[39]: rf = RandomForestClassifier(n_estimators=1000, random_state=42) rf.fit(X_train[['Age', 'Fare', 'Sex']], y_train) accuracy_score(y_test, rf.predict(X_test[['Age', 'Fare', 'Sex']])) # Обратите внимание, что отбор признаков нужно проводить на отложенной выборке.