聚類的作用
通過聚類,我們可以跟蹤統計消費者信息,發現具有相似消費習慣的羣體,並據此開發相應的產品或者市場策略
監督學習與無監督學習
監督學習
利用樣本輸入和期望輸出來學習如何預測的技術
- 神經網絡
- 決策樹
- 向量支持機
- 貝葉斯過濾
無監督學習
無監督學習不是利用樣本進行訓練,而是要在一組數據中找尋某種結構
- 聚類
- 非負矩陣因式分解
- 自組織映射
數據源
由於本文主要講述聚類,所以對數據來源不做記錄,此處提供本文數據源下載地址
讀取數據:
# 讀取博客的統計數據
def readFile():
with open('blogdata.txt', 'r')as f:
lines = [line for line in f]
# 第一行數據是列名稱,去掉第一個blog字樣
colNames = lines[0].replace('\n', '').split('\t')[1:]
# 每一行的第一個數據是行名稱
rowNames = []
# data數據不包含每一行的第一列
data = []
for i in range(1, len(lines)):
# 去除換行符合空格
l = lines[i].replace('\n', '').split('\t')
# 行名是第一個數據
rowNames.append(l[0])
data.append([float(x) for x in l[1:]])
return colNames, rowNames, data
聚類分類
- 分級聚類
- k-均值聚類
- 二位空間聚類
分級聚類
通過連續的把最相近的羣組合併爲新的羣組來構造一個全新的羣組,每一個羣組都是從單一元素開始的,其過程如下圖:
在上圖中,每次都把最近的兩個元素放在一起,合成一個新的元素,然後求得這個元素裏兩個元素的平均值作爲新的值,再重複上面過程直到最後只剩下一個元素就可以了,而所有的元素內容都會保存在我們的數據中
對於數據的緊密度,我們依然採用上一篇提到過的皮爾遜係數:
# 比較數據d1和d2的相似度
def pearson(d1, d2):
# 求和
sum1 = sum(d1)
sum2 = sum(d2)
# 平方和
sumSq1 = sum([pow(v, 2) for v in d1])
sumSq2 = sum([pow(v, 2) for v in d2])
# 乘積之和
pSum = sum([d1[i] * d2[i] for i in range(len(d1))])
num = pSum - (sum1 * sum2 / len(d1))
den = math.sqrt(((sumSq1 - pow(sum1, 2) / len(d1)) * (sumSq2 - pow(sum2, 2) / len(d2))))
if den == 0: return 0
# num/den得到皮爾遜係數,這個數字越大表示兩個數據集相似度越高
# 用1-num/den得到的結果表示兩個數據集之間的距離,相似度越高距離越近
return 1 - num / den
新建一個類作爲聚類的載體:
# 用來保存聚合數據的類
# vec 保存聚合數據
# left 是聚合數據的左節點
# right 是聚合數據的右節點
# id 可以用來判斷數據是原始數據還是聚合數據,如果是原始數據還可以根據id獲取對應的行名稱
# distance中保存原始數據的距離
class bicluster:
def __init__(self, vec, left=None, right=None, id=None, distance=None):
self.vec = vec
self.left = left
self.right = right
self.id = id
self.distance = distance
有了上面的載體,我們只需要循環把兩個最相近的組聚合,然後重複這個過程,就能得到一個包含了所有數據的最終聚類
# 分級聚類,將數據聚合成一個bicluster對象
def hcluster(data, distance=pearson):
distances = {}
currentclustId = -1
# 原始的聚類就是所有數據的集合
clust = [bicluster(data[i], id=i) for i in range(len(data))]
# 大循環
while len(clust) > 1:
# 默認0/1是每次大循環開始時最近的數據
# lowestpair保存最近的一組數據,closest保存他們的距離
lowestpair = (0, 1)
closest = distance(clust[0].vec, clust[1].vec)
# 兩次循環保證所有數據可以比較
for i in range(len(clust)):
for j in range(len(clust)):
# 不跟自己比
if i == j: continue
# 如果當前數據沒有計算過才計算,不直接用i,j是因爲聚合之後i,j就不跟原始的數據對應了
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:
# 在這個大循環結束之前,i/j組合還可以代表最近的組
lowestpair = (i, j)
closest = d
# 獲取當前最近組的所有項的平均值
mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2 for i in range(len(data[0]))]
# 構造新的組,這個組中包含了子數據的所有信息
newclust = bicluster(mergevec, left=clust[lowestpair[0]], right=clust[lowestpair[1]], id=currentclustId,
distance=closest)
# 清除原始數據組,加入新數據
currentclustId -= 1
del clust[lowestpair[1]]
del clust[lowestpair[0]]
clust.append(newclust)
print(clust[0])
return clust[0]
通過循環執行,得到了唯一的聚類,我們可以來嘗試一下使用:
colNames, rowNames, data = readFile()
hcluster(data)
這個過程會消耗一定的時間,結果會打印出來一個bicluster的對象
但是這樣一個對象並不能讓我們直觀的感受到各個數據之間的關係,所以我們需要想辦法使聚類的結構關係可視化,此處引入一個很好用的Python圖像處理庫PythonImagingLibrary,簡稱PIL,如果對這個庫不瞭解的可以在網上學習一下簡單的使用,此處不做過多介紹
要繪製圖片,我們需要知道各個元素的高度和圖片的寬度,由於線條的長度會根據原始數據的誤差進行調整,所以我們還需要計算出總得誤差並據此生成一個誤差因子:
# 獲取聚類的高度
def getHeight(bicluster):
# 是原始數據,高度爲1
if bicluster.left is None and bicluster.right is None:
return 1
# 非原始數據,高度是兩個子數據高度之和
else:
return getHeight(bicluster.left) + getHeight(bicluster.right)
#獲取聚類的誤差
def getDepth(bicluster):
# 原始數據誤差爲0
if bicluster.left is None and bicluster.right is None:
return 0
# 聚合數據取誤差較大者
else:
return max(getDepth(bicluster.left), getDepth(bicluster.right)) + bicluster.distance
這樣我們就可以開始繪圖了:
# 繪製圖片
def drawDendrogram(bicluster, labels, jpge='clusters.jpeg'):
# 設置寬高數據
h = getHeight(bicluster) * 20
w = 1200
depath = getDepth(bicluster)
# 寬度固定,所有留一點額外的空間
scaling = float((w - 150) / depath)
image = Image.new('RGB', (w, h), (255, 255, 255))
draw = ImageDraw.Draw(image)
draw.line((0, h / 2, 10, h / 2), (255, 0, 0))
print('draw start...')
drawNode(bicluster, draw, 10, h / 2, scaling, labels)
image.save(jpge, 'JPEG')
#遞歸繪製細節
def drawNode(bicluster, draw, x, y, scaling, labels):
# 原始數據,顯示文字即可
if bicluster.left is None and bicluster.right is None:
draw.text((x + 5, y - 7), labels[bicluster.id], (0, 0, 0))
# 聚合數據,根據聚合兩個元素的距離來畫
else:
h1 = getHeight(bicluster.left) * 20
h2 = getHeight(bicluster.right) * 20
# 留出兩個子元素高度的空隙
top = y - (h1 + h2) / 2
bottom = y + (h1 + h2) / 2
# 畫出豎直的線,高度是兩個子元素高度的一半
draw.line((x, top + h1 / 2, x, bottom - h2 / 2), fill=(255, 0, 0))
# 畫出水平的線,寬度是縮放係數X距離
ll = scaling * bicluster.distance
draw.line((x, top + h1 / 2, x + ll, top + h1 / 2), fill=(255, 0, 0))
draw.line((x, bottom - h2 / 2, x + ll, bottom - h2 / 2), fill=(255, 0, 0))
# 循環,畫左右兩個子節點
drawNode(bicluster.left, draw, x + ll, top + h1 / 2, scaling, labels)
drawNode(bicluster.right, draw, x + ll, bottom - h2 / 2, scaling, labels)
完成上面的代碼之後就可以把我們得到的分級聚類結果顯示出來了,還不趕緊試試
有時候我們不僅想獲得行元素的聚類,也想看一下列元素的聚類結果,對應我們這次操作的數據來看,行元素的聚類可以讓我們查看博客之間的相似性,但是對列元素也就是詞組的相似性分析有時候對我們也很有意義,所以我們可以對數據做一下矩陣轉換,再對變換過的矩陣做相同的操作,得到我們想要的結果:
def translateXY(data):
result = []
# 獲取列數,用此進行循環,每個新的組包含了原來的一列元素
for x in range(len(data[0])):
# x代表第x列,y代表第y行,這樣把每一列的元素都取出來形成新的數組
newrow = [data[y][x] for y in range(len(data))]
# 這樣添加的數據,他的列數和他的索引是相同的
result.append(newrow)
return result
一起調用一下看看:
colNames, rowNames, data = readFile()
drawDendrogram(hcluster(data), rowNames)
drawDendrogram(hcluster(translateXY(data)), colNames, jpge='trans_clusters.jpeg')
print('end')
怎麼樣,同級目錄下是不是多出來兩張圖片,顯示了分組的詳細情況?
優點:形象,直觀
缺點:並沒有真正將數據分組,計算量比較大,很耗時
k-均值聚類
k-均值聚類首先確認每一列元素的範圍(計算最大值最小值),然後隨機生成k個行,這些行裏的每一個列元素都在範圍之內
之後我們可以根據元素和這k個隨機行之間的距離把元素分爲k組,得到了k個組之後我們再把這k個組的所有數據取平均值,就得到了新的組,然後以新的組爲中心,不斷的重複運算直到組不再變化,就得到了k個組,過程如下:
代碼實現:
# k均值聚類
def kclust(data, rowNames, distance=pearson, k=5):
# 存一下列數,經常要用
col_num = len(data[0])
# 隨機列的數據
randomrows = []
# 通過k-v的形式存儲每個隨機聚點下的子數據
last_clusts = {}
new_clusts = {}
# 循環列,拿到每一列的最大值和最小值
# for x in range(len(data[0])):
# # 最大值
# col_max = max([row[x] for row in data])
# # 最小值
# col_min = min([row[x] for row in data])
# # 每一列對應的正好是索引
# max_min.append((col_max, col_min))
# 簡寫如下:
# 存儲每一列的最大值和最小值
max_min = [(max([row[x] for row in data]), min([row[x] for row in data])) for x in range(col_num)]
# 隨機k個行數據
for i in range(k):
# max_min[j][0]-max_min[j][1]表示取最大值和最小值的差值,在用這個值X隨機數,在加上最小值
# 得到了最大值和最小值之間的一個隨機值
# 把上述過程進行列數個次數,就得到了一個隨機行
random_row = [(random.randint(0, 1) * (max_min[j][0] - max_min[j][1]) + max_min[j][1]) for j in range(col_num)]
randomrows.append(random_row)
# 大循環進行到數據不再更改
while True:
for i in range(k):
new_clusts[i] = []
# 拿每一行去跟隨機行比,找到最近的,算進他的組裏
for i in range(len(data)):
# 默認最近的是第一個隨機行
c_index = 0
closest = distance(data[i], randomrows[c_index])
for j in range(1, k):
d = distance(randomrows[j], data[i])
# 找到了更近的
if d < closest:
c_index = j
closest = d
# 把數據放入最近的聚點的名下
new_clusts[c_index].append((rowNames[i], data[i]))
# 如果重新排之後數據沒變化,說明已完成,退出循環
if last_clusts == new_clusts: break
# 數據複製,直接=的話會一直相同,用copy複製出來
last_clusts = new_clusts.copy()
# randomrows.clear()
# for k in new_clusts:
#
# # 如果組裏沒東西,過
# if new_clusts[k] is None or len(new_clusts[k]) == 0: continue
# # 對於組中的每一列,求平均值,形成一個結果組,放進原來的隨機組裏
# randomrows.append(
# [sum([row[x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)]
# )
# 簡寫如下:
randomrows = [[sum([row[1][x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)] for k in
new_clusts if new_clusts[k] is not None and len(new_clusts[k]) != 0]
return new_clusts
由於函數使用了隨機的中心點作爲開始,所以每次聚類的結果都可能不同
對偏好的聚類
皮爾遜係數更適用於統計數據,如果我們採用的是0/1表示的有/無的數據,就需要其他的度量方法,Tanimoto係數可以滿足我們的需求
他是通過數據的交集除以並集得到元素的相關度的,結果越大說明元素越相關
def tanimoto(d1, d2):
# r1/r2表示d1/d2中的非無數據個數,sr表示交集個數,此處我採用0表示沒有數據,1表示有
r1, r2, sr = 0, 0, 0
for i in range(len(d1)):
if d1[i] == 1:
r1 += 1
if d2[i] == 1:
r2 += 1
if d1[i] == d2[i]:
sr += 1
# sr/(r1+r2-sr)得到的數據越大說明相似度越高,但是不利於我們看距離,
# 所以用1-sr/(r1+r2-sr)來表示距離,值越小說明距離越近,相似度越高
return 1.0-float(sr / (r1 + r2 - sr))
二維聚類
def scaledown(data, distance=pearson, rate=0.01):
n = len(data)
# 記錄上次的誤差值
last_err = None
# 記錄數據的真實距離,這是我們的目標結果
realDis = [[distance(data[j], data[i]) for j in range(n)] for i in range(n)]
# 每一列隨機生成一個座標點,代表這一列的位置
rpoints = [[random.random(), random.random()] for i in range(n)]
# 做一個雙層數組存儲數據信息
fakeDis = [[[0.0] for j in range(n)] for i in range(n)]
while True:
# 求模擬點之間的距離,視爲當前距離
for i in range(n):
for j in range(n):
fakeDis[i][j] = math.sqrt(sum([pow(rpoints[j][x] - rpoints[i][x], 2) for x in range(2)]))
grad = [[0.0, 0.0] for i in range(n)]
total_err = 0
for i in range(n):
for j in range(n):
if i == j: continue
# 記錄當前兩個點的誤差值
err = (fakeDis[i][j] - realDis[i][j]) / realDis[i][j]
# i來移動,移動的距離是i,j在x/y軸上的差值/當前距離X誤差
grad[i][0] += ((rpoints[i][0] - rpoints[j][0]) / fakeDis[i][j]) * err
grad[i][1] += ((rpoints[i][1] - rpoints[j][1]) / fakeDis[i][j]) * err
total_err += abs(err)
print(total_err)
# 移動之後如果會更混亂,則停止
if last_err is not None and total_err >= last_err: break
last_err = total_err
# 根據計算結果移動點的位置
for i in range(n):
rpoints[i][0] -= grad[i][0] * rate
rpoints[i][1] -= grad[i][1] * rate
return rpoints
繪製聚類結果
def drawPoints(points, labels, jpeg='sdc.jpeg'):
# 白色背景圖
image = Image.new('RGB', (2000, 2000), (255, 255, 255))
draw = ImageDraw.Draw(image)
# 取出移動完畢的點,拿到相應的名稱顯示出來
for i in range(len(points)):
x = points[i][0] * 1000
y = points[i][1] * 1000
draw.text((x, y), labels[i], fill=(0, 0, 0))
image.save(jpeg)
調用
colNames, rowNames, data = readFile()
drawPoints(scaledown(data), rowNames)