集体智慧编程----第三章发现群组


这章中的主要内容:

  • 从各种不同的来源中构造算法所需的数据
  • 两种不同的聚类算法;
  • 更多有关距离度量(distance metrics)的知识
  • 简单的图形可视化代码,用以观察所生成的群组
  • 如何将异常复制的数据集投影到二维空间中

监督学习和无监督学习

利用样本输入和期望输出来学习如何预测的技术被称为监督学习法(supervised learning methods)。例如神经网络、决策树、向量支持机、贝叶斯过滤。当我们想要利用这些方法中的任何一种来提取信息时,我们可以传入一组输入,然后期望应用程序能够根据其此前学到的知识来产生一个输出。

聚类是无监督学习。与神经网络或决策树不同,无监督学习算法不是利用带有正确答案的样本数据进行“训练”。它们的目的是要在一组数据中找寻某种结构,而这些数据本身不是我们要找的答案。

对博客用户进行分类

对订阅源中的单词进行计数

对wc={}的定义不解,原来是python中的字典类型的定义。这篇文章有讲解,关于feedparser的用法解释
RSS订阅源是一个包含博客及其所有文章条目信息的简单XML文档。为了给每个博客中的单词技术,首先第一步就是要解析这些订阅源

import feedparser
import re

#返回一个RSS订阅源的标题和包含单词统计情况的字典
def getwordcounts(url):
    #解析订阅源
    d=feedparser.parse(url)
    wc={}

    #循环遍历所有的文章条目
    for e in d.entries:
        if 'summary' in e: summary = e.summary
        else: summary=e.description

        #提取一个单词列表
        words=getwords(e.title+' '+summary)
        for word in words:
            wc.setdefault(word,0)
            wc[word]+=1
        return d.feed.title,wc

每个RSS和Atom订阅源都会包含一个标题和一组文章条目。通常,每个文章条目都有一个摘要,或者是包含了条目中世纪文本的描述性标签。函数getwordcounts将摘要传给函数getwords,后者会将其中所有的HTML标记剥离掉,并以非字母字符作为分隔符拆分出单词,再将结果以列表的形式加以返回。

generatefeedvector.py文件中的主体代码(这些代码不单独构成一个函数)循环遍历订阅源并生成数据集。代码的第一部分遍历feedlist.txt文件中的每一行,然后生成针对每个博客的单词统计,以及出现这些单词的博客数目(apcount)。

apcount={}
wordcounts={}
feedlist=[line for line in open('data/feedlist.txt')]
for feedurl in feedlist:
    title,wc=getwordcounts(feedurl)
    wordcounts[title]=wc
    for word,count in wc.items():
        apcount.setdefault(word,0)
        if count>1:
            apcount[word]+=1

下一步,建立一个单词列表,将其实际用于针对每个博客的单词计数。因为想“the"这样的单词几乎到处都是,而像”flim-flam"这样的单词则有可能只出现在个别博客中,所以通过只选择介于某个百分比范围内的单词,我们可以减少需要考查的单词总量。在本例中,我们可以将10%定为下届,将50%定为上届,不过假如你发现有过多常见或鲜见的单词出现,不妨尝试不同的边界值。

wordlist=[]
for w,bc in apcount.items():
    frac=float(bc)/len(feedlist)
    if frac>0.1 and frac<0.5: wordlist.append(w)

最后,我们利用上述单词列表和博客列表来建立一个文本文件,其中包含一个大矩阵,记录着针对每个博客的所有单词的统计情况:

out=open('data/blogdata.txt','w')
out.write('Blog')
for word in wordlist: out.write('\t%s' % word)
out.write('\n')
for blog,wc in wordcounts.items():
    out.write(blog)
    for word in wordlist:
        if word in wc: out.write('\t%d' % wc[word])
        else: out.write('\t0')
    out.write('\n')

网络问题feedparser的访问没有返回值,还好网上能找到对应的数据,之后会放到GitHub中。

大矩阵的大概样子就是这样的

分级聚类

