Тук ще опитаме да видим малко "machine learning на практика". Ще разгледаме набор от данни от Kaggle, ще си поиграем с него и ще се опитаме да направим регресор, който да вади добри резултати. Идеята е да видим как изглежда процеса на решаване на реален проблем.
Нека да започнем с малко библиотеки:
import sys
import sklearn
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
Може да изтеглите данните с които ще работим от тук:
https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data
Трябат ви поне train.csv
и test.csv
, които да поставите в data/house-prices
спрямо директорията в която е notebook-а.
train = pd.read_csv('data/house-prices/train.csv', index_col=['Id'])
test = pd.read_csv('data/house-prices/test.csv', index_col=['Id'])
Нека да видим каква е формата на данните. Атрибутът shape
връща размерността на данните (редове и колони).
print('train:', train.shape)
print('test:', test.shape)
В test има една колона по-малко. Нека да видим коя е:
print('missing column:', set(train.columns) - set(test.columns))
Ще работим с трейн сета. С тестовия няма какво да правим, защото липсва колоната, която предсказваме.
del test
# Това го нямаше на python лекцията. Помага да осовобидм памет, когато ни е нужна. Без да убиваме процеса.
Да видим какви колони има в трейн сета.
train.columns
!head -25 data/house-prices/data_description.txt
Може да видим статистика какви стойности има в колоната и разпределението им:
print(train.LandContour.value_counts())
print(train.Electrical.value_counts())
print(train.Alley.value_counts())
print(train.FullBath.value_counts())
Pandas
може да показва и графики:
train.FullBath.value_counts().plot(kind='bar');
Да погледнем какво разпределение имат данните.
sns.distplot(train.SalePrice);
Ще започнем с baseline модел. Идеята е възможно най-бързо да намерим отправна точка, която да подобряваме.
Първо трябва да разделим данните на x_train
, y_train
и x_test
и y_test
за да проверим дали моделът генерализира.
from sklearn.model_selection import train_test_split
train_no_sale_price = train.drop(labels=['SalePrice'], axis=1)
train_sale_price = train['SalePrice']
x_train, x_test, y_train, y_test = train_test_split(train_no_sale_price,
train_sale_price,
test_size=0.3,
random_state=4330)
Дали не сме забравили какво връща train_test_split
? Нека видим какви размери имат новите променливи:
for df in [x_train, x_test, y_train, y_test]:
print(df.shape)
Нека започнем с линейна регресия:
from sklearn.linear_model import LinearRegression
regressor = LinearRegression()
regressor.fit(x_train, y_train)
В този момент идва мега шефа – супер дейта сцайънтист и казва: Събирай си багажа и да те няма! :D
Някой трябва да дебъгва.
print((x_train.values == 'Abnorml').sum())
col_idx = pd.np.argmax(x_train.values == 'Abnorml', axis=1).max()
x_train.iloc[:, col_idx].value_counts()
Проблемът е че не можем директно да вкараме текстови стойности в модела.
Как най-лесно да вземем всички числови стойности? pandas
има метод describe()
, който показва статистика за колоните с числови стойности.
x_train.describe()
Може да конфигурираме pandas да показва повече колони и редове:
pd.options.display.max_columns = 36
pd.options.display.max_rows = 10
Ще вземем числовите колони по относително мързелив начин, ползвайки резултата от describe
:
numeric_column_names = x_train.describe().columns
print(numeric_column_names)
Да потренираме отново
regressor.fit(x_train[numeric_column_names], y_train)
Е, нали ML беше лесен, хвърлям данните на модела или невронната мрежа и готово.
Ще подълбаем още, за да открием модела:
x_train[numeric_column_names].isnull().sum().sort_values()
x_train[['LotFrontage','GarageYrBlt', 'MasVnrArea']].hist();
В горния пример слагаме ;
за да не се отпечата върнатата стойност на hist()
в notebook-а. Правим го за да е по-прегледно. В Python няма нужда от ;
иначе.
По някакъв начин трябва да се отървем от празните стойности. Ето няколко варианта:
В нашия случай ще ги заменим с 0, защото е подходящо с тези данни. И е най-бързо.
x_train[numeric_column_names] = x_train[numeric_column_names].fillna(0)
Не обръщайте внимание на warning-а. В този случай е ОК и когато говорим за pandas по-натам ще стане по-ясно защо се случва. Може да намерите хубав tutorial по въпроса тук:
x_train[numeric_column_names].isnull().sum(axis=0).value_counts()
third time's a charm
Proverb definition:
One is sure to succeed at a task or event on the third try.
May jokingly be extended to fourth, fifth or higher numbers if success is not achieved on the third try.
regressor.fit(x_train[numeric_column_names], y_train)
Успех!
Следва да видим какво е научил:
regressor.score(x_train[numeric_column_names], y_train)
Някакво число. При максимум 1 - не е зле.
Все пак, какво предсказва? Нека да видим как са разпределени предвижданията:
predictions = regressor.predict(x_train[numeric_column_names])
print(predictions[:8])
sns.distplot(predictions);
Може да видим и как е разпределена грешката спрямо стойностите, които трябва да предвидим:
differences = (predictions - y_train).round(0)
print(differences[:8])
sns.distplot(differences);
Може да видим отклоненията между това което сме предвидили и резултата, който е трябвало да познаем:
1 - predictions / y_train
predictions / y_train
ще бъде 1 ако сме познали съвсем точно. 1 - predictions / y_train
ще ни върне отклонението в процент.
Може да начертаем същото нещо на диаграма:
plt.figure(figsize=(12,12))
sns.regplot(y_train, predictions)
plt.grid(True)
plt.show()
По x имаме оригиналната цена, а по y – нашето предвиждане. Ако моделът ни познаваше съвсем точно, всички точки щяха да лежат на правата. Тук може да видим разликите грешката, която правим.
Нека да пробваме с тест сета:
x_test[numeric_column_names].isnull().sum().sort_values(ascending=False)[:4]
x_test[numeric_column_names] = x_test[numeric_column_names].fillna(0)
predictions_test = regressor.predict(x_test[numeric_column_names])
plt.figure(figsize=(12,12))
sns.regplot(y_test, predictions_test)
plt.grid(True)
plt.show()
print("score for test:", regressor.score(x_test[numeric_column_names], y_test))
y_test - predictions_test
deviation = 1 - predictions_test / y_test
deviation[:8]
sns.distplot(deviation)
Какво научи модела? Теглата (weights) на всеки един от атрибутите. Нека го визуализираме:
plt.figure(figsize=(12,8))
barplot = sns.barplot(x=numeric_column_names, y=regressor.coef_, orient='vertical')
plt.setp(barplot.get_xticklabels(), rotation=90); plt.grid(True);
Тук може да видим някои интересни неща – олоните OverallQual
и BsmtFullBath
имат много голяма стойност. Също, наличието на гараж (GarageCars
) добавя около 16 000 към цената на апартамента.
В крайна сметка, линейната регресия е просто линейно уравнение с определени коефициенти (тези горе). Може да ги видим в самия регресор:
print(regressor.intercept_)
print(regressor.coef_)
Моделът в момента:
цена = 56000 + 17000 * OverallQual + 16000 * GarageCars + 12000 * BsmtFullBath - 14000 * KitchenAbvGr
$y = a_0 + a_1 * x_1 + a_2 * x_2 ... a_n * x_n$
print(regressor.intercept_, " + ",)
list(zip(regressor.coef_, "*"*len(regressor.coef_), numeric_column_names, ))
pd.options.display.max_rows = 10
train.dtypes
categorical_columns = train.dtypes[train.dtypes == 'object'].index
print(categorical_columns)
Би било добре.... да разгледаме какво съдържат всички колони, но...
Hint: Трябва да ги умножаваме с тегла – трябва да са числови стойности, а не низове.
Може да ползваме нещо, наречено LabelEncoder
. То ще замени замени всеки уникален низ с число.
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(["paris", "paris", "tokyo", "amsterdam"])
print("Learned classes:", le.classes_)
capitals_list = ["tokyo", "tokyo", "paris"]
print("transofrmed:", capitals_list, " to:", le.transform(["tokyo", "tokyo", "paris"]))
print("inverse:", [2, 2, 1, 0], " to:", le.inverse_transform([2, 2, 1, 0]))
Тук обаче имаме проблем.
Ако теглото за столица е 4. Тогава Амстердам ще има стойност 4 * 0 = 0
, а Париж 4 * 1 = 4
.
Но според Лъчо, Амстердамския кекс > Парижкия кроасан
.
Тук може да ни помогне OneHotEncoder
. Той ще създаде по една колона за всяка стойност, която ще бъде 0
или 1
спрямо дали колоната има тази стойност или не:
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder()
enc.fit([[0, 0, 3],
[1, 1, 0],
[0, 2, 1],
[1, 0, 2]])
print("Number of unique IDs for column:", enc.n_values_)
print("Where does the feature start/end:", enc.feature_indices_)
print(enc.transform([[0, 1, 1]]).toarray())
print(enc.transform([[1, 2, 3]]).toarray())
Обаче LabelEncoder работи само с 1 колона. Трябва да ги завъртим в цикъл за всички колони.
train = train.fillna("")
encoders = {col: LabelEncoder().fit(train[col]) for col in categorical_columns}
print(encoders['MSZoning'].classes_)
print(encoders['Street'].classes_)
Сега ще променим низивете към новите идентификатори:
def encode_categorical(data, columns, encoders):
data = data.fillna("")
return pd.DataFrame({col: encoders[col].transform(data[col]) for col in columns},
index = data.index)
train_encoded = encode_categorical(train, categorical_columns, encoders)
pd.options.display.max_columns=12
train_encoded.head(8)
И сега в one-hot:
one_hot_encoder = OneHotEncoder().fit(train_encoded)
print(one_hot_encoder.transform(train_encoded[:10]).todense())
print(one_hot_encoder.transform(train_encoded).shape)
from sklearn.linear_model import LinearRegression
one_hot_x_train = one_hot_encoder.transform(encode_categorical(x_train[categorical_columns], categorical_columns, encoders))
cat_regression = LinearRegression().fit(one_hot_x_train, y_train)
print(cat_regression.score(one_hot_x_train, y_train))
one_hot_x_test = one_hot_encoder.transform(encode_categorical(x_test[categorical_columns], categorical_columns, encoders))
print(cat_regression.score(one_hot_x_test, y_test))
Предишните резултати за трейн: 0.798 за тест: 0.841
Новите са: 0.877 и 0.743
Този модел овърфитва, но пак има стойност. Можем да съединим фичърите в общ модел.
x_train[numeric_column_names].shape, one_hot_x_train.shape
new_x_train = pd.np.concatenate([one_hot_x_train.todense(), x_train[numeric_column_names]], axis=1)
new_x_test = pd.np.concatenate([one_hot_x_test.todense(), x_test[numeric_column_names]], axis=1)
print(new_x_train.shape, new_x_test.shape)
Това почва да омръзва вече :D
all_data_lr = LinearRegression().fit(new_x_train, y_train)
print(all_data_lr.score(new_x_train, y_train))
print(all_data_lr.score(new_x_test, y_test))
0.798 за тест: 0.841
0.877 и 0.743
Новите: 0.937, за тест: 0.847 (голямо подобрение за трейн и малко за тест сета)
Да пробваме регуларизация
from sklearn.linear_model import Ridge
alpha = [0.01, 0.1, 1, 10, 100]
for a in alpha:
all_data_lr = Ridge(alpha = a).fit(new_x_train, y_train)
print('alpha:', a)
print(all_data_lr.score(new_x_train, y_train), all_data_lr.score(new_x_test, y_test))
print()
Новите резултати при alpha= 10:
трейн: 0.893310713513
тест: 0.892240478482
Моделът е доста по-добър и генерализира добре. Може и да изтиска още малко ако се търси в диапазона 0.1 до 100 на alpha.
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
sns.distplot(y_train)
plt.subplot(1, 2, 2)
sns.distplot(pd.np.log10(y_train))
Тук имаше малко магия – първата дистрибуция ила long-tail форма. Понякога като прекараме такава дистрибуция през логаритъм, тя започва да изглежда по-стандартно. В случая (произволно) избрахме логаритъм при основа 10, което свежда нещата до що-годе стандартна дистрибуция.
all_data_lr = LinearRegression().fit(new_x_train, pd.np.log10(y_train))
print(all_data_lr.score(new_x_train, pd.np.log10(y_train)))
print(all_data_lr.score(new_x_test, pd.np.log10(y_test)))
# Предишни резултати:
# трейн: 0.893310713513
# тест: 0.892240478482
Вече имаме по-добри резултати.
Изглежда, обаче, че модела overfit-а. Може да пробваме с регуляризация. Нека да потърсим подходящ alpha
параметър:
alphas = [0.01, 0.1, 1, 10, 100]
for alpha in alphas:
all_data_lr = Ridge(alpha = alpha).fit(new_x_train, pd.np.log10(y_train))
print('alpha:', alpha)
print(all_data_lr.score(new_x_train, pd.np.log10(y_train)), all_data_lr.score(new_x_test, pd.np.log10(y_test)))
print()
from sklearn.metrics import r2_score
for alpha in [0.01, 0.1, 1, 10, 100]:
all_data_lr = Ridge(alpha = alpha).fit(new_x_train, pd.np.log10(y_train))
print('alpha:', alpha)
print(r2_score(10**all_data_lr.predict(new_x_train), y_train))
print(r2_score(10**all_data_lr.predict(new_x_test), y_test))
print()
print(r2_score(y_train, 10**all_data_lr.predict(new_x_train)))
print(r2_score(y_test, 10**all_data_lr.predict(new_x_test)))
alpha: 10
0.914572813642
0.925776887973
Хубава статия за още масажиране на данните:
https://www.kaggle.com/pmarcelino/comprehensive-data-exploration-with-python/notebook
Може да ползваме PolynomialFeatures
да генерираме всички възможни полиноми от втора степен с входните данни:
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree=2, interaction_only=False, include_bias=True)
poly_features.fit(new_x_train)
poly_x_train = poly_features.transform(new_x_train)
poly_x_test = poly_features.transform(new_x_test)
print(new_x_train.shape)
print(poly_x_train.shape)
Нека да видим резултата с различни регуляризации.
for a in [10e6, 10e7, 10e8, 10e9, 10e10]:
print('alpha:', a)
all_data_lr = Ridge(alpha=a).fit(poly_x_train, pd.np.log10(y_train))
print(all_data_lr.score(poly_x_train, pd.np.log10(y_train)), all_data_lr.score(poly_x_test, pd.np.log10(y_test)))
print()
Този модел прави стабилен overfitting – явно е твърде сложен.
Като правило, когато има повече колони, отколкото редове, това се случва.
train = pd.read_csv('data/house-prices/train.csv', index_col=['Id'])
test = pd.read_csv('data/house-prices/test.csv', index_col=['Id'])
def pipeline_data(data):
_numeric = data[numeric_column_names].fillna(0)
_categorical = data[categorical_columns].fillna("")
_encoded = encode_categorical(_categorical, categorical_columns, encoders)
_one_hot_encoded = one_hot_encoder.transform(_encoded)
_merged = pd.np.concatenate([_one_hot_encoded.todense(), _numeric], axis=1)
return _merged
full_x = pipeline_data(train)
full_y = train['SalePrice']
# Fix test set missing data problems
test['MSZoning'].fillna('RL', inplace=True)
test['Utilities'].fillna('AllPub', inplace=True)
test['Exterior1st'].fillna('VinylSd', inplace=True)
test['Exterior2nd'].fillna('VinylSd', inplace=True)
test['KitchenQual'].fillna('TA', inplace=True)
test['Functional'].fillna('Typ', inplace=True)
test['SaleType'].fillna('WD', inplace=True)
test_set = pipeline_data(test)
ridge = Ridge(alpha=0.1).fit(full_x, pd.np.log10(full_y))
predictions = 10**ridge.predict(test_set)
predictions_df = pd.DataFrame({"Id":test.index, "SalePrice":predictions})
predictions_df.to_csv("/Users/lachezar/Downloads/submit_me.csv", index=False)
predictions_df.head()
Има ли значение дали правим регресия или класификация? Ако колоната, която предсказваме съдържа ['скъпа', 'евтина', 'средна']
Има ли значение кой модел ползваме - LinearRegression, RandomForest, Neural Network, kNN?
Колко кобминации можем да направим за модела?
4 часа разглеждане на данни, чистене и оформяне на pipeline и фичъри. 20 минути трениране и оптимизиране на модели.
Прочетете документациите
Упражнение
Изберете си dataset от https://www.kaggle.com/datasets и му направете "Exploratory data analysis" в jupyter notebook. Изберете си колона и натренирайте Линейна или логистична регресия. Оценете модела срещу тренировачното и тестово множество.