由先前的对于机器学习概念的简单介绍,可以知道,聚类属于非监督学习,它是将一些数据划分为几个群组的操作,从一组数据中找寻某种结构。各个群组内的数据项都是紧密相关的,群组内外的数据是不相关的。
这里我主要想讲述两个聚类算法:1、分级聚类;2、K-均值聚类。
类似的,所有的算法都需要为其构造所需要的数据结构,在这里我们以博客的聚类为例。首先我们需要获取数据, 这里我用爬虫获取新浪博客的一些RSS订阅源地址。由这些RSS订阅源地址可以得到对应博客用户的几篇博客文章。然后根据这两种聚类算法对这些博客用户进行聚类处理。
首先说一下获取数据的过程:直接上代码,这里主要用了两个模块,其中一个模块用了一个可以很好处理RSS订阅源的模块feedparser。其中第一个模块,主要是利用feedparser来处理RSS订阅源地址的,从RSS订阅源地址中获取相应博客的文章;第二个模块,主要是从新浪博客首页获取大量的RSS订阅源地址。利用这两个模块,把每个博客用户的文章分别存到一个txt文件中。
为了构造出聚类算法所需的数据结构,除去这两个模块,还需要对博客数据进行中文分词处理,中文分词网上有很多关于中科院分词系统的代码,这里就不介绍了。
# -*- coding: utf-8 -*- import feedparser import re # 返回一个RSS订阅源的所有文章标题以及文本内容 def gettxt(url): # 解析RSS订阅源 d = feedparser.parse(url) blogcontent = {} # 存放RSS订阅源中所有 文章的名称:文章内容 # 循环遍历所有文章条目 for e in d.entries: # print(e.title) if 'summary' in e: summary = e.summary else: summary = e.description # 去除html标签 txt = re.compile(r'<[^>]+>').sub('', summary) txt1 = re.compile(' ').sub('', txt) # 其中的' '是html里的空格占位符 blogcontent[e.title] = txt1 return d.feed.title, blogcontent url = "http://blog.sina.com.cn/rss/1658067891.xml" title, blogcontent = gettxt(url)
# -*- coding: utf-8 -*- ''' 本模块的总体思路: 输出(目的):大量新浪博客的RSS订阅源地址。 输入:新浪博客首页地址————http://blog.sina.com.cn/ 思路:由新浪博客首页可以获取大量的博客地址bloglink, 由这些博客地址bloglink可以构造出RSS订阅源地址blog_RSSlink ''' import urllib2 import re import generateFeedVector import codecs def getblog_RSSlink(url): # 获取新浪博客首页的源代码 html = urllib2.urlopen(url) data = html.read() # print(data) # 提取首页源代码中的博客地址links # <div id="bloglink" class="bloglink"><a href="http://blog.sina.com.cn/zenglixia">http://blog.sina.com.cn/zenglixia</a> link_pat = '"(http://blog.sina.com.cn/s/blog_.*?)"' links = re.compile(link_pat).findall(data) # for link in bloglinks: # print(link) # 提取所有博客links里的bloglink bloglinks = [] for link in links: # 获取links每个link的源代码 html_link = urllib2.urlopen(link) data_link = html_link.read() # 提取bloglink的正则表达式 bloglink_pat = '<div id="bloglink" class="bloglink"><a.*?>(.*?)</a>' bloglink = re.compile(bloglink_pat).findall(data_link) # 返回的是一个列表 # if blog_link != []: # bloglink = blog_link[0] if bloglink != [] and bloglink[0] not in bloglinks: bloglinks.append(bloglink[0]) print(len(bloglinks)) # 由bloglink构造blog_RSSlink # bloglink地址为http://blog.sina.com.cn/***或http://blog.sina.com.cn/u/*** # 通过观察可以看出RSS订阅源地址为http://blog.sina.com.cn/rss/***.xml blog_RSSlinks = [] for bloglink in bloglinks: # 利用正则表达式:首先提取bloglink里的*** name_pat = 'http://blog.sina.com.cn/(.*)' name_pat_2 = 'http://blog.sina.com.cn/u/(.*)' if bloglink.find('/u/') != -1: name = re.compile(name_pat_2).findall(bloglink) else: name = re.compile(name_pat).findall(bloglink) # name = re.compile(name_pat).findall(bloglink) blog_RSSlink = 'http://blog.sina.com.cn/rss/' + name[0] + '.xml' blog_RSSlinks.append(blog_RSSlink) return blog_RSSlinks url = 'http://blog.sina.com.cn/' blog_RSSlinks = getblog_RSSlink(url) print(len(blog_RSSlinks)) titles = [] wcs = [] for blog_RSSlink in blog_RSSlinks: title, wc = generateFeedVector.gettxt(blog_RSSlink) titles.append(title) wcs.append(wc) # 将获取的博客内容存档到txt文件里,然后对其进行后续处理(分词,构建聚类所要求的数据格式)
# 将unicode编码写入txt文件
for i in range(len(titles)): file = codecs.open('D:/pyworkspace_python2/blogContent/blogUser' + str(i + 1) + '.txt', 'wb', encoding='utf-8') file.write('blog_name:' + titles[i] + '\r\n') for t in wcs[i]: file.write(t + ':\r\n') file.write(wcs[i][t] + '\r\n')
将每个博客订阅源的博客文章分词之后,为了用来聚类,需要将每个博客订阅源表达为由一组直观的数字组成的向量。向量中的数值表示相应的词在博客订阅源中出现的次数。在这里我们需要构建一个单词列表,然后构建的博客订阅源向量都以这个单词列表为基准,这是因为像有些词语比如介词,连词等几乎在每个博客订阅源都出现,而有的词语又可能只出现在个别博客中,这些词语对博客聚类不仅没有起到积极作用,反而增加了向量的维度。因此我们只选择介于某个百分比范围内的单词,这里的百分比范围需要根据实际情况或者聚类的性能来进行不断的调试。构造“博客-单词“矩阵的过程由以下代码可知,我们将“博客-单词”矩阵保存在一个txt文件中:
# -*- coding: utf-8 -*- import re ''' 本模块的作用: 将各博客内容进行分词,且去除词性以及标点符号; 然后对每个博客词语进行统计数量; 按出现比例去除一些词语,最后形成一个“博客-词语”矩阵 ''' def getBlog_wordsMatrix(filepath_qianzhui, file_count): # apcount用来存放词语所出现的次数(这个出现次数指的是出现这个词语的博客数目,例子:在A博客中出现,计数为1,在B博客中出现,数量加1。) apcount = {} # wcs存放每个博客订阅源内容中所有词语的计数 wcs = {} for i in range(2, file_count + 2): wc = {} # 对一个博客内容的词语进行计数 # 首先从txt文件里读取出博客内容来 filepath = filepath_qianzhui + str(i) + '.txt' file = open(filepath) data = file.read() file.close() words = [word.strip() for word in data.split(' ')] # 以空格作为切分条件将字符串切分成一个个的词语 # 从所有词语中去掉词性以及标点符号,留下的词语保存到wordlist里 wordlist = [] pat = '/.+' for word in words: if word.find('/w') == -1 and word.find('/x') == -1: w = re.compile(pat).sub('', word) # print(w) if w != '': wordlist.append(w) # print(wordlist) # 对博客内的单词进行计数 for word in wordlist: wc.setdefault(word, 0) wc[word] += 1 # 用wcs存放各个博客单词计数wc wcs['blog' + str(i)] = wc # 遍历每个博客,查看词语出现的次数(次数指的是出现某词语的博客数目) for blogUser in wcs.keys(): # for word, count in wcs[blogUser]: # wcs[blogUser]为一个字典,如果不加items会出现这个错误。ValueError: too many values to unpack 类错误,多为输入或者输出参数数量不一致导致 for word, count in wcs[blogUser].items(): apcount.setdefault(word, 0) if count >= 1: apcount[word] += 1 # 根据出现词语的博客数目来过滤掉出现次数很多和出现次数很少的词语,减少需要考查的单词总量 endwordlist = [] # endwordlist保存需要考查的单词 for w, c in apcount.items(): frac = float(c)/len(wcs.keys()) # 计算单词出现的比例 if frac > 0.1 and frac < 0.5: endwordlist.append(w) for word in endwordlist: print(word) print(len(endwordlist)) # 将博客-词矩阵存入blogdata.txt文本文件中 ''' 矩阵形式为: 其中第一行为:Blog word1,word2,...,wordn 其中第一列为:blog1 blog2 ... blogm ''' out = open('D:/pyworkspace_python2/blogdata.txt', 'w') out.write('Blog') for word in endwordlist: out.write('\t%s' % word) out.write('\n') for blogUser, wc in wcs.items(): out.write(blogUser) for word in endwordlist: if word in wc.keys(): out.write('\t%d' % wc[word]) else: out.write('\t0') out.write('\n') filepath_qianzhui = 'D:/pyworkspace_python2/newBlogContent/blogUser' file_count = 188 getBlog_wordsMatrix(filepath_qianzhui, file_count)
以上数据的构造过程就结束了。我们得到了可以应用于聚类的数据结构了。接下来正式进入主题:分级聚类和K-均值聚类。
1、分级聚类
分级聚类通过连续不断地将两个最为相似的群组两两合并,其中每个群组都是从单一元素开始的,在每次迭代的过程中,计算每两个群组的距离,并将距离最近的两个群组合并为一个群组,这个过程会一直重复下去,直到只剩下一个聚类为止。(简单的说,就是每次迭代从所有群组中只找距离最近的两个进行合并,如果当前群组为99个,经过一次迭代,合并两个群组,就从99个群组变为了98个群组)。经过分级聚类得到的聚类结果用树状的形式可以形象的表示出其聚类的过程。
其中距离计算我们采用皮尔逊相关系数来计算。运用皮尔逊相关系数的原因是因为对于博客来说,有的博客比其他博客包含的更多的文章条目,或者文章条目的长度比其他博客的更长,这样会导致这些博客在总体上比其他博客包含更多的词汇。
接下来是分级聚类算法的代码:
# -*- coding: utf-8 -*- ''' 一、分级聚类 分级聚类的思想为: 分级聚类是通过连续不断地将两个最为相似的群组两两合并,来构造出一个群组的层级结构。 其中每个群组都是从单一元素开始的。(此处的单一元素就是一个博客) 分级聚类算法会计算每两个群组间的距离,并将距离最近的两个群组合并成一个新的群组。 (新群组所在的位置位于这两个元素的中间) 要计算两个群组间的距离,本文采用皮尔逊相关度来计算。这是由于: 一些博客比其他博客包含更多的文章条目,或者文章条目的长度比其他博客的更长,这样会 导致这些博客在总体上比其他博客包含更多的词汇(这也就是欧几里德距离里所谓的夸大分值问题)。 皮尔逊相关度可以解决这一问题,是因为它判断的是两组数据与某条直线的拟合程度。 ''' from math import sqrt # 皮尔逊相关度计算函数 def pearson(v1, v2): # 求和 sum1 = sum(v1) sum2 = sum(v2) # 求平方和 sum1Sq = sum([pow(v, 2) for v in v1]) sum2Sq = sum([pow(v, 2) for v in v2]) # 求乘积之和 pSum = sum([v1[i] * v2[i] for i in range(len(v1))]) # 计算皮尔逊相关度(Pearson score) n = len(v1) num = pSum - (sum1 * sum2 / n) den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n)) if den == 0: return 0 return 1.0 - num / den ''' 皮尔逊相关度的计算结果再两者完全匹配的情况下为1.0,而在两者毫无关系的情况下则为0.0。 返回1.0减去皮尔逊相关度,这样做的目的是为了让相似度越大的两个元素之间的距离变得更小。 ''' # readfile函数用来读取 “博客-词”矩阵文件 def readfile(filepath): lines = [line for line in file(filepath)] # 一种按行读取文件的方法 # 第一行是列标题 colnames = lines[0].strip().split('\t') rownames = [] data = [] for line in lines[1: ]: p = line.strip().split('\t') # 每行的第一列是行名 rownames.append(p[0]) # 每行的剩余部分就是该行对应的数据 data.append([float(x) for x in p[1: ]]) return rownames, colnames, data # 编写一个类,代表“聚类”这一类型 class bicluster: def __init__(self, vec, left=None, right=None, distance=0.0, id=None): ''' :param vec: 表示该聚类(群组)的向量表示 :param left: 表示该聚类其中的一个元素 :param right: 表示该聚类另一个元素 :param distance: 表示该聚类两个元素之间的距离 :param id: id可以唯一标识该聚类,这样可以使后续操作更加方便 ''' self.left = left self.right = right self.vec = vec self.id = id self.distance = distance # 分级聚类算法 def hcluster(rows, distance=pearson): ''' :param rows: 表示每一个博客词语的计数(即readfile函数中所返回的每一行的数据) :param distance: 表示元素之间距离计算公式(这里使用皮尔逊相关度计算方法) :return: 返回聚类结果。(分级聚类的终止条件是:直到只剩下一个聚类为止) ''' print('分级聚类开始。') distances = {} currentclustid = -1 # 用来标识第一次产生的新的聚类中的id # 最开始的聚类就是数据集中的行 clust = [bicluster(rows[i], id=i) for i in range(len(rows))] # 分级聚类的终止条件是:最后只剩下一个聚类,因此这里循环条件为判断聚类的个数是否大于1 while len(clust) > 1: lowestpair = (0, 1) # 和数组中寻找最小元素的方法一样(将数组第一个元素当做最小,然后拿剩下元素去和这个最小元素相比) # 这里首先将第一对(id=0, id=1)的距离当做最小,然后拿剩下每两个元素的距离去和这个最小距离相比 closest = distance(clust[lowestpair[0]].vec, clust[lowestpair[1]].vec) # 遍历每一个配对,寻找最小距离 for i in range(len(clust)): for j in range(i + 1, len(clust)): # 用distances来缓存距离的计算值,distances是通过一组数组id的判断来检验的,而id又是唯一标识一个聚类的 if (clust[i].id, clust[j].id) not in distances: distances[(clust[i].id, clust[j].id)] = distance(clust[i].vec, clust[j].vec) d = distances[(clust[i].id, clust[j].id)] if d < closest: closest = d lowestpair = (i, j) # lowestpair用一个元组来表示,来存放最小距离的两个元素在元素集合中的索引 # 计算两个聚类的平均值(作为合并聚类的向量表示) mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2.0 for i in range(len(clust[0].vec))] # 建立新的聚类,不在原始集合中的聚类,其id为负数 newcluster = bicluster(mergevec, left=clust[lowestpair[0]], right=clust[lowestpair[1]], distance=closest, id=currentclustid) # 不在原始集合中的聚类,其id为负数 currentclustid = currentclustid - 1 # 两个聚类合并为了一个聚类,在原聚类集合中去掉这两个聚类,加入合并后的聚类 ''' 在删除这两个聚类的时候,要先删除后一个聚类(索引值更大的聚类),举个例子: 如果最后只剩下两个聚类,这两个聚类进行合并,如果先删除索引值较小的, 则列表clust只剩下一个元素,索引只有0,如果再删除索引值较大的聚类, 就会出现错误:IndexError: list assignment index out of range ''' # del clust[lowestpair[0]] # 错误IndexError: list assignment index out of range # del clust[lowestpair[1]] del clust[lowestpair[1]] del clust[lowestpair[0]] clust.append(newcluster) # 循环结束后只剩下一个聚类,因此clust只有一个元素了。 print('分级聚类结束。') return clust[0] filepath = 'D:/pyworkspace_python2/blogdata.txt' blognames, words, data = readfile(filepath) # clust则为最终的聚类结果,可以编写相应的绘图代码将 # 聚类过程用树状的结果表示出来 clust = hcluster(data, distance=pearson)
具体的树状图绘制过程后续再来补充。。。
分级聚类的结果为一棵形象直观的树,但是这个方法有两个缺点:其一是,分级聚类并没有将数组真正的拆分成不同的群组;其二是,分级聚类算法的计算量太大,每次都要计算每两个群组间的距离。而K-均值聚类相比于分级聚类,这两个问题都得到了解决,不仅将数据拆分为了K个聚类,而且计算量相比于分级聚类也相对较小(具体在后续介绍)。
接下来要说一下K-均值聚类。
2、K-均值聚类
K-均值聚类,由名称可以看到一个K,这个K表示的就是要将数据聚类成K个群组,K是事先给定的。对于K-均值聚类,首先随机确定K个中心点位置(代表聚类中心的点)。然后将各个数据项分配给最临近的钟建新,待分配完成之后,聚类中心就会移到分配给该聚类的所有节点的平均位置处,然后整个分配过程重新开始。这个过程一直重复下去,直到分配过程不再产生变化为止(当分配情况与前一次相同时,迭代过程就结束了)。具体代码如下:
# -*- coding: utf-8 -*- ''' 二、K-均值聚类 K-均值聚类的思想为: K-均值聚类首先要预先告知算法希望生成的聚类数量,即K。然后随机创建K个聚类中心(初始聚类中心), 然后进行迭代计算,每次迭代计算所有数据项与K个聚类中心的距离,将各个数据项分配给最临近的中心点 ,每次分配完成之后,聚类中心数据更新为分配给它的所有项的平均位置。在迭代过程当中,如果某次迭代 的分配情况和上一次分配情况相同,则终止迭代,输出最后的聚类结果。 这里距离的计算还是使用皮尔逊系数。 ''' from math import sqrt import random # 皮尔逊相关度计算函数 def pearson(v1, v2): # 求和 sum1 = sum(v1) sum2 = sum(v2) # 求平方和 sum1Sq = sum([pow(v, 2) for v in v1]) sum2Sq = sum([pow(v, 2) for v in v2]) # 求乘积之和 pSum = sum([v1[i] * v2[i] for i in range(len(v1))]) # 计算皮尔逊相关度(Pearson score) n = len(v1) num = pSum - (sum1 * sum2 / n) den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n)) if den == 0: return 0 return 1.0 - num / den ''' 皮尔逊相关度的计算结果再两者完全匹配的情况下为1.0,而在两者毫无关系的情况下则为0.0。 返回1.0减去皮尔逊相关度,这样做的目的是为了让相似度越大的两个元素之间的距离变得更小。 ''' def readfile(filepath): lines = [line for line in file(filepath)] # 一种按行读取文件的方法 # 第一行是列标题 colnames = lines[0].strip().split('\t') rownames = [] data = [] for line in lines[1: ]: p = line.strip().split('\t') # 每行的第一列是行名 rownames.append(p[0]) # 每行的剩余部分就是该行对应的数据 data.append([float(x) for x in p[1: ]]) return rownames, colnames, data # K-均值聚类算法 def kcluster(rows, distance=pearson, k=4): ''' :param rows: 表示每一个博客词语的计数(即readfile函数中所返回的每一行的数据) :param distance: 表示元素之间距离计算公式(这里使用皮尔逊相关度计算方法) :param k: K-均值聚类的聚类个数K :return: 最终聚类结果(k个聚类) ''' print('K-均值聚类开始。') # 确定每个点的最小值和最大值 ranges = [] for i in range(len(rows[0])): for row in rows: nums = [] nums.append(row[i]) ranges.append((min(nums), max(nums))) # 这一行与上面几行功能一样 # ranges = [(min([row[i] for row in rows]), max([row[i] for row in rows])) for i in range(len(rows[0]))] # 随机创建k个中心点 clusters = [] for j in range(k): one_cluster = [] for i in range(len(rows[0])): one_cluster.append(random.random() * (ranges[i][1] - ranges[i][0]) + ranges[i][0]) clusters.append(one_cluster) # 这一行与上面几行功能一样 # clusters = [[random.random() * (ranges[i][1] - ranges[i][0]) + ranges[i][0] for i in range(len(rows[0]))] for j in range(k)] # 聚类迭代过程 lastmatches = None for t in range(100): # 迭代次数为100次 print('Iteration %d' % t) bestmatches = [[] for i in range(k)] # 在每一行(每一个数据项)中寻找距离最近的中心点 for j in range(len(rows)): row = rows[j] # 对于每一行row(每一个数据项),求出与该row距离最近的中心点bestmatch bestmatch = 0 for i in range(k): d = distance(clusters[i], row) if d < distance(clusters[bestmatch], row): bestmatch = i # bestmatches[bestmatch].append(row) bestmatches[bestmatch].append(j) # 如果迭代结果与上一次相同,则整个过程结束 if bestmatches == lastmatches: break lastmatches = bestmatches # 把中心点移到其所有成员的平均位置处 for i in range(k): avgs = [0.0] * len(rows[0]) if len(bestmatches[i]) > 0: for rowid in bestmatches[i]: for m in range(len(rows[rowid])): avgs[m] += rows[rowid][m] for j in range(len(avgs)): avgs[j] = avgs[j] / len(bestmatches[i]) clusters[i] = avgs print('K-均值聚类完成。') return bestmatches filepath = 'D:/pyworkspace_python2/blogdata.txt' blognames, words, data = readfile(filepath) # kclust即为K-均值聚类的结果 kclust = kcluster(data, k=10)
以上分级聚类和K-均值聚类算法中有部分共通的代码:皮尔逊相关系数以及从txt中读取“博客-单词”矩阵的代码。还可以通过对矩阵转置来对单词进行聚类处理。具体将聚类结果可视化表示出来的代码,后续再补充。。