1:改進我們的特征
在上一個任務中,我們完成了我們在Kaggle上一個機器學習比賽的第一個比賽提交泰坦尼克號:災難中的機器學習。
可是我們提交的分數並不是非常高。有三種主要的方法可以讓我們能夠提高他:
- 用一個更好的機器學習算法;
- 生成更好的特征;
- 合並多重機器學習算法。
在這節的任務總,我們將會完成這三個。首先,我們將找到一個不同的算法來使用邏輯回歸——隨記森林(randaom forests)。
2:隨機森林簡介
正如我們在上一節任務中順便提到的,決策樹能從數據中學會非線性趨勢。一個例子如下:
| Age | Sex | Survived |
|---|---|---|
| 5 | 0 | 1 |
| 30 | 1 | 0 |
| 70 | 0 | 1 |
| 20 | 0 | 1 |
正如你所見,Age年齡和Survived幸存之間並不是線性相關——30歲的人沒有幸存,但是70歲和20歲的人幸存了。
我們反而可以在Age,Sex,Survived之間的關系上建立一個決策樹模型。你之前可能見過決策樹或者流程圖,決策樹算法在概念上和他們並沒有任何的不同。我們從樹的根節點開始我們的數據行,然后直到我們能准確的將行分別插到子節點。一個例子:

