吳恩達機器學習課程-作業7-K-means聚類和主成分分析(python實現)

Machine Learning(Andrew) ex7-K-means Clustering and Principal Component Analysis

椰汁筆記

K-means Clustering

前面學習的內容都是監督學習,這將是我們學習的第一個非監督學習算法。
我們先把這個算法說清楚再說作業。

  • 這個算法是幹什麼的?
    將沒有標籤的數據,劃分成K組。通過這個算法我們可以將數據進行分類,具體可以應用到根據用戶數據將用戶進行分類,對各類用戶提供更加精細的服務等。
  • 這個算法是大概是怎麼做的?
    這裏一起宏觀地理解一下這個算法。舉個例子我們需要將下面地數據進行聚類,我很可以很輕鬆地看出數據大致可以分爲兩類,左下角和右上角。
    在這裏插入圖片描述
    K-means算法怎麼運行將這組數據分爲兩類呢?首先目標很明確需要分爲兩類,因此K=2。先選擇2個聚類簇中心,這個可以隨便選。
    在這裏插入圖片描述
    1.接着我們根據數據點距離紅色簇中心還是藍色簇中心更近,將點分爲兩簇。這裏使用歐氏距離表示遠近。
    在這裏插入圖片描述
    2.接着我們對於每一個簇,重新計算一個簇中心,這個中心就是這個簇所有點地平均位置。
    在這裏插入圖片描述
    接着以這兩個簇中心,重新將數據進行分類,也就是重複1步驟,接着再重複2步驟。這樣不斷迭代,當簇中心不再移動的時候,根據此時簇中心的分類結果就是最後的聚類結果。
    在這裏插入圖片描述
  • 這個算法的細節什麼樣的?
    首先這個算法的優化目標是所有的點離其所屬簇中心點的距離最小
    J(x(1),,x(m),μ1,,μK)=1mi=1m(x(i)μx(i)) J(x^{(1)},\dots,x^{(m)},\mu_1,\dots,\mu_K)=\frac{1}{m}\sum_{i=1}^{m}(x^{(i)}-\mu_{x^{(i)}})
    算法描述
    Input:K,{x(1),x(2),,x(m)}Randomly initialize K cluster centroids μ1,μ2,,μKRepeate{x(i)=index (from 1 to K) of cluster centroids closest to x(i),(for i=1 to m)μk=average of points assigned to cluster k,(for k=1 to K)} Input:K,\{x^{(1)},x^{(2)},\dots,x^{(m)}\} \\\textrm{Randomly initialize K cluster centroids }\mu_1,\mu_2,\dots,\mu_K \\\textrm{Repeate\{} \\x^{(i)}=\textrm{index (from 1 to K) of cluster centroids closest to }x^{(i)},(for\ i=1\ to\ m) \\\mu_k=\textrm{average of points assigned to cluster k},(for\ k=1\ to\ K) \\\}
  • 如何隨機初始化簇中心點呢?
    可以直接在數據中直接選擇點作爲簇中心點。但是這裏可能會因爲初始化的不同,導致優化到局部最優。爲了避免局部最優,我們可以通過多次不同的隨機初始化,來識別局部最優解。
  • 聚類的數量K怎麼確定?
    首先可以通過觀察數據集,來直觀判斷,但是隻適用與一維和二維的數據。另外一個可能可行的辦法是ELBOW method
    通過嘗試不同的K值,畫出最後算法收斂時的損失值與K的函數圖像,若存在一個像肘部一樣的突變點,即可作爲K值。
    但是可能畫出的曲線會很平滑,因此這個算法有時不太適用。

下面就開始作業吧

  • 1.1 Implementing K-means

首先需要實現,爲每個點找到最近的簇中心,作爲當前點的標籤
c(i)=j that minimizes x(i)μj2 c^{(i)}=j\textrm{ that minimizes }||x^{(i)}-\mu_j||^2

def find_closet_centroids(X, centroids):
    """
    尋找所屬簇
    :param X: ndarray,所有點
    :param centroids: ndarray,上一步計算出或初始化的簇中心
    :return: ndarray,每個點所屬於的簇
    """
    res = np.zeros((1,))
    for x in X:
        res = np.append(res, np.argmin(np.sqrt(np.sum((centroids - x) ** 2, axis=1))))
    return res[1:]

測試一下

    data = sio.loadmat("data\\ex7data2.mat")
    X = data['X']  # (300,2)
    init_centroids = np.array([[3, 3], [6, 2], [8, 5]])
    idx = find_closet_centroids(X, init_centroids)
    print(idx[0:3]) # [0. 2. 1.]

接着實現第二個部分,重新計算簇中心
μk=1CkiCkx(i) \mu_k=\frac{1}{C_k}\sum_{i\in C_k}x^{(i)}

