这个系列主要是面向做工程的同事做一些分享,旨在让大家都可以应用机器学习来解决问题,而不仅仅是看看理论浅尝辄止。
机器学习是一门包含多方面知识的学科,想要几天掌握是不太可能的。但是如果把它当做一个工具来使用,不追本溯源,其实不需要花费太多的时间。
这一系列分享的目的在于,希望全部完成以后,任何一个会写代码但对机器学习还不了解的同学,都可以上手运用机器学习的工具来完成一些预测任务,如分类或回归。
机器学习包含了有监督学习与无监督学习,但是由于时间有限,这一系列分享的内容仅包含有监督学习,因此下文的机器学习特指有监督学习
机器学习也就是让机器从已知数据中自动的学习出规律,从而再对未知的数据进行预测。如何让机器从数据中学习知识?从什么样的数据才能学习到知识?解决这两个问题是有套路可用的,下面就来讲如何去套路。
现在已经有非常多种成熟的机器学习算法,如KNN,逻辑回归,决策树,SVM,各种ensemble方法,以及深度学习的神经网络。不同的算法适用于不同的情况。学习这些算法需要花费大量的时间与精力,但是可以把这些算法全部看成一个API来进行调用。比如对于二分类的任务,不管用什么算法,输入输出都是一样的,即训练时输入样本集,输出训练好的模型。预测时,输入未知样本,输出类别标签,通常为0/1。
把不同的算法抽象成API以后,就可以暂且不需要知道背后的原理也能开心的应用了。
有了算法API接下来就是准备好数据,输入的数据应该长什么样呢?输入的数据应该是结构化的表格数据。训练模型的输入数据也叫做训练集,可以看成数据库的一张表,每行记录为一个样本,每个字段为一个特征。其中有一个字段标示每个样本的类别或者是某个连续值。由于机器学习模型都是数学模型,每一个特征都需要转换为数值型数据。
归纳一下:只需要准备好一个二维数组,每个维度都是数值型数据,可以是连续也可以是离散的,就可以调用算法API进行学习了。
第一讲就围绕如何准备机器学习的数据来进行讲解,包括了数据探索以及一些数据预处理的技巧。
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%config InlineBackend.figure_format = 'png' #set 'png' here when working on notebook
%matplotlib inline
这里的案例数据采用Kaggle Titanic数据集,每行代表一个乘客,每列为乘客的特征。标签是存活与否,即这是一个二分类任务。
下面是几个字段的说明:
- survival 生还与否 (0 = No; 1 = Yes)
- pclass 乘客乘坐的舱位级别 (1 = 1st; 2 = 2nd; 3 = 3rd)
- name 姓名
- sex 性别
- age 年龄
- sibsp 船上兄弟姐妹的数量
- parch 船上父母或子女的数量
- ticket 票号
- fare 票价
- cabin 客舱
- embarked 登船港口 (C = Cherbourg; Q = Queenstown; S = Southampton)
#载入数据,案例数据使用Kaggle Titanic数据集
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train.head(10)
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th… | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
5 | 6 | 0 | 3 | Moran, Mr. James | male | NaN | 0 | 0 | 330877 | 8.4583 | NaN | Q |
6 | 7 | 0 | 1 | McCarthy, Mr. Timothy J | male | 54.0 | 0 | 0 | 17463 | 51.8625 | E46 | S |
7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.0 | 3 | 1 | 349909 | 21.0750 | NaN | S |
8 | 9 | 1 | 3 | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) | female | 27.0 | 0 | 2 | 347742 | 11.1333 | NaN | S |
9 | 10 | 1 | 2 | Nasser, Mrs. Nicholas (Adele Achem) | female | 14.0 | 1 | 0 | 237736 | 30.0708 | NaN | C |
实际的工作中,我们可能要从多个数据源收集数据,最终再汇总成一个结构化的表格。这里原始的输入数据已经是结构化的表格数据,减少了很多ETL的工作。但是这些数据还不能直接作为输入数据给机器学习算法,因为有很多字段还存在非数值型数据,以及缺失数据NaN
。接下来先简单的把这些数据都转换为数值型数据。
#1. 丢弃认为对分类没有必要的特征,如有大量缺失值的Cabin。Ticket为票号,比较散乱无序,暂且认为与乘客的生还与否没有关系。
train = train.drop(['Ticket','Cabin'], axis=1)
test = test.drop(['Ticket','Cabin'], axis=1)
# 'Embarked' , 'Sex', 'Pclass'都算是标签型的特征, 先看一下它们都有哪些数值, 是否含有缺失值, 并统计一下频数
print 'Embarked'
print train['Embarked'].unique()
print train['Embarked'].value_counts()
print '-----------------------------'
print 'Sex'
print train['Sex'].unique()
print train['Sex'].value_counts()
print '-----------------------------'
print 'Pclass'
print train['Pclass'].unique()
print train['Pclass'].value_counts()
print '-----------------------------'
print 'SibSp'
print train['SibSp'].unique()
print train['SibSp'].value_counts()
print '-----------------------------'
print 'Parch'
print train['Parch'].unique()
print train['Parch'].value_counts()
print '-----------------------------'
Embarked
['S' 'C' 'Q' nan]
S 644
C 168
Q 77
Name: Embarked, dtype: int64
-----------------------------
Sex
['male' 'female']
male 577
female 314
Name: Sex, dtype: int64
-----------------------------
Pclass
[3 1 2]
3 491
1 216
2 184
Name: Pclass, dtype: int64
-----------------------------
SibSp
[1 0 3 4 2 5 8]
0 608
1 209
2 28
4 18
3 16
8 7
5 5
Name: SibSp, dtype: int64
-----------------------------
Parch
[0 1 2 5 3 4 6]
0 678
1 118
2 80
5 5
3 5
4 4
6 1
Name: Parch, dtype: int64
-----------------------------
#将训练集与测试集除类别标签外的字段合并,统一进行数据预处理
#选取认为有用的几个特征,Name暂时不用
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
all_data = pd.concat((train[features], test[features]))
all_data.head()
Pclass | Sex | Age | SibSp | Parch | Fare | Embarked | |
---|---|---|---|---|---|---|---|
0 | 3 | male | 22.0 | 1 | 0 | 7.2500 | S |
1 | 1 | female | 38.0 | 1 | 0 | 71.2833 | C |
2 | 3 | female | 26.0 | 0 | 0 | 7.9250 | S |
3 | 1 | female | 35.0 | 1 | 0 | 53.1000 | S |
4 | 3 | male | 35.0 | 0 | 0 | 8.0500 | S |
#进行缺失值填充
# 类别的缺失值采用最多的值填充
all_data['Embarked'] = all_data['Embarked'].fillna('S')
# 数值型缺失值采用中位数填充
all_data = all_data.fillna(all_data.median())
all_data.describe()
Pclass | Age | SibSp | Parch | Fare | |
---|---|---|---|---|---|
count | 1309.000000 | 1309.000000 | 1309.000000 | 1309.000000 | 1309.000000 |
mean | 2.294882 | 29.503186 | 0.498854 | 0.385027 | 33.281086 |
std | 0.837836 | 12.905241 | 1.041658 | 0.865560 | 51.741500 |
min | 1.000000 | 0.170000 | 0.000000 | 0.000000 | 0.000000 |
25% | 2.000000 | 22.000000 | 0.000000 | 0.000000 | 7.895800 |
50% | 3.000000 | 28.000000 | 0.000000 | 0.000000 | 14.454200 |
75% | 3.000000 | 35.000000 | 1.000000 | 0.000000 | 31.275000 |
max | 3.000000 | 80.000000 | 8.000000 | 9.000000 | 512.329200 |
from sklearn.preprocessing import LabelEncoder
### 只做简单特征处理
le_embarked = LabelEncoder()
le_embarked.fit(all_data['Embarked'])
all_data['Embarked'] = le_embarked.transform(all_data['Embarked'])
all_data['Sex'] = all_data['Sex'].map(lambda x:1 if x == 'male' else 0)
all_data.head()
Pclass | Sex | Age | SibSp | Parch | Fare | Embarked | |
---|---|---|---|---|---|---|---|
0 | 3 | 1 | 22.0 | 1 | 0 | 7.2500 | 2 |
1 | 1 | 0 | 38.0 | 1 | 0 | 71.2833 | 0 |
2 | 3 | 0 | 26.0 | 0 | 0 | 7.9250 | 2 |
3 | 1 | 0 | 35.0 | 1 | 0 | 53.1000 | 2 |
4 | 3 | 1 | 35.0 | 0 | 0 | 8.0500 | 2 |
此时输入的数据已经全部转换为数值型,可以输入给机器学习算法进行学习与预测。因为这一节不讲解具体的算法,因此选用一个基本的逻辑回归
from sklearn.linear_model import LogisticRegressionCV
from sklearn.model_selection import cross_val_score
def accuracy_cv(model):
"""
计算交叉验证的准确率
"""
return cross_val_score(model, X_train, y, scoring='accuracy', cv=3)
# 生成训练数据
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']
X_train.head()
Pclass | Sex | Age | SibSp | Parch | Fare | Embarked | |
---|---|---|---|---|---|---|---|
0 | 3 | 1 | 22.0 | 1 | 0 | 7.2500 | 2 |
1 | 1 | 0 | 38.0 | 1 | 0 | 71.2833 | 0 |
2 | 3 | 0 | 26.0 | 0 | 0 | 7.9250 | 2 |
3 | 1 | 0 | 35.0 | 1 | 0 | 53.1000 | 2 |
4 | 3 | 1 | 35.0 | 0 | 0 | 8.0500 | 2 |
#定义模型与交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()
print '简单的特征处理准确率为 %f' % accuracy
简单的特征处理准确率为 0.787879
截至上面的步骤,已经完成了一次完整的机器学习,并且交叉验证的分类准确率达到79%,且生还与未生还的样本分布并不算倾斜,这个结果比随机猜测要高多了,说明这么简单的几步就让机器学到了一些规律。把机器学习当做一个黑箱子工具来使用,也不是完全不靠谱的。
但是机器学习是一个不断优化的过程,在商业价值高的任务上,如CTR预估,0.1%的提高都能带来显著的收益提升。因此不能满足于一个看似满意的结果,还需要不断的提升。下面的内容就是通过数据探索与处理来提升最终的准确率。
# 从原始训练、测试数据重新载入数据,此时使用'Name'特征
features = ['Name','Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
all_data = pd.concat((train[features], test[features]))
# 填充'Embarked'的缺失值
all_data['Embarked'] = all_data['Embarked'].fillna('S')
# 填充'Fare'缺失值,使用中位数填充
all_data['Fare'] = all_data['Fare'].fillna(all_data['Fare'].median())
# 填充'Age'缺失值,使用中位数填充
#all_data['Age'] = all_data['Age'].fillna(all_data['Age'].mean())
all_data.head()
Name | Pclass | Sex | Age | SibSp | Parch | Fare | Embarked | |
---|---|---|---|---|---|---|---|---|
0 | Braund, Mr. Owen Harris | 3 | male | 22.0 | 1 | 0 | 7.2500 | S |
1 | Cumings, Mrs. John Bradley (Florence Briggs Th… | 1 | female | 38.0 | 1 | 0 | 71.2833 | C |
2 | Heikkinen, Miss. Laina | 3 | female | 26.0 | 0 | 0 | 7.9250 | S |
3 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | 1 | female | 35.0 | 1 | 0 | 53.1000 | S |
4 | Allen, Mr. William Henry | 3 | male | 35.0 | 0 | 0 | 8.0500 | S |
数据探索就是从已有的数据里面,再挖掘出一些有助于提升最终结果的特征,这方面没有固定的套路,但是很有意思,依赖对数据的理解,对业务的理解
新增一个家庭特征, 反映该乘客是单人乘船还是随家庭乘船 , 从交叉验证的结果来看,这个特征是失败的
all_data['Family'] = all_data['SibSp'] + all_data['Parch']
all_data['Family'].loc[all_data['Family'] > 0] = 1
all_data['Family'].loc[all_data['Family'] == 0] = 0
把这些变成标签型特征再进行one hot编码, 从交叉验证结果来看这个尝试是失败的
all_data['SibSp'] = all_data['SibSp'].map(lambda x:str(x))
all_data['Parch'] = all_data['Parch'].map(lambda x:str(x))
将每个乘客的Title作为一个特征 , 从结果看这是一个显著的特征
def get_title(x):
if 'Mr' in x:
return 'Mr'
elif 'Mrs' in x:
return 'Mrs'
elif 'Miss' in x:
return 'Miss'
elif 'Master' in x:
return 'Master'
else:
return 'Rare'
all_data['Title'] = all_data['Name'].map(get_title)
年龄的缺失数据, 这个方法也有助于提升结果
all_data['Age'].loc[(all_data['Title'] == 'Mr') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Mr')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Mrs') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Mrs')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Miss') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Miss')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Master') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Master')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Rare') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Rare')].mean()
判断是否是小孩
all_data['Child'] = all_data['Age']
all_data['Child'].loc[all_data['Child'] < 10] = 1
all_data['Child'].loc[all_data['Child'] >= 10] = 0
处理数值型倾斜数据,数据倾斜指的是,单维度的数据不呈正态分布,有可能左偏有可能右边,而对数据进行log变换,可以减小其倾斜度。
matplotlib.rcParams['figure.figsize'] = (12.0, 6.0)
fare = pd.DataFrame({'Fare':all_data['Fare'], 'log(Fare+1)':np.log1p(all_data['Fare'])})
fare.hist()
# 对倾斜数据做log变换 , 这一步在这个数据集上对结果的提升并没有帮助
print 'skew before log trick: %f ' % all_data['Fare'].skew()
all_data['Fare'] = np.log1p(all_data['Fare'])
print 'skew after log trick: %f' % all_data['Fare'].skew()
skew before log trick: 4.369510
skew after log trick: 0.542617
不进行One Hot编码,直接标注0,1,2,3
le_title = LabelEncoder()
le_title.fit(all_data['Title'])
all_data['Title'] = le_title.transform(all_data['Title'])
le_embarked = LabelEncoder()
le_embarked.fit(all_data['Embarked'])
all_data['Embarked'] = le_embarked.transform(all_data['Embarked'])
all_data['Sex'] = all_data['Sex'].map(lambda x:1 if x == 'male' else 0)
all_data = all_data.drop('Name', axis=1)
# 生成训练集
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']
# 训练模型并进行交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()
print '未使用One-Hot编码准确率为 %f' % accuracy
0.810325476992
对标签特征进行序数编码是不太妥当的,因为大多数算法接受数值型输入,是假设这些数值都是连续值,也就是有序的。而标签本身是无序的、不存在大小关系的,因此需要使用另一种编码方式,也就是One-Hot编码。
# 对标签特征进行One Hot编码,由于Pclass是被标示为数值型,为了使用get_dummies API,先转换为字符串
all_data['Pclass'] = all_data['Pclass'].map(lambda x:str(x))
all_data = pd.get_dummies(all_data.drop(['Name'], axis=1))
# 生成训练集
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']
# 训练模型并进行交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()
print '使用One-Hot编码准确率为 %f' % accuracy
使用One-Hot编码准确率为 0.822671