This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View 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>
集合16次都去打篮球
</li>
<li>
集合24次去打篮球2次不去打篮球
</li>
<li>
集合33次去打篮球3次不去打篮球。
</li>
按照纯度指标来说集合1&gt;集合2&gt;集合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>
集合15次去打篮球1次不去打篮球
</li>
<li>
集合23次去打篮球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-于是我们可以用下面的方式来记录D1D2D3
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/7D2在父节点的概率是2/7D3在父节点的概率是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="">
如果你觉得这篇文章有所价值,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View 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概率平方和。
通过下面这个例子,我们计算一下两个集合的基尼系数分别为多少:
集合16个都去打篮球
集合23个去打篮球3个不去打篮球。
针对集合1所有人都去打篮球所以p(Ck|t)=1因此GINI(t)=1-1=0。
针对集合2有一半人去打篮球而另一半不去打篮球所以p(C1|t)=0.5p(C2|t)=0.5GINI(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(&quot;CART分类树准确率 %.4lf&quot; % 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>
最后给你留两道思考题吧你能说下ID3C4.5以及CART分类树在做节点划分时的区别吗第二个问题是sklearn中有个手写数字数据集调用的方法是load_digits()你能否创建一个CART分类树对手写数字数据集做分类另外选取一部分测试集统计下分类树的准确率
欢迎你在评论下面留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起交流。

View 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())
```
运行结果:
```
&lt;class 'pandas.core.frame.DataFrame'&gt;
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模型预测&amp;评估**
在预测中我们首先需要得到测试集的特征值矩阵然后使用训练好的决策树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.9820cross_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函数那么这两个函数有什么区别
欢迎你在评论区留言与我分享你的答案,也欢迎点击“请朋友读”把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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(B1A=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的性别判断性别的过程就是一个分类的过程。根据以往的经验我们通常会从身高、体重、鞋码、头发长短、服饰、声音等角度进行判断。这里的“经验”就是一个训练好的关于性别判断的模型其训练数据是日常中遇到的各式各样的人以及这些人实际的性别数据。
**离散数据案例**
我们遇到的数据可以分为两种一种是离散数据另一种是连续数据。那什么是离散数据呢离散就是不连续的意思有明确的边界比如整数123就是离散数据而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/4P(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)&gt;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>
这三种概率分别属于先验概率、后验概率和条件概率的哪一种?
欢迎在评论区分享你的答案,我也会和你一起讨论。如果你觉得这篇文章对你有帮助,欢迎分享给你的朋友,一起来交流。

View 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个文档
文档1this is the bayes document
文档2this is the second second document
文档3and the third one
文档4is 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&lt;alpha&lt;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>
请使用朴素贝叶斯分类对训练集进行训练,并对测试集进行验证,并给出测试集的准确率。
最后你不妨思考一下,假设我们要判断一个人的性别,是通过身高、体重、鞋码、外貌等属性进行判断的,如果我们用朴素贝叶斯做分类,适合使用哪种朴素贝叶斯分类器?停用词的作用又是什么?
欢迎你在评论区进行留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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也就是我们想要找的最优超平面。中间求解的过程会用到拉格朗日乘子和KKTKarush-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作为正集BCD作为负集
2样本B作为正集ACD作为负集
3样本C作为正集ABD作为负集
4样本D作为正集ABC作为负集。
这种方法针对K个分类需要训练K个分类器分类速度较快但训练速度较慢因为每个分类器都需要对全部样本进行训练而且负样本数量远大于正样本数量会造成样本不对称的情况而且当增加新的分类比如第K+1类时需要重新对分类器进行构造。
2.一对一法
一对一法的初衷是想在训练的时候更加灵活。我们可以在任意两类样本之间构造一个SVM这样针对K类的样本就会有C(k,2)类分类器。
比如我们想要划分A、B、C三个类可以构造3个分类器
1分类器1A、B
2分类器2A、C
3分类器3B、C。
当对一个未知样本进行分类时每一个分类器都会有一个分类结果即为1票最终得票最多的类别就是整个未知样本的类别。
这样做的好处是如果新增一类不需要重新训练所有的SVM只需要训练和新增这一类样本的分类器。而且这种方式在训练单个SVM模型的时候训练速度快。
但这种方法的不足在于分类器的个数与K的平方成正比所以当K较大时训练和测试的时间会比较慢。
## 总结
今天我给你讲了SVM分类器它在文本分类尤其是针对二分类任务性能卓越。同样针对多分类的情况我们可以采用一对多或者一对一的方法多个二值分类器组合成一个多分类器。
另外关于SVM分类器的概念我希望你能掌握以下的三个程度
<li>
完全线性可分情况下的线性分类器也就是线性可分的情况是最原始的SVM它最核心的思想就是找到最大的分类间隔
</li>
<li>
大部分线性可分情况下的线性分类器,引入了软间隔的概念。软间隔,就是允许一定量的样本分类错误;
</li>
<li>
线性不可分情况下的非线性分类器,引入了核函数。它让原有的样本空间通过核函数投射到了一个高维的空间中,从而变得线性可分。
</li>
在SVM的推导过程中有大量的数学公式这里不进行推导演绎因为除了写论文你大部分时候不会用到这些公式推导。
所以最重要的还是理解我上面讲的这些概念能在实际工作中使用SVM才是最重要的。下一节我会和你讲如何用sklearn工具包进行SVM分类带你做一个实际的案例。
最后你能说一下你对有监督学习和无监督学习的理解吗以及SVM最主要的思想就是硬间隔、软间隔和核函数。你是如何理解它们的
欢迎你在评论区进行留言,与我分享你的答案。也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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>
sigmoidsigmoid核函数
</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(&quot;./data.csv&quot;)
# 数据探索
# 因为数据集中列比较多我们需要把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(&quot;id&quot;,axis=1,inplace=True)
# 将B良性替换为0M恶性替换为1
data['diagnosis']=data['diagnosis'].map({'M':1,'B':0})
```
然后我们要做特征字段的筛选首先需要观察下features_mean各变量之间的关系这里我们可以用DataFrame的corr()函数,然后用热力图帮我们可视化呈现。同样,我们也会看整体良性、恶性肿瘤的诊断情况。
```
# 将肿瘤诊断结果可视化
sns.countplot(data['diagnosis'],label=&quot;Count&quot;)
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使用又有什么样的体会呢
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来交流,一起来进步。

View 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="">
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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(&quot;KNN准确率: %.4lf&quot; % 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(&quot;多项式朴素贝叶斯准确率: %.4lf&quot; % accuracy_score(test_y, predict_y))
# 创建CART决策树分类器
dtc = DecisionTreeClassifier()
dtc.fit(train_mm_x, train_y)
predict_y = dtc.predict(test_mm_x)
print(&quot;CART决策树准确率: %.4lf&quot; % 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中的手写数字数据集再跑一遍程序看看分类器的准确率是多少
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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-shiftDBSCANSpectral 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[[&quot;2019年国际排名&quot;,&quot;2018世界杯&quot;,&quot;2015亚洲杯&quot;]]
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该如何编写程序呢运行的结果又是如何
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View File

@@ -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(&quot;L&quot;, (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(&quot;weixin_mark.jpg&quot;, &quot;JPEG&quot;)
```
代码中有一些参数,我来给你讲解一下这些参数的作用和设置方法。
我们使用了fit和predict这两个函数来做数据的训练拟合和预测因为传入的参数是一样的我们可以同时进行fit和predict操作这样我们可以直接使用fit_predict(data)得到聚类的结果。得到聚类的结果label后实际上是一个一维的向量我们需要把它转化成图像尺寸的矩阵。label的聚类结果是从0开始统计的当聚类数为2的时候聚类的标识label=0或者1。
如果你想对图像聚类的结果进行可视化直接看0和1是看不出来的还需要将0和1转化为灰度值。灰度值一般是在0-255的范围内我们可以将label=0设定为灰度值255label=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)
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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 LikelihoodLikelihood代表可能性所以最大似然也就是最大可能性的意思。
什么是最大似然呢?举个例子,有一男一女两个同学,现在要对他俩进行身高的比较,谁会更高呢?根据我们的经验,相同年龄下男性的平均身高比女性的高一些,所以男同学高的可能性会很大。这里运用的就是最大似然的概念。
最大似然估计是什么呢?它指的就是一件事情已经发生了,然后反推更有可能是什么因素造成的。还是用一男一女比较身高为例,假设有一个人比另一个人高,反推他可能是男性。最大似然估计是一种通过已知结果,估计参数的方法。
那么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重复上面的计算过程可以推理出来硬币顺序应该是{AABBA}。
这个过程实际上是通过假设的参数来估计未知参数,即“每次投掷是哪枚硬币”。
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聚类的相同和不同之处又有哪些
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来交流。

View 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分数。
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View File

@@ -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)&gt;1代表有提升
</li>
<li>
提升度(A→B)=1代表有没有提升也没有下降
</li>
<li>
提升度(A→B)&lt;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的工作原理吗相比于AprioriFP-Growth算法都有哪些改进
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事,一起来学习。

View 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}}
[{啤酒} -&gt; {尿布}, {牛奶} -&gt; {尿布}, {面包} -&gt; {尿布}, {牛奶, 面包} -&gt; {尿布}]
```
你能从代码中看出来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+'&amp;cat=1002&amp;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(&quot;//*&quot;).get_attribute(&quot;outerHTML&quot;)
html = etree.HTML(html)
# 设置电影名称,导演演员 的XPATH
movie_lists = html.xpath(&quot;/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']&quot;)
name_lists = html.xpath(&quot;/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='meta abstract_2']&quot;)
# 获取返回的数据个数
num = len(movie_lists)
if num &gt; 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 &gt;= 14: #有可能一页会有14个电影
# 继续下一页
return True
else:
# 没有下一页
return False
# 开始的ID为0每页增加15
start = 0
while start&lt;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。我用页面返回的数据个数来判断当前所处的页面序号。如果数据个数&gt;15也就是第一页第一页的第一条数据是广告我们需要忽略。如果数据个数=15代表是中间页需要点击“下一页”也就是翻页。如果数据个数&lt;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}}
[{徐峥} -&gt; {黄渤}]
```
你能看出来,宁浩导演喜欢用徐峥和黄渤,并且有徐峥的情况下,一般都会用黄渤。你也可以用上面的代码来挖掘下其他导演选择演员的规律。
## 总结
Apriori算法的核心就是理解频繁项集和关联规则。在算法运算的过程中还要重点掌握对支持度、置信度和提升度的理解。在工具使用上你可以使用efficient-apriori这个工具包它会把每一条数据中的项item放到一个集合篮子里来处理不考虑项item之间的先后顺序。
在实际运用中你还需要灵活处理,比如导演如何选择演员这个案例,虽然工具的使用会很方便,但重要的还是数据挖掘前的准备过程,也就是获取某个导演的电影数据集。
<img src="https://static001.geekbang.org/resource/image/28/9d/282c25e8651b3e0b675be7267d13629d.png" alt=""><br>
最后给你留两道思考题吧。请你编写代码挖掘下张艺谋导演使用演员的频繁项集和关联规则最小支持度可以设置为0.1或0.05。另外你认为Apriori算法中的最小支持度和最小置信度一般设置为多少比较合理
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View File

@@ -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.33330.22220.22220.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都有哪些应用场景吗
欢迎在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View 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 = [(&quot;A&quot;, &quot;B&quot;), (&quot;A&quot;, &quot;C&quot;), (&quot;A&quot;, &quot;D&quot;), (&quot;B&quot;, &quot;A&quot;), (&quot;B&quot;, &quot;D&quot;), (&quot;C&quot;, &quot;A&quot;), (&quot;D&quot;, &quot;B&quot;), (&quot;D&quot;, &quot;C&quot;)]
for edge in edges:
G.add_edge(edge[0], edge[1])
pagerank_list = nx.pagerank(G, alpha=1)
print(&quot;pagerank值是&quot;, 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.csvEmails.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(&quot;./input/Emails.csv&quot;)
# 读取别名文件
file = pd.read_csv(&quot;./input/Aliases.csv&quot;)
aliases = {}
for index, row in file.iterrows():
aliases[row['Alias']] = row['PersonId']
# 读取人名文件
file = pd.read_csv(&quot;./input/Persons.csv&quot;)
persons = {}
for index, row in file.iterrows():
persons[row['Id']] = row['Name']
# 针对别名进行转换
def unify_name(name):
# 姓名统一小写
name = str(name).lower()
# 去掉, 和 @后面的内容
name = name.replace(&quot;,&quot;,&quot;&quot;).split(&quot;@&quot;)[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 =&gt; 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'] &lt; 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值。
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View File

@@ -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算法是如何训练弱分类器从而得到一个强分类器的
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。

View File

@@ -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(&quot;房价预测结果 &quot;, pred_y)
print(&quot;均方误差 = &quot;,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(&quot;决策树均方误差 = &quot;,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(&quot;KNN均方误差 = &quot;,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算法对泰坦尼克号乘客的生存做预测看看它和决策树模型谁的准确率更高
你也可以把这篇文章分享给你的朋友或者同事,一起切磋一下。

View 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())
```
这是运行结果:
```
&lt;class 'pandas.core.frame.DataFrame'&gt;
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
&lt;class 'pandas.core.frame.DataFrame'&gt;
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 cant 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算法
### 答疑1SVM多分类器是集成算法么
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.00.9或者0.8。最小支持度和数据集大小和特点有关可以尝试一些数值来观察结果比如0.10.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上成功运行的代码分享)
欢迎你在评论区与我分享你的答案,也欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。