分级聚类通过连续不断地将最为相似地群组两两合并,来构造一个群组地层级结构。其中地每个群组都是从单一元素开始地,在本章地例子中,这个单一元素就是博客。在每次迭代地过程中,分级聚类算法会计算每两个群组间地距离,并将距离最近的两个群组合并成一个新的群组。这一过程会一直重复下去,知道只剩一个群组为止。

待分级聚类完成之后,我们可以采用一种图形化的方式来展现所得的结果,这种图被称为树状图
在这里插入图片描述
我们将示范如何对博客数据集进行聚类,以构造博客的层级结构;如果构造成功,我们将实现按主题对博客进行分组。首先,我们需要一个方法来加载数据文件。新建一个名为clusters.py的文件

def readfile(filename):
    lines=[line for line in open(filename)]

    #第一行是列标题
    colnames=lines[0].strip().split('\t')[1:]
    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

上述函数将数据集中的头一行数据读入一个代表列名的列表,并将最左边一列读入一个代表行名的列表,最后它又将剩下的所有数据都放入一个大列表,其中的每一项对应于数据集中的一行数据。数据集中任一单元格内的计数值,都可以由一个行号和列号来唯一定位,此行号和列号同时还对应于列表rownames和colnames中的索引。

在本章的例子中,一些博客比其他博客包含更多的文章条目,或者文章条目的长度比其他博客的更长,这样会导致这些博客在总体上比其他博客包含更多的词汇。皮尔逊相关度可以纠正这一问题,因为它判断的其实是两组数据与某条直线的拟合程度。此处,皮尔逊相关度的计算代码将接受两个数字列表作为参数,并返回这两个列表的相关度分值:

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)))
    
    #计算r(Pearson score)
    num=pSum-(sum1*sum2/len(v1))
    den=sqrt((sum1Sq-pow(sum1,2)/len(v1))*(sum2Sq-pow(sum2,2)/len(v1)))
    if den==0: return 0
    return 1.0-num/den

皮尔逊相关度的计算结果在两者完全匹配的情况下为1.0,而在两者毫无关系的情况下则为0.0。上述代码的最后一行,返回的是以1.0减去皮尔逊相关度之后的结果,这样做的目的是为了让相似度越大的两个元素之间的距离变得更小。

我们可以新建一个bicluster类,将所有这些属性存放其中,并以此来描述这颗层级数。

class bicluster:
    def __int__(self,vec,left=None,right=None,distance=0.0,id=None):
        self.left=left
        self.right=right
        self.vec=vec
        self.id=id
        self.distance=distance

聚类算法的核心,以一组对应于原始数据项的聚类开始。函数的主循环部分会尝试每一组可能的配对并计算它们的相关度,以此来找出最佳配对。最佳配对的两个聚类会被合并成一个新的聚类。新生成的聚类中所包含的数据,等于将两个旧聚类的数据求均值之后得到的结果。这一过程会一直重复下去,直到只剩下一个聚类为止。由于整个计算过程可能会非常耗时,所以不妨将每个配对的相关度计算结果保存起来,因为这样的计算会反复发生,直到配对中的某一项被合并到另一个聚类中为止。

对python中运用class的解读,在我看来就是面向对象的编程。

因为每个聚类都指向构造该聚类时被合并的另两个聚类,所以我们可以递归搜索由该函数最终返回的聚类,以重建所有的聚类及叶节点。

def hcluster(rows,distance=pearson):
    distances={}
    currentclustid=-1

    #最开始的聚类就是数据集中的行
    clust=[bicluster(rows[i],id=i) for i in range(len(rows))]

    while len(clust)>1:
        lowestpair=(0,1)
        closest=distance(clust[0].vec,clust[1].vec)

        #遍历每一个配对,寻找最小距离
        for i in range(len(clust)):
            for j in range(i+1,len(clust)):
                #用distance来缓存距离的计算值
                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)

                #计算两个聚类的平均值
                mergevec=[
                    (clust[lowestpair[0]].vec[i]+clust[lowestpair[1]].vec[i])/2.0
                          for i in range(len(clust[0].vec))]

                #建立新的聚类
                newcluster=bicluster(mergevec,left=clust[lowestpair[0]],
                                     right=clust[lowestpair[1]],
                                     distance=closest,id=currentclustid)

                #不在原始集合中的聚类,其id为负数
                currentclustid-=1
                del clust[lowestpair[1]]
                del clust[lowestpair[0]]
                clust.append(newcluster)
    return clust[0]

