吳恩達機器學習課程-作業8-異常檢測和推薦系統(python實現)

Machine Learning(Andrew) ex8-Anomaly Detection and Recommender Systems

椰汁筆記

Anomaly detection

這是也是一個非監督學習算法

  • 異常檢測做什麼?
    從一組數據中找到那些“異常”的數據,基於高斯分佈(正太分佈)。生活中的很多事情都是符合高斯分佈的,對於數據也是如此。我們通過參數估計,估計出數據符合的高斯分佈參數,當其中的數據分佈在高斯分佈中概率很小的地方,就認爲這是異常數據。
  • 具體怎麼做?
    選擇可以描述異常狀態的特徵作爲輸入
    x(1),x(2),,x(m) x^{(1)},x^{(2)},\dots,x^{(m)}
    根據以往的數據估計高斯分佈的參數(對每一個特徵)
    μj=1mi=0mxj(i)σj2=1mi=0m(xj(i)μj)2 \mu_j=\frac{1}{m}\sum_{i=0}^{m}x_j^{(i)} \\\sigma_j^2=\frac{1}{m}\sum_{i=0}^{m}(x_j^{(i)}-\mu_j)^2
    對於一個新的數據,預測其發生概率
    P(x)=j=1nP(xj;μj;σj2)=j=1n12πσje(xjμ)2j2σj2 P(x)=\prod_{j=1}^nP(x_j;\mu_j;\sigma_j^2)=\prod_{j=1}^n\frac{1}{\sqrt{2\pi}\sigma_j}e^{-\frac{(x_j-\mu_)^2j}{2\sigma_j^2}}
    當概率小於一定閾值後認定爲異常。
  • 這個算法有什麼缺點?
    可以看到,之前的模型中對每個特徵都是獨立地處理,最後的組合只是簡單的相乘。這樣就是存在一些問題,特徵之間的關聯沒有捕捉到。
    升級的方式就是多元高斯分佈,將不再單獨考慮特徵,而是將特徵一起考慮,自動捕捉之間的關聯。
    參數的估計變爲,其中的sigma爲協方差矩陣
    μ=1mi=1mx(i)Σ=1mi=1m(x(i)μ)(x(i)μ)T \mu=\frac{1}{m}\sum_{i=1}^{m}x^{(i)} \\\Sigma=\frac{1}{m}\sum_{i=1}^{m}(x^{(i)}-\mu)(x^{(i)}-\mu)^T
    預測變爲
    P(x;μ;Σ)=1(2π)n2Σ12e12(xμ)TΣ1(xμ) P(x;\mu;\Sigma)=\frac{1}{(2\pi)^{\frac{n}{2}}|\Sigma|^{\frac{1}{2}}}e^{-\frac{1}{2}(x-\mu)^T\Sigma^{-1}(x-\mu)}
    這個模型有個前提就是m>n,而且協方差矩陣是非奇異矩陣。另外這個計算也是複雜的。
  • 怎麼評估算法的效果?
    使用標籤化的數據,計算F1score
  • 怎麼感覺異常檢測可以用監督學習做呢?
    總結一下異常檢測和監督學習的適合場景
    • 異常檢測
      (1)異常數據很少,y=1的數據很少
      (2)正常數據很多,y=0的數據很多
      (3)異常的類型太多
      (4)未來異常的類型是未知的
      典型應用:欺騙檢測,監測機器
    • 監督學習
      (1)y=0和y=1的數據都很多
      (2)異常的例子夠多,且未來的異常與以往相同
      典型應用:垃圾郵件分類,天氣預測

下面進行作業,先可視化數據

data = sio.loadmat("data\\ex8data1.mat")
X = data['X']  # (307,2)
plt.scatter(X[..., 0], X[..., 1], marker='x', label='point')
plt.show()

在這裏插入圖片描述

  • 1.1 Gaussian distribution
    假設我們已經知道了高斯模型的參數,計算概率
def gaussian_distribution(X, mu, sigma2):
    """
    根據高斯模型參數,計算概率
    :param X: ndarray,數據
    :param mu: ndarray,均值
    :param sigma2: ndarray,方差
    :return: ndarray,概率
    """
    p = (1 / np.sqrt(2 * np.pi * sigma2)) * np.exp(-(X - mu) ** 2 / (2 * sigma2))
    return np.prod(p, axis=1)  # 橫向累乘
  • 1.2 Estimating parameters for a Gaussian

根據數據估計出模型的參數

