python机器学习爬虫Python研究刚刚开始@IT·互联网

如何用Python和深度神经网络锁定即将流失的客户?

2017-11-19  本文已影响1964人  王树义

Python深度学习简明实战教程来了。别犹豫了,赶紧从零开始,搭建你自己的第一个深度学习模型吧!

想不想了解如何用Python快速搭建深度神经网络,完成数据分类任务?本文一步步为你展示这一过程,让你初步领略深度学习模型的强大和易用。

image

烦恼

作为一名数据分析师,你来到这家跨国银行工作已经半年了。

今天上午,老板把你叫到办公室,面色凝重。

你心里直打鼓,以为自己捅了什么篓子。幸好老板的话让你很快打消了顾虑。

他发愁,是因为最近欧洲区的客户流失严重,许多客户都跑到了竞争对手那里接受服务了。老板问你该怎么办?

你脱口而出“做好客户关系管理啊!”

老板看了你一眼,缓慢地说“我们想知道哪些客户最可能在近期流失”。

没错,在有鱼的地方钓鱼,才是上策。

你明白了自己的任务——通过数据锁定即将流失的客户。这个工作,确实是你这个数据分析师分内的事儿。

你很庆幸,这半年做了很多的数据动态采集和整理工作,使得你手头就有一个比较完备的客户数据集。

下面你需要做的,就是如何从数据中“沙里淘金”,找到那些最可能流失的客户。

可是,该怎么做呢?

你拿出欧洲区客户的数据,端详起来。

image

客户主要分布在法国、德国和西班牙。

你手里掌握的信息,包括他们的年龄、性别、信用、办卡信息等。客户是否已流失的信息在最后一列(Exited)。

怎么用这些数据来判断顾客是否会流失呢?

以你的专业素养,很容易就判断出这是一个分类问题,属于机器学习中的监督式学习。但是,你之前并没有做过实际项目,该如何着手呢?

别发愁,我一步步给你演示如何用Python和深度神经网络(或者叫“深度学习”)来完成这个分类任务,帮你锁定那些即将流失的客户。

环境

工欲善其事,必先利其器。我们先来安装和搭建环境。

首先是安装Python。

请到这个网址下载Anaconda的最新版本。

image

请选择左侧的Python 3.6版本下载安装。

其次是新建文件夹,起名为demo-customer-churn-ann,并且从这个链接下载数据,放到该文件夹下。

(注:样例数据来自于匿名化处理后的真实数据集,下载自superdatascience官网。)

打开终端(或者命令行工具),进入demo-customer-churn-ann目录,执行以下命令:

jupyter notebook

浏览器中会显示如下界面:

image

点击界面右上方的New按钮,新建一个Python 3 Notebook,起名为customer-churn-ann。

image

准备工作结束,下面我们开始清理数据。

清理

首先,读入数据清理最常用的pandas和numpy包。

import numpy as np
import pandas as pd

customer_churn.csv里读入数据:

df = pd.read_csv('customer_churn.csv')

看看读入效果如何:

df.head()

这里我们使用了head()函数,只显示前5行。

image

可以看到,数据完整无误读入。但是并非所有的列都对我们预测用户流失有作用。我们一一甄别一下:

上述数据列甄别过程,就叫做“特征工程”(Feature Engineering),这是机器学习里面最常用的数据预处理方法。如果我们的数据量足够大,机器学习模型足够复杂,是可以跳过这一步的。但是由于我们的数据只有10000条,还需要手动筛选特征。

选定了特征之后,我们来生成特征矩阵X,把刚才我们决定保留的特征都写进来。

X = df.loc[:,['CreditScore', 'Geography', 'Gender', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'HasCrCard', 'IsActiveMember', 'EstimatedSalary']]

看看特征矩阵的前几行:

X.head()

显示结果如下:

image

2017-11-19_19-2-2_snapshots-01.jpg

特征矩阵构建准确无误,下面我们构建目标数据y,也就是用户是否流失。

y = df.Exited
y.head()
0    1
1    0
2    1
3    0
4    0
Name: Exited, dtype: int64

此时我们需要的数据基本上齐全了。但是我们发现其中有几列数据还不符合我们的要求。

要做机器学习,只能给机器提供数值,而不能是字符串。可是看看我们的特征矩阵:

X.head()
image

2017-11-19_19-2-2_snapshots-01.jpg

显然其中的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()
image

2017-11-19_19-2-11_snapshots-03.jpg

