【机器学习实战】-- Titanic 数据集(2)-- 感知机
1. 写在前面:
本篇属于实战部分,更注重于算法在实际项目中的应用。如需对感知机算法本身有进一步的了解,可参考以下链接,在本人学习的过程中,起到了很大的帮助:
统计学习方法 李航
感知机原理小结 https://www.cnblogs.com/pinard/p/6042320.html
空间中任意一点到超平面距离的公式推导 https://www.cnblogs.com/yanganling/p/8007050.html
2. 数据集:
数据集地址:https://www.kaggle.com/c/titanic
Titanic数据集是Kaggle上参与人数最多的项目之一。数据本身简单小巧,适合初学者上手,深入了解比较各个机器学习算法。
数据集包含11个变量:PassengerID、Pclass、Name、Sex、Age、SibSp、Parch、Ticket、Fare、Cabin、Embarked,通过这些数据来预测乘客在Titanic事故中是否幸存下来。
3. 算法简介:
感知机属于分类模型,是一个古老而基础的模型,与支持向量机有一定程度的相似,同时也是神经网络的基础。
感知机属于线性模型,因此线性模型中常用的L1、L2正则化同样使用与感知机。
3.1 感知机模型:
由于不同材料中对多个多维数据的表达不尽相同,这里参考《统计学习方法》中李航老师的写法:
给定一个数据集:$T=\left \{ \left ( x_{1}, y_{1} \right ), \left ( x_{2}, y_{2} \right ), ..., \left ( x_{N}, y_{N} \right ) \right \}$,其中$x_{i}\in X\subseteq \bf{R^{n}}$,$y_{i} \in Y = \left \{+1, -1 \right \}$,$i = 1,2,...,N$。这代表数据集共有 N 对 实例,每个实例 $x_{i}$都是n维的。
从输入空间到输出空间的如下函数被称作感知机模型:
$f(x) = \rm{sign} \left( w \cdot x + b \right) $,其中sign是符号函数:$sign(x)= \begin{cases} +1& {x\geq0}\\ -1& {x< 0} \end{cases}$
3.2 感知机损失函数:
一般情况下,损失函数的选取是所有实例的预测值$f(x_{i})$与实际值$y_{i}$的差。而在感知机中,我们有一个特殊的损失函数,它更便于理解,也更便于后续的算法学习过程。我们将损失函数定义为:误分类点到超平面$S$的总距离。
首先,需要知道的是空间$\bf{R^{n}}$中任意一点$x_{0}$到超平面 $w \cdot x + b=0$的距离是:$ \frac{1}{\left \| w \right \|} \left | w \cdot x_{0} + b \right | $
其次,由于 $w \cdot x_{i} + b >0$时,$y_{i} = -1$,$w \cdot x_{i} + b <0$时,$y_{i} = 1$,因此,误分类点$x_{i}$到超平面$S$的距离是 $-\frac{1}{\left \| w \right \|} \left ( w \cdot x_{i} + b \right )$
由此,假设超平面$S$的误分类点集合为$M$,则所有误分类点到超平面$S$的总距离为:$-\frac{1}{\left \| w \right \|} \sum_{x_{i}\in M}y_{i} \left ( w \cdot x_{i} + b \right )$,感知机中只考虑函数间隔(即不考虑分母$\frac{1}{\left \| w \right \|}$)
最终,可以得到感知机的损失函数为:
$$ L\left ( w, b \right ) = -\sum_{x_{i} \in M} y_{i} \left ( w \cdot x_{i} + b \right ) $$
3.3 感知机学习过程
感知机的学习过程即求解感知机损失函数的最小化问题:
$$ \min_{w,b} L(w,b) = -\sum_{x_{i} \in M} y_{i} (w \cdot x_{i} + b) $$
求解过程采用的是随机梯度下降 -- SGD(stochastic gradient descent),损失函数的梯度为:
$$ \nabla_{w} L(w,b) = - \sum_{x_{i} \in M}y_{i}x_{i} $$
$$ \nabla_{b} L(w,b) = -\sum_{x_{i} \in M}y_{i} $$
每次选取随机的一个误分类点$(x_{i}, y_{i})$,对 $w$, $b$进行更新:
$$ w = w + \alpha y_{i}x_{i} $$
$$ b = b + \alpha y_{i} $$
其中 $\alpha$ 是学习率(learning rate)。
如果原原数据集是线性可分的,在合适的学习率下,经过迭代,损失函数$ L(w,b) $将减小至0。如果原数据是线性不可分的话,则需要设定其他的迭代终止条件。
3.4 感知机学习的对偶形式
感知机的对偶形式和原始形式相对应,但能提高求解速度。下面具体解释一下它是怎么提高求解速度的。
首先,假设$w$和$b$的初始取值都为0。对于误分类点$ (x_{i}, y_{i}) $,在学习过程中更新了$n_{i}$次,则 $w$, $b$ 关于$(x_{i}, y_{i})$的增量分别为 $n_{i}\alpha y_{i}x_{i}$ 和 $n_{i}\alpha y_{i}$。
接下来,考虑所有误分类点,并令 $\eta_{i} = n_{i}\alpha$,则最后学习到的 $w$ 和 $b$ 分别为:
$$ w = \sum_{i =1}^{N} \eta_{i}y_{i}x_{i} $$
$$ b = \sum_{i=1}^{N} \eta_{i}y_{i} $$
最后,有了以上结果,将$w$带回感知机模型 $f(x) = \rm{sign}\left ( \sum_{j=1}^{N} \alpha_{j}y_{j}x_{j} \cdot x + b \right ) $:
*) 每次迭代过程中,判断 $(x_{i}, y_{i})$ 是否为误分类点时,判断方程为:$y_{i}\left ( w \cdot x_{i} + b \right ) = y_{i}\left ( \sum_{j=1}^{N} \eta_{j}y_{j}x_{j} \cdot x_{i} + b \right )$
*) 如果判断方程 $y_{i}\left ( \sum_{j=1}^{N} \eta_{j}y_{j}x_{j} \cdot x_{i} + b \right ) \leq 0$,则更新 $\eta_{i} = \eta_{i} + \alpha$, $b = b + \alpha y_{i}$
对偶形式中能够提升求解速度的原因啊就在于,在对偶形式中,训练实例仅以内积的形式出现。预先将训练集中的实例间的内积计算出来,后续每次迭代过程中直接使用。而这个矩阵就是Gram矩阵:
$$ G=\left [ x_{i} \cdot x_{j} \right ]_{N\times N} $$
4. 实战:
在本篇实战中,将在上一篇 【机器学习实战】-- Titanic 数据集(1)-- 朴素贝叶斯 的基础上,进一步介绍sklearn库在实战中的应用:
- 使用Pipeline将数据清洗和模型预测结合起来,使代码更简洁(但同时也会带来麻烦)
- 使用GridSearch选取最优参数
- Perceptron的正则化
首先,和之前一样,将数据进行导入:
1 import pandas as pd 2 import numpy as np 3 import matplotlib.pyplot as plt 4 from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler 5 from sklearn.impute import SimpleImputer 6 from sklearn.pipeline import Pipeline, FeatureUnion 7 from sklearn.model_selection import cross_val_score, GridSearchCV, ParameterGrid, StratifiedKFold 8 from sklearn.linear_model import Perceptron 9 from sklearn.metrics import accuracy_score, precision_score, recall_score 10 from sklearn.base import TransformerMixin, BaseEstimator 11 12 13 class DataFrameSelector(BaseEstimator, TransformerMixin): 14 def __init__(self, attribute_name): 15 self.attribute_name = attribute_name 16 17 def fit(self, x, y=None): 18 return self 19 20 def transform(self, x): 21 return x[self.attribute_name].values 22 23 24 # Load data 25 data_train = pd.read_csv('train.csv') 26 27 train_x = data_train.drop('Survived', axis=1) 28 train_y = data_train['Survived']
4.1 数据清洗和模型预测结合起来
1 # Data cleaning 2 cat_attribs = ['Pclass', 'Sex', 'Embarked'] 3 dis_attribs = ['SibSp', 'Parch'] 4 con_attribs = ['Age', 'Fare'] 5 6 # encoder: OneHotEncoder()、OrdinalEncoder() 7 cat_pipeline = Pipeline([ 8 ('selector', DataFrameSelector(cat_attribs)), 9 ('imputer', SimpleImputer(strategy='most_frequent')), 10 ('encoder', OneHotEncoder()), 11 ]) 12 13 dis_pipeline = Pipeline([ 14 ('selector', DataFrameSelector(dis_attribs)), 15 ('scaler', StandardScaler()), 16 ('imputer', SimpleImputer(strategy='most_frequent')), 17 ]) 18 19 con_pipeline = Pipeline([ 20 ('selector', DataFrameSelector(con_attribs)), 21 ('scaler', StandardScaler()), 22 ('imputer', SimpleImputer(strategy='mean')), 23 ]) 24 25 full_pipeline = FeatureUnion( 26 transformer_list=[ 27 ('con_pipeline', con_pipeline), 28 ('dis_pipeline', dis_pipeline), 29 ('cat_pipeline', cat_pipeline), 30 ] 31 ) 32 33 # Applying the model without knowing train_x_cleaned 34 full_pipeline_estimator = Pipeline([ 35 ('clean', full_pipeline), 36 ('predict', Perceptron()), 37 ]) 38 39 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=2) 40 41 param_grid = [ 42 {'predict__penalty': ['l2', 'l1', 'elasticnet', None], 43 'predict__alpha': [0.002, 0.005, 0.0001], 44 'predict__class_weight': ['balanced', None]}, 45 ] 46 47 grid_search = GridSearchCV(full_pipeline_estimator, param_grid, cv=cv, scoring='accuracy') 48 grid_search.fit(train_x, train_y) 49 predicted_y = grid_search.predict(train_x)
将数据清洗和模型预测结合起来,看似十分简洁,但其中隐藏着几个问题:
Q1. full_pipeline_estimator 无法直观地获取清洗后的数据,因为清洗只是整个pipeline中的一部分,直接使用 full_pipeline_estimator.fit_transform(train_x, train_y)甚至会报错,因为整个pipeline中最后一步Perception()并没有fit_transform()的用法。
Q2. 强行将 数据清洗 和 模型预测 结合在一起,实际上并没有对整个算法运行、参数调优起到什么作用。可以看到,在 GridSearchCV 的 param_grid 参数中,都是属于 ‘predict’ 这一步的参数。
Q3. 由于在数据清洗中,有涉及到缺失值的处理,无论是 ‘mean’ 还是 ‘most frequent’ 都是对全体数据而言的。而如果在 GridSearchCV 中 使用,程序将对每个fold中的数据单独进行数据清理,将导致每个fold中对缺失值的处理不一致。
对于第1,第2个问题,我们可以参考sklearn的官方文档获得相应解答:
“The purpose of the pipeline is to assemble several steps that can be cross-validated together while setting different parameters. For this, it enables setting parameters of the various steps using their names and the parameter name separated by a ‘__’. ”
Pipeline的目的,是将若干个可以交叉验证的步骤组合在一起,并且同时可以设置不同的参数。为此,它能够通过 “步骤名_参数名” 的形式来设置参数。
A1:因此,对于问题1,可以通过以来代码来获取清洗后的数据:
1 train_x_cleaned = grid_search.best_estimator_.named_steps['clean'].fit_transform(train_x)
A2:对于问题2,从官方文档中也获得了解答:无需设置参数的步骤,不需要放入pipeline中。那我们可以将什么放入piepline中呢,比如数据清洗后的降维,如PCA等。
A3:第三个问题则是比较严重的问题。
首先,我们查看整个 grid_search 的最优解:
In[2]: grid_search.best_params_ Out[2]: {'predict__alpha': 0.0001, 'predict__class_weight': None, 'predict__penalty': 'l2'}
1 In[21]: 2 print('Result of full pipeline estimator:') 3 print(accuracy_score(train_y, predicted_y)) 4 print(precision_score(train_y, predicted_y)) 5 print(recall_score(train_y, predicted_y)) 6 Out[21]: 7 Result of full pipeline estimator: 8 0.7833894500561167 9 0.752542372881356 10 0.6491228070175439
接下来,对每个 fold中的最优解进行查看,查看每个fold中的最优解和整个grid_search的最优解是否一致。在这一过程中,我们希望找到的最优解是针对这个fold中的所有数据的,所以不再需要 cross-validation。有两种方法可以达成这一效果:
(1)巧妙地使用GridSearchCV中的关键字cv,使用 cv2= [(slice(None), slice(None))]
1 In[48]: 2 for train_index, test_index in cv.split(train_x, train_y): 3 X_train, X_test = train_x.loc[train_index], train_x.loc[test_index] 4 y_train, y_test = train_y.loc[train_index], train_y.loc[test_index] 5 cv2 = [(slice(None), slice(None))] 6 grid_search = GridSearchCV(full_pipeline_estimator, param_grid, cv=cv2, scoring='accuracy') 7 grid_search.fit(X_train, y_train) 8 predicted_y = grid_search.predict(X_train) 9 print(accuracy_score(y_train, predicted_y)) 10 print(grid_search.best_params_) 11 12 Out[48]: 13 0.7907303370786517 14 {'predict__alpha': 0.0001, 'predict__class_weight': None, 'predict__penalty': 'l1'} 15 0.7629733520336606 16 {'predict__alpha': 0.0001, 'predict__class_weight': None, 'predict__penalty': 'l1'} 17 0.7769985974754559 18 {'predict__alpha': 0.002, 'predict__class_weight': 'balanced', 'predict__penalty': None} 19 0.7980364656381487 20 {'predict__alpha': 0.0001, 'predict__class_weight': None, 'predict__penalty': 'l1'} 21 0.7699859747545582 22 {'predict__alpha': 0.0001, 'predict__class_weight': None, 'predict__penalty': 'l2'}
(2)使用 ParameterGrid,遍历其中的参数:
1 In[49]: 2 param_grid_ = [ 3 {'penalty': ['l2', 'l1', 'elasticnet', None], 4 'alpha': [0.002, 0.005, 0.0001], 5 'class_weight': ['balanced', None]}, 6 ] 7 for train_index, test_index in cv.split(train_x, train_y): 8 X_train, X_test = train_x.loc[train_index], train_x.loc[test_index] 9 y_train, y_test = train_y.loc[train_index], train_y.loc[test_index] 10 best_score = 0 11 best_parameter = [] 12 for g in ParameterGrid(param_grid_): 13 full_pipeline_estimator.named_steps['predict'].set_params(**g) 14 full_pipeline_estimator.fit(X_train, y_train) 15 predicted_y = full_pipeline_estimator.predict(X_train) 16 score = accuracy_score(y_train, predicted_y) 17 if score > best_score: 18 best_score = score 19 best_parameter = g 20 print(best_score) 21 print(best_parameter) 22 23 Out[49]: 24 0.7907303370786517 25 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'} 26 0.7629733520336606 27 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'} 28 0.7769985974754559 29 {'alpha': 0.002, 'class_weight': 'balanced', 'penalty': None} 30 0.7980364656381487 31 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'} 32 0.7699859747545582 33 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l2'}
可以看到两者的效果是一致的。并且grid_search全局的最优解,只在最后一个fold中是最优解。这一方面可能是由于不同参数组合间的结果差距很小;另一方面,对数据预处理的不同,导致它们面对的并不是完全相同的训练数据
4.2 数据清洗和模型预测分开
在上一节中,我们看到了将数据清洗和模型预测合在一起的弊端,那么我们再将其分开,看看结果:
1 # Data cleaning 2 cat_attribs = ['Pclass', 'Sex', 'Embarked'] 3 dis_attribs = ['SibSp', 'Parch'] 4 con_attribs = ['Age', 'Fare'] 5 6 # encoder: OneHotEncoder()、OrdinalEncoder() 7 cat_pipeline = Pipeline([ 8 ('selector', DataFrameSelector(cat_attribs)), 9 ('imputer', SimpleImputer(strategy='most_frequent')), 10 ('encoder', OneHotEncoder()), 11 ]) 12 13 dis_pipeline = Pipeline([ 14 ('selector', DataFrameSelector(dis_attribs)), 15 ('scaler', StandardScaler()), 16 ('imputer', SimpleImputer(strategy='most_frequent')), 17 ]) 18 19 con_pipeline = Pipeline([ 20 ('selector', DataFrameSelector(con_attribs)), 21 ('scaler', StandardScaler()), 22 ('imputer', SimpleImputer(strategy='mean')), 23 ]) 24 25 full_pipeline = FeatureUnion( 26 transformer_list=[ 27 ('con_pipeline', con_pipeline), 28 ('dis_pipeline', dis_pipeline), 29 ('cat_pipeline', cat_pipeline), 30 ] 31 ) 32 33 # Applying the model with train_x_cleaned 34 train_x_cleaned = full_pipeline.fit_transform(train_x) 35 36 clf = Perceptron() 37 # cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=2) 38 # cross_val_score(clf, train_x_cleaned, train_y, cv=cv) 39 40 param_grid2 = [ 41 {'penalty': ['l2', 'l1', 'elasticnet', None], 42 'alpha': [0.002, 0.005, 0.0001], 43 'class_weight': ['balanced', None]}, 44 ] 45 46 grid_search2 = GridSearchCV(clf, param_grid2, cv=cv, scoring='accuracy') 47 grid_search2.fit(train_x_cleaned, train_y) 48 49 predicted_y2 = grid_search2.predict(train_x_cleaned)
查看整个 grid_search 的最优解:
1 In[8]: grid_search2.best_params_ 2 Out[8]: {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'}
In[22]: print('Result of full pipeline + separate estimator:') print(accuracy_score(train_y, predicted_y2)) print(precision_score(train_y, predicted_y2)) print(recall_score(train_y, predicted_y2)) Out[22]: Result of full pipeline + separate estimator: 0.7710437710437711 0.6938202247191011 0.7222222222222222
同样,对每个 Fold 中的最优解进行查看:
1 In[55]: 2 for train_index, test_index in cv.split(train_x_cleaned, train_y): 3 X_train, X_test = train_x_cleaned[train_index], train_x_cleaned[test_index] 4 y_train, y_test = train_y[train_index], train_y[test_index] 5 cv2 = [(slice(None), slice(None))] 6 # cv = ShuffleSplit(test_size=0.2, train_size=1.0, n_splits=1, random_state=0) 7 grid_search2 = GridSearchCV(clf, param_grid2, cv=cv2, scoring='accuracy') 8 grid_search2.fit(X_train, y_train) 9 predicted_y = grid_search2.predict(X_train) 10 print(accuracy_score(y_train, predicted_y)) 11 print(grid_search2.best_params_) 12 13 Out[55]: 14 0.8061797752808989 15 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'} 16 0.7924263674614306 17 {'alpha': 0.005, 'class_weight': None, 'penalty': 'l1'} 18 0.7980364656381487 19 {'alpha': 0.002, 'class_weight': 'balanced', 'penalty': 'l2'} 20 0.791023842917251 21 {'alpha': 0.002, 'class_weight': None, 'penalty': 'l2'} 22 0.7741935483870968 23 {'alpha': 0.0001, 'class_weight': None, 'penalty': 'l1'}
可以看到,整个 grid_search 的最优解,在2个fold的数据中是最优解,占比较“数据清洗+模型预测”的方式有所提升,这一定程度上是由于数据预处理的相同。
4.3 Perceptron的正则化
在上两节中,可以看到 Perception的参数中有 'l1', 'l2'。它们都是正则化的一种。同时可以通过 coef_ 参数发现,‘l1’ 正则化,会有许多系数是0,而 ‘l2’ 正则化则没有。这体现了 ‘l1’ 正则化的一个特点:可以在约束参数的同时,对数据进行降维。
4.4 结果分析
我们将4.2节中的方法得到的最优参数 " alpha = 0.0001, class_weight = None, penalty = 'l1' ",用于预测集,并将结果上传kaggle,结果如下:
|
训练集 accuracy |
训练集 precision |
训练集 recall |
预测集 accuracy(需上传kaggle获取结果) |
|
| 朴素贝叶斯最优解 | 0.790 | 0.731 | 0.716 | 0.756 |
| 感知机 | 0.771 | 0.694 | 0.722 | 0.722 |
浙公网安备 33010602011771号