def estimate_parameters_for_gaussian_distribution(X):
    """
    估計數據估計參數
    :param X: ndarray,數據
    :return: (ndarray,ndarray),均值和方差
    """
    mu = np.mean(X, axis=0)  # 計算方向因該是沿着0,遍歷每組數據
    sigma2 = np.var(X, axis=0)  # N-ddof爲除數,ddof默認爲0
    return mu, sigma2

根據估計出的參數,畫出高斯分佈的等高線

def visualize_contours(mu, sigma2):
    """
    畫出高斯分佈的等高線
    :param mu: ndarray,均值
    :param sigma2: ndarray,方差
    :return: None
    """
    x = np.linspace(5, 25, 100)
    y = np.linspace(5, 25, 100)
    xx, yy = np.meshgrid(x, y)
    X = np.concatenate((xx.reshape(-1, 1), yy.reshape(-1, 1)), axis=1)
    z = gaussian_distribution(X, mu, sigma2).reshape(xx.shape)
    cont_levels = [10 ** h for h in range(-20, 0, 3)]  # 當z爲當前列表的值時才繪出等高線
    plt.contour(xx, yy, z, cont_levels)
    mu, sigma2 = estimate_parameters_for_gaussian_distribution(X)
    p = gaussian_distribution(X, mu, sigma2)
    visualize_contours(mu, sigma2)

在這裏插入圖片描述

  • 1.3 Selecting the threshold, ε

要判斷出是否爲異常,需要一個閾值。可以通過取不同的閾值,計算標籤數據的F1score等量化的評價的標準,選取最好的閾值。
首先需要對模型計算出的結果,進行誤差分析,計算F1-score。這裏需要注意,F1-score,precision和recall的計算,需要判斷分母爲0的情況。

def error_analysis(yp, yt):
    """
    計算誤差分析值F1-score
    :param yp: ndarray,預測值
    :param yt: ndarray,實際值
    :return: float,F1-score
    """
    tp, fp, fn, tn = 0, 0, 0, 0
    for i in range(len(yp)):
        if yp[i] == yt[i]:
            if yp[i] == 1:
                tp += 1
            else:
                tn += 1
        else:
            if yp[i] == 1:
                fp += 1
            else:
                fn += 1
    precision = tp / (tp + fp) if tp + fp else 0  # 防止除以0
    recall = tp / (tp + fn) if tp + fn else 0
    f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0
    return f1

封裝閾值選擇函數,閾值從預測值的最小到最大的範圍中遍歷,計算F1-score選擇出最好的閾值選擇

def select_threshold(yval, pval):
    """
    根據預測值和真實值確定最好的閾值
    :param yval: ndarray,真實值(這裏是0或1)
    :param pval: ndarray,預測值(這裏是[0,1]的概率)
    :return: (float,float),閾值和F1-score
    """
    epsilons = np.linspace(min(pval), max(pval), 1000)
    l = np.zeros((1, 2))
    for e in epsilons:
        ypre = (pval < e).astype(float)
        f1 = error_analysis(ypre, yval)
        l = np.concatenate((l, np.array([[e, f1]])), axis=0)
    index = np.argmax(l[..., 1])
    return l[index, 0], l[index, 1]

測試

    Xval = data['Xval']  # (307,2)
    yval = data['yval']  # (307,1)
    e, f1 = select_threshold(yval.ravel(), gaussian_distribution(Xval, mu, sigma2))
    print('best choice of epsilon is ', e, ',the F1 score is ', f1)
    # best choice of epsilon is  8.999852631901393e-05 ,the F1 score is  0.8750000000000001

利用選擇出來的閾值完善模型,對異常進行預測。

def detection(X, e, mu, sigma2):
    """
    根據高斯模型檢測出異常數據
    :param X: ndarray,需要檢查的數據
    :param e: float,閾值
    :param mu: ndarray,均值
    :param sigma2: ndarray,方差
    :return: ndarray,異常數據
    """
    p = gaussian_distribution(X, mu, sigma2)
    anomaly_points = np.array([X[i] for i in range(len(p)) if p[i] < e])
    return anomaly_points

我們將異常的數據點圈出來

def visualize_dataset(X):
    plt.scatter(X[..., 0], X[..., 1], marker='x', label='point')
    
def circle_anomaly_points(X):
    plt.scatter(X[..., 0], X[..., 1], s=80, facecolors='none', edgecolors='r', label='anomaly point')
    anomaly_points = detection(X, e, mu, sigma2)
    circle_anomaly_points(anomaly_points)
    plt.title('anomaly detection')
    plt.legend()
    plt.show()

在這裏插入圖片描述

  • 1.4 High dimensional dataset