为了加快对每一对博客的相关度计算过程,递归遍历聚类树,并将其以类似文件系统层级结构的形式打印出来。

def printclust(clust,labels=None,n=0):
    #利用缩进来建立层级布局
    for i in range(n):print(' ',)
    if clust.id<0:
        #负数标记代表这是一个分支
        print ('-')
    else:
        #正数标记代表这是一个叶节点
        if labels==None: print(clust.id)
        else: print(labels[clust.id])

    #现在开始打印右侧分支和左侧分支
    if clust.left!=None: printclust(clust.left,labels=labels,n=n+1)
    if clust.right!=None: printclust(clust.right,labels=labels,n=n+1)

运行输出的结果为:

此处列出的是集合中的原始数据项。破折号代表的,是由两个或更多项合并而成的聚类。

绘制树状图

PIL有一个针对windows平台的安装程序和一个针对其他平台的源代码发布包。

在cluster.py文件的开始处:

from PIL import Image,ImageDraw

首先,需要利用一个函数来返回给定聚类的总体高度。在确定图形的整体高度和放置不同节点的位置时,知道聚类的总体高度是很有必要的。

def getheight(clust):
    #这是一个叶节点吗?若是,则高度为1
    if clust.left==None and clust.right==None: return 1

    #否则,高度为每个分支的高度之和
    return getheight(clust.left)+getheight(clust.right)

我们还需要知道根节点的总体误差。因为线条的长度会根据每个节点的误差进行相应的调整,所以我们需要根据总的误差生成一个缩放因子(scaling factory)。一个节点的误差深度等于其下所属的每个分支的最大可能误差。

def getdepth(clust):
    #一个叶节点的距离是0.0
    if clust.left==None and clust.right==None: return 0
    #一个枝节点的距离等于左右两侧分支中距离较大者
    #加上该枝节点自身的距离
    return max(getdepth(clust.left),getdepth(clust.right))+clust.distance

函数drawdenbrogram为每一个最终生成的聚类创建一个高度为20像素、宽度固定的图片。其中的缩放因子是由固定宽度除以总的深度值得到的。该函数为图片建立相应的draw对象,然后在根节点的位置调用drawnode函数,并令其处于整幅图片左侧正中间的位置。

def drawdendrogram(clust,labels,jpeg='clusters.jpg'):
    #高度和宽度
    h=getheight(clust)*20
    w=1200
    depth=getdepth(clust)

    #由于宽度是固定的,因此我们需要对距离值做相应的调整
    scaling=float(w-150)/depth

    #新建一个白色背景的图片
    img=Image.new('RGB',(w,h),(255,255,255))
    draw=ImageDraw.Draw(img)

    draw.line((0,h/2,10,h/2),fill=(255,0,0))

    #画第一个节点
    drawnode(draw,clust,10,(h/2),scaling,labels)
    img.save(jpeg,'JPEG')

此处最为重要的函数是drawnode,它接受一个聚类及其位置作为输入参数。函数取到子节点的高度,并计算出这些节点所在的位置,然后用线条将它们连接起来----包括一条长长的垂直线和两条水平线。水平线的长度是由聚类中的误差情况决定的。线条越长就越表明,合并在一起的两个聚类差别很大,而线条越短则越表明,两个聚类的相似度很高。

