用 TensorFlow 實現 k-means 聚類代碼解析


k-means 是聚類中比較簡單的一種。用這個例子說一下感受一下 TensorFlow 的強大功能和語法。

一、 TensorFlow 的安裝

按照官網上的步驟一步一步來即可,我使用的是 virtualenv 這種方式。

二、代碼功能

在\([0,0]\) 到 \([1,1]\) 的單位正方形中,隨機生成 \(N\) 個點,然后把這 \(N\) 個點聚為 \(K\) 類。
最終結果如下,在 0.29s 內,經過 17 次迭代,找到了4個類的中心,並給出了各個點歸屬的類。

Found in 0.29 seconds
iterations, 17
Centroids:  [[ 0.24536976  0.73962539]
 [ 0.25338876  0.23666154]
 [ 0.75791192  0.25526255]
 [ 0.7544601   0.75478882]]
Cluster assignments: [1 1 2 ..., 0 2 1]  

而最小平方誤差的變化如下。可以看出逐漸變小。

三、代碼解析

  1. 引入相應的庫

    # copy from https://gist.github.com/dave-andersen/265e68a5e879b5540ebc
    # add summary
    import tensorflow as tf
    import numpy as np
    import time
    
  2. 定義問題規模以及一些變量

    N=10000  # 要被聚類的點的數目
    K=4      # 被聚為K類
    MAX_ITERS = 1000 #最大迭代數目
    test_writer = tf.summary.FileWriter("log")  # TensorBorad 數據存儲數目
    start = time.time() # 起始時間
    
  3. 初始化

    points = tf.Variable(tf.random_uniform([N,2])) #隨機生成N個點
    cluster_assignments = tf.Variable(tf.zeros([N], dtype=tf.int64)) #這個變量表示每個點所屬的類別,初始化為第0類
    
    # Silly initialization:  Use the first K points as the starting
    # centroids.  In the real world, do this better.
    centroids = tf.Variable(tf.slice(points.initialized_value(), [0,0], [K,2])) # 每個類的中心
    

    下面用數學表達式來說明一下,這里下標不是從 0 開始。
    \(N\) 個點 points 用下面式子來表達。
    \[[[x_1,y_1],[x_2,y_2],...[x_n,y_n]]\]
    每個點所屬類別 cluster_assignments 用下面式子表達:
    \[[\lambda_1,\lambda_2,...\lambda_n]\] 其中,\(\lambda_i<k\)
    每個類的中心 centroids 用下面是式子表達:
    \[[[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky]]\]

  4. 計算每個點距離每個類中心的距離

    # Replicate to N copies of each centroid and K copies of each
    # point, then subtract and compute the sum of squared distances.
    rep_centroids = tf.reshape(tf.tile(centroids, [N, 1]), [N, K, 2])
    rep_points = tf.reshape(tf.tile(points, [1, K]), [N, K, 2])
    sum_squares = tf.reduce_sum(tf.square(rep_points - rep_centroids), 
                                reduction_indices=2)
    

    先看tf.tile 函數。根據文檔tf.tile(centroids, [N, 1])函數執行完,結果如下:
    \[[[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky],[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky],...[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky]]\]
    即有 \(NK\)個點。然后經過 tf.reshape 函數,結果如下:
    \[[[[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky]],\\ [[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky]],\\ ...\\ [[c_1x,c_1y],[c_2x,c_2y],...[c_kx,c_ky]]]\]
    即可以看做一個\(N \times K\)的矩陣,每一個元素是一個點。這就是 rep_centroids的大致理解。
    同理可得,rep_points結果如下:
    \[[[[x_1,y_1],[x_1,y_1],...[x_1,y_1]],\\ [[x_2,y_2],[x_2,y_2],...[x_2,y_2]],\\ ...\\ [[x_n,y_n],[x_n,y_n],...[x_n,y_n]]]\]
    也可以看做一個\(N \times K\)的矩陣。每一行所有元素都相同,是第\(i\)個點。
    tf.square(rep_points - rep_centroids)結果如下: \[[[[[(x_1-c_1x)^2,(y_1-c_1y)^2],[(x_1-c_2x)^2,(y_1-c_2y)^2],...[(x_1-c_kx)^2,(y_1-c_ky)^2]],\\ [[(x_2-c_1x)^2,(y_2-c_1y)^2],[(x_2-c_2x)^2,(y_2-c_2y)^2],...[(x_2-c_kx)^2,(y_2-c_ky)^2]],\\ ...\\ [[(x_n-c_1x)^2,(y_n-c_1y)^2],[(x_n-c_2x)^2,(y_n-c_2y)^2],...[(x_n-c_kx)^2,(y_n-c_ky)^2]]]\]
    sum_squares的結果,是把\(N \times K\)矩陣的每一個元素變為一個值,如下:
    \[[[[d_{11},d_{12},...d_{1k}],\\ [d_{11},d_{22},...d_{2k}],\\ ...\\ [d_{n1},d_{n2},...d_{nk}]]\]
    其中
    \[d_{ij} = (x_i-c_jx)^2+(y_i-c_jy)^2 \]
    即 \(d_{ij}\) 是第 \(i\) 個點和第 \(k\) 個類的中心的距離。

  5. 判斷點所屬的類

    # Use argmin to select the lowest-distance point
    best_centroids = tf.argmin(sum_squares, 1)
    did_assignments_change = tf.reduce_any(tf.not_equal(best_centroids, 
                                                        cluster_assignments))  
    

    best_centroids是一行中,最小的數值的下標,即某個點應該被判定為哪一類。
    did_assignments_change表示新判定的類別和上次迭代的類別有沒有變化。

  6. 更新類別的中心

    def bucket_mean(data, bucket_ids, num_buckets):
    total = tf.unsorted_segment_sum(data, bucket_ids, num_buckets)
    count = tf.unsorted_segment_sum(tf.ones_like(data), bucket_ids, num_buckets)
    return total / count
    
    # 新的分簇點
    means = bucket_mean(points, best_centroids, K)
    currentSqure = tf.reduce_sum(tf.reduce_min(sum_squares,1))/N
    tf.summary.scalar('currentSqure', currentSqure)
    merged = tf.summary.merge_all()
    

    先看bucket_mean 函數。data 被傳入了被聚類的\(N\)個點,bucket_ids被傳入了每個點所屬的類別,num_buckets 是類別的數目。
    tf.unsorted_segment_sum可以理解為根據第二個參數(bucket_ids),把data分為不同的集合,然后分別對每一個集合求和。

    因此total即把\(N\)個點分為\(K\)個集合,然后對每一個集合求和。count則是求出每一個集合的個數。相除即得到每一個集合(即每一個類)的中心。
    means即新划分的各個類的中心。
    currentSqure是當前划分的最小平方誤差。然后把變量計入日志中。

  7. 指定迭代結構

    # Do not write to the assigned clusters variable until after
    # computing whether the assignments have changed - hence with_dependencies
    with tf.control_dependencies([did_assignments_change]):
        do_updates = tf.group(
            centroids.assign(means),
            cluster_assignments.assign(best_centroids))
    

    如果did_assignments_change有變化,那么把means賦值給centroids,把best_centroids賦值給cluster_assignments

  8. 啟動 session

    init = tf.initialize_all_variables()
    sess = tf.Session()
    sess.run(init)
    
    changed = True
    iters = 0
    
  9. 進行迭代

    while changed and iters < MAX_ITERS:
        iters += 1
        [summary,changed, _] = sess.run([merged,did_assignments_change, do_updates])
        test_writer.add_summary(summary,iters) #寫入日志
    
    test_writer.close()
    [centers, assignments] = sess.run([centroids, cluster_assignments])
    end = time.time()
    print ("Found in %.2f seconds" %(end-start)) 
    print(  "iterations,",iters )
    print("Centroids: " ,centers)
    print( "Cluster assignments:",assignments)
    print("cluster_assignments",cluster_assignments)
    

    指定最多迭代次數。值得注意的是,每一次迭代,都要把數據顯式寫入log中。

四、感受

  1. 機器學習的計算量太大了,並行性也很明顯。
  2. 對結果要有評估,否則意義不大。
    這個例子中,隨機生成的數據,根據算也可以進行聚類。但是意義顯然不大。
  3. 有一個圖形化的界面很重要。TensorBoard 可以直觀看出平方誤差在不斷變化

五、參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM