mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
mod
This commit is contained in:
243
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/17 丨决策树(上):要不要去打篮球?决策树来告诉你.md
Normal file
243
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/17 丨决策树(上):要不要去打篮球?决策树来告诉你.md
Normal file
@@ -0,0 +1,243 @@
|
||||
<audio id="audio" title="17 丨决策树(上):要不要去打篮球?决策树来告诉你" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f6/42/f62875ac408e70b3595a23e6ba200442.mp3"></audio>
|
||||
|
||||
想象一下一个女孩的妈妈给她介绍男朋友的场景:
|
||||
|
||||
女儿:长的帅不帅?
|
||||
|
||||
妈妈:挺帅的。
|
||||
|
||||
女儿:有没有房子?
|
||||
|
||||
妈妈:在老家有一个。
|
||||
|
||||
女儿:收入高不高?
|
||||
|
||||
妈妈:还不错,年薪百万。
|
||||
|
||||
女儿:做什么工作的?
|
||||
|
||||
妈妈:IT男,互联网公司做数据挖掘的。
|
||||
|
||||
女儿:好,那我见见。
|
||||
|
||||
在现实生活中,我们会遇到各种选择,不论是选择男女朋友,还是挑选水果,都是基于以往的经验来做判断。如果把判断背后的逻辑整理成一个结构图,你会发现它实际上是一个树状图,这就是我们今天要讲的**决策树**。
|
||||
|
||||
## 决策树的工作原理
|
||||
|
||||
决策树基本上就是把我们以前的经验总结出来。我给你准备了一个打篮球的训练集。如果我们要出门打篮球,一般会根据“天气”、“温度”、“湿度”、“刮风”这几个条件来判断,最后得到结果:去打篮球?还是不去?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/90/dca4224b342894f12f54a9cb41d8cd90.jpg" alt=""><br>
|
||||
上面这个图就是一棵典型的决策树。我们在做决策树的时候,会经历两个阶段:**构造和剪枝**。
|
||||
|
||||
**构造**
|
||||
|
||||
什么是构造呢?构造就是生成一棵完整的决策树。简单来说,**构造的过程就是选择什么属性作为节点的过程**,那么在构造过程中,会存在三种节点:
|
||||
|
||||
<li>
|
||||
根节点:就是树的最顶端,最开始的那个节点。在上图中,“天气”就是一个根节点;
|
||||
</li>
|
||||
<li>
|
||||
内部节点:就是树中间的那些节点,比如说“温度”、“湿度”、“刮风”;
|
||||
</li>
|
||||
<li>
|
||||
叶节点:就是树最底部的节点,也就是决策结果。
|
||||
</li>
|
||||
|
||||
节点之间存在父子关系。比如根节点会有子节点,子节点会有子子节点,但是到了叶节点就停止了,叶节点不存在子节点。那么在构造过程中,你要解决三个重要的问题:
|
||||
|
||||
<li>
|
||||
选择哪个属性作为根节点;
|
||||
</li>
|
||||
<li>
|
||||
选择哪些属性作为子节点;
|
||||
</li>
|
||||
<li>
|
||||
什么时候停止并得到目标状态,即叶节点。
|
||||
</li>
|
||||
|
||||
**剪枝**
|
||||
|
||||
决策树构造出来之后是不是就万事大吉了呢?也不尽然,我们可能还需要对决策树进行剪枝。剪枝就是给决策树瘦身,这一步想实现的目标就是,不需要太多的判断,同样可以得到不错的结果。之所以这么做,是为了防止“过拟合”(Overfitting)现象的发生。
|
||||
|
||||
“过拟合”这个概念你一定要理解,它指的就是模型的训练结果“太好了”,以至于在实际应用的过程中,会存在“死板”的情况,导致分类错误。
|
||||
|
||||
欠拟合,和过拟合就好比是下面这张图中的第一个和第三个情况一样,训练的结果“太好“,反而在实际应用过程中会导致分类错误。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/df/d30bfa3954ffdf5baf47ce53df9366df.jpg" alt=""><br>
|
||||
造成过拟合的原因之一就是因为训练集中样本量较小。如果决策树选择的属性过多,构造出来的决策树一定能够“完美”地把训练集中的样本分类,但是这样就会把训练集中一些数据的特点当成所有数据的特点,但这个特点不一定是全部数据的特点,这就使得这个决策树在真实的数据分类中出现错误,也就是模型的“泛化能力”差。
|
||||
|
||||
泛化能力指的分类器是通过训练集抽象出来的分类能力,你也可以理解是举一反三的能力。如果我们太依赖于训练集的数据,那么得到的决策树容错率就会比较低,泛化能力差。因为训练集只是全部数据的抽样,并不能体现全部数据的特点。
|
||||
|
||||
既然要对决策树进行剪枝,具体有哪些方法呢?一般来说,剪枝可以分为“预剪枝”(Pre-Pruning)和“后剪枝”(Post-Pruning)。
|
||||
|
||||
预剪枝是在决策树构造时就进行剪枝。方法是在构造的过程中对节点进行评估,如果对某个节点进行划分,在验证集中不能带来准确性的提升,那么对这个节点进行划分就没有意义,这时就会把当前节点作为叶节点,不对其进行划分。
|
||||
|
||||
后剪枝就是在生成决策树之后再进行剪枝,通常会从决策树的叶节点开始,逐层向上对每个节点进行评估。如果剪掉这个节点子树,与保留该节点子树在分类准确性上差别不大,或者剪掉该节点子树,能在验证集中带来准确性的提升,那么就可以把该节点子树进行剪枝。方法是:用这个节点子树的叶子节点来替代该节点,类标记为这个节点子树中最频繁的那个类。
|
||||
|
||||
## 如何判断要不要去打篮球?
|
||||
|
||||
我给你准备了打篮球的数据集,训练数据如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/07/327eafa4a33e3e76ca86ac59195c0307.png" alt=""><br>
|
||||
我们该如何构造一个判断是否去打篮球的决策树呢?再回顾一下决策树的构造原理,在决策过程中有三个重要的问题:将哪个属性作为根节点?选择哪些属性作为后继节点?什么时候停止并得到目标值?
|
||||
|
||||
显然将哪个属性(天气、温度、湿度、刮风)作为根节点是个关键问题,在这里我们先介绍两个指标:**纯度**和**信息熵**。
|
||||
|
||||
先来说一下纯度。你可以把决策树的构造过程理解成为寻找纯净划分的过程。数学上,我们可以用纯度来表示,纯度换一种方式来解释就是让目标变量的分歧最小。
|
||||
|
||||
我在这里举个例子,假设有3个集合:
|
||||
|
||||
<li>
|
||||
集合1:6次都去打篮球;
|
||||
</li>
|
||||
<li>
|
||||
集合2:4次去打篮球,2次不去打篮球;
|
||||
</li>
|
||||
<li>
|
||||
集合3:3次去打篮球,3次不去打篮球。
|
||||
</li>
|
||||
|
||||
按照纯度指标来说,集合1>集合2>集合3。因为集合1的分歧最小,集合3的分歧最大。
|
||||
|
||||
然后我们再来介绍信息熵(entropy)的概念,**它表示了信息的不确定度**。
|
||||
|
||||
在信息论中,随机离散事件出现的概率存在着不确定性。为了衡量这种信息的不确定性,信息学之父香农引入了信息熵的概念,并给出了计算信息熵的数学公式:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/d5/741f0ed01c53fd53f0e75204542abed5.png" alt=""><br>
|
||||
p(i|t)代表了节点t为分类i的概率,其中log2为取以2为底的对数。这里我们不是来介绍公式的,而是说存在一种度量,它能帮我们反映出来这个信息的不确定度。当不确定性越大时,它所包含的信息量也就越大,信息熵也就越高。
|
||||
|
||||
我举个简单的例子,假设有2个集合
|
||||
|
||||
<li>
|
||||
集合1:5次去打篮球,1次不去打篮球;
|
||||
</li>
|
||||
<li>
|
||||
集合2:3次去打篮球,3次不去打篮球。
|
||||
</li>
|
||||
|
||||
在集合1中,有6次决策,其中打篮球是5次,不打篮球是1次。那么假设:类别1为“打篮球”,即次数为5;类别2为“不打篮球”,即次数为1。那么节点划分为类别1的概率是5/6,为类别2的概率是1/6,带入上述信息熵公式可以计算得出:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/a4/aba6bb24c3444923bfa2320119ce54a4.png" alt=""><br>
|
||||
同样,集合2中,也是一共6次决策,其中类别1中“打篮球”的次数是3,类别2“不打篮球”的次数也是3,那么信息熵为多少呢?我们可以计算得出:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/c2/e05fe27109330b49453b62505e37e4c2.png" alt=""><br>
|
||||
从上面的计算结果中可以看出,信息熵越大,纯度越低。当集合中的所有样本均匀混合时,信息熵最大,纯度最低。
|
||||
|
||||
我们在构造决策树的时候,会基于纯度来构建。而经典的 “不纯度”的指标有三种,分别是信息增益(ID3算法)、信息增益率(C4.5算法)以及基尼指数(Cart算法)。
|
||||
|
||||
我们先看下ID3算法。ID3算法计算的是**信息增益**,信息增益指的就是划分可以带来纯度的提高,信息熵的下降。它的计算公式,是父亲节点的信息熵减去所有子节点的信息熵。在计算的过程中,我们会计算每个子节点的归一化信息熵,即按照每个子节点在父节点中出现的概率,来计算这些子节点的信息熵。所以信息增益的公式可以表示为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bf/34/bfea7626733fff6180341c9dda3d4334.png" alt=""><br>
|
||||
公式中D是父亲节点,Di是子节点,Gain(D,a)中的a作为D节点的属性选择。
|
||||
|
||||
假设天气=晴的时候,会有5次去打篮球,5次不打篮球。其中D1刮风=是,有2次打篮球,1次不打篮球。D2 刮风=否,有3次打篮球,4次不打篮球。那么a 代表节点的属性,即天气=晴。
|
||||
|
||||
你可以在下面的图例中直观地了解这几个概念。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/67/40810468abc4140f45f3a09a2d427667.jpg" alt=""><br>
|
||||
比如针对图上这个例子,D作为节点的信息增益为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/82/f23f88a18b1227398c2ab3ef445d5382.png" alt=""><br>
|
||||
也就是D节点的信息熵-2个子节点的归一化信息熵。2个子节点归一化信息熵=3/10的D1信息熵+7/10的D2信息熵。
|
||||
|
||||
我们基于ID3的算法规则,完整地计算下我们的训练集,训练集中一共有7条数据,3个打篮球,4个不打篮球,所以根节点的信息熵是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/9b/9f01e1d1e8082e55850676da50a84f9b.png" alt="">
|
||||
|
||||
如果你将天气作为属性的划分,会有三个叶子节点D1、D2和D3,分别对应的是晴天、阴天和小雨。我们用+代表去打篮球,-代表不去打篮球。那么第一条记录,晴天不去打篮球,可以记为1-,于是我们可以用下面的方式来记录D1,D2,D3:
|
||||
|
||||
D1(天气=晴天)={1-,2-,6+}
|
||||
|
||||
D2(天气=阴天)={3+,7-}
|
||||
|
||||
D3(天气=小雨)={4+,5-}
|
||||
|
||||
我们先分别计算三个叶子节点的信息熵:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/7f/8537ab10a1a3747d22059bfbbd2aa17f.png" alt=""><br>
|
||||
因为D1有3个记录,D2有2个记录,D3有2个记录,所以D中的记录一共是3+2+2=7,即总数为7。所以D1在D(父节点)中的概率是3/7,D2在父节点的概率是2/7,D3在父节点的概率是2/7。那么作为子节点的归一化信息熵= 3/7*0.918+2/7*1.0+2/7*1.0=0.965。
|
||||
|
||||
因为我们用ID3中的信息增益来构造决策树,所以要计算每个节点的信息增益。
|
||||
|
||||
天气作为属性节点的信息增益为,Gain(D ,天气)=0.985-0.965=0.020。。
|
||||
|
||||
同理我们可以计算出其他属性作为根节点的信息增益,它们分别为 :
|
||||
|
||||
Gain(D ,温度)=0.128<br>
|
||||
Gain(D ,湿度)=0.020<br>
|
||||
Gain(D ,刮风)=0.020
|
||||
|
||||
我们能看出来温度作为属性的信息增益最大。因为ID3就是要将信息增益最大的节点作为父节点,这样可以得到纯度高的决策树,所以我们将温度作为根节点。其决策树状图分裂为下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/28/7067d83113c892a586e703d8b2e19828.jpg" alt=""><br>
|
||||
然后我们要将上图中第一个叶节点,也就是D1={1-,2-,3+,4+}进一步进行分裂,往下划分,计算其不同属性(天气、湿度、刮风)作为节点的信息增益,可以得到:
|
||||
|
||||
Gain(D ,湿度)=1<br>
|
||||
Gain(D ,天气)=1<br>
|
||||
Gain(D ,刮风)=0.3115
|
||||
|
||||
我们能看到湿度,或者天气为D1的节点都可以得到最大的信息增益,这里我们选取湿度作为节点的属性划分。同理,我们可以按照上面的计算步骤得到完整的决策树,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/48/1187ab0048daeec40cd261ea26cd3448.jpg" alt=""><br>
|
||||
于是我们通过ID3算法得到了一棵决策树。ID3的算法规则相对简单,可解释性强。同样也存在缺陷,比如我们会发现ID3算法倾向于选择取值比较多的属性。这样,如果我们把“编号”作为一个属性(一般情况下不会这么做,这里只是举个例子),那么“编号”将会被选为最优属性 。但实际上“编号”是无关属性的,它对“打篮球”的分类并没有太大作用。
|
||||
|
||||
所以ID3有一个缺陷就是,有些属性可能对分类任务没有太大作用,但是他们仍然可能会被选为最优属性。这种缺陷不是每次都会发生,只是存在一定的概率。在大部分情况下,ID3都能生成不错的决策树分类。针对可能发生的缺陷,后人提出了新的算法进行改进。
|
||||
|
||||
## 在ID3算法上进行改进的C4.5算法
|
||||
|
||||
那么C4.5都在哪些方面改进了ID3呢?
|
||||
|
||||
**1. 采用信息增益率**
|
||||
|
||||
因为ID3在计算的时候,倾向于选择取值多的属性。为了避免这个问题,C4.5采用信息增益率的方式来选择属性。信息增益率=信息增益/属性熵,具体的计算公式这里省略。
|
||||
|
||||
当属性有很多值的时候,相当于被划分成了许多份,虽然信息增益变大了,但是对于C4.5来说,属性熵也会变大,所以整体的信息增益率并不大。
|
||||
|
||||
**2. 采用悲观剪枝**
|
||||
|
||||
ID3构造决策树的时候,容易产生过拟合的情况。在C4.5中,会在决策树构造之后采用悲观剪枝(PEP),这样可以提升决策树的泛化能力。
|
||||
|
||||
悲观剪枝是后剪枝技术中的一种,通过递归估算每个内部节点的分类错误率,比较剪枝前后这个节点的分类错误率来决定是否对其进行剪枝。这种剪枝方法不再需要一个单独的测试数据集。
|
||||
|
||||
**3. 离散化处理连续属性**
|
||||
|
||||
C4.5可以处理连续属性的情况,对连续的属性进行离散化的处理。比如打篮球存在的“湿度”属性,不按照“高、中”划分,而是按照湿度值进行计算,那么湿度取什么值都有可能。该怎么选择这个阈值呢,**C4.5选择具有最高信息增益的划分所对应的阈值**。
|
||||
|
||||
**4. 处理缺失值**
|
||||
|
||||
针对数据集不完整的情况,C4.5也可以进行处理。
|
||||
|
||||
假如我们得到的是如下的数据,你会发现这个数据中存在两点问题。第一个问题是,数据集中存在数值缺失的情况,如何进行属性选择?第二个问题是,假设已经做了属性划分,但是样本在这个属性上有缺失值,该如何对样本进行划分?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/95/50b43c1820c03561f3ca3e627b454995.png" alt=""><br>
|
||||
我们不考虑缺失的数值,可以得到温度D={2-,3+,4+,5-,6+,7-}。温度=高:D1={2-,3+,4+} ;温度=中:D2={6+,7-};温度=低:D3={5-} 。这里+号代表打篮球,-号代表不打篮球。比如ID=2时,决策是不打篮球,我们可以记录为2-。
|
||||
|
||||
针对将属性选择为温度的信息增为:
|
||||
|
||||
Gain(D′, 温度)=Ent(D′)-0.792=1.0-0.792=0.208<br>
|
||||
属性熵=1.459, 信息增益率Gain_ratio(D′, 温度)=0.208/1.459=0.1426。
|
||||
|
||||
D′的样本个数为6,而D的样本个数为7,所以所占权重比例为6/7,所以Gain(D′,温度)所占权重比例为6/7,所以:<br>
|
||||
Gain_ratio(D, 温度)=6/7*0.1426=0.122。
|
||||
|
||||
这样即使在温度属性的数值有缺失的情况下,我们依然可以计算信息增益,并对属性进行选择。
|
||||
|
||||
Cart算法在这里不做介绍,我会在下一讲给你讲解这个算法。现在我们总结下ID3和C4.5算法。首先ID3算法的优点是方法简单,缺点是对噪声敏感。训练数据如果有少量错误,可能会产生决策树分类错误。C4.5在ID3的基础上,用信息增益率代替了信息增益,解决了噪声敏感的问题,并且可以对构造树进行剪枝、处理连续数值以及数值缺失等情况,但是由于C4.5需要对数据集进行多次扫描,算法效率相对较低。
|
||||
|
||||
## 总结
|
||||
|
||||
前面我们讲了两种决策树分类算法ID3和C4.5,了解了它们的数学原理。你可能会问,公式这么多,在实际使用中该怎么办呢?实际上,我们可以使用一些数据挖掘工具使用它们,比如Python的sklearn,或者是Weka(一个免费的数据挖掘工作平台),它们已经集成了这两种算法。只是我们在了解了这两种算法之后,才能更加清楚这两种算法的优缺点。
|
||||
|
||||
我们总结下,这次都讲到了哪些知识点呢?
|
||||
|
||||
首先我们采用决策树分类,需要了解它的原理,包括它的构造原理、剪枝原理。另外在信息度量上,我们需要了解信息度量中的纯度和信息熵的概念。在决策树的构造中,一个决策树包括根节点、子节点、叶子节点。在属性选择的标准上,度量方法包括了信息增益和信息增益率。在算法上,我讲解了两种算法:ID3和C4.5,其中ID3是基础的决策树算法,C4.5在它的基础上进行了改进,也是目前决策树中应用广泛的算法。然后在了解这些概念和原理后,强烈推荐你使用工具,具体工具的使用我会在后面进行介绍。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/7a/d02e69930c8cf00c93578536933ad07a.png" alt="">
|
||||
|
||||
最后我们留一道思考题吧。请你用下面的例子来模拟下决策树的流程,假设好苹果的数据如下,请用ID3算法来给出好苹果的决策树。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/09/0a759fd725add916417c2c294600b609.png" alt="">
|
||||
|
||||
如果你觉得这篇文章有所价值,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。
|
||||
|
||||
|
||||
227
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/18丨决策树(中):CART,一棵是回归树,另一棵是分类树.md
Normal file
227
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/18丨决策树(中):CART,一棵是回归树,另一棵是分类树.md
Normal file
@@ -0,0 +1,227 @@
|
||||
<audio id="audio" title="18丨决策树(中):CART,一棵是回归树,另一棵是分类树" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/d6/2cc78634b5338a0be9a52d53f0e2d4d6.mp3"></audio>
|
||||
|
||||
上节课我们讲了决策树,基于信息度量的不同方式,我们可以把决策树分为ID3算法、C4.5算法和CART算法。今天我来带你学习CART算法。CART算法,英文全称叫做Classification And Regression Tree,中文叫做分类回归树。ID3和C4.5算法可以生成二叉树或多叉树,而CART只支持二叉树。同时CART决策树比较特殊,既可以作分类树,又可以作回归树。
|
||||
|
||||
那么你首先需要了解的是,什么是分类树,什么是回归树呢?
|
||||
|
||||
我用下面的训练数据举个例子,你能看到不同职业的人,他们的年龄不同,学习时间也不同。如果我构造了一棵决策树,想要基于数据判断这个人的职业身份,这个就属于分类树,因为是从几个分类中来做选择。如果是给定了数据,想要预测这个人的年龄,那就属于回归树。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/cf/af89317aa55ac3b9f068b0f370fcb9cf.png" alt=""><br>
|
||||
分类树可以处理离散数据,也就是数据种类有限的数据,它输出的是样本的类别,而回归树可以对连续型的数值进行预测,也就是数据在某个区间内都有取值的可能,它输出的是一个数值。
|
||||
|
||||
## CART分类树的工作流程
|
||||
|
||||
通过上一讲,我们知道决策树的核心就是寻找纯净的划分,因此引入了纯度的概念。在属性选择上,我们是通过统计“不纯度”来做判断的,ID3是基于信息增益做判断,C4.5在ID3的基础上做了改进,提出了信息增益率的概念。实际上CART分类树与C4.5算法类似,只是属性选择的指标采用的是基尼系数。
|
||||
|
||||
你可能在经济学中听过说基尼系数,它是用来衡量一个国家收入差距的常用指标。当基尼系数大于0.4的时候,说明财富差异悬殊。基尼系数在0.2-0.4之间说明分配合理,财富差距不大。
|
||||
|
||||
基尼系数本身反应了样本的不确定度。当基尼系数越小的时候,说明样本之间的差异性小,不确定程度低。分类的过程本身是一个不确定度降低的过程,即纯度的提升过程。所以CART算法在构造分类树的时候,会选择基尼系数最小的属性作为属性的划分。
|
||||
|
||||
我们接下来详解了解一下基尼系数。基尼系数不好懂,你最好跟着例子一起手动计算下。
|
||||
|
||||
假设t为节点,那么该节点的GINI系数的计算公式为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/89/f9bb4cce5b895499cabc714eb372b089.png" alt=""><br>
|
||||
这里p(Ck|t)表示节点t属于类别Ck的概率,节点t的基尼系数为1减去各类别Ck概率平方和。
|
||||
|
||||
通过下面这个例子,我们计算一下两个集合的基尼系数分别为多少:
|
||||
|
||||
集合1:6个都去打篮球;
|
||||
|
||||
集合2:3个去打篮球,3个不去打篮球。
|
||||
|
||||
针对集合1,所有人都去打篮球,所以p(Ck|t)=1,因此GINI(t)=1-1=0。
|
||||
|
||||
针对集合2,有一半人去打篮球,而另一半不去打篮球,所以,p(C1|t)=0.5,p(C2|t)=0.5,GINI(t)=1-(0.5*0.5+0.5*0.5)=0.5。
|
||||
|
||||
通过两个基尼系数你可以看出,集合1的基尼系数最小,也证明样本最稳定,而集合2的样本不稳定性更大。
|
||||
|
||||
在CART算法中,基于基尼系数对特征属性进行二元分裂,假设属性A将节点D划分成了D1和D2,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/9a/69a90a43146898150a0de0811c6fef9a.jpg" alt=""><br>
|
||||
节点D的基尼系数等于子节点D1和D2的归一化基尼系数之和,用公式表示为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/1e/107fed838cb75df62eb149499db20c1e.png" alt=""><br>
|
||||
归一化基尼系数代表的是每个子节点的基尼系数乘以该节点占整体父亲节点D中的比例。
|
||||
|
||||
上面我们已经计算了集合D1和集合D2的GINI系数,得到:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/0c/aa423c65b32bded13212b7e20fb65a0c.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/09/77/092a0ea87aabc5da482ff8a992691b77.png" alt="">
|
||||
|
||||
所以在属性A的划分下,节点D的基尼系数为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/f8/3c08d5cd66a8ea098c397e14f1469ff8.png" alt="">
|
||||
|
||||
节点D被属性A划分后的基尼系数越大,样本集合的不确定性越大,也就是不纯度越高。
|
||||
|
||||
## 如何使用CART算法来创建分类树
|
||||
|
||||
通过上面的讲解你可以知道,CART分类树实际上是基于基尼系数来做属性划分的。在Python的sklearn中,如果我们想要创建CART分类树,可以直接使用DecisionTreeClassifier这个类。创建这个类的时候,默认情况下criterion这个参数等于gini,也就是按照基尼系数来选择属性划分,即默认采用的是CART分类树。
|
||||
|
||||
下面,我们来用CART分类树,给iris数据集构造一棵分类决策树。iris这个数据集,我在Python可视化中讲到过,实际上在sklearn中也自带了这个数据集。基于iris数据集,构造CART分类树的代码如下:
|
||||
|
||||
```
|
||||
# encoding=utf-8
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
from sklearn.datasets import load_iris
|
||||
# 准备数据集
|
||||
iris=load_iris()
|
||||
# 获取特征集和分类标识
|
||||
features = iris.data
|
||||
labels = iris.target
|
||||
# 随机抽取33%的数据作为测试集,其余为训练集
|
||||
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=0)
|
||||
# 创建CART分类树
|
||||
clf = DecisionTreeClassifier(criterion='gini')
|
||||
# 拟合构造CART分类树
|
||||
clf = clf.fit(train_features, train_labels)
|
||||
# 用CART分类树做预测
|
||||
test_predict = clf.predict(test_features)
|
||||
# 预测结果与测试集结果作比对
|
||||
score = accuracy_score(test_labels, test_predict)
|
||||
print("CART分类树准确率 %.4lf" % score)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
CART分类树准确率 0.9600
|
||||
|
||||
```
|
||||
|
||||
如果我们把决策树画出来,可以得到下面的图示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/40/c1e2f9e4a299789bb6cc23afc6fd3140.png" alt=""><br>
|
||||
首先train_test_split可以帮助我们把数据集抽取一部分作为测试集,这样我们就可以得到训练集和测试集。
|
||||
|
||||
使用clf = DecisionTreeClassifier(criterion=‘gini’)初始化一棵CART分类树。这样你就可以对CART分类树进行训练。
|
||||
|
||||
使用clf.fit(train_features, train_labels)函数,将训练集的特征值和分类标识作为参数进行拟合,得到CART分类树。
|
||||
|
||||
使用clf.predict(test_features)函数进行预测,传入测试集的特征值,可以得到测试结果test_predict。
|
||||
|
||||
最后使用accuracy_score(test_labels, test_predict)函数,传入测试集的预测结果与实际的结果作为参数,得到准确率score。
|
||||
|
||||
我们能看到sklearn帮我们做了CART分类树的使用封装,使用起来还是很方便的。
|
||||
|
||||
**CART回归树的工作流程**
|
||||
|
||||
CART回归树划分数据集的过程和分类树的过程是一样的,只是回归树得到的预测结果是连续值,而且评判“不纯度”的指标不同。在CART分类树中采用的是基尼系数作为标准,那么在CART回归树中,如何评价“不纯度”呢?实际上我们要根据样本的混乱程度,也就是样本的离散程度来评价“不纯度”。
|
||||
|
||||
样本的离散程度具体的计算方式是,先计算所有样本的均值,然后计算每个样本值到均值的差值。我们假设x为样本的个体,均值为u。为了统计样本的离散程度,我们可以取差值的绝对值,或者方差。
|
||||
|
||||
其中差值的绝对值为样本值减去样本均值的绝对值:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6f/97/6f9677a70b1edff85e9e467f3e52bd97.png" alt=""><br>
|
||||
方差为每个样本值减去样本均值的平方和除以样本个数:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/c1/045fd5afb7b53f17a8accd6f337f63c1.png" alt=""><br>
|
||||
所以这两种节点划分的标准,分别对应着两种目标函数最优化的标准,即用最小绝对偏差(LAD),或者使用最小二乘偏差(LSD)。这两种方式都可以让我们找到节点划分的方法,通常使用最小二乘偏差的情况更常见一些。
|
||||
|
||||
我们可以通过一个例子来看下如何创建一棵CART回归树来做预测。
|
||||
|
||||
## 如何使用CART回归树做预测
|
||||
|
||||
这里我们使用到sklearn自带的波士顿房价数据集,该数据集给出了影响房价的一些指标,比如犯罪率,房产税等,最后给出了房价。
|
||||
|
||||
根据这些指标,我们使用CART回归树对波士顿房价进行预测,代码如下:
|
||||
|
||||
```
|
||||
# encoding=utf-8
|
||||
from sklearn.metrics import mean_squared_error
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.datasets import load_boston
|
||||
from sklearn.metrics import r2_score,mean_absolute_error,mean_squared_error
|
||||
from sklearn.tree import DecisionTreeRegressor
|
||||
# 准备数据集
|
||||
boston=load_boston()
|
||||
# 探索数据
|
||||
print(boston.feature_names)
|
||||
# 获取特征集和房价
|
||||
features = boston.data
|
||||
prices = boston.target
|
||||
# 随机抽取33%的数据作为测试集,其余为训练集
|
||||
train_features, test_features, train_price, test_price = train_test_split(features, prices, test_size=0.33)
|
||||
# 创建CART回归树
|
||||
dtr=DecisionTreeRegressor()
|
||||
# 拟合构造CART回归树
|
||||
dtr.fit(train_features, train_price)
|
||||
# 预测测试集中的房价
|
||||
predict_price = dtr.predict(test_features)
|
||||
# 测试集的结果评价
|
||||
print('回归树二乘偏差均值:', mean_squared_error(test_price, predict_price))
|
||||
print('回归树绝对值偏差均值:', mean_absolute_error(test_price, predict_price))
|
||||
|
||||
```
|
||||
|
||||
运行结果(每次运行结果可能会有不同):
|
||||
|
||||
```
|
||||
['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO' 'B' 'LSTAT']
|
||||
回归树二乘偏差均值: 23.80784431137724
|
||||
回归树绝对值偏差均值: 3.040119760479042
|
||||
|
||||
```
|
||||
|
||||
如果把回归树画出来,可以得到下面的图示(波士顿房价数据集的指标有些多,所以树比较大):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/65/61/65a3855aed648b32994b808296a40b61.png" alt="">
|
||||
|
||||
你可以在[这里](https://pan.baidu.com/s/1RKD6-IwAzL--cL0jt4GPiQ)下载完整PDF文件。
|
||||
|
||||
我们来看下这个例子,首先加载了波士顿房价数据集,得到特征集和房价。然后通过train_test_split帮助我们把数据集抽取一部分作为测试集,其余作为训练集。
|
||||
|
||||
使用dtr=DecisionTreeRegressor()初始化一棵CART回归树。
|
||||
|
||||
使用dtr.fit(train_features, train_price)函数,将训练集的特征值和结果作为参数进行拟合,得到CART回归树。
|
||||
|
||||
使用dtr.predict(test_features)函数进行预测,传入测试集的特征值,可以得到预测结果predict_price。
|
||||
|
||||
最后我们可以求得这棵回归树的二乘偏差均值,以及绝对值偏差均值。
|
||||
|
||||
我们能看到CART回归树的使用和分类树类似,只是最后求得的预测值是个连续值。
|
||||
|
||||
## CART决策树的剪枝
|
||||
|
||||
CART决策树的剪枝主要采用的是CCP方法,它是一种后剪枝的方法,英文全称叫做cost-complexity prune,中文叫做代价复杂度。这种剪枝方式用到一个指标叫做节点的表面误差率增益值,以此作为剪枝前后误差的定义。用公式表示则是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/95/6b9735123d45e58f0b0afc7c3f68cd95.png" alt=""><br>
|
||||
其中Tt代表以t为根节点的子树,C(Tt)表示节点t的子树没被裁剪时子树Tt的误差,C(t)表示节点t的子树被剪枝后节点t的误差,|Tt|代子树Tt的叶子数,剪枝后,T的叶子数减少了|Tt|-1。
|
||||
|
||||
所以节点的表面误差率增益值等于节点t的子树被剪枝后的误差变化除以剪掉的叶子数量。
|
||||
|
||||
因为我们希望剪枝前后误差最小,所以我们要寻找的就是最小α值对应的节点,把它剪掉。这时候生成了第一个子树。重复上面的过程,继续剪枝,直到最后只剩下根节点,即为最后一个子树。
|
||||
|
||||
得到了剪枝后的子树集合后,我们需要用验证集对所有子树的误差计算一遍。可以通过计算每个子树的基尼指数或者平方误差,取误差最小的那个树,得到我们想要的结果。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了CART决策树,它是一棵决策二叉树,既可以做分类树,也可以做回归树。你需要记住的是,作为分类树,CART采用基尼系数作为节点划分的依据,得到的是离散的结果,也就是分类结果;作为回归树,CART可以采用最小绝对偏差(LAD),或者最小二乘偏差(LSD)作为节点划分的依据,得到的是连续值,即回归预测结果。
|
||||
|
||||
最后我们来整理下三种决策树之间在属性选择标准上的差异:
|
||||
|
||||
<li>
|
||||
ID3算法,基于信息增益做判断;
|
||||
</li>
|
||||
<li>
|
||||
C4.5算法,基于信息增益率做判断;
|
||||
</li>
|
||||
<li>
|
||||
CART算法,分类树是基于基尼系数做判断。回归树是基于偏差做判断。
|
||||
</li>
|
||||
|
||||
实际上这三个指标也是计算“不纯度”的三种计算方式。
|
||||
|
||||
在工具使用上,我们可以使用sklearn中的DecisionTreeClassifier创建CART分类树,通过DecisionTreeRegressor创建CART回归树。
|
||||
|
||||
你可以用代码自己跑一遍我在文稿中举到的例子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/84/5cfe1151f88befc1178eca3252890f84.png" alt=""><br>
|
||||
最后给你留两道思考题吧,你能说下ID3,C4.5,以及CART分类树在做节点划分时的区别吗?第二个问题是,sklearn中有个手写数字数据集,调用的方法是load_digits(),你能否创建一个CART分类树,对手写数字数据集做分类?另外选取一部分测试集,统计下分类树的准确率?
|
||||
|
||||
欢迎你在评论下面留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起交流。
|
||||
|
||||
|
||||
411
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/19丨决策树(下):泰坦尼克乘客生存预测.md
Normal file
411
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/19丨决策树(下):泰坦尼克乘客生存预测.md
Normal file
@@ -0,0 +1,411 @@
|
||||
<audio id="audio" title="19丨决策树(下):泰坦尼克乘客生存预测" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/24/c0/2488ceea1b379251231440e4f03b50c0.mp3"></audio>
|
||||
|
||||
在前面的两篇文章中,我给你讲了决策树算法。决策树算法是经常使用的数据挖掘算法,这是因为决策树就像一个人脑中的决策模型一样,呈现出来非常直观。基于决策树还诞生了很多数据挖掘算法,比如随机森林(Random forest)。
|
||||
|
||||
今天我来带你用决策树进行项目的实战。
|
||||
|
||||
决策树分类的应用场景非常广泛,在各行各业都有应用,比如在金融行业可以用决策树做贷款风险评估,医疗行业可以用决策树生成辅助诊断,电商行业可以用决策树对销售额进行预测等。
|
||||
|
||||
在了解决策树的原理后,今天我们用sklearn工具解决一个实际的问题:泰坦尼克号乘客的生存预测。
|
||||
|
||||
## sklearn中的决策树模型
|
||||
|
||||
首先,我们需要掌握sklearn中自带的决策树分类器DecisionTreeClassifier,方法如下:
|
||||
|
||||
```
|
||||
clf = DecisionTreeClassifier(criterion='entropy')
|
||||
|
||||
```
|
||||
|
||||
到目前为止,sklearn中只实现了ID3与CART决策树,所以我们暂时只能使用这两种决策树,在构造DecisionTreeClassifier类时,其中有一个参数是criterion,意为标准。它决定了构造的分类树是采用ID3分类树,还是CART分类树,对应的取值分别是entropy或者gini:
|
||||
|
||||
<li>
|
||||
entropy: 基于信息熵,也就是ID3算法,实际结果与C4.5相差不大;
|
||||
</li>
|
||||
<li>
|
||||
gini:默认参数,基于基尼系数。CART算法是基于基尼系数做属性划分的,所以criterion=gini时,实际上执行的是CART算法。
|
||||
</li>
|
||||
|
||||
我们通过设置criterion='entropy’可以创建一个ID3决策树分类器,然后打印下clf,看下决策树在sklearn中是个什么东西?
|
||||
|
||||
```
|
||||
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
|
||||
max_features=None, max_leaf_nodes=None,
|
||||
min_impurity_decrease=0.0, min_impurity_split=None,
|
||||
min_samples_leaf=1, min_samples_split=2,
|
||||
min_weight_fraction_leaf=0.0, presort=False, random_state=None,
|
||||
splitter='best')
|
||||
|
||||
```
|
||||
|
||||
这里我们看到了很多参数,除了设置criterion采用不同的决策树算法外,一般建议使用默认的参数,默认参数不会限制决策树的最大深度,不限制叶子节点数,认为所有分类的权重都相等等。当然你也可以调整这些参数,来创建不同的决策树模型。
|
||||
|
||||
我整理了这些参数代表的含义:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/0c/ead008e025a039c8731884ce2e29510c.jpg" alt="">
|
||||
|
||||
在构造决策树分类器后,我们可以使用fit方法让分类器进行拟合,使用predict方法对新数据进行预测,得到预测的分类结果,也可以使用score方法得到分类器的准确率。
|
||||
|
||||
下面这个表格是fit方法、predict方法和score方法的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/f8/3c7057b582b8078129c8342cde709ef8.png" alt="">
|
||||
|
||||
## Titanic乘客生存预测
|
||||
|
||||
**问题描述**
|
||||
|
||||
泰坦尼克海难是著名的十大灾难之一,究竟多少人遇难,各方统计的结果不一。现在我们可以得到部分的数据,具体数据你可以从GitHub上下载:[https://github.com/cystanford/Titanic_Data](https://github.com/cystanford/Titanic_Data)
|
||||
|
||||
(完整的项目代码见:[https://github.com/cystanford/Titanic_Data/blob/master/titanic_analysis.py](https://github.com/cystanford/Titanic_Data/blob/master/titanic_analysis.py) 你可以跟着学习后自己练习)
|
||||
|
||||
其中数据集格式为csv,一共有两个文件:
|
||||
|
||||
<li>
|
||||
train.csv是训练数据集,包含特征信息和存活与否的标签;
|
||||
</li>
|
||||
<li>
|
||||
test.csv: 测试数据集,只包含特征信息。
|
||||
</li>
|
||||
|
||||
现在我们需要用决策树分类对训练集进行训练,针对测试集中的乘客进行生存预测,并告知分类器的准确率。
|
||||
|
||||
在训练集中,包括了以下字段,它们具体为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/0a/2c14e00b64a73f40d75451a180c57b0a.png" alt=""><br>
|
||||
**生存预测的关键流程**
|
||||
|
||||
我们要对训练集中乘客的生存进行预测,这个过程可以划分为两个重要的阶段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/7f/6c08ca83a20a969100e5ceddee0ab27f.jpg" alt="">
|
||||
|
||||
<li>
|
||||
**准备阶段**:我们首先需要对训练集、测试集的数据进行探索,分析数据质量,并对数据进行清洗,然后通过特征选择对数据进行降维,方便后续分类运算;
|
||||
</li>
|
||||
<li>
|
||||
**分类阶段**:首先通过训练集的特征矩阵、分类结果得到决策树分类器,然后将分类器应用于测试集。然后我们对决策树分类器的准确性进行分析,并对决策树模型进行可视化。
|
||||
</li>
|
||||
|
||||
下面,我分别对这些模块进行介绍。
|
||||
|
||||
**模块1:数据探索**
|
||||
|
||||
数据探索这部分虽然对分类器没有实质作用,但是不可忽略。我们只有足够了解这些数据的特性,才能帮助我们做数据清洗、特征选择。
|
||||
|
||||
那么如何进行数据探索呢?这里有一些函数你需要了解:
|
||||
|
||||
<li>
|
||||
使用info()了解数据表的基本情况:行数、列数、每列的数据类型、数据完整度;
|
||||
</li>
|
||||
<li>
|
||||
使用describe()了解数据表的统计情况:总数、平均值、标准差、最小值、最大值等;
|
||||
</li>
|
||||
<li>
|
||||
使用describe(include=[‘O’])查看字符串类型(非数字)的整体情况;
|
||||
</li>
|
||||
<li>
|
||||
使用head查看前几行数据(默认是前5行);
|
||||
</li>
|
||||
<li>
|
||||
使用tail查看后几行数据(默认是最后5行)。
|
||||
</li>
|
||||
|
||||
我们可以使用Pandas便捷地处理这些问题:
|
||||
|
||||
```
|
||||
import pandas as pd
|
||||
# 数据加载
|
||||
train_data = pd.read_csv('./Titanic_Data/train.csv')
|
||||
test_data = pd.read_csv('./Titanic_Data/test.csv')
|
||||
# 数据探索
|
||||
print(train_data.info())
|
||||
print('-'*30)
|
||||
print(train_data.describe())
|
||||
print('-'*30)
|
||||
print(train_data.describe(include=['O']))
|
||||
print('-'*30)
|
||||
print(train_data.head())
|
||||
print('-'*30)
|
||||
print(train_data.tail())
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
<class 'pandas.core.frame.DataFrame'>
|
||||
RangeIndex: 891 entries, 0 to 890
|
||||
Data columns (total 12 columns):
|
||||
PassengerId 891 non-null int64
|
||||
Survived 891 non-null int64
|
||||
Pclass 891 non-null int64
|
||||
Name 891 non-null object
|
||||
Sex 891 non-null object
|
||||
Age 714 non-null float64
|
||||
SibSp 891 non-null int64
|
||||
Parch 891 non-null int64
|
||||
Ticket 891 non-null object
|
||||
Fare 891 non-null float64
|
||||
Cabin 204 non-null object
|
||||
Embarked 889 non-null object
|
||||
dtypes: float64(2), int64(5), object(5)
|
||||
memory usage: 83.6+ KB
|
||||
None
|
||||
------------------------------
|
||||
PassengerId Survived ... Parch Fare
|
||||
count 891.000000 891.000000 ... 891.000000 891.000000
|
||||
mean 446.000000 0.383838 ... 0.381594 32.204208
|
||||
std 257.353842 0.486592 ... 0.806057 49.693429
|
||||
min 1.000000 0.000000 ... 0.000000 0.000000
|
||||
25% 223.500000 0.000000 ... 0.000000 7.910400
|
||||
50% 446.000000 0.000000 ... 0.000000 14.454200
|
||||
75% 668.500000 1.000000 ... 0.000000 31.000000
|
||||
max 891.000000 1.000000 ... 6.000000 512.329200
|
||||
|
||||
[8 rows x 7 columns]
|
||||
------------------------------
|
||||
Name Sex ... Cabin Embarked
|
||||
count 891 891 ... 204 889
|
||||
unique 891 2 ... 147 3
|
||||
top Peter, Mrs. Catherine (Catherine Rizk) male ... B96 B98 S
|
||||
freq 1 577 ... 4 644
|
||||
|
||||
[4 rows x 5 columns]
|
||||
------------------------------
|
||||
PassengerId Survived Pclass ... Fare Cabin Embarked
|
||||
0 1 0 3 ... 7.2500 NaN S
|
||||
1 2 1 1 ... 71.2833 C85 C
|
||||
2 3 1 3 ... 7.9250 NaN S
|
||||
3 4 1 1 ... 53.1000 C123 S
|
||||
4 5 0 3 ... 8.0500 NaN S
|
||||
|
||||
[5 rows x 12 columns]
|
||||
------------------------------
|
||||
PassengerId Survived Pclass ... Fare Cabin Embarked
|
||||
886 887 0 2 ... 13.00 NaN S
|
||||
887 888 1 1 ... 30.00 B42 S
|
||||
888 889 0 3 ... 23.45 NaN S
|
||||
889 890 1 1 ... 30.00 C148 C
|
||||
890 891 0 3 ... 7.75 NaN Q
|
||||
|
||||
[5 rows x 12 columns]
|
||||
|
||||
```
|
||||
|
||||
**模块2:数据清洗**
|
||||
|
||||
通过数据探索,我们发现Age、Fare和Cabin这三个字段的数据有所缺失。其中Age为年龄字段,是数值型,我们可以通过平均值进行补齐;Fare为船票价格,是数值型,我们也可以通过其他人购买船票的平均值进行补齐。
|
||||
|
||||
具体实现的代码如下:
|
||||
|
||||
```
|
||||
# 使用平均年龄来填充年龄中的nan值
|
||||
train_data['Age'].fillna(train_data['Age'].mean(), inplace=True)
|
||||
test_data['Age'].fillna(test_data['Age'].mean(),inplace=True)
|
||||
# 使用票价的均值填充票价中的nan值
|
||||
train_data['Fare'].fillna(train_data['Fare'].mean(), inplace=True)
|
||||
test_data['Fare'].fillna(test_data['Fare'].mean(),inplace=True)
|
||||
|
||||
```
|
||||
|
||||
Cabin为船舱,有大量的缺失值。在训练集和测试集中的缺失率分别为77%和78%,无法补齐;Embarked为登陆港口,有少量的缺失值,我们可以把缺失值补齐。
|
||||
|
||||
首先观察下Embarked字段的取值,方法如下:
|
||||
|
||||
```
|
||||
print(train_data['Embarked'].value_counts())
|
||||
|
||||
```
|
||||
|
||||
结果如下:
|
||||
|
||||
```
|
||||
S 644
|
||||
C 168
|
||||
Q 77
|
||||
|
||||
```
|
||||
|
||||
我们发现一共就3个登陆港口,其中S港口人数最多,占到了72%,因此我们将其余缺失的Embarked数值均设置为S:
|
||||
|
||||
```
|
||||
# 使用登录最多的港口来填充登录港口的nan值
|
||||
train_data['Embarked'].fillna('S', inplace=True)
|
||||
test_data['Embarked'].fillna('S',inplace=True)
|
||||
|
||||
```
|
||||
|
||||
**模块3:特征选择**
|
||||
|
||||
特征选择是分类器的关键。特征选择不同,得到的分类器也不同。那么我们该选择哪些特征做生存的预测呢?
|
||||
|
||||
通过数据探索我们发现,PassengerId为乘客编号,对分类没有作用,可以放弃;Name为乘客姓名,对分类没有作用,可以放弃;Cabin字段缺失值太多,可以放弃;Ticket字段为船票号码,杂乱无章且无规律,可以放弃。其余的字段包括:Pclass、Sex、Age、SibSp、Parch和Fare,这些属性分别表示了乘客的船票等级、性别、年龄、亲戚数量以及船票价格,可能会和乘客的生存预测分类有关系。具体是什么关系,我们可以交给分类器来处理。
|
||||
|
||||
因此我们先将Pclass、Sex、Age等这些其余的字段作特征,放到特征向量features里。
|
||||
|
||||
```
|
||||
# 特征选择
|
||||
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
|
||||
train_features = train_data[features]
|
||||
train_labels = train_data['Survived']
|
||||
test_features = test_data[features]
|
||||
|
||||
```
|
||||
|
||||
特征值里有一些是字符串,这样不方便后续的运算,需要转成数值类型,比如Sex字段,有male和female两种取值。我们可以把它变成Sex=male和Sex=female两个字段,数值用0或1来表示。
|
||||
|
||||
同理Embarked有S、C、Q三种可能,我们也可以改成Embarked=S、Embarked=C和Embarked=Q三个字段,数值用0或1来表示。
|
||||
|
||||
那该如何操作呢,我们可以使用sklearn特征选择中的DictVectorizer类,用它将可以处理符号化的对象,将符号转成数字0/1进行表示。具体方法如下:
|
||||
|
||||
```
|
||||
from sklearn.feature_extraction import DictVectorizer
|
||||
dvec=DictVectorizer(sparse=False)
|
||||
train_features=dvec.fit_transform(train_features.to_dict(orient='record'))
|
||||
|
||||
```
|
||||
|
||||
你会看到代码中使用了fit_transform这个函数,它可以将特征向量转化为特征值矩阵。然后我们看下dvec在转化后的特征属性是怎样的,即查看dvec的feature_names_属性值,方法如下:
|
||||
|
||||
```
|
||||
print(dvec.feature_names_)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
['Age', 'Embarked=C', 'Embarked=Q', 'Embarked=S', 'Fare', 'Parch', 'Pclass', 'Sex=female', 'Sex=male', 'SibSp']
|
||||
|
||||
```
|
||||
|
||||
你可以看到原本是一列的Embarked,变成了“Embarked=C”“Embarked=Q”“Embarked=S”三列。Sex列变成了“Sex=female”“Sex=male”两列。
|
||||
|
||||
这样train_features特征矩阵就包括10个特征值(列),以及891个样本(行),即891行,10列的特征矩阵。
|
||||
|
||||
**模块4:决策树模型**
|
||||
|
||||
刚才我们已经讲了如何使用sklearn中的决策树模型。现在我们使用ID3算法,即在创建DecisionTreeClassifier时,设置criterion=‘entropy’,然后使用fit进行训练,将特征值矩阵和分类标识结果作为参数传入,得到决策树分类器。
|
||||
|
||||
```
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
# 构造ID3决策树
|
||||
clf = DecisionTreeClassifier(criterion='entropy')
|
||||
# 决策树训练
|
||||
clf.fit(train_features, train_labels)
|
||||
|
||||
```
|
||||
|
||||
**模块5:模型预测&评估**
|
||||
|
||||
在预测中,我们首先需要得到测试集的特征值矩阵,然后使用训练好的决策树clf进行预测,得到预测结果pred_labels:
|
||||
|
||||
```
|
||||
test_features=dvec.transform(test_features.to_dict(orient='record'))
|
||||
# 决策树预测
|
||||
pred_labels = clf.predict(test_features)
|
||||
|
||||
```
|
||||
|
||||
在模型评估中,决策树提供了score函数可以直接得到准确率,但是我们并不知道真实的预测结果,所以无法用预测值和真实的预测结果做比较。我们只能使用训练集中的数据进行模型评估,可以使用决策树自带的score函数计算下得到的结果:
|
||||
|
||||
```
|
||||
# 得到决策树准确率
|
||||
acc_decision_tree = round(clf.score(train_features, train_labels), 6)
|
||||
print(u'score准确率为 %.4lf' % acc_decision_tree)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
score准确率为 0.9820
|
||||
|
||||
```
|
||||
|
||||
你会发现你刚用训练集做训练,再用训练集自身做准确率评估自然会很高。但这样得出的准确率并不能代表决策树分类器的准确率。
|
||||
|
||||
这是为什么呢?
|
||||
|
||||
因为我们没有测试集的实际结果,因此无法用测试集的预测结果与实际结果做对比。如果我们使用score函数对训练集的准确率进行统计,正确率会接近于100%(如上结果为98.2%),无法对分类器的在实际环境下做准确率的评估。
|
||||
|
||||
那么有什么办法,来统计决策树分类器的准确率呢?
|
||||
|
||||
这里可以使用K折交叉验证的方式,交叉验证是一种常用的验证分类准确率的方法,原理是拿出大部分样本进行训练,少量的用于分类器的验证。K折交叉验证,就是做K次交叉验证,每次选取K分之一的数据作为验证,其余作为训练。轮流K次,取平均值。
|
||||
|
||||
K折交叉验证的原理是这样的:
|
||||
|
||||
<li>
|
||||
将数据集平均分割成K个等份;
|
||||
</li>
|
||||
<li>
|
||||
使用1份数据作为测试数据,其余作为训练数据;
|
||||
</li>
|
||||
<li>
|
||||
计算测试准确率;
|
||||
</li>
|
||||
<li>
|
||||
使用不同的测试集,重复2、3步骤。
|
||||
</li>
|
||||
|
||||
在sklearn的model_selection模型选择中提供了cross_val_score函数。cross_val_score函数中的参数cv代表对原始数据划分成多少份,也就是我们的K值,一般建议K值取10,因此我们可以设置CV=10,我们可以对比下score和cross_val_score两种函数的正确率的评估结果:
|
||||
|
||||
```
|
||||
import numpy as np
|
||||
from sklearn.model_selection import cross_val_score
|
||||
# 使用K折交叉验证 统计决策树准确率
|
||||
print(u'cross_val_score准确率为 %.4lf' % np.mean(cross_val_score(clf, train_features, train_labels, cv=10)))
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
cross_val_score准确率为 0.7835
|
||||
|
||||
```
|
||||
|
||||
你可以看到,score函数的准确率为0.9820,cross_val_score准确率为 0.7835。
|
||||
|
||||
这里很明显,对于不知道测试集实际结果的,要使用K折交叉验证才能知道模型的准确率。
|
||||
|
||||
**模块6:决策树可视化**
|
||||
|
||||
sklearn的决策树模型对我们来说,还是比较抽象的。我们可以使用Graphviz可视化工具帮我们把决策树呈现出来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/a0/4ae6016c6c7d1371586c5be015247da0.png" alt=""><br>
|
||||
安装Graphviz库需要下面的几步:
|
||||
|
||||
<li>
|
||||
安装graphviz工具,这里是它的下载地址;[http://www.graphviz.org/download/](http://www.graphviz.org/download/)
|
||||
</li>
|
||||
<li>
|
||||
将Graphviz添加到环境变量PATH中;
|
||||
</li>
|
||||
<li>
|
||||
需要Graphviz库,如果没有可以使用pip install graphviz进行安装。
|
||||
</li>
|
||||
|
||||
这样你就可以在程序里面使用Graphviz对决策树模型进行呈现,最后得到一个决策树可视化的PDF文件,可视化结果文件Source.gv.pdf你可以在GitHub上下载:[https://github.com/cystanford/Titanic_Data](https://github.com/cystanford/Titanic_Data)
|
||||
|
||||
## 决策树模型使用技巧总结
|
||||
|
||||
今天我用泰坦尼克乘客生存预测案例把决策树模型的流程跑了一遍。在实战中,你需要注意一下几点:
|
||||
|
||||
<li>
|
||||
特征选择是分类模型好坏的关键。选择什么样的特征,以及对应的特征值矩阵,决定了分类模型的好坏。通常情况下,特征值不都是数值类型,可以使用DictVectorizer类进行转化;
|
||||
</li>
|
||||
<li>
|
||||
模型准确率需要考虑是否有测试集的实际结果可以做对比,当测试集没有真实结果可以对比时,需要使用K折交叉验证cross_val_score;
|
||||
</li>
|
||||
<li>
|
||||
Graphviz可视化工具可以很方便地将决策模型呈现出来,帮助你更好理解决策树的构建。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f0/ea/f09fd3c8b1ce771624b803978f01c9ea.png" alt=""><br>
|
||||
我上面讲了泰坦尼克乘客生存预测的六个关键模块,请你用sklearn中的决策树模型独立完成这个项目,对测试集中的乘客是否生存进行预测,并给出模型准确率评估。数据从GitHub上下载即可。
|
||||
|
||||
最后给你留一个思考题吧,我在构造特征向量时使用了DictVectorizer类,使用fit_transform函数将特征向量转化为特征值矩阵。DictVectorizer类同时也提供transform函数,那么这两个函数有什么区别?
|
||||
|
||||
欢迎你在评论区留言与我分享你的答案,也欢迎点击“请朋友读”把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
213
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/20丨朴素贝叶斯分类(上):如何让机器判断男女?.md
Normal file
213
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/20丨朴素贝叶斯分类(上):如何让机器判断男女?.md
Normal file
@@ -0,0 +1,213 @@
|
||||
<audio id="audio" title="20丨朴素贝叶斯分类(上):如何让机器判断男女?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/88/80520c6d6d4d58188788705e4cbc0d88.mp3"></audio>
|
||||
|
||||
很多人都听说过贝叶斯原理,在哪听说过?基本上是在学概率统计的时候知道的。有些人可能会说,我记不住这些概率论的公式,没关系,我尽量用通俗易懂的语言进行讲解。
|
||||
|
||||
贝叶斯原理是英国数学家托马斯·贝叶斯提出的。贝叶斯是个很神奇的人,他的经历类似梵高。生前没有得到重视,死后,他写的一篇关于归纳推理的论文被朋友翻了出来,并发表了。这一发表不要紧,结果这篇论文的思想直接影响了接下来两个多世纪的统计学,是科学史上著名的论文之一。
|
||||
|
||||
贝叶斯原理跟我们的生活联系非常紧密。举个例子,如果你看到一个人总是花钱,那么会推断这个人多半是个有钱人。当然这也不是绝对,也就是说,当你不能准确预知一个事物本质的时候,你可以依靠和事物本质相关的事件来进行判断,如果事情发生的频次多,则证明这个属性更有可能存在。
|
||||
|
||||
## 贝叶斯原理
|
||||
|
||||
贝叶斯原理是怎么来的呢?贝叶斯为了解决一个叫“逆向概率”问题写了一篇文章,尝试解答在没有太多可靠证据的情况下,怎样做出更符合数学逻辑的推测。
|
||||
|
||||
什么是“逆向概率”呢?
|
||||
|
||||
所谓“逆向概率”是相对“正向概率”而言。正向概率的问题很容易理解,比如我们已经知道袋子里面有N 个球,不是黑球就是白球,其中M个是黑球,那么把手伸进去摸一个球,就能知道摸出黑球的概率是多少。但这种情况往往是上帝视角,即了解了事情的全貌再做判断。
|
||||
|
||||
在现实生活中,我们很难知道事情的全貌。贝叶斯则从实际场景出发,提了一个问题:如果我们事先不知道袋子里面黑球和白球的比例,而是通过我们摸出来的球的颜色,能判断出袋子里面黑白球的比例么?
|
||||
|
||||
正是这样的一个问题,影响了接下来近200年的统计学理论。这是因为,贝叶斯原理与其他统计学推断方法截然不同,它是建立在主观判断的基础上:在我们不了解所有客观事实的情况下,同样可以先估计一个值,然后根据实际结果不断进行修正。
|
||||
|
||||
我们用一个题目来体会下:假设有一种病叫做“贝叶死”,它的发病率是万分之一,即10000 人中会有1个人得病。现有一种测试可以检验一个人是否得病的准确率是99.9%,它的误报率是0.1%,那么现在的问题是,如果一个人被查出来患有“叶贝死”,实际上患有的可能性有多大?
|
||||
|
||||
你可能会想说,既然查出患有“贝叶死”的准确率是99.9%,那是不是实际上患“贝叶死”的概率也是99.9%呢?实际上不是的。你自己想想,在10000个人中,还存在0.1%的误查的情况,也就是10个人没有患病但是被诊断成阳性。当然10000个人中,也确实存在一个患有贝叶死的人,他有99.9%的概率被检查出来。所以你可以粗算下,患病的这个人实际上是这11个人里面的一员,即实际患病比例是1/11≈9%。
|
||||
|
||||
上面这个例子中,实际上涉及到了贝叶斯原理中的几个概念:
|
||||
|
||||
**先验概率**:
|
||||
|
||||
通过经验来判断事情发生的概率,比如说“贝叶死”的发病率是万分之一,就是先验概率。再比如南方的梅雨季是6-7月,就是通过往年的气候总结出来的经验,这个时候下雨的概率就比其他时间高出很多。
|
||||
|
||||
**后验概率**:
|
||||
|
||||
后验概率就是发生结果之后,推测原因的概率。比如说某人查出来了患有“贝叶死”,那么患病的原因可能是A、B或C。患有“贝叶死”是因为原因A的概率就是后验概率。它是属于条件概率的一种。
|
||||
|
||||
**条件概率**:
|
||||
|
||||
事件A 在另外一个事件B已经发生条件下的发生概率,表示为P(A|B),读作“在B 发生的条件下A 发生的概率”。比如原因A的条件下,患有“贝叶死”的概率,就是条件概率。
|
||||
|
||||
**似然函数(likelihood function)**:
|
||||
|
||||
你可以把概率模型的训练过程理解为求参数估计的过程。举个例子,如果一个硬币在10次抛落中正面均朝上。那么你肯定在想,这个硬币是均匀的可能性是多少?这里硬币均匀就是个参数,似然函数就是用来衡量这个模型的参数。似然在这里就是可能性的意思,它是关于统计参数的函数。
|
||||
|
||||
介绍完贝叶斯原理中的这几个概念,我们再来看下贝叶斯原理,实际上贝叶斯原理就是求解后验概率,我们假设:A 表示事件 “测出为阳性”, 用B1 表示“患有贝叶死”, B2 表示“没有患贝叶死”。根据上面那道题,我们可以得到下面的信息。
|
||||
|
||||
患有贝叶死的情况下,测出为阳性的概率为P(A|B1)=99.9%,没有患贝叶死,但测出为阳性的概率为P(A|B2)=0.1%。另外患有贝叶死的概率为 P(B1)=0.01%,没有患贝叶死的概率P(B2)=99.99%。
|
||||
|
||||
那么我们检测出来为阳性,而且是贝叶死的概率P(B1,A)=P(B1)*P(A|B1)=0.01%*99.9%=0.00999%。
|
||||
|
||||
这里P(B1,A)代表的是联合概率,同样我们可以求得P(B2,A)=P(B2)*P(A|B2)=99.99%*0.1%=0.09999%。
|
||||
|
||||
然后我们想求得是检查为阳性的情况下,患有贝叶死的概率,也即是P(B1|A)。
|
||||
|
||||
所以检查出阳性,且患有贝叶死的概率为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/21/faf6ff2fa5c9fef58a6e9d3508283f21.png" alt=""><br>
|
||||
检查出是阳性,但没有患有贝叶死的概率为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fd/60/fdf678a495966059681368a068261a60.png" alt=""><br>
|
||||
这里我们能看出来0.01%+0.1%均出现在了P(B1|A)和P(B2|A)的计算中作为分母。我们把它称之为论据因子,也相当于一个权值因子。
|
||||
|
||||
其中P(B1)、P(B2)就是先验概率,我们现在知道了观测值,就是被检测出来是阳性,来求患贝叶死的概率,也就是求后验概率。求后验概率就是贝叶斯原理要求的,基于刚才求得的P(B1|A),P(B2|A),我们可以总结出贝叶斯公式为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/5e/3163faf3b511e61408b46053aad7825e.png" alt=""><br>
|
||||
由此,我们可以得出通用的贝叶斯公式:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/4b/99f0e50baffa2c572ea0db6c5df4474b.png" alt=""><br>
|
||||
**朴素贝叶斯**
|
||||
|
||||
讲完贝叶斯原理之后,我们再来看下今天重点要讲的算法,朴素贝叶斯。**它是一种简单但极为强大的预测建模算法**。之所以称为朴素贝叶斯,是因为它假设每个输入变量是独立的。这是一个强硬的假设,实际情况并不一定,但是这项技术对于绝大部分的复杂问题仍然非常有效。
|
||||
|
||||
朴素贝叶斯模型由两种类型的概率组成:
|
||||
|
||||
<li>
|
||||
每个**类别的概率**P(Cj);
|
||||
</li>
|
||||
<li>
|
||||
每个属性的**条件概率**P(Ai|Cj)。
|
||||
</li>
|
||||
|
||||
我来举个例子说明下什么是类别概率和条件概率。假设我有7个棋子,其中3个是白色的,4个是黑色的。那么棋子是白色的概率就是3/7,黑色的概率就是4/7,这个就是类别概率。
|
||||
|
||||
假设我把这7个棋子放到了两个盒子里,其中盒子A里面有2个白棋,2个黑棋;盒子B里面有1个白棋,2个黑棋。那么在盒子A中抓到白棋的概率就是1/2,抓到黑棋的概率也是1/2,这个就是条件概率,也就是在某个条件(比如在盒子A中)下的概率。
|
||||
|
||||
在朴素贝叶斯中,我们要统计的是属性的条件概率,也就是假设取出来的是白色的棋子,那么它属于盒子A的概率是2/3。
|
||||
|
||||
为了训练朴素贝叶斯模型,我们需要先给出训练数据,以及这些数据对应的分类。那么上面这两个概率,也就是类别概率和条件概率。他们都可以从给出的训练数据中计算出来。一旦计算出来,概率模型就可以使用贝叶斯原理对新数据进行预测。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/d8/a48b541615039f40b3c524367065ead8.jpg" alt=""><br>
|
||||
另外我想告诉你的是,贝叶斯原理、贝叶斯分类和朴素贝叶斯这三者之间是有区别的。
|
||||
|
||||
贝叶斯原理是最大的概念,它解决了概率论中“逆向概率”的问题,在这个理论基础上,人们设计出了贝叶斯分类器,朴素贝叶斯分类是贝叶斯分类器中的一种,也是最简单,最常用的分类器。朴素贝叶斯之所以朴素是因为它假设属性是相互独立的,因此对实际情况有所约束,如果属性之间存在关联,分类准确率会降低。不过好在对于大部分情况下,朴素贝叶斯的分类效果都不错。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/fa/8d16d796670bb4901c7a4c13ca3aa1fa.jpg" alt="">
|
||||
|
||||
## 朴素贝叶斯分类工作原理
|
||||
|
||||
朴素贝叶斯分类是常用的贝叶斯分类方法。我们日常生活中看到一个陌生人,要做的第一件事情就是判断TA的性别,判断性别的过程就是一个分类的过程。根据以往的经验,我们通常会从身高、体重、鞋码、头发长短、服饰、声音等角度进行判断。这里的“经验”就是一个训练好的关于性别判断的模型,其训练数据是日常中遇到的各式各样的人,以及这些人实际的性别数据。
|
||||
|
||||
**离散数据案例**
|
||||
|
||||
我们遇到的数据可以分为两种,一种是离散数据,另一种是连续数据。那什么是离散数据呢?离散就是不连续的意思,有明确的边界,比如整数1,2,3就是离散数据,而1到3之间的任何数,就是连续数据,它可以取在这个区间里的任何数值。
|
||||
|
||||
我以下面的数据为例,这些是根据你之前的经验所获得的数据。然后给你一个新的数据:身高“高”、体重“中”,鞋码“中”,请问这个人是男还是女?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/de/5d/de0eb88143721c4503d10f0f7adc685d.png" alt=""><br>
|
||||
针对这个问题,我们先确定一共有3个属性,假设我们用A代表属性,用A1, A2, A3分别为身高=高、体重=中、鞋码=中。一共有两个类别,假设用C代表类别,那么C1,C2分别是:男、女,在未知的情况下我们用Cj表示。
|
||||
|
||||
那么我们想求在A1、A2、A3属性下,Cj的概率,用条件概率表示就是P(Cj|A1A2A3)。根据上面讲的贝叶斯的公式,我们可以得出:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/64/556ae2a160ce37ca48a456b7dc61e564.png" alt=""><br>
|
||||
因为一共有2个类别,所以我们只需要求得P(C1|A1A2A3)和P(C2|A1A2A3)的概率即可,然后比较下哪个分类的可能性大,就是哪个分类结果。
|
||||
|
||||
在这个公式里,因为P(A1A2A3)都是固定的,我们想要寻找使得P(Cj|A1A2A3)的最大值,就等价于求P(A1A2A3|Cj)P(Cj)最大值。
|
||||
|
||||
我们假定Ai之间是相互独立的,那么:
|
||||
|
||||
P(A1A2A3|Cj)=P(A1|Cj)P(A2|Cj)P(A3|Cj)
|
||||
|
||||
然后我们需要从Ai和Cj中计算出P(Ai|Cj)的概率,带入到上面的公式得出P(A1A2A3|Cj),最后找到使得P(A1A2A3|Cj)最大的类别Cj。
|
||||
|
||||
我分别求下这些条件下的概率:
|
||||
|
||||
P(A1|C1)=1/2, P(A2|C1)=1/2, P(A3|C1)=1/4,P(A1|C2)=0, P(A2|C2)=1/2, P(A3|C2)=1/2,所以P(A1A2A3|C1)=1/16, P(A1A2A3|C2)=0。
|
||||
|
||||
因为P(A1A2A3|C1)P(C1)>P(A1A2A3|C2)P(C2),所以应该是C1类别,即男性。
|
||||
|
||||
**连续数据案例**
|
||||
|
||||
我们做了一个离散的数据案例,实际生活中我们得到的是连续的数值,比如下面这组数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/28/2c1a8ae18ec5f6f50455aa54a24ad328.png" alt=""><br>
|
||||
那么如果给你一个新的数据,身高180、体重120,鞋码41,请问该人是男是女呢?
|
||||
|
||||
公式还是上面的公式,这里的困难在于,由于身高、体重、鞋码都是连续变量,不能采用离散变量的方法计算概率。而且由于样本太少,所以也无法分成区间计算。怎么办呢?
|
||||
|
||||
这时,可以假设男性和女性的身高、体重、鞋码都是正态分布,通过样本计算出均值和方差,也就是得到正态分布的密度函数。有了密度函数,就可以把值代入,算出某一点的密度函数的值。比如,男性的身高是均值179.5、标准差为3.697的正态分布。所以男性的身高为180的概率为0.1069。怎么计算得出的呢?你可以使用EXCEL的NORMDIST(x,mean,standard_dev,cumulative)函数,一共有4个参数:
|
||||
|
||||
<li>
|
||||
x:正态分布中,需要计算的数值;
|
||||
</li>
|
||||
<li>
|
||||
Mean:正态分布的平均值;
|
||||
</li>
|
||||
<li>
|
||||
Standard_dev:正态分布的标准差;
|
||||
</li>
|
||||
<li>
|
||||
Cumulative:取值为逻辑值,即False或True。它决定了函数的形式。当为TRUE时,函数结果为累积分布;为False时,函数结果为概率密度。
|
||||
</li>
|
||||
|
||||
这里我们使用的是NORMDIST(180,179.5,3.697,0)=0.1069。
|
||||
|
||||
同理我们可以计算得出男性体重为120的概率为0.000382324,男性鞋码为41号的概率为0.120304111。
|
||||
|
||||
所以我们可以计算得出:
|
||||
|
||||
P(A1A2A3|C1)=P(A1|C1)P(A2|C1)P(A3|C1)=0.1069**0.000382324**0.120304111=4.9169e-6
|
||||
|
||||
同理我们也可以计算出来该人为女的可能性:
|
||||
|
||||
P(A1A2A3|C2)=P(A1|C2)P(A2|C2)P(A3|C2)=0.00000147489**0.015354144**0.120306074=2.7244e-9
|
||||
|
||||
很明显这组数据分类为男的概率大于分类为女的概率。
|
||||
|
||||
当然在Python中,有第三方库可以直接帮我们进行上面的操作,这个我们会在下节课中介绍。这里主要是给你讲解下具体的运算原理。
|
||||
|
||||
## 朴素贝叶斯分类器工作流程
|
||||
|
||||
朴素贝叶斯分类常用于文本分类,尤其是对于英文等语言来说,分类效果很好。它常用于垃圾文本过滤、情感预测、推荐系统等。
|
||||
|
||||
流程可以用下图表示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/3f/acd7a8e882bf0205f9b33c43fd61453f.jpg" alt=""><br>
|
||||
从图片你也可以看出来,朴素贝叶斯分类器需要三个流程,我来给你一一讲解下这几个流程。
|
||||
|
||||
**第一阶段:准备阶段**
|
||||
|
||||
在这个阶段我们需要确定特征属性,比如上面案例中的“身高”、“体重”、“鞋码”等,并对每个特征属性进行适当划分,然后由人工对一部分数据进行分类,形成训练样本。
|
||||
|
||||
这一阶段是整个朴素贝叶斯分类中唯一需要人工完成的阶段,其质量对整个过程将有重要影响,分类器的质量很大程度上由特征属性、特征属性划分及训练样本质量决定。
|
||||
|
||||
**第二阶段:训练阶段**
|
||||
|
||||
这个阶段就是生成分类器,主要工作是计算每个类别在训练样本中的出现频率及每个特征属性划分对每个类别的条件概率。
|
||||
|
||||
输入是特征属性和训练样本,输出是分类器。
|
||||
|
||||
**第三阶段:应用阶段**
|
||||
|
||||
这个阶段是使用分类器对新数据进行分类。输入是分类器和新数据,输出是新数据的分类结果。
|
||||
|
||||
好了,在这次课中你了解了概率论中的贝叶斯原理,朴素贝叶斯的工作原理和工作流程,也对朴素贝叶斯的强大和限制有了认识。下一节中,我将带你实战,亲自掌握Python中关于朴素贝叶斯分类器工具的使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/de/10/debcda91831caefd356d377ddd1aad10.png" alt=""><br>
|
||||
最后给你留两道思考题吧,第一道题,离散型变量和连续变量在朴素贝叶斯模型中的处理有什么差别呢?
|
||||
|
||||
第二个问题是,如果你的女朋友,在你的手机里发现了和别的女人的暧昧短信,于是她开始思考了3个概率问题,你来判断下下面的3个概率分别属于哪种概率:
|
||||
|
||||
<li>
|
||||
你在没有任何情况下,出轨的概率;
|
||||
</li>
|
||||
<li>
|
||||
如果你出轨了,那么你的手机里有暧昧短信的概率;
|
||||
</li>
|
||||
<li>
|
||||
在你的手机里发现了暧昧短信,认为你出轨的概率。
|
||||
</li>
|
||||
|
||||
这三种概率分别属于先验概率、后验概率和条件概率的哪一种?
|
||||
|
||||
欢迎在评论区分享你的答案,我也会和你一起讨论。如果你觉得这篇文章对你有帮助,欢迎分享给你的朋友,一起来交流。
|
||||
|
||||
|
||||
323
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/21丨朴素贝叶斯分类(下):如何对文档进行分类?.md
Normal file
323
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/21丨朴素贝叶斯分类(下):如何对文档进行分类?.md
Normal file
@@ -0,0 +1,323 @@
|
||||
<audio id="audio" title="21丨朴素贝叶斯分类(下):如何对文档进行分类?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ee/e3/eec0c33f4d835287d62cc89d4c1e58e3.mp3"></audio>
|
||||
|
||||
我们上一节讲了朴素贝叶斯的工作原理,今天我们来讲下这些原理是如何指导实际业务的。
|
||||
|
||||
朴素贝叶斯分类最适合的场景就是文本分类、情感分析和垃圾邮件识别。其中情感分析和垃圾邮件识别都是通过文本来进行判断。从这里你能看出来,这三个场景本质上都是文本分类,这也是朴素贝叶斯最擅长的地方。所以朴素贝叶斯也常用于自然语言处理NLP的工具。
|
||||
|
||||
今天我带你一起使用朴素贝叶斯做下文档分类的项目,最重要的工具就是sklearn这个机器学习神器。
|
||||
|
||||
## sklearn机器学习包
|
||||
|
||||
sklearn的全称叫Scikit-learn,它给我们提供了3个朴素贝叶斯分类算法,分别是高斯朴素贝叶斯(GaussianNB)、多项式朴素贝叶斯(MultinomialNB)和伯努利朴素贝叶斯(BernoulliNB)。
|
||||
|
||||
这三种算法适合应用在不同的场景下,我们应该根据特征变量的不同选择不同的算法:
|
||||
|
||||
**高斯朴素贝叶斯**:特征变量是连续变量,符合高斯分布,比如说人的身高,物体的长度。
|
||||
|
||||
**多项式朴素贝叶斯**:特征变量是离散变量,符合多项分布,在文档分类中特征变量体现在一个单词出现的次数,或者是单词的TF-IDF值等。
|
||||
|
||||
**伯努利朴素贝叶斯**:特征变量是布尔变量,符合0/1分布,在文档分类中特征是单词是否出现。
|
||||
|
||||
伯努利朴素贝叶斯是以文件为粒度,如果该单词在某文件中出现了即为1,否则为0。而多项式朴素贝叶斯是以单词为粒度,会计算在某个文件中的具体次数。而高斯朴素贝叶斯适合处理特征变量是连续变量,且符合正态分布(高斯分布)的情况。比如身高、体重这种自然界的现象就比较适合用高斯朴素贝叶斯来处理。而文本分类是使用多项式朴素贝叶斯或者伯努利朴素贝叶斯。
|
||||
|
||||
## 什么是TF-IDF值呢?
|
||||
|
||||
我在多项式朴素贝叶斯中提到了“词的TF-IDF值”,如何理解这个概念呢?
|
||||
|
||||
TF-IDF是一个统计方法,用来评估某个词语对于一个文件集或文档库中的其中一份文件的重要程度。
|
||||
|
||||
TF-IDF实际上是两个词组Term Frequency和Inverse Document Frequency的总称,两者缩写为TF和IDF,分别代表了词频和逆向文档频率。
|
||||
|
||||
**词频TF**计算了一个单词在文档中出现的次数,它认为一个单词的重要性和它在文档中出现的次数呈正比。
|
||||
|
||||
**逆向文档频率IDF**,是指一个单词在文档中的区分度。它认为一个单词出现在的文档数越少,就越能通过这个单词把该文档和其他文档区分开。IDF越大就代表该单词的区分度越大。
|
||||
|
||||
**所以TF-IDF实际上是词频TF和逆向文档频率IDF的乘积**。这样我们倾向于找到TF和IDF取值都高的单词作为区分,即这个单词在一个文档中出现的次数多,同时又很少出现在其他文档中。这样的单词适合用于分类。
|
||||
|
||||
## TF-IDF如何计算
|
||||
|
||||
首先我们看下词频TF和逆向文档概率IDF的公式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/4d/bc31ff1f31f9cd26144404221f705d4d.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/65/b7ad53560f61407e6964e7436da14365.png" alt="">
|
||||
|
||||
为什么IDF的分母中,单词出现的文档数要加1呢?因为有些单词可能不会存在文档中,为了避免分母为0,统一给单词出现的文档数都加1。
|
||||
|
||||
**TF-IDF=TF*IDF。**
|
||||
|
||||
你可以看到,TF-IDF值就是TF与IDF的乘积,这样可以更准确地对文档进行分类。比如“我”这样的高频单词,虽然TF词频高,但是IDF值很低,整体的TF-IDF也不高。
|
||||
|
||||
我在这里举个例子。假设一个文件夹里一共有10篇文档,其中一篇文档有1000个单词,“this”这个单词出现20次,“bayes”出现了5次。“this”在所有文档中均出现过,而“bayes”只在2篇文档中出现过。我们来计算一下这两个词语的TF-IDF值。
|
||||
|
||||
针对“this”,计算TF-IDF值:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/63/12/63abe3ce8aa0ea4a78ba537b5504df12.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/7e/b5ac88c4e2a71cc2d4ceef4c01e0ba7e.png" alt="">
|
||||
|
||||
所以TF-IDF=0.02*(-0.0414)=-8.28e-4。
|
||||
|
||||
针对“bayes”,计算TF-IDF值:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/8d/3bbe56a7b76513604bfe6b39b890dd8d.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/2e/1e8b7465b9949fe071e95aede172a52e.png" alt="">
|
||||
|
||||
TF-IDF=0.005*0.5229=2.61e-3。
|
||||
|
||||
很明显“bayes”的TF-IDF值要大于“this”的TF-IDF值。这就说明用“bayes”这个单词做区分比单词“this”要好。
|
||||
|
||||
**如何求TF-IDF**
|
||||
|
||||
在sklearn中我们直接使用TfidfVectorizer类,它可以帮我们计算单词TF-IDF向量的值。在这个类中,取sklearn计算的对数log时,底数是e,不是10。
|
||||
|
||||
下面我来讲下如何创建TfidfVectorizer类。
|
||||
|
||||
## TfidfVectorizer类的创建:
|
||||
|
||||
创建TfidfVectorizer的方法是:
|
||||
|
||||
```
|
||||
TfidfVectorizer(stop_words=stop_words, token_pattern=token_pattern)
|
||||
|
||||
```
|
||||
|
||||
我们在创建的时候,有两个构造参数,可以自定义停用词stop_words和规律规则token_pattern。需要注意的是传递的数据结构,停用词stop_words是一个列表List类型,而过滤规则token_pattern是正则表达式。
|
||||
|
||||
什么是停用词?停用词就是在分类中没有用的词,这些词一般词频TF高,但是IDF很低,起不到分类的作用。为了节省空间和计算时间,我们把这些词作为停用词stop words,告诉机器这些词不需要帮我计算。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/e9/040723cc99b36e8ad7e45aa31e0690e9.png" alt=""><br>
|
||||
当我们创建好TF-IDF向量类型时,可以用fit_transform帮我们计算,返回给我们文本矩阵,该矩阵表示了每个单词在每个文档中的TF-IDF值。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/43/0d2263fbc97beb520680382f08656b43.png" alt=""><br>
|
||||
在我们进行fit_transform拟合模型后,我们可以得到更多的TF-IDF向量属性,比如,我们可以得到词汇的对应关系(字典类型)和向量的IDF值,当然也可以获取设置的停用词stop_words。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/6b/a42780a5bca0531e75a294b4e2fe356b.png" alt=""><br>
|
||||
举个例子,假设我们有4个文档:
|
||||
|
||||
文档1:this is the bayes document;
|
||||
|
||||
文档2:this is the second second document;
|
||||
|
||||
文档3:and the third one;
|
||||
|
||||
文档4:is this the document。
|
||||
|
||||
现在想要计算文档里都有哪些单词,这些单词在不同文档中的TF-IDF值是多少呢?
|
||||
|
||||
首先我们创建TfidfVectorizer类:
|
||||
|
||||
```
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
tfidf_vec = TfidfVectorizer()
|
||||
|
||||
```
|
||||
|
||||
然后我们创建4个文档的列表documents,并让创建好的tfidf_vec对documents进行拟合,得到TF-IDF矩阵:
|
||||
|
||||
```
|
||||
documents = [
|
||||
'this is the bayes document',
|
||||
'this is the second second document',
|
||||
'and the third one',
|
||||
'is this the document'
|
||||
]
|
||||
tfidf_matrix = tfidf_vec.fit_transform(documents)
|
||||
|
||||
```
|
||||
|
||||
输出文档中所有不重复的词:
|
||||
|
||||
```
|
||||
print('不重复的词:', tfidf_vec.get_feature_names())
|
||||
|
||||
```
|
||||
|
||||
运行结果
|
||||
|
||||
```
|
||||
不重复的词: ['and', 'bayes', 'document', 'is', 'one', 'second', 'the', 'third', 'this']
|
||||
|
||||
```
|
||||
|
||||
输出每个单词对应的id值:
|
||||
|
||||
```
|
||||
print('每个单词的ID:', tfidf_vec.vocabulary_)
|
||||
|
||||
```
|
||||
|
||||
运行结果
|
||||
|
||||
```
|
||||
每个单词的ID: {'this': 8, 'is': 3, 'the': 6, 'bayes': 1, 'document': 2, 'second': 5, 'and': 0, 'third': 7, 'one': 4}
|
||||
|
||||
```
|
||||
|
||||
输出每个单词在每个文档中的TF-IDF值,向量里的顺序是按照词语的id顺序来的:
|
||||
|
||||
```
|
||||
print('每个单词的tfidf值:', tfidf_matrix.toarray())
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
每个单词的tfidf值: [[0. 0.63314609 0.40412895 0.40412895 0. 0.
|
||||
0.33040189 0. 0.40412895]
|
||||
[0. 0. 0.27230147 0.27230147 0. 0.85322574
|
||||
0.22262429 0. 0.27230147]
|
||||
[0.55280532 0. 0. 0. 0.55280532 0.
|
||||
0.28847675 0.55280532 0. ]
|
||||
[0. 0. 0.52210862 0.52210862 0. 0.
|
||||
0.42685801 0. 0.52210862]]
|
||||
|
||||
```
|
||||
|
||||
## 如何对文档进行分类
|
||||
|
||||
如果我们要对文档进行分类,有两个重要的阶段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/c3/257e01f173e8bc78b37b71b2358ff7c3.jpg" alt="">
|
||||
|
||||
<li>
|
||||
**基于分词的数据准备**,包括分词、单词权重计算、去掉停用词;
|
||||
</li>
|
||||
<li>
|
||||
**应用朴素贝叶斯分类进行分类**,首先通过训练集得到朴素贝叶斯分类器,然后将分类器应用于测试集,并与实际结果做对比,最终得到测试集的分类准确率。
|
||||
</li>
|
||||
|
||||
下面,我分别对这些模块进行介绍。
|
||||
|
||||
**模块1:对文档进行分词**
|
||||
|
||||
在准备阶段里,最重要的就是分词。那么如果给文档进行分词呢?英文文档和中文文档所使用的分词工具不同。
|
||||
|
||||
在英文文档中,最常用的是NTLK包。NTLK包中包含了英文的停用词stop words、分词和标注方法。
|
||||
|
||||
```
|
||||
import nltk
|
||||
word_list = nltk.word_tokenize(text) #分词
|
||||
nltk.pos_tag(word_list) #标注单词的词性
|
||||
|
||||
```
|
||||
|
||||
在中文文档中,最常用的是jieba包。jieba包中包含了中文的停用词stop words和分词方法。
|
||||
|
||||
```
|
||||
import jieba
|
||||
word_list = jieba.cut (text) #中文分词
|
||||
|
||||
```
|
||||
|
||||
**模块2:加载停用词表**
|
||||
|
||||
我们需要自己读取停用词表文件,从网上可以找到中文常用的停用词保存在stop_words.txt,然后利用Python的文件读取函数读取文件,保存在stop_words数组中。
|
||||
|
||||
```
|
||||
stop_words = [line.strip().decode('utf-8') for line in io.open('stop_words.txt').readlines()]
|
||||
|
||||
```
|
||||
|
||||
**模块3:计算单词的权重**
|
||||
|
||||
这里我们用到sklearn里的TfidfVectorizer类,上面我们介绍过它使用的方法。
|
||||
|
||||
直接创建TfidfVectorizer类,然后使用fit_transform方法进行拟合,得到TF-IDF特征空间features,你可以理解为选出来的分词就是特征。我们计算这些特征在文档上的特征向量,得到特征空间features。
|
||||
|
||||
```
|
||||
tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5)
|
||||
features = tf.fit_transform(train_contents)
|
||||
|
||||
```
|
||||
|
||||
这里max_df参数用来描述单词在文档中的最高出现率。假设max_df=0.5,代表一个单词在50%的文档中都出现过了,那么它只携带了非常少的信息,因此就不作为分词统计。
|
||||
|
||||
一般很少设置min_df,因为min_df通常都会很小。
|
||||
|
||||
**模块4:生成朴素贝叶斯分类器**
|
||||
|
||||
我们将特征训练集的特征空间train_features,以及训练集对应的分类train_labels传递给贝叶斯分类器clf,它会自动生成一个符合特征空间和对应分类的分类器。
|
||||
|
||||
这里我们采用的是多项式贝叶斯分类器,其中alpha为平滑参数。为什么要使用平滑呢?因为如果一个单词在训练样本中没有出现,这个单词的概率就会被计算为0。但训练集样本只是整体的抽样情况,我们不能因为一个事件没有观察到,就认为整个事件的概率为0。为了解决这个问题,我们需要做平滑处理。
|
||||
|
||||
当alpha=1时,使用的是Laplace平滑。Laplace平滑就是采用加1的方式,来统计没有出现过的单词的概率。这样当训练样本很大的时候,加1得到的概率变化可以忽略不计,也同时避免了零概率的问题。
|
||||
|
||||
当0<alpha<1时,使用的是Lidstone平滑。对于Lidstone平滑来说,alpha 越小,迭代次数越多,精度越高。我们可以设置alpha为0.001。
|
||||
|
||||
```
|
||||
# 多项式贝叶斯分类器
|
||||
from sklearn.naive_bayes import MultinomialNB
|
||||
clf = MultinomialNB(alpha=0.001).fit(train_features, train_labels)
|
||||
|
||||
```
|
||||
|
||||
**模块5:使用生成的分类器做预测**
|
||||
|
||||
首先我们需要得到测试集的特征矩阵。
|
||||
|
||||
方法是用训练集的分词创建一个TfidfVectorizer类,使用同样的stop_words和max_df,然后用这个TfidfVectorizer类对测试集的内容进行fit_transform拟合,得到测试集的特征矩阵test_features。
|
||||
|
||||
```
|
||||
test_tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5, vocabulary=train_vocabulary)
|
||||
test_features=test_tf.fit_transform(test_contents)
|
||||
|
||||
```
|
||||
|
||||
然后我们用训练好的分类器对新数据做预测。
|
||||
|
||||
方法是使用predict函数,传入测试集的特征矩阵test_features,得到分类结果predicted_labels。predict函数做的工作就是求解所有后验概率并找出最大的那个。
|
||||
|
||||
```
|
||||
predicted_labels=clf.predict(test_features)
|
||||
|
||||
```
|
||||
|
||||
**模块6:计算准确率**
|
||||
|
||||
计算准确率实际上是对分类模型的评估。我们可以调用sklearn中的metrics包,在metrics中提供了accuracy_score函数,方便我们对实际结果和预测的结果做对比,给出模型的准确率。
|
||||
|
||||
使用方法如下:
|
||||
|
||||
```
|
||||
from sklearn import metrics
|
||||
print metrics.accuracy_score(test_labels, predicted_labels)
|
||||
|
||||
```
|
||||
|
||||
## 数据挖掘神器sklearn
|
||||
|
||||
从数据挖掘的流程来看,一般包括了获取数据、数据清洗、模型训练、模型评估和模型部署这几个过程。
|
||||
|
||||
sklearn中包含了大量的数据挖掘算法,比如三种朴素贝叶斯算法,我们只需要了解不同算法的适用条件,以及创建时所需的参数,就可以用模型帮我们进行训练。在模型评估中,sklearn提供了metrics包,帮我们对预测结果与实际结果进行评估。
|
||||
|
||||
在文档分类的项目中,我们针对文档的特点,给出了基于分词的准备流程。一般来说NTLK包适用于英文文档,而jieba适用于中文文档。我们可以根据文档选择不同的包,对文档提取分词。这些分词就是贝叶斯分类中最重要的特征属性。基于这些分词,我们得到分词的权重,即特征矩阵。
|
||||
|
||||
通过特征矩阵与分类结果,我们就可以创建出朴素贝叶斯分类器,然后用分类器进行预测,最后预测结果与实际结果做对比即可以得到分类器在测试集上的准确率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/6e/2e2962ddb7e85a71e0cecb9c6d13306e.png" alt="">
|
||||
|
||||
## 练习题
|
||||
|
||||
我已经讲了中文文档分类中的6个关键的模块,最后,我给你留一道对中文文档分类的练习题吧。
|
||||
|
||||
我将中文文档数据集上传到了GitHub上,[点击这里下载](https://github.com/cystanford/text_classification)。
|
||||
|
||||
数据说明:
|
||||
|
||||
1. 文档共有4种类型:女性、体育、文学、校园;
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/28/67abc1783f7c4e7cd69194fafc514328.png" alt="">
|
||||
|
||||
1. 训练集放到train文件夹里,测试集放到test文件夹里,停用词放到stop文件夹里。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0c/0f/0c374e3501cc28a24687bc030733050f.png" alt=""><br>
|
||||
请使用朴素贝叶斯分类对训练集进行训练,并对测试集进行验证,并给出测试集的准确率。
|
||||
|
||||
最后你不妨思考一下,假设我们要判断一个人的性别,是通过身高、体重、鞋码、外貌等属性进行判断的,如果我们用朴素贝叶斯做分类,适合使用哪种朴素贝叶斯分类器?停用词的作用又是什么?
|
||||
|
||||
欢迎你在评论区进行留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
148
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/22丨SVM(上):如何用一根棍子将蓝红两色球分开?.md
Normal file
148
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/22丨SVM(上):如何用一根棍子将蓝红两色球分开?.md
Normal file
@@ -0,0 +1,148 @@
|
||||
<audio id="audio" title="22丨SVM(上):如何用一根棍子将蓝红两色球分开?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/49/9dedf904e39f47c1865d15ab91b18d49.mp3"></audio>
|
||||
|
||||
今天我来带你进行SVM的学习,SVM的英文叫Support Vector Machine,中文名为支持向量机。它是常见的一种分类方法,在机器学习中,SVM是有监督的学习模型。
|
||||
|
||||
什么是有监督的学习模型呢?它指的是我们需要事先对数据打上分类标签,这样机器就知道这个数据属于哪个分类。同样无监督学习,就是数据没有被打上分类标签,这可能是因为我们不具备先验的知识,或者打标签的成本很高。所以我们需要机器代我们部分完成这个工作,比如将数据进行聚类,方便后续人工对每个类进行分析。SVM作为有监督的学习模型,通常可以帮我们模式识别、分类以及回归分析。
|
||||
|
||||
听起来,是不是很高大上。我先带你做个小练习。
|
||||
|
||||
练习1:桌子上我放了红色和蓝色两种球,请你用一根棍子将这两种颜色的球分开。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/88/210bc20b5963d474d425ad1ca9ac6888.jpg" alt=""><br>
|
||||
你可以很快想到解决方案,在红色和蓝色球之间画条直线就好了,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/bb/ec657a6d274b5afb4d8169df9c248abb.jpg" alt=""><br>
|
||||
练习2:这次难度升级,桌子上依然放着红色、蓝色两种球,但是它们的摆放不规律,如下图所示。如何用一根棍子把这两种颜色分开呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b4/a3/b4b9793cdec47d0ea1528ff1922973a3.jpg" alt=""><br>
|
||||
你可能想了想,认为一根棍子是分不开的。除非把棍子弯曲,像下面这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/eb/144d72013a808e955e78718f6df3d2eb.jpg" alt=""><br>
|
||||
所以这里直线变成了曲线。如果在同一个平面上来看,红蓝两种颜色的球是很难分开的。那么有没有一种方式,可以让它们自然地分开呢?
|
||||
|
||||
这里你可能会灵机一动,猛拍一下桌子,这些小球瞬间腾空而起,如下图所示。在腾起的那一刹那,出现了一个水平切面,恰好把红、蓝两种颜色的球分开。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/dc/9e07b78932456cd8a6f46d7ea65bdadc.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/34/f3497cd97c8bb06e952efcdae6059434.jpg" alt=""><br>
|
||||
在这里,二维平面变成了三维空间。原来的曲线变成了一个平面。这个平面,我们就叫做超平面。
|
||||
|
||||
## SVM的工作原理
|
||||
|
||||
用SVM计算的过程就是帮我们找到那个超平面的过程,这个超平面就是我们的SVM分类器。
|
||||
|
||||
我们再过头来看最简单的练习1,其实我们可以有多种直线的划分,比如下图所示的直线A、直线B和直线C,究竟哪种才是更好的划分呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/e7/7459de7fbe99af85cbfdacf2333f84e7.jpg" alt=""><br>
|
||||
很明显图中的直线B更靠近蓝色球,但是在真实环境下,球再多一些的话,蓝色球可能就被划分到了直线B的右侧,被认为是红色球。同样直线A更靠近红色球,在真实环境下,如果红色球再多一些,也可能会被误认为是蓝色球。所以相比于直线A和直线B,直线C的划分更优,因为它的鲁棒性更强。
|
||||
|
||||
那怎样才能寻找到直线C这个更优的答案呢?这里,我们引入一个SVM特有的概念:**分类间隔**。
|
||||
|
||||
实际上,我们的分类环境不是在二维平面中的,而是在多维空间中,这样直线C就变成了决策面C。
|
||||
|
||||
在保证决策面不变,且分类不产生错误的情况下,我们可以移动决策面C,直到产生两个极限的位置:如图中的决策面A和决策面B。极限的位置是指,如果越过了这个位置,就会产生分类错误。这样的话,两个极限位置A和B之间的分界线C就是最优决策面。极限位置到最优决策面C之间的距离,就是“分类间隔”,英文叫做margin。
|
||||
|
||||
如果我们转动这个最优决策面,你会发现可能存在多个最优决策面,它们都能把数据集正确分开,这些最优决策面的分类间隔可能是不同的,而那个拥有“最大间隔”(max margin)的决策面就是SVM要找的最优解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/ea/506cc4b85a9206cca12048b29919a7ea.jpg" alt=""><br>
|
||||
**点到超平面的距离公式**
|
||||
|
||||
在上面这个例子中,如果我们把红蓝两种颜色的球放到一个三维空间里,你发现决策面就变成了一个平面。这里我们可以用线性函数来表示,如果在一维空间里就表示一个点,在二维空间里表示一条直线,在三维空间中代表一个平面,当然空间维数还可以更多,这样我们给这个线性函数起个名称叫做“超平面”。超平面的数学表达可以写成:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/28/765e87a2d9d6358f1274478dacbbce28.png" alt=""><br>
|
||||
在这个公式里,w、x是n维空间里的向量,其中x是函数变量;w是法向量。法向量这里指的是垂直于平面的直线所表示的向量,它决定了超平面的方向。
|
||||
|
||||
**SVM就是帮我们找到一个超平面**,这个超平面能将不同的样本划分开,同时使得样本集中的点到这个分类超平面的最小距离(即分类间隔)最大化。
|
||||
|
||||
在这个过程中,**支持向量**就是离**分类超平面**最近的样本点,实际上如果确定了支持向量也就确定了这个超平面。所以支持向量决定了分类间隔到底是多少,而在最大间隔以外的样本点,其实对分类都没有意义。
|
||||
|
||||
所以说, SVM就是求解最大分类间隔的过程,我们还需要对分类间隔的大小进行定义。
|
||||
|
||||
首先,我们定义某类样本集到超平面的距离是这个样本集合内的样本到超平面的最短距离。我们用di代表点xi到超平面wxi+b=0的欧氏距离。因此我们要求di的最小值,用它来代表这个样本到超平面的最短距离。di可以用公式计算得出:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/76/8342b5253cb4c294c72cef6802814176.png" alt=""><br>
|
||||
其中||w||为超平面的范数,di的公式可以用解析几何知识进行推导,这里不做解释。
|
||||
|
||||
**最大间隔的优化模型**
|
||||
|
||||
我们的目标就是找出所有分类间隔中最大的那个值对应的超平面。在数学上,这是一个凸优化问题(凸优化就是关于求凸集中的凸函数最小化的问题,这里不具体展开)。通过凸优化问题,最后可以求出最优的w和b,也就是我们想要找的最优超平面。中间求解的过程会用到拉格朗日乘子,和KKT(Karush-Kuhn-Tucker)条件。数学公式比较多,这里不进行展开。
|
||||
|
||||
## 硬间隔、软间隔和非线性SVM
|
||||
|
||||
假如数据是完全的线性可分的,那么学习到的模型可以称为硬间隔支持向量机。**换个说法,硬间隔指的就是完全分类准确,不能存在分类错误的情况。软间隔,就是允许一定量的样本分类错误**。
|
||||
|
||||
我们知道,实际工作中的数据没有那么“干净”,或多或少都会存在一些噪点。所以线性可分是个理想情况。这时,我们需要使用到软间隔SVM(近似线性可分),比如下面这种情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/a6/7c913ee92cdcf4d461f0ebc1314123a6.jpg" alt=""><br>
|
||||
另外还存在一种情况,就是非线性支持向量机。
|
||||
|
||||
比如下面的样本集就是个非线性的数据。图中的两类数据,分别分布为两个圆圈的形状。那么这种情况下,不论是多高级的分类器,只要映射函数是线性的,就没法处理,SVM 也处理不了。这时,我们需要引入一个新的概念:**核函数。它可以将样本从原始空间映射到一个更高维的特质空间中,使得样本在新的空间中线性可分**。这样我们就可以使用原来的推导来进行计算,只是所有的推导是在新的空间,而不是在原来的空间中进行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/23/5530b0e61085a213ef1d0dfe02b70223.jpg" alt=""><br>
|
||||
所以在非线性SVM中,核函数的选择就是影响SVM最大的变量。最常用的核函数有线性核、多项式核、高斯核、拉普拉斯核、sigmoid核,或者是这些核函数的组合。这些函数的区别在于映射方式的不同。通过这些核函数,我们就可以把样本空间投射到新的高维空间中。
|
||||
|
||||
当然软间隔和核函数的提出,都是为了方便我们对上面超平面公式中的w*和b*进行求解,从而得到最大分类间隔的超平面。
|
||||
|
||||
## 用SVM如何解决多分类问题
|
||||
|
||||
SVM本身是一个二值分类器,最初是为二分类问题设计的,也就是回答Yes或者是No。而实际上我们要解决的问题,可能是多分类的情况,比如对文本进行分类,或者对图像进行识别。
|
||||
|
||||
针对这种情况,我们可以将多个二分类器组合起来形成一个多分类器,常见的方法有“一对多法”和“一对一法”两种。
|
||||
|
||||
1.一对多法
|
||||
|
||||
假设我们要把物体分成A、B、C、D四种分类,那么我们可以先把其中的一类作为分类1,其他类统一归为分类2。这样我们可以构造4种SVM,分别为以下的情况:
|
||||
|
||||
(1)样本A作为正集,B,C,D作为负集;
|
||||
|
||||
(2)样本B作为正集,A,C,D作为负集;
|
||||
|
||||
(3)样本C作为正集,A,B,D作为负集;
|
||||
|
||||
(4)样本D作为正集,A,B,C作为负集。
|
||||
|
||||
这种方法,针对K个分类,需要训练K个分类器,分类速度较快,但训练速度较慢,因为每个分类器都需要对全部样本进行训练,而且负样本数量远大于正样本数量,会造成样本不对称的情况,而且当增加新的分类,比如第K+1类时,需要重新对分类器进行构造。
|
||||
|
||||
2.一对一法
|
||||
|
||||
一对一法的初衷是想在训练的时候更加灵活。我们可以在任意两类样本之间构造一个SVM,这样针对K类的样本,就会有C(k,2)类分类器。
|
||||
|
||||
比如我们想要划分A、B、C三个类,可以构造3个分类器:
|
||||
|
||||
(1)分类器1:A、B;
|
||||
|
||||
(2)分类器2:A、C;
|
||||
|
||||
(3)分类器3:B、C。
|
||||
|
||||
当对一个未知样本进行分类时,每一个分类器都会有一个分类结果,即为1票,最终得票最多的类别就是整个未知样本的类别。
|
||||
|
||||
这样做的好处是,如果新增一类,不需要重新训练所有的SVM,只需要训练和新增这一类样本的分类器。而且这种方式在训练单个SVM模型的时候,训练速度快。
|
||||
|
||||
但这种方法的不足在于,分类器的个数与K的平方成正比,所以当K较大时,训练和测试的时间会比较慢。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了SVM分类器,它在文本分类尤其是针对二分类任务性能卓越。同样,针对多分类的情况,我们可以采用一对多,或者一对一的方法,多个二值分类器组合成一个多分类器。
|
||||
|
||||
另外关于SVM分类器的概念,我希望你能掌握以下的三个程度:
|
||||
|
||||
<li>
|
||||
完全线性可分情况下的线性分类器,也就是线性可分的情况,是最原始的SVM,它最核心的思想就是找到最大的分类间隔;
|
||||
</li>
|
||||
<li>
|
||||
大部分线性可分情况下的线性分类器,引入了软间隔的概念。软间隔,就是允许一定量的样本分类错误;
|
||||
</li>
|
||||
<li>
|
||||
线性不可分情况下的非线性分类器,引入了核函数。它让原有的样本空间通过核函数投射到了一个高维的空间中,从而变得线性可分。
|
||||
</li>
|
||||
|
||||
在SVM的推导过程中,有大量的数学公式,这里不进行推导演绎,因为除了写论文,你大部分时候不会用到这些公式推导。
|
||||
|
||||
所以最重要的还是理解我上面讲的这些概念,能在实际工作中使用SVM才是最重要的。下一节我会和你讲如何用sklearn工具包进行SVM分类,带你做一个实际的案例。
|
||||
|
||||
最后,你能说一下你对有监督学习和无监督学习的理解吗?以及,SVM最主要的思想就是硬间隔、软间隔和核函数。你是如何理解它们的?
|
||||
|
||||
欢迎你在评论区进行留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
243
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/23丨SVM(下):如何进行乳腺癌检测?.md
Normal file
243
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/23丨SVM(下):如何进行乳腺癌检测?.md
Normal file
@@ -0,0 +1,243 @@
|
||||
<audio id="audio" title="23丨SVM(下):如何进行乳腺癌检测?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/cc/b338f169d9377c97fe6353c962591dcc.mp3"></audio>
|
||||
|
||||
讲完了SVM的原理之后,今天我来带你进行SVM的实战。
|
||||
|
||||
在此之前我们先来回顾一下SVM的相关知识点。SVM是有监督的学习模型,我们需要事先对数据打上分类标签,通过求解最大分类间隔来求解二分类问题。如果要求解多分类问题,可以将多个二分类器组合起来形成一个多分类器。
|
||||
|
||||
上一节中讲到了硬间隔、软间隔、非线性SVM,以及分类间隔的公式,你可能会觉得比较抽象。这节课,我们会在实际使用中,讲解对工具的使用,以及相关参数的含义。
|
||||
|
||||
## 如何在sklearn中使用SVM
|
||||
|
||||
在Python的sklearn工具包中有SVM算法,首先需要引用工具包:
|
||||
|
||||
```
|
||||
from sklearn import svm
|
||||
|
||||
```
|
||||
|
||||
SVM既可以做回归,也可以做分类器。
|
||||
|
||||
当用SVM做回归的时候,我们可以使用SVR或LinearSVR。SVR的英文是Support Vector Regression。这篇文章只讲分类,这里只是简单地提一下。
|
||||
|
||||
当做分类器的时候,我们使用的是SVC或者LinearSVC。SVC的英文是Support Vector Classification。
|
||||
|
||||
我简单说一下这两者之前的差别。
|
||||
|
||||
从名字上你能看出LinearSVC是个线性分类器,用于处理线性可分的数据,只能使用线性核函数。上一节,我讲到SVM是通过核函数将样本从原始空间映射到一个更高维的特质空间中,这样就使得样本在新的空间中线性可分。
|
||||
|
||||
如果是针对非线性的数据,需要用到SVC。在SVC中,我们既可以使用到线性核函数(进行线性划分),也能使用高维的核函数(进行非线性划分)。
|
||||
|
||||
如何创建一个SVM分类器呢?
|
||||
|
||||
我们首先使用SVC的构造函数:model = svm.SVC(kernel=‘rbf’, C=1.0, gamma=‘auto’),这里有三个重要的参数kernel、C和gamma。
|
||||
|
||||
kernel代表核函数的选择,它有四种选择,只不过默认是rbf,即高斯核函数。
|
||||
|
||||
<li>
|
||||
linear:线性核函数
|
||||
</li>
|
||||
<li>
|
||||
poly:多项式核函数
|
||||
</li>
|
||||
<li>
|
||||
rbf:高斯核函数(默认)
|
||||
</li>
|
||||
<li>
|
||||
sigmoid:sigmoid核函数
|
||||
</li>
|
||||
|
||||
这四种函数代表不同的映射方式,你可能会问,在实际工作中,如何选择这4种核函数呢?我来给你解释一下:
|
||||
|
||||
线性核函数,是在数据线性可分的情况下使用的,运算速度快,效果好。不足在于它不能处理线性不可分的数据。
|
||||
|
||||
多项式核函数可以将数据从低维空间映射到高维空间,但参数比较多,计算量大。
|
||||
|
||||
高斯核函数同样可以将样本映射到高维空间,但相比于多项式核函数来说所需的参数比较少,通常性能不错,所以是默认使用的核函数。
|
||||
|
||||
了解深度学习的同学应该知道sigmoid经常用在神经网络的映射中。因此当选用sigmoid核函数时,SVM实现的是多层神经网络。
|
||||
|
||||
上面介绍的4种核函数,除了第一种线性核函数外,其余3种都可以处理线性不可分的数据。
|
||||
|
||||
参数C代表目标函数的惩罚系数,惩罚系数指的是分错样本时的惩罚程度,默认情况下为1.0。当C越大的时候,分类器的准确性越高,但同样容错率会越低,泛化能力会变差。相反,C越小,泛化能力越强,但是准确性会降低。
|
||||
|
||||
参数gamma代表核函数的系数,默认为样本特征数的倒数,即gamma = 1 / n_features。
|
||||
|
||||
在创建SVM分类器之后,就可以输入训练集对它进行训练。我们使用model.fit(train_X,train_y),传入训练集中的特征值矩阵train_X和分类标识train_y。特征值矩阵就是我们在特征选择后抽取的特征值矩阵(当然你也可以用全部数据作为特征值矩阵);分类标识就是人工事先针对每个样本标识的分类结果。这样模型会自动进行分类器的训练。我们可以使用prediction=model.predict(test_X)来对结果进行预测,传入测试集中的样本特征矩阵test_X,可以得到测试集的预测分类结果prediction。
|
||||
|
||||
同样我们也可以创建线性SVM分类器,使用model=svm.LinearSVC()。在LinearSVC中没有kernel这个参数,限制我们只能使用线性核函数。由于LinearSVC对线性分类做了优化,对于数据量大的线性可分问题,使用LinearSVC的效率要高于SVC。
|
||||
|
||||
如果你不知道数据集是否为线性,可以直接使用SVC类创建SVM分类器。
|
||||
|
||||
在训练和预测中,LinearSVC和SVC一样,都是使用model.fit(train_X,train_y)和model.predict(test_X)。
|
||||
|
||||
## 如何用SVM进行乳腺癌检测
|
||||
|
||||
在了解了如何创建和使用SVM分类器后,我们来看一个实际的项目,数据集来自美国威斯康星州的乳腺癌诊断数据集,[点击这里进行下载](https://github.com/cystanford/breast_cancer_data/)。
|
||||
|
||||
医疗人员采集了患者乳腺肿块经过细针穿刺(FNA)后的数字化图像,并且对这些数字图像进行了特征提取,这些特征可以描述图像中的细胞核呈现。肿瘤可以分成良性和恶性。部分数据截屏如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/97/6a/97a33c5bfc182d571e9707db653eff6a.png" alt=""><br>
|
||||
数据表一共包括了32个字段,代表的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/13/1e6af6fa8bebdfba10457c111b5e9c13.jpg" alt="">
|
||||
|
||||
上面的表格中,mean代表平均值,se代表标准差,worst代表最大值(3个最大值的平均值)。每张图像都计算了相应的特征,得出了这30个特征值(不包括ID字段和分类标识结果字段diagnosis),实际上是10个特征值(radius、texture、perimeter、area、smoothness、compactness、concavity、concave points、symmetry和fractal_dimension_mean)的3个维度,平均、标准差和最大值。这些特征值都保留了4位数字。字段中没有缺失的值。在569个患者中,一共有357个是良性,212个是恶性。
|
||||
|
||||
好了,我们的目标是生成一个乳腺癌诊断的SVM分类器,并计算这个分类器的准确率。首先设定项目的执行流程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/97/f9/9768905bf3cf6d8946a64caa8575e1f9.png" alt="">
|
||||
|
||||
<li>
|
||||
首先我们需要加载数据源;
|
||||
</li>
|
||||
<li>
|
||||
在准备阶段,需要对加载的数据源进行探索,查看样本特征和特征值,这个过程你也可以使用数据可视化,它可以方便我们对数据及数据之间的关系进一步加深了解。然后按照“完全合一”的准则来评估数据的质量,如果数据质量不高就需要做数据清洗。数据清洗之后,你可以做特征选择,方便后续的模型训练;
|
||||
</li>
|
||||
<li>
|
||||
在分类阶段,选择核函数进行训练,如果不知道数据是否为线性,可以考虑使用SVC(kernel=‘rbf’) ,也就是高斯核函数的SVM分类器。然后对训练好的模型用测试集进行评估。
|
||||
</li>
|
||||
|
||||
按照上面的流程,我们来编写下代码,加载数据并对数据做部分的探索:
|
||||
|
||||
```
|
||||
# 加载数据集,你需要把数据放到目录中
|
||||
data = pd.read_csv("./data.csv")
|
||||
# 数据探索
|
||||
# 因为数据集中列比较多,我们需要把dataframe中的列全部显示出来
|
||||
pd.set_option('display.max_columns', None)
|
||||
print(data.columns)
|
||||
print(data.head(5))
|
||||
print(data.describe())
|
||||
|
||||
```
|
||||
|
||||
这是部分的运行结果,完整结果你可以自己跑一下。
|
||||
|
||||
```
|
||||
Index(['id', 'diagnosis', 'radius_mean', 'texture_mean', 'perimeter_mean',
|
||||
'area_mean', 'smoothness_mean', 'compactness_mean', 'concavity_mean',
|
||||
'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean',
|
||||
'radius_se', 'texture_se', 'perimeter_se', 'area_se', 'smoothness_se',
|
||||
'compactness_se', 'concavity_se', 'concave points_se', 'symmetry_se',
|
||||
'fractal_dimension_se', 'radius_worst', 'texture_worst',
|
||||
'perimeter_worst', 'area_worst', 'smoothness_worst',
|
||||
'compactness_worst', 'concavity_worst', 'concave points_worst',
|
||||
'symmetry_worst', 'fractal_dimension_worst'],
|
||||
dtype='object')
|
||||
id diagnosis radius_mean texture_mean perimeter_mean area_mean \
|
||||
0 842302 M 17.99 10.38 122.80 1001.0
|
||||
1 842517 M 20.57 17.77 132.90 1326.0
|
||||
2 84300903 M 19.69 21.25 130.00 1203.0
|
||||
3 84348301 M 11.42 20.38 77.58 386.1
|
||||
4 84358402 M 20.29 14.34 135.10 1297.0
|
||||
|
||||
```
|
||||
|
||||
接下来,我们就要对数据进行清洗了。
|
||||
|
||||
运行结果中,你能看到32个字段里,id是没有实际含义的,可以去掉。diagnosis字段的取值为B或者M,我们可以用0和1来替代。另外其余的30个字段,其实可以分成三组字段,下划线后面的mean、se和worst代表了每组字段不同的度量方式,分别是平均值、标准差和最大值。
|
||||
|
||||
```
|
||||
# 将特征字段分成3组
|
||||
features_mean= list(data.columns[2:12])
|
||||
features_se= list(data.columns[12:22])
|
||||
features_worst=list(data.columns[22:32])
|
||||
# 数据清洗
|
||||
# ID列没有用,删除该列
|
||||
data.drop("id",axis=1,inplace=True)
|
||||
# 将B良性替换为0,M恶性替换为1
|
||||
data['diagnosis']=data['diagnosis'].map({'M':1,'B':0})
|
||||
|
||||
```
|
||||
|
||||
然后我们要做特征字段的筛选,首先需要观察下features_mean各变量之间的关系,这里我们可以用DataFrame的corr()函数,然后用热力图帮我们可视化呈现。同样,我们也会看整体良性、恶性肿瘤的诊断情况。
|
||||
|
||||
```
|
||||
# 将肿瘤诊断结果可视化
|
||||
sns.countplot(data['diagnosis'],label="Count")
|
||||
plt.show()
|
||||
# 用热力图呈现features_mean字段之间的相关性
|
||||
corr = data[features_mean].corr()
|
||||
plt.figure(figsize=(14,14))
|
||||
# annot=True显示每个方格的数据
|
||||
sns.heatmap(corr, annot=True)
|
||||
plt.show()
|
||||
|
||||
```
|
||||
|
||||
这是运行的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/4d/a65435de48cee8091bd5f83d286ddb4d.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/6e/0780e76fd3807759ab4881c2c39cb76e.png" alt=""><br>
|
||||
热力图中对角线上的为单变量自身的相关系数是1。颜色越浅代表相关性越大。所以你能看出来radius_mean、perimeter_mean和area_mean相关性非常大,compactness_mean、concavity_mean、concave_points_mean这三个字段也是相关的,因此我们可以取其中的一个作为代表。
|
||||
|
||||
那么如何进行特征选择呢?
|
||||
|
||||
特征选择的目的是降维,用少量的特征代表数据的特性,这样也可以增强分类器的泛化能力,避免数据过拟合。
|
||||
|
||||
我们能看到mean、se和worst这三组特征是对同一组内容的不同度量方式,我们可以保留mean这组特征,在特征选择中忽略掉se和worst。同时我们能看到mean这组特征中,radius_mean、perimeter_mean、area_mean这三个属性相关性大,compactness_mean、daconcavity_mean、concave points_mean这三个属性相关性大。我们分别从这2类中选择1个属性作为代表,比如radius_mean和compactness_mean。
|
||||
|
||||
这样我们就可以把原来的10个属性缩减为6个属性,代码如下:
|
||||
|
||||
```
|
||||
# 特征选择
|
||||
features_remain = ['radius_mean','texture_mean', 'smoothness_mean','compactness_mean','symmetry_mean', 'fractal_dimension_mean']
|
||||
|
||||
```
|
||||
|
||||
对特征进行选择之后,我们就可以准备训练集和测试集:
|
||||
|
||||
```
|
||||
# 抽取30%的数据作为测试集,其余作为训练集
|
||||
train, test = train_test_split(data, test_size = 0.3)# in this our main data is splitted into train and test
|
||||
# 抽取特征选择的数值作为训练和测试数据
|
||||
train_X = train[features_remain]
|
||||
train_y=train['diagnosis']
|
||||
test_X= test[features_remain]
|
||||
test_y =test['diagnosis']
|
||||
|
||||
```
|
||||
|
||||
在训练之前,我们需要对数据进行规范化,这样让数据同在同一个量级上,避免因为维度问题造成数据误差:
|
||||
|
||||
```
|
||||
# 采用Z-Score规范化数据,保证每个特征维度的数据均值为0,方差为1
|
||||
ss = StandardScaler()
|
||||
train_X = ss.fit_transform(train_X)
|
||||
test_X = ss.transform(test_X)
|
||||
|
||||
```
|
||||
|
||||
最后我们可以让SVM做训练和预测了:
|
||||
|
||||
```
|
||||
# 创建SVM分类器
|
||||
model = svm.SVC()
|
||||
# 用训练集做训练
|
||||
model.fit(train_X,train_y)
|
||||
# 用测试集做预测
|
||||
prediction=model.predict(test_X)
|
||||
print('准确率: ', metrics.accuracy_score(test_y,prediction))
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
准确率: 0.9181286549707602
|
||||
|
||||
```
|
||||
|
||||
准确率大于90%,说明训练结果还不错。完整的代码你可以从[GitHub](https://github.com/cystanford/breast_cancer_data)上下载。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你一起做了乳腺癌诊断分类的SVM实战,从这个过程中你应该能体会出来整个执行的流程,包括数据加载、数据探索、数据清洗、特征选择、SVM训练和结果评估等环节。
|
||||
|
||||
sklearn已经为我们提供了很好的工具,对上节课中讲到的SVM的创建和训练都进行了封装,让我们无需关心中间的运算细节。但正因为这样,我们更需要对每个流程熟练掌握,通过实战项目训练数据化思维和对数据的敏感度。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/79/82/797fe646ae4668139600fca2c50c5282.png" alt=""><br>
|
||||
最后给你留两道思考题吧。还是这个乳腺癌诊断的数据,请你用LinearSVC,选取全部的特征(除了ID以外)作为训练数据,看下你的分类器能得到多少的准确度呢?另外你对sklearn中SVM使用又有什么样的体会呢?
|
||||
|
||||
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来交流,一起来进步。
|
||||
129
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/24丨KNN(上):如何根据打斗和接吻次数来划分电影类型?.md
Normal file
129
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/24丨KNN(上):如何根据打斗和接吻次数来划分电影类型?.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<audio id="audio" title="24丨KNN(上):如何根据打斗和接吻次数来划分电影类型?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/22/4c6cb0d4b941254d064f275d496e1122.mp3"></audio>
|
||||
|
||||
今天我来带你进行KNN的学习,KNN的英文叫K-Nearest Neighbor,应该算是数据挖掘算法中最简单的一种。
|
||||
|
||||
我们先用一个例子体会下。
|
||||
|
||||
假设,我们想对电影的类型进行分类,统计了电影中打斗次数、接吻次数,当然还有其他的指标也可以被统计到,如下表所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/87/6dac3a9961e69aa86d80de32bdc00987.png" alt=""><br>
|
||||
我们很容易理解《战狼》《红海行动》《碟中谍6》是动作片,《前任3》《春娇救志明》《泰坦尼克号》是爱情片,但是有没有一种方法让机器也可以掌握这个分类的规则,当有一部新电影的时候,也可以对它的类型自动分类呢?
|
||||
|
||||
我们可以把打斗次数看成X轴,接吻次数看成Y轴,然后在二维的坐标轴上,对这几部电影进行标记,如下图所示。对于未知的电影A,坐标为(x,y),我们需要看下离电影A最近的都有哪些电影,这些电影中的大多数属于哪个分类,那么电影A就属于哪个分类。实际操作中,我们还需要确定一个K值,也就是我们要观察离电影A最近的电影有多少个。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/cc/fa0aa02dae219b21de5984371950c3cc.png" alt="">
|
||||
|
||||
## KNN的工作原理
|
||||
|
||||
“近朱者赤,近墨者黑”可以说是KNN的工作原理。整个计算过程分为三步:
|
||||
|
||||
<li>
|
||||
计算待分类物体与其他物体之间的距离;
|
||||
</li>
|
||||
<li>
|
||||
统计距离最近的K个邻居;
|
||||
</li>
|
||||
<li>
|
||||
对于K个最近的邻居,它们属于哪个分类最多,待分类物体就属于哪一类。
|
||||
</li>
|
||||
|
||||
**K值如何选择**
|
||||
|
||||
你能看出整个KNN的分类过程,K值的选择还是很重要的。那么问题来了,K值选择多少是适合的呢?
|
||||
|
||||
如果 K 值比较小,就相当于未分类物体与它的邻居非常接近才行。这样产生的一个问题就是,如果邻居点是个噪声点,那么未分类物体的分类也会产生误差,这样KNN分类就会产生过拟合。
|
||||
|
||||
如果K值比较大,相当于距离过远的点也会对未知物体的分类产生影响,虽然这种情况的好处是鲁棒性强,但是不足也很明显,会产生欠拟合情况,也就是没有把未分类物体真正分类出来。
|
||||
|
||||
所以K值应该是个实践出来的结果,并不是我们事先而定的。在工程上,我们一般采用交叉验证的方式选取 K 值。
|
||||
|
||||
交叉验证的思路就是,把样本集中的大部分样本作为训练集,剩余的小部分样本用于预测,来验证分类模型的准确性。所以在KNN算法中,我们一般会把K值选取在较小的范围内,同时在验证集上准确率最高的那一个最终确定作为K值。
|
||||
|
||||
**距离如何计算**
|
||||
|
||||
在KNN算法中,还有一个重要的计算就是关于距离的度量。两个样本点之间的距离代表了这两个样本之间的相似度。距离越大,差异性越大;距离越小,相似度越大。
|
||||
|
||||
关于距离的计算方式有下面五种方式:
|
||||
|
||||
<li>
|
||||
欧氏距离;
|
||||
</li>
|
||||
<li>
|
||||
曼哈顿距离;
|
||||
</li>
|
||||
<li>
|
||||
闵可夫斯基距离;
|
||||
</li>
|
||||
<li>
|
||||
切比雪夫距离;
|
||||
</li>
|
||||
<li>
|
||||
余弦距离。
|
||||
</li>
|
||||
|
||||
其中前三种距离是KNN中最常用的距离,我给你分别讲解下。
|
||||
|
||||
**欧氏距离**是我们最常用的距离公式,也叫做欧几里得距离。在二维空间中,两点的欧式距离就是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f8/80/f8d4fe58ec9580a4ffad5cee263b1b80.png" alt=""><br>
|
||||
同理,我们也可以求得两点在n维空间中的距离:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/6a/40efe7cb4a2571e55438b55f8d37366a.png" alt=""><br>
|
||||
**曼哈顿距离**在几何空间中用的比较多。以下图为例,绿色的直线代表两点之间的欧式距离,而红色和黄色的线为两点的曼哈顿距离。所以曼哈顿距离等于两个点在坐标系上绝对轴距总和。用公式表示就是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/aa/bda520e8ee34ea19df8dbad3da85faaa.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/43/dd19ca4f0be3f60b526e9ea0b7d13543.jpg" alt=""><br>
|
||||
**闵可夫斯基距离**不是一个距离,而是一组距离的定义。对于n维空间中的两个点 x(x1,x2,…,xn) 和 y(y1,y2,…,yn) , x 和 y 两点之间的闵可夫斯基距离为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/c5/4d614c3d6722c02e4ea03cb1e6653dc5.png" alt=""><br>
|
||||
其中p代表空间的维数,当p=1时,就是曼哈顿距离;当p=2时,就是欧氏距离;当p→∞时,就是切比雪夫距离。
|
||||
|
||||
**那么切比雪夫距离**怎么计算呢?二个点之间的切比雪夫距离就是这两个点坐标数值差的绝对值的最大值,用数学表示就是:max(|x1-y1|,|x2-y2|)。
|
||||
|
||||
**余弦距离**实际上计算的是两个向量的夹角,是在方向上计算两者之间的差异,对绝对数值不敏感。在兴趣相关性比较上,角度关系比距离的绝对值更重要,因此余弦距离可以用于衡量用户对内容兴趣的区分度。比如我们用搜索引擎搜索某个关键词,它还会给你推荐其他的相关搜索,这些推荐的关键词就是采用余弦距离计算得出的。
|
||||
|
||||
## KD树
|
||||
|
||||
其实从上文你也能看出来,KNN的计算过程是大量计算样本点之间的距离。为了减少计算距离次数,提升KNN的搜索效率,人们提出了KD树(K-Dimensional的缩写)。KD树是对数据点在K维空间中划分的一种数据结构。在KD树的构造中,每个节点都是k维数值点的二叉树。既然是二叉树,就可以采用二叉树的增删改查操作,这样就大大提升了搜索效率。
|
||||
|
||||
在这里,我们不需要对KD树的数学原理了解太多,你只需要知道它是一个二叉树的数据结构,方便存储K维空间的数据就可以了。而且在sklearn中,我们直接可以调用KD树,很方便。
|
||||
|
||||
## 用KNN做回归
|
||||
|
||||
KNN不仅可以做分类,还可以做回归。首先讲下什么是回归。在开头电影这个案例中,如果想要对未知电影进行类型划分,这是一个分类问题。首先看一下要分类的未知电影,离它最近的K部电影大多数属于哪个分类,这部电影就属于哪个分类。
|
||||
|
||||
如果是一部新电影,已知它是爱情片,想要知道它的打斗次数、接吻次数可能是多少,这就是一个回归问题。
|
||||
|
||||
那么KNN如何做回归呢?
|
||||
|
||||
对于一个新电影X,我们要预测它的某个属性值,比如打斗次数,具体特征属性和数值如下所示。此时,我们会先计算待测点(新电影X)到已知点的距离,选择距离最近的K个点。假设K=3,此时最近的3个点(电影)分别是《战狼》,《红海行动》和《碟中谍6》,那么它的打斗次数就是这3个点的该属性值的平均值,即(100+95+105)/3=100次。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/16/35dc8cc7d781c94b0fbaa0b53c01f716.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了KNN的原理,以及KNN中的几个关键因素。比如针对K值的选择,我们一般采用交叉验证的方式得出。针对样本点之间的距离的定义,常用的有5种表达方式,你也可以自己来定义两个样本之间的距离公式。不同的定义,适用的场景不同。比如在搜索关键词推荐中,余弦距离是更为常用的。
|
||||
|
||||
另外你也可以用KNN进行回归,通过K个邻居对新的点的属性进行值的预测。
|
||||
|
||||
KNN的理论简单直接,针对KNN中的搜索也有相应的KD树这个数据结构。KNN的理论成熟,可以应用到线性和非线性的分类问题中,也可以用于回归分析。
|
||||
|
||||
不过KNN需要计算测试点与样本点之间的距离,当数据量大的时候,计算量是非常庞大的,需要大量的存储空间和计算时间。另外如果样本分类不均衡,比如有些分类的样本非常少,那么该类别的分类准确率就会低很多。
|
||||
|
||||
当然在实际工作中,我们需要考虑到各种可能存在的情况,比如针对某类样本少的情况,可以增加该类别的权重。
|
||||
|
||||
同样KNN也可以用于推荐算法,虽然现在很多推荐系统的算法会使用TD-IDF、协同过滤、Apriori算法,不过针对数据量不大的情况下,采用KNN作为推荐算法也是可行的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d6/0f/d67073bef9247e1ca7a58ae7869f390f.png" alt=""><br>
|
||||
最后我给你留几道思考题吧,KNN的算法原理和工作流程是怎么样的?KNN中的K值又是如何选择的?
|
||||
|
||||
## 上一篇文章思考题的代码
|
||||
|
||||
我在上篇文章里留了一道思考题,你可以在[GitHub](http://github.com/cystanford/breast_cancer_data)上看到我写的关于这道题的代码(完整代码和文章案例代码差别不大),供你借鉴。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/44/fa09558150152cdb250e715ae9047544.png" alt="">
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
227
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/25丨KNN(下):如何对手写数字进行识别?.md
Normal file
227
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/25丨KNN(下):如何对手写数字进行识别?.md
Normal file
@@ -0,0 +1,227 @@
|
||||
<audio id="audio" title="25丨KNN(下):如何对手写数字进行识别?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bd/11/bd87b5d9179bfd3740fd19f6b7641b11.mp3"></audio>
|
||||
|
||||
今天我来带你进行KNN的实战。上节课,我讲了KNN实际上是计算待分类物体与其他物体之间的距离,然后通过统计最近的K个邻居的分类情况,来决定这个物体的分类情况。
|
||||
|
||||
这节课,我们先看下如何在sklearn中使用KNN算法,然后通过sklearn中自带的手写数字数据集来进行实战。
|
||||
|
||||
之前我还讲过SVM、朴素贝叶斯和决策树分类,我们还可以用这个数据集来做下训练,对比下这四个分类器的训练结果。
|
||||
|
||||
## 如何在sklearn中使用KNN
|
||||
|
||||
在Python的sklearn工具包中有KNN算法。KNN既可以做分类器,也可以做回归。如果是做分类,你需要引用:
|
||||
|
||||
```
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
|
||||
```
|
||||
|
||||
如果是做回归,你需要引用:
|
||||
|
||||
```
|
||||
from sklearn.neighbors import KNeighborsRegressor
|
||||
|
||||
|
||||
```
|
||||
|
||||
从名字上你也能看出来Classifier对应的是分类,Regressor对应的是回归。一般来说如果一个算法有Classifier类,都能找到相应的Regressor类。比如在决策树分类中,你可以使用DecisionTreeClassifier,也可以使用决策树来做回归DecisionTreeRegressor。
|
||||
|
||||
好了,我们看下如何在sklearn中创建KNN分类器。
|
||||
|
||||
这里,我们使用构造函数KNeighborsClassifier(n_neighbors=5, weights=‘uniform’, algorithm=‘auto’, leaf_size=30),这里有几个比较主要的参数,我分别来讲解下:
|
||||
|
||||
1.n_neighbors:即KNN中的K值,代表的是邻居的数量。K值如果比较小,会造成过拟合。如果K值比较大,无法将未知物体分类出来。一般我们使用默认值5。
|
||||
|
||||
2.weights:是用来确定邻居的权重,有三种方式:
|
||||
|
||||
<li>
|
||||
weights=uniform,代表所有邻居的权重相同;
|
||||
</li>
|
||||
<li>
|
||||
weights=distance,代表权重是距离的倒数,即与距离成反比;
|
||||
</li>
|
||||
<li>
|
||||
自定义函数,你可以自定义不同距离所对应的权重。大部分情况下不需要自己定义函数。
|
||||
</li>
|
||||
|
||||
3.algorithm:用来规定计算邻居的方法,它有四种方式:
|
||||
|
||||
<li>
|
||||
algorithm=auto,根据数据的情况自动选择适合的算法,默认情况选择auto;
|
||||
</li>
|
||||
<li>
|
||||
algorithm=kd_tree,也叫作KD树,是多维空间的数据结构,方便对关键数据进行检索,不过KD树适用于维度少的情况,一般维数不超过20,如果维数大于20之后,效率反而会下降;
|
||||
</li>
|
||||
<li>
|
||||
algorithm=ball_tree,也叫作球树,它和KD树一样都是多维空间的数据结果,不同于KD树,球树更适用于维度大的情况;
|
||||
</li>
|
||||
<li>
|
||||
algorithm=brute,也叫作暴力搜索,它和KD树不同的地方是在于采用的是线性扫描,而不是通过构造树结构进行快速检索。当训练集大的时候,效率很低。
|
||||
</li>
|
||||
|
||||
4.leaf_size:代表构造KD树或球树时的叶子数,默认是30,调整leaf_size会影响到树的构造和搜索速度。
|
||||
|
||||
创建完KNN分类器之后,我们就可以输入训练集对它进行训练,这里我们使用fit()函数,传入训练集中的样本特征矩阵和分类标识,会自动得到训练好的KNN分类器。然后可以使用predict()函数来对结果进行预测,这里传入测试集的特征矩阵,可以得到测试集的预测分类结果。
|
||||
|
||||
## 如何用KNN对手写数字进行识别分类
|
||||
|
||||
手写数字数据集是个非常有名的用于图像识别的数据集。数字识别的过程就是将这些图片与分类结果0-9一一对应起来。完整的手写数字数据集MNIST里面包括了60000个训练样本,以及10000个测试样本。如果你学习深度学习的话,MNIST基本上是你接触的第一个数据集。
|
||||
|
||||
今天我们用sklearn自带的手写数字数据集做KNN分类,你可以把这个数据集理解成一个简版的MNIST数据集,它只包括了1797幅数字图像,每幅图像大小是8*8像素。
|
||||
|
||||
好了,我们先来规划下整个KNN分类的流程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/78/8af94562f6bd3ac42036ec47f5ad2578.jpg" alt=""><br>
|
||||
整个训练过程基本上都会包括三个阶段:
|
||||
|
||||
<li>
|
||||
数据加载:我们可以直接从sklearn中加载自带的手写数字数据集;
|
||||
</li>
|
||||
<li>
|
||||
准备阶段:在这个阶段中,我们需要对数据集有个初步的了解,比如样本的个数、图像长什么样、识别结果是怎样的。你可以通过可视化的方式来查看图像的呈现。通过数据规范化可以让数据都在同一个数量级的维度。另外,因为训练集是图像,每幅图像是个8*8的矩阵,我们不需要对它进行特征选择,将全部的图像数据作为特征值矩阵即可;
|
||||
</li>
|
||||
<li>
|
||||
分类阶段:通过训练可以得到分类器,然后用测试集进行准确率的计算。
|
||||
</li>
|
||||
|
||||
好了,按照上面的步骤,我们一起来实现下这个项目。
|
||||
|
||||
首先是加载数据和对数据的探索:
|
||||
|
||||
```
|
||||
# 加载数据
|
||||
digits = load_digits()
|
||||
data = digits.data
|
||||
# 数据探索
|
||||
print(data.shape)
|
||||
# 查看第一幅图像
|
||||
print(digits.images[0])
|
||||
# 第一幅图像代表的数字含义
|
||||
print(digits.target[0])
|
||||
# 将第一幅图像显示出来
|
||||
plt.gray()
|
||||
plt.imshow(digits.images[0])
|
||||
plt.show()
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
(1797, 64)
|
||||
[[ 0. 0. 5. 13. 9. 1. 0. 0.]
|
||||
[ 0. 0. 13. 15. 10. 15. 5. 0.]
|
||||
[ 0. 3. 15. 2. 0. 11. 8. 0.]
|
||||
[ 0. 4. 12. 0. 0. 8. 8. 0.]
|
||||
[ 0. 5. 8. 0. 0. 9. 8. 0.]
|
||||
[ 0. 4. 11. 0. 1. 12. 7. 0.]
|
||||
[ 0. 2. 14. 5. 10. 12. 0. 0.]
|
||||
[ 0. 0. 6. 13. 10. 0. 0. 0.]]
|
||||
0
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/3c/625b7e95a22c025efa545d7144ec5f3c.png" alt=""><br>
|
||||
我们对原始数据集中的第一幅进行数据可视化,可以看到图像是个8*8的像素矩阵,上面这幅图像是一个“0”,从训练集的分类标注中我们也可以看到分类标注为“0”。
|
||||
|
||||
sklearn自带的手写数字数据集一共包括了1797个样本,每幅图像都是8*8像素的矩阵。因为并没有专门的测试集,所以我们需要对数据集做划分,划分成训练集和测试集。因为KNN算法和距离定义相关,我们需要对数据进行规范化处理,采用Z-Score规范化,代码如下:
|
||||
|
||||
```
|
||||
# 分割数据,将25%的数据作为测试集,其余作为训练集(你也可以指定其他比例的数据作为训练集)
|
||||
train_x, test_x, train_y, test_y = train_test_split(data, digits.target, test_size=0.25, random_state=33)
|
||||
# 采用Z-Score规范化
|
||||
ss = preprocessing.StandardScaler()
|
||||
train_ss_x = ss.fit_transform(train_x)
|
||||
test_ss_x = ss.transform(test_x)
|
||||
|
||||
```
|
||||
|
||||
然后我们构造一个KNN分类器knn,把训练集的数据传入构造好的knn,并通过测试集进行结果预测,与测试集的结果进行对比,得到KNN分类器准确率,代码如下:
|
||||
|
||||
```
|
||||
# 创建KNN分类器
|
||||
knn = KNeighborsClassifier()
|
||||
knn.fit(train_ss_x, train_y)
|
||||
predict_y = knn.predict(test_ss_x)
|
||||
print("KNN准确率: %.4lf" % accuracy_score(test_y, predict_y))
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
KNN准确率: 0.9756
|
||||
|
||||
```
|
||||
|
||||
好了,这样我们就构造好了一个KNN分类器。之前我们还讲过SVM、朴素贝叶斯和决策树分类。我们用手写数字数据集一起来训练下这些分类器,然后对比下哪个分类器的效果更好。代码如下:
|
||||
|
||||
```
|
||||
# 创建SVM分类器
|
||||
svm = SVC()
|
||||
svm.fit(train_ss_x, train_y)
|
||||
predict_y=svm.predict(test_ss_x)
|
||||
print('SVM准确率: %0.4lf' % accuracy_score(test_y, predict_y))
|
||||
# 采用Min-Max规范化
|
||||
mm = preprocessing.MinMaxScaler()
|
||||
train_mm_x = mm.fit_transform(train_x)
|
||||
test_mm_x = mm.transform(test_x)
|
||||
# 创建Naive Bayes分类器
|
||||
mnb = MultinomialNB()
|
||||
mnb.fit(train_mm_x, train_y)
|
||||
predict_y = mnb.predict(test_mm_x)
|
||||
print("多项式朴素贝叶斯准确率: %.4lf" % accuracy_score(test_y, predict_y))
|
||||
# 创建CART决策树分类器
|
||||
dtc = DecisionTreeClassifier()
|
||||
dtc.fit(train_mm_x, train_y)
|
||||
predict_y = dtc.predict(test_mm_x)
|
||||
print("CART决策树准确率: %.4lf" % accuracy_score(test_y, predict_y))
|
||||
|
||||
```
|
||||
|
||||
运行结果如下:
|
||||
|
||||
```
|
||||
SVM准确率: 0.9867
|
||||
多项式朴素贝叶斯准确率: 0.8844
|
||||
CART决策树准确率: 0.8556
|
||||
|
||||
```
|
||||
|
||||
这里需要注意的是,我们在做多项式朴素贝叶斯分类的时候,传入的数据不能有负数。因为Z-Score会将数值规范化为一个标准的正态分布,即均值为0,方差为1,数值会包含负数。因此我们需要采用Min-Max规范化,将数据规范化到[0,1]范围内。
|
||||
|
||||
好了,我们整理下这4个分类器的结果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/e8/0f498e0197935bfe15d9b1209bad8fe8.png" alt=""><br>
|
||||
你能看出来KNN的准确率还是不错的,和SVM不相上下。
|
||||
|
||||
你可以自己跑一遍整个代码,在运行前还需要import相关的工具包(下面的这些工具包你都会用到,所以都需要引用):
|
||||
|
||||
```
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn import preprocessing
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.datasets import load_digits
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
from sklearn.svm import SVC
|
||||
from sklearn.naive_bayes import MultinomialNB
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
```
|
||||
|
||||
代码中,我使用了train_test_split做数据集的拆分,使用matplotlib.pyplot工具包显示图像,使用accuracy_score进行分类器准确率的计算,使用preprocessing中的StandardScaler和MinMaxScaler做数据的规范化。
|
||||
|
||||
完整的代码你可以从[GitHub](https://github.com/cystanford/knn)上下载。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你一起做了手写数字分类识别的实战,分别用KNN、SVM、朴素贝叶斯和决策树做分类器,并统计了四个分类器的准确率。在这个过程中你应该对数据探索、数据可视化、数据规范化、模型训练和结果评估的使用过程有了一定的体会。在数据量不大的情况下,使用sklearn还是方便的。
|
||||
|
||||
如果数据量很大,比如MNIST数据集中的6万个训练数据和1万个测试数据,那么采用深度学习+GPU运算的方式会更适合。因为深度学习的特点就是需要大量并行的重复计算,GPU最擅长的就是做大量的并行计算。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/e1/d08f489c3bffaacb6910f32a0fa600e1.png" alt=""><br>
|
||||
最后留两道思考题吧,请你说说项目中KNN分类器的常用构造参数,功能函数都有哪些,以及你对KNN使用的理解?如果把KNN中的K值设置为200,数据集还是sklearn中的手写数字数据集,再跑一遍程序,看看分类器的准确率是多少?
|
||||
|
||||
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
198
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/26丨K-Means(上):如何给20支亚洲球队做聚类?.md
Normal file
198
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/26丨K-Means(上):如何给20支亚洲球队做聚类?.md
Normal file
@@ -0,0 +1,198 @@
|
||||
<audio id="audio" title="26丨K-Means(上):如何给20支亚洲球队做聚类?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/86/bc32d39df47bfa5cc044cd9c2a778886.mp3"></audio>
|
||||
|
||||
今天我来带你进行K-Means的学习。K-Means是一种非监督学习,解决的是聚类问题。K代表的是K类,Means代表的是中心,你可以理解这个算法的本质是确定K类的中心点,当你找到了这些中心点,也就完成了聚类。
|
||||
|
||||
那么请你和我思考以下三个问题:
|
||||
|
||||
<li>
|
||||
如何确定K类的中心点?
|
||||
</li>
|
||||
<li>
|
||||
如何将其他点划分到K类中?
|
||||
</li>
|
||||
<li>
|
||||
如何区分K-Means与KNN?
|
||||
</li>
|
||||
|
||||
如果理解了上面这3个问题,那么对K-Means的原理掌握得也就差不多了。
|
||||
|
||||
先请你和我思考一个场景,假设我有20支亚洲足球队,想要将它们按照成绩划分成3个等级,可以怎样划分?
|
||||
|
||||
## K-Means的工作原理
|
||||
|
||||
对亚洲足球队的水平,你可能也有自己的判断。比如一流的亚洲球队有谁?你可能会说伊朗或韩国。二流的亚洲球队呢?你可能说是中国。三流的亚洲球队呢?你可能会说越南。
|
||||
|
||||
其实这些都是靠我们的经验来划分的,那么伊朗、中国、越南可以说是三个等级的典型代表,也就是我们每个类的中心点。
|
||||
|
||||
所以回过头来,如何确定K类的中心点?一开始我们是可以随机指派的,当你确认了中心点后,就可以按照距离将其他足球队划分到不同的类别中。
|
||||
|
||||
这也就是K-Means的中心思想,就是这么简单直接。你可能会问:如果一开始,选择一流球队是中国,二流球队是伊朗,三流球队是韩国,中心点选择错了怎么办?其实不用担心,K-Means有自我纠正机制,在不断的迭代过程中,会纠正中心点。中心点在整个迭代过程中,并不是唯一的,只是你需要一个初始值,一般算法会随机设置初始的中心点。
|
||||
|
||||
好了,那我来把K-Means的工作原理给你总结下:
|
||||
|
||||
<li>
|
||||
选取K个点作为初始的类中心点,这些点一般都是从数据集中随机抽取的;
|
||||
</li>
|
||||
<li>
|
||||
将每个点分配到最近的类中心点,这样就形成了K个类,然后重新计算每个类的中心点;
|
||||
</li>
|
||||
<li>
|
||||
重复第二步,直到类不发生变化,或者你也可以设置最大迭代次数,这样即使类中心点发生变化,但是只要达到最大迭代次数就会结束。
|
||||
</li>
|
||||
|
||||
## 如何给亚洲球队做聚类
|
||||
|
||||
对于机器来说需要数据才能判断类中心点,所以我整理了2015-2019年亚洲球队的排名,如下表所示。
|
||||
|
||||
我来说明一下数据概况。
|
||||
|
||||
其中2019年国际足联的世界排名,2015年亚洲杯排名均为实际排名。2018年世界杯中,很多球队没有进入到决赛圈,所以只有进入到决赛圈的球队才有实际的排名。如果是亚洲区预选赛12强的球队,排名会设置为40。如果没有进入亚洲区预选赛12强,球队排名会设置为50。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/4a/d8ac2a98aa728d64f919bac088ed574a.png" alt=""><br>
|
||||
针对上面的排名,我们首先需要做的是数据规范化。你可以把这些值划分到[0,1]或者按照均值为0,方差为1的正态分布进行规范化。具体数据规范化的步骤可以看下13篇,也就是[数据变换](https://time.geekbang.org/column/article/77059)那一篇。
|
||||
|
||||
我先把数值都规范化到[0,1]的空间中,得到了以下的数值表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/17/a722eeab035fb13751a6dc5c0530ed17.png" alt=""><br>
|
||||
如果我们随机选取中国、日本、韩国为三个类的中心点,我们就需要看下这些球队到中心点的距离。
|
||||
|
||||
距离有多种计算的方式,有关距离的计算我在KNN算法中也讲到过:
|
||||
|
||||
<li>
|
||||
欧氏距离
|
||||
</li>
|
||||
<li>
|
||||
曼哈顿距离
|
||||
</li>
|
||||
<li>
|
||||
切比雪夫距离
|
||||
</li>
|
||||
<li>
|
||||
余弦距离
|
||||
</li>
|
||||
|
||||
欧氏距离是最常用的距离计算方式,这里我选择欧氏距离作为距离的标准,计算每个队伍分别到中国、日本、韩国的距离,然后根据距离远近来划分。我们看到大部分的队,会和中国队聚类到一起。这里我整理了距离的计算过程,比如中国和中国的欧氏距离为0,中国和日本的欧式距离为0.732003。如果按照中国、日本、韩国为3个分类的中心点,欧氏距离的计算结果如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/e9/b603ccdb93420c8455aea7278efaece9.png" alt=""><br>
|
||||
然后我们再重新计算这三个类的中心点,如何计算呢?最简单的方式就是取平均值,然后根据新的中心点按照距离远近重新分配球队的分类,再根据球队的分类更新中心点的位置。计算过程这里不展开,最后一直迭代(重复上述的计算过程:计算中心点和划分分类)到分类不再发生变化,可以得到以下的分类结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/98/12c6039884ee99742fbbebf198425998.png" alt=""><br>
|
||||
所以我们能看出来第一梯队有日本、韩国、伊朗、沙特、澳洲;第二梯队有中国、伊拉克、阿联酋、乌兹别克斯坦;第三梯队有卡塔尔、泰国、越南、阿曼、巴林、朝鲜、印尼、叙利亚、约旦、科威特和巴勒斯坦。
|
||||
|
||||
## 如何使用sklearn中的K-Means算法
|
||||
|
||||
sklearn是Python的机器学习工具库,如果从功能上来划分,sklearn可以实现分类、聚类、回归、降维、模型选择和预处理等功能。这里我们使用的是sklearn的聚类函数库,因此需要引用工具包,具体代码如下:
|
||||
|
||||
```
|
||||
from sklearn.cluster import KMeans
|
||||
|
||||
```
|
||||
|
||||
当然K-Means只是sklearn.cluster中的一个聚类库,实际上包括K-Means在内,sklearn.cluster一共提供了9种聚类方法,比如Mean-shift,DBSCAN,Spectral clustering(谱聚类)等。这些聚类方法的原理和K-Means不同,这里不做介绍。
|
||||
|
||||
我们看下K-Means如何创建:
|
||||
|
||||
```
|
||||
KMeans(n_clusters=8, init='k-means++', n_init=10, max_iter=300, tol=0.0001, precompute_distances='auto', verbose=0, random_state=None, copy_x=True, n_jobs=1, algorithm='auto')
|
||||
|
||||
```
|
||||
|
||||
我们能看到在K-Means类创建的过程中,有一些主要的参数:
|
||||
|
||||
<li>
|
||||
**n_clusters**: 即K值,一般需要多试一些K值来保证更好的聚类效果。你可以随机设置一些K值,然后选择聚类效果最好的作为最终的K值;
|
||||
</li>
|
||||
<li>
|
||||
**max_iter**: 最大迭代次数,如果聚类很难收敛的话,设置最大迭代次数可以让我们及时得到反馈结果,否则程序运行时间会非常长;
|
||||
</li>
|
||||
<li>
|
||||
**n_init**:初始化中心点的运算次数,默认是10。程序是否能快速收敛和中心点的选择关系非常大,所以在中心点选择上多花一些时间,来争取整体时间上的快速收敛还是非常值得的。由于每一次中心点都是随机生成的,这样得到的结果就有好有坏,非常不确定,所以要运行n_init次, 取其中最好的作为初始的中心点。如果K值比较大的时候,你可以适当增大n_init这个值;
|
||||
</li>
|
||||
<li>
|
||||
**init:** 即初始值选择的方式,默认是采用优化过的k-means++方式,你也可以自己指定中心点,或者采用random完全随机的方式。自己设置中心点一般是对于个性化的数据进行设置,很少采用。random的方式则是完全随机的方式,一般推荐采用优化过的k-means++方式;
|
||||
</li>
|
||||
<li>
|
||||
**algorithm**:k-means的实现算法,有“auto” “full”“elkan”三种。一般来说建议直接用默认的"auto"。简单说下这三个取值的区别,如果你选择"full"采用的是传统的K-Means算法,“auto”会根据数据的特点自动选择是选择“full”还是“elkan”。我们一般选择默认的取值,即“auto” 。
|
||||
</li>
|
||||
|
||||
在创建好K-Means类之后,就可以使用它的方法,最常用的是fit和predict这个两个函数。你可以单独使用fit函数和predict函数,也可以合并使用fit_predict函数。其中fit(data)可以对data数据进行k-Means聚类。 predict(data)可以针对data中的每个样本,计算最近的类。
|
||||
|
||||
现在我们要完整地跑一遍20支亚洲球队的聚类问题。我把数据上传到了[GitHub](https://github.com/cystanford/kmeans)上,你可以自行下载。
|
||||
|
||||
```
|
||||
# coding: utf-8
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn import preprocessing
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
# 输入数据
|
||||
data = pd.read_csv('data.csv', encoding='gbk')
|
||||
train_x = data[["2019年国际排名","2018世界杯","2015亚洲杯"]]
|
||||
df = pd.DataFrame(train_x)
|
||||
kmeans = KMeans(n_clusters=3)
|
||||
# 规范化到[0,1]空间
|
||||
min_max_scaler=preprocessing.MinMaxScaler()
|
||||
train_x=min_max_scaler.fit_transform(train_x)
|
||||
# kmeans算法
|
||||
kmeans.fit(train_x)
|
||||
predict_y = kmeans.predict(train_x)
|
||||
# 合并聚类结果,插入到原数据中
|
||||
result = pd.concat((data,pd.DataFrame(predict_y)),axis=1)
|
||||
result.rename({0:u'聚类'},axis=1,inplace=True)
|
||||
print(result)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
国家 2019年国际排名 2018世界杯 2015亚洲杯 聚类
|
||||
0 中国 73 40 7 2
|
||||
1 日本 60 15 5 0
|
||||
2 韩国 61 19 2 0
|
||||
3 伊朗 34 18 6 0
|
||||
4 沙特 67 26 10 0
|
||||
5 伊拉克 91 40 4 2
|
||||
6 卡塔尔 101 40 13 1
|
||||
7 阿联酋 81 40 6 2
|
||||
8 乌兹别克斯坦 88 40 8 2
|
||||
9 泰国 122 40 17 1
|
||||
10 越南 102 50 17 1
|
||||
11 阿曼 87 50 12 1
|
||||
12 巴林 116 50 11 1
|
||||
13 朝鲜 110 50 14 1
|
||||
14 印尼 164 50 17 1
|
||||
15 澳洲 40 30 1 0
|
||||
16 叙利亚 76 40 17 1
|
||||
17 约旦 118 50 9 1
|
||||
18 科威特 160 50 15 1
|
||||
19 巴勒斯坦 96 50 16 1
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了K-Means算法原理,我们再来看下开篇我给你提的三个问题。
|
||||
|
||||
如何确定K类的中心点?其中包括了初始的设置,以及中间迭代过程中中心点的计算。在初始设置中,会进行n_init次的选择,然后选择初始中心点效果最好的为初始值。在每次分类更新后,你都需要重新确认每一类的中心点,一般采用均值的方式进行确认。
|
||||
|
||||
如何将其他点划分到K类中?这里实际上是关于距离的定义,我们知道距离有多种定义的方式,在K-Means和KNN中,我们都可以采用欧氏距离、曼哈顿距离、切比雪夫距离、余弦距离等。对于点的划分,就看它离哪个类的中心点的距离最近,就属于哪一类。
|
||||
|
||||
如何区分K-Means和KNN这两种算法呢?刚学过K-Means和KNN算法的同学应该能知道两者的区别,但往往过了一段时间,就容易混淆。所以我们可以从三个维度来区分K-Means和KNN这两个算法:
|
||||
|
||||
<li>
|
||||
首先,这两个算法解决数据挖掘的两类问题。K-Means是聚类算法,KNN是分类算法。
|
||||
</li>
|
||||
<li>
|
||||
这两个算法分别是两种不同的学习方式。K-Means是非监督学习,也就是不需要事先给出分类标签,而KNN是有监督学习,需要我们给出训练数据的分类标识。
|
||||
</li>
|
||||
<li>
|
||||
最后,K值的含义不同。K-Means中的K值代表K类。KNN中的K值代表K个最接近的邻居。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/eb/c5/eb60546c6a3d9bc6a1538049c26723c5.png" alt=""><br>
|
||||
那么学完了今天的内容后,你能说一下K-Means的算法原理吗?如果我们把上面的20支亚洲球队用K-Means划分成5类,在规范化数据的时候采用标准化的方式(即均值为0,方差为1),该如何编写程序呢?运行的结果又是如何?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<audio id="audio" title="27丨K-Means(下):如何使用K-Means对图像进行分割?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/32/68974aaf0975c8e01c9dab7897c2a732.mp3"></audio>
|
||||
|
||||
上节课,我讲解了K-Means的原理,并且用K-Means对20支亚洲球队进行了聚类,分成3个梯队。今天我们继续用K-Means进行聚类的实战。聚类的一个常用场景就是对图像进行分割。
|
||||
|
||||
图像分割就是利用图像自身的信息,比如颜色、纹理、形状等特征进行划分,将图像分割成不同的区域,划分出来的每个区域就相当于是对图像中的像素进行了聚类。单个区域内的像素之间的相似度大,不同区域间的像素差异性大。这个特性正好符合聚类的特性,所以你可以把图像分割看成是将图像中的信息进行聚类。当然聚类只是分割图像的一种方式,除了聚类,我们还可以基于图像颜色的阈值进行分割,或者基于图像边缘的信息进行分割等。
|
||||
|
||||
## 将微信开屏封面进行分割
|
||||
|
||||
上节课,我讲了sklearn工具包中的K-Means算法使用,我们现在用K-Means算法对微信页面进行分割。微信开屏图如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/a2/50457e4e1fbd288c125364a6904774a2.png" alt=""><br>
|
||||
我们先设定下聚类的流程,聚类的流程和分类差不多,如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/78/8af94562f6bd3ac42036ec47f5ad2578.jpg" alt=""><br>
|
||||
在准备阶段里,我们需要对数据进行加载。因为处理的是图像信息,我们除了要获取图像数据以外,还需要获取图像的尺寸和通道数,然后基于图像中每个通道的数值进行数据规范化。这里我们需要定义个函数load_data,来帮我们进行图像加载和数据规范化。代码如下:
|
||||
|
||||
```
|
||||
# 加载图像,并对数据进行规范化
|
||||
def load_data(filePath):
|
||||
# 读文件
|
||||
f = open(filePath,'rb')
|
||||
data = []
|
||||
# 得到图像的像素值
|
||||
img = image.open(f)
|
||||
# 得到图像尺寸
|
||||
width, height = img.size
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
# 得到点(x,y)的三个通道值
|
||||
c1, c2, c3 = img.getpixel((x, y))
|
||||
data.append([c1, c2, c3])
|
||||
f.close()
|
||||
# 采用Min-Max规范化
|
||||
mm = preprocessing.MinMaxScaler()
|
||||
data = mm.fit_transform(data)
|
||||
return np.mat(data), width, height
|
||||
|
||||
```
|
||||
|
||||
因为jpg格式的图像是三个通道(R,G,B),也就是一个像素点具有3个特征值。这里我们用c1、c2、c3来获取平面坐标点(x,y)的三个特征值,特征值是在0-255之间。
|
||||
|
||||
为了加快聚类的收敛,我们需要采用Min-Max规范化对数据进行规范化。我们定义的load_data函数返回的结果包括了针对(R,G,B)三个通道规范化的数据,以及图像的尺寸信息。在定义好load_data函数后,我们直接调用就可以得到相关信息,代码如下:
|
||||
|
||||
```
|
||||
# 加载图像,得到规范化的结果img,以及图像尺寸
|
||||
img, width, height = load_data('./weixin.jpg')
|
||||
|
||||
```
|
||||
|
||||
假设我们想要对图像分割成2部分,在聚类阶段,我们可以将聚类数设置为2,这样图像就自动聚成2类。代码如下:
|
||||
|
||||
```
|
||||
# 用K-Means对图像进行2聚类
|
||||
kmeans =KMeans(n_clusters=2)
|
||||
kmeans.fit(img)
|
||||
label = kmeans.predict(img)
|
||||
# 将图像聚类结果,转化成图像尺寸的矩阵
|
||||
label = label.reshape([width, height])
|
||||
# 创建个新图像pic_mark,用来保存图像聚类的结果,并设置不同的灰度值
|
||||
pic_mark = image.new("L", (width, height))
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
# 根据类别设置图像灰度, 类别0 灰度值为255, 类别1 灰度值为127
|
||||
pic_mark.putpixel((x, y), int(256/(label[x][y]+1))-1)
|
||||
pic_mark.save("weixin_mark.jpg", "JPEG")
|
||||
|
||||
```
|
||||
|
||||
代码中有一些参数,我来给你讲解一下这些参数的作用和设置方法。
|
||||
|
||||
我们使用了fit和predict这两个函数来做数据的训练拟合和预测,因为传入的参数是一样的,我们可以同时进行fit和predict操作,这样我们可以直接使用fit_predict(data)得到聚类的结果。得到聚类的结果label后,实际上是一个一维的向量,我们需要把它转化成图像尺寸的矩阵。label的聚类结果是从0开始统计的,当聚类数为2的时候,聚类的标识label=0或者1。
|
||||
|
||||
如果你想对图像聚类的结果进行可视化,直接看0和1是看不出来的,还需要将0和1转化为灰度值。灰度值一般是在0-255的范围内,我们可以将label=0设定为灰度值255,label=1设定为灰度值127。具体方法是用int(256/(label[x][y]+1))-1。可视化的时候,主要是通过设置图像的灰度值进行显示。所以我们把聚类label=0的像素点都统一设置灰度值为255,把聚类label=1的像素点都统一设置灰度值为127。原来图像的灰度值是在0-255之间,现在就只有2种颜色(也就是灰度为255,和灰度127)。
|
||||
|
||||
有了这些灰度信息,我们就可以用image.new创建一个新的图像,用putpixel函数对新图像的点进行灰度值的设置,最后用save函数保存聚类的灰度图像。这样你就可以看到聚类的可视化结果了,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/6b/9420b9bedf2e3514b0624543a69fb06b.png" alt=""><br>
|
||||
上面是分割成2个部分的分割可视化,完整代码见[这里](https://github.com/cystanford/kmeans/blob/master/kmeans1.py)。
|
||||
|
||||
[https://github.com/cystanford/kmeans/blob/master/kmeans1.py](https://github.com/cystanford/kmeans/blob/master/kmeans1.py)
|
||||
|
||||
如果我们想要分割成16个部分,该如何对不同分类设置不同的颜色值呢?这里需要用到skimage工具包,它是图像处理工具包。你需要使用pip install scikit-image来进行安装。
|
||||
|
||||
这段代码可以将聚类标识矩阵转化为不同颜色的矩阵:
|
||||
|
||||
```
|
||||
from skimage import color
|
||||
# 将聚类标识矩阵转化为不同颜色的矩阵
|
||||
label_color = (color.label2rgb(label)*255).astype(np.uint8)
|
||||
label_color = label_color.transpose(1,0,2)
|
||||
images = image.fromarray(label_color)
|
||||
images.save('weixin_mark_color.jpg')
|
||||
|
||||
```
|
||||
|
||||
代码中,我使用skimage中的label2rgb函数来将label分类标识转化为颜色数值,因为我们的颜色值范围是[0,255],所以还需要乘以255进行转化,最后再转化为np.uint8类型。unit8类型代表无符号整数,范围是0-255之间。
|
||||
|
||||
得到颜色矩阵后,你可以把它输出出来,这时你发现输出的图像是颠倒的,原因可能是图像源拍摄的时候本身是倒置的。我们需要设置三维矩阵的转置,让第一维和第二维颠倒过来,也就是使用transpose(1,0,2),将原来的(0,1,2)顺序转化为(1,0,2)顺序,即第一维和第二维互换。
|
||||
|
||||
最后我们使用fromarray函数,它可以通过矩阵来生成图片,并使用save进行保存。
|
||||
|
||||
最后得到的分类标识颜色化图像是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/b7/d26df5f1ed26cca53118a99aa04484b7.png" alt=""><br>
|
||||
完整的代码见[这里](https://github.com/cystanford/kmeans/blob/master/kmeans2.py)。
|
||||
|
||||
[https://github.com/cystanford/kmeans/blob/master/kmeans2.py](https://github.com/cystanford/kmeans/blob/master/kmeans2.py)
|
||||
|
||||
刚才我们做的是聚类的可视化。如果我们想要看到对应的原图,可以将每个簇(即每个类别)的点的RGB值设置为该簇质心点的RGB值,也就是簇内的点的特征均为质心点的特征。
|
||||
|
||||
我给出了完整的代码,代码中,我可以把范围为0-255的数值投射到1-256数值之间,方法是对每个数值进行加1,你可以自己来运行下:
|
||||
|
||||
```
|
||||
# -*- coding: utf-8 -*-
|
||||
# 使用K-means对图像进行聚类,并显示聚类压缩后的图像
|
||||
import numpy as np
|
||||
import PIL.Image as image
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn import preprocessing
|
||||
import matplotlib.image as mpimg
|
||||
# 加载图像,并对数据进行规范化
|
||||
def load_data(filePath):
|
||||
# 读文件
|
||||
f = open(filePath,'rb')
|
||||
data = []
|
||||
# 得到图像的像素值
|
||||
img = image.open(f)
|
||||
# 得到图像尺寸
|
||||
width, height = img.size
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
# 得到点(x,y)的三个通道值
|
||||
c1, c2, c3 = img.getpixel((x, y))
|
||||
data.append([(c1+1)/256.0, (c2+1)/256.0, (c3+1)/256.0])
|
||||
f.close()
|
||||
return np.mat(data), width, height
|
||||
# 加载图像,得到规范化的结果imgData,以及图像尺寸
|
||||
img, width, height = load_data('./weixin.jpg')
|
||||
# 用K-Means对图像进行16聚类
|
||||
kmeans =KMeans(n_clusters=16)
|
||||
label = kmeans.fit_predict(img)
|
||||
# 将图像聚类结果,转化成图像尺寸的矩阵
|
||||
label = label.reshape([width, height])
|
||||
# 创建个新图像img,用来保存图像聚类压缩后的结果
|
||||
img=image.new('RGB', (width, height))
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
c1 = kmeans.cluster_centers_[label[x, y], 0]
|
||||
c2 = kmeans.cluster_centers_[label[x, y], 1]
|
||||
c3 = kmeans.cluster_centers_[label[x, y], 2]
|
||||
img.putpixel((x, y), (int(c1*256)-1, int(c2*256)-1, int(c3*256)-1))
|
||||
img.save('weixin_new.jpg')
|
||||
|
||||
```
|
||||
|
||||
完整代码见[这里](https://github.com/cystanford/kmeans/blob/master/kmeans3.py)。
|
||||
|
||||
[https://github.com/cystanford/kmeans/blob/master/kmeans3.py](https://github.com/cystanford/kmeans/blob/master/kmeans3.py)
|
||||
|
||||
你可以看到我没有用到sklearn自带的MinMaxScaler,而是自己写了Min-Max规范化的公式。这样做的原因是我们知道RGB每个通道的数值在[0,255]之间,所以我们可以用每个通道的数值+1/256,这样数值就会在[0,1]之间。
|
||||
|
||||
对图像做了Min-Max空间变换之后,还可以对其进行反变换,还原出对应原图的通道值。
|
||||
|
||||
对于点(x,y),我们找到它们所属的簇label[x,y],然后得到这个簇的质心特征,用c1,c2,c3表示:
|
||||
|
||||
```
|
||||
c1 = kmeans.cluster_centers_[label[x, y], 0]
|
||||
c2 = kmeans.cluster_centers_[label[x, y], 1]
|
||||
c3 = kmeans.cluster_centers_[label[x, y], 2]
|
||||
|
||||
```
|
||||
|
||||
因为c1, c2, c3对应的是数据规范化的数值,因此我们还需要进行反变换,即:
|
||||
|
||||
```
|
||||
c1=int(c1*256)-1
|
||||
c2=int(c2*256)-1
|
||||
c3=int(c3*256)-1
|
||||
|
||||
```
|
||||
|
||||
然后用img.putpixel设置点(x,y)反变换后得到的特征值。最后用img.save保存图像。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们用K-Means做了图像的分割,其实不难发现K-Means聚类有个缺陷:聚类个数K值需要事先指定。如果你不知道该聚成几类,那么最好会给K值多设置几个,然后选择聚类结果最好的那个值。
|
||||
|
||||
通过今天的图像分割,你发现用K-Means计算的过程在sklearn中就是几行代码,大部分的工作还是在预处理和后处理上。预处理是将图像进行加载,数据规范化。后处理是对聚类后的结果进行反变换。
|
||||
|
||||
如果涉及到后处理,你可以自己来设定数据规范化的函数,这样反变换的函数比较容易编写。
|
||||
|
||||
另外我们还学习了如何在Python中如何对图像进行读写,具体的代码如下,上文中也有相应代码,你也可以自己对应下:
|
||||
|
||||
```
|
||||
import PIL.Image as image
|
||||
# 得到图像的像素值
|
||||
img = image.open(f)
|
||||
# 得到图像尺寸
|
||||
width, height = img.size
|
||||
|
||||
```
|
||||
|
||||
这里会使用PIL这个工具包,它的英文全称叫Python Imaging Library,顾名思义,它是Python图像处理标准库。同时我们也使用到了skimage工具包(scikit-image),它也是图像处理工具包。用过Matlab的同学知道,Matlab处理起图像来非常方便。skimage可以和它相媲美,集成了很多图像处理函数,其中对不同分类标识显示不同的颜色。在Python中图像处理工具包,我们用的是skimage工具包。
|
||||
|
||||
这节课没有太多的理论概念,主要讲了K-Means聚类工具,数据规范化工具,以及图像处理工具的使用,并在图像分割中进行运用。其中涉及到的工具包比较多,你需要在练习的时候多加体会。当然不同尺寸的图像,K-Means运行的时间也是不同的。如果图像尺寸比较大,你可以事先进行压缩,长宽在200像素内运行速度会比较快,如果超过了1000像素,速度会很慢。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/99/5a3f0dfaf5e6aaca1e96f488f8a10999.png" alt=""><br>
|
||||
今天我讲了如何使用K-Means聚类做图像分割,谈谈你使用的体会吧。另外我在[GitHub](https://github.com/cystanford/kmeans/blob/master/baby.jpg)上上传了一张baby.jpg的图片,请你编写代码用K-Means聚类方法将它分割成16个部分。
|
||||
|
||||
链接:[https://github.com/cystanford/kmeans/blob/master/baby.jpg](https://github.com/cystanford/kmeans/blob/master/baby.jpg)
|
||||
|
||||
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
97
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/28丨EM聚类(上):如何将一份菜等分给两个人?.md
Normal file
97
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/28丨EM聚类(上):如何将一份菜等分给两个人?.md
Normal file
@@ -0,0 +1,97 @@
|
||||
<audio id="audio" title="28丨EM聚类(上):如何将一份菜等分给两个人?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/e6/6d752319263686e1e5602069b257e0e6.mp3"></audio>
|
||||
|
||||
今天我来带你学习EM聚类。EM的英文是Expectation Maximization,所以EM算法也叫最大期望算法。
|
||||
|
||||
我们先看一个简单的场景:假设你炒了一份菜,想要把它平均分到两个碟子里,该怎么分?
|
||||
|
||||
很少有人用称对菜进行称重,再计算一半的分量进行平分。大部分人的方法是先分一部分到碟子A中,然后再把剩余的分到碟子B中,再来观察碟子A和B里的菜是否一样多,哪个多就匀一些到少的那个碟子里,然后再观察碟子A和B里的是否一样多……整个过程一直重复下去,直到份量不发生变化为止。
|
||||
|
||||
你能从这个例子中看到三个主要的步骤:初始化参数、观察预期、重新估计。首先是先给每个碟子初始化一些菜量,然后再观察预期,这两个步骤实际上就是期望步骤(Expectation)。如果结果存在偏差就需要重新估计参数,这个就是最大化步骤(Maximization)。这两个步骤加起来也就是EM算法的过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/3c/91f617ac484a7de011108ae99bd8cb3c.jpg" alt="">
|
||||
|
||||
## EM算法的工作原理
|
||||
|
||||
说到EM算法,我们先来看一个概念“最大似然”,英文是Maximum Likelihood,Likelihood代表可能性,所以最大似然也就是最大可能性的意思。
|
||||
|
||||
什么是最大似然呢?举个例子,有一男一女两个同学,现在要对他俩进行身高的比较,谁会更高呢?根据我们的经验,相同年龄下男性的平均身高比女性的高一些,所以男同学高的可能性会很大。这里运用的就是最大似然的概念。
|
||||
|
||||
最大似然估计是什么呢?它指的就是一件事情已经发生了,然后反推更有可能是什么因素造成的。还是用一男一女比较身高为例,假设有一个人比另一个人高,反推他可能是男性。最大似然估计是一种通过已知结果,估计参数的方法。
|
||||
|
||||
那么EM算法是什么?它和最大似然估计又有什么关系呢?EM算法是一种求解最大似然估计的方法,通过观测样本,来找出样本的模型参数。
|
||||
|
||||
再回过来看下开头我给你举的分菜的这个例子,实际上最终我们想要的是碟子A和碟子B中菜的份量,你可以把它们理解为想要求得的**模型参数**。然后我们通过EM算法中的E步来进行观察,然后通过M步来进行调整A和B的参数,最后让碟子A和碟子B的参数不再发生变化为止。
|
||||
|
||||
实际我们遇到的问题,比分菜复杂。我再给你举个一个投掷硬币的例子,假设我们有A和B两枚硬币,我们做了5组实验,每组实验投掷10次,然后统计出现正面的次数,实验结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/e4/c8b3f2489735a21ad86d05fb9e8c0de4.png" alt=""><br>
|
||||
投掷硬币这个过程中存在隐含的数据,即我们事先并不知道每次投掷的硬币是A还是B。假设我们知道这个隐含的数据,并将它完善,可以得到下面的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/0d/91eace1de7799a2d2d392908b462730d.png" alt=""><br>
|
||||
我们现在想要求得硬币A和B出现正面次数的概率,可以直接求得:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/51/d8/51ba3cc97ed9b786f4d95f937b207bd8.png" alt="">
|
||||
|
||||
而实际情况是我不知道每次投掷的硬币是A还是B,那么如何求得硬币A和硬币B出现正面的概率呢?
|
||||
|
||||
这里就需要采用EM算法的思想。
|
||||
|
||||
1.初始化参数。我们假设硬币A和B的正面概率(随机指定)是θA=0.5和θB=0.9。
|
||||
|
||||
2.计算期望值。假设实验1投掷的是硬币A,那么正面次数为5的概率为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/e0/09babe7d1f543d6ff800005d556823e0.png" alt=""><br>
|
||||
公式中的C(10,5)代表的是10个里面取5个的组合方式,也就是排列组合公式,0.5的5次方乘以0.5的5次方代表的是其中一次为5次为正面,5次为反面的概率,然后再乘以C(10,5)等于正面次数为5的概率。
|
||||
|
||||
假设实验1是投掷的硬币B ,那么正面次数为5的概率为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/f6/7b1bab8bf4eecb0c55b34fb8049374f6.png" alt=""><br>
|
||||
所以实验1更有可能投掷的是硬币A。
|
||||
|
||||
然后我们对实验2~5重复上面的计算过程,可以推理出来硬币顺序应该是{A,A,B,B,A}。
|
||||
|
||||
这个过程实际上是通过假设的参数来估计未知参数,即“每次投掷是哪枚硬币”。
|
||||
|
||||
3.通过猜测的结果{A, A, B, B, A}来完善初始化的参数θA和θB。
|
||||
|
||||
然后一直重复第二步和第三步,直到参数不再发生变化。
|
||||
|
||||
简单总结下上面的步骤,你能看出EM算法中的E步骤就是通过旧的参数来计算隐藏变量。然后在M步骤中,通过得到的隐藏变量的结果来重新估计参数。直到参数不再发生变化,得到我们想要的结果。
|
||||
|
||||
## EM聚类的工作原理
|
||||
|
||||
上面你能看到EM算法最直接的应用就是求参数估计。如果我们把潜在类别当做隐藏变量,样本看做观察值,就可以把聚类问题转化为参数估计问题。这也就是EM聚类的原理。
|
||||
|
||||
相比于K-Means算法,EM聚类更加灵活,比如下面这两种情况,K-Means会得到下面的聚类结果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/ca/bafc98deb68400100fde69a41ebc66ca.jpg" alt=""><br>
|
||||
因为K-Means是通过距离来区分样本之间的差别的,且每个样本在计算的时候只能属于一个分类,称之为是硬聚类算法。而EM聚类在求解的过程中,实际上每个样本都有一定的概率和每个聚类相关,叫做软聚类算法。
|
||||
|
||||
你可以把EM算法理解成为是一个框架,在这个框架中可以采用不同的模型来用EM进行求解。常用的EM聚类有GMM高斯混合模型和HMM隐马尔科夫模型。GMM(高斯混合模型)聚类就是EM聚类的一种。比如上面这两个图,可以采用GMM来进行聚类。
|
||||
|
||||
和K-Means一样,我们事先知道聚类的个数,但是不知道每个样本分别属于哪一类。通常,我们可以假设样本是符合高斯分布的(也就是正态分布)。每个高斯分布都属于这个模型的组成部分(component),要分成K类就相当于是K个组成部分。这样我们可以先初始化每个组成部分的高斯分布的参数,然后再看来每个样本是属于哪个组成部分。这也就是E步骤。
|
||||
|
||||
再通过得到的这些隐含变量结果,反过来求每个组成部分高斯分布的参数,即M步骤。反复EM步骤,直到每个组成部分的高斯分布参数不变为止。
|
||||
|
||||
这样也就相当于将样本按照GMM模型进行了EM聚类。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/3b/18fe6407b90130e5e4fa74467b1d493b.jpg" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
EM算法相当于一个框架,你可以采用不同的模型来进行聚类,比如GMM(高斯混合模型),或者HMM(隐马尔科夫模型)来进行聚类。GMM是通过概率密度来进行聚类,聚成的类符合高斯分布(正态分布)。而HMM用到了马尔可夫过程,在这个过程中,我们通过状态转移矩阵来计算状态转移的概率。HMM在自然语言处理和语音识别领域中有广泛的应用。
|
||||
|
||||
在EM这个框架中,E步骤相当于是通过初始化的参数来估计隐含变量。M步骤就是通过隐含变量反推来优化参数。最后通过EM步骤的迭代得到模型参数。
|
||||
|
||||
在这个过程里用到的一些数学公式这节课不进行展开。你需要重点理解EM算法的原理。通过上面举的炒菜的例子,你可以知道EM算法是一个不断观察和调整的过程。
|
||||
|
||||
通过求硬币正面概率的例子,你可以理解如何通过初始化参数来求隐含数据的过程,以及再通过求得的隐含数据来优化参数。
|
||||
|
||||
通过上面GMM图像聚类的例子,你可以知道很多K-Means解决不了的问题,EM聚类是可以解决的。在EM框架中,我们将潜在类别当做隐藏变量,样本看做观察值,把聚类问题转化为参数估计问题,最终把样本进行聚类。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/80/d839e80d911add15add41163fa03ee80.png" alt=""><br>
|
||||
最后给你留两道思考题吧,你能用自己的话说一下EM算法的原理吗?EM聚类和K-Means聚类的相同和不同之处又有哪些?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
|
||||
|
||||
190
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/29丨EM聚类(下):用EM算法对王者荣耀英雄进行划分.md
Normal file
190
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/29丨EM聚类(下):用EM算法对王者荣耀英雄进行划分.md
Normal file
@@ -0,0 +1,190 @@
|
||||
<audio id="audio" title="29丨EM聚类(下):用EM算法对王者荣耀英雄进行划分" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/d6/6879af42f10088cac0d936d2dec92dd6.mp3"></audio>
|
||||
|
||||
今天我来带你进行EM的实战。上节课,我讲了EM算法的原理,EM算法相当于一个聚类框架,里面有不同的聚类模型,比如GMM高斯混合模型,或者HMM隐马尔科夫模型。其中你需要理解的是EM的两个步骤,E步和M步:E步相当于通过初始化的参数来估计隐含变量,M步是通过隐含变量来反推优化参数。最后通过EM步骤的迭代得到最终的模型参数。
|
||||
|
||||
今天我们进行EM算法的实战,你需要思考的是:
|
||||
|
||||
<li>
|
||||
如何使用EM算法工具完成聚类?
|
||||
</li>
|
||||
<li>
|
||||
什么情况下使用聚类算法?我们用聚类算法的任务目标是什么?
|
||||
</li>
|
||||
<li>
|
||||
面对王者荣耀的英雄数据,EM算法能帮助我们分析出什么?
|
||||
</li>
|
||||
|
||||
## 如何使用EM工具包
|
||||
|
||||
在Python中有第三方的EM算法工具包。由于EM算法是一个聚类框架,所以你需要明确你要用的具体算法,比如是采用GMM高斯混合模型,还是HMM隐马尔科夫模型。
|
||||
|
||||
这节课我们主要讲解GMM的使用,在使用前你需要引入工具包:
|
||||
|
||||
```
|
||||
|
||||
from sklearn.mixture import GaussianMixture
|
||||
|
||||
|
||||
```
|
||||
|
||||
我们看下如何在sklearn中创建GMM聚类。
|
||||
|
||||
首先我们使用gmm = GaussianMixture(n_components=1, covariance_type=‘full’, max_iter=100)来创建GMM聚类,其中有几个比较主要的参数(GMM类的构造参数比较多,我筛选了一些主要的进行讲解),我分别来讲解下:
|
||||
|
||||
1.n_components:即高斯混合模型的个数,也就是我们要聚类的个数,默认值为1。如果你不指定n_components,最终的聚类结果都会为同一个值。
|
||||
|
||||
2.covariance_type:代表协方差类型。一个高斯混合模型的分布是由均值向量和协方差矩阵决定的,所以协方差的类型也代表了不同的高斯混合模型的特征。协方差类型有4种取值:
|
||||
|
||||
<li>
|
||||
covariance_type=full,代表完全协方差,也就是元素都不为0;
|
||||
</li>
|
||||
<li>
|
||||
covariance_type=tied,代表相同的完全协方差;
|
||||
</li>
|
||||
<li>
|
||||
covariance_type=diag,代表对角协方差,也就是对角不为0,其余为0;
|
||||
</li>
|
||||
<li>
|
||||
covariance_type=spherical,代表球面协方差,非对角为0,对角完全相同,呈现球面的特性。
|
||||
</li>
|
||||
|
||||
3.max_iter:代表最大迭代次数,EM算法是由E步和M步迭代求得最终的模型参数,这里可以指定最大迭代次数,默认值为100。
|
||||
|
||||
创建完GMM聚类器之后,我们就可以传入数据让它进行迭代拟合。
|
||||
|
||||
我们使用fit函数,传入样本特征矩阵,模型会自动生成聚类器,然后使用prediction=gmm.predict(data)来对数据进行聚类,传入你想进行聚类的数据,可以得到聚类结果prediction。
|
||||
|
||||
你能看出来拟合训练和预测可以传入相同的特征矩阵,这是因为聚类是无监督学习,你不需要事先指定聚类的结果,也无法基于先验的结果经验来进行学习。只要在训练过程中传入特征值矩阵,机器就会按照特征值矩阵生成聚类器,然后就可以使用这个聚类器进行聚类了。
|
||||
|
||||
## 如何用EM算法对王者荣耀数据进行聚类
|
||||
|
||||
了解了GMM聚类工具之后,我们看下如何对王者荣耀的英雄数据进行聚类。
|
||||
|
||||
首先我们知道聚类的原理是“人以群分,物以类聚”。通过聚类算法把特征值相近的数据归为一类,不同类之间的差异较大,这样就可以对原始数据进行降维。通过分成几个组(簇),来研究每个组之间的特性。或者我们也可以把组(簇)的数量适当提升,这样就可以找到可以互相替换的英雄,比如你的对手选择了你擅长的英雄之后,你可以选择另一个英雄作为备选。
|
||||
|
||||
我们先看下数据长什么样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/a0/3c4e14e7b33fc211f96fe0108f6196a0.png" alt=""><br>
|
||||
这里我们收集了69名英雄的20个特征属性,这些属性分别是最大生命、生命成长、初始生命、最大法力、法力成长、初始法力、最高物攻、物攻成长、初始物攻、最大物防、物防成长、初始物防、最大每5秒回血、每5秒回血成长、初始每5秒回血、最大每5秒回蓝、每5秒回蓝成长、初始每5秒回蓝、最大攻速和攻击范围等。
|
||||
|
||||
具体的数据集你可以在GitHub上下载:[https://github.com/cystanford/EM_data](https://github.com/cystanford/EM_data)。
|
||||
|
||||
现在我们需要对王者荣耀的英雄数据进行聚类,我们先设定项目的执行流程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/78/8af94562f6bd3ac42036ec47f5ad2578.jpg" alt="">
|
||||
|
||||
<li>
|
||||
首先我们需要加载数据源;
|
||||
</li>
|
||||
<li>
|
||||
在准备阶段,我们需要对数据进行探索,包括采用数据可视化技术,让我们对英雄属性以及这些属性之间的关系理解更加深刻,然后对数据质量进行评估,是否进行数据清洗,最后进行特征选择方便后续的聚类算法;
|
||||
</li>
|
||||
<li>
|
||||
聚类阶段:选择适合的聚类模型,这里我们采用GMM高斯混合模型进行聚类,并输出聚类结果,对结果进行分析。
|
||||
</li>
|
||||
|
||||
按照上面的步骤,我们来编写下代码。完整的代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: utf-8 -*-
|
||||
import pandas as pd
|
||||
import csv
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from sklearn.mixture import GaussianMixture
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
# 数据加载,避免中文乱码问题
|
||||
data_ori = pd.read_csv('./heros7.csv', encoding = 'gb18030')
|
||||
features = [u'最大生命',u'生命成长',u'初始生命',u'最大法力', u'法力成长',u'初始法力',u'最高物攻',u'物攻成长',u'初始物攻',u'最大物防',u'物防成长',u'初始物防', u'最大每5秒回血', u'每5秒回血成长', u'初始每5秒回血', u'最大每5秒回蓝', u'每5秒回蓝成长', u'初始每5秒回蓝', u'最大攻速', u'攻击范围']
|
||||
data = data_ori[features]
|
||||
|
||||
# 对英雄属性之间的关系进行可视化分析
|
||||
# 设置plt正确显示中文
|
||||
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
|
||||
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
|
||||
# 用热力图呈现features_mean字段之间的相关性
|
||||
corr = data[features].corr()
|
||||
plt.figure(figsize=(14,14))
|
||||
# annot=True显示每个方格的数据
|
||||
sns.heatmap(corr, annot=True)
|
||||
plt.show()
|
||||
|
||||
# 相关性大的属性保留一个,因此可以对属性进行降维
|
||||
features_remain = [u'最大生命', u'初始生命', u'最大法力', u'最高物攻', u'初始物攻', u'最大物防', u'初始物防', u'最大每5秒回血', u'最大每5秒回蓝', u'初始每5秒回蓝', u'最大攻速', u'攻击范围']
|
||||
data = data_ori[features_remain]
|
||||
data[u'最大攻速'] = data[u'最大攻速'].apply(lambda x: float(x.strip('%'))/100)
|
||||
data[u'攻击范围']=data[u'攻击范围'].map({'远程':1,'近战':0})
|
||||
# 采用Z-Score规范化数据,保证每个特征维度的数据均值为0,方差为1
|
||||
ss = StandardScaler()
|
||||
data = ss.fit_transform(data)
|
||||
# 构造GMM聚类
|
||||
gmm = GaussianMixture(n_components=30, covariance_type='full')
|
||||
gmm.fit(data)
|
||||
# 训练数据
|
||||
prediction = gmm.predict(data)
|
||||
print(prediction)
|
||||
# 将分组结果输出到CSV文件中
|
||||
data_ori.insert(0, '分组', prediction)
|
||||
data_ori.to_csv('./hero_out.csv', index=False, sep=',')
|
||||
|
||||
```
|
||||
|
||||
运行结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/fb/dbe96b767d7f3ff2dd9f44b651cde8fb.png" alt="">
|
||||
|
||||
```
|
||||
[28 14 8 9 5 5 15 8 3 14 18 14 9 7 16 18 13 3 5 4 19 12 4 12
|
||||
12 12 4 17 24 2 7 2 2 24 2 2 24 6 20 22 22 24 24 2 2 22 14 20
|
||||
14 24 26 29 27 25 25 28 11 1 23 5 11 0 10 28 21 29 29 29 17]
|
||||
|
||||
```
|
||||
|
||||
同时你也能看到输出的聚类结果文件hero_out.csv(它保存在你本地运行的文件夹里,程序会自动输出这个文件,你可以自己看下)。
|
||||
|
||||
我来简单讲解下程序的几个模块。
|
||||
|
||||
**关于引用包**
|
||||
|
||||
首先我们会用DataFrame数据结构来保存读取的数据,最后的聚类结果会写入到CSV文件中,因此会用到pandas和CSV工具包。另外我们需要对数据进行可视化,采用热力图展现属性之间的相关性,这里会用到matplotlib.pyplot和seaborn工具包。在数据规范化中我们使用到了Z-Score规范化,用到了StandardScaler类,最后我们还会用到sklearn中的GaussianMixture类进行聚类。
|
||||
|
||||
**数据可视化的探索**
|
||||
|
||||
你能看到我们将20个英雄属性之间的关系用热力图呈现了出来,中间的数字代表两个属性之间的关系系数,最大值为1,代表完全正相关,关系系数越大代表相关性越大。从图中你能看出来“最大生命”“生命成长”和“初始生命”这三个属性的相关性大,我们只需要保留一个属性即可。同理我们也可以对其他相关性大的属性进行筛选,保留一个。你在代码中可以看到,我用features_remain数组保留了特征选择的属性,这样就将原本的20个属性降维到了13个属性。
|
||||
|
||||
**关于数据规范化**
|
||||
|
||||
我们能看到“最大攻速”这个属性值是百分数,不适合做矩阵运算,因此我们需要将百分数转化为小数。我们也看到“攻击范围”这个字段的取值为远程或者近战,也不适合矩阵运算,我们将取值做个映射,用1代表远程,0代表近战。然后采用Z-Score规范化,对特征矩阵进行规范化。
|
||||
|
||||
**在聚类阶段**
|
||||
|
||||
我们采用了GMM高斯混合模型,并将结果输出到CSV文件中。
|
||||
|
||||
这里我将输出的结果截取了一段(设置聚类个数为30):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/ce/5c74ffe6741f1bf1bfdf7711932d47ce.png" alt=""><br>
|
||||
第一列代表的是分组(簇),我们能看到张飞、程咬金分到了一组,牛魔、白起是一组,老夫子自己是一组,达摩、典韦是一组。聚类的特点是相同类别之间的属性值相近,不同类别的属性值差异大。因此如果你擅长用典韦这个英雄,不防试试达摩这个英雄。同样你也可以在张飞和程咬金中进行切换。这样就算你的英雄被别人选中了,你依然可以有备选的英雄可以使用。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你一起做了EM聚类的实战,具体使用的是GMM高斯混合模型。从整个流程中可以看出,我们需要经过数据加载、数据探索、数据可视化、特征选择、GMM聚类和结果分析等环节。
|
||||
|
||||
聚类和分类不一样,聚类是无监督的学习方式,也就是我们没有实际的结果可以进行比对,所以聚类的结果评估不像分类准确率一样直观,那么有没有聚类结果的评估方式呢?这里我们可以采用Calinski-Harabaz指标,代码如下:
|
||||
|
||||
```
|
||||
from sklearn.metrics import calinski_harabaz_score
|
||||
print(calinski_harabaz_score(data, prediction))
|
||||
|
||||
```
|
||||
|
||||
指标分数越高,代表聚类效果越好,也就是相同类中的差异性小,不同类之间的差异性大。当然具体聚类的结果含义,我们需要人工来分析,也就是当这些数据被分成不同的类别之后,具体每个类表代表的含义。
|
||||
|
||||
另外聚类算法也可以作为其他数据挖掘算法的预处理阶段,这样我们就可以将数据进行降维了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/d7/43b35b8f49ac83799ea1ca88383609d7.png" alt=""><br>
|
||||
最后依然是两道思考题。针对王者荣耀的英雄数据集,我进行了特征选择,实际上王者荣耀的英雄数量并不多,我们可以省略特征选择这个阶段,你不妨用全部的特征值矩阵进行聚类训练,来看下聚类得到的结果。第二个问题是,依然用王者荣耀英雄数据集,在聚类个数为3以及聚类个数为30的情况下,请你使用GMM高斯混合模型对数据集进行聚类,并得出Calinski_Harabaz分数。
|
||||
|
||||
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<audio id="audio" title="30丨关联规则挖掘(上):如何用Apriori发现用户购物规则?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4a/8b/4ada60ac4eba556bbdb88dd59f4ca68b.mp3"></audio>
|
||||
|
||||
今天我来带你进行关联规则挖掘的学习,关联规则这个概念,最早是由Agrawal等人在1993年提出的。在1994年Agrawal等人又提出了基于关联规则的Apriori算法,至今Apriori仍是关联规则挖掘的重要算法。
|
||||
|
||||
关联规则挖掘可以让我们从数据集中发现项与项(item与item)之间的关系,它在我们的生活中有很多应用场景,“购物篮分析”就是一个常见的场景,这个场景可以从消费者交易记录中发掘商品与商品之间的关联关系,进而通过商品捆绑销售或者相关推荐的方式带来更多的销售量。所以说,关联规则挖掘是个非常有用的技术。
|
||||
|
||||
在今天的内容中,希望你能带着问题,和我一起来搞懂以下几个知识点:
|
||||
|
||||
<li>
|
||||
搞懂关联规则中的几个重要概念:支持度、置信度、提升度;
|
||||
</li>
|
||||
<li>
|
||||
Apriori算法的工作原理;
|
||||
</li>
|
||||
<li>
|
||||
在实际工作中,我们该如何进行关联规则挖掘。
|
||||
</li>
|
||||
|
||||
## 搞懂关联规则中的几个概念
|
||||
|
||||
我举一个超市购物的例子,下面是几名客户购买的商品列表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f7/1c/f7d0cc3c1a845bf790b344f62372941c.png" alt=""><br>
|
||||
**什么是支持度呢?**
|
||||
|
||||
支持度是个百分比,它指的是某个商品组合出现的次数与总次数之间的比例。支持度越高,代表这个组合出现的频率越大。
|
||||
|
||||
在这个例子中,我们能看到“牛奶”出现了4次,那么这5笔订单中“牛奶”的支持度就是4/5=0.8。
|
||||
|
||||
同样“牛奶+面包”出现了3次,那么这5笔订单中“牛奶+面包”的支持度就是3/5=0.6。
|
||||
|
||||
**什么是置信度呢?**
|
||||
|
||||
它指的就是当你购买了商品A,会有多大的概率购买商品B,在上面这个例子中:
|
||||
|
||||
置信度(牛奶→啤酒)=2/4=0.5,代表如果你购买了牛奶,有多大的概率会购买啤酒?
|
||||
|
||||
置信度(啤酒→牛奶)=2/3=0.67,代表如果你购买了啤酒,有多大的概率会购买牛奶?
|
||||
|
||||
我们能看到,在4次购买了牛奶的情况下,有2次购买了啤酒,所以置信度(牛奶→啤酒)=0.5,而在3次购买啤酒的情况下,有2次购买了牛奶,所以置信度(啤酒→牛奶)=0.67。
|
||||
|
||||
所以说置信度是个条件概念,就是说在A发生的情况下,B发生的概率是多少。
|
||||
|
||||
**什么是提升度呢?**
|
||||
|
||||
我们在做商品推荐的时候,重点考虑的是提升度,因为提升度代表的是“商品A的出现,对商品B的出现概率提升的”程度。
|
||||
|
||||
还是看上面的例子,如果我们单纯看置信度(可乐→尿布)=1,也就是说可乐出现的时候,用户都会购买尿布,那么当用户购买可乐的时候,我们就需要推荐尿布么?
|
||||
|
||||
实际上,就算用户不购买可乐,也会直接购买尿布的,所以用户是否购买可乐,对尿布的提升作用并不大。我们可以用下面的公式来计算商品A对商品B的提升度:
|
||||
|
||||
提升度(A→B)=置信度(A→B)/支持度(B)
|
||||
|
||||
这个公式是用来衡量A出现的情况下,是否会对B出现的概率有所提升。
|
||||
|
||||
所以提升度有三种可能:
|
||||
|
||||
<li>
|
||||
提升度(A→B)>1:代表有提升;
|
||||
</li>
|
||||
<li>
|
||||
提升度(A→B)=1:代表有没有提升,也没有下降;
|
||||
</li>
|
||||
<li>
|
||||
提升度(A→B)<1:代表有下降。
|
||||
</li>
|
||||
|
||||
## Apriori的工作原理
|
||||
|
||||
明白了关联规则中支持度、置信度和提升度这几个重要概念,我们来看下Apriori算法是如何工作的。
|
||||
|
||||
首先我们把上面案例中的商品用ID来代表,牛奶、面包、尿布、可乐、啤酒、鸡蛋的商品ID分别设置为1-6,上面的数据表可以变为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/33/e30fe11a21191259e6a93568461fa933.png" alt=""><br>
|
||||
Apriori算法其实就是查找频繁项集(frequent itemset)的过程,所以首先我们需要定义什么是频繁项集。
|
||||
|
||||
频繁项集就是支持度大于等于最小支持度(Min Support)阈值的项集,所以小于最小值支持度的项目就是非频繁项集,而大于等于最小支持度的项集就是频繁项集。
|
||||
|
||||
项集这个概念,英文叫做itemset,它可以是单个的商品,也可以是商品的组合。我们再来看下这个例子,假设我随机指定最小支持度是50%,也就是0.5。
|
||||
|
||||
我们来看下Apriori算法是如何运算的。
|
||||
|
||||
首先,我们先计算单个商品的支持度,也就是得到K=1项的支持度:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/de/fff5ba49aff930bba71c98685be4fcde.png" alt=""><br>
|
||||
因为最小支持度是0.5,所以你能看到商品4、6是不符合最小支持度的,不属于频繁项集,于是经过筛选商品的频繁项集就变成:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/b6/ae108dc65c33e9ed9546a0d91bd881b6.png" alt=""><br>
|
||||
在这个基础上,我们将商品两两组合,得到k=2项的支持度:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/a3/a51fd814ebd68304e3cb137630af3ea3.png" alt=""><br>
|
||||
我们再筛掉小于最小值支持度的商品组合,可以得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/c8/a087cd1bd2a9e033105de275834b79c8.png" alt=""><br>
|
||||
我们再将商品进行K=3项的商品组合,可以得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/9c/a7f4448cc5031b1edf304c9aed94039c.png" alt="">
|
||||
|
||||
再筛掉小于最小值支持度的商品组合,可以得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/0f/d51fc9137a537d8cb96fa21707cab70f.png" alt=""><br>
|
||||
通过上面这个过程,我们可以得到K=3项的频繁项集{1,2,3},也就是{牛奶、面包、尿布}的组合。
|
||||
|
||||
到这里,你已经和我模拟了一遍整个Apriori算法的流程,下面我来给你总结下Apriori算法的递归流程:
|
||||
|
||||
<li>
|
||||
K=1,计算K项集的支持度;
|
||||
</li>
|
||||
<li>
|
||||
筛选掉小于最小支持度的项集;
|
||||
</li>
|
||||
<li>
|
||||
如果项集为空,则对应K-1项集的结果为最终结果。
|
||||
</li>
|
||||
|
||||
否则K=K+1,重复1-3步。
|
||||
|
||||
## Apriori的改进算法:FP-Growth算法
|
||||
|
||||
我们刚完成了Apriori算法的模拟,你能看到Apriori在计算的过程中有以下几个缺点:
|
||||
|
||||
<li>
|
||||
可能产生大量的候选集。因为采用排列组合的方式,把可能的项集都组合出来了;
|
||||
</li>
|
||||
<li>
|
||||
每次计算都需要重新扫描数据集,来计算每个项集的支持度。
|
||||
</li>
|
||||
|
||||
所以Apriori算法会浪费很多计算空间和计算时间,为此人们提出了FP-Growth算法,它的特点是:
|
||||
|
||||
<li>
|
||||
创建了一棵FP树来存储频繁项集。在创建前对不满足最小支持度的项进行删除,减少了存储空间。我稍后会讲解如何构造一棵FP树;
|
||||
</li>
|
||||
<li>
|
||||
整个生成过程只遍历数据集2次,大大减少了计算量。
|
||||
</li>
|
||||
|
||||
所以在实际工作中,我们常用FP-Growth来做频繁项集的挖掘,下面我给你简述下FP-Growth的原理。
|
||||
|
||||
**1.创建项头表(item header table)**
|
||||
|
||||
创建项头表的作用是为FP构建及频繁项集挖掘提供索引。
|
||||
|
||||
这一步的流程是先扫描一遍数据集,对于满足最小支持度的单个项(K=1项集)按照支持度从高到低进行排序,这个过程中删除了不满足最小支持度的项。
|
||||
|
||||
项头表包括了项目、支持度,以及该项在FP树中的链表。初始的时候链表为空。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/f5/69ce07c61a654faafb4f5114df1557f5.png" alt=""><br>
|
||||
**2.构造FP树**
|
||||
|
||||
FP树的根节点记为NULL节点。
|
||||
|
||||
整个流程是需要再次扫描数据集,对于每一条数据,按照支持度从高到低的顺序进行创建节点(也就是第一步中项头表中的排序结果),节点如果存在就将计数count+1,如果不存在就进行创建。同时在创建的过程中,需要更新项头表的链表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/92/eadaaf6585379815e62aad99386c7992.png" alt=""><br>
|
||||
**3.通过FP树挖掘频繁项集**
|
||||
|
||||
到这里,我们就得到了一个存储频繁项集的FP树,以及一个项头表。我们可以通过项头表来挖掘出每个频繁项集。
|
||||
|
||||
具体的操作会用到一个概念,叫“条件模式基”,它指的是以要挖掘的节点为叶子节点,自底向上求出FP子树,然后将FP子树的祖先节点设置为叶子节点之和。
|
||||
|
||||
我以“啤酒”的节点为例,从FP树中可以得到一棵FP子树,将祖先节点的支持度记为叶子节点之和,得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/0f/9951cda824fc9823136231e7c8e70d0f.png" alt=""><br>
|
||||
你能看出来,相比于原来的FP树,尿布和牛奶的频繁项集数减少了。这是因为我们求得的是以“啤酒”为节点的FP子树,也就是说,在频繁项集中一定要含有“啤酒”这个项。你可以再看下原始的数据,其中订单1{牛奶、面包、尿布}和订单5{牛奶、面包、尿布、可乐}并不存在“啤酒”这个项,所以针对订单1,尿布→牛奶→面包这个项集就会从FP树中去掉,针对订单5也包括了尿布→牛奶→面包这个项集也会从FP树中去掉,所以你能看到以“啤酒”为节点的FP子树,尿布、牛奶、面包项集上的计数比原来少了2。
|
||||
|
||||
条件模式基不包括“啤酒”节点,而且祖先节点如果小于最小支持度就会被剪枝,所以“啤酒”的条件模式基为空。
|
||||
|
||||
同理,我们可以求得“面包”的条件模式基为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/13/41026c8f25b64b01125c8b8d6a19a113.png" alt=""><br>
|
||||
所以可以求得面包的频繁项集为{尿布,面包},{尿布,牛奶,面包}。同样,我们还可以求得牛奶,尿布的频繁项集,这里就不再计算展示。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了Apriori算法,它是在“购物篮分析”中常用的关联规则挖掘算法,在Apriori算法中你最主要是需要明白支持度、置信度、提升度这几个概念,以及Apriori迭代计算频繁项集的工作流程。
|
||||
|
||||
Apriori算法在实际工作中需要对数据集扫描多次,会消耗大量的计算时间,所以在2000年FP-Growth算法被提出来,它只需要扫描两次数据集即可以完成关联规则的挖掘。FP-Growth算法最主要的贡献就是提出了FP树和项头表,通过FP树减少了频繁项集的存储以及计算时间。
|
||||
|
||||
当然Apriori的改进算法除了FP-Growth算法以外,还有CBA算法、GSP算法,这里就不进行介绍。
|
||||
|
||||
你能发现一种新理论的提出,往往是先从最原始的概念出发,提出一种新的方法。原始概念最接近人们模拟的过程,但往往会存在空间和时间复杂度过高的情况。所以后面其他人会对这个方法做改进型的创新,重点是在空间和时间复杂度上进行降维,比如采用新型的数据结构。你能看出树在存储和检索中是一个非常好用的数据结构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/35/c7aee3b17269139ed3d5a6b82cc56735.png" alt=""><br>
|
||||
最后给你留两道思考题吧,你能说一说Apriori的工作原理吗?相比于Apriori,FP-Growth算法都有哪些改进?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来学习。
|
||||
|
||||
|
||||
239
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/31丨关联规则挖掘(下):导演如何选择演员?.md
Normal file
239
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/31丨关联规则挖掘(下):导演如何选择演员?.md
Normal file
@@ -0,0 +1,239 @@
|
||||
<audio id="audio" title="31丨关联规则挖掘(下):导演如何选择演员?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9f/d1/9ffd041fdd8bebf8c686ae7166c566d1.mp3"></audio>
|
||||
|
||||
上次我给你讲了关联规则挖掘的原理。关联规则挖掘在生活中有很多使用场景,不仅是商品的捆绑销售,甚至在挑选演员决策上,你也能通过关联规则挖掘看出来某个导演选择演员的倾向。
|
||||
|
||||
今天我来带你用Apriori算法做一个项目实战。你需要掌握的是以下几点:
|
||||
|
||||
<li>
|
||||
熟悉上节课讲到的几个重要概念:支持度、置信度和提升度;
|
||||
</li>
|
||||
<li>
|
||||
熟悉与掌握Apriori工具包的使用;
|
||||
</li>
|
||||
<li>
|
||||
在实际问题中,灵活运用。包括数据集的准备等。
|
||||
</li>
|
||||
|
||||
## 如何使用Apriori工具包
|
||||
|
||||
Apriori虽然是十大算法之一,不过在sklearn工具包中并没有它,也没有FP-Growth算法。这里教你个方法,来选择Python中可以使用的工具包,你可以通过[https://pypi.org/](https://pypi.org/) 搜索工具包。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/c7/76a3b34beccbe7b69a11951b4efd80c7.png" alt=""><br>
|
||||
这个网站提供的工具包都是Python语言的,你能找到8个Python语言的Apriori工具包,具体选择哪个呢?建议你使用第二个工具包,即efficient-apriori。后面我会讲到为什么推荐这个工具包。
|
||||
|
||||
首先你需要通过pip install efficient-apriori 安装这个工具包。
|
||||
|
||||
然后看下如何使用它,核心的代码就是这一行:
|
||||
|
||||
```
|
||||
itemsets, rules = apriori(data, min_support, min_confidence)
|
||||
|
||||
```
|
||||
|
||||
其中data是我们要提供的数据集,它是一个list数组类型。min_support参数为最小支持度,在efficient-apriori工具包中用0到1的数值代表百分比,比如0.5代表最小支持度为50%。min_confidence是最小置信度,数值也代表百分比,比如1代表100%。
|
||||
|
||||
关于支持度、置信度和提升度,我们再来简单回忆下。
|
||||
|
||||
支持度指的是某个商品组合出现的次数与总次数之间的比例。支持度越高,代表这个组合出现的概率越大。
|
||||
|
||||
置信度是一个条件概念,就是在A发生的情况下,B发生的概率是多少。
|
||||
|
||||
提升度代表的是“商品A的出现,对商品B的出现概率提升了多少”。
|
||||
|
||||
接下来我们用这个工具包,跑一下上节课中讲到的超市购物的例子。下面是客户购买的商品列表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/a6/a48f4a2961c3be811431418eb84aeaa6.png" alt="">
|
||||
|
||||
具体实现的代码如下:
|
||||
|
||||
```
|
||||
from efficient_apriori import apriori
|
||||
# 设置数据集
|
||||
data = [('牛奶','面包','尿布'),
|
||||
('可乐','面包', '尿布', '啤酒'),
|
||||
('牛奶','尿布', '啤酒', '鸡蛋'),
|
||||
('面包', '牛奶', '尿布', '啤酒'),
|
||||
('面包', '牛奶', '尿布', '可乐')]
|
||||
# 挖掘频繁项集和频繁规则
|
||||
itemsets, rules = apriori(data, min_support=0.5, min_confidence=1)
|
||||
print(itemsets)
|
||||
print(rules)
|
||||
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
{1: {('啤酒',): 3, ('尿布',): 5, ('牛奶',): 4, ('面包',): 4}, 2: {('啤酒', '尿布'): 3, ('尿布', '牛奶'): 4, ('尿布', '面包'): 4, ('牛奶', '面包'): 3}, 3: {('尿布', '牛奶', '面包'): 3}}
|
||||
[{啤酒} -> {尿布}, {牛奶} -> {尿布}, {面包} -> {尿布}, {牛奶, 面包} -> {尿布}]
|
||||
|
||||
```
|
||||
|
||||
你能从代码中看出来,data是个List数组类型,其中每个值都可以是一个集合。实际上你也可以把data数组中的每个值设置为List数组类型,比如:
|
||||
|
||||
```
|
||||
data = [['牛奶','面包','尿布'],
|
||||
['可乐','面包', '尿布', '啤酒'],
|
||||
['牛奶','尿布', '啤酒', '鸡蛋'],
|
||||
['面包', '牛奶', '尿布', '啤酒'],
|
||||
['面包', '牛奶', '尿布', '可乐']]
|
||||
|
||||
```
|
||||
|
||||
两者的运行结果是一样的,efficient-apriori 工具包把每一条数据集里的项式都放到了一个集合中进行运算,并没有考虑它们之间的先后顺序。因为实际情况下,同一个购物篮中的物品也不需要考虑购买的先后顺序。
|
||||
|
||||
而其他的Apriori算法可能会因为考虑了先后顺序,出现计算频繁项集结果不对的情况。所以这里采用的是efficient-apriori这个工具包。
|
||||
|
||||
## 挖掘导演是如何选择演员的
|
||||
|
||||
在实际工作中,数据集是需要自己来准备的,比如今天我们要挖掘导演是如何选择演员的数据情况,但是并没有公开的数据集可以直接使用。因此我们需要使用之前讲到的Python爬虫进行数据采集。
|
||||
|
||||
不同导演选择演员的规则是不同的,因此我们需要先指定导演。数据源我们选用豆瓣电影。
|
||||
|
||||
先来梳理下采集的工作流程。
|
||||
|
||||
首先我们先在[https://movie.douban.com](https://movie.douban.com)搜索框中输入导演姓名,比如“宁浩”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/ef/eaba9861825a38b6fbd5af1bff7194ef.png" alt=""><br>
|
||||
页面会呈现出来导演之前的所有电影,然后对页面进行观察,你能观察到以下几个现象:
|
||||
|
||||
<li>
|
||||
页面默认是15条数据反馈,第一页会返回16条。因为第一条数据实际上这个导演的概览,你可以理解为是一条广告的插入,下面才是真正的返回结果。
|
||||
</li>
|
||||
<li>
|
||||
每条数据的最后一行是电影的演出人员的信息,第一个人员是导演,其余为演员姓名。姓名之间用“/”分割。
|
||||
</li>
|
||||
|
||||
有了这些观察之后,我们就可以编写抓取程序了。在代码讲解中你能看出这两点观察的作用。抓取程序的目的是为了生成宁浩导演(你也可以抓取其他导演)的数据集,结果会保存在csv文件中。完整的抓取代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: utf-8 -*-
|
||||
# 下载某个导演的电影数据集
|
||||
from efficient_apriori import apriori
|
||||
from lxml import etree
|
||||
import time
|
||||
from selenium import webdriver
|
||||
import csv
|
||||
driver = webdriver.Chrome()
|
||||
# 设置想要下载的导演 数据集
|
||||
director = u'宁浩'
|
||||
# 写CSV文件
|
||||
file_name = './' + director + '.csv'
|
||||
base_url = 'https://movie.douban.com/subject_search?search_text='+director+'&cat=1002&start='
|
||||
out = open(file_name,'w', newline='', encoding='utf-8-sig')
|
||||
csv_write = csv.writer(out, dialect='excel')
|
||||
flags=[]
|
||||
# 下载指定页面的数据
|
||||
def download(request_url):
|
||||
driver.get(request_url)
|
||||
time.sleep(1)
|
||||
html = driver.find_element_by_xpath("//*").get_attribute("outerHTML")
|
||||
html = etree.HTML(html)
|
||||
# 设置电影名称,导演演员 的XPATH
|
||||
movie_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']")
|
||||
name_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='meta abstract_2']")
|
||||
# 获取返回的数据个数
|
||||
num = len(movie_lists)
|
||||
if num > 15: #第一页会有16条数据
|
||||
# 默认第一个不是,所以需要去掉
|
||||
movie_lists = movie_lists[1:]
|
||||
name_lists = name_lists[1:]
|
||||
for (movie, name_list) in zip(movie_lists, name_lists):
|
||||
# 会存在数据为空的情况
|
||||
if name_list.text is None:
|
||||
continue
|
||||
# 显示下演员名称
|
||||
print(name_list.text)
|
||||
names = name_list.text.split('/')
|
||||
# 判断导演是否为指定的director
|
||||
if names[0].strip() == director and movie.text not in flags:
|
||||
# 将第一个字段设置为电影名称
|
||||
names[0] = movie.text
|
||||
flags.append(movie.text)
|
||||
csv_write.writerow(names)
|
||||
print('OK') # 代表这页数据下载成功
|
||||
print(num)
|
||||
if num >= 14: #有可能一页会有14个电影
|
||||
# 继续下一页
|
||||
return True
|
||||
else:
|
||||
# 没有下一页
|
||||
return False
|
||||
|
||||
# 开始的ID为0,每页增加15
|
||||
start = 0
|
||||
while start<10000: #最多抽取1万部电影
|
||||
request_url = base_url + str(start)
|
||||
# 下载数据,并返回是否有下一页
|
||||
flag = download(request_url)
|
||||
if flag:
|
||||
start = start + 15
|
||||
else:
|
||||
break
|
||||
out.close()
|
||||
print('finished')
|
||||
|
||||
```
|
||||
|
||||
代码中涉及到了几个模块,我简单讲解下这几个模块。
|
||||
|
||||
在引用包这一段,我们使用csv工具包读写CSV文件,用efficient_apriori完成Apriori算法,用lxml进行XPath解析,time工具包可以让我们在模拟后有个适当停留,代码中我设置为1秒钟,等HTML数据完全返回后再进行HTML内容的获取。使用selenium的webdriver来模拟浏览器的行为。
|
||||
|
||||
在读写文件这一块,我们需要事先告诉python的open函数,文件的编码是utf-8-sig(对应代码:encoding=‘utf-8-sig’),这是因为我们会用到中文,为了避免编码混乱。
|
||||
|
||||
编写download函数,参数传入我们要采集的页面地址(request_url)。针对返回的HTML,我们需要用到之前讲到的Chrome浏览器的XPath Helper工具,来获取电影名称以及演出人员的XPath。我用页面返回的数据个数来判断当前所处的页面序号。如果数据个数>15,也就是第一页,第一页的第一条数据是广告,我们需要忽略。如果数据个数=15,代表是中间页,需要点击“下一页”,也就是翻页。如果数据个数<15,代表最后一页,没有下一页。
|
||||
|
||||
在程序主体部分,我们设置start代表抓取的ID,从0开始最多抓取1万部电影的数据(一个导演不会超过1万部电影),每次翻页start自动增加15,直到flag=False为止,也就是不存在下一页的情况。
|
||||
|
||||
你可以模拟下抓取的流程,获得指定导演的数据,比如我上面抓取的宁浩的数据。这里需要注意的是,豆瓣的电影数据可能是不全的,但基本上够我们用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/16/5ea61131d1fce390040cf0edf6897a16.png" alt=""><br>
|
||||
有了数据之后,我们就可以用Apriori算法来挖掘频繁项集和关联规则,代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: utf-8 -*-
|
||||
from efficient_apriori import apriori
|
||||
import csv
|
||||
director = u'宁浩'
|
||||
file_name = './'+director+'.csv'
|
||||
lists = csv.reader(open(file_name, 'r', encoding='utf-8-sig'))
|
||||
# 数据加载
|
||||
data = []
|
||||
for names in lists:
|
||||
name_new = []
|
||||
for name in names:
|
||||
# 去掉演员数据中的空格
|
||||
name_new.append(name.strip())
|
||||
data.append(name_new[1:])
|
||||
# 挖掘频繁项集和关联规则
|
||||
itemsets, rules = apriori(data, min_support=0.5, min_confidence=1)
|
||||
print(itemsets)
|
||||
print(rules)
|
||||
|
||||
```
|
||||
|
||||
代码中使用的apriori方法和开头中用Apriori获取购物篮规律的方法类似,比如代码中都设定了最小支持度和最小置信系数,这样我们可以找到支持度大于50%,置信系数为1的频繁项集和关联规则。
|
||||
|
||||
这是最后的运行结果:
|
||||
|
||||
```
|
||||
{1: {('徐峥',): 5, ('黄渤',): 6}, 2: {('徐峥', '黄渤'): 5}}
|
||||
[{徐峥} -> {黄渤}]
|
||||
|
||||
```
|
||||
|
||||
你能看出来,宁浩导演喜欢用徐峥和黄渤,并且有徐峥的情况下,一般都会用黄渤。你也可以用上面的代码来挖掘下其他导演选择演员的规律。
|
||||
|
||||
## 总结
|
||||
|
||||
Apriori算法的核心就是理解频繁项集和关联规则。在算法运算的过程中,还要重点掌握对支持度、置信度和提升度的理解。在工具使用上,你可以使用efficient-apriori这个工具包,它会把每一条数据中的项(item)放到一个集合(篮子)里来处理,不考虑项(item)之间的先后顺序。
|
||||
|
||||
在实际运用中你还需要灵活处理,比如导演如何选择演员这个案例,虽然工具的使用会很方便,但重要的还是数据挖掘前的准备过程,也就是获取某个导演的电影数据集。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/9d/282c25e8651b3e0b675be7267d13629d.png" alt=""><br>
|
||||
最后给你留两道思考题吧。请你编写代码挖掘下张艺谋导演使用演员的频繁项集和关联规则,最小支持度可以设置为0.1或0.05。另外你认为Apriori算法中的最小支持度和最小置信度,一般设置为多少比较合理?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<audio id="audio" title="32丨PageRank(上):搞懂Google的PageRank算法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ff/ed/ff8827a9931eba791c5591f8dc8227ed.mp3"></audio>
|
||||
|
||||
互联网发展到现在,搜索引擎已经非常好用,基本上输入关键词,都能找到匹配的内容,质量还不错。但在1998年之前,搜索引擎的体验并不好。早期的搜索引擎,会遇到下面的两类问题:
|
||||
|
||||
<li>
|
||||
返回结果质量不高:搜索结果不考虑网页的质量,而是通过时间顺序进行检索;
|
||||
</li>
|
||||
<li>
|
||||
容易被人钻空子:搜索引擎是基于检索词进行检索的,页面中检索词出现的频次越高,匹配度越高,这样就会出现网页作弊的情况。有些网页为了增加搜索引擎的排名,故意增加某个检索词的频率。
|
||||
</li>
|
||||
|
||||
基于这些缺陷,当时Google的创始人拉里·佩奇提出了PageRank算法,目的就是要找到优质的网页,这样Google的排序结果不仅能找到用户想要的内容,而且还会从众多网页中筛选出权重高的呈现给用户。
|
||||
|
||||
Google的两位创始人都是斯坦福大学的博士生,他们提出的PageRank算法受到了论文影响力因子的评价启发。当一篇论文被引用的次数越多,证明这篇论文的影响力越大。正是这个想法解决了当时网页检索质量不高的问题。
|
||||
|
||||
## PageRank的简化模型
|
||||
|
||||
我们先来看下PageRank是如何计算的。
|
||||
|
||||
我假设一共有4个网页A、B、C、D。它们之间的链接信息如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/36/814d53ff8d73113631482e71b7c53636.png" alt=""><br>
|
||||
这里有两个概念你需要了解一下。
|
||||
|
||||
出链指的是链接出去的链接。入链指的是链接进来的链接。比如图中A有2个入链,3个出链。
|
||||
|
||||
简单来说,一个网页的影响力=所有入链集合的页面的加权影响力之和,用公式表示为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/0c/70104ab44fa1d9d690f99dc328d8af0c.png" alt=""><br>
|
||||
u为待评估的页面,$B_{u}$ 为页面u的入链集合。针对入链集合中的任意页面v,它能给u带来的影响力是其自身的影响力PR(v)除以v页面的出链数量,即页面v把影响力PR(v)平均分配给了它的出链,这样统计所有能给u带来链接的页面v,得到的总和就是网页u的影响力,即为PR(u)。
|
||||
|
||||
所以你能看到,出链会给被链接的页面赋予影响力,当我们统计了一个网页链出去的数量,也就是统计了这个网页的跳转概率。
|
||||
|
||||
在这个例子中,你能看到A有三个出链分别链接到了B、C、D上。那么当用户访问A的时候,就有跳转到B、C或者D的可能性,跳转概率均为1/3。
|
||||
|
||||
B有两个出链,链接到了A和D上,跳转概率为1/2。
|
||||
|
||||
这样,我们可以得到A、B、C、D这四个网页的转移矩阵M:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/d4/204b0934f166d6945a90185aa2c95dd4.png" alt=""><br>
|
||||
我们假设A、B、C、D四个页面的初始影响力都是相同的,即:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/b8/a8eb12b5242e082b5d2281300c326bb8.png" alt=""><br>
|
||||
当进行第一次转移之后,各页面的影响力$w_{1}$变为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/8c/fcbcdd8e96384f855b4f7c842627ff8c.png" alt=""><br>
|
||||
然后我们再用转移矩阵乘以$w_{1}$得到$w_{2}$结果,直到第n次迭代后$w_{n}$影响力不再发生变化,可以收敛到(0.3333,0.2222,0.2222,0.2222),也就是对应着A、B、C、D四个页面最终平衡状态下的影响力。
|
||||
|
||||
你能看出A页面相比于其他页面来说权重更大,也就是PR值更高。而B、C、D页面的PR值相等。
|
||||
|
||||
至此,我们模拟了一个简化的PageRank的计算过程,实际情况会比这个复杂,可能会面临两个问题:
|
||||
|
||||
1.等级泄露(Rank Leak):如果一个网页没有出链,就像是一个黑洞一样,吸收了其他网页的影响力而不释放,最终会导致其他网页的PR值为0。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/62/77336108b0233638a35bfd7450438162.png" alt=""><br>
|
||||
2.等级沉没(Rank Sink):如果一个网页只有出链,没有入链(如下图所示),计算的过程迭代下来,会导致这个网页的PR值为0(也就是不存在公式中的V)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/e6/0d113854fb56116d79efe7f0e0374fe6.png" alt=""><br>
|
||||
针对等级泄露和等级沉没的情况,我们需要灵活处理。
|
||||
|
||||
比如针对等级泄露的情况,我们可以把没有出链的节点,先从图中去掉,等计算完所有节点的PR值之后,再加上该节点进行计算。不过这种方法会导致新的等级泄露的节点的产生,所以工作量还是很大的。
|
||||
|
||||
有没有一种方法,可以同时解决等级泄露和等级沉没这两个问题呢?
|
||||
|
||||
## PageRank的随机浏览模型
|
||||
|
||||
为了解决简化模型中存在的等级泄露和等级沉没的问题,拉里·佩奇提出了PageRank的随机浏览模型。他假设了这样一个场景:用户并不都是按照跳转链接的方式来上网,还有一种可能是不论当前处于哪个页面,都有概率访问到其他任意的页面,比如说用户就是要直接输入网址访问其他页面,虽然这个概率比较小。
|
||||
|
||||
所以他定义了阻尼因子d,这个因子代表了用户按照跳转链接来上网的概率,通常可以取一个固定值0.85,而1-d=0.15则代表了用户不是通过跳转链接的方式来访问网页的,比如直接输入网址。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/8f/5f40c980c2f728f12159058ea19a4d8f.png" alt=""><br>
|
||||
其中N为网页总数,这样我们又可以重新迭代网页的权重计算了,因为加入了阻尼因子d,一定程度上解决了等级泄露和等级沉没的问题。
|
||||
|
||||
通过数学定理(这里不进行讲解)也可以证明,最终PageRank随机浏览模型是可以收敛的,也就是可以得到一个稳定正常的PR值。
|
||||
|
||||
## PageRank在社交影响力评估中的应用
|
||||
|
||||
网页之间会形成一个网络,是我们的互联网,论文之间也存在着相互引用的关系,可以说我们所处的环境就是各种网络的集合。
|
||||
|
||||
只要是有网络的地方,就存在出链和入链,就会有PR权重的计算,也就可以运用我们今天讲的PageRank算法。
|
||||
|
||||
我们可以把PageRank算法延展到社交网络领域中。比如在微博上,如果我们想要计算某个人的影响力,该怎么做呢?
|
||||
|
||||
一个人的微博粉丝数并不一定等于他的实际影响力。如果按照PageRank算法,还需要看这些粉丝的质量如何。如果有很多明星或者大V关注,那么这个人的影响力一定很高。如果粉丝是通过购买僵尸粉得来的,那么即使粉丝数再多,影响力也不高。
|
||||
|
||||
同样,在工作场景中,比如说脉脉这个社交软件,它计算的就是个人在职场的影响力。如果你的工作关系是李开复、江南春这样的名人,那么你的职场影响力一定会很高。反之,如果你是个学生,在职场上被链入的关系比较少的话,职场影响力就会比较低。
|
||||
|
||||
同样,如果你想要看一个公司的经营能力,也可以看这家公司都和哪些公司有合作。如果它合作的都是世界500强企业,那么这个公司在行业内一定是领导者,如果这个公司的客户都是小客户,即使数量比较多,业内影响力也不一定大。
|
||||
|
||||
除非像淘宝一样,有海量的中小客户,最后大客户也会找上门来寻求合作。所以权重高的节点,往往会有一些权重同样很高的节点在进行合作。
|
||||
|
||||
## PageRank给我们带来的启发
|
||||
|
||||
PageRank可以说是Google搜索引擎重要的技术之一,在1998年帮助Google获得了搜索引擎的领先优势,现在PageRank已经比原来复杂很多,但它的思想依然能带给我们很多启发。
|
||||
|
||||
比如,如果你想要自己的媒体影响力有所提高,就尽量要混在大V圈中;如果想找到高职位的工作,就尽量结识公司高层,或者认识更多的猎头,因为猎头和很多高职位的人员都有链接关系。
|
||||
|
||||
同样,PageRank也可以帮我们识别链接农场。链接农场指的是网页为了链接而链接,填充了一些没有用的内容。这些页面相互链接或者指向了某一个网页,从而想要得到更高的权重。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了PageRank的算法原理,对简化的PageRank模型进行了模拟。针对简化模型中存在的等级泄露和等级沉没这两个问题,PageRank的随机浏览模型引入了阻尼因子d来解决。
|
||||
|
||||
同样,PageRank有很广的应用领域,在许多网络结构中都有应用,比如计算一个人的微博影响力等。它也告诉我们,在社交网络中,链接的质量非常重要。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/7d/f936296fed70f27ba23064ec14a7e37d.png" alt=""><br>
|
||||
学完今天的内容,你不妨说说PageRank的算法原理?另外在现实生活中,除了我在文中举到的几个例子,你还能说一些PageRank都有哪些应用场景吗?
|
||||
|
||||
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
218
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/33丨PageRank(下):分析希拉里邮件中的人物关系.md
Normal file
218
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/33丨PageRank(下):分析希拉里邮件中的人物关系.md
Normal file
@@ -0,0 +1,218 @@
|
||||
<audio id="audio" title="33丨PageRank(下):分析希拉里邮件中的人物关系" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/29/26184d17ba65dc29e7af467ae4c59a29.mp3"></audio>
|
||||
|
||||
上节课我们讲到PageRank算法经常被用到网络关系的分析中,比如在社交网络中计算个人的影响力,计算论文的影响力或者网站的影响力等。
|
||||
|
||||
今天我们就来做一个关于PageRank算法的实战,在这之前,你需要思考三个问题:
|
||||
|
||||
<li>
|
||||
如何使用工具完成PageRank算法,包括使用工具创建网络图,设置节点、边、权重等,并通过创建好的网络图计算节点的PR值;
|
||||
</li>
|
||||
<li>
|
||||
对于一个实际的项目,比如希拉里的9306封邮件(工具包中邮件的数量),如何使用PageRank算法挖掘出有影响力的节点,并且绘制网络图;
|
||||
</li>
|
||||
<li>
|
||||
如何对创建好的网络图进行可视化,如果网络中的节点数较多,如何筛选重要的节点进行可视化,从而得到精简的网络关系图。
|
||||
</li>
|
||||
|
||||
## 如何使用工具实现PageRank算法
|
||||
|
||||
PageRank算法工具在sklearn中并不存在,我们需要找到新的工具包。实际上有一个关于图论和网络建模的工具叫NetworkX,它是用Python语言开发的工具,内置了常用的图与网络分析算法,可以方便我们进行网络数据分析。
|
||||
|
||||
上节课,我举了一个网页权重的例子,假设一共有4个网页A、B、C、D,它们之间的链接信息如图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/47/ea/47e5f21d16b15a98d4a32a73ebd477ea.png" alt=""><br>
|
||||
针对这个例子,我们看下用NetworkX如何计算A、B、C、D四个网页的PR值,具体代码如下:
|
||||
|
||||
```
|
||||
import networkx as nx
|
||||
# 创建有向图
|
||||
G = nx.DiGraph()
|
||||
# 有向图之间边的关系
|
||||
edges = [("A", "B"), ("A", "C"), ("A", "D"), ("B", "A"), ("B", "D"), ("C", "A"), ("D", "B"), ("D", "C")]
|
||||
for edge in edges:
|
||||
G.add_edge(edge[0], edge[1])
|
||||
pagerank_list = nx.pagerank(G, alpha=1)
|
||||
print("pagerank值是:", pagerank_list)
|
||||
|
||||
```
|
||||
|
||||
NetworkX工具把中间的计算细节都已经封装起来了,我们直接调用PageRank函数就可以得到结果:
|
||||
|
||||
```
|
||||
pagerank值是: {'A': 0.33333396911621094, 'B': 0.22222201029459634, 'C': 0.22222201029459634, 'D': 0.22222201029459634}
|
||||
|
||||
```
|
||||
|
||||
我们通过NetworkX创建了一个有向图之后,设置了节点之间的边,然后使用PageRank函数就可以求得节点的PR值,结果和上节课中我们人工模拟的结果一致。
|
||||
|
||||
好了,运行完这个例子之后,我们来看下NetworkX工具都有哪些常用的操作。
|
||||
|
||||
**1.关于图的创建**
|
||||
|
||||
图可以分为无向图和有向图,在NetworkX中分别采用不同的函数进行创建。无向图指的是不用节点之间的边的方向,使用nx.Graph() 进行创建;有向图指的是节点之间的边是有方向的,使用nx.DiGraph()来创建。在上面这个例子中,存在A→D的边,但不存在D→A的边。
|
||||
|
||||
**2.关于节点的增加、删除和查询**
|
||||
|
||||
如果想在网络中增加节点,可以使用G.add_node(‘A’)添加一个节点,也可以使用G.add_nodes_from([‘B’,‘C’,‘D’,‘E’])添加节点集合。如果想要删除节点,可以使用G.remove_node(node)删除一个指定的节点,也可以使用G.remove_nodes_from([‘B’,‘C’,‘D’,‘E’])删除集合中的节点。
|
||||
|
||||
那么该如何查询节点呢?
|
||||
|
||||
如果你想要得到图中所有的节点,就可以使用G.nodes(),也可以用G.number_of_nodes()得到图中节点的个数。
|
||||
|
||||
**3.关于边的增加、删除、查询**
|
||||
|
||||
增加边与添加节点的方式相同,使用G.add_edge(“A”, “B”)添加指定的“从A到B”的边,也可以使用add_edges_from函数从边集合中添加。我们也可以做一个加权图,也就是说边是带有权重的,使用add_weighted_edges_from函数从带有权重的边的集合中添加。在这个函数的参数中接收的是1个或多个三元组[u,v,w]作为参数,u、v、w分别代表起点、终点和权重。
|
||||
|
||||
另外,我们可以使用remove_edge函数和remove_edges_from函数删除指定边和从边集合中删除。
|
||||
|
||||
另外可以使用edges()函数访问图中所有的边,使用number_of_edges()函数得到图中边的个数。
|
||||
|
||||
以上是关于图的基本操作,如果我们创建了一个图,并且对节点和边进行了设置,就可以找到其中有影响力的节点,原理就是通过PageRank算法,使用nx.pagerank(G)这个函数,函数中的参数G代表创建好的图。
|
||||
|
||||
## 如何用PageRank揭秘希拉里邮件中的人物关系
|
||||
|
||||
了解了NetworkX工具的基础使用之后,我们来看一个实际的案例:希拉里邮件人物关系分析。
|
||||
|
||||
希拉里邮件事件相信你也有耳闻,对这个数据的背景我们就不做介绍了。你可以从GitHub上下载这个数据集:[https://github.com/cystanford/PageRank](https://github.com/cystanford/PageRank)。
|
||||
|
||||
整个数据集由三个文件组成:Aliases.csv,Emails.csv和Persons.csv,其中Emails文件记录了所有公开邮件的内容,发送者和接收者的信息。Persons这个文件统计了邮件中所有人物的姓名及对应的ID。因为姓名存在别名的情况,为了将邮件中的人物进行统一,我们还需要用Aliases文件来查询别名和人物的对应关系。
|
||||
|
||||
整个数据集包括了9306封邮件和513个人名,数据集还是比较大的。不过这一次我们不需要对邮件的内容进行分析,只需要通过邮件中的发送者和接收者(对应Emails.csv文件中的MetadataFrom和MetadataTo字段)来绘制整个关系网络。因为涉及到的人物很多,因此我们需要通过PageRank算法计算每个人物在邮件关系网络中的权重,最后筛选出来最有价值的人物来进行关系网络图的绘制。
|
||||
|
||||
了解了数据集和项目背景之后,我们来设计到执行的流程步骤:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/c9/72132ffbc1209301f0876178c75927c9.jpg" alt="">
|
||||
|
||||
<li>
|
||||
首先我们需要加载数据源;
|
||||
</li>
|
||||
<li>
|
||||
在准备阶段:我们需要对数据进行探索,在数据清洗过程中,因为邮件中存在别名的情况,因此我们需要统一人物名称。另外邮件的正文并不在我们考虑的范围内,只统计邮件中的发送者和接收者,因此我们筛选MetadataFrom和MetadataTo这两个字段作为特征。同时,发送者和接收者可能存在多次邮件往来,需要设置权重来统计两人邮件往来的次数。次数越多代表这个边(从发送者到接收者的边)的权重越高;
|
||||
</li>
|
||||
<li>
|
||||
在挖掘阶段:我们主要是对已经设置好的网络图进行PR值的计算,但邮件中的人物有500多人,有些人的权重可能不高,我们需要筛选PR值高的人物,绘制出他们之间的往来关系。在可视化的过程中,我们可以通过节点的PR值来绘制节点的大小,PR值越大,节点的绘制尺寸越大。
|
||||
</li>
|
||||
|
||||
设置好流程之后,实现的代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: utf-8 -*-
|
||||
# 用 PageRank 挖掘希拉里邮件中的重要任务关系
|
||||
import pandas as pd
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
import matplotlib.pyplot as plt
|
||||
# 数据加载
|
||||
emails = pd.read_csv("./input/Emails.csv")
|
||||
# 读取别名文件
|
||||
file = pd.read_csv("./input/Aliases.csv")
|
||||
aliases = {}
|
||||
for index, row in file.iterrows():
|
||||
aliases[row['Alias']] = row['PersonId']
|
||||
# 读取人名文件
|
||||
file = pd.read_csv("./input/Persons.csv")
|
||||
persons = {}
|
||||
for index, row in file.iterrows():
|
||||
persons[row['Id']] = row['Name']
|
||||
# 针对别名进行转换
|
||||
def unify_name(name):
|
||||
# 姓名统一小写
|
||||
name = str(name).lower()
|
||||
# 去掉, 和 @后面的内容
|
||||
name = name.replace(",","").split("@")[0]
|
||||
# 别名转换
|
||||
if name in aliases.keys():
|
||||
return persons[aliases[name]]
|
||||
return name
|
||||
# 画网络图
|
||||
def show_graph(graph, layout='spring_layout'):
|
||||
# 使用 Spring Layout 布局,类似中心放射状
|
||||
if layout == 'circular_layout':
|
||||
positions=nx.circular_layout(graph)
|
||||
else:
|
||||
positions=nx.spring_layout(graph)
|
||||
# 设置网络图中的节点大小,大小与 pagerank 值相关,因为 pagerank 值很小所以需要 *20000
|
||||
nodesize = [x['pagerank']*20000 for v,x in graph.nodes(data=True)]
|
||||
# 设置网络图中的边长度
|
||||
edgesize = [np.sqrt(e[2]['weight']) for e in graph.edges(data=True)]
|
||||
# 绘制节点
|
||||
nx.draw_networkx_nodes(graph, positions, node_size=nodesize, alpha=0.4)
|
||||
# 绘制边
|
||||
nx.draw_networkx_edges(graph, positions, edge_size=edgesize, alpha=0.2)
|
||||
# 绘制节点的 label
|
||||
nx.draw_networkx_labels(graph, positions, font_size=10)
|
||||
# 输出希拉里邮件中的所有人物关系图
|
||||
plt.show()
|
||||
# 将寄件人和收件人的姓名进行规范化
|
||||
emails.MetadataFrom = emails.MetadataFrom.apply(unify_name)
|
||||
emails.MetadataTo = emails.MetadataTo.apply(unify_name)
|
||||
# 设置遍的权重等于发邮件的次数
|
||||
edges_weights_temp = defaultdict(list)
|
||||
for row in zip(emails.MetadataFrom, emails.MetadataTo, emails.RawText):
|
||||
temp = (row[0], row[1])
|
||||
if temp not in edges_weights_temp:
|
||||
edges_weights_temp[temp] = 1
|
||||
else:
|
||||
edges_weights_temp[temp] = edges_weights_temp[temp] + 1
|
||||
# 转化格式 (from, to), weight => from, to, weight
|
||||
edges_weights = [(key[0], key[1], val) for key, val in edges_weights_temp.items()]
|
||||
# 创建一个有向图
|
||||
graph = nx.DiGraph()
|
||||
# 设置有向图中的路径及权重 (from, to, weight)
|
||||
graph.add_weighted_edges_from(edges_weights)
|
||||
# 计算每个节点(人)的 PR 值,并作为节点的 pagerank 属性
|
||||
pagerank = nx.pagerank(graph)
|
||||
# 将 pagerank 数值作为节点的属性
|
||||
nx.set_node_attributes(graph, name = 'pagerank', values=pagerank)
|
||||
# 画网络图
|
||||
show_graph(graph)
|
||||
|
||||
# 将完整的图谱进行精简
|
||||
# 设置 PR 值的阈值,筛选大于阈值的重要核心节点
|
||||
pagerank_threshold = 0.005
|
||||
# 复制一份计算好的网络图
|
||||
small_graph = graph.copy()
|
||||
# 剪掉 PR 值小于 pagerank_threshold 的节点
|
||||
for n, p_rank in graph.nodes(data=True):
|
||||
if p_rank['pagerank'] < pagerank_threshold:
|
||||
small_graph.remove_node(n)
|
||||
# 画网络图,采用circular_layout布局让筛选出来的点组成一个圆
|
||||
show_graph(small_graph, 'circular_layout')
|
||||
|
||||
```
|
||||
|
||||
运行结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/b1/419f7621392045f07bcd03f9e4c7c8b1.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/e1/3f08f61360e8a82a23a16e44d2b973e1.png" alt=""><br>
|
||||
针对代码中的几个模块我做个简单的说明:
|
||||
|
||||
**1.函数定义**
|
||||
|
||||
人物的名称需要统一,因此我设置了unify_name函数,同时设置了show_graph函数将网络图可视化。NetworkX提供了多种可视化布局,这里我使用spring_layout布局,也就是呈中心放射状。
|
||||
|
||||
除了spring_layout外,NetworkX还有另外三种可视化布局,circular_layout(在一个圆环上均匀分布节点),random_layout(随机分布节点 ),shell_layout(节点都在同心圆上)。
|
||||
|
||||
**2.计算边权重**
|
||||
|
||||
邮件的发送者和接收者的邮件往来可能不止一次,我们需要用两者之间邮件往来的次数计算这两者之间边的权重,所以我用edges_weights_temp数组存储权重。而上面介绍过在NetworkX中添加权重边(即使用add_weighted_edges_from函数)的时候,接受的是u、v、w的三元数组,因此我们还需要对格式进行转换,具体转换方式见代码。
|
||||
|
||||
**3.PR值计算及筛选**
|
||||
|
||||
我使用nx.pagerank(graph)计算了节点的PR值。由于节点数量很多,我们设置了PR值阈值,即pagerank_threshold=0.005,然后遍历节点,删除小于PR值阈值的节点,形成新的图small_graph,最后对small_graph进行可视化(对应运行结果的第二张图)。
|
||||
|
||||
## 总结
|
||||
|
||||
在上节课中,我们通过矩阵乘法求得网页的权重,这节课我们使用NetworkX可以得到相同的结果。
|
||||
|
||||
另外我带你用PageRank算法做了一次实战,我们将一个复杂的网络图,通过PR值的计算、筛选,最终得到了一张精简的网络图。在这个过程中我们学习了NetworkX工具的使用,包括创建图、节点、边及PR值的计算。
|
||||
|
||||
实际上掌握了PageRank的理论之后,在实战中往往就是一行代码的事。但项目与理论不同,项目中涉及到的数据量比较大,你会花80%的时间(或80%的代码量)在预处理过程中,比如今天的项目中,我们对别名进行了统一,对边的权重进行计算,同时还需要把计算好的结果以可视化的方式呈现。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/30/42/307055050e005ba5092028a074a5c142.png" alt=""><br>
|
||||
今天我举了一个网页权重的例子,假设一共有4个网页A、B、C、D。它们之间的链接信息如文章中的图示。我们假设用户有15%的概率随机跳转,请你编写代码重新计算这4个节点的PR值。
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="34丨AdaBoost(上):如何使用AdaBoost提升分类器性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/4a/d0fb67a24425fe85dbd3bd7a7389e64a.mp3"></audio>
|
||||
|
||||
今天我们学习AdaBoost算法。在数据挖掘中,分类算法可以说是核心算法,其中AdaBoost算法与随机森林算法一样都属于分类算法中的集成算法。
|
||||
|
||||
集成的含义就是集思广益,博取众长,当我们做决定的时候,我们先听取多个专家的意见,再做决定。集成算法通常有两种方式,分别是投票选举(bagging)和再学习(boosting)。投票选举的场景类似把专家召集到一个会议桌前,当做一个决定的时候,让K个专家(K个模型)分别进行分类,然后选择出现次数最多的那个类作为最终的分类结果。再学习相当于把K个专家(K个分类器)进行加权融合,形成一个新的超级专家(强分类器),让这个超级专家做判断。
|
||||
|
||||
所以你能看出来,投票选举和再学习还是有区别的。Boosting的含义是提升,它的作用是每一次训练的时候都对上一次的训练进行改进提升,在训练的过程中这K个“专家”之间是有依赖性的,当引入第K个“专家”(第K个分类器)的时候,实际上是对前K-1个专家的优化。而bagging在做投票选举的时候可以并行计算,也就是K个“专家”在做判断的时候是相互独立的,不存在依赖性。
|
||||
|
||||
## AdaBoost的工作原理
|
||||
|
||||
了解了集成算法的两种模式之后,我们来看下今天要讲的AdaBoost算法。
|
||||
|
||||
AdaBoost的英文全称是Adaptive Boosting,中文含义是自适应提升算法。它由Freund等人于1995年提出,是对Boosting算法的一种实现。
|
||||
|
||||
什么是Boosting算法呢?Boosting算法是集成算法中的一种,同时也是一类算法的总称。这类算法通过训练多个弱分类器,将它们组合成一个强分类器,也就是我们俗话说的“三个臭皮匠,顶个诸葛亮”。为什么要这么做呢?因为臭皮匠好训练,诸葛亮却不好求。因此要打造一个诸葛亮,最好的方式就是训练多个臭皮匠,然后让这些臭皮匠组合起来,这样往往可以得到很好的效果。这就是Boosting算法的原理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/b4/8e88b8a952d872ea46b7dd7c084747b4.jpg" alt=""><br>
|
||||
我可以用上面的图来表示最终得到的强分类器,你能看出它是通过一系列的弱分类器根据不同的权重组合而成的。
|
||||
|
||||
假设弱分类器为$G_{i}(x)$,它在强分类器中的权重$α_{i}$,那么就可以得出强分类器f(x):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/4f/58f7ff50e49f3cd96f6d4f0e590da04f.png" alt=""><br>
|
||||
有了这个公式,为了求解强分类器,你会关注两个问题:
|
||||
|
||||
<li>
|
||||
如何得到弱分类器,也就是在每次迭代训练的过程中,如何得到最优弱分类器?
|
||||
</li>
|
||||
<li>
|
||||
每个弱分类器在强分类器中的权重是如何计算的?
|
||||
</li>
|
||||
|
||||
我们先来看下第二个问题。实际上在一个由K个弱分类器中组成的强分类器中,如果弱分类器的分类效果好,那么权重应该比较大,如果弱分类器的分类效果一般,权重应该降低。所以我们需要基于这个弱分类器对样本的分类错误率来决定它的权重,用公式表示就是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/24/3242899fb2e4545f0aedaab7a9368724.png" alt=""><br>
|
||||
其中$e_{i}$代表第i个分类器的分类错误率。
|
||||
|
||||
然后我们再来看下第一个问题,如何在每次训练迭代的过程中选择最优的弱分类器?
|
||||
|
||||
实际上,AdaBoost算法是通过改变样本的数据分布来实现的。AdaBoost会判断每次训练的样本是否正确分类,对于正确分类的样本,降低它的权重,对于被错误分类的样本,增加它的权重。再基于上一次得到的分类准确率,来确定这次训练样本中每个样本的权重。然后将修改过权重的新数据集传递给下一层的分类器进行训练。这样做的好处就是,通过每一轮训练样本的动态权重,可以让训练的焦点集中到难分类的样本上,最终得到的弱分类器的组合更容易得到更高的分类准确率。
|
||||
|
||||
我们可以用$D_{k+1}$代表第k+1轮训练中,样本的权重集合,其中$W_{k+1,1}$代表第k+1轮中第一个样本的权重,以此类推$W_{k+1,N}$代表第k+1轮中第N个样本的权重,因此用公式表示为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/b6/d9b32e1d065e39861f266709640b2bb6.png" alt=""><br>
|
||||
第k+1轮中的样本权重,是根据该样本在第k轮的权重以及第k个分类器的准确率而定,具体的公式为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1a/58/1a6c650c3b7aa6d44cccf3b9dff81258.png" alt="">
|
||||
|
||||
## AdaBoost算法示例
|
||||
|
||||
了解AdaBoost的工作原理之后,我们看一个例子,假设我有10个训练样本,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/38/734c8272df1f96903be1777733a10f38.png" alt=""><br>
|
||||
现在我希望通过AdaBoost构建一个强分类器。
|
||||
|
||||
该怎么做呢?按照上面的AdaBoost工作原理,我们来模拟一下。
|
||||
|
||||
首先在第一轮训练中,我们得到10个样本的权重为1/10,即初始的10个样本权重一致,D1=(0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1)。
|
||||
|
||||
假设我有3个基础分类器:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/a4/325756eb08b5b3fd55402c9a8ba4dca4.png" alt=""><br>
|
||||
我们可以知道分类器f1的错误率为0.3,也就是x取值6、7、8时分类错误;分类器f2的错误率为0.4,即x取值0、1、2、9时分类错误;分类器f3的错误率为0.3,即x取值为3、4、5时分类错误。
|
||||
|
||||
这3个分类器中,f1、f3分类器的错误率最低,因此我们选择f1或f3作为最优分类器,假设我们选f1分类器作为最优分类器,即第一轮训练得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/fb/3dd329577aef1a810a1c130095a3e0fb.png" alt=""><br>
|
||||
根据分类器权重公式得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/60/f92e515d7ad7c1ee5f3bf45574bf3060.png" alt=""><br>
|
||||
然后我们对下一轮的样本更新求权重值,代入$W_{k+1,i}$和$D_{k+1}$的公式,可以得到新的权重矩阵:D2=(0.0715, 0.0715, 0.0715, 0.0715, 0.0715, 0.0715, 0.1666, 0.1666, 0.1666, 0.0715)。
|
||||
|
||||
在第二轮训练中,我们继续统计三个分类器的准确率,可以得到分类器f1的错误率为0.1666*3,也就是x取值为6、7、8时分类错误。分类器f2的错误率为0.0715*4,即x取值为0、1、2、9时分类错误。分类器f3的错误率为0.0715*3,即x取值3、4、5时分类错误。
|
||||
|
||||
在这3个分类器中,f3分类器的错误率最低,因此我们选择f3作为第二轮训练的最优分类器,即:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/40/687202173085a62e2c7b32deb05e9440.png" alt=""><br>
|
||||
根据分类器权重公式得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/8b/ce8a4e319726f159104681a4152e3a8b.png" alt=""><br>
|
||||
同样,我们对下一轮的样本更新求权重值,代入$W_{k+1,i}$和$D_{k+1}$的公式,可以得到D3=(0.0455,0.0455,0.0455,0.1667, 0.1667,0.01667,0.1060, 0.1060, 0.1060, 0.0455)。
|
||||
|
||||
在第三轮训练中,我们继续统计三个分类器的准确率,可以得到分类器f1的错误率为0.1060*3,也就是x取值6、7、8时分类错误。分类器f2的错误率为0.0455*4,即x取值为0、1、2、9时分类错误。分类器f3的错误率为0.1667*3,即x取值3、4、5时分类错误。
|
||||
|
||||
在这3个分类器中,f2分类器的错误率最低,因此我们选择f2作为第三轮训练的最优分类器,即:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/15/8847a9e60b38a79c08086e1620d6d915.png" alt=""><br>
|
||||
我们根据分类器权重公式得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/c3/0efb64e73269ee142cde91de532627c3.png" alt=""><br>
|
||||
假设我们只进行3轮的训练,选择3个弱分类器,组合成一个强分类器,那么最终的强分类器G(x) = 0.4236G1(x) + 0.6496G2(x)+0.7514G3(x)。
|
||||
|
||||
实际上AdaBoost算法是一个框架,你可以指定任意的分类器,通常我们可以采用CART分类器作为弱分类器。通过上面这个示例的运算,你体会一下AdaBoost的计算流程即可。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲了AdaBoost算法的原理,你可以把它理解为一种集成算法,通过训练不同的弱分类器,将这些弱分类器集成起来形成一个强分类器。在每一轮的训练中都会加入一个新的弱分类器,直到达到足够低的错误率或者达到指定的最大迭代次数为止。实际上每一次迭代都会引入一个新的弱分类器(这个分类器是每一次迭代中计算出来的,是新的分类器,不是事先准备好的)。
|
||||
|
||||
在弱分类器的集合中,你不必担心弱分类器太弱了。实际上它只需要比随机猜测的效果略好一些即可。如果随机猜测的准确率是50%的话,那么每个弱分类器的准确率只要大于50%就可用。AdaBoost的强大在于迭代训练的机制,这样通过K个“臭皮匠”的组合也可以得到一个“诸葛亮”(强分类器)。
|
||||
|
||||
当然在每一轮的训练中,我们都需要从众多“臭皮匠”中选择一个拔尖的,也就是这一轮训练评比中的最优“臭皮匠”,对应的就是错误率最低的分类器。当然每一轮的样本的权重都会发生变化,这样做的目的是为了让之前错误分类的样本得到更多概率的重复训练机会。
|
||||
|
||||
同样的原理在我们的学习生活中也经常出现,比如善于利用错题本来提升学习效率和学习成绩。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/00/10ddea37b3fdea2ec019f38b59ac6b00.png" alt=""><br>
|
||||
最后你能说说你是如何理解AdaBoost中弱分类器,强分类器概念的?另外,AdaBoost算法是如何训练弱分类器从而得到一个强分类器的?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<audio id="audio" title="35丨AdaBoost(下):如何使用AdaBoost对房价进行预测?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f8/be/f843e0c85e8c96488000c2c6a0ffffbe.mp3"></audio>
|
||||
|
||||
今天我带你用AdaBoost算法做一个实战项目。AdaBoost不仅可以用于分类问题,还可以用于回归分析。
|
||||
|
||||
我们先做个简单回忆,什么是分类,什么是回归呢?实际上分类和回归的本质是一样的,都是对未知事物做预测。不同之处在于输出结果的类型,分类输出的是一个离散值,因为物体的分类数有限的,而回归输出的是连续值,也就是在一个区间范围内任何取值都有可能。
|
||||
|
||||
这次我们的主要目标是使用AdaBoost预测房价,这是一个回归问题。除了对项目进行编码实战外,我希望你能掌握:
|
||||
|
||||
<li>
|
||||
AdaBoost工具的使用,包括使用AdaBoost进行分类,以及回归分析。
|
||||
</li>
|
||||
<li>
|
||||
使用其他的回归工具,比如决策树回归,对比AdaBoost回归和决策树回归的结果。
|
||||
</li>
|
||||
|
||||
## 如何使用AdaBoost工具
|
||||
|
||||
我们可以直接在sklearn中使用AdaBoost。如果我们要用AdaBoost进行分类,需要在使用前引用代码:
|
||||
|
||||
```
|
||||
from sklearn.ensemble import AdaBoostClassifier
|
||||
|
||||
```
|
||||
|
||||
我们之前讲到过,如果你看到了Classifier这个类,一般都会对应着Regressor类。AdaBoost也不例外,回归工具包的引用代码如下:
|
||||
|
||||
```
|
||||
from sklearn.ensemble import AdaBoostRegressor
|
||||
|
||||
```
|
||||
|
||||
我们先看下如何在sklearn中创建AdaBoost分类器。
|
||||
|
||||
我们需要使用AdaBoostClassifier(base_estimator=None, n_estimators=50, learning_rate=1.0, algorithm=’SAMME.R’, random_state=None)这个函数,其中有几个比较主要的参数,我分别来讲解下:
|
||||
|
||||
<li>
|
||||
base_estimator:代表的是弱分类器。在AdaBoost的分类器和回归器中都有这个参数,在AdaBoost中默认使用的是决策树,一般我们不需要修改这个参数,当然你也可以指定具体的分类器。
|
||||
</li>
|
||||
<li>
|
||||
n_estimators:算法的最大迭代次数,也是分类器的个数,每一次迭代都会引入一个新的弱分类器来增加原有的分类器的组合能力。默认是50。
|
||||
</li>
|
||||
<li>
|
||||
learning_rate:代表学习率,取值在0-1之间,默认是1.0。如果学习率较小,就需要比较多的迭代次数才能收敛,也就是说学习率和迭代次数是有相关性的。当你调整learning_rate的时候,往往也需要调整n_estimators这个参数。
|
||||
</li>
|
||||
<li>
|
||||
algorithm:代表我们要采用哪种boosting算法,一共有两种选择:SAMME 和SAMME.R。默认是SAMME.R。这两者之间的区别在于对弱分类权重的计算方式不同。
|
||||
</li>
|
||||
<li>
|
||||
random_state:代表随机数种子的设置,默认是None。随机种子是用来控制随机模式的,当随机种子取了一个值,也就确定了一种随机规则,其他人取这个值可以得到同样的结果。如果不设置随机种子,每次得到的随机数也就不同。
|
||||
</li>
|
||||
|
||||
那么如何创建AdaBoost回归呢?
|
||||
|
||||
我们可以使用AdaBoostRegressor(base_estimator=None, n_estimators=50, learning_rate=1.0, loss=‘linear’, random_state=None)这个函数。
|
||||
|
||||
你能看出来回归和分类的参数基本是一致的,不同点在于回归算法里没有algorithm这个参数,但多了一个loss参数。
|
||||
|
||||
loss代表损失函数的设置,一共有3种选择,分别为linear、square和exponential,它们的含义分别是线性、平方和指数。默认是线性。一般采用线性就可以得到不错的效果。
|
||||
|
||||
创建好AdaBoost分类器或回归器之后,我们就可以输入训练集对它进行训练。我们使用fit函数,传入训练集中的样本特征值train_X和结果train_y,模型会自动拟合。使用predict函数进行预测,传入测试集中的样本特征值test_X,然后就可以得到预测结果。
|
||||
|
||||
## 如何用AdaBoost对房价进行预测
|
||||
|
||||
了解了AdaBoost工具包之后,我们看下sklearn中自带的波士顿房价数据集。
|
||||
|
||||
这个数据集一共包括了506条房屋信息数据,每一条数据都包括了13个指标,以及一个房屋价位。
|
||||
|
||||
13个指标的含义,可以参考下面的表格:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/b7/426dec532f34d7f458e36ee59a6617b7.png" alt=""><br>
|
||||
这些指标分析得还是挺细的,但实际上,我们不用关心具体的含义,要做的就是如何通过这13个指标推导出最终的房价结果。
|
||||
|
||||
如果你学习了之前的算法实战,这个数据集的预测并不复杂。
|
||||
|
||||
首先加载数据,将数据分割成训练集和测试集,然后创建AdaBoost回归模型,传入训练集数据进行拟合,再传入测试集数据进行预测,就可以得到预测结果。最后将预测的结果与实际结果进行对比,得到两者之间的误差。具体代码如下:
|
||||
|
||||
```
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import mean_squared_error
|
||||
from sklearn.datasets import load_boston
|
||||
from sklearn.ensemble import AdaBoostRegressor
|
||||
# 加载数据
|
||||
data=load_boston()
|
||||
# 分割数据
|
||||
train_x, test_x, train_y, test_y = train_test_split(data.data, data.target, test_size=0.25, random_state=33)
|
||||
# 使用AdaBoost回归模型
|
||||
regressor=AdaBoostRegressor()
|
||||
regressor.fit(train_x,train_y)
|
||||
pred_y = regressor.predict(test_x)
|
||||
mse = mean_squared_error(test_y, pred_y)
|
||||
print("房价预测结果 ", pred_y)
|
||||
print("均方误差 = ",round(mse,2))
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
房价预测结果 [20.2 10.4137931 14.63820225 17.80322581 24.58931298 21.25076923
|
||||
27.52222222 17.8372093 31.79642857 20.86428571 27.87431694 31.09142857
|
||||
12.81666667 24.13131313 12.81666667 24.58931298 17.80322581 17.66333333
|
||||
27.83 24.58931298 17.66333333 20.90823529 20.10555556 20.90823529
|
||||
28.20877193 20.10555556 21.16882129 24.58931298 13.27619048 31.09142857
|
||||
17.08095238 26.19217391 9.975 21.03404255 26.74583333 31.09142857
|
||||
25.83960396 11.859375 13.38235294 24.58931298 14.97931034 14.46699029
|
||||
30.12777778 17.66333333 26.19217391 20.10206186 17.70540541 18.45909091
|
||||
26.19217391 20.10555556 17.66333333 33.31025641 14.97931034 17.70540541
|
||||
24.64421053 20.90823529 25.83960396 17.08095238 24.58931298 21.43571429
|
||||
19.31617647 16.33733333 46.04888889 21.25076923 17.08095238 25.83960396
|
||||
24.64421053 11.81470588 17.80322581 27.63636364 23.59731183 17.94444444
|
||||
17.66333333 27.7253886 20.21465517 46.04888889 14.97931034 9.975
|
||||
17.08095238 24.13131313 21.03404255 13.4 11.859375 26.19214286
|
||||
21.25076923 21.03404255 47.11395349 16.33733333 43.21111111 31.65730337
|
||||
30.12777778 20.10555556 17.8372093 18.40833333 14.97931034 33.31025641
|
||||
24.58931298 22.88813559 18.27179487 17.80322581 14.63820225 21.16882129
|
||||
26.91538462 24.64421053 13.05 14.97931034 9.975 26.19217391
|
||||
12.81666667 26.19214286 49.46511628 13.27619048 17.70540541 25.83960396
|
||||
31.09142857 24.13131313 21.25076923 21.03404255 26.91538462 21.03404255
|
||||
21.16882129 17.8372093 12.81666667 21.03404255 21.03404255 17.08095238
|
||||
45.16666667]
|
||||
均方误差 = 18.05
|
||||
|
||||
```
|
||||
|
||||
这个数据集是比较规范的,我们并不需要在数据清洗,数据规范化上花太多精力,代码编写起来比较简单。
|
||||
|
||||
同样,我们可以使用不同的回归分析模型分析这个数据集,比如使用决策树回归和KNN回归。
|
||||
|
||||
编写代码如下:
|
||||
|
||||
```
|
||||
# 使用决策树回归模型
|
||||
dec_regressor=DecisionTreeRegressor()
|
||||
dec_regressor.fit(train_x,train_y)
|
||||
pred_y = dec_regressor.predict(test_x)
|
||||
mse = mean_squared_error(test_y, pred_y)
|
||||
print("决策树均方误差 = ",round(mse,2))
|
||||
# 使用KNN回归模型
|
||||
knn_regressor=KNeighborsRegressor()
|
||||
knn_regressor.fit(train_x,train_y)
|
||||
pred_y = knn_regressor.predict(test_x)
|
||||
mse = mean_squared_error(test_y, pred_y)
|
||||
print("KNN均方误差 = ",round(mse,2))
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
决策树均方误差 = 23.84
|
||||
KNN均方误差 = 27.87
|
||||
|
||||
```
|
||||
|
||||
你能看到相比之下,AdaBoost的均方误差更小,也就是结果更优。虽然AdaBoost使用了弱分类器,但是通过50个甚至更多的弱分类器组合起来而形成的强分类器,在很多情况下结果都优于其他算法。因此AdaBoost也是常用的分类和回归算法之一。
|
||||
|
||||
## AdaBoost与决策树模型的比较
|
||||
|
||||
在sklearn中AdaBoost默认采用的是决策树模型,我们可以随机生成一些数据,然后对比下AdaBoost中的弱分类器(也就是决策树弱分类器)、决策树分类器和AdaBoost模型在分类准确率上的表现。
|
||||
|
||||
如果想要随机生成数据,我们可以使用sklearn中的make_hastie_10_2函数生成二分类数据。假设我们生成12000个数据,取前2000个作为测试集,其余作为训练集。
|
||||
|
||||
有了数据和训练模型后,我们就可以编写代码。我设置了AdaBoost的迭代次数为200,代表AdaBoost由200个弱分类器组成。针对训练集,我们用三种模型分别进行训练,然后用测试集进行预测,并将三个分类器的错误率进行可视化对比,可以看到这三者之间的区别:
|
||||
|
||||
```
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from sklearn import datasets
|
||||
from sklearn.metrics import zero_one_loss
|
||||
from sklearn.tree import DecisionTreeClassifier
|
||||
from sklearn.ensemble import AdaBoostClassifier
|
||||
# 设置AdaBoost迭代次数
|
||||
n_estimators=200
|
||||
# 使用
|
||||
X,y=datasets.make_hastie_10_2(n_samples=12000,random_state=1)
|
||||
# 从12000个数据中取前2000行作为测试集,其余作为训练集
|
||||
train_x, train_y = X[2000:],y[2000:]
|
||||
test_x, test_y = X[:2000],y[:2000]
|
||||
# 弱分类器
|
||||
dt_stump = DecisionTreeClassifier(max_depth=1,min_samples_leaf=1)
|
||||
dt_stump.fit(train_x, train_y)
|
||||
dt_stump_err = 1.0-dt_stump.score(test_x, test_y)
|
||||
# 决策树分类器
|
||||
dt = DecisionTreeClassifier()
|
||||
dt.fit(train_x, train_y)
|
||||
dt_err = 1.0-dt.score(test_x, test_y)
|
||||
# AdaBoost分类器
|
||||
ada = AdaBoostClassifier(base_estimator=dt_stump,n_estimators=n_estimators)
|
||||
ada.fit(train_x, train_y)
|
||||
# 三个分类器的错误率可视化
|
||||
fig = plt.figure()
|
||||
# 设置plt正确显示中文
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei']
|
||||
ax = fig.add_subplot(111)
|
||||
ax.plot([1,n_estimators],[dt_stump_err]*2, 'k-', label=u'决策树弱分类器 错误率')
|
||||
ax.plot([1,n_estimators],[dt_err]*2,'k--', label=u'决策树模型 错误率')
|
||||
ada_err = np.zeros((n_estimators,))
|
||||
# 遍历每次迭代的结果 i为迭代次数, pred_y为预测结果
|
||||
for i,pred_y in enumerate(ada.staged_predict(test_x)):
|
||||
# 统计错误率
|
||||
ada_err[i]=zero_one_loss(pred_y, test_y)
|
||||
# 绘制每次迭代的AdaBoost错误率
|
||||
ax.plot(np.arange(n_estimators)+1, ada_err, label='AdaBoost Test 错误率', color='orange')
|
||||
ax.set_xlabel('迭代次数')
|
||||
ax.set_ylabel('错误率')
|
||||
leg=ax.legend(loc='upper right',fancybox=True)
|
||||
plt.show()
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/35/8ad4bb6a8c6848f2061ff6f442568735.png" alt=""><br>
|
||||
从图中你能看出来,弱分类器的错误率最高,只比随机分类结果略好,准确率稍微大于50%。决策树模型的错误率明显要低很多。而AdaBoost模型在迭代次数超过25次之后,错误率有了明显下降,经过125次迭代之后错误率的变化形势趋于平缓。
|
||||
|
||||
因此我们能看出,虽然单独的一个决策树弱分类器效果不好,但是多个决策树弱分类器组合起来形成的AdaBoost分类器,分类效果要好于决策树模型。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你用AdaBoost回归分析对波士顿房价进行了预测。因为这是个回归分析的问题,我们直接使用sklearn中的AdaBoostRegressor即可。如果是分类,我们使用AdaBoostClassifier。
|
||||
|
||||
另外我们将AdaBoost分类器、弱分类器和决策树分类器做了对比,可以看出经过多个弱分类器组合形成的AdaBoost强分类器,准确率要明显高于决策树算法。所以AdaBoost的优势在于框架本身,它通过一种迭代机制让原本性能不强的分类器组合起来,形成一个强分类器。
|
||||
|
||||
其实在现实工作中,我们也能找到类似的案例。IBM服务器追求的是单个服务器性能的强大,比如打造超级服务器。而Google在创建集群的时候,利用了很多PC级的服务器,将它们组成集群,整体性能远比一个超级服务器的性能强大。
|
||||
|
||||
再比如我们讲的“三个臭皮匠,顶个诸葛亮”,也就是AdaBoost的价值所在。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/17/6c4fcd75a65dc354bc65590c18e77d17.png" alt=""><br>
|
||||
今天我们用AdaBoost分类器与决策树分类做对比的时候,使用到了sklearn中的make_hastie_10_2函数生成数据。实际上在[第19篇](http://time.geekbang.org/column/article/79072),我们对泰坦尼克号的乘客做生存预测的时候,也讲到了决策树工具的使用。你能不能编写代码,使用AdaBoost算法对泰坦尼克号乘客的生存做预测,看看它和决策树模型,谁的准确率更高?
|
||||
|
||||
你也可以把这篇文章分享给你的朋友或者同事,一起切磋一下。
|
||||
|
||||
|
||||
209
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/36丨数据分析算法篇答疑.md
Normal file
209
极客时间专栏/数据分析实战45讲/第二模块:数据分析算法篇/36丨数据分析算法篇答疑.md
Normal file
@@ -0,0 +1,209 @@
|
||||
<audio id="audio" title="36丨数据分析算法篇答疑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/dc/3a270c9d41162f04114d357a1dc5eedc.mp3"></audio>
|
||||
|
||||
算法篇更新到现在就算结束了,因为这一模块比较难,所以大家提出了形形色色的问题。我总结了同学们经常遇到的问题,精选了几个有代表性的来作为答疑。没有列出的问题,我也会在评论区陆续解答。
|
||||
|
||||
## 17-19篇:决策树
|
||||
|
||||
### 答疑1:在探索数据的代码中,print(boston.feature_names)有什么作用?
|
||||
|
||||
boston是sklearn自带的数据集,里面有5个keys,分别是data、target、feature_names、DESCR和filename。其中data代表特征矩阵,target代表目标结果,feature_names代表data对应的特征名称,DESCR是对数据集的描述,filename对应的是boston这个数据在本地的存放文件路径。
|
||||
|
||||
针对sklearn中自带的数据集,你可以查看下加载之后,都有哪些字段。调用方法如下:
|
||||
|
||||
```
|
||||
boston=load_boston()
|
||||
print(boston.keys())
|
||||
|
||||
```
|
||||
|
||||
通过boston.keys()你可以看到,boston数据集的字段包括了[‘data’, ‘target’, ‘feature_names’, ‘DESCR’, ‘filename’]。
|
||||
|
||||
### 答疑2:决策树的剪枝在sklearn中是如何实现的?
|
||||
|
||||
实际上决策树分类器,以及决策树回归器(对应DecisionTreeRegressor类)都没有集成剪枝步骤。一般对决策树进行缩减,常用的方法是在构造DecisionTreeClassifier类时,对参数进行设置,比如max_depth表示树的最大深度,max_leaf_nodes表示最大的叶子节点数。
|
||||
|
||||
通过调整这两个参数,就能对决策树进行剪枝。当然也可以自己编写剪枝程序完成剪枝。
|
||||
|
||||
### 答疑3:对泰坦尼克号的乘客做生存预测的时候,Carbin字段缺失率分别为77%和78%,Age和Fare字段有缺失值,是如何判断出来的?
|
||||
|
||||
首先我们需要对数据进行探索,一般是将数据存储到DataFrame中,使用df.info()可以看到表格的一些具体信息,代码如下:
|
||||
|
||||
```
|
||||
# 数据加载
|
||||
train_data = pd.read_csv('./Titanic_Data/train.csv')
|
||||
test_data = pd.read_csv('./Titanic_Data/test.csv')
|
||||
print(train_data.info())
|
||||
print(test_data.info())
|
||||
|
||||
```
|
||||
|
||||
这是运行结果:
|
||||
|
||||
```
|
||||
<class 'pandas.core.frame.DataFrame'>
|
||||
RangeIndex: 891 entries, 0 to 890
|
||||
Data columns (total 12 columns):
|
||||
PassengerId 891 non-null int64
|
||||
Survived 891 non-null int64
|
||||
Pclass 891 non-null int64
|
||||
Name 891 non-null object
|
||||
Sex 891 non-null object
|
||||
Age 714 non-null float64
|
||||
SibSp 891 non-null int64
|
||||
Parch 891 non-null int64
|
||||
Ticket 891 non-null object
|
||||
Fare 891 non-null float64
|
||||
Cabin 204 non-null object
|
||||
Embarked 889 non-null object
|
||||
dtypes: float64(2), int64(5), object(5)
|
||||
memory usage: 83.6+ KB
|
||||
None
|
||||
<class 'pandas.core.frame.DataFrame'>
|
||||
RangeIndex: 418 entries, 0 to 417
|
||||
Data columns (total 11 columns):
|
||||
PassengerId 418 non-null int64
|
||||
Pclass 418 non-null int64
|
||||
Name 418 non-null object
|
||||
Sex 418 non-null object
|
||||
Age 332 non-null float64
|
||||
SibSp 418 non-null int64
|
||||
Parch 418 non-null int64
|
||||
Ticket 418 non-null object
|
||||
Fare 417 non-null float64
|
||||
Cabin 91 non-null object
|
||||
Embarked 418 non-null object
|
||||
dtypes: float64(2), int64(4), object(5)
|
||||
memory usage: 36.0+ KB
|
||||
None
|
||||
|
||||
```
|
||||
|
||||
你可以关注下运行结果中Carbin的部分,你能看到在训练集中一共891行数据,Carbin有数值的只有204个,那么缺失率为1-204/891=77%,同样在测试集中一共有418行数据,Carbin有数值的只有91个,那么缺失率为1-91/418=78%。
|
||||
|
||||
同理你也能看到在训练集中,Age字段有缺失值。在测试集中,Age字段和Fare字段有缺失值。
|
||||
|
||||
### 答疑4:在用pd.read_csv时报错“UnicodeDecodeError utf-8 codec can’t decode byte 0xcf in position 15: invalid continuation byte”是什么问题?
|
||||
|
||||
一般在Python中遇到编码问题,尤其是中文编码出错,是比较常见的。有几个常用的解决办法,你可以都试一下:
|
||||
|
||||
<li>
|
||||
将read_csv中的编码改为gb18030,代码为:data = pd.read_csv(filename, encoding = ‘gb18030’)。
|
||||
</li>
|
||||
<li>
|
||||
代码前添加# -**- coding: utf-8 -**-。
|
||||
</li>
|
||||
|
||||
我说一下gb18030和utf-8的区别。utf-8是国际通用字符编码,gb18030是新出的国家标准,不仅包括了简体和繁体,也包括了一些不常见的中文,相比于utf-8更全,容错率更高。
|
||||
|
||||
为了让编辑器对中文更加支持,你也可以在代码最开始添加# -**- coding: utf-8 -**- 的说明,再结合其他方法解决编码出错的问题。
|
||||
|
||||
## 第20-21篇:朴素贝叶斯
|
||||
|
||||
### 答疑1:在朴素贝叶斯中,我们要统计的是属性的条件概率,也就是假设取出来的是白色的棋子,那么它属于盒子 A 的概率是 2/3。这个我算的是3/5,跟老师的不一样,老师可以给一下详细步骤吗?
|
||||
|
||||
不少同学都遇到了这个问题,我来统一解答下。
|
||||
|
||||
这里我们需要运用贝叶斯公式(我在文章中也给出了),即:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/a1/88f2981f938fac38980f1325fe7046a1.png" alt=""><br>
|
||||
假设A代表白棋子,B1代表A盒,B2代表B盒。带入贝叶斯公式,我们可以得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/63/8a/633e213385195fb958520f513a3d9f8a.png" alt=""><br>
|
||||
其中$P(B_{1})$代表A盒的概率,7个棋子,A盒有4个,所以$P(B_{1})$=4/7。
|
||||
|
||||
$P(B_{2})$代表B盒的概率,7个棋子,B盒有3个,所以$P(B_{2})$=3/7。
|
||||
|
||||
最终求取出来的是白色的棋子,那么它属于 A盒的概率$P(B_{1}|A)$= 2/3。
|
||||
|
||||
## 22-23篇:SVM算法
|
||||
|
||||
### 答疑1:SVM多分类器是集成算法么?
|
||||
|
||||
SVM算法最初是为二分类问题设计的,如果我们想要把SVM分类器用于多分类问题,常用的有一对一方法和一对多方法(我在文章中有介绍到)。
|
||||
|
||||
集成学习的概念你这样理解:通过构造和使用多个分类器完成分类任务,也就是我们所说的博取众长。
|
||||
|
||||
以上是SVM多分类器和集成算法的概念,关于SVM多分类器是否属于集成算法,我认为你需要这样理解。
|
||||
|
||||
在SVM的多分类问题中,不论是采用一对一,还是一对多的方法,都会构造多个分类器,从这个角度来看确实在用集成学习的思想,通过这些分类器完成最后的学习任务。
|
||||
|
||||
不过我们一般所说的集成学习,需要有两个基本条件:
|
||||
|
||||
<li>
|
||||
每个分类器的准确率要比随机分类的好,即准确率大于50%;
|
||||
</li>
|
||||
<li>
|
||||
每个分类器应该尽量相互独立,这样才能博采众长,否则多个分类器一起工作,和单个分类器工作相差不大。
|
||||
</li>
|
||||
|
||||
所以你能看出,在集成学习中,虽然每个弱分类器性能不强,但都可以独立工作,完成整个分类任务。而在SVM多分类问题中,不论是一对一,还是一对多的方法,每次都在做一个二分类问题,并不能直接给出多分类的结果。
|
||||
|
||||
此外,当我们谈集成学习的时候,通常会基于单个分类器之间是否存在依赖关系,进而分成Boosting或者Bagging方法。如果单个分类器存在较强的依赖关系,需要串行使用,也就是我们所说的Boosting方法。如果单个分类器之间不存在强依赖关系,可以并行工作,就是我们所说的Bagging或者随机森林方法(Bagging的升级版)。
|
||||
|
||||
所以,一个二分类器构造成多分类器是采用了集成学习的思路,不过在我们谈论集成学习的时候,通常指的是Boosing或者Bagging方法,因为需要每个分类器(弱分类器)都有分类的能力。
|
||||
|
||||
## 26-27篇:K-Means
|
||||
|
||||
### 答疑1:我在给20支亚洲球队做聚类模拟的时候,使用K-Means算法需要重新计算这三个类的中心点,最简单的方式就是取平均值,然后根据新的中心点按照距离远近重新分配球队的分类。对中心点的重新计算不太理解。
|
||||
|
||||
实际上是对属于这个类别的点的特征值求平均,即为新的中心点的特征值。
|
||||
|
||||
比如都属于同一个类别里面有10个点,那么新的中心点就是这10个点的中心点,一种简单的方式就是取平均值。比如文章中的足球队一共有3个指标,每个球队都有这三个指标的特征值,那么新的中心点,就是取这个类别中的这些点的这三个指标特征值的平均值。
|
||||
|
||||
## 28-29篇:EM聚类
|
||||
|
||||
### 答疑1:关于EM聚类初始参数设置的问题,初始参数随机设置会影响聚类的效果吗。会不会初始参数不对,聚类就出错了呢?
|
||||
|
||||
实际上只是增加了迭代次数而已。
|
||||
|
||||
EM算法的强大在于它的鲁棒性,或者说它的机制允许初始化参数存在误差。
|
||||
|
||||
举个例子,EM的核心是通过参数估计来完成聚类。如果你想要把菜平均分到两个盘子中,一开始A盘的菜很少,B盘的菜很多,我们只要通过EM不断迭代,就会让两个盘子的菜量一样多,只是迭代的次数多一些而已。
|
||||
|
||||
另外多说一句,我们学的这些数据挖掘的算法,不论是EM、Adaboost还是K-Means,最大的价值都是它们的思想。我们在使用工具的时候都会设置初始化参数,比如在K-Means中要选择中心点,即使一开始只是随机选择,最后通过迭代都会得到不错的效果。所以说学习这些算法,就是学习它们的思想。
|
||||
|
||||
## 30-31篇:关联规则挖掘
|
||||
|
||||
### 答疑1:看不懂构造FP树的过程,面包和啤酒为什么会拆分呢?
|
||||
|
||||
FP-Growth中有一个概念叫条件模式基。它在创建FP树的时候还用不上,我们主要通过扫描整个数据和项头表来构造FP树。条件模式基用于挖掘频繁项。通过找到每个项(item)的条件模式基,递归挖掘频繁项集。
|
||||
|
||||
### 答疑2:不怎么会找元素的XPath路径。
|
||||
|
||||
XPath的作用大家应该都能理解,具体的使用其实就是经验和技巧的问题。
|
||||
|
||||
我的方法就是不断尝试,而且XPath有自己的规则,绝大部分的情况下都是以//开头,因为想要匹配所有的元素。我们也可以找一些关键的特征来进行匹配,比如class='item-root’的节点,或者id='root’都是很好的特征。通过观察id或class,也可以自己编写XPath,这样写的XPath会更短。总之,都是要不断尝试,才能找到自己想要找的内容,寻找XPath的过程就是一个找规律的过程。
|
||||
|
||||
### 答疑3:最小支持度可以设置小一些,如果最小支持度小,那么置信度就要设置得相对大一点,不然即使提升度高,也有可能是巧合。这个参数跟数据量以及项的数量有关。理解对吗?
|
||||
|
||||
一般来说最小置信度都会大一些,比如1.0,0.9或者0.8。最小支持度和数据集大小和特点有关,可以尝试一些数值来观察结果,比如0.1,0.5。
|
||||
|
||||
## 34-35篇:AdaBoost算法
|
||||
|
||||
### 答疑1:关于$Z_{k}$和$y_{i}$的含义
|
||||
|
||||
第 k+1 轮的样本权重,是根据该样本在第 k 轮的权重以及第 k 个分类器的准确率而定,具体的公式为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/cd/1c812efcf6173652cf152f2ad25987cd.png" alt=""><br>
|
||||
其中$Z_{k}$, $y_{i}$代表什么呢?
|
||||
|
||||
$Z_{k}$代表规范化因子,我们知道第K+1轮样本的权重为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/c8/ce857425d5465209bf7cd2529e31e3c8.png" alt=""><br>
|
||||
为了让样本权重之和为1,我们需要除以规范化因子$Z_{k}$,所以:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/58/940d9b4b8889c668074e1dbaac275f58.png" alt=""><br>
|
||||
$y_{i}$代表的是目标的结果,我在AdaBoost工作原理之后,列了一个10个训练样本的例子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/ed/df33bd6ee148b1333b531252e5a936ed.png" alt=""><br>
|
||||
你能看到通常我们把X作为特征值,y作为目标结果。在算法篇下的实战练习中,我们一般会把训练集分成train_X和train_y,其中train_X代表特征矩阵,train_y代表目标结果。
|
||||
|
||||
我发现大家对工具的使用和场景比较感兴趣,所以最后留两道思考题。
|
||||
|
||||
第一道题是,在数据挖掘的工具里,我们大部分情况下使用的是sklearn,它自带了一些数据集,你能列举下sklearn自带的数据集都有哪些么?我在第18篇使用print(boston.feature_names)来查看boston数据集的特征名称(数据集特征矩阵的index名称),你能查看下其他数据集的特征名称都是什么吗?列举1-2个sklearn数据集即可。
|
||||
|
||||
第二个问题是,对于数据挖掘算法来说,基础就是数据集。Kaggle网站之所以受到数据科学从业人员的青睐就是因为有众多比赛的数据集,以及社区间的讨论交流。你是否有使用过Kaggle网站的经历,如果有的话,可以分享下你的使用经验吗?如果你是个数据分析的新人,当看到Kaggle网站时,能否找到适合初学者的kernels么(其他人在Kaggle上成功运行的代码分享)?
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user