def drawnode(draw,clust,x,y,scaling,labels):
    if clust.id<0:
        h1=getheight(clust.left)*20
        h2=getheight(clust.right)*20
        top=y-(h1+h2)/2
        bottom=y+(h1+h2)/2
        #线的长度
        l1=clust.distance*scaling
        #聚类到其子节点的垂直线
        draw.line((x,top+h1/2,x,bottom-h2/2),fill=(255,0,0))

        #连接左侧节点的水平线
        draw.line((x,top+h1/2,x+l1,top+h1/2),fill=(255,0,0))

        #连接右侧节点的水平线
        draw.line((x,bottom-h2/2,x+l1,bottom-h2/2),fill=(255,0,0))

        #调用函数绘制左右节点
        drawnode(draw,clust.left,x+l1,top+h1/2,scaling,labels)
        drawnode(draw,clust.right,x+l1,bottom-h2/2,scaling,labels)
    else:
        #如果这是一个叶节点,则绘制节点的标签
        draw.text((x+5,y-7),labels[clust.id],(0,0,0))
blognames,words,data=readfile("./data/blogdata.txt")
clust=hcluster(data)
drawdendrogram(clust,blognames,jpeg='blogclust.jpg')

在这里插入图片描述
由于我不能很好的理清该算法的流程,所以我使用debug的方式可以很好的梳理,但是在debug跳到循环的最后的时候,出现了我的条件不起作用。以下是当时写的条件。
在这里插入图片描述
以上的条件是完全不起作用的,要写成以下的条件
在这里插入图片描述

列聚类

同时在行和列上对数据进行聚类常常是有必要的。当我们进行市场研究的时候,对消费群体进行分组可能是很有意义的,这将有助于我们摸清消费者的统计信息和产品的状况,还可能有助于我们确定哪些上架商品可以进行捆绑销售。在博客数据集中,列代表的是单词,知道哪些单词时常会结合在一起是哦那个,可能时非常有意义的。

要利用此前编好的函数实现针对列的聚类,最容易的一种方式就是将整个数据集转置(rotate),使列变成行,其中的每一行都对应一组数字,这组数字指明了某个单词在每篇博客中出现的次数。

blognames,words,data=readfile("./data/blogdata.txt")
newdata=rotatematrix(data)
clust=hcluster(newdata)
drawdendrogram(clust,blognames,jpeg='blogclust1.jpg')

在这里插入图片描述
关于聚类有一点很重要:当数据项的数量比变量多的时候,出现无意义聚类的可能性就会增加。由於单词的数量比博客多很多,因此我们会发现,在博客聚类中出现的模式要比单词聚类中出现的更为合理。

K-均值聚类

分级聚类的结果为我们返回了一颗形象直观的树,但是这种方法有两个缺点。在没有额外投入的情况下,树形视图是不会真正将数据拆分成不同组的,而且该算法的计算量非常惊人。因为我们必须计算每两个配对项之间的关系,并且在合并项之后,这些关系还得重新再计算,所以再处理很大规模的数据集时,该算法的运行速度会非常缓慢。

我们会预先告诉算法希望生成的聚类数量,然后算法会根据数据的结构状况来确定聚类的大小。首先会随机确定k个中心位置(位于空间中代表聚类中心的点),然后将各个数据项分配给最临近的中心点。待分配完成之后,聚类中心就会移动到分配给该聚类的所有节点的平均位置处,然后整个分配过程重新开始。这一过程会一直重复下去,知道分配过程不再产生变化为止。
在这里插入图片描述

def kcluster(rows,distance=pearson,k=4):
    #确定每个点的最小值和最大值
    ranges= [(min([row[i] for row in rows]),max([row[i] for row in rows]))
             for i in range(len(rows[0]))]
    #随机创建k个中心点
    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):#迭代的次数
        print('Iteration %d' % t)
        bestmatches=[[] for i in range(k)]

        #在每一行中寻找距离最近的中心点
        for j in range(len(rows)):
            row=rows[j]
            bestmatch=0
            for i in range(k):
                d=distance(clusters[i],row)
                if d<distance(clusters[bestmatch],row): bestmatch=i
            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]/=len(bestmatches[i])
                clusters[i]=avgs

    return bestmatches

上述代码在每个变量的值域范围内随机构造了一组聚类。当每次迭代进行的时候,算法会将每一行数据分配给某个中心点,然后再将中心点的数据更新为分配给它的所有项的平均位置。当分配情况与前一次相同时,迭代过程就结束了,同时算法会返回k组序列,其中每个 序列代表一个聚类。