显然,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],
       [1],
       ...,
       [1],
       [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次都能判断正确。

但是,这样是否足够?

我们或许可以调整决策树的参数做优化,尝试改进预测结果。

或者我们可以采用深度学习

深度

深度学习的使用场景,往往是因为原有的模型经典机器学习模型过于简单,无法把握复杂数据特性。

我不准备给你讲一堆数学公式,咱们动手做个实验。

请你打开这个网址

你会看到如下图所示的深度学习游乐场:

image

右侧的图形,里面是蓝色数据,外圈是黄色数据。你的任务就是要用模型分类两种不同数据。

你说那还不容易?我一眼就看出来了。

你看出来没有用。通过你的设置,让机器也能正确区分,才算数。

图中你看到许多加减号。咱们就通过操纵它们来玩儿一玩儿模型。

首先,点图中部上方的"2 HIDDEN LAYERS"左侧减号,把中间隐藏层数降低为1。

image

然后,点击"2 neurons"上面的减号,把神经元数量减少为1。

把页面上方的Activation函数下拉框打开,选择“Sigmoid”。

现在的模型,其实就是经典的逻辑回归(Logistic Regression)。

image

点击左上方的运行按钮,我们看看执行效果。

image

由于模型过于简单,所以机器绞尽脑汁,试图用一条直线切分二维平面上的两类节点。

损失(loss)居高不下。训练集和测试集损失都在0.4左右,显然不符合我们的分类需求。

下面我们试试增加层数和神经元数量。这次点击加号,把隐藏层数加回到2,两层神经元数量都取2。

image

再次点击运行。

经过一段时间,结果稳定了下来,你发现这次电脑用了两条线,把平面切分成了3部分。

image

测试集损失下降到了0.25左右,而训练集损失更是降低到了0.2以下。

模型复杂了,效果似乎更好一些。

再接再厉,我们把第一个隐藏层的神经元数量增加为4看看。

image

点击运行,不一会儿有趣的事情就发生了。

image

机器用一条近乎完美的曲线把平面分成了内外两个部分。测试集和训练集损失都极速下降,训练集损失甚至接近于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函数是激活函数的一种。它大概长这个样子。

image

如果你想了解激活函数的更多知识,请参考后文的学习资源部分。

隐藏层里,每一层我们都设置了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)

注意这里多了几个参数,我们来解释一下。

以下就是电脑输出的最终训练结果。其实中间运行过程看着更激动人心,你自己试一下就知道了。

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
Training Step: 7500  | total loss: �[1m�[32m0.40385�[0m�[0m | time: 0.659s
| Adam | epoch: 030 | loss: 0.40385 - acc: 0.8487 -- iter: 8000/8000
--

我们看到训练集的损失(loss)大概为0.4左右。

打开终端,我们输入

tensorboard --logdir=/tmp/tflearn_logs/

然后在浏览器里输入http://localhost:6006/

可以看到如下界面:

image

这是模型训练过程的可视化图形,可以看到准确度的攀升和损失降低的曲线。

打开GRAPHS标签页,我们可以查看神经网络的结构图形。

image

我们搭积木的过程,在此处一目了然。

评估

训练好了模型,我们来尝试做个预测吧。

看看测试集的特征矩阵第一行。

X_test[0]
array([ 1.75486502, -0.57369368, -0.55204276, -1.09168714, -0.36890377,
        1.04473698,  0.8793029 , -0.92159124,  0.64259497,  0.9687384 ,
        1.61085707])

我们就用它来预测一下分类结果。

y_pred = model.predict(X_test)

打印出来看看:

y_pred[0]
array([ 0.70956731,  0.29043278], dtype=float32)

模型判断该客户不流失的可能性为0.70956731。

我们看看实际标签数据:

y_test[0]
array([ 1.,  0.])

客户果然没有流失。这个预测是对的。

但是一个数据的预测正确与否,是无法说明问题的。我们下面跑整个测试集,并且使用evaluate函数评价模型。

score = model.evaluate(X_test, y_test)
print('Test accuarcy: %0.4f%%' % (score[0] * 100))
Test accuarcy: 84.1500%

在测试集上,准确性达到84.15%,好样的!

希望在你的努力下,机器做出的准确判断可以帮助银行有效锁定可能流失的客户,降低客户的流失率,继续日进斗金。

说明

你可能觉得,深度学习也没有什么厉害的嘛。原先的决策树算法,那么简单就能实现,也可以达到80%以上的准确度。写了这么多语句,深度学习结果也无非只提升了几个百分点而已。

首先,准确度达到某种高度后,提升是不容易的。这就好像学生考试,从不及格到及格,付出的努力并不需要很高;从95分提升到100,却是许多人一辈子也没有完成的目标。

其次,在某些领域里,1%的提升意味着以百万美元计的利润,或者几千个人的生命因此得到拯救。

第三,深度学习的崛起,是因为大数据的环境。在许多情况下,数据越多,深度学习的优势就越明显。本例中只有10000条记录,与“大数据”的规模还相去甚远。

学习资源

如果你对深度学习感兴趣,推荐以下学习资源。

首先是教材。

第一本是Deep Learning,绝对的经典。

image

第二本是Hands-On Machine Learning with Scikit-Learn and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems,深入浅出,通俗易懂。

image

其次是MOOC。

推荐吴恩达(Andrew Ng)教授在Coursera上的两门课程。

一门是机器学习。这课推出有年头了,但是非常有趣和实用。具体的介绍请参考拙作《机器学习哪里有这么玄?》以及《如何用MOOC组合掌握机器学习?》。

image

一门是深度学习。这是个系列课程,包括5门子课程。今年推出的新课,自成体系,但是最好有前面那门课程作为基础。

image

讨论

你对深度学习感兴趣吗?之前有没有做过深度学习项目?你掌握了哪些深度学习框架?有没有什么建议给初学者?欢迎留言,把心得分享给大家,我们一起交流讨论。

如果你对我的文章感兴趣,欢迎点赞,并且微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)。

如果本文可能对你身边的亲友有帮助,也欢迎你把本文通过微博或朋友圈分享给他们。让他们一起参与到我们的讨论中来。

延伸阅读

如何用《玉树芝兰》入门数据科学?

数据科学相关文章合集(玉树芝兰)

上一篇下一篇

猜你喜欢

热点阅读