在上圖中,我們得到了我們初步的數據和:
- 做了一個初步的划分。右邊是
Age超過29的所有行,左邊是Age小於19的所有行。 - 左邊那組全是幸存者,所以我們將其定為葉子節點並賦值給
Survived結果1。 - 右邊那組並不是全都有同樣的結果,所以我們再以
Sex列的值划分一次。 - 最后我們右邊有了兩個葉子節點,一個全是幸存者,另一個都沒有幸存。
對於一個新行,我們可以利用這個決策樹來計算出幸存結果。
| Age | Sex | Survived |
|---|---|---|
| 40 | 0 | ? |
根據我們的樹,我們首先將划分到右邊,然后再划分到左邊。我們將預測上面那個人會幸存。(Survived = 1)
決策樹有一個重大的缺點就是他們在訓練數據集上會過擬合。因為我們根據划分建立了一個很深的決策樹,我們結束的時候回有很多大量的規則源於訓練數據集中特殊的奇怪特征,並且無法歸納這些的新的數據集。
隨機森林可以起到一些幫助。隨機森林,我們使用微隨機輸入數據來構造出許許多多的樹,並且微隨機的划分這些點。隨機森林中的每一顆樹都是訓練數據全集的一個隨機子集。
每一棵樹中的每個划分點都能在任一隨機子集的潛在列上執行划分。通過求所有樹的平均預測值,我們能得到一個更好的整體預測和最小的過擬合。
3: 實現一個隨機森林
感謝上帝,sklearn已經實現了一個非常棒的隨機森林。我們可以使用它在我們的數據上構建一個隨機森林並且生成交叉驗證預測。
說明
對titanic數據框(已經加載了的數據)做一個交叉驗證的預測。用3交叉。用儲存在alg的隨機森林算法去做交叉驗證。用predictions去預測Survived列。將結果賦值到scores。
你可以使用cross_validation.cross_val_score來完成這些。
在做完交叉驗證預測之后,將我們的scores的平均值打印出來。
from sklearn import cross_validation
from sklearn.ensemble import RandomForestClassifier
predictors = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]
# Initialize our algorithm with the default paramters
# n_estimators is the number of trees we want to make
# min_samples_split is the minimum number of rows we need to make a split
# min_samples_leaf is the minimum number of samples we can have at the place where a tree branch ends (the bottom points of the tree)
alg = RandomForestClassifier(random_state=1, n_estimators=10, min_samples_split=2, min_samples_leaf=1)
答案:
from sklearn import cross_validation
from sklearn.ensemble import RandomForestClassifier
predictors = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]
# Initialize our algorithm with the default paramters
# n_estimators is the number of trees we want to make
# min_samples_split is the minimum number of rows we need to make a split
# min_samples_leaf is the minimum number of samples we can have at the place where a tree branch ends (the bottom points of the tree)
alg = RandomForestClassifier(random_state=1, n_estimators=10, min_samples_split=2, min_samples_leaf=1)
# Compute the accuracy score for all the cross validation folds. (much simpler than what we did before!)
scores = cross_validation.cross_val_score(alg, titanic[predictors], titanic["Survived"], cv=3)
# Take the mean of the scores (because we have one for each fold)
print(scores.mean())
4:調參
我們可以做的最簡單的能夠提高隨機森林的精度的第一件事情就是增加我們使用的樹的數量。訓練更多的樹會花費更多的時間,但是事實是使用更多的樹求數據的不同子集的平均預測值會很大的提升預測的准確率(一定程度上)。
我們也可以調整min_samples_split和min_samples_leaf變量來減少過擬合。
決策樹是如何工作的(正如我們在視頻中解釋的一樣),往下一直都會有切分節點,樹過深會導致擬合了數據中一些獨特的特征,但是不是真的有用的特征。因此,增加min_samples_split和min_samples_leaf能夠減少過擬合,實際上當我們在未知數據上做預測時這將會提高我們的分數。一個較少過擬合的模型,會使結果變得更好,使得在未知數據中表現得更好,但是在可見數據中卻不好。
指令
為了提高准確率,當我們初始化alg的時候就要調整我們的參數。在titanic數據框上做交叉驗證預測。使用predictors預測Survived列。將結果賦值給scores。
在做完交叉驗證預測之后,將我們的scores的平均值打印出來。
alg = RandomForestClassifier(random_state=1, n_estimators=150, min_samples_split=4, min_samples_leaf=2)
答案:
alg = RandomForestClassifier(random_state=1, n_estimators=150, min_samples_split=4, min_samples_leaf=2)
# Compute the accuracy score for all the cross validation folds. (much simpler than what we did before!)
scores = cross_validation.cross_val_score(alg, titanic[predictors], titanic["Survived"], cv=3)
# Take the mean of the scores (because we have one for each fold)
print(scores.mean())
5:生成新特征
我們也可以生成新特征。一些點子:
- 名字的長度——這和那人有多富有,所以在泰坦尼克上的位置有關。
- 一個家庭的總人數(
SibSp+Parch)。
一個很簡答的方法就是使用pandas數據框的.apply方法來生成特征。這會對你傳入數據框(dataframe)或序列(series)的每一個元素應用一個函數。我們也可以傳入一個lambda函數使我們能夠定義一個匿名函數。
一個匿名的函數的語法是lambda x:len(x)。x將傳入的值作為輸入值——在本例中,就是乘客的名字。表達式的右邊和返回結果將會應用於x。.apply方法讀取這些所有輸出並且用他們構造出一個pandas序列。我們可以將這個序列賦值到一個數據框列。
demo:
# Generating a familysize column
titanic["FamilySize"] = titanic["SibSp"] + titanic["Parch"]
# The .apply method generates a new series
titanic["NameLength"] = titanic["Name"].apply(lambda x: len(x))
6:使用頭銜
我們可以從乘客的名字中提取出他們的頭銜。頭銜的格式是Master.,Mr.,Mrs.。有一些非常常見的頭銜,也有一些“長尾理論”中的一次性頭銜只有僅僅一個或者兩個乘客使用。第一步使用正則表達式提取頭銜,然后將每一個唯一頭銜匹配成(map)整型數值。
之后我們將得到一個准確的和Title相對應的數值列。
import re
# A function to get the title from a name.
def get_title(name):
# Use a regular expression to search for a title. Titles always consist of capital and lowercase letters, and end with a period.
title_search = re.search(' ([A-Za-z]+)\.', name)
# If the title exists, extract and return it.
if title_search:
return title_search.group(1)
return ""
# Get all the titles and print how often each one occurs.
titles = titanic["Name"].apply(get_title)
print(pandas.value_counts(titles))
# Map each title to an integer. Some titles are very rare, and are compressed into the same codes as other titles.
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Dr": 5, "Rev": 6, "Major": 7, "Col": 7, "Mlle": 8, "Mme": 8, "Don": 9, "Lady": 10, "Countess": 10, "Jonkheer": 10, "Sir": 9, "Capt": 7, "Ms": 2}
for k,v in title_mapping.items():
titles[titles == k] = v
# Verify that we converted everything.
print(pandas.value_counts(titles))
# Add in the title column.
titanic["Title"] = titles
7:家族
我們也可以生成一個特征來表示哪些人是一個家族。因為幸存看起來非常依靠你的家族和你旁邊的人,這是一個成為好特征的好機會。
為了完成這個任務,我們將通過FamilySize連接某些人的姓來得到一個家庭編號。然后我們將基於他們的家庭編號給每個人賦值一個代碼。
demo:
import operator
# A dictionary mapping family name to id
family_id_mapping = {}
# A function to get the id given a row
def get_family_id(row):
# Find the last name by splitting on a comma
last_name = row["Name"].split(",")[0]
# Create the family id
family_id = "{0}{1}".format(last_name, row["FamilySize"])
# Look up the id in the mapping
if family_id not in family_id_mapping:
if len(family_id_mapping) == 0:
current_id = 1
else:
# Get the maximum id from the mapping and add one to it if we don't have an id
current_id = (max(family_id_mapping.items(), key=operator.itemgetter(1))[1] + 1)
family_id_mapping[family_id] = current_id
return family_id_mapping[family_id]
# Get the family ids with the apply method
family_ids = titanic.apply(get_family_id, axis=1)
# There are a lot of family ids, so we'll compress all of the families under 3 members into one code.
family_ids[titanic["FamilySize"] < 3] = -1
# Print the count of each unique id.
print(pandas.value_counts(family_ids))
titanic["FamilyId"] = family_ids
8:找出最好的特征
在任何的機器學習任務中,特征工程都是最重要的部分,有許多的特征可以讓我們計算。但是我們也需要找到一種方法來計算出哪個特征是最好的。
一種方法就是使用單特征選擇器(univariate feature selection),這種方法的本質是一列一列的遍歷計算出和我們想要預測的結果(Survived)最密切關聯的那一列。
和以前一樣,sklearn有一個叫做SelectKBest的函數將會幫助我們完成特征選擇。這個函數會從數據中選出最好的特征,並且允許我們指定選擇的數量。
指令
我們已經更新predictors。為titanic數據框做用3折交叉驗證預測。用predictors預測Survived列。將結果賦值給scores。
在做完交叉驗證預測之后,將我們的scores的平均值打印出來。
import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
predictors = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked", "FamilySize", "Title", "FamilyId"]
# Perform feature selection
selector = SelectKBest(f_classif, k=5)
selector.fit(titanic[predictors], titanic["Survived"])
# Get the raw p-values for each feature, and transform from p-values into scores
scores = -np.log10(selector.pvalues_)
# Plot the scores. See how "Pclass", "Sex", "Title", and "Fare" are the best?
plt.bar(range(len(predictors)), scores)
plt.xticks(range(len(predictors)), predictors, rotation='vertical')
plt.show()
# Pick only the four best features.
predictors = ["Pclass", "Sex", "Fare", "Title"]
alg = RandomForestClassifier(random_state=1, n_estimators=150, min_samples_split=8, min_samples_leaf=4)
答案:
import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
predictors = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked", "FamilySize", "Title", "FamilyId"]
# Perform feature selection
selector = SelectKBest(f_classif, k=5)
selector.fit(titanic[predictors], titanic["Survived"])
# Get the raw p-values for each feature, and transform from p-values into scores
scores = -np.log10(selector.pvalues_)
# Plot the scores. See how "Pclass", "Sex", "Title", and "Fare" are the best?
plt.bar(range(len(predictors)), scores)
plt.xticks(range(len(predictors)), predictors, rotation='vertical')
plt.show()
# Pick only the four best features.
predictors = ["Pclass", "Sex", "Fare", "Title"]
alg = RandomForestClassifier(random_state=1, n_estimators=150, min_samples_split=8, min_samples_leaf=4)
# Compute the accuracy score for all the cross validation folds. (much simpler than what we did before!)
scores = cross_validation.cross_val_score(alg, titanic[predictors], titanic["Survived"], cv=3)
# Take the mean of the scores (because we have one for each fold)
print(scores.mean())
9:梯度提升(Gradient Boosting)
另外一種方法是以決策樹為基礎的梯度提升分類器。提升包含了一個接一個的訓練決策樹,並且將一個樹的誤差傳入到下一棵樹。所以每一顆樹都是以它之前的所有樹為基礎構造的。不過如果我們建造太多的樹會導致過擬合。當你得到了100棵左右的樹,這會非常容易過擬合和訓練出數據集中的怪特征。當我們的數據集極小時,我們將會把樹的數量限制在25。
另外一種防止過擬合的方法就是限制在梯度提升過程中建立的每一棵樹的深度。我們將樹的高度限制到3來分避免過擬合。
我們將試圖用提升來替換掉我們的隨機森林方法並且觀察是否會提升我們的預測准確率。
10:集成(Ensembling)
為了提升我們的預測准確率,我們能做的一件事就是集成不同的分類器。集成的意思就是我們利用一系列的分類器的信息來生成預測結果而不是僅僅用一個。在實踐中,這意味着我們是求他們預測結果的平均值。
通常來說,我們集成越多的越不同的模型,我們結果的准確率就會越高。多樣性的意思是模型從不同列生成結果,或者使用一個非常不同的方法來生成預測結果。集成一個隨機森林和一個決策樹大概不會得到一個非常好的結果,因為他們非常的相似。換句話說,集成一個線性回歸和一個隨機森林可以工作得非常棒。
一個關於集成的警示就是我們使用的分類器的准確率必須都是差不多的。集成一個分類器的准確率比另外一個差得多將會導致最后的結果也變差。
在這一節中,我們將會集成基於大多數線性預測訓練的邏輯回歸(有一個線性排序,和Survived有一些關聯)和一個在所有預測元素上訓練的梯度提升樹。
在我們集成的時候會保持事情的簡單——我們將會求我們從分類器中得到的行概率(0~1)的平均值,然后假定所有大於0.5的匹配成1,小於等於0.5的匹配成0。
demo:
from sklearn.ensemble import GradientBoostingClassifier
import numpy as np
# The algorithms we want to ensemble.
# We're using the more linear predictors for the logistic regression, and everything with the gradient boosting classifier.
algorithms = [
[GradientBoostingClassifier(random_state=1, n_estimators=25, max_depth=3), ["Pclass", "Sex", "Age", "Fare", "Embarked", "FamilySize", "Title", "FamilyId"]],
[LogisticRegression(random_state=1), ["Pclass", "Sex", "Fare", "FamilySize", "Title", "Age", "Embarked"]]
]
# Initialize the cross validation folds
kf = KFold(titanic.shape[0], n_folds=3, random_state=1)
predictions = []
for train, test in kf:
train_target = titanic["Survived"].iloc[train]
full_test_predictions = []
# Make predictions for each algorithm on each fold
for alg, predictors in algorithms:
# Fit the algorithm on the training data.
alg.fit(titanic[predictors].iloc[train,:], train_target)
# Select and predict on the test fold.
# The .astype(float) is necessary to convert the dataframe to all floats and avoid an sklearn error.
test_predictions = alg.predict_proba(titanic[predictors].iloc[test,:].astype(float))[:,1]
full_test_predictions.append(test_predictions)
# Use a simple ensembling scheme -- just average the predictions to get the final classification.
test_predictions = (full_test_predictions[0] + full_test_predictions[1]) / 2
# Any value over .5 is assumed to be a 1 prediction, and below .5 is a 0 prediction.
test_predictions[test_predictions <= .5] = 0
test_predictions[test_predictions > .5] = 1
predictions.append(test_predictions)
# Put all the predictions together into one array.
predictions = np.concatenate(predictions, axis=0)
# Compute accuracy by comparing to the training data.
accuracy = sum(predictions[predictions == titanic["Survived"]]) / len(predictions)
print(accuracy)
11:在測試集上匹配我們的變化
我們將在結尾討論可以使這個分析變得更好我們能夠做的很多事情,但是現在讓我們來完成一個提交。
第一筆我們將在訓練數據集上做的改動匹配到測試數據集上,就像我們在上一章做的那樣。我們將測試數據集讀取到titanic_test中。我們匹配我們的改變:
- 生成
NameLength,表示名字的長度。 - 生成
FamilySize,表示家庭的大小。 - 添加
Title列,保持和我們之前做的匹配一樣。 - 添加
FamilyId列,測試數據集合訓練數據集的id保持一致。
指令
添加
NameLength列到titanic_test中。和我們在titanic數據框中使用同樣的方法來完成。
# First, we'll add titles to the test set.
titles = titanic_test["Name"].apply(get_title)
# We're adding the Dona title to the mapping, because it's in the test set, but not the training set
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Dr": 5, "Rev": 6, "Major": 7, "Col": 7, "Mlle": 8, "Mme": 8, "Don": 9, "Lady": 10, "Countess": 10, "Jonkheer": 10, "Sir": 9, "Capt": 7, "Ms": 2, "Dona": 10}
for k,v in title_mapping.items():
titles[titles == k] = v
titanic_test["Title"] = titles
# Check the counts of each unique title.
print(pandas.value_counts(titanic_test["Title"]))
# Now, we add the family size column.
titanic_test["FamilySize"] = titanic_test["SibSp"] + titanic_test["Parch"]
# Now we can add family ids.
# We'll use the same ids that we did earlier.
print(family_id_mapping)
family_ids = titanic_test.apply(get_family_id, axis=1)
family_ids[titanic_test["FamilySize"] < 3] = -1
titanic_test["FamilyId"] = family_ids
答案:
# First, we'll add titles to the test set.
titles = titanic_test["Name"].apply(get_title)
# We're adding the Dona title to the mapping, because it's in the test set, but not the training set
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Dr": 5, "Rev": 6, "Major": 7, "Col": 7, "Mlle": 8, "Mme": 8, "Don": 9, "Lady": 10, "Countess": 10, "Jonkheer": 10, "Sir": 9, "Capt": 7, "Ms": 2, "Dona": 10}
for k,v in title_mapping.items():
titles[titles == k] = v
titanic_test["Title"] = titles
# Check the counts of each unique title.
print(pandas.value_counts(titanic_test["Title"]))
# Now, we add the family size column.
titanic_test["FamilySize"] = titanic_test["SibSp"] + titanic_test["Parch"]
# Now we can add family ids.
# We'll use the same ids that we did earlier.
print(family_id_mapping)
family_ids = titanic_test.apply(get_family_id, axis=1)
family_ids[titanic_test["FamilySize"] < 3] = -1
titanic_test["FamilyId"] = family_ids
titanic_test["NameLength"] = titanic_test["Name"].apply(lambda x: len(x))
12:在測試集上做預測
現在我們已經有了一些更好的預測了,所以讓我們來創建另外一個提交。
指令
通過將預測結果小於等於0.5的轉換成0,將大於0.5的轉換成1,將所有的結果轉換成非0即1。
然后,用.astype(int)方法將預測結果轉換成整型——如果你不這樣做,Kaggle將會給你0分。
最后,生成一個第一列是PassengerId,第二列是Survived(這就是預測結果)。
predictors = ["Pclass", "Sex", "Age", "Fare", "Embarked", "FamilySize", "Title", "FamilyId"]
algorithms = [
[GradientBoostingClassifier(random_state=1, n_estimators=25, max_depth=3), predictors],
[LogisticRegression(random_state=1), ["Pclass", "Sex", "Fare", "FamilySize", "Title", "Age", "Embarked"]]
]
full_predictions = []
for alg, predictors in algorithms:
# Fit the algorithm using the full training data.
alg.fit(titanic[predictors], titanic["Survived"])
# Predict using the test dataset. We have to convert all the columns to floats to avoid an error.
predictions = alg.predict_proba(titanic_test[predictors].astype(float))[:,1]
full_predictions.append(predictions)
# The gradient boosting classifier generates better predictions, so we weight it higher.
predictions = (full_predictions[0] * 3 + full_predictions[1]) / 4
答案:
predictors = ["Pclass", "Sex", "Age", "Fare", "Embarked", "FamilySize", "Title", "FamilyId"]
algorithms = [
[GradientBoostingClassifier(random_state=1, n_estimators=25, max_depth=3), predictors],
[LogisticRegression(random_state=1), ["Pclass", "Sex", "Fare", "FamilySize", "Title", "Age", "Embarked"]]
]
full_predictions = []
for alg, predictors in algorithms:
# Fit the algorithm using the full training data.
alg.fit(titanic[predictors], titanic["Survived"])
# Predict using the test dataset. We have to convert all the columns to floats to avoid an error.
predictions = alg.predict_proba(titanic_test[predictors].astype(float))[:,1]
full_predictions.append(predictions)
# The gradient boosting classifier generates better predictions, so we weight it higher.
predictions = (full_predictions[0] * 3 + full_predictions[1]) / 4
predictions[predictions <= .5] = 0
predictions[predictions > .5] = 1
predictions = predictions.astype(int)
submission = pandas.DataFrame({
"PassengerId": titanic_test["PassengerId"],
"Survived": predictions
})
13:最后的感想
現在,我們有了一個提交!這應該能讓你在排行榜得到0.799的分數。你可以使用submission.to_csv("kaggle.csv",index=False)生成一個提交文件。
你任然可以在特征工程上做很多事情:
- 嘗試用和船艙相關的特征。
- 觀察家庭大小特征是否會有幫助——一個家庭中女性的數量多使全家更可能幸存?
- 乘客的國籍能為其幸存提高什么幫助?
在算法方面我們也有非常多的事情可以做:
- 嘗試在集成中加入隨機森林分類器。
- 在這個數據上支持向量機也許會很有效。
- 我們可以試試神經網絡。
- 提升一個不同的基礎匪類器也許會更好。
集成方法:
- 多數表決是比概率平均更好的集成方法?
這個數據集很容易過擬合,因為沒有很多的數據,所以你將會得到很小的准確率提升。你也可以測試一個不同的有更多數據和更多特征輸入的Kaggle比賽。
希望你喜歡這個指南,祝你在機器學習競賽中好運!