def compute_centroids(X, idx):
    """
    計算新的簇中心
    :param X: ndarray,所有點
    :param idx: ndarray,每個點對應的簇號
    :return: ndarray,所有新簇中心
    """
    K = int(np.max(idx)) + 1
    m = X.shape[0]
    n = X.shape[-1]
    centroids = np.zeros((K, n))
    counts = np.zeros((K, n))
    for i in range(m):
        centroids[int(idx[i])] += X[i]
        counts[int(idx[i])] += 1
    centroids = centroids / counts
    return centroids

繼續使用上個例子測試

	print(compute_centroids(X, idx))
	# [[2.42830111 3.15792418]
 	#  [5.81350331 2.63365645]
 	#  [7.11938687 3.6166844 ]]

將這兩步封裝成一個完整的K-means算法,這裏用到的隨機初始化簇中心,在1.3實現,我直接先用一下,具體的函數看1.3。而且算法的迭代輪數可以指定一個很大的輪數保證計算完後算法收斂。我選擇每次計算當前的目標值,當目標值不變時,算法收斂退出。
需要先實現損失函數

def cost(X, idx, centrodis):
    c = 0
    for i in range(len(X)):
        c += np.sum((X[i] - centrodis[int(idx[i])]) ** 2)
    c /= len(X)
    return c
def k_means(X, K):
    """
    k-means聚類算法
    :param X: ndarray,所有的數據
    :param K: int,聚類的類數
    :return: tuple,(idx, centroids_all)
                idx,ndarray爲每個數據所屬類標籤
                centroids_all,[ndarray,...]計算過程中每輪的簇中心
    """
    centroids = random_initialization(X, K)
    centroids_all = [centroids]
    idx = np.zeros((1,))
    last_c = -1
    now_c = -2
    # iterations = 200
    # for i in range(iterations):
    while now_c != last_c:  # 當收斂時結束算法,或者可以利用指定迭代輪數
        idx = find_closet_centroids(X, centroids)
        last_c = now_c
        now_c = cost(X, idx, centroids)
        centroids = compute_centroids(X, idx)
        centroids_all.append(centroids)
    return idx, centroids_all
  • 1.2 K-means on example dataset

對數據集進行聚類

    data = sio.loadmat("data\\ex7data2.mat")
    X = data['X']  # (300,2)
    idx, centroids_all = k_means(X, 3)

這裏的聚類算中返回了每輪計算的簇中心,就是爲了後面的可視化簇中心變化過程,畫出聚類結果和簇中心的移動路線

def visualizing(X, idx, centroids_all):
    """
    可視化聚類結果和簇中心的移動過程
    :param X: ndarray,所有的數據
    :param idx: ndarray,每個數據所屬類標籤
    :param centroids_all: [ndarray,...]計算過程中每輪的簇中心
    :return: None
    """
    plt.scatter(X[..., 0], X[..., 1], c=idx)
    xx = []
    yy = []
    for c in centroids_all:
        xx.append(c[..., 0])
        yy.append(c[..., 1])
    plt.plot(xx, yy, 'rx--')
    plt.show()

看一看結果

	visualizing(X, idx, centroids_all)

在這裏插入圖片描述
可能會出現局部最優的情況
在這裏插入圖片描述

  • 1.3 Random initialization

一般簇中心的初始化是從數據中隨機選擇K組

def random_initialization(X, K):
    """
    隨機選擇K組數據,作爲簇中心
    :param X: ndarray,所有點
    :param K: int,聚類的類數
    :return: ndarray,簇中心
    """
    res = np.zeros((1, X.shape[-1]))
    m = X.shape[0]
    rl = []
    while True:
        index = random.randint(0, m)
        if index not in rl:
            rl.append(index)
        if len(rl) >= K:
            break
    for index in rl:
        res = np.concatenate((res, X[index].reshape(1, -1)), axis=0)
    return res[1:]
  • 1.4 Image compression with K-means

接下來將聚類算法用於圖片壓縮上,先講一講思路
圖片都是由若干像素點構成的,每個像素點都有顏色,這個顏色一般是RGB編碼,意思是所有顏色都可以通過Red,Green,Blue來表示,RGB編碼下三種顏色每個通過一個8比特的整數來表示強度,因此一個像素點需要24bit來表示顏色。
圖片上的每個像素點都需要使用24bit存儲顏色,我們的壓縮方法是,通過聚類將圖片上的顏色分爲16種,我們只存儲這十六種顏色,每個像素點上只需要4比特存儲對應顏色的序號,即可達到有損壓縮圖片的目的。

首先引入需要的聚類算法實現,這裏單獨創建了一個文件

