第三章發現羣組
這章中的主要內容:
- 從各種不同的來源中構造算法所需的數據
- 兩種不同的聚類算法;
- 更多有關距離度量(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')
顯示了多維縮放算法的執行結果