從我對整個職業生涯的規劃出發,我不僅想做一些高質量的應用(軟體工程的角度),還想做一些激動人心的應用,所以我希望能在機器學習的方向走,儘管我在大學粗淺的學了些皮毛,但如果要把機器學習作為職業發展的話這些還差得遠,所以我開始寫了這個系列的文章。
我希望通過這個系列能對機器學習領域的知識點做一個總結,所以對於Machine Learning這個部分,我的目標是寫出能讓高中生看得懂。
前言
繼上一章 Re:從零開始的機器學習 - Machine Learning(一) 線性迴歸,我還是繼續按照史丹佛的路線,這一章來說說邏輯迴歸(Logistic Regression)
分類(Classification)
和迴歸(Regression)一樣,分類(Classification)問題也是機器學習裡面很大的一塊。
分類問題是機器學習非常重要的一個組成部分,它的目標是根據已知樣本的某些特徵,判斷一個新的樣本屬於哪種已知的樣本類。
其實常見的例子很多,判斷一個郵件是否是垃圾郵件之類的,預測一個使用者是否對我的商品感興趣,以及影象處理裡面對影象進行的分類。
分類問題有簡單的二分類也有多分類。正文
這篇部落格主要講的是邏輯迴歸(Logistic regression)。
邏輯迴歸LR(Logistic Regression)
看到名字的時候你可能會有一些奇怪,為什麼明明叫邏輯“迴歸”卻用在分類問題上。雖然這個名字似乎指示著什麼,但是邏輯迴歸實際上是分類演算法。我猜它之所以這樣命名是因為在它的學習方法上和線性迴歸相似,但是損失(loss)和梯度(gradient)函式的表達不同。特別的,邏輯迴歸使用 S型函式(sigmoid)而不是線性迴歸的連續輸出。當我們深入到實現中去,我們會了解到更多。
首先我們先把邏輯迴歸放到一邊,之前也說了邏輯迴歸是用來解決分類問題的,對於分類問題,我們實際上是希望得到一個分類器(Classifier),當輸入資料之後,這個分類器能給我預測這個資料屬於某一類的概率,也就是說我們需要的是一個概率。
上一節我們介紹的線性迴歸,其輸出的是預測值,其假設函式(Hypothesis Function)也就是輸出預測值的函式是這樣的。
而邏輯迴歸則是預測屬於某一類的概率,所以我們讓其假設函式是下面這個 這個函式的意義其實是當輸入為x時輸出y=1的概率,其實就是二分類問題裡面是某個東西的概率。讀者現在可能會對這個函式有所疑問,比如為什麼是這個函式,這個留在後面會討論。這裡出現了條件概率,實際上就是指事件A在另外一個事件B已經發生條件下的發生概率。高中生可以補這部分也可以暫時不補
我們把這個函式g(z)叫做sigmoid函式,很明顯這個函式的值域是0到1的開區間。接下來我們會詳細介紹一下這個函式。
Sigmoid
Sigmoid函式的函式表示式如下
為什麼是Sigmoid
損失函式(Loss Function)
上一小節我們也說過,為了修正引數Θ我們需要有個手段來衡量當前引數Θ的優秀程度。損失函式(Loss Function)就是用來衡量假設函式(hypothesis function)的準確性。
對於邏輯迴歸來說,我們希望的是當預測概率約接近實際情況(0或1)的時候誤差最小,而且不希望曲線是一條直線,而是對於越接近的地方變化越小,約遠離的地方變化越大的函式。
下面就是邏輯迴歸的損失函式。
我們可以將函式合併一下,畢竟這種分段函式處理起來不是很舒服,其實就是下圖這樣,也很好理解,畢竟二分類訓練資料y只有0和1兩個值。
這樣我們就可以算出在一個訓練集中基於當前引數Θ得到結果的誤差了。
向量化程式設計
其實上一小節的實戰中我們已經用到了向量化程式設計。
# 計算損失,用了向量化程式設計而不是for迴圈
def computeLoss(X, y, theta):
inner = np.power(((X * theta.T) - y), 2)
return np.sum(inner) / (2 * len(X))
複製程式碼
向量化程式設計是提高演算法速度的一種有效方法。為了提升特定數值運算操作(如矩陣相乘、矩陣相加、矩陣-向量乘法等)的速度,數值計算和平行計算的研究人員已經努力了幾十年。向量化程式設計的思想就是儘量使用這些被高度優化的數值運算操作來實現我們的學習演算法。
換句話說就是儘量避免使用for迴圈,畢竟矩陣相乘這種場景非常適合平行計算,在巨量的資料面前效能收益非常明顯。
如果剛剛的損失函式用向量化程式設計的思想來表示的話
如果一時不理解的話我先解釋一下,我們先假設共m個資料,而這個模型中變數有n個。則矩陣h就是(m, n) X (n, 1)也就是(m,1)矩陣,矩陣h的意義就是這m個資料的預測值。損失函式中y的轉置為(1, m),相乘後得到(1, 1)也就是一個值,這兩個矩陣相乘的意義則是對應的預測值取對數乘以對應的實際值,最後加在一起。
(m,n)表示維度為m行n列的矩陣,如果學過矩陣的乘法應該知道矩陣相乘(m, n) X (n, k)得到的矩陣是(m, k)
凹函式
邏輯迴歸的梯度下降法(Gradient Descent)
關於梯度下降法我在上一小節裡面已經做了比較具體的描述,如果忘記了可以回去翻翻。我們剛剛知道了怎麼評價當前引數Θ的好壞,現在我們需要做的是使用梯度下降法來調整引數。
依舊是對損失函式求偏導數,別忘記α是學習速率的意思。 向量化表示為損失函式偏導數求解過程(選修)
過擬合問題
對於一個訓練資料集,視覺化後如下圖所示。
對於三個不同的分類器劃分出的邊界的三種情況,我們對其有不同的稱呼 第一種,分類非常不準,這種我們叫欠擬合(underfitting)第二種,分類得恰到好處,這種其實沒有特別的稱呼。
第三種,分類太過於契合訓練資料了。這種我們稱為過擬合(overfitting)
過擬合所產生的問題也很明顯,它實在太過於契合訓練集了,對於我們來說,第二個曲線才是我們想要的,過擬合的結果太過於契合訓練資料,實用性可想而知的低。
而解決過擬合的方法主要有兩種
減少特徵的數量,這個很好理解,更多的特徵意味著劃分出來的函式曲線可以越複雜。這個可以擴充套件到以後會講的特徵工程(Feature Engineering)
使用正則化項, 保持所有的特徵,但是保證引數θj不會變得巨大。正則化項非常適合在我們擁有很多稍微有點用的特徵的時候。
正則化項(regularizer)
正則化項其實也叫懲罰項(penalty term),其作用是減緩過擬合問題,其實就是在損失函式後面加一個含有各個Θ的項,這樣做的目的是讓Θ也參與損失函式的計算,這樣由於我們需要求的是損失函式的最小值,這個項就會限制Θ的大小。
這個正則化項的目的其實是一個權衡,我們即希望引數Θ能在訓練資料集上表現得比較好,又不希望引數Θ訓練出來的值非常大而產生一些奇葩的劃分曲線,就像下圖這樣的。
實踐
在這篇文章中我們將目標從預測連續值的迴歸問題轉到將結果進行分類的分類問題。
資料都可以在我的GitHub庫上下載到。
環境
如果你不想被配環境煩死的話,我真的推薦裝Anaconda
,除此之外要說的就是我用的都是Python3.x。
背景
本篇的實戰主要是利用邏輯迴歸來幫助招生辦篩選申請人。假設你想要基於兩次考試的結果預測每個申請人是否會被錄取。你有之前歷史申請人的歷史資料,包括兩次考試的分數以及最後他們是否被錄取。為了完此目的,我們將使用邏輯迴歸建立一個基於考試分數的分類模型(classification model )以估計錄取的可能性。
程式碼及註釋
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 讀入訓練資料
# windows使用者路徑可能需要修改下,後期有時間可能會做統一
def loadData(path):
trainingData = pd.read_csv(path, header=None, names=['Exam 1', 'Exam 2', 'Admitted'])
trainingData.head()
trainingData.describe()
positive = trainingData[trainingData['Admitted'].isin([1])]
negative = trainingData[trainingData['Admitted'].isin([0])]
fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(positive['Exam 1'], positive['Exam 2'], s=50, c='b', marker='o', label='Admitted')
ax.scatter(negative['Exam 1'], negative['Exam 2'], s=50, c='r', marker='x', label='Not Admitted')
ax.legend()
ax.set_xlabel('Exam 1 Score')
ax.set_ylabel('Exam 2 Score')
# plt.show()
return trainingData
# 計算損失,用了向量化程式設計而不是for迴圈,公式在部落格中有詳細描述和證明。
def computeLoss(X, y, theta):
theta = np.copy(theta)
X = np.copy(X)
y = np.copy(y)
m = X.shape[0]
h = sigmoid(np.matmul(X, theta.T))
first = np.matmul(-(y.T), np.log(h))
second = np.matmul((1 - y).T, np.log(1 - h))
return np.sum(first - second) / m
# 梯度下降部分
def gradientDescent(X, y, theta, alpha, iters):
m = X.shape[0] # 資料項數m
temp = np.matrix(np.zeros(theta.shape))
# parameters = 1
cost = np.zeros(iters)
for i in range(iters):
error = sigmoid(np.matmul(X, theta.T)) - y
theta = theta - ((alpha/m) * np.matmul(X.T, error)).T
cost[i] = computeLoss(X, y, theta)
return theta, cost
def predict(theta, X):
probability = sigmoid(np.matmul(X, theta.T))
return [1 if x >= 0.5 else 0 for x in probability]
trainingData = loadData(os.getcwd() + '/../../data/ex2data1.txt')
# 插入常數項
trainingData.insert(0, 'Ones', 1)
cols = trainingData.shape[1]
X = trainingData.iloc[:,0:cols-1]
y = trainingData.iloc[:,cols-1:cols]
# 初始化X、Y以及theta矩陣
X = np.matrix(X.values)
y = np.matrix(y.values)
theta = np.matrix(np.zeros(3))
# 計算訓練前的損失值
computeLoss(X, y, theta)
# 使用梯度下降得到模型引數
alpha = 0.001
iters = 20000
theta_fin, loss = gradientDescent(X, y, theta, alpha, iters)
# 計算訓練後的引數的損失值 (不優化)
computeLoss(X, y, theta_fin) #
# 損失隨著迭代次數的變化 (不優化)
# fig, ax = plt.subplots(figsize=(12,8))
# ax.plot(np.arange(iters), loss, 'r')
# ax.set_xlabel('Iterations')
# ax.set_ylabel('Cost')
# ax.set_title('Error vs. Training Epoch')
# plt.show()
# 不理解為什麼不優化的會這麼低,是學習速率沒動態變化麼?
predictions = predict(theta_fin, X)
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(predictions, y)]
accuracy = (sum(map(int, correct)) % len(correct))
print('accuracy 1 = {0}%'.format(accuracy)) # 60%
# 使用scipy的optimize來做優化
import scipy.optimize as opt
# 換了下引數位置讓其符合fmin_tnc
def gradient(theta, X, y):
theta = np.matrix(theta)
X = np.matrix(X)
y = np.matrix(y)
parameters = int(theta.ravel().shape[1])
grad = np.zeros(parameters)
error = sigmoid(X * theta.T) - y
for i in range(parameters):
term = np.multiply(error, X[:,i])
grad[i] = np.sum(term) / len(X)
return grad
# 換了下引數位置讓其符合fmin_tnc
def computeLoss2(theta, X, y):
theta = np.copy(theta)
X = np.copy(X)
y = np.copy(y)
m = X.shape[0]
h = sigmoid(np.matmul(X, theta.T))
first = np.matmul(-(y.T), np.log(h))
second = np.matmul((1 - y).T, np.log(1 - h))
return np.sum(first - second) / m
result = opt.fmin_tnc(func=computeLoss2, x0=theta, fprime=gradient, args=(X, y))
predictions = predict(np.matrix(result[0]), X)
correct = [1 if ((a == 1 and b == 1) or (a == 0 and b == 0)) else 0 for (a, b) in zip(predictions, y)]
accuracy = (sum(map(int, correct)) % len(correct))
print('accuracy 2 = {0}%'.format(accuracy)) # 89%
複製程式碼
解釋其實註釋裡面都比較清楚,就不贅述了。
結果
疑問
程式碼中使用了兩個方法,一個是和上一章一樣的手動梯度下降更新theta值,另一個是使用scipy的優化方法fmin_tnc。最後的準確率差別很大不知道為什麼。
本文章來源於 - 樑王(lwio、lwyj123)