目录索引
1.背景知识
在我们谈论决策树的时候我们先来玩一个游戏好咯。
2016年是奥运年,我最喜欢的两个运动员,(内心戏:当然是女的咯。因为我也是妹子,哈哈哈。)一个当然是女王隆达罗西,还有一个就是伊辛巴耶娃咯。
好的,现在我们就来玩猜运动员的游戏。
我在心里想一个运动员的名字,比如说就是伊辛巴耶娃。然后你有20次的提问机会,但是我只能回答你是还是不是这两种可能。
我们可以这样对话:
你:男的?
我:不是
你:参加过往届奥运会?
我:是
你:参加过两次?
我:不是
你:参加过三次?
我:是
你:参加的是田赛
我:是
你:耶辛巴伊娃
我:恭喜你,答对了!
以上我们玩的这个过程就有一点点像决策树算法。
我们经常使用决策树处理分类问题,近年来,决策树也是经常使用的数据挖掘的算法。
决策树的概念是非常简单的,我们可以通过一个图形来快速的了解决策数。我用了《机器学习与实战》这本书的内容来讲解。如图1所示,下面的这个流程图就是一个决策树,正方形代表的是判断模块(decision block),椭圆形代表的是终止模块(terminating block),表示已经得出结论,可以终止运行,从判断模块引出的左右箭头称作分支(branch)
这是一个假想的邮件分类系统。首先这个系统会检测发送邮件的域名地址,如果地址为myEmployer.com 则将邮件归类到“无聊时需要阅读的邮件”如果没有这个域名我们就检查邮件中的内容是不是包含了“曲棍球”的邮件。如果包含则把这些邮件放置在“需要及时处理的朋友邮件”,否则就把这些邮件归类到“无需阅读的垃圾邮件”
2.构造决策树
根据上面的描述我们已经发现构造决策树做分类的时候首要目的就是每次分类的时候都能找到最容易区分一个集合和另一个集合的特征。在上面例子中,我们首先就是查找邮件的域名,在第一次分类的时候,邮件的域名就是我们最重要的分类特征。
为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征,完成测试之后,原始数据集就被划分为几个数据子集。根据我们挑选出的最佳特征,这些数据会被分成两类。我们分别检测这两类,如果类别相同则不需要再次划分,如果类别不同,我们要重复上面的步骤。就是在被划分出的子集当中在挑选其他的重要特征把这些数据在细分成其他的类别。
根据这个描述,我们可以很容易发现这个过程就是一个递归的过程,怎么找到这些最佳特征,我们要做的事情就需要了解一些数学概念。我们需要用到信息论的知识来划分数据集。
3.一些需要了解的数学概念
划分数据集的原则就是:将无序的数据变得更加有序。我们可以有多种方法来划分数据,在这里我们构建决策树算法使用的是信息论划分数据集,然后编写代码将理论应用到具体的数据集上,然后编写代码构建决策树。我们在组织杂乱无章的数据时使用信息论度量信息。
比如我给出一条信息:
我爱你1314.
这就是一条简单的信息,这个时候我们可以对这个信息做一些分类,比如说找出这句话中的动词,数字,以及代词,名词等。我们要知道的就是 信息处理就是将杂乱无章的信息用数理统计的方法表示出来。
在这里我们可以将这个信息分成 三类: 代词(名词),动词,以及数字,我们用X1表示代词,X2表示动词,X3表示数字,p(xi)表示的是这个分类在这个信息当中出现的概率。那么我们就可以将信息定义为:
高中毕业的人都知道吧,概率p(Xi)是一个分数,然后对数函数以2为基底,它是比1大的,如果幂是分数,基底是大于1的那个这个值是个负数。为了方便处理,前面添加负号。
信息熵,简称熵是用来表示信息的期望值得。
3.1 信息熵
根据百科词条的定义,我们先来看一下信息论中的一下基本概念
信息论:
信息论是运用概率论与数理统计的方法研究信息、信息熵、通信系统、数据传输、密码学、数据压缩等问题的应用数学学科。信息系统就是广义的通信系统,泛指某种信息从一处传送到另一处所需的全部设备所构成的系统。
1948年,香农提出了“信息熵”的概念,解决了对信息的量化度量问题。信息熵这个词是C.E.香农从热力学中借用过来的。热力学中的热熵是表示分子状态混乱程度的物理量。香农用信息熵的概念来描述信源的不确定度。
我们可以用信息熵来度量信息量的多少。
在给出信息熵的计算公式的时候,我想先说几个基本的概念,以便于你理解信息熵的计算公式。
3.2随机变量
我们可以先看下面的一些问题。
某人射击一次,可能出现命中0环,命中1环…,命中10环等结果。即可能出现的结果可以由0,1,2,3,4,5,6,7,8,9,10这11个数来表示。
在某次的产品检验中,在可能含有次品的100件产品中任意抽取4件来检验,那么其中含有的次品可能的是0件,1件,2件,3件,4件,即可能出现的结果可以由0,1,2,3,4这5个数来表示。
我们把上面这些事件称为随机实验,随机实验想要得到的结果(例如射击一次命中的环数)可以用一个变量来表示的话,那么这样的变量就叫做随机变量(random variable)
随机变量的一些特征:
1. 可用数表示
2. 实验前可判断出所有可能取值
3. 实验前不能判断具体取哪个值
4. 所有可能值按照某种顺序列出
离散型随机变量
随机变量的取值是可以一一列出的比如上面所说的射击事件
连续型随机变量
那就是取值不能一一列出的咯,比如说一天内气温的变化量
推广:
一般地,如果X是随机变量,若有Y=f(X),则Y也是随机变量
3.3数学期望
在概率论中,数学期望简称期望,通俗的说就是平均值,它表示的是随机变量的取值的平均水平。
计算的公式
X1,X2,X3,……,Xn为这离散型随机变量,p(X1),p(X2),p(X3),……p(Xn)为这几个数据的概率函数。在随机出现的几个数据中p(X1),p(X2),p(X3),……p(Xn)概率函数就理解为数据X1,X2,X3,……,Xn出现的频率f(Xi).则:
E(X) = X1*p(X1) + X2*p(X2) + …… + Xn*p(Xn) = X1*f1(X1) + X2*f2(X2) + …… + Xn*fn(Xn)
上面的这个看着有点恶心,我们来温故一下当年高中数学课本中的东东,分分钟暴露了年龄的数学课本啊,但是还是很喜欢
(在这里我们主要考虑离散型随机变量)
总而言之,数学期望就是随机变量的取值乘以在随机实验中这个随机变量取到的概率。
推广一下
下面来举个例子
如果你已经理解了数学期望,随机变量这些概念那么我们就来说说信息熵的计算。
都说了熵是表示信息的期望值,信息的期望值,信息的期望值,如果您已经看懂了数学期望怎么算,那么你应该会很容易理解信息熵会怎么计算。
还是刚才那个例子,我们给出了一个信息:我爱你1314,然后把这个信息分为三类,然后我们要计算这个信息的熵。
那是不是就要计算这个信息所有类别的可能值得数学期望了。
那么熵的公式就是下面这个样子的:
其中n表示的是这个信息被分为n类。
4.决策树构建的一般流程
- 收集数据:任何你能收集数据的方法
- 准备数据: 决策树的算法只适用于标称型数据(可理解为离散型的,不连续的),因此数值型的数据(连续的数据)必须离散化。
- 分析数据: 可以使用任何方法,构造树完成之后,我们要检查图形是否符合预期。
- 训练算法:构造决策树的数据结构。
- 测试算法: 使用经验树计算错误率。
- 使用算法: 此步骤可以适用于任何监督学习算法,而使用决策数可以更好的理解数据的内在含义 (why? 对比于其他算法,比如说k均值算法,就是把给定的数据按照相似度分为一类,每一类表示什么你可能就不知道了。就像我们上一章讲的那个例子,可以用决策树做邮件的分类系统,我们可以根据分类标签知道这个邮件是垃圾邮件还是需要立刻处理的邮件)
5. 数据的构建
我们使用的例子还是《机器学习与实战》那本书上的例子。我把写作的思路和流程改了一下,还有这本书里好多错误,我好想帮作者重写这本书,或许不是作者的错误,是翻译和排版的错误。
首先我们第一步还是收集数据:
在这张表中我们可以发现这里有5个数据,这里有两个特征(要不要浮出水面生存,和是否有脚蹼)来划分这5个生物是鱼类还是非鱼类。
现在我们要做的就是是要根据第一个特征还是第二个特征来划分数据,进行分类。
我们使用python来构建我们的代码。
我们创建一个名为trees.py的python文件,然后在下面输入以下的代码
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']] # 我们定义了一个list来表示我们的数据集,这里的数据对应的是上表中的数据
labels = ['no surfacing','flippers']
return dataSet, labels
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
其中第一列的1表示的是不需要浮出水面就可以生存的,0则表示相反。 第二列同样是1表示有脚蹼,0表示的是没有。
这个时候我们来测试以下我们的数据集。
我用的是linux系统,我们打开一个终端来测试以下我们的数据。
我们创建完这个文件之后,进入到这个文件的目录下。我把这个文件保存在~/code 这个路径下。
我们输入python,进入shell命令,如下图所示
代码如下:
>>> import trees
>>> reload(trees)
<module 'trees' from 'trees.pyc'>
>>> myDat,labels=trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们来说说这段代码:
import的作用:
导入/引入一个python标准模块,其中包括.py文件、带有init.py文件的目录。
例如:
import module_name[,module1,...]
from module_name import *|child[,child1,...]
- 1
- 2
- 3
注意:
多次重复使用import语句时,不会重新加载被指定的模块,只是把对该模块的内存地址给引用到本地变量环境。
也就是说使用import的时候引用的module只会被加载一次,只会被加载一次,系统会把这个模块的地址给引用它的代码或者是这个python文件。
我们来测试以下
构建两个文件 a.py 和 b.py
其中a.py的代码如下:
#!/usr/bin/env python
# coding=utf-8
import os
print 'in a.py file'
print 'The address of os is:', id(os)
print '***end***'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我们在b.py中写如下代码:
#!/usr/bin/env python
# coding=utf-8
#filename: b.py
import a
import os
print "*****************"
print 'in b file'
print 'The adress of b file is:',id(os)
import a
print 'The adress of a module is:',id(a)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
这个时候,我们来测试以下结果:
我们在a,b两个文件中都引入了os 模块但是我们发现它的地址都没有改变。
reload
reload 的目的是为了开发期的 “edit and debug”/即编即调
reload 的作用:
对已经加载的模块进行重新加载,一般用于原模块有变化等特殊情况,reload前该模块必须已经import过。
e.g:
import os
reload(os)
- 1
- 2
说明:
reload会重新加载已加载的模块,但原来已经使用的实例还是会使用旧的模块,而新生产的实例会使用新的模块;reload后还是用原来的内存地址;不能支持from。。import。。格式的模块进行重新加载。
我们在举一个例子:
创建两个文件c.py, 以及文件 d.py
#!/usr/bin/env python
# coding=utf-8
# filename : c.py
import os
print 'in c.py file'
print 'The address of os is:', id(os)
print '***end***'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这个时候我们用d.py这个文件去引用c这个模块
#!/usr/bin/env python
# coding=utf-8
#filename: d.py
import c
import os
print "*****************"
print 'in d file'
print 'The adress of os is:',id(os)
print 'The address of c file is:',id(c)
print '*****reload******'
reload(c)
print '****reload*****'
print 'The adress of c module is:',id(c)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
接下来我们来测试一下结果:
可以发现reload和import的区别就是一个只能加载模块多次,一个可以加载一次
总之,我们的数据集就这么愉快的创建好了。
6.计算给定数据的信息熵
在决策树算法中最重要的目的我们已经在前几章说过了,就是根据信息论的方法找到最合适的特征来划分数据集。在这里,我们首先要计算所有类别的所有可能值的香农熵,根据香农熵来我们按照取最大信息增益(information gain)的方法划分我们的数据集。
我们的数据集如下表所示:
根据这张表,我们使用python来构建我们的数据集。
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']] # 我们定义了一个list来表示我们的数据集,这里的数据对应的是上表中的数据
labels = ['no surfacing','flippers']
return dataSet, labels
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
其中第一列的1表示的是不需要浮出水面就可以生存的,0则表示相反。 第二列同样是1表示有脚蹼,0表示的是没有。
在构建完数据集之后我们需要计算数据集的香农熵。
根据香农熵的定义可以知道:
根据这个公式我们来编写相应的代码。(注意:我们是计算每个类别的香农熵,也就是鱼类还是非鱼类的香农熵。在这我们的数据集当中我们用’yes’表示是鱼类,用‘no’表示非鱼类)
# 代码功能:计算香农熵
from math import log #我们要用到对数函数,所以我们需要引入math模块中定义好的log函数(对数函数)
def calcShannonEnt(dataSet):#传入数据集
# 在这里dataSet是一个链表形式的的数据集
countDataSet = len(dataSet) # 我们计算出这个数据集中的数据个数,在这里我们的值是5个数据集
labelCounts={} # 构建字典,用键值对的关系我们表示出 我们数据集中的类别还有对应的关系
for featVec in dataSet: 通过for循环,我们每次取出一个数据集,如featVec=[1,1,'yes']
currentLabel=featVec[-1] # 取出最后一列 也就是类别的那一类,比如说‘yes’或者是‘no’
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print labelCounts # 最后得到的结果是 {'yes': 2, 'no': 3}
shannonEnt = 0.0 # 计算香农熵, 根据公式
for key in labelCounts:
prob = float(labelCounts[key])/countDataSet
shannonEnt -= prob * log(prob,2)
return shannonEnt
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在python中我们使用 a=[]来定义一格list,我们使用a={}来定义一个字典。
例如:
website = {1:”google”,”second”:”baidu”,3:”facebook”,”twitter”:4}
>>>#用d.keys()的方法得到dict的所有键,结果是list
>>> website.keys()
[1, 'second', 3, 'twitter']
- 1
- 2
- 3
注意是d.keys()
>>>#用d.values()的方法得到dict的所有值,如果里面没有嵌套别的dict,结果是list
>>> website.values()
['google', 'baidu', 'facebook', 4]
>>>#用items()的方法得到了一组一组的键值对,
>>>#结果是list,只不过list里面的元素是元组
>>> website.items()
[(1, 'google'), ('second', 'baidu'), (3, 'facebook'), ('twitter', 4)]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
接下来我们来测试一下,通过 reload来测试
>>> import trees
>>> reload(trees)
<module 'trees' from 'trees.pyc'>
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> trees.calcShannonEnt(myDat)
{'yes': 2, 'no': 3}
0.9709505944546686
>>>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
我们贴出实验的完整代码:
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
def calcShannonEnt(dataSet):
countDataSet = len(dataSet)
labelCounts={}
for featVec in dataSet:
currentLabel=featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print labelCounts
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/countDataSet
shannonEnt -= prob * log(prob,2)
return shannonEnt
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
我们再把计算的过程总结一下,方便大家理解:
1.计算数据集中实例的总是,也就是样本的总数。我们把这个值保存成一格单独的变量以便之后方便使用,提高代码的效率
2.创建字典,用于保存类别信息。在整个数据集当中有多少个类别,每个类别的个数是多少
3. 在我们创建的数据字典中,它的键是我们数据集中最后一列的值。如果当前键不存在则把这个键加入到字典当中,依次统计出现类别的次数
4. 最后使用所有类标签对应的次数来计算它们的概论
5.计算香农熵
香农熵越高,则说明混合的数据越多,我们可以在数据集当中添加更多的分类,来观察一下熵是怎么变化的。
>>> myDat[0][-1]='maybe'
>>> myDat
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(myDat)
{'maybe': 1, 'yes': 1, 'no': 3}
1.3709505944546687
>>>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
对比一下可以发现熵增加了:
得到熵之后我们就可以按照获取最大信息增益的方法划分数据集。
7.划分数据集
7.1 基本概念
在度量数据集的无序程度的时候,分类算法除了需要测量信息熵,还需要划分数据集,度量花费数据集的熵,以便判断当前是否正确的划分了数据集。
我们将对每个特征数据集划分的结果计算一次信息熵,然后判断按照那个特征划分数据集是最好的划分方式。
也就是说,我们依次选取我们数据集当中的所有特征作为我们划定的特征,然后计算选取该特征时的信息增益,当信息增益最大时我们就选取对应信息增益最大的特征作为我们分类的最佳特征。
下面是我们的数据集:
我们用python语言表示出这个数据集
dataSet= [[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’], [0, 1, ‘no’], [0, 1, ‘no’]]
在这个数据集当中有两个特征,就是每个样本的第一列和第二列,最后一列是它们所属的分类。
我们划分数据集是为了计算根据那个特征我们可以得到最大的信息增益,那么根据这个特征来划分数据就是最好的分类方法。
因此我们需要遍历每一个特征,然后计算按照这种划分方式得出的信息增益。信息增益是指数据集在划分数据前后信息的变化量。
7.2 具体操作
划分数据集的方式我们首先选取第一个特征的第一个可能取值来筛选信息。然后再选取第一个特征的第二个可能的取值来划分我们的信息。之后我们再选取第二个特征的第一个可能的取值来划分数据集,以此类推。
e.g:
[[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’], [0, 1, ‘no’], [0, 1, ‘no’]]
这个是我们的数据集。
如果我们选取第一个特征值也就是需不需要浮到水面上才能生存来划分我们的数据,这里生物有两种可能,1就是需要,0就是不需要。那么第一个特征的取值就是两种。
如果我们按照第一个特征的第一个可能的取值来划分数据也就是当所有的样本的第一列取1的时候满足的样本,那就是如下三个:
[1, 1, ‘yes’], [1, 1, ‘yes’], [1, 0, ‘no’]
可以理解为这个特征为一条分界线,我们选取完这个特征之后这个特征就要从我们数据集中剔除,因为要把他理解为分界线。那么划分好的数据就是:
[[1, ‘yes’], [1, ‘yes’], [0, ‘no’]]
如果我们以第一个特征的第二个取值来划分数据集,那么得到的数据子集就是下面这个样子:
[[1, ‘no’], [1, ‘no’]]
因此我们可以很容易的来构建出我们的代码:
下面我们来分析一下这段代码,
# 代码功能:划分数据集
def splitDataSet(dataSet,axis,value): #传入三个参数第一个参数是我们的数据集,是一个链表形式的数据集;第二个参数是我们的要依据某个特征来划分数据集
retDataSet = [] #由于参数的链表dataSet我们拿到的是它的地址,也就是引用,直接在链表上操作会改变它的数值,所以我们新建一格链表来做操作
for featVec in dataSet:
if featVec[axis] == value: #如果某个特征和我们指定的特征值相等
#除去这个特征然后创建一个子特征
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
#将满足条件的样本并且经过切割后的样本都加入到我们新建立的样本中
retDataSet.append(reduceFeatVec)
return retDataSet
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
总的来说,这段代码的功能就是按照某个特征的取值来划分数据集。
为方便您测试实验我们在贴出这段代码:
def splitDataSet(dataSet,axis,value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在这里我们可以注意到一个关于链表的操作:
那就是extend 和append
它们的用法和区别如下所示:
下面我们再来测试一下我们的数据:
先给出实验的完整代码
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
def calcShannonEnt(dataSet):
countDataSet = len(dataSet)
labelCounts={}
for featVec in dataSet:
currentLabel=featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print labelCounts
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/countDataSet
shannonEnt -= prob * log(prob,2)
return shannonEnt
def splitDataSet(dataSet,axis,value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
我们输入下面代码测试一下,你可以在linux系统的终端中输入python或者是ipython 来进行测试:
In [1]: import trees
In [2]: reload(trees)
Out[2]: <module 'trees' from 'trees.pyc'>
In [3]: myDat,labels=trees.createDataSet()
In [4]: myDat
Out[4]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
In [5]: trees.splitDataSet(myDat,0,1)
Out[5]: [[1, 'yes'], [1, 'yes'], [0, 'no']]
In [6]: trees.splitDataSet(myDat,0,0)
Out[6]: [[1, 'no'], [1, 'no']]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
知道怎么划分数据集之后,我们接下来的工作就是遍历整个样本集合的特征值,然后循环计算香农熵,找到最好的特征划分方式。
8.计算信息增益
我们主要是要找到划分数据前后的最大信息增益,然后找到根据那个特征来做划分,分类出来的效果更好。
下面给出代码是怎么实现的
我们来分析一下代码:
注意: 在使用这段代码的时候我们对处理的数据是有要求的。
1.数据集必须是链表的表示出来的
2.数据集的每一个样本也是链表形式
3.数据集的每一个样本都必须是前面的所有列都是样本的特征值的取值范围,所有样本的最后一列是样本的类别。
4.每个样本的列数必须相同
首先我们的样本集合是:
dataSet = [[1,1,’yes’],
[1,1,’yes’],
[1,0,’no’],
[0,1,’no’],
[0,1,’no’]] # 我们定义了一个list来表示我们的数据集,这里的数据对应的是上表中的数据
我们的目的已经很明确了,就是依次遍历每一个特征,在这里我们的特征只有两个,就是需不需要浮出水面,有没有脚蹼。然后计算出根据每一个特征划分产生的数据集的熵,和初始的数据集的熵比较,我们找出和初始数据集差距最大的。那么这个特征就是我们划分时最合适的分类特征。
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1 # 获取我们样本集中的某一个样本的特征数(因为每一个样本的特征数是相同的,相当于这个代码就是我们可以作为分类依据的所有特征个数)我们的样本最后一列是样本所属的类别,所以要减去类别信息,在我们的例子中特征数就是2
baseEntropy = calcShannonEnt(dataSet) #计算样本的初始香农熵
bestInfoGain =0.0 #初始化最大信息增益
bestFeature = -1 #和最佳划分特征
for i in range(numFeatures): # range(2)那么i的取值就是0,1。 在这里i表示的我们的第几个特征
featList = [sample[i] for sample in dataSet]
# 我们首先遍历整个数据集,首先得到第一个特征值可能的取值,然后把它赋值给一个链表,我们第一个特征值取值是[1,1,1,0,0],其实只有【1,0】两个取值
uniqueVals = set(featList)#我们使用集合这个数据类型删除多余重复的原始使得其中只有唯一的值。
#执行的结果如下所示:
```
In [8]: featList=[1,1,1,0,0]
In [9]: uniqueVals=set(featList)
In [10]: uniqueVals
Out[10]: {0, 1}
```
newEntropy = 0.0
for value in uniqueVals: #uniqueVals中保存的是我们某个样本的特征值的所有的取值的可能性
subDataSet = splitDataSet(dataSet,i,value)
# 在这里划分数据集,比如说第一个特征的第一个取值得到一个子集,第一个特征的特征又会得到另一个特征。当然这是第二次循环
prob = len(subDataSet)/float(len(dataSet))#我们以第一个特征来说明,根据第一个特征可能的取值划分出来的子集的概率
newEntropy += prob * calcShannonEnt(subDataSet)# 这里比较难理解我们下面在详细说明
infoGain = baseEntropy - newEntropy # 计算出信息增益
#找出最佳信息增益,是个学计算机的小朋友都懂吧,哨兵法
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
被我标注的有点恶心,我在贴一遍代码,方便大家测试:
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain =0.0
bestFeature = -1
for i in range(numFeatures):
featList = [sample[i] for sample in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
难点
下面我们来重点解释一下刚才我们没有说的那句代码:
newEntropy += prob * calcShannonEnt(subDataSet)
为什么根据这个代码就可以计算出划分子集的熵了呢?
首先我们还是要回顾一下计算数学期望的方法,我们必须明确熵是信息的数学期望。
对于随机变量要计算它的数学期望我们是这样来计算的:
X | X1 | X2 |
---|---|---|
p | p1 | p2 |
E(x)=P1*X1 + P2 *X2
我么都以第一特征值来说明问题
同理,我们可以先计算出按照第一个特征值的第一个可能取值划分满足的子集的概率,和第一个特征值的第二个可能取值划分满足子集的概率。之后分别乘以它们子集计算出的香农熵。
香农熵 | H1 | H2 |
---|---|---|
特征值划分出子集概率 | p1 | p2 |
E(H)=H1×P1+H2×P2
e.g. 第一个特征为例
这是我们的数据集
我们按照第一个特征的第一个取值划分得到的数据集是:
得到了两个数据集,那么占总的数据集就是2/5
我们按照第一个特征的第二个取值划分得到的数据集是:
得到了三个数据集,那么占总的数据集就是3/5
我们分别来计算一下它们的香农熵
××××××
我们观察一下数据,也充分的验证了分类越多,香农熵会越大,当我们的分类只有一类是香农熵是0
××××××
我们采用列表法来计算一下熵:
某个特征的不同取值对应的香农熵 | 0.0 | 0.9182958340544896 |
---|---|---|
特征值划分出子集概率 | 0.4 | 0.6 |
根据期望的定义,我们在第一章讲过的公式,那么就可以计算出这个特征的信息熵
我们来测试一下结果:
In [1]: import trees
In [2]: reload(trees)
Out[2]: <module 'trees' from 'trees.pyc'>
In [3]: myDat,labels=trees.createDataSet()
In [4]: myDat
Out[4]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
In [5]: trees.chooseBestFeatureToSplit(myDat)
#总的数据分类
{'yes': 2, 'no': 3}
#按照第一个特征的第一个取值来划分
{'no': 2}
#按照第一个特征的第二个取值来划分
{'yes': 2, 'no': 1}
##按照第二个特征的第一个取值来划分
{'no': 1}
#按照第二个特征的第二个取值来划分
{'yes': 2, 'no': 2}
Out[5]: 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
我们可以看出计算的结果是根据第一个特征划分比较好。
这个其实也很明显了,我们可以观察一下我们的数据按照第一个特征来划分,当特征为1时生物分组有两个属于鱼类,一个属于非鱼类,另一个分组全部属于非鱼类。
如果按照第二个特征分类,一组中两个属于鱼类,两个属于非鱼来,另一组中只有一个是非鱼类。也就是按照第二特征来划分,错误率比较大。
完整的代码:
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
def calcShannonEnt(dataSet):
countDataSet = len(dataSet)
labelCounts={}
for featVec in dataSet:
currentLabel=featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print labelCounts
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/countDataSet
shannonEnt -= prob * log(prob,2)
return shannonEnt
def splitDataSet(dataSet,axis,value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain =0.0
bestFeature = -1
for i in range(numFeatures):
featList = [sample[i] for sample in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
9.特殊情况的处理
在之前的决策树算法中我们已经讲解了从数据集构造决策树算法的功能模块。
首先是创建数据集,然后计算香农熵,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分好之后,数据将被向下传递到树分支的一个节点,在这个节点上我们可以再次划分数据,所以我们可以采用递归的原则处理数据集。
递归的结束条件是:遍历完所有划分的数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一格叶子节点或者终止块。
根据特征来划分属性,我们知道每划分一次分类就会消耗一格特征值,如果我们使用完所有的特征但是类别还没有划分完那么我们就采用多数表决的方法来确定叶子节点。
比如说我们使用完所有的特征值之后划分,得到的最后的数据集是下面这个样子的:
[[‘yes’],[‘yes’],[‘maybe’]]
但是我们现在已经没有特征值了,那么我们就不能用计算香农熵的方法计算最大信息增益,这个时候就用投票表决的方式来分类。
那么我们发现 我们的数据集中 ‘yes’是两个,’maybe’是一个那么我们就按照这个来把它们分开。
下面我们来完成代码:
def majorityCnt(classList): # 传入的参数是已经划分完所有特征之后剩余的数据集,
#例如[['yes'],['yes'],['maybe']]
classCount={} #参数是已经划分完所有特征之后剩余的数据集,
#例如[['yes'],['yes'],['maybe']]
classCount={} #创建一个字典
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
# 根据上述的语句,以及我们的例子,我们最终可以得到的结果如下: {'yes':2,'maybe':1}
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1),reverse=True)#这个语句比较复杂,我们在下面详细讲解一下。
# 使用字典iteritems
return sortedClassCount[0][0]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
下面我们来分析在这段代码中比较复杂的代码:
sorted(classCount.iteritems(), key=operator.itemgetter(1),reverse=True)
- 1
在这里我们使用iteritems()这个函数得到我们字典中的所有元素,就是一组一组的键-值对。
之后我们定义一个叫做key的函数,这个名字可以任意取,大家都是这么定义的,之后我们通过itemgetter这个函数对我们字典中的元素进行排序。operator.itemgetter(1)表示按照元素的第二个进行排序,也就是分类出现的多少。我们的字典每一个元素都有两部分组成,也就是按照值来排序,reverse=True 表示按照递减的顺序来排序。
sortedClassCount[0][0] 表示的是按照分类的个数最多的元素的那个类。
10.递归构建决策树
之前我们已经学习了怎么根据信息论的方法,把一个数据集从杂乱无章的数据集中划分出来,我们使用信息论来构建决策树一级一级分类的方法就是一个递归的过程。
它的工作原理如下:
- 得到原始数据集,然后基于最好的属性值划分数据集。每一次划分数据集,我们都要消耗一个特征,根据某个特征将某些性质相同的元素剥离出来
- 划分数据的时候我们根据香农熵,计算信息增益之后找到最好的属性值进行数据的划分。
- 由于特征值可能有多于两个的,因此可能存在大于两个分支的数据集划分
- 第一次划分数据将向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据集,因此我们可以采用递归的原则来处理数据集。
我们都知道递归必须要有一个终止条件,1)如果程序已经遍历完了所有的特征属性,2)或者每个分支下的所有实例都具有相同的分类,我们得到一个叶子节点或者终止块.这个就是我们递归的终止条件.
出现1这种情况的特殊情况就是我们之前在决策树算法(五)——处理一些特殊的分类 这篇文中已经详细的分析过了.当已经遍历完所有的特征属性但是任然还有一些类别灭有找出,那么我们就根据选举投票的方法来进行分类.
当然对于第一个结束条件算法可以终止,我们还可以设置算法可以划分的最大分组的数目.
11.创建决策树代码
下面我们来构建决策树的代码,使用递归来进行.
我们还是打开我们之前的文件trees.py, 在这个文件中添加如下的代码:
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len (classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeatLabel])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
之后我们来分析一下这段代码:
还是用我们之前几章的那个数据集:
在这里任然需要注意我们的数据集是前面每一项都是特征值,最后一项是我们的类别信息.如下所示
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
- 1
- 2
- 3
- 4
- 5
#这里的第一条语句就是获得dataSet中的所有数据的类别:
classList = [example[-1] for example in dataSet]
#这种写法是python语法的一个特色,简单明了快捷随意.就是喜欢python这么随性,哈哈,像我.
# example中每次取出的是dataSet中的一个元素,e.g. [0,1,'no']
#example[-1] 就是每个元素的最后一列.
- 1
- 2
- 3
- 4
- 5
我们来看下执行结果:
之后的两个if条件是递归终止条件.
if classList.count(classList[0]) == len (classList):
return classList[0]
# 这个条件语句是表示所有的数据都已经划分完成,每个类别已经完全相同
#这样递归可以结束
#count()函数中接受一个参数,表示的是这个参数在某个序列中出现的次数
#如果这个classList中的元素完全相同,那么这个参数的count(classList[0])应该是等于这个List的长度的.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
if len(dataSet[0]) == 1:
return majorityCnt(classList)
# 第二个递归条件表示的只剩最后的类别信息的数据集.
#因为决策树算法每做一次信息的划分,都会消耗一个特征,当特征
#消耗完之后还有类别不同那么我们就需要投票表决了
- 1
- 2
- 3
- 4
- 5
看这张图应该很清楚了.
注意 :上面这两个条件语句都是我们递归结束的条件.
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
#之后我们调用chooseBestFeatureToSplit函数
- 1
- 2
- 3
chooseBestFeatureToSplit函数的原型如下(我们在之前已经讲过):
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain =0.0
bestFeature = -1
for i in range(numFeatures):
featList = [sample[i] for sample in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
这个函数 返回的是最好的特征值,通过计算最大信息增益获得的.
# bestFeat 存储的最好的特征的下标.它和我们的label是一一对应的
bestFeatLabel = labels[bestFeat]
- 1
- 2
- 3
在这里我们可以看出,我们的数据集有两个特征,就是no surfing 和 flippers . 每个数据集的第一列表示的是有还是不需要no surfing, 第二列表示的有没有flippers. 1表示有,2表示没有.
我们第一次调用chooseBestFeatureToSplit函数,结果告诉我们选择第一个特征比较好
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
# bestFeatLabel中存储了最佳特征的标签
myTree = {bestFeatLabel:{}} # 构建数据字典
del(labels[bestFeatLabel])# 删除最佳特征值
- 1
- 2
- 3
- 4
- 5
- 6
- 7
#找出最佳特征向量对应的所有特征值
featValues = [example[bestFeat] for example in dataSet]
# 除去重复的特征值
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
#递归调用构建决策树
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
我们来测试一下我们的实验结果.
mytree 包含了很多代表结构信息的嵌套字典. 在代码中也可以看到我们实际上是用一个数据字典来构建我们的决策树.
第一个 no surfing 是第一个划分数据集的特征名称,在其下面有分为两类,特征是0的不是鱼类,是1的有被继续划分了.
到这里我们决策树算法算是讲完了,我们贴出整个分类的完整代码.
#!/usr/bin/env python
# coding=utf-8
# author: chicho
# running: python trees.py
# filename : trees.py
from math import log
import operator
def createDataSet():
dataSet = [[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
def calcShannonEnt(dataSet):
countDataSet = len(dataSet)
labelCounts={}
for featVec in dataSet:
currentLabel=featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print labelCounts
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/countDataSet
shannonEnt -= prob * log(prob,2)
return shannonEnt
def splitDataSet(dataSet,axis,value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain =0.0
bestFeature = -1
for i in range(numFeatures):
featList = [sample[i] for sample in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len (classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeatLabel])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
写在后面的话
糟糕,我还爱他,还是会想他
连给幸福一个机会我都不会
你必须非常努力,才可以看起来毫不费力
要么就不做,要做就做最好
本文来自一位漂亮的小姐姐博客,写的还不错。 小姐姐博客