由于函数选用随机的中心点作为开始,所以返回结果的顺序几乎总是不同的。根据中心点初始位置的不同,最终聚类中所包含的内容也可能会有所不同。

blognames,words,data=readfile("./data/blogdata.txt")
kclust = kcluster(data,k=10)
print([blognames[r] for r in kclust[3]])

针对偏好的聚类

获取数据和准备数据

Beautiful Soup

Beautiful Soup是一个解析网页和构造结构化数据表达形式的函数库。

收集来自Zebo的结果

但是这个网站在国内已经访问不到了,但是获取数据的代码还是可以参考的。我们很容易就可以判断出页面的哪些部分对应于物品的列表,因为它们都带有名为bgverdanasmal的CSS类。我们可以利用这一点来提取页面中的重要数据。

from bs4 import BeautifulSoup
import urllib.request as urllib2
import re
chare=re.compile(r'[!-\.&]')
itemowners={}

#要去除的单词
dropwords=['a','new','some','more','my','own','the','many','other','another']

currentuser=0
for i in range(1,51):
    #搜索“用户希望拥有的物品”所对应的URL
    c = urllib2.urlopen(
        'http://member.zebo.com/Main?event_key=USERSEARCH&wiowiw=wiw&keyword=car&page=%d'
        % (i))
    soup = BeautifulSoup(c.read())
    for td in soup('td'):
        #寻找带有bgverdanasmall类的表格单元格
        if ('class' in dict(td.attrs) and td['class'] == 'bgverdanasmall'):
            items = [re.sub(chare, '', str(a.contents[0]).lower()).strip() for a in td('a')]
            for item in items:
                # 去除多余的单词
                txt = ' '.join([t for t in item.split(' ') if t not in dropwords])
                if len(txt) < 2: continue
                itemowners.setdefault(txt, {})
                itemowners[txt][currentuser] = 1
            currentuser += 1

上述代码将下载和解析从Zebo上搜索到的包含“用户希望拥有的物品”的前50个页面。因为所有物品的文字都是随意输入的,所以需要进行大量的清理工作,其中包括去除像“a”和“some"这样的单词,去除标点符号,以及将所有文本转换成小写。

代码首先会构造一个列表,其中包含的是超过5个人都希望拥有的物品,然后再构造一个以匿名用户为列、以物品为行的矩阵,最后再将该矩阵写入一个文件。

out=file('zebo.txt','w')
out.write('Item')
for user in range(0,currentuser): out.write('\tU%d' % user)
out.write('\n')
for item,owners in itemowners.items():
  if len(owners)>10:
    out.write(item)
    for user in range(0,currentuser):
      if user in owners: out.write('\t1')
      else: out.write('\t0')
    out.write('\n')

定义距离度量标准

皮尔逊相关度很适合于博客数据集,该数据集中所包含的是单词的实际统计值。而在此处,数据集却只有1和0两种取值,分别代表着有或者无。假如我们对同时希望拥有两件物品的人在物品方面互有重叠的情况进行度量,那或许是一件更有意义的事情。采用一种被称为Tanimoto系数(Tanimoto coefficient)的度量方法,它代表的是交集与并集的比例。

def tanimoto(v1,v2):
    c1,c2,shr=0,0,0
    
    for i in range(len(v1)):
        if v1[i] !=0: c1+=1 #出现在v1中
        if v2[i] !=0: c2+=2 #出现在v2中
        if v1[i] !=0 and v2[i] !=0: shr+=1 #在两个向量中都出现
    
    return 1.0-(float(shr)/(c1+c2-shr))

上述代码将返回一个介于1.0和0.0之间的值。其中1.0代表不存在同时喜欢两件物品的人,而0.0则代表所有人都同时喜欢两个向量中的物品。

对结果进行聚类

wants,people,data=readfile('./data/zebo.txt')
clust=hcluster(data,distance=tanimoto)
drawdendrogram(clust,wants)

在这里插入图片描述

以二维形式展现数据

