超平面划分
如果两个数据集在空间中是两个凸集,那么它们可以用一个超平面来实现线性分割. 因此机器学习的最基础分类问题就是学习一个超平面, 来解决凸集划分问题. 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