机器学习系列文章目录
文章目录
十五节
关于分类算法,我的讲解已经告一段落,从这一小节开始,我们进入聚类算法的学习。不知道你是否对
前面讲解的 “什么是聚类问题” 还有印象,我在这里再简单介绍一下。
聚类算法属于无监督学习,与分类算法这种有监督学习不同的是,聚类算法事先并不需要知道数据的类
别标签,而只是根据数据特征去学习,找到相似数据的特征,然后把已知的数据集划分成几个不同的类
别。
比如说我们有一堆树叶,对于分类问题来说,我们已经知道了过去的每一片树叶的类别。比如这个是枫
树叶,那个是橡树叶,经过学习之后拿来一片新的叶子,你看了一眼,然后说这是枫树叶。而对于聚类
问题,这里一堆树叶的具体类别你是不知道的,所以你只能学习,这个叶子是圆的,那个是五角星形
的;这个边缘光滑,那个边缘有锯齿…… 这样你根据自己的判定,把一箱子树叶分成了几个小堆,但是
这一堆到底是什么树叶你还是不知道的。
一个例子
今天我要介绍的聚类算法称为 K-means 算法。首先我还是讲一个小例子来介绍一下这个方法的思路。
假设我们在罪恶都市里,有三个区域,每个区域有一个帮派进行管理。每个帮派都有一个大佬,每个大
佬都管理着一群小弟,小弟们也有不同的等级。大佬给高级小弟安排任务,高级小弟再给低级小弟安排
任务,而低级小弟们负责具体实施。有些小弟可能就在自己的区域活动,管理本区域内的店铺、保障本
区域的治安;有些小弟可能会负责跟其他两个帮派联络、洽谈地盘、交易等业务。
这个时候来了个国民警卫队要整治这个区域,所以他们希望能够把帮派的关系理清楚,但是有那么多人
该从哪里入手呢?最好的方案当然是先把大佬抓到,然后看一下他都联系谁就一目了然了。但是国民警
卫队也不知道谁是大佬,那要怎么办?
假设国民警卫队已经知道这里面有三个帮派,那么国民警卫队派人在每个区设一个点,先随便抓一个
人,最开始可能抓到的只是一个边缘小弟,甚至有一些可能抓到的两个是同一个帮派。但是没关系,先
假设他是大佬,看跟他联系密切的都是哪些人,然后再从这些人里找一个跟其他人联系更密切的人。就
这样反复寻找,最后终于找到每个帮派的大佬,而大佬联系的那些人自然就是这个帮派的小弟了。
算法原理
上面的小故事可以看到一些 K-means 的思想。接着我们来具体介绍一下算法的原理。假设我们的数据
总共有 m 条,我们计划分为 3 个类别。如果我们的数据有两个特征维度,那我们的数据就分布在一个
二维平面上,如果有十个维度,就分布在一个十维的空间中。
第一轮,先随机在这个空间中选取三个点,我们称之为中心点,当然选取的三个点不一定是实际的数据
点。接着计算所有的点到这三个点的距离,这里的距离计算仍然使用的是欧氏距离,每个点都选择距离
最近的那个作为自己的中心点。这个时候我们就已经把数据划分成了三个组。使用每个组的数据计算出
这些数据的一个均值,使用这个均值作为下一轮迭代的中心点。
后面若干轮重复上面的过程进行迭代,当达到一些条件,比如说规定的轮次或者中心点的变动很小等,
就可以停止运行了。
K-means 的算法原理就已经解释完了,也是非常简洁、易于理解,但是这里面有一些问题需要解决。
如何确定 k 值
在算法实现的过程中,我们面临的问题就是如何确定 K 值。因为在日常的情况下,我们也不知道这些数
据到底会有多少个类别,或者分为多少个类别会比较好,所以在选择 K 值的时候比较困难,只能根据经
验先拍一个数值。
有一个比较常用的方法,叫作手肘法。就是去循环尝试 K 值,计算在不同的 K 值情况下,所有数据的损
失,即用每一个数据点到中心点的距离之和计算平均距离。可以想到,当 K=1 的时候,这个距离和肯定
是最大的;当 K=m 的时候,每个点也是自己的中心点,这个时候全局的距离和是 0,平均距离也是 0,
当然我们不可能设置成 K=m。
而在逐渐加大 K 的过程中,会有一个点,使这个平均距离发生急剧的变化,如果把这个距离与 K 的关系
画出来,就可以看到一个拐点,也就是我们说的手肘。
如下图,我在这里虚拟了一份数据,可以看到在 K=4 的时候就是我们的肘点,在这个肘点前平均距离下
降迅速,在 4 之后平均距离下降变得缓慢。但是这个方法只能适用 K 值不那么大的情况,如果 K 值较
大,如几千几万,那迭代的次数就太多了,当然你也可以选择一个比较大的学习率来加以改进。不过总
体而言,需要消耗一定的时间。
要确定 K 值确实是一项比较费时费力的事情,但是也是必须要做的事情。下面我们来看看这个算法的优
缺点。
算法优缺点
优点
简洁明了,计算复杂度低。 K-means 的原理非常容易理解,整个计算过程与数学推理也不是很困
难。
收敛速度较快。 通常经过几个轮次的迭代之后就可以获得还不错的效果。
缺点
结果不稳定。 由于初始值随机设定,以及数据的分布情况,每次学习的结果往往会有一些差异。
无法解决样本不均衡的问题。 对于类别数据量差距较大的情况无法进行判断。
容易收敛到局部最优解。 在局部最优解的时候,迭代无法引起中心点的变化,迭代将结束。
受噪声影响较大。 如果存在一些噪声数据,会影响均值的计算,进而引起聚类的效果偏差。
尝试动手
和前面一样,在对 K-means 算法有了一定了解之后,我们来动手尝试通过代码来实际感受 K-means 算
法的效果。这次我们使用的仍然是鸢尾花数据集,当然,由于是聚类,我们不需要使用标签数据,只需
要使用特征数据就可以了。
通过运行上面的代码,会输出下面的这幅图像,当然,我们的鸢尾花数据集的属性有四个维度,这里输
出的图像我们只使用了两个维度,但是仍然可以看出通过 K-means 计算出的中心点与数据分布基本上
是一致的,而且效果也还不错。
扩展内容
做完了实践,我们再来看一下 K-means 都有什么样的衍生方法。由于 K-means 也是一种非常不错的方
法,所以有很多人为了改正它存在的一些问题进行了相应的研究。
K-means++
第一种是 K-means++,这种方法主要在初始选取中心点的时候进行了优化。原本第一轮是随机进行选
取的,但是由于算法可能会陷入局部最优解,随机地选取可能引起结果的不稳定。K-means++ 则是从
已有的数据中随机地进行多次选取 K 个中心点,每次都计算这一次选中的中心点的距离,然后取一组最
大的作为初始化中心点。
mini batch K-means
第二种 mini batch 方法,主要是基于在数据量和数据维度都特别大的情况下,针对运算变得异常缓慢
的问题进行的改进。我们前面提到,K-means 的收敛速度相对较快,所以前面几步的变动比较大,到
了后面的步骤其实只有非常小的变动。mini batch 的方案就是在迭代时,不再使用所有的点,而是每个
集合中选取一部分点进行计算,从而降低计算的复杂度。
总结
写到这里,本课时的主要内容已经告一段落。这节课我们进入了新的算法类型——聚类算法的学习。在
开头我又简单介绍了一下什么是聚类算法,聚类与分类有什么样的区别,接着就讲到了本节课的主角
——K-means 算法,它是一种非常简洁的基于划分的聚类算法。与前面一样,在介绍完算法的思想之
后我加入了一段代码来实现快速上手,并且加入了一个画图的方法来展示聚类的效果。
在看完了这一课时的内容之后,你是否能在自己的工作中使用 K-means 来解决问题了呢?下一课时,
我们将介绍另外一种聚类算法 “DBScan”,到时见。
十六节
一个例子
想象有一个很大的广场,上面种了很多的鲜花和绿草。快要到国庆节了,园丁要把上面的鲜花和绿草打
造成四个字:欢度国庆。于是园丁开始动手,用绿草作为背景去填补空白的区域,用红色的鲜花摆成文
字的形状,鲜花和绿草之间都要留下至少一米的空隙,让文字看起来更加醒目。
国庆节过后,园丁让他的大侄子把这些花和草收起来运回仓库,可是大侄子是红绿色盲,不能通过颜色
来判断,这些绿草和鲜花的面积又非常大,没有办法画出一个区域来告知大侄子。这可怎么办呢?
想来想去,园丁一拍脑袋跟大侄子说:“你就从一个位置开始收,只要跟它连着的距离在一米以内的,
你就摞在一起;如果是一米以外的,你就再重新放一堆。” 大侄子得令,开开心心地去收拾花盆了。最
后呢,大侄子一共整理了三堆花盆:所有的绿草盆都摞在一起,“国” 字用的红花摞在一起,“庆” 字用的
红花摞在了一起。这就是一个关于密度聚类的例子
算法原理
上面的例子看起来比较简单,但是在算法的处理上我们首先有个问题要处理,那就是如何去衡量密度。
在 DBSCAN 中,衡量密度主要使用两个指标,即半径和最少样本量。
对于一个已知的点,以它为中心,以给定的半径画一个圆,落在这个圆内的就是与当前点比较紧密的
点;而如果在这个圆内的点达到一定的数量,即达到最少样本量,就可以认为这个区域是比较稠密的。
在算法的开始,要给出半径和最少样本量,然后对所有的数据进行初始化,如果一个样本符合在它的半
径区域内存在大于最少样本量的样本,那么这个样本就被标记为核心对象。
(算法步骤2:首选任意选取一个点,然后找到到这个点距离小于等于 eps 的所有的点。如果距起始点的距离在 eps 之内的数据点个数小于 min_samples,那么这个点被标记为噪声。如果距离在 eps 之内的数据点个数大于 min_samples,则这个点被标记为核心样本,并被分配一个新的簇标签。
然后访问该点的所有邻居(在距离 eps 以内)。如果它们还没有被分配一个簇,那么就将刚刚创建的新的簇标签分配给它们。如果它们是核心样本,那么就依次访问其邻居,以此类推。簇逐渐增大,直到在簇的 eps 距离内没有更多的核心样本为止。
选取另一个尚未被访问过的点,并重复相同的过程。)
这里我画了一幅图,假设我们的最小样本量为 6,那么这里面的 A、B、C 为三个核心对象
对于在整个样本空间中的样本,可以存在下面几种关系:
直接密度可达: 如果一个点在核心对象的半径区域内,那么这个点和核心对象称为直接密度可达,比如
上图中的 A 和 B 、B 和 C 等。
密度可达: 如果有一系列的点,都满足上一个点到这个点是密度直达,那么这个系列中不相邻的点就称
为密度可达,比如 A 和 D。
密度相连: 如果通过一个核心对象出发,得到两个密度可达的点,那么这两个点称为密度相连,比如这
里的 E 和 F 点就是密度相连。
一口气介绍了这么多概念,其实对照着图片都很好理解的,我们再来看 DBSCAN 接下来的处理步骤。
经过了初始化之后,再从整个样本集中去抽取样本点,如果这个样本点是核心对象,那么从这个点出
发,找到所有密度可达的对象,构成一个簇。
如果这个样本点不是核心对象,那么再重新寻找下一个点。
不断地重复这个过程,直到所有的点都被处理过。
这个时候,我们的样本点就会连成一片,也就变成一个一个的连通区域,其中的每一个区域就是我们所
获得的一个聚类结果。
当然,在结果中也有可能存在像 G 一样的点,游离于其他的簇,这样的点称为异常点。
DBSCAN 的原理你只是看字面解释的话可能会有点迷惑,最好结合图片来进行理解,自己手动画一下
图,来分析一下上面的几种概念,应该就比较容易理解了。接下来我们看看它都有哪些优缺点。
算法优缺点
优点
不需要划分个数。 跟 K-means 比起来,DBSCAN 不需要人为地制定划分的类别个数,而可以通
过计算过程自动分出。
可以处理噪声点。 经过 DBSCAN 的计算,那些距离较远的数据不会被记入到任何一个簇中,从而
成为噪声点,这个特色也可以用来寻找异常点。
可以处理任意形状的空间聚类问题。 从我们的例子就可以看出来,与 K-means 不同,DBSCAN
可以处理各种奇怪的形状,只要这些数据够稠密就可以了。
缺点
需要指定最小样本量和半径两个参数。 这对于开发人员极其困难,要对数据非常了解并进行很好
的数据分析。而且根据整个算法的过程可以看出,DBSCAN 对这两个参数十分敏感,如果这两个
参数设定得不准确,最终的效果也会受到很大的影响。
数据量大时开销也很大。 在计算过程中,需要对每个簇的关系进行管理。所以当数据量大的话,
内存的消耗也非常严重。
如果样本集的密度不均匀、聚类间距差相差很大时,聚类质量较差。
关于算法的优缺点就先介绍这么多,在使用的过程中十分要注意的就是最小样本量和半径这两个参数,
最好预先对数据进行一些分析,来加强我们的判断。下面我们进入到动手环节,用代码来实现 DBSCAN
的使用。
尝试动手
今天我们使用的数据集不再是鸢尾花数据集,我们要使用 datasets 的另外一个生成数据的功能。
在下面的代码中可以看到,我调用了 make_moons 这个方法,在 sklearn 的官网上,我们可以看到关
于这个方法的介绍:生成两个交错的半圆环,从下面的生成图像我们也能够看到,这里生成的数据结
果,是两个绿色的半圆形。
此外,我们今天调用的聚类方法是 sklearn.cluster 中的 dbscan。
from sklearn import datasets import pandas as pd import matplotlib.pyplot as plt from sklearn.cluster import dbscan #今天使用的新算法包 #生成500个点 噪声为0.1 X, _ = datasets.make_moons(500, noise=0.1, random_state=1) df = pd.DataFrame(X, columns=[‘x’, ‘y’]) df.plot.scatter(‘x’,‘y’, s = 200,alpha = 0.5, c = “green”, title = ‘dataset by DBSCAN’) plt.show()
使用上面的方法,会生成一份数据,我们最后还调用了 plot 方法把数据绘制出来,就是下图所显示的
样子,就像两个弯弯的月亮互相缠绕在一起。
接下来,我们就开始使用 dbscan 算法来进行聚类运算。可以看到我为 dbscan 算法配置了初始的邻域
半径和最少样本量。
eps为邻域半径,min_samples为最少样本量 core_samples, cluster_ids = dbscan(X, eps=0.2, min_samples=20) # cluster_ids中-1表示对应的点为噪声 df = pd.DataFrame(np.c_[X, cluster_ids], columns=[‘x’, ‘y’, ‘cluster_id’]) df[‘cluster_id’] = df[‘cluster_id’].astype(‘i2’) #绘制结果图像 df.plot.scatter(‘x’, ‘y’, s=200, c=list(df[‘cluster_id’]), cmap=‘Reds’, colorbar=False, alpha=0.6, title=‘DBSCAN cluster result’) plt.show()
最后,我们使用不同的颜色来标识聚类的结果,从图上可以看出有两个大类,也就是两个月亮的形状被
聚类算法算了出来。
但是眼尖的同学可能看到,在月亮两头的区域有一些非常浅色的点,跟两个类别的颜色都不一样,这里
就是最后产生的噪声点,根据我们设置的参数计算,这些点不属于任何一个类别。
总结
完成了动手环节,这节课的主要内容就介绍完了。这节课我们学习了聚类算法的第二个方法 “DBSCAN”
算法。它是基于密度的聚类方法,与前面讲的 K-means 不同的是,它可以很好地解决数据形状不规则
的情况。
在算法原理环节,有几个概念需要你仔细去理解,只要明白了那几个概念,DBSCAN 算法的核心也就可
以掌握了。
总体来讲,DBSCAN 是一个比较简单明了的算法,没有太多复杂的数学运算,但是在实践中要想用好
DBSCAN 却不是十分容易,这主要是因为两个初始化参数比较难以设定,对于新手来说可能会有些困
难,但是不要怕,你终会成长为一名有经验的数据挖掘工程师的。
十七节
在第一个实践课(使用 XGB 实现酒店信息消歧)中其实没有涉及太多的代码,主要是以介绍思路为
主。在这一课时中,我将提供一个较为完整的代码,带领你亲自实践一下。
理解业务
在旅行场景下,城市——我们通常称为目的地,是一个很重要的信息。根据用户对于目的地的偏好,我
们既可以把目的地作为一个特征用于推荐系统中,也可以把目的地当作一个被推荐的信息直接推荐给用
户。所以,我们有一个需求,就是把相似的目的地整理出来,然后可以通过这些相似目的地做相关推
荐,或者是相关目的地的推荐。
理解数据
可以想到,这是一个比较典型的聚类问题,我们只要能够把相似的城市按照一定的相关性聚在一起,就
可以完成我们的需求,当然具体的效果要根据结果不断地进行调整。
那我们就来看一下我们的数据。
思来想去我们只有很多目的地的名字,但这些目的地并没有什么统一标准的特征可以给我们做向量,那
么该怎么去给这些目的地计算相关性呢?
这时不禁想到,我们有很多用户写过游记,这些游记里总会出现各种各样的目的地名字,对于相似的目
的地,那用户所写的内容也会有一定的相似性,不管是地理位置接近,还是消费价位类似,或者是可以
玩的内容存在一定的相似性。
总之,我们可以靠这些内容把这些城市的名字关联起来,而且不同于结构化的信息,游记是用户自己来
写的内容,里面对于目的地的认知也是用户的认知,所以如果我们能够从中发现关联性,再应用到用户
身上也是比较合理的。比如说 “三亚” 如果只是按客观属性来划分,那应该是“海边”,但是很多用户去三
亚,除了看海本身,还有家庭出游等,这些是只能从用户的角度才会产生的认知。
这里,我们就要用到一个 Word2Vec 算法,它可以学习输入的文本,并输出一个词向量模型,经过
Word2Vec 算法处理之后,每一个词都会变成一个预设长度的数值向量。这个算法会在后面的章节进行
更详细的讲解,这里我们大概知道它的功能就可以了。下面我们进入到具体代码实现的环节,看看如何
训练一个这样的模型。
准备数据与模型训练
准备数据
我们获取所有需要用到的文本数据,在这里使用了全量的游记文本数据。我们首先要对数据进行清洗,
去除掉异常的数据,比如内容过短、获取失败,或者是存在特殊字符、使用纯英文 / 泰语写的游记,等
等。
完成了这个步骤之后我们要对文本内容进行分词,因为我期望 Word2Vec 最终构建的向量是词级别的。
完成分词之后,我们把数据存储在文本文件中,其中每一行是一篇内容。
接下来就要训练我们的 Word2Vec 模型了。
训练 Word2Vec 模型
这里我们使用了一个新的算法包:Gensim。不知道你是否还记得我在之前介绍过这个工具包,它主要
用于从原始的非结构化文本信息中,通过无监督算法学习文本向量表达。这里面支持 TF-IDF、LSA、
LDA 和 Word2Vec 等多种算法模型。来看一下代码。
import gensim
import os
import re
import sys
import multiprocessing
from time import time
class getSentence(object):
def __init__(self, dirname):
self.dirname = dirname
文本可以存储在多个文本文件中,存放在一个文件目录下,这里构建了一个迭代方法,循环读取目录下
的所有文件。
我这里使用的文件目录为 traindata,在 traindata 下面有 31 个语料文件,其中每个有 1G 左右,如下
图所示。
def __iter__(self):
for root, dirs, files in os.walk(self.dirname):# 用于通过在目录树中游走输出在目录中的文件名,向上或者向下。
for filename in files:
file_path = root + '/' + filename
for line in open(file_path):
try:
s_line = line.strip()#用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
if s_line == "":
continue
word_line = [word for word in s_line.split()]
yield word_line
except Exception:
print("catch exception")
yield ""
if __name__ == '__main__':
begin = time()
sentences = getSentence("traindata")
model = gensim.models.Word2Vec(sentences, size=200, window=15, min_count=10, workers=multiprocessing.cpu_count())
model.save("model/word2vec_gensim")
model.wv.save_word2vec_format("model/word2vec_org", "model/vocabulary", binary=False)
end = time()
print("Total procesing time: %d seconds" % (end - begin))
在正常的情况下,我们会在 model 路径下看到几个文件。其中比较重要的两个,一个 vocabulary 是词
典文件,记录了出现过的词汇以及词汇出现的次数;一个 word2vec_gensim 是生成的向量文件。
通过上面的方法,我们成功获取到了很多词汇的向量,这里我的词汇量大概有 1000w 左右。但是我们
这次所需要的是寻找相似城市,所以对于那些非城市名字的词汇就没有什么价值了。
于是我们这里使用我们自己的城市词库与词汇表进行匹配,对于没有在词汇表中出现过的城市名称也没
有办法计算,要把这部分剔除掉。不用担心,如果这么多的语料都没有出现过的城市也一定是没有人去
过的城市
训练 K-means 模型
下面我们就可以开始训练我们的 K-means 模型了。像我们前面用过的一样,K-means 是在 sklearn 里
面的一个模块。具体步骤如下所示。
import gensim
from sklearn.cluster import KMeans
from sklearn.externals import joblib
from time import time
def load_model():
model = gensim.models.Word2Vec.load('../word2vec/model/word2vec_gensim')
return model
def load_filterword():
fd = open("mddwords.txt", "r")
filterword = []
for line in fd.readlines():
line = line.strip()
filterword.append(line)
return filterword
if __name__ == "__main__": start = time()
model = load_model()
filterword = load_filterword()
print(len(filterword))
wordvector = []
filterkey = {}
for word in filterword: wordvector.append(model[word])
filterkey[word] = model[word]
print(len(wordvector))
clf = KMeans(n_clusters=2000, max_iter=100, n_jobs=10)
s = clf.fit_predict(wordvector)
joblib.dump(clf, "kmeans_mdd2000.pkl")
labels = clf.labels_
labellist = labels.tolist()
print(clf.inertia_)
fp = open("label_mdd2000", 'w')
fp.write(str(labellist))
fp.close()
fp1 = open("keys_mdd2000", 'w')
for key in filterkey: fp1.write(key + '\n')
print("over")
end = time()
print("use time")
print(end - start)
经过上面的步骤,我们就训练好了 K-means 模型,当然,经过反复尝试,最终确定的不是 2000 这个
簇数量,而是使用了 100 个簇的结果。我们尝试了 50、100、200、500、1000、2000 等多个聚类的
结果,经过我们最后的对比评估,100 个簇的时候效果较好,于是我们最终选择了这个模型。
下图是我从结果中抽了一些簇的 TOP 结果生成的图片,可以看到聚类的效果还是很不错的。比如右下
角那一簇基本都是日本关西的城市名字,左下角基本都是川藏线上的地点。
有了已经训练好的模型,我们就知道了这些相似城市的名称以及它们所属的簇。接下来我们要做的,就
是把这些数据存储到数据库中,并在具体的业务中进行应用了。
当然,随着时间的推移,在积累了一段时间的数据之后,我们还要对模型进行重新迭代,以期望获得更
好的结果。
总结
在这一节实践课程中,我着重介绍了整个模型训练环节的代码,其中主要写了两段代码,分别训练了
Word2Vec 模型和 K-means 模型。除了数据部分,这些代码几乎可以复制即运行。
到这一课时,关于聚类问题的内容就告一段落了,在数据缺少标注的时候,聚类算法是十分常用的,它
可以帮助我们了解数据情况。当然,聚类方法也存在一些局限,还需要在日常的工作中多加练习,不断
积累自己的经验。