利用***多维缩放(multidimensional scaling)***技术,我们可以为数据集找到一种二维表达形式。算法根据没对数据项之间的差距情况,尝试绘制出一幅图来,图中各数据项之间的距离远近,对应于它们彼此间的差异程度。为了做到这一点,算法首先需要计算出所有项之间的目标距离。在博客数据集中,我们采用了皮尔逊相关度技术来对各数据项进行比较。此处有一个示例
在这里插入图片描述
将所有数据项随机放置在二维图上

所有数据项两两间的当前距离值都是根据实际距离(即差平方之和)计算求得的。
在这里插入图片描述

针对每两两构成的一对数据项,我们将它们的目标距离与当前距离进行比较,并求出一个误差值。根据误差的情况,我们会根据比例将每个数据项的所在位置移近或移远少许量。下图显示了我们对数据项A的施力情况。图中A与B之间的距离为0.5,而两者的目标距离仅为0.2,因此我们必须将A朝B的方向移进一点才行。于此同时,我们还将A推离了C和D,因为它距离C和D都太近了。

每一个节点的移动,都是所有其他节点施加在该节点上的推或拉的综合效应。节点每移动一次,其当前距离和目标距离间的差距就会减少一些。这一过程会不断地重复多次,直到我们无法再通过移动节点来减少总体误差为止。

实现这一功能的函数接受一个数据向量作为参数,并返回一个只包含两列的向量,即数据项再二维图上的X座标和Y座标。

def scaledown(data,distance=pearson,rate=0.01):
    n = len(data)

    #每一对数据项之间的真实距离
    realdist=[[distance(data[i],data[j]) for j in range(n)]
               for i in range(0,n)]

    outersum=0.0

    #随机初始化节点再二维空间中的起始位置
    loc=[[random.random(),random.random()] for i in range(n)]
    fakedist=[[0.0 for j in range(n)] for i in range(n)]

    lasterror=None
    for m in range(0,1000):
        #寻找投影后的距离
        for i in range(n):
            for j in range(n):
                fakedist[i][j]=sqrt(sum([pow(loc[i][x]-loc[j][x],2)
                                         for x in range(len(loc[i]))]))
        #移动节点
        grad=[[0.0,0.0] for i in range(n)]

        totalerror=0
        for k in range(n):
            for j in range(n):
                if j==k: continue
                #误差值等于目标距离与当前距离之间差值的百分比
                errorterm=(fakedist[j][k]-realdist[j][k])/realdist[j][k]

                #每一个节点都需要根据误差的多少,按比例移离或移向其他节点(每一个节点的移动,都是所有其他节点施加在该节点上的推或拉的综合效应)
                grad[k][0] += ((loc[k][0] - loc[j][0])/fakedist[j][k])*errorterm
                grad[k][1] += ((loc[k][1] - loc[j][1]) / fakedist[j][k]) * errorterm

                #记录总的误差值
                totalerror += abs(errorterm)
            print(totalerror)

            #如果节点移动之后的情况变得更糟,则程序结束
            if lasterror and lasterror<totalerror: break
            lasterror = totalerror

            #根据rate参数与grad值相乘的结果,移动每一个节点
            for k in range(n):
                loc[k][0]==rate*grad[k][0]
                loc[k][1]==rate*grad[k][1]

        return loc

为了看到函数执行的效果,我们可以利用PIL再生成一幅图,根据新的座标值,在图上标出所有数据项的位置及其对应的标签。

def draw2d(data,labels,jpeg='mds2d.jpg'):
    img=Image.new('RGB',(2000,2000),(255,255,255))
    draw=ImageDraw.Draw(img)
    for i in range(len(data)):
        x = (data[i][0]+0.5)*1000
        y = (data[i][1]+0.5)*1000
        draw.text((x,y),labels[i],(0,0,0))
    img.save(jpeg,'JPEG')

调用scaledown获得二维形式的数据集,然后再调用draw2d将其绘制出来:

blognames,words,data=readfile('./data/blogdata.txt')
coords=scaledown(data)
draw2d(coords,blognames,jpeg='blogs2d.jpg')


显示了多维缩放算法的执行结果

代码:以上的代码在GitHub上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章