超平面劃分
如果兩個數據集在空間中是兩個凸集,那麼它們可以用一個超平面來實現線性分割. 因此機器學習的最基礎分類問題就是學習一個超平面, 來解決凸集劃分問題. m維超平面的表達式可以寫成, 其中w和x都是m維列向量, b是常數偏置. 很直觀的, 對給定的w, 滿足條件的x是在m維空間中的一個m-1維橫切面.這個w就是超平面的法向量. 因爲是法向量, 所以w的範數和超平面的形狀沒有關係, 即w乘以一個常數c, 超平面不發生變化.
我們會用到的一個參數是"空間中任意一點到超平面的歐氏距離.這個距離的推導如下, 首先我們在超平面上找任意一點, 連接它和x形成向量, 距離d等於向量在超平面法向量方向,即w的方向上的投影.也就是
使用超平面二分類樣本時, 我們會計算的數值, 如果大於0就是正樣本, 小於0就是負樣本.
感知機
感知機的思想是, 我們首先初始化一個超平面, 如果發現有某些樣本是錯分的, 就把超平面向反方向移動一點. 這樣反覆多次就能找到能儘可能正確分類的平面了.但是, 如果我們naive地設定"正確劃分的loss=0,錯誤劃分的loss=1",則算法的損失函數不可導(導數爲0)不能訓練. 爲了更體面地解決問題, 找到一種能求導的損失函數,我們會用"錯誤劃分的樣本距離超平面的距離"作爲損失函數, 也就是
relu是線性整流函數,當輸入<0時輸出恆爲0,當大於等於0時輸出就是輸入值.y是樣本標籤,正樣本爲+1,負樣本爲-1. 這樣,我們的損失函數的意義就是, 當y標籤和模型預測符號相反, 即錯誤劃分時, 就把這個樣本距離超平面的距離做爲損失的一部分.這時函數是連續可導的(在relu的x=0處不可導,但我們使用浮點數計算時不考慮這種不好的數學性質).
我們可以用梯度下降優化這個目標函數, 爲了更容易求解, 我們可以把分母的項移去. 爲什麼可以移去呢? 可以設想, 我們的距離計算對每個樣本點, w範數都是相同的, 即使去掉了也只是相當於在損失函數上乘以一個係數, 並不影響損失的梯度(下降方向),只會影響數值.
使用梯度下降法更新即可, 需要注意的使用mini-batch時, 梯度的大小和batch-size大小相關, 而且因爲我們丟棄了||w||, 它也和w範數相關. 在使用梯度下降更新參數時, 如果學習率設置不當會出現不收斂. 我們可以用一些歸一化方法縮放梯度再用於更新.
邏輯迴歸
邏輯迴歸和感知機類似,都是超平面劃分的線性模型. 一個不同點是, 邏輯迴歸返回的不是分類類別, 而是分類爲正樣本的概率. 它的模型是
計算概率p使用的激活函數是sigmoid,可以把任意大小的輸入o變換到0-1區間, 也就是一個概率. 而概率模型我們知道, 可以用對數似然損失來優化.這裏我們設正樣本的y爲1, 負樣本的y爲0.模型計算正樣本的概率是p, 負樣本的概率是(1-p).
也就是標籤爲正樣本時,使用p計算負對數似然, 負樣本時使用(1-p)計算. 優化這個目標函數就能讓算法收斂到一個讓似然最大的超平面.
使用梯度下降同樣要計算梯度,計算過程如下
需要注意的是, 邏輯迴歸並不同於感知機. 感知機會在數據集上嘗試劃分正確儘可能多的樣本, 但邏輯迴歸有時並不保證能正確劃分某些本可能正確劃分的樣本, 它有時會讓某些樣本處於0.3,0.4的概率, 來最大化似然. 所以邏輯迴歸的性質會讓它刻意誤分類一些異常樣本, 因此有着比感知機更好的泛化能力.
SVM
支持向量機, 可以稱得上是監督學習的珠峯. 它有着大間隔劃分的性質, 能有效防止過擬合.還有核技巧可以幫助模型實現非線性的劃分.很多教材和講座會用約束優化的對偶定理得到一個SVM的對偶問題, 但我一直不是很喜歡這種說法, 爲了解決原問題而編出一個新問題這個流程就像是在繞圈子. 這裏就從無約束的角度, 基於感知機來講SVM.
在SVM中,我們不止設置一個決策平面,還會有兩個支持平面和. 如果我們假設數據是線性可分的, 那麼決策平面一定位於兩類樣本之間. 而現在我們想從兩類樣本之間確定一個最好的超平面,這個超平面滿足的性質是到兩類樣本的最小距離最大.很自然的, 我們可以看出這個超平面到兩類樣本的最小距離應該相等. 這時上面的支持超平面就派上用場了. 設到超平面最近的兩個樣本是和, 樣本距離, 也就是. 又因爲w的縮放對超平面的性質沒有影響, 我們不妨讓C = 1, 即兩個樣本和會分別落在和超平面上. 其他樣本都在這兩個超平面以外.
如果用上面的一系列假設, 我們的損失函數就應該是: 如果正樣本越過了超平面, 即那麼它受到隨距離線性增長的懲罰. 同樣如果正樣本越過了超平面, 即也受到懲罰. 如果我們設y是樣本標籤,正樣本爲+1,負樣本爲-1. 則我們有保證兩類樣本到決策超平面的最小距離相等的新型感知機
同樣的道理, 等價於
到此還不算結束, 因爲我們還想要這個最小距離最大化. 我們知道, 而, 即, 也就是最大化距離, 只需要最小化w即可. 最小化一個參數常用的做法是正則化, 也就是對w施加L2的懲罰(L1的懲罰性質較差, 容易讓w爲0).這樣我們就得到了最大化最小距離的損失.
我們把上面兩個損失函數加起來, 並給感知機正確分類的一項乘上一個常數係數, 用來調控(正確分類) vs (最大化間隔)兩個目標的重要程度.
優化上面的損失函數就能得到最大間隔的線性SVM, SVM還可以用非線性核技巧來處理不是線性可分的樣本, 我們有機會再討論.
貝葉斯統計
由上面的貝葉斯公式, 我們可以根據特徵x給出類別y的判別概率. P(x|y)是似然, P(y)是先驗. 放到上面的二分類問題裏, P(x|y)就是對類別y的所有樣本, 估計出的一個條件概率分佈. 而P(y)一般是常數, 可以直接計算不同類別樣本在數據集中所佔的比例得到. 在決策時, 我們對每個類別用貝葉斯公式計算P(y|x)並取最大. 這就是貝葉斯決策, 只要我們能得到精確的似然概率密度函數P(x|y)就能做出讓錯誤率最小的決策.
如果x的特徵有很多種, 維度很高, 概率密度的空間是很大很複雜的, 我們可能很難估計出一個合適的P(x|y)的形式. 這時我們可以藉助樸素貝葉斯假設, 認爲條件獨立, 就能用用單個維度的分佈的乘積估計高維情況下的似然概率密度P(x|y).
決策樹
線性超平面劃分的算法多用於處理連續特徵, 貝葉斯統計既能處理連續特徵也能處理離散特徵, 而決策樹是一種非常適合處理離散特徵的算法. 它的思想非常簡單, 在每個樹節點, 我們對樣本x的一個特徵做判斷, 如果滿足就下溯到當前結點的第一個子結點繼續判斷, 如果滿足就下溯到當前結點的第二個子結點繼續判斷,以此類推, 最後我們到達沒有子結點的葉結點, 就可以下決策說這個樣本x是某一類樣本.
怎樣從訓練集得到這樣一棵決策樹呢,我們要先引入信息熵和信息增益的概念.
對一個數據集合,我們用下面的方法計算信息熵
可以看出, 信息熵是一定是正數, 而且數據集中的樣本種類越複雜, 混亂度越高則信息熵越大. 而數據集純度很高時信息熵很小.
使用信息熵我們可以衡量一個數據集的純度, 進而我們可以計算, 在數據集D上, 選擇特徵a劃分得到的幾個子數據集的信息增益
上式的意義是, 我們用劃分前的信息熵減去, 劃分後每一個子數據集的信息熵, 因爲子數據集的樣本總數不同, 我們用數量的佔比加權處理子數據集的信息熵. 一般而言, 信息增益越大, 意味着用a劃分的純度提升越大. 我們就以這種判據, 在每次劃分前遍歷所有劃分可能性, 並選擇最好的那一種.
用信息增益劃分樹就是ID3算法. 算法從訓練集出發, 每次都用當前結點擁有的數據集選擇一個屬性來做劃分, 產生多個子結點, 然後再對子結點繼續做劃分. 知道所有屬性都被劃分完, 或者當前結點擁有的數據集都是同一類爲止, 這時我們產生葉結點. 每個結點都擁有決策信息, 也就是來到當前結點node的樣本x應該被分到node->property類. node->property應該是當前結點擁有的數據集中樣本數最多的那個類別.
有時我們會面臨從未在訓練集中出現的樣本, 這時我們就直接停止下溯, 把它歸類到當前非葉結點的property即可. 如果要處理連續特徵, ID3給出的方法是對連續特徵進行二分, 即排序樣本, 並遍歷所有中位點, 計算以它爲中心進行二分後得到的信息增益.
超平面分類器實現
我們就不從零敲一個類出來了, 畢竟反向傳播和維度處理等等還是挺麻煩的. 這裏我們直接用pytorch的反向傳播工具來計算梯度, 並進行梯度下降優化.
import torch
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
import torch.nn as nn
torch.manual_seed(2020)
Xp = torch.randn(20,2)
Xp[:,0] = 0.5*(Xp[:,0]+3)
Xp[:,1] = 0.8*(Xp[:,1]+2)
Xn = torch.randn(20,2)
Xn[:,0] = 0.4*(Xn[:,0]-1)
Xn[:,1] = 0.4*(Xn[:,1]-2)
X = torch.cat((Xp,Xn),axis = 0)
y = torch.cat((torch.ones(20),-1*torch.ones(20)))
y = y.reshape(-1,1)
plt.scatter(X[:20,0].numpy(),X[:20,1].numpy() , marker = '+', c = 'r')
plt.scatter(X[20:,0].numpy(),X[20:,1].numpy() , marker = '_', c = 'r')
# 用梯度下降優化感知機損失
def Perceptron_loss(X,y,w,b):
return torch.mean(F.relu(-y*(X.mm(w)+b)))
def train(loss_func, lr):
w = torch.randn(2,1)
b = torch.randn(1)
w.requires_grad = True
b.requires_grad = True
for _ in range(1000):
loss = loss_func(X,y,w,b)
loss.backward()
w.data -= lr*w.grad.data
b.data -= lr*b.grad.data
w.grad.data.zero_()
b.grad.data.zero_()
return w,b
def Show(w,b):
xx = np.linspace(-3,3)
slope = (-w[0]/w[1]).data.numpy()
bias = (-b/w[1]).data.numpy()
yy = slope*xx+bias
plt.plot(xx,yy, c = 'black')
plt.scatter(X[:20,0].numpy(),X[:20,1].numpy() , marker = '+', c = 'r')
plt.scatter(X[20:,0].numpy(),X[20:,1].numpy() , marker = '_', c = 'r')
plt.axis([-3,3,-2,4])
plt.show()
w,b = train(Perceptron_loss, 0.02)
Show(w,b)
# 用梯度下降優化邏輯迴歸損失
y = torch.cat((torch.ones(20),torch.zeros(20)))
y = y.reshape(-1,1)
def Logistic_loss(X,y,w,b):
p = torch.sigmoid(X.mm(w)+b)
return torch.mean(-y*torch.log(p)-(1-y)*torch.log(1-p))
w,b = train(Logistic_loss, 0.01)
Show(w,b)
# 用梯度下降優化linear SVM損失
y = torch.cat((torch.ones(20),-1*torch.ones(20)))
y = y.reshape(-1,1)
def SVM_loss(X,y,w,b,lamda = 150):
out = X.mm(w)+b
return F.mse_loss(w,torch.zeros_like(w))+lamda*torch.mean(F.relu(-y*out+1))
w,b = train(SVM_loss, 0.02)
Show(w,b)
從上面的三種超平面可以看出它們的區別,感知機保證儘可能多的樣本正確分類, 但是解不穩定, 而且不保證超平面的性質好壞. 邏輯迴歸最大化對數似然, 解穩定, 而且泛化性較好. SVM不但保證正確分類, 而且是兩類樣本最中間的超平面, 泛化性更好.
高斯樸素貝葉斯
Sklearn中提供了GaussianNB分類器. 它的思想是在維度獨立的基礎上, 認爲每個維度都符合簡單一維高斯分佈. 實現起來其實相當簡單, 我們知道高斯分佈的參數估計就是計算樣本均值和樣本方差, 所以我們只需要對每個類單獨計算每個維度上的均值和方差作爲模型參數. 預測時直接用連乘計算概率即可.
from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
def gaussian(X, mu, sigma):
num = np.exp(-(X-mu)**2/2/sigma**2)
denum = (2*np.pi)**0.5*sigma
return num/denum
def GaussianNB(X,y):
'''
輸入X:nxm, y:n,
返回一個MU:Cxm,SIGMA:Cxm
'''
n,m = X.shape
C = y.max()+1
MU = np.zeros((C,m))
SIGMA = np.zeros((C,m))
for i in range(C):
indices = np.where(y==i)
MU[i] = np.mean(X[indices],axis = 0)
SIGMA[i] = np.var(X[indices],axis = 0)**0.5
return MU,SIGMA
def NBPredict(X, MU, SIGMA):
'''
用參數預測X的標籤
'''
C,m = MU.shape
n,_ = X.shape
probs = np.zeros((C,n))
for i in range(C):
probs_i = gaussian(X, MU[i], SIGMA[i])
probs[i] = np.prod(probs_i,axis = 1)
return np.argmax(probs,axis = 0)
X,y = datasets.load_iris(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.2, random_state = 3)
MU,SIGMA = GaussianNB(X_train,y_train)
y_pred = NBPredict(X_test,MU,SIGMA)
print("Number of mislabeled points out of a test %d points : %d"
% (X_test.shape[0],(y_test != y_pred).sum()))
Number of mislabeled points out of a test 30 points : 1
ID3
前面給出了決策樹的思想, 但是具體實現時還有一些需要另作注意的事項. 比如如果某個結點按照離散特徵劃分, 而當前樣本出現了從未在測試集中見過的屬性, 我們這時可以直接停止, 把當前結點的知識賦予這個樣本. 事實上這個做法很符合貝葉斯定理, 知之爲知之, 不知爲不知. 另外, 如果訓練集中本就有缺失值該如何處理? 連續特徵有沒有更好的最優劃分點搜索方式? 決策樹能在訓練集上跑滿正確率, 但是會出現過擬合又該如何處理? 這些細節都是實現時需要另作注意的.
這裏我們實現一個用於連續特徵的決策樹分類器, 基於ID3算法進行. 因爲樣本量和維度都較小, 我們每次劃分都遍歷nxm種劃分可能性來尋找最大信息增益. 爲了避免過擬合, 我們限制樹的高度, 最多劃分5次.
def Ent(labels):
ent = 0
Cset = set(labels)
tot = len(labels)
for i in Cset:
p = len(np.where(labels==i)[0])/tot
ent -= p*np.log2(p)
return ent
def Gain(X,y):
'''
return
Mid_list: array, shape = (n-1, m)
表示所有劃分用的中值點,Mid_list[i][j]是第j維的第i個劃分點
Gain_list: array, shape = (n-1, m)
表示以Mid_list[i][j]劃分後的信息增益
'''
n,m = X.shape
Mid_list = np.zeros((n-1,m))
Gain_list = np.zeros((n-1,m))
Ent_y = Ent(y)
for j in range(m):
X_sorted = X[np.argsort(X[:,j]),j]
Mid_list[:,j] = 0.5*X_sorted[:-1]+0.5*X_sorted[1:]
for i in range(n-1):
yl = y[X[:,j]<=Mid_list[i,j]]
yg = y[X[:,j]>Mid_list[i,j]]
n1,n2,n3 = len(yl),len(yg),len(yl)+len(yg)
Gain_list[i][j] = Ent_y-n2/n3*Ent(yg)-n1/n3*Ent(yl)
return Mid_list,Gain_list
from collections import Counter
class Node:
def __init__(self):
self.species = 0
self.dim = 0
self.mid = 0
self.leaf = True
self.left = None
self.right = None
def grow(self, X, y, max_depth = 4, max_data = 3):
n,m = X.shape
cnt = Counter(y)
self.species = max(set(y),key = lambda x:cnt[x])
if len(cnt)==1:
return
if max_depth==1:
return
Mid_list,Gain_list = Gain(X,y)
besti_of_each_dim = []
best_of_each_dim = []
for j in range(m):
besti_of_each_dim.append(np.argmax(Gain_list[:,j]))
best_of_each_dim.append(np.max(Gain_list[:,j]))
j = np.argmax(np.array(best_of_each_dim))
i = besti_of_each_dim[j]
self.mid = Mid_list[i][j]
self.dim = j
indicesl = np.where(X[:,j]<=self.mid)
indicesg = np.where(X[:,j]>self.mid)
if len(indicesl[0])<=max_data or len(indicesg[0])<=max_data:
return
self.left = Node()
self.right = Node()
self.left.grow(X[indicesl],y[indicesl],max_depth-1)
self.right.grow(X[indicesg],y[indicesg],max_depth-1)
self.leaf = False
def predict(self, x):
if self.leaf:
return self.species
elif x[self.dim]<=self.mid:
return self.left.predict(x)
else:
return self.right.predict(x)
tree = Node()
tree.grow(X_train,y_train)
y_pred = np.array([tree.predict(x) for x in X_test])
print("Number of mislabeled points out of a test %d points : %d"
% (X_test.shape[0],(y_test != y_pred).sum()))
Number of mislabeled points out of a test 30 points : 1