如何用Python和深度神经网络发现即将流失的客户?
想不想了解如何用Python快速搭建深度神经网络,完成数据分类任务?本文一步步为你展示这一过程,让你初步领略深度学习模型的强大和易用。
(由于微信公众号外部链接的限制,文中的部分链接可能无法正确打开。如有需要,请点击文末的“阅读原文”按钮,访问可以正常显示外链的版本。)
烦恼
作为一名数据分析师,你来到这家跨国银行工作已经半年了。
今天上午,老板把你叫到办公室,面色凝重。
你心里直打鼓,以为自己捅了什么篓子。幸好老板的话让你很快打消了顾虑。
他发愁,是因为最近欧洲区的客户流失严重,许多客户都跑到了竞争对手那里接受服务了。老板问你该怎么办?
你脱口而出“做好客户关系管理啊!”
老板看了你一眼,缓慢地说“我们想知道哪些客户最可能在近期流失”。
没错,在有鱼的地方钓鱼,才是上策。
你明白了自己的任务——通过数据锁定即将流失的客户。这个工作,确实是你这个数据分析师分内的事儿。
你很庆幸,这半年做了很多的数据动态采集和整理工作,使得你手头就有一个比较完备的客户数据集。
下面你需要做的,就是如何从数据中“沙里淘金”,找到那些最可能流失的客户。
可是,该怎么做呢?
你拿出欧洲区客户的数据,端详起来。
客户主要分布在法国、德国和西班牙。
你手里掌握的信息,包括他们的年龄、性别、信用、办卡信息等。客户是否已流失的信息在最后一列(Exited)。
怎么用这些数据来判断顾客是否会流失呢?
以你的专业素养,很容易就判断出这是一个分类问题,属于机器学习中的监督式学习。但是,你之前并没有做过实际项目,该如何着手呢?
别发愁,我一步步给你演示如何用Python和深度神经网络(或者叫“深度学习”)来完成这个分类任务,帮你锁定那些即将流失的客户。
环境
工欲善其事,必先利其器。我们先来安装和搭建环境。
首先是安装Python。
请到这个网址下载Anaconda的最新版本。
请选择左侧的Python 3.6版本下载安装。
其次是新建文件夹,起名为demo-customer-churn-ann,并且从这个链接下载数据,放到该文件夹下。
(注:样例数据来自于匿名化处理后的真实数据集,下载自superdatascience官网。)
打开终端(或者命令行工具),进入demo-customer-churn-ann目录,执行以下命令:
jupyter notebook
浏览器中会显示如下界面:
点击界面右上方的New按钮,新建一个Python 3 Notebook,起名为customer-churn-ann。
准备工作结束,下面我们开始清理数据。
清理
首先,读入数据清理最常用的pandas和numpy包。
import numpy as npimport pandas as pd
从
customer_churn.csv
里读入数据:
df = pd.read_csv('customer_churn.csv')
看看读入效果如何:
df.head()
这里我们使用了
head()
函数,只显示前5行。
可以看到,数据完整无误读入。但是并非所有的列都对我们预测用户流失有作用。我们一一甄别一下:
- RowNumber:行号,这个肯定没用,删除
- CustomerID:用户编号,这个是顺序发放的,删除
- Surname:用户姓名,对流失没有影响,删除
- CreditScore:信用分数,这个很重要,保留
- Geography:用户所在国家/地区,这个有影响,保留
- Gender:用户性别,可能有影响,保留
- Age:年龄,影响很大,年轻人更容易切换银行,保留
- Tenure:当了本银行多少年用户,很重要,保留
- Balance:存贷款情况,很重要,保留
- NumOfProducts:使用产品数量,很重要,保留
- HasCrCard:是否有本行信用卡,很重要,保留
- IsActiveMember:是否活跃用户,很重要,保留
- EstimatedSalary:估计收入,很重要,保留
- Exited:是否已流失,这将作为我们的标签数据
上述数据列甄别过程,就叫做“特征工程”(Feature Engineering),这是机器学习里面最常用的数据预处理方法。如果我们的数据量足够大,机器学习模型足够复杂,是可以跳过这一步的。但是由于我们的数据只有10000条,还需要手动筛选特征。
选定了特征之后,我们来生成特征矩阵X,把刚才我们决定保留的特征都写进来。
X = df.loc[:,['CreditScore', 'Geography', 'Gender', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'HasCrCard', 'IsActiveMember', 'EstimatedSalary']]
看看特征矩阵的前几行:
X.head()
显示结果如下:
特征矩阵构建准确无误,下面我们构建目标数据y,也就是用户是否流失。
y = df.Exited![2017-11-19_19-2-2_snapshots-01.jpg](http://upload-images.jianshu.io/upload_images/64542-a15e6d0d91c8b28e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
y.head()
0 1
1 0
2 1
3 0
4 0
Name: Exited, dtype: int64
此时我们需要的数据基本上齐全了。但是我们发现其中有几列数据还不符合我们的要求。
要做机器学习,只能给机器提供数值,而不能是字符串。可是看看我们的特征矩阵:
X.head()
显然其中的Geography和Gender两项数据都不符合要求。它们都是分类数据。我们需要做转换,把它们变成数值。
在Scikit-learn工具包里面,专门提供了方便的工具
LabelEncoder
,让我们可以方便地将类别信息变成数值。
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
labelencoder1 = LabelEncoder()
X.Geography= labelencoder1.fit_transform(X.Geography)
labelencoder2 = LabelEncoder()
X.Gender = labelencoder2.fit_transform(X.Gender)
我们需要转换两列,所以建立了两个不同的labelencoder。转换的函数叫做
fit_transform
。
经过转换,此时我们再来看看特征矩阵的样子:
X.head()
显然,Geography和Gender这两列都从原先描述类别的字符串,变成了数字。
这样是不是就完事大吉了呢?
不对,Gender还好说,只有两种取值方式,要么是男,要么是女。我们可以把“是男性”定义为1,那么女性就取值为0。两种取值只是描述类别不同,没有歧义。
而Geography就不同了。因为数据集里面可能的国家地区取值有3种,所以就转换成了0(法国)、1(德国)、2(西班牙)。问题是,这三者之间真的有序列(大小)关系吗?
答案自然是否定的。我们其实还是打算用数值描述分类而已。但是取值有数量的序列差异,就会给机器带来歧义。它并不清楚不同的取值只是某个国家的代码,可能会把这种大小关系带入模型计算,从而产生错误的结果。
解决这个问题,我们就需要引入
OneHotEncoder
。它也是Scikit-learn提供的一个类,可以帮助我们把类别的取值转变为多个变量组合表示。
咱们这个数据集里,可以把3个国家分别用3个数字组合来表示。例如法国从原先的0,变成
(1, 0, 0)
,德国从1变成
(0, 1, 0)
,而西班牙从2变成
(0, 0, 1)
。
这样,再也不会出现0和1之外的数字来描述类别,从而避免机器产生误会,错把类别数字当成大小来计算了。
特征矩阵里面,我们只需要转换国别这一列。因为它在第1列的位置(从0开始计数),因而
categorical_features
只填写它的位置信息。
onehotencoder = OneHotEncoder(categorical_features = [1])
X = onehotencoder.fit_transform(X).toarray()
这时候,我们的特征矩阵数据框就被转换成了一个数组。注意所有被OneHotEncoder转换的列会排在最前面,然后才是那些保持原样的数据列。
我们只看转换后的第一行:
X[0]
array([ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00,
6.19000000e+02, 0.00000000e+00, 4.20000000e+01,
2.00000000e+00, 0.00000000e+00, 1.00000000e+00,
1.00000000e+00, 1.00000000e+00, 1.01348880e+05])
这样,总算转换完毕了吧?
没有。
因为本例中,OneHotEncoder转换出来的3列数字,实际上是不独立的。给定其中两列的信息,你自己都可以计算出其中的第3列取值。
好比说,某一行的前两列数字是
(0, 0)
,那么第三列肯定是1。因为这是转换规则决定的。3列里只能有1个是1,其余都是0。
如果你做过多元线性回归,应该知道这种情况下,我们是需要去掉其中一列,才能继续分析的。不然会落入“虚拟变量陷阱”(dummy variable trap)。
我们删掉第0列,避免掉进坑里。
X = np.delete(X, [0], 1)
再次打印第一行:
X[0]
array([ 0.00000000e+00, 0.00000000e+00, 6.19000000e+02,
0.00000000e+00, 4.20000000e+01, 2.00000000e+00,
0.00000000e+00, 1.00000000e+00, 1.00000000e+00,
1.00000000e+00, 1.01348880e+05])
检查完毕,现在咱们的特征矩阵处理基本完成。
但是监督式学习,最重要的是有标签(label)数据。本例中的标签就是用户是否流失。我们目前的标签数据框,是这个样子的。
y.head()
0 1
1 0
2 1
3 0
4 0
Name: Exited, dtype: int64
它是一个行向量,我们需要把它先转换成为列向量。你可以想象成把它“竖过来”。
y = y[:, np.newaxis]
y
array([[1],
[0]])
这样在后面训练的时候,他就可以和前面的特征矩阵一一对应来操作计算了。
既然标签代表了类别,我们也把它用OneHotEncoder转换,这样方便我们后面做分类学习。
onehotencoder = OneHotEncoder()
y = onehotencoder.fit_transform(y).toarray()
此时的标签变成两列数据,一列代表顾客存留,一列代表顾客流失。
y
array([[ 0., 1.],
[ 1., 0.],
[ 0., 1.],
[ 0., 1.],
[ 0., 1.],
[ 1., 0.]])
总体的数据已经齐全了。但是我们 不能 把它们 都用来 训练。
这就好像老师不应该把考试题目拿来给学生做作业和练习一样。只有考学生没见过的题,才能区分学生是掌握了正确的解题方法,还是死记硬背了作业答案。
我们拿出20%的数据,放在一边,等着用来做测试。其余8000条数据用来训练机器学习模型。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)
我们看看训练集的长度:
len(X_train)
8000
再看看测试集的长度:
len(X_test)
2000
确认无误。
是不是可以开始机器学习了?
可以,但是下面这一步也很关键。我们需要把数据进行标准化处理。因为原先每一列数字的取值范围都各不相同,因此有的列方差要远远大于其他列。这样对机器来说,也是很困扰的。数据的标准化处理,可以在保持列内数据多样性的同时,尽量减少不同类别之间差异的影响,可以让机器公平对待全部特征。
我们调用Scikit-learn的
StandardScaler
类来完成这一过程。
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
注意,我们只对特征矩阵做标准化,标签是不能动的。另外训练集和测试集需要按照
统一的标准
变化。所以你看,训练集上,我们用了
fit_transform
函数,先拟合后转换;而在测试集上,我们直接用训练集拟合的结果,只做转换。
X_train
array([[-0.5698444 , 1.74309049, 0.16958176, ..., 0.64259497,
-1.03227043, 1.10643166],
[ 1.75486502, -0.57369368, -2.30455945, ..., 0.64259497,
0.9687384 , -0.74866447],
[-0.5698444 , -0.57369368, -1.19119591, ..., 0.64259497,
-1.03227043, 1.48533467],
[-0.5698444 , -0.57369368, 0.9015152 , ..., 0.64259497,
-1.03227043, 1.41231994],
[-0.5698444 , 1.74309049, -0.62420521, ..., 0.64259497,
0.9687384 , 0.84432121],
[ 1.75486502, -0.57369368, -0.28401079, ..., 0.64259497,
-1.03227043, 0.32472465]])
你会发现,许多列的方差比原先小得多。机器学习起来,会更加方便。
数据清理和转换工作至此完成。
决策树
如果读过我的《贷还是不贷:如何用Python和机器学习帮你决策?》一文,你应该有一种感觉——这个问题和贷款审批决策很像啊!既然在该文中,决策树很好使,我们继续用决策树不就好了?
好的,我们先测试一下经典机器学习算法表现如何。
从Scikit-learn中,读入决策树工具。然后拟合训练集数据。
from sklearn import tree
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)
然后,利用我们建立的决策树模型做出预测。
y_pred = clf.predict(X_test)
打印预测结果:
y_pred
array([[ 1., 0.],
[ 0., 1.],
[ 1., 0.],
[ 1., 0.],
[ 1., 0.],
[ 0., 1.]])
这样看不出来什么。让我们调用Scikit-learn的
classification_report
模块,生成分析报告。
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))
precision recall f1-score support
0 0.89 0.86 0.87 1595
1 0.51 0.58 0.54 405
avg / total 0.81 0.80 0.81 2000
经检测,决策树在咱们的数据集上,表现得还是不错的。总体的准确率为0.81,召回率为0.80,f1分数为0.81,已经很高了。对10个客户做流失可能性判断,它有8次都能判断正确。
但是,这样是否足够?
我们或许可以调整决策树的参数做优化,尝试改进预测结果。
或者我们可以采用 深度学习 。
深度
深度学习的使用场景,往往是因为原有的模型经典机器学习模型过于简单,无法把握复杂数据特性。
我不准备给你讲一堆数学公式,咱们动手做个实验。
请你打开这个网址。
你会看到如下图所示的深度学习游乐场:
右侧的图形,里面是蓝色数据,外圈是黄色数据。你的任务就是要用模型分类两种不同数据。
你说那还不容易?我一眼就看出来了。
你看出来没有用。通过你的设置,让机器也能正确区分,才算数。
图中你看到许多加减号。咱们就通过操纵它们来玩儿一玩儿模型。
首先,点图中部上方的”2 HIDDEN LAYERS”左侧减号,把中间隐藏层数降低为1。
然后,点击”2 neurons”上面的减号,把神经元数量减少为1。
把页面上方的Activation函数下拉框打开,选择“Sigmoid”。
现在的模型,其实就是经典的逻辑回归(Logistic Regression)。
点击左上方的运行按钮,我们看看执行效果。
由于模型过于简单,所以机器绞尽脑汁,试图用一条直线切分二维平面上的两类节点。
损失(loss)居高不下。训练集和测试集损失都在0.4左右,显然不符合我们的分类需求。
下面我们试试增加层数和神经元数量。这次点击加号,把隐藏层数加回到2,两层神经元数量都取2。
再次点击运行。
经过一段时间,结果稳定了下来,你发现这次电脑用了两条线,把平面切分成了3部分。
测试集损失下降到了0.25左右,而训练集损失更是降低到了0.2以下。
模型复杂了,效果似乎更好一些。
再接再厉,我们把第一个隐藏层的神经元数量增加为4看看。
点击运行,不一会儿有趣的事情就发生了。
机器用一条近乎完美的曲线把平面分成了内外两个部分。测试集和训练集损失都极速下降,训练集损失甚至接近于0。
这告诉我们,许多时候模型过于简单带来的问题,可以通过加深隐藏层次、增加神经元的方法提升模型复杂度,加以改进。
目前流行的划分方法,是用隐藏层的数量多少来区分是否“深度”。当神经网络中隐藏层数量达到3层以上时,就被称为“深度神经网络”,或者“深度学习”。
久闻大名的深度学习,原来就是这么简单。
如果有时间的话,建议你自己在这个游乐场里多动手玩儿一玩儿。你会很快对神经网络和深度学习有个感性认识。
框架
游乐场背后使用的引擎,就是Google的深度学习框架Tensorflow。
所谓框架,就是别人帮你构造好的基础软件应用。你可以通过调用它们,避免自己重复发明轮子,大幅度节省时间,提升效率。
支持Python语言的深度学习的框架有很多,除了Tensorflow外,还有PyTorch, Theano和MXNet等。
我给你的建议是,找到一个你喜欢的软件包,深入学习使用,不断实践来提升自己的技能。 千万不要 跟别人争论哪个深度学习框架更好。一来萝卜白菜各有所爱,每个人都有自己的偏好;二来深度学习的江湖水很深,言多有失。说错了话,别的门派可能会不高兴哟。
我比较喜欢Tensorflow。但是Tensorflow本身是个底层库。虽然随着版本的更迭,界面越来越易用。但是对初学者来说,许多细节依然有些过于琐碎,不容易掌握。
初学者的耐心有限,挫折过多容易放弃。
幸好,还有几个高度抽象框架,是建立在Tensorflow之上的。如果你的任务是 应用 现成的深度学习模型,那么这些框架会给你带来非常大的便利。
这些框架包括Keras, TensorLayer等。咱们今天将要使用的,叫做TFlearn。
它的特点,就是长得很像Scikit-learn。这样如果你熟悉经典机器学习模型,学起来会特别轻松省力。
实战
闲话就说这么多,下面咱们继续写代码吧。
写代码之前,请回到终端下,运行以下命令,安装几个软件包:
pip install tensorflow
pip install tflearn
执行完毕后,回到Notebook里。
我们呼叫tflearn框架。
import tflearn
然后,我们开始搭积木一样,搭神经网络层。
首先是输入层。
net = tflearn.input_data(shape=[None, 11])
注意这里的写法,因为我们输入的数据,是特征矩阵。而经过我们处理后,特征矩阵现在有11列,因此shape的第二项写11。
shape的第一项,None,指的是我们要输入的特征矩阵行数。因为我们现在是搭建模型,后面特征矩阵有可能一次输入,有可能分成组块输入,长度可大可小,无法事先确定。所以这里填None。tflearn会在我们实际执行训练的时候,自己读入特征矩阵的尺寸,来处理这个数值。
下面我们搭建隐藏层。这里我们要使用深度学习,搭建3层。
net = tflearn.fully_connected(net, 6, activation='relu')
net = tflearn.fully_connected(net, 6, activation='relu')
net = tflearn.fully_connected(net, 6, activation='relu')
activation刚才在深度学习游乐场里面我们遇到过,代表激活函数。如果没有它,所有的输入输出都是线性关系。
Relu函数是激活函数的一种。它大概长这个样子。
如果你想了解激活函数的更多知识,请参考后文的学习资源部分。
隐藏层里,每一层我们都设置了6个神经元。其实至今为之,也不存在最优神经元数量的计算公式。工程界的一种做法,是把输入层的神经元数量,加上输出层神经元数量,除以2取整。咱们这里就是用的这种方法,得出6个。
搭好了3个中间隐藏层,下面我们来搭建输出层。
net = tflearn.fully_connected(net, 2, activation='softmax')
net = tflearn.regression(net)
这里我们用两个神经元做输出,并且说明使用回归方法。输出层选用的激活函数为softmax。处理分类任务的时候,softmax比较合适。它会告诉我们每一类的可能性,其中数值最高的,可以作为我们的分类结果。
积木搭完了,下面我们告诉TFlearn,以刚刚搭建的结构,生成模型。
model = tflearn.DNN(net)
有了模型,我们就可以使用拟合功能了。你看是不是跟Scikit-learn的使用方法很相似呢?
model.fit(X_train, y_train, n_epoch=30, batch_size=32, show_metric=True)
注意这里多了几个参数,我们来解释一下。
-
n_epoch
:数据训练几个轮次。 -
batch_size
:每一次输入给模型的数据行数。 -
show_metric
:训练过程中要不要打印结果。
以下就是电脑输出的最终训练结果。其实中间运行过程看着更激动人心,你自己试一下就知道了。
Training Step: 7499 | total loss: [1m[32m0.39757[0m[0m | time: 0.656s
| Adam | epoch: 030 | loss: 0.39757 - acc: 0.8493 -- iter: 7968/8000