Machine Learning(Andrew) ex8-Anomaly Detection and Recommender Systems
椰汁筆記
Anomaly detection
這是也是一個非監督學習算法
- 異常檢測做什麼?
從一組數據中找到那些“異常”的數據,基於高斯分佈(正太分佈)。生活中的很多事情都是符合高斯分佈的,對於數據也是如此。我們通過參數估計,估計出數據符合的高斯分佈參數,當其中的數據分佈在高斯分佈中概率很小的地方,就認爲這是異常數據。 - 具體怎麼做?
選擇可以描述異常狀態的特徵作爲輸入
根據以往的數據估計高斯分佈的參數(對每一個特徵)
對於一個新的數據,預測其發生概率
當概率小於一定閾值後認定爲異常。 - 這個算法有什麼缺點?
可以看到,之前的模型中對每個特徵都是獨立地處理,最後的組合只是簡單的相乘。這樣就是存在一些問題,特徵之間的關聯沒有捕捉到。
升級的方式就是多元高斯分佈,將不再單獨考慮特徵,而是將特徵一起考慮,自動捕捉之間的關聯。
參數的估計變爲,其中的sigma爲協方差矩陣
預測變爲
這個模型有個前提就是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,同樣進行電影評分預測。
但是不管是用戶特徵還是電影特徵都難以手動確定,那怎麼實現推薦系統呢?
協同過濾算法
現實情況是我們都不知道用戶特徵和電影特徵,只知道用戶對哪些電影評分,評分的分數。協同過濾算法的思路是我們直接一起訓練用戶特徵和電影特徵,首先隨機初始化用戶特徵和電影特徵,在每一次梯度下降過程中,先後更新用戶特徵和電影特徵,最後就能得到用戶特徵和電影特徵並進行預測。
爲什麼可以先後跟新用戶特徵和電影特徵呢?因爲他們的優化目標函數是相同的。
只需要保證用戶特徵和電影特徵的特徵數量是相同的即可
具體的算法流程是:
1.隨機初始化用戶特徵theta和電影特徵x,這裏需要保證兩個維度一樣
2.使用梯度下降或其他優化算法更新兩個特徵參數
3.用着兩個特徵預測評分,進行排序推薦
- 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:
}
另外還有一種叫做mini-batch gradient descent,他的想法就是每次迭代使用b個樣本而不是隨機梯度下降中的一個樣本,這個b一般取2到100。當向量化做得好時,可以比隨機梯度下降的速度更快。
Repeat{
while(i<m){
}
}
怎麼判斷隨機梯度下降收斂呢?
每迭代1000輪,計算最後1000個樣本的平均損失值,並畫出圖像
這個1000的選擇需要注意,如果太小圖像產生很多噪聲,越大圖像越平滑。
同時如果學習速率不變,隨機梯度下降的收斂值會在局部最小周圍擾動。爲了使擾動儘可能的小,可以動態改變學習速率
由於隨機梯度下降每次只對一個數據進行擬合,我們可以用這個構建一個在線學習算法,每次到來一個數據就用隨機梯度下降進行擬合這個數據。
完整的代碼會同步 在我的github
歡迎指正錯誤