異常檢測的方法都寫好了,對於高維的數據,也是一樣的操作就行

    data2 = sio.loadmat("data\\ex8data2.mat")
    X = data2['X']
    Xval = data2['Xval']
    yval = data2['yval']
    mu, sigma2 = estimate_parameters_for_gaussian_distribution(X)
    e, f1 = select_threshold(yval.ravel(), gaussian_distribution(Xval, mu, sigma2))
    anomaly_points = detection(X, e, mu, sigma2)
    print('\n\nfor this high dimensional dataset \nbest choice of epsilon is ', e, ',the F1 score is ', f1)
    print('the number of anomaly points is', anomaly_points.shape[0])
# for this high dimensional dataset 
# best choice of epsilon is  1.3786074982000235e-18 ,the F1 score is  0.6153846153846154
# the number of anomaly points is 117

Recommender Systems

拿電影網站來說,通常我們都會給自己看過的電影評分,作爲網站運營者要進行其他電影的推薦,那麼就需要根據你之前的評分。
假設我們對每個電影給出一個向量x代表一系列特徵,那麼根據用戶對部分電影的評分,我們可以使用線性迴歸的方式求出一個向量theta這個向量代表用戶的一系列特徵,那麼我們就可以通過計算theta^T·x得到對於用戶未評分電影的預測評分,就可以通過這個進行推薦。
從令一個角度看,假設我們對每個用戶給出一個向量theta,與上面相同的方法,也可以求出電影的特徵向量x,同樣進行電影評分預測。
但是不管是用戶特徵還是電影特徵都難以手動確定,那怎麼實現推薦系統呢?
協同過濾算法
現實情況是我們都不知道用戶特徵和電影特徵,只知道用戶對哪些電影評分,評分的分數。協同過濾算法的思路是我們直接一起訓練用戶特徵和電影特徵,首先隨機初始化用戶特徵和電影特徵,在每一次梯度下降過程中,先後更新用戶特徵和電影特徵,最後就能得到用戶特徵和電影特徵並進行預測。
爲什麼可以先後跟新用戶特徵和電影特徵呢?因爲他們的優化目標函數是相同的。
J(x(1),,x(nm);θ(1),,θ(nu))=12(i,j):r(i,j)=1((θ(j))Tx(i)y(i,j))2+λ2i=1nmk=1n(xk(i))2+λ2j=1nuk=1n(θk(j))2 J(x^{(1)},\dots,x^{(n_m)};\theta^{(1)},\dots,\theta^{(n_u)})=\frac{1}{2}\sum_{(i,j):r(i,j)=1}((\theta^{(j)})^Tx^{(i)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum_{i=1}^{n_m}\sum{k=1}{n}(x_k^{(i)})^2+\frac{\lambda}{2}\sum_{j=1}^{n_u}\sum{k=1}{n}(\theta_k^{(j)})^2
只需要保證用戶特徵和電影特徵的特徵數量是相同的即可
具體的算法流程是:
1.隨機初始化用戶特徵theta和電影特徵x,這裏需要保證兩個維度一樣
2.使用梯度下降或其他優化算法更新兩個特徵參數
xk(i)=xk(i)α((i,j):r(i,j)=1((θ(j))Tx(i)y(i,j))θk(j)+λxk(i))θk(j)=θk(j)α((i,j):r(i,j)=1((θ(j))Tx(i)y(i,j))xk(i)+λθk(j)) x_k^{(i)}=x_k^{(i)}-\alpha(\sum_{(i,j):r(i,j)=1}((\theta^{(j)})^Tx^{(i)}-y^{(i,j)})\theta_k^{(j)}+\lambda x_k^{(i)}) \\\theta_k^{(j)}=\theta_k^{(j)}-\alpha(\sum_{(i,j):r(i,j)=1}((\theta^{(j)})^Tx^{(i)}-y^{(i,j)})x_k^{(i)}+\lambda \theta_k^{(j)})
3.用着兩個特徵預測評分,進行排序推薦
predict=θTx predict=\theta^Tx

  • 2.2.1 Collaborative filtering cost function

實現目標函數,參數應該是一維向量,需要將用戶特徵和電影特徵一維序列化,在計算損失值內部還原爲原來的形式

def serialize(X, theta):
    """
    參數一維向量化
    :param X: ndarray,電影特徵
    :param theta: ndarray,用戶特徵
    :return: ndarray,一維化向量
    """
    return np.concatenate((X.flatten(), theta.flatten()), axis=0)


def deserialize(params, nm, nu, nf):
    """
    將一維化參數向量還原
    :param params: ndarray,一維化的用戶特徵和電影特徵
    :param nm: int,電影數量
    :param nu: int,用戶數量
    :param nf: int,特徵數量
    :return: (ndarray,ndarray) 電影特徵,用戶特徵
    """
    X = params[:nm * nf].reshape(nm, nf)
    theta = params[nm * nf:].reshape(nu, nf)
    return X, theta

然後我們再來實現損失值計算,這裏直接將正則化項加入。這裏與線性迴歸不同的地方就是不用添加x0和theta0,正則化項裏面就不需要去掉0項

def collaborative_filtering_cost(params, Y, R, nm, nu, nf, l=0.0):
    """
    協同過濾算法目標函數
    :param params: ndarray,一維化的用戶特徵和電影特徵
    :param Y: ndarray,表明用戶的評分
    :param R: ndarray,表明哪些用戶評價了哪些電影
    :param nm: int,電影數量
    :param nu: int,用戶數量
    :param nf: int,特徵數量
    :param l: float,懲罰參數
    :return: float,損失值
    """
    X, theta = deserialize(params, nm, nu, nf)
    part1 = np.sum(((X.dot(theta.T) - Y) ** 2) * R) / 2
    part2 = l * np.sum(theta ** 2) / 2
    part3 = l * np.sum(X ** 2) / 2
    return part1 + part2 + part3

測試不帶正則化項時,直接將參數賦爲 0

    data1 = sio.loadmat("data\\ex8_movies.mat")
    Y = data1["Y"]  # (1682,943)
    R = data1["R"]  # (1682,943)
    data2 = sio.loadmat("data\\ex8_movieParams.mat")
    X = data2["X"]  # (1682,10)
    theta = data2["Theta"]  # (943,10)
    nu = data2["num_users"][0][0]  # (1,1) 943
    nm = data2["num_movies"][0][0]  # (1,1) 1682
    nf = data2["num_features"][0][0]  # (1,1) 10
    # 題目中計算數據不是全部數據,取nm=5,nu=4,nf=3,值爲22.224603725685675
    nu = 4
    nm = 5
    nf = 3
    X = X[:nm, :nf]
    theta = theta[:nu, :nf]
    Y = Y[:nm, :nu]
    R = R[:nm, :nu]
    print(collaborative_filtering_cost(serialize(X, theta), Y, R, nm, nu, nf))
    # 22.224603725685675
  • 2.2.2 Collaborative filtering gradient

實現協同過濾梯度下降,這裏也先實現正則部分

def collaborative_filtering_gradient(params, Y, R, nm, nu, nf, l=0.0):
    """
    協同過濾梯度下降
    :param params: ndarray,一維化的用戶特徵和電影特徵
    :param Y: ndarray,表明用戶的評分
    :param R: ndarray,表明哪些用戶評價了哪些電影
    :param nm: int,電影數量
    :param nu: int,用戶數量
    :param nf: int,特徵數量
    :param l: float,懲罰參數
    :return: ndarray,跟新後的一維化的用戶特徵和電影特徵
    """
    X, theta = deserialize(params, nm, nu, nf)
    g_X = ((X.dot(theta.T) - Y) * R).dot(theta) + l * X
    g_theta = ((X.dot(theta.T) - Y) * R).T.dot(X) + l * theta
    return serialize(g_X, g_theta)
  • 2.2.3 Regularized cost function

上面已經完成,直接使用,當懲罰參數爲1.5時的

    # 正則化時選擇的lambda爲1.5
    print(collaborative_filtering_cost(serialize(X, theta), Y, R, nm, nu, nf, 1.5))
    # 31.34405624427422
  • 2.2.4 Regularized gradient

已經實現
測試需要用到梯度下降檢測,就是求一個近視的導數。不知道爲什麼,梯度下降檢測運行得太慢,沒有跑出結果。

def check_gradient(params, Y, R, nm, nu, nf):
    # X, theta = deserialize(params, nm, nu, nf)
    e = 0.0001
    m = len(params)
    g_params = np.zeros((m,))
    for i in range(m):
        temp = np.zeros((m,))
        temp[i] = e
        g_params[i] = (collaborative_filtering_cost(params + temp, Y, R, nm, nu, nf) -
                       collaborative_filtering_cost(params - temp, Y, R, nm, nu, nf)) / (2 * e)
    return g_params
  • 2.3 Learning movie recommendations

這裏訓練模型,首先添加自定義數據

    # 先添加一組自定義的用戶數據
    my_ratings = np.zeros((1682, 1))
    my_ratings[0] = 4
    my_ratings[97] = 2
    my_ratings[6] = 3
    my_ratings[11] = 5
    my_ratings[53] = 4
    my_ratings[63] = 5
    my_ratings[65] = 3
    my_ratings[68] = 5
    my_ratings[182] = 4
    my_ratings[225] = 5
    my_ratings[354] = 5

    Y = np.concatenate((Y, my_ratings), axis=1)
    R = np.concatenate((R, my_ratings > 0), axis=1)
    nu += 1

利用scipy.optimize.minimize()的高級優化方法去做

    params = serialize(np.random.random((nm, nf)), np.random.random((nu, nf)))
    res = opt.minimize(fun=collaborative_filtering_cost, x0=params, args=(Y, R, nm, nu, nf, 10),
                       method='TNC',
                       jac=collaborative_filtering_gradient)

利用優化後的參數進行預測

    trained_X, trained_theta = deserialize(res.x, nm, nu, nf)
    predict = trained_X.dot(trained_theta.T)
    my_predict = predict[..., -1]

選出十個最高的評分,顯示推薦結果。我們還要讀入電影名。

    # 讀入電影標籤
    f = open("data\\movie_ids.txt", "r")
    movies = []
    for line in f.readlines():
        movies.append(line.split(' ', 1)[-1][:-1])
	# 先打印構造的用戶數據
    for i in range(len(my_ratings)):
        if my_ratings[i] > 0:
            print(my_ratings[i], movies[i])
    # 從預測結果中選10個最優推薦
    # 由於訓練初始化參數的不同會導致最後的結果不同
    print("Top recommendations for you:")
    for i in range(10):
        index = int(np.argmax(my_predict))
        print("Predicting rating ", my_predict[index], " for movie ", movies[index])
        my_predict[index] = -1

在這裏插入圖片描述
由於初始化參數的問題,推薦的結果可能出現差異。我們用可量化的評價方法進行評價看看模型的誤差怎麼樣就行了。這裏的評價是將預測和實際評分計算方差,未評分的不計算。可以看到還是不錯

    # 用均方誤差來評價
    Y = Y.flatten()
    R = R.flatten()
    predict = predict.flatten()
    true_y = []
    pre_y = []
    for i in range(len(Y)):
        if R[i] == 1:
            true_y.append(Y[i])
            pre_y.append(predict[i])
    print("當前訓練對嶽原始數據集的均方誤差", mean_squared_error(true_y, pre_y))
    # 當前訓練對嶽原始數據集的均方誤差 0.6400023155268085

另外當用戶沒有任何評分記錄時,學習結果將會是theta爲0.爲了避免所有都是0,導致計算出的所有預測評分都是0,我們可以先將評分數據減去均值。最後預測再加上。


最後在再記錄一下,後面課程學習的內容

隨機梯度下降

之前我們使用的都是批量梯度下降,每次迭代需要遍歷所有數據。在現在,爲了使算法更加的好,通常都會使用大量的數據去訓練算法。隨着數據量的增多,數據無法完全存儲在內存中,數據讀取需要到磁盤,反覆讀取操作將會是非常地費時。
因此隨機梯度下降是個很好的選擇,將數據排列順序打亂,每次迭代只根據一組數據進行擬合。外層循環爲1-10次,通常情況下只需要遍歷一次數據即可
Repeat{
for i=0 to m:
θj=θjα(hθ(x(i))y(i))xj(i)(for j=0 to n) \theta_j=\theta_j-\alpha(h_\theta(x^{(i)})-y^{(i)})x_j^{(i)}\textrm{(for j=0 to n)}
}
另外還有一種叫做mini-batch gradient descent,他的想法就是每次迭代使用b個樣本而不是隨機梯度下降中的一個樣本,這個b一般取2到100。當向量化做得好時,可以比隨機梯度下降的速度更快。
Repeat{
while(i<m){
θj=θjα1bk=1i+b(hθ(x(k))y(k))xj(k)(for j=0 to n)i=i+b \theta_j=\theta_j-\alpha\frac{1}{b}\sum_{k=1}^{i+b}(h_\theta(x^{(k)})-y^{(k)})x_j^{(k)}\textrm{(for j=0 to n)} \\i=i+b
}
}

怎麼判斷隨機梯度下降收斂呢?
每迭代1000輪,計算最後1000個樣本的平均損失值,並畫出圖像
這個1000的選擇需要注意,如果太小圖像產生很多噪聲,越大圖像越平滑。

同時如果學習速率不變,隨機梯度下降的收斂值會在局部最小周圍擾動。爲了使擾動儘可能的小,可以動態改變學習速率
α=const1const2+iterations \alpha=\frac{const1}{const2+iterations}

由於隨機梯度下降每次只對一個數據進行擬合,我們可以用這個構建一個在線學習算法,每次到來一個數據就用隨機梯度下降進行擬合這個數據。


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

歡迎指正錯誤

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