import ex7_K_means_Clustering_and_PCA.k_means_clustering as k_means

進行圖片壓縮,將rgb顏色作爲聚類的點數據,每個顏色的表示是一個列表包含三個數。這裏聚類結束後構造新的圖片矩陣,不是很規範,應該是直接存儲bit。

def compress(image, colors_num):
    """
    壓縮圖片
    :param image: ndarray,原始圖片
    :param colors_num: int,壓縮後的顏色數量
    :return: (ndarray,ndarray),第一個每個像素點存儲一個值,第二個爲顏色矩陣
    """
    d1, d2, _ = image.shape
    raw_image = image.reshape(d1 * d2, -1)  # 展開成二維數組
    idx, centroids_all = k_means.k_means(raw_image, colors_num)
    colors = centroids_all[-1]
    compressed_image = np.zeros((1, 1))  # 構造壓縮後的圖片格式
    for i in range(d1 * d2):
        compressed_image = np.concatenate((compressed_image, idx[i].reshape(1, -1)), axis=0)
    compressed_image = compressed_image[1:].reshape(d1, d2, -1)
    return compressed_image, colors

爲了可視化效果,還需要將壓縮後的圖片格式轉化爲可以顯示的標準格式

def compressed_format_to_normal_format(compressed_image, colors):
    """
    將壓縮後的圖片轉爲正常可以顯示的圖片格式
    :param compressed_image: ndarray,壓縮後的圖片,存儲顏色序號
    :param colors: ndarray,顏色列表
    :return: ndarray,正常的rgb格式圖片
    """
    d1, d2, _ = compressed_image.shape
    normal_format_image = np.zeros((1, len(colors[0])))
    compressed_image = compressed_image.reshape(d1 * d2, -1)
    for i in range(d1 * d2):
        normal_format_image = np.concatenate((normal_format_image, colors[int(compressed_image[i][0])].reshape(1, -1)),
                                             axis=0)
    normal_format_image = normal_format_image[1:].reshape(d1, d2, -1)
    return normal_format_image

看一看效果

    image = plt.imread("data\\bird_small.png")  # (128,128,3)
    plt.subplot(1, 2, 1)
    plt.imshow(image)
    plt.axis('off')
    plt.title("raw image")
    plt.subplot(1, 2, 2)
    compressed_image, colors = compress(image, 16)
    print(compressed_image.shape, colors.shape)
    plt.imshow(compressed_format_to_normal_format(compressed_image, colors))
    plt.axis('off')
    plt.title("compressed image")
    plt.show()

在這裏插入圖片描述


Principal Component Analysis

這部分我覺得是這個課程裏面比較難理解的一部分,說實話我的這部分我也理解地不是很好。

  • 主成分分析法是幹什麼用的?
    數據降維,話句話說就是將數據地特徵數量變少,但又不是簡單地刪除特徵。數據降維地目的可以是壓縮數據,減少數據的存儲空間,讓算法提速;也可以是將數據降到二維或者三維進行可視化
  • 主成分分析法在做什麼?
    上面說到主成分分析法用於數據降維,大概理解一下它怎麼做的。現在我們數據維度爲n,我想通過降維讓數據變成k維。
    那麼PCA做的就是對於n維空間的數據,尋找一個K維的“面”,讓這些數據到這個“面”的距離最短,這個距離又叫做投影誤差。
    找到這個“面”後,將n維空間的點投影到這個“面”,因此所有點都投影到了k維空間,因此可以特徵數量變爲了k。
    假設n=2,k=1,那麼就是將二維平面上的點投影到一個向量上。假設n=3,k=2,那麼就是將三維空間的點投影到一個平面上。
  • 主成分分析法具體怎麼做呢?
    對於數據要從n維降到k維
    首先對數據進行feature scaling/mean normalization,也就是歸一化
    其次計算協方差矩陣
    Σ=1mXTX \Sigma=\frac{1}{m}X^TX
    接着計算sigma矩陣的“特徵向量”,這裏使用奇異值分解(single value decomposition)。
    [U,S,V]=svd(Σ) [U,S,V]=svd(\Sigma)
    利用U計算新的特徵,若要下降到K維,取U的前K列構成新矩陣
    Z=UreduceTX Z=U_{reduce}^TX
  • 2.1 Example Dataset
    data = sio.loadmat("data\\ex7data1.mat")
    X = data['X']  # (50,2)
    plt.scatter(X[..., 0], X[..., 1], marker='x', c='b')
    plt.show()

在這裏插入圖片描述

  • 2.2 Implementing PCA

實現PCA首先要做的就是對數據的處理進行歸一化,注意這裏的方差的計算,默認ddof爲0,通常情況下是使用ddof=1,就是方差計算中最後除以m還是m-1的不同。

def data_preprocess(X):
    """
    數據歸一化
    :param X: ndarray,原始數據
    :return: (ndarray.ndarray,ndarray),處理後的數據,每個特徵均值,每個特徵方差
    """
    mean = np.mean(X, axis=0)
    std = np.std(X, axis=0, ddof=1)  # 默認ddof=0, 這裏一定要修改
    return (X - mean) / std, mean, std

numpy中有奇異值分解的功能,直接使用

def pca(X):
    sigma = X.T.dot(X) / len(X)  # (n,m)x(m,n) (n,n)
    u, s, v = np.linalg.svd(sigma)  # u(n,n) s(n,), v(n,n)
    return u, s, v
  • 2.3 Dimensionality Reduction with PCA

使用PCA進行降維

def project_data(X, U, K):
    """
    數據降維
    :param X: ndarray,原始數據
    :param U: ndarray,奇異值分解後的U
    :param K: int,目標維度
    :return: ndarray,降維後的數據
    """
    return X.dot(U[..., :K])

逆向思維,還可以進行降維後的升維

def reconstruct_data(Z, U, K):
    """
    數據升維
    :param Z: ndarray,降維後的數據
    :param U: ndarray,奇異值分解後的U
    :param K: int,降維的維度
    :return: ndarray,原始數據
    """
    return Z.dot(U[..., :K].T)

測試一下

    data = sio.loadmat("data\\ex7data1.mat")
    X = data['X']  # (50,2)
    normalized_X, _, _ = data_preprocess(X)
    u, _, _ = pca(normalized_X)  # (2,2)
    Z = project_data(normalized_X, u, 1)
    print(Z[0]) # [1.48127391]
    rec_X = reconstruct_data(Z, u, 1)
    print(rec_X[0]) # [-1.04741883 -1.04741883]

將這個投影可視化

    plt.scatter(normalized_X[..., 0], normalized_X[..., 1], marker='x', c='b', label='normalized x')
    plt.scatter(rec_X[..., 0], rec_X[..., 1], marker='x', c='r', label='reconstructed x')
    plt.title("Visualizing the projections")
    for i in range(len(normalized_X)):
        plt.plot([normalized_X[i][0], rec_X[i][0]], [normalized_X[i][1], rec_X[i][1]], 'k--')
    plt.xlim((-3, 2))
    plt.ylim((-3, 2))
    plt.legend()
    plt.show()

在這裏插入圖片描述

  • 2.4 Face Image Dataset

將PCA應用到人類數據集上,當前的每張人臉圖片爲1024像素,因此爲1024維。我們的目標是將數據降維到36像素,也就是36維。
這裏單獨創建一個文件,先引入實現好的PCA

import ex7_K_means_Clustering_and_PCA.PCA as pca

使用PCA進行降維

    data = sio.loadmat("data\\ex7faces.mat")
    X = data['X']  # (5000,1024)
    nor_X, _, _ = pca.data_preprocess(X)
    u, _, _ = pca.pca(nor_X)
    Z = pca.project_data(nor_X, u, 36)
    rec_X = pca.reconstruct_data(Z, u, 36)

將前後的圖片可視化對比一下,記得要想人臉位置爲正向需要轉置一下

def visualizing_images(X, d):
    """
    可視化圖片
    :param X: ndarray,圖片
    :param d: int,一行展示多少張圖片
    :return: None
    """
    m = len(X)
    n = X.shape[-1]
    s = int(np.sqrt(n))
    for i in range(1, m + 1):
        plt.subplot(m / d, d, i)
        plt.axis('off')
        plt.imshow(X[i - 1].reshape(s, s).T, cmap='Greys_r')  # 要把臉擺正需要轉置
    plt.show()
    visualizing_images(X[:25], 5)
    visualizing_images(rec_X[:25], 5)

可以看到臉部的特徵還是保留了的
在這裏插入圖片描述在這裏插入圖片描述


總結

  • K的選擇
    1mi=1mx(i)xapprox(i)21mi=1mx(i)20.01 \frac{\frac{1}{m}\sum_{i=1}^m||x^{(i)}-x^{(i)}_{approx}||^2}{\frac{1}{m}\sum_{i=1}^m||x^{(i)}||^2}\le0.01
    選擇K使上面的值儘可能小,一般是小於0.01。
    這個公式的上面部分是平均平方投影誤差,下面部分是平均數據方差。這個比值的意思是降維損失值。0.01可以理解爲99%的方差被保留。
  • PCA可以對監督學習算法進行加速
  • PCA不推薦用於解決過擬合問題,因爲它沒有考慮y,會忽略一些信息。過擬合問題還是使用正則化解決。

完整的代碼會同步 在我的github

歡迎指正錯誤

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