Python之ML–机器学习分类算法
介绍最早以算法方式描述的分类机器学习算法:感知器(perceptron)和自适应线性神经元(adaptive linear neuron).我们将使用python循序渐进地实现一个感知器,并且通过训练使其具备对鸢尾花数据集中数据进行分类的能力
主要知识点:
- 机器学习算法的直观知识
- 使用numpy,pandas和matplotlib读取,处理和可视化数据
- python实现线性分类算法
一.人造神经元
为了了解大脑的工作原理以设计人工智能系统,沃伦.麦卡洛可(Warren McCullock)与沃尔特.皮茨(Walter Pitts)在1943年提出来第一个脑神经元的抽象模型,也称为麦卡洛可–皮茨神经元(MCP),神经元是大脑相互连接的神经细胞,它可以处理和传递化学信号和电信号
from IPython.display import Image
麦卡洛可和皮茨将神经细胞描述为一个具备二进制输出的逻辑门.树突接收多个输入信号,如果累加的信号超过某一阈值,经细胞体的整合就会生成一个输出信号,并通过轴突进行传递
几年后,弗兰克.罗森布拉特(Frank Rossenblatt)基于MCP神经元模型提出了第一个感知器学习法则.在此感知器规则中,罗森布拉特提出了一个自学习算法,此算法可以自己通过优化得到权重系数,此系数与输入值的乘积决定了神经元是否被激活
关于线性代数的知识可通过如下链接免费获取:http://www.cs.cmu.edu/~zkolter/course/linalg/linalg_notes.pdf
linalg_notes.pdf
下图中,左图说明了感知器模型的激励函数如何将输入z=w^Tx转换到二值输出(-1或1),右图说明了感知器模型如何将两个可区分类别进行线性区分
MCP神经元和罗森布拉特阈值感知器的理念就是,通过模拟的方式还原大脑中单个神经元的工作方式:它是否被激活.这样罗森布拉特感知器最初的规则非常简单,可总结为如下几步:
- 将权重初始化为零或一个极小的随机数
- 迭代所有训练样本x^i,执行如下操作
-
- 计算输出值y^
- 更新权重
需要注意的是:感知器收敛的前提是两个类别必须是线性可分的,且学习速率足够小.如果两个类别无法通过一个线性决策边界进行划分,可以为模型在训练数据集上的学习迭代次数设置一个最大值,或者设置一个允许错误分类样本数量的阈值—否则,感知器训练算法将永远不停地更新权值
总结一下感知器的基本概念:
上图说明了感知器如何接收样本x的输入,并将其与权值w进行加权以计算净输入(net input).进而净输入被传递到激励函数(在此为单位阶跃函数),然后生成值为+1或-1的二值输出,并以其作为样本的预测类标.在学习阶段,此输出用来计算预测的误差并更新权重
二.实现感知器学习算法
通过使用面向对象编程的方式在一个python类中定义感知器的接口,使得我们可以初始化新的感知器对象,并使用类中定义的fit方法从数据中进行学习,使用predict方法进行预测.按照python开发的惯例,对于那些并非在初始化对象时创建但是又被对象中其他方法调用的属性,可以在后面添加一个下划线,例如:self.w_
import numpy as np
class Perceptron(object):
def __init__(self,eta=0.01,n_iter=10):
"""
Parameters
----------
eta:float
Learning rate(between 0.0 and 1.0)
n_iter:int
Passes over the training dataset
Attributes
----------
w_:1d-array
Weights after fitting
errors_:list
Number of misclassifications in every epoch
"""
self.eta=eta
self.n_iter=n_iter
def fit(self,X,y):
"""
Parameters
----------
X:{array-like},shape=[n_samples,n_features]
y:array-like,shape=[n_samples]
Target values
"""
self.w_=np.zeros(1+X.shape[1])
self.errors_=[]
for _ in range(self.n_iter):
errors=0
for xi,target in zip(X,y):
update=self.eta*(target-self.predict(xi))
self.w_[1:]+=update*xi
self.w_[0]+=update
errors+=int(update!=0.0)
self.errors_.append(errors)
return self
def net_input(self,X):
"""Calculate net input"""
return np.dot(X,self.w_[1:])+self.w_[0]
def predict(self,X):
"""Return class label after unit step"""
return np.where(self.net_input(X)>=0.0,1,-1)
在感知器实现过程中,我们实例化一个Perceptron对象时,给出了一个学习速率eta和在训练数据集上进行迭代的次数n_iter.通过fit方法,我们将self.w_中的权值初始化为一个零向量R^m+1,其中m是数据集中维度(特征)的数量,我们在此基础上增加了一个0权重列(也就是阈值)
2.1基于鸢尾花数据集训练感知器模型
挑选鸢尾花数据集中山鸢尾(Setosa)和变色鸢尾(Versicolor)两种花的信息作为测试数据.虽然感知器并不将数据样本特征的数量限定为两个,但出于可视化方面的原因,我们只考虑数据集中萼片长度(sepal length)和花瓣长度(petal-length)这两个特征.不过,感知器算法可以扩展到多类别的分类器应用中,比如通过一对多(One-vs.-all,OvA)
首先我们使用pandas库直接从UCI机器学习库中将鸢尾花数据集转换为DataFrame对象并加载到内存中,并使用tail方法显示数据的最后5行以确保数据正确加载
import pandas as pd
df=pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data",header=None)
df.tail()
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
145 | 6.7 | 3.0 | 5.2 | 2.3 | Iris-virginica |
146 | 6.3 | 2.5 | 5.0 | 1.9 | Iris-virginica |
147 | 6.5 | 3.0 | 5.2 | 2.0 | Iris-virginica |
148 | 6.2 | 3.4 | 5.4 | 2.3 | Iris-virginica |
149 | 5.9 | 3.0 | 5.1 | 1.8 | Iris-virginica |
接下来,我们从中提取前100个类标,其中分别包含50个山鸢尾类标和50个变色鸢尾类标,并将这些类标用两个整数值来替代:1代替变色鸢尾,-1代表山鸢尾.同时把pandas DataFrame产生的对应的整数类标赋给Numpy的向量y,提取这100个训练样本的第一个特征列(萼片长度)和第三个特征(花瓣长度),并赋给属性矩阵X
import matplotlib.pyplot as plt
import numpy as np
y=df.iloc[0:100,4].values
y=np.where(y=='Iris-setosa',-1,1)
X=df.iloc[0:100,[0,2]].values
plt.scatter(X[:50,0],X[:50,1],color='red',marker='o',label='setosa')
plt.scatter(X[50:100,0],X[50:100,1],color='blue',marker='x',label='versicolor')
plt.xlabel('petal length')
plt.ylabel('sepal length')
plt.legend(loc='upper left')
plt.show()
现在,我们可以使用抽取出的鸢尾化数据集来训练感知器.同时将绘制每次迭代的错误分类数量的折线图,以检验算法是否收敛并找到可以分开两种类型鸢尾花的决策边界
ppn=Perceptron(eta=0.1,n_iter=10)
ppn.fit(X,y)
plt.plot(range(1,len(ppn.errors_)+1),ppn.errors_,marker='o')
plt.xlabel('Epochs')
plt.ylabel('Number of misclassifications')
plt.show()
通过一个简单的函数来实现队二维数据集决策边界的可视化
from matplotlib.colors import ListedColormap
def plot_decision_regions(X,y,classifier,resolution=0.02):
markers=('s','x','o','^','v')
colors=('red','blue','lightgreen','gray','cyan')
cmap=ListedColormap(colors[:len(np.unique(y))])
x1_min,x1_max=X[:,0].min()-1,X[:,0].max()+1
x2_min,x2_max=X[:,1].min()-1,X[:,1].max()+1
xx1,xx2=np.meshgrid(np.arange(x1_min,x1_max,resolution),np.arange(x2_min,x2_max,resolution))
Z=classifier.predict(np.array([xx1.ravel(),xx2.ravel()]).T)
Z=Z.reshape(xx1.shape)
plt.contourf(xx1,xx2,Z,alpha=0.4,cmap=cmap)
plt.xlim(xx1.min(),xx1.max())
plt.ylim(xx2.min(),xx2.max())
for idx,cl in enumerate(np.unique(y)):
plt.scatter(x=X[y==cl,0],y=X[y==cl,1],alpha=0.8,c=cmap(idx),marker=markers[idx],label=cl)
plot_decision_regions(X,y,classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')
plt.show()
注意:感知器所面临的最大问题是算法的收敛.Frank Rosenblatt从数学上证明了如果两个类别可以通过线性超平面进行划分,则感知器算法一定会收敛.但是如果两个类别无法通过线性判定边界完全正确地划分,则权重会不断更新.为防止发生此类事件,通常事先设置权重更新的最大迭代次数
三.自适应线性神经元及其学习的收敛性
自适应线性神经网络(Adaptive Linear Neuron,Adaline)算法相当有趣,它阐明了代价函数的核心概念,并且对其做了最小化优化,这是理解逻辑斯谛回归(logistic regression),支持向量机(support vector machine)的高级机器学习分类算法的基础
基于Adeline规则的权重更新是通过一个连续的线性激励函数来完成的,而不像Rosenblatt感知器那样使用单位阶跃函数,这是二者的主要区别
线性激励函数在更新权重的同时,我们使用量化器(quantizer)对类标进行预测,量化器与前面提到的单单位阶跃函数类似
1.通过梯度下降最小化代价函数
机器学习中监督学习算法的一个核心组成在于:在学习阶段定义一个待优化的目标函数.这个目标函数通常是需要我们做最小化处理的代价函数
2.实现自适应线性神经元
我们将在前面实现的感知器代码的基础上修改fit方法,将其权重的更新改为通过梯度下降最小化代价函数来实现Adaline算法
class AdalineGD(object):
def __init__(self,eta=0.01,n_iter=50):
self.eta=eta
self.n_iter=n_iter
def fit(self,X,y):
self.w_=np.zeros(1+X.shape[1])
self.cost_=[]
for i in range(self.n_iter):
output=self.net_input(X)
errors=(y-output)
self.w_[1:]+=self.eta*X.T.dot(errors)
self.w_[0]+=self.eta*errors.sum()
cost=(errors**2).sum()/2.0
self.cost_.append(cost)
return self
def net_input(self,X):
return np.dot(X,self.w_[1:])+self.w_[0]
def activation(self,X):
return self.net_input(X)
def predict(self,X):
return np.where(self.activation(X)>=0.0,1,-1)
我们分别使用eta=0.01和eta=0.0001两个学习速率来描绘迭代次数与代价函数的图像
fig,ax=plt.subplots(nrows=1,ncols=2,figsize=(8,4))
ada1=AdalineGD(n_iter=10,eta=0.01).fit(X,y)
ax[0].plot(range(1,len(ada1.cost_)+1),np.log10(ada1.cost_),marker='o')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('log(Sum-squared-error)')
ax[0].set_title('Adaline-learning rate 0.01')
ada2=AdalineGD(n_iter=10,eta=0.0001).fit(X,y)
ax[1].plot(range(1,len(ada2.cost_)+1),np.log10(ada2.cost_),marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('log(Sum-squared-error)')
ax[1].set_title('Adaline-learning rate 0.0001')
plt.show()
左边的图像显示了学习速率过大可能会出现的问题—并没有使代价函数的值尽可能的低,反而因为算法跳过了全局最优解,导致误差随着迭代次数增加而增大
右边的图像代价函数逐渐减小,但是选择的学习速率eta=0.0001的值太小,以致为了达到算法收敛的目标,需要更多的迭代次数
下图说明了我们如何通过更改特定的权重参数值来最小化代价函数J(左子图).右子图展示了如果学习速率选择过大会发生什么情况:算法跳过全局最优解(全局最小值)
梯度下降就是通过特征缩放而受益的众多算法之一.在此,采用一种称作标准化的特征缩放方法,此方法可以使数据具备标准正态分布的特性:各特征值的均值为0,标准差为1
标准化可以简单地通过Numpy的mean和std方法来完成
X_std=np.copy(X)
X_std[:,0]=(X[:,0]-X[:,0].mean())/X[:,0].std()
X_std[:,1]=(X[:,1]-X[:,1].mean())/X[:,1].std()
在进行标准化操作后,我们以学习速率eta=0.01再次对Adaline进行训练,看看它是否是收敛的
ada=AdalineGD(n_iter=15,eta=0.01)
ada.fit(X_std,y)
plot_decision_regions(X_std,y,classifier=ada)
plt.title('Adaline-Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.show()
plt.plot(range(1,len(ada.cost_)+1),ada.cost_,marker='o')
plt.xlabel('Epochs')
plt.ylabel('Sum-squared-error')
plt.show()
3.大规模机器学习与随机梯度下降
在上一节中,我们学习了如何使用整个训练数据集沿着梯度相反的方向进行优化,以最小化代价函数;这也是此方法有时称作批量梯度下降的原因.假定现在有一个包含几百万条数据的巨大数据集,这个量非同寻常的.由于向全局最优点移动的每一步都需要使用整个数据集来进行评估,因此这种情况下使用批量梯度下降的计算成本非常高
一个常用的替代批量梯度下降的优化算法是随机梯度下降(stochastic gradient descent),有时也称作迭代梯度下降(iterative gradient descent)或者在线梯度下降(on-line gradient descent)
为了通过随机梯度下降得到更加准确的结果,让数据以随机的方式提供给算法是非常重要的,这也是我们每次迭代都要打乱训练集以防止进入循环的原因
由于我们已经使用梯度下降实现了Adaline学习规则,因此只需在此基础上将学习算法中的权重更新改为通过随机梯度下降来实现即可
from numpy.random import seed
class AdalineSGD(object):
def __init__(self,eta=0.01,n_iter=10,shuffle=True,random_state=None):
self.eta=eta
self.n_iter=n_iter
self.w_initialized=False
self.shuffle=shuffle
if random_state:
seed(random_state)
def fit(self,X,y):
self._initialize_weights(X.shape[1])
self.cost_=[]
for i in range(self.n_iter):
if self.shuffle:
X,y=self._shuffle(X,y)
cost=[]
for xi,target in zip(X,y):
cost.append(self._update_weights(xi,target))
avg_cost=sum(cost)/len(y)
self.cost_.append(avg_cost)
return self
def partial_fit(self,X,y):
if not self.w_initialized:
self._initialize_weights(X.shape[1])
if y.ravel().shape[0]>1:
for xi,target in zip(X,y):
self._update_weights(xi.target)
else:
self._update_weights(X,y)
return self
def _shuffle(self,X,y):
r=np.random.permutation(len(y))
return X[r],y[r]
def _initialize_weights(self,m):
self.w_=np.zeros(1+m)
self.w_initialized=True
def _update_weights(self,xi,target):
output=self.net_input(xi)
error=(target-output)
self.w_[1:]+=self.eta*xi.dot(error)
self.w_[0]+=self.eta*error
cost=0.5*error**2
return cost
def net_input(self,X):
return np.dot(X,self.w_[1:]+self.w_[0])
def activation(self,X):
return self.net_input(X)
def predict(self,X):
return np.where(self.activation(X)>=0.0,1,-1)
分类器AdalineSGD中_shuffle方法的工作原理如下:通过numpy.random的permutation函数,我们生成一个包含0-100的不重复的随机序列.这些数字可以作为索引帮助打乱我们的特征矩阵和类标向量
接下来,我们就可以通过fit方法训练AdalineSGD分类器,并应用plot_decision_regions方法绘制训练结果
ada=AdalineSGD(n_iter=15,eta=0.01,random_state=1)
ada.fit(X_std,y)
plot_decision_regions(X_std,y,classifier=ada)
plt.title('Adaline-Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.show()
plt.plot(range(1,len(ada.cost_)+1),ada.cost_,marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.show()