2.训练简单的机器学习分类算法
2.1 人工神经元——机器学习的早期历史
Warren McCulloch和Walter Pitts于1943年提出简化脑细胞的概念,即所谓的McCulloch-Pitts(MCP)神经元。在这个概念中,神经细胞被描述为带有二元输出的简单逻辑门,多个信号到达树突,然后整合到细胞体,并当累计信号量超过一定阈值时,输出信号将通过树突。随后Frank Rosenblatt提出感知器学习规则的概念,他提出一个算法,能够先自动学习最优的权重系数,再乘以输入特征,继而做出神经元是否被触发的决策。在监督学习和分类的场景下,这样的算法可以用来预测新数据点的类别归属。
2.1.1 人工神经元的正式定义
当我们把人工神经元的逻辑放在二元分类的场景时,可以把两个类分别命名为1(正类)和-1(负类),同时定义决策函数(
ϕ
(
z
)
\phi(z)
ϕ(z)),该函数接受特定输入值
x
x
x 的线性组合及其相应的权重向量
ω
\omega
ω ,两者计算的结果
z
z
z 为所谓的净输入
z
=
ω
1
x
1
+
ω
2
x
2
+
.
.
.
+
ω
m
x
m
:
z=\omega_1x_1+\omega_2x_2+...+\omega_mx_m:
z=ω1x1+ω2x2+...+ωmxm:
ω
=
[
ω
1
ω
2
⋮
ω
m
]
,
x
=
[
x
1
x
2
⋮
x
m
]
\omega = \begin{bmatrix} \omega_1 \\ \omega_2 \\ \vdots \\ \omega_m \\ \end{bmatrix}, x = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_m \\ \end{bmatrix}
ω=
ω1ω2⋮ωm
,x=
x1x2⋮xm
如果某个特定样本的净输入值
x
(
i
)
x^{(i)}
x(i) 大于定义的阈值
θ
\theta
θ 则预测结果为1,否则为-1。在感知器算法中,决策函数
ϕ
(
⋅
)
\phi(·)
ϕ(⋅) 是单位阶跃函数的一个变体:
ϕ
(
z
)
=
{
1
,
z
≥
0
−
1
,
e
l
s
e
\phi(z)=\begin{cases} 1 , z≥0 \\ -1 , else \\ \end{cases}
ϕ(z)={1,z≥0−1,else
单位阶跃函数:阶跃函数是一种特殊的连续时间函数,是一个从0跳变到1的过程,属于奇异函数。
为了简化起见,我们把阈值
θ
\theta
θ 放在等式的左边,权重零定义为
ω
0
=
−
θ
\omega_0 = -\theta
ω0=−θ,
x
0
=
1
x_0 = 1
x0=1,这样就可以用更紧凑的方式来表达
z
z
z :
z
=
ω
0
x
0
+
ω
1
x
1
+
.
.
.
+
ω
m
x
m
=
ω
T
x
z=\omega_0x_0+\omega_1x_1+...+\omega_mx_m = \omega^Tx
z=ω0x0+ω1x1+...+ωmxm=ωTx
以及
ϕ
(
z
)
=
{
1
,
z
≥
0
−
1
,
e
l
s
e
\phi(z)=\begin{cases} 1 , z≥0 \\ -1 , else \\ \end{cases}
ϕ(z)={1,z≥0−1,else
在机器学习中,通常把负的阈值或权重
ω
0
=
−
θ
\omega_0 = -\theta
ω0=−θ 称为偏置。
2.1.2 感知器学习规则
MCP神经元和Rosenblatt阈值感知器模型后背的逻辑是,用还原论的方法来模仿大脑神经元的工作情况:要么触发,要么不触发。因此,罗森布拉特的初始感知器规则相当简单,其感知器算法可以总结为以下几个步骤:
1)把权重初始化为0或者小的随机数;
2)分别对每个训练样本
x
(
i
)
x^{(i)}
x(i) 计算输出值
y
^
\widehat{y}
y
并更新权重
输出值为预先定义好的单位阶跃函数预测的分类标签,同时更新权重向量
ω
\omega
ω 的每个值
ω
j
\omega_j
ωj ,更准确的表达式为:
ω
j
:
=
ω
j
+
Δ
ω
j
\omega_j : = \omega_j + \Delta\omega_j
ωj:=ωj+Δωj
其中
Δ
ω
j
\Delta\omega_j
Δωj 是用来更新
ω
\omega
ω 的值,根据感知器学习规则计算该值如下:
Δ
ω
j
=
η
(
y
(
i
)
−
y
^
(
i
)
)
x
j
(
i
)
\Delta\omega_j = \eta(y^{(i)}-\widehat{y}^{(i)})x^{(i)}_j
Δωj=η(y(i)−y
(i))xj(i)
η \eta η 为学习速率,一般是0~1之间的常数;
y ( i ) y^{(i)} y(i) 为第 i 个训练样本的正确类标签;
y ^ ( i ) \widehat{y}^{(i)} y (i) 为预测的分类标签。
需要注意的是,权重向量中的所有值将同时被更新,这意味着在所有权中通过对应更新值
Δ
ω
j
\Delta\omega_j
Δωj 更新之前,不会重新计算
y
^
(
i
)
\widehat{y}^{(i)}
y
(i)。具体来说,二维数据集的更新可以表示为:
Δ
ω
0
=
η
(
y
(
i
)
−
输
出
(
i
)
)
\Delta\omega_0 = \eta(y^{(i)}-输出^{(i)})
Δω0=η(y(i)−输出(i))
Δ
ω
1
=
η
(
y
(
i
)
−
输
出
(
i
)
)
x
1
(
i
)
\Delta\omega_1 = \eta(y^{(i)}-输出^{(i)})x^{(i)}_1
Δω1=η(y(i)−输出(i))x1(i)
Δ
ω
2
=
η
(
y
(
i
)
−
输
出
(
i
)
)
x
2
(
i
)
\Delta\omega_2 = \eta(y^{(i)}-输出^{(i)})x^{(i)}_2
Δω2=η(y(i)−输出(i))x2(i)
简单思考可知,在感知器正确预测两类标签的情况下,保持权重不变,因为更新值为0:
(
1
)
y
(
i
)
=
−
1
,
y
^
(
i
)
=
−
1
,
Δ
ω
j
=
η
(
−
1
−
(
−
1
)
)
x
j
(
i
)
=
0
(1) y^{(i)}=-1, \widehat{y}^{(i)}=-1, \Delta\omega_j = \eta(-1-(-1))x^{(i)}_j = 0
(1)y(i)=−1,y
(i)=−1,Δωj=η(−1−(−1))xj(i)=0
(
2
)
y
(
i
)
=
1
,
y
^
(
i
)
=
1
,
Δ
ω
j
=
η
(
1
−
1
)
x
j
(
i
)
=
0
(2) y^{(i)}=1, \widehat{y}^{(i)}=1, \Delta\omega_j = \eta(1-1)x^{(i)}_j = 0
(2)y(i)=1,y
(i)=1,Δωj=η(1−1)xj(i)=0
然而,如果预测有误,权重应偏向阳或阴的目标类:
(
3
)
y
(
i
)
=
1
,
y
^
(
i
)
=
−
1
,
Δ
ω
j
=
η
(
1
−
(
−
1
)
)
x
j
(
i
)
=
η
(
2
)
x
j
(
i
)
(3) y^{(i)}=1, \widehat{y}^{(i)}=-1, \Delta\omega_j = \eta(1-(-1))x^{(i)}_j = \eta(2)x^{(i)}_j
(3)y(i)=1,y
(i)=−1,Δωj=η(1−(−1))xj(i)=η(2)xj(i)
(
4
)
y
(
i
)
=
−
1
,
y
^
(
i
)
=
1
,
Δ
ω
j
=
η
(
−
1
−
1
)
x
j
(
i
)
=
η
(
−
2
)
x
j
(
i
)
(4) y^{(i)}=-1, \widehat{y}^{(i)}=1, \Delta\omega_j = \eta(-1-1)x^{(i)}_j = \eta(-2)x^{(i)}_j
(4)y(i)=−1,y
(i)=1,Δωj=η(−1−1)xj(i)=η(−2)xj(i)
为了能够更好地理解乘积因子
x
j
(
i
)
x^{(i)}_j
xj(i),我们来看一个简单的示例,其中:
y
^
(
i
)
=
−
1
,
y
(
i
)
=
1
,
η
=
1
\widehat{y}^{(i)}=-1,y^{(i)}=1, \eta=1
y
(i)=−1,y(i)=1,η=1
假设
x
j
(
i
)
=
0.5
x^{(i)}_j=0.5
xj(i)=0.5,在这个模型中,模型将本来为正类的样本判断为负类(-1)。此时更新的权重变为:
Δ
ω
j
=
(
1
−
(
−
1
)
)
×
0.5
=
1
\Delta\omega_j =(1-(-1))\times0.5=1
Δωj=(1−(−1))×0.5=1
这是因为在这种情况下,把相应的权重增加1,这样当下次再遇到该样本时,净输入
x
j
(
i
)
×
ω
j
x^{(i)}_j\times\omega_j
xj(i)×ωj就会更偏向阳,从而更有可能超过单位阶跃函数的阈值,从而把该样本分类为+1。**权重更新与
x
j
(
i
)
x^{(i)}_j
xj(i)成正比。
再例如,有另外一个样本
x
j
(
i
)
=
2
x^{(i)}_j=2
xj(i)=2被错误地分类为-1,这时候更新的权重为:
Δ
ω
j
=
(
1
−
(
−
1
)
)
×
2
=
4
\Delta\omega_j =(1-(-1))\times2=4
Δωj=(1−(−1))×2=4
这时候就把决策边界推到更大,以确保下一次分类的正确性。
2.2 用python实现感知器学习算法
2.2.1 面向对象的感知器API
import numpy as np
class Perceptron(object):
"""Perceptron classifier
Parameters
-------------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-------------
w_ : id-array
Weights after fitting.
errors_ : list
Number of misclassifications (updates) in each epoch.
"""
def __init__(self,eta=0.01, n_iter=50,random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self,x,y):
"""Fit training data,
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples ans n_features is the number of features.
Y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01,
size=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)
2.2.2 在鸢尾花数据集上训练感知器模型
首先,加载鸢尾花数据集。将鸢尾花数据集加载到DataFrame对象,然后用tail方法把最后的5行数据列出来以确保数据加载的正确性。示例代码如下:
import os
import pandas as pd
df = pd.read_csv('D:/AAAAAuseful/wzu/课程/机器学习/《Python机器学习(原书第3版)》_源代码/Chapter02/iris.data',header=None, encoding='utf-8')
print(df.tail())
输出结果为:
接下来,提取与50朵山鸢尾花和50朵变色鸢尾花相对应的前100个分类标签,然后将其转换为整数型的分类标签1 (versicolor) 和-1 (setosa) ,并存入向量y,再通过调用pandas的DataFrame的value方法互动二相应的NumPy表达式。再从100个训练样本中提取特征的第一列(萼片长度)和第三列(花瓣长度),并将它们存入特征矩阵x,经过可视化处理后形成二维散点图。代码如下:
import matplotlib.pyplot as plt
import numpy as np
#select setosa and versicolor
y = df.iloc[0:100,4].values
y = np.where(y == 'Iris-setosa',-1,1)
#extract sepal length and petal length
X = df.iloc[0:100,[0,2]].values
#plot data
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('sepal length [cm]')
plt.ylabel('petal length [cm]')
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 updates')
plt.show()
执行后可见分类错误和迭代次数之间的关系,如下图所示:
如上图所示,感知器在第六次迭代后开始收敛,现在可以开始完美地对训练样本进行分类了。通过下面的函数来完成二维数据集决策边界的可视化:
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, resolution=0.02):
# setup marker generator and color map
markers = ('s', 'x', 'o', '^', 'v')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
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.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class examples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.8,
c=colors[idx],
marker=markers[idx],
label=cl,
edgecolor='black')
在这个函数中,首先通过ListedColormap根据颜色列表来定义一些颜色和标记并创建色度图。随后确定两个特征的最小值和最大值,调用Numpy和meshgrid函数,利用特征向量来创建网格数组对xx1和xx2.因为是在两个特征维度上训练感知器分类器,所以需要对网格数组进行扁平化,以创建一个与鸢尾花训练数据子集相同列数的矩阵,故调用predict方法来预测相应网格点的分类标签z。
在把预测获得的分类标签z改造成与xx1和xx2相同维数的网格后,调用Matplotlib的contourf函数画出轮廓图,把网格数组的每个预测分类结果标注在不同颜色的决策区域内,代码如下:
plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')
plt.show()
得到输出结果如下:
可以看到,感知器通过学习掌握了决策边界,完美地为鸢尾花训练数据子集进行了分类。
在鸢尾花数据集的感知器模型中,我们仅仅考虑的是二元分类器,但其实感知器算法可以扩展到多元分类,例如通过一对多(OvA) 技术。
多元分类的OvA方法
OvA也称一对其余(OvR),是可以把分类器从二元扩展到多元的一种技术。OvA可以为每个类别训练一个分类器,所训练的类被视为正类,所有其他类的样本都被视为负类。假设要对新的数据样本进行分类,就可以用n个分类器,其中n为分类标签的数量,并以最高的置信度为特定样本分配分类标签。在感知器的场景下,将用OvA来选择与最大净输入值相关的分类标签。
2.3 自适应线性神经元和学习收敛
自适应线性神经元(Adaline) 是一种单层神经网络(NN),是由Bernard Widrow及Tedd Hoff联合提出的,是对感知器算法的优化和改进。它说明了定义和最小化连续代价函数的关键概念,为理解诸如逻辑回归、支持向量机和回归模型等更高级的分类机器学习算法奠定了基础。
Adaline算法的规则(也称Widrow-Hoff规则)与感知器之间的关键差异在于,Adaline算法规则的权重基于线性激活函数的更新,而感知器则是基于单位阶跃函数。
激活函数(Activation Function) 是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。在神经元中,输入的input经过一系列加权求和后作用于另一个函数,这个函数就是这里的激活函数。类似于人类大脑中基于神经元的模型,激活函数最终决定了是否传递信号以及要发射给下一个神经元的内容。在人工神经网络中,一个节点的激活函数定义了该节点在给定的输入或输入集合下的输出。
Adaline的先行激活函数
ϕ
(
z
)
\phi(z)
ϕ(z)是净输入的等同函数,即
ϕ
(
ω
T
x
)
=
ω
T
x
\phi(\omega^Tx)=\omega^Tx
ϕ(ωTx)=ωTx
虽然线性激活函数可用于学习权重,但我们仍然使用阈值函数进行最终的预测,这与上文提到的单位阶跃函数类似。在连续评估正确的分类标签与线性激活函数之后,Adaline算法通过比较实际标签与线性激活函数的连续有效输出以计算模型误差,并更新权重。与之相反,感应器则是比较实际分类标签和预测分类标签。
2.3.1 通过梯度下降最小化代价函数
监督机器学习算法的一个关键组成部分是在学习过程中优化的目标函数,该目标函数通常就是我们想要的最小化的代价函数。对Adaline而言,可以把学习权重的代价函数J定义为计算结果与真正分类标签之间的误差平方和SSE:
J
(
ω
)
=
1
2
∑
i
(
y
(
i
)
−
ϕ
(
z
(
i
)
)
)
2
J(\omega)=\frac{1}{2}\displaystyle\sum_{i}(y^{(i)}-\phi(z^{(i)}))^2
J(ω)=21i∑(y(i)−ϕ(z(i)))2
公式中的 1 2 \frac{1}{2} 21只是为了方便使与权重参数相关的代价函数或者损失函数的梯度推导更容易。
代价函数作为凸函数,具有凸起的优点,此时便可以使用梯度下降的优化算法来寻找权重,最小化代价函数以分类鸢尾花数据集样本。
梯度:函数对它的各个自变量求偏导后,由偏导数组成的一个向量。
梯度下降:目标是搜索出来一个能够让函数的值尽可能小的位置。主要逻辑可以理解为走下坡路直到抵达局部或全局代价最小为止。每次迭代都向梯度相反的方向上迈一步,步幅由学习速率以及梯度斜率来决定。
采用梯度下降法通过在代价函数 J ( ω ) J(\omega) J(ω)和梯度 ∇ J ( ω ) \nabla J(\omega) ∇J(ω)的相反方向上迈出一步来更新得到新的权重,即Adaline的学习规则变为: ω : ω + Δ ω \omega:\omega+\Delta\omega ω:ω+Δω
2.3.2 用Python实现Adaline
因为感知器算法规则和Adaline非常接近,因此我们仅在感知器实现的基础上修改fit方法,通过梯度下降最小化代价函数来更新权重。示例代码如下:
class AdalineGD(object):
"""ADAptive LInear NEuron classifier
Parameters
------------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
------------
w_ : 1d-array
Weights after fitting.
cost_ : list
Sum-of-squares cost function value in each epoch.
"""
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
"""Fit training data.
Parameters
-----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples and n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : obeject
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0, scale=0.01, size=1 + X.shape[1])
self.cost_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_[1:] += self.eta * X.T.dot(errors)
self.w_[0] += self.eta * errors.sum()
cost = (errors**2).sum() / 2
self.cost_.append(cost)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_[1:]) + self.w_[0]
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0, 1, -1)
在以上代码中,不再像感知器那样在每次训练模型后都更新权重,而是根据整个训练数据集来计算梯度,调用self.eta * errors.sum()来计算偏置单元(零权重),再调用self.eta * X.T.dot(errors)来计算从1到m的权重。
代码中 activation 方法仅仅作为一个标识函数来说明信息是如何通过单层神经网络流动的:从输入数据、净输入、激活到输出。最后再把收集的代价存储在 self.cost_ 列表中,以检验训练后的算法是否收敛。在实践中,我们经常要通过实验找到达到最优收敛的最佳学习效率
η
\eta
η,接下来的代码将展示选择
η
=
0.01
\eta=0.01
η=0.01和
η
=
0.0001
\eta=0.0001
η=0.0001两个不同的学习速率时代价函数与迭代次数的关系,并画出关系图:
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 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),ada2.cost_, marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Sum-squared-error')
ax[1].set_title('Adaline - Learning rate 0.0001')
plt.show()
执行代码,输出得到:
从上图可以看到,存在两种不同的问题。左图显示选择学习速率太大将会出现的情况,此时因为所选的全局最小值太低,所以代价函数无法最小化,导致结果误差经过迭代后变得越来越大。而右图则显示选择的学习速率太小,以至于算法需要经过多次迭代才能够收敛到全局最低代价。
2.3.3 通过特征缩放改善梯度下降
机器学习算法大多都需要通过某种形式的特征缩放来优化性能,梯度下降也同样。在这里,我们使用一种标准化特征缩放的方法来优化梯度下降,它会使数据具有标准正态分布的特性,即零均值和单位方差。这个标准化过程可以促进梯度下降学习更快地收敛,但它不会让原始数据集呈正态分布。
标准化会使得每个特征的均值以零为中心,并且每个特征的标准差为1(单位方差)。例如,对第
j
j
j个特征的标准化,就是简单地用每个训练样本值减去均值
μ
j
\mu_j
μj,然后再除以标准差
σ
j
\sigma_j
σj,即
x
j
′
=
x
j
−
μ
j
σ
j
x'_j=\frac{x_j-\mu_j}{\sigma_j}
xj′=σjxj−μj
其中
x
j
x_j
xj是包含所有n个训练样本的第
j
j
j个特征值的向量,该标准化技术可以应用于数据集的每个特征
j
j
j。
优化器必须遍历几个步骤才能发现好的或者最优解(全局代价最小),这是标准化有助于梯度下降学习的原因之一。用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()
通过以上代码实现标准化之后,再次训练Adaline,然后在学习速率 η = 0.01 \eta=0.01 η=0.01的条件下,可以看看它经过几轮迭代后完成了收敛:
ada_gd = AdalineGD(n_iter=15, eta=0.01)
ada_gd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_gd)
plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
plt.plot(range(1, len(ada_gd.cost_) + 1), ada_gd.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Sum-squared-error')
plt.tight_layout()
plt.show()
执行上述代码后得到决策区域和代价下降情况如下图所示:
2.3.4 大规模机器学习与随机梯度下降
以上基于整个训练数据集来计算代价梯度的方法被称为批量梯度下降。但当我们拥有一个数百万个数据点的非常大的数据集时,继续使用批量梯度下降的计算成本就巨大,因为向全局最小值的方向每迈出一步,都需要重新评估整个训练数据集。
这时候就需要考虑随机梯度下降(SGD) 算法,有时也被称为迭代或在线梯度下降法。此方法并不基于所有样本
x
(
i
)
x^{(i)}
x(i)的累计误差之和来更新权重,而是逐渐更新每个训练样本的权重,即
Δ
ω
=
η
(
y
(
i
)
−
ϕ
(
z
(
i
)
)
)
x
(
i
)
\Delta\omega=\eta(y^{(i)}-\phi(z^{(i)}))x^{(i)}
Δω=η(y(i)−ϕ(z(i)))x(i)
随机梯度下降可以看作是梯度下降的近似,但因为需要更频繁地更新权重,因此通常收敛地更快。因为要根据单个训练实例来计算每个梯度,所以误差平面比梯度下降噪声更大。如果采用非线性代价函数,随机梯度下降更容易逃脱浅度局部极小值。使用随机梯度下降时,要注意将训练数据以随即顺序呈现出来,同时要对训练数据集重新洗牌以防止迭代循环。
在训练中调整学习速率
在随机梯度下降地视线中,固定的学习速率 η \eta η经常被自适应学习速率替代。例如: C 1 [ 迭代次数 ] + C 2 \frac{C_1}{[迭代次数]+C_2} [迭代次数]+C2C1
其中 C 1 C_1 C1和 C 2 C_2 C2是常数。随机梯度下降并没有到达全局最小值,而是在一个非常靠近这个点的区域,自适应学习速率可以随时间下降,因此可以把代价进一步最小化。
随机梯度下降还有一个优点是它可以用于在线学习。在线学习中模型可以在数据到达时实现完成训练,这对累积大量数据的情况特别有用,系统可以立即适应变化,而且在存储空间有限的情况下,可以在更新模型后丢弃训练数据。
接下来我们对上一节的梯度下降学习算法做一些调整使其通过随机梯度下降更新权重。在调用fit方法的过程中,将在每个样本训练之后更新权重。此外将在实现在线学习时调用额外的partial_fit方法,不再重新初始化权重。为了验算算法在训练后是否收敛,将在每次迭代后计算训练样本的平均代价。同时还要增加一个选项,在每次迭代开始之前,对训练数据重新洗牌以避免在优化代价函数时重复循环。通过random_state函数允许为反复训练定义随机种子。代码如下:
class AdalineSGD(object):
"""ADAptive LInear NEuron classifier.
Parameters
-----------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passea over the training dataset.
shuffle : bool (default: True)
Shuffles training data every epoch if True to prevent cycles.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
coat_ : list
Sum-of-squares cost function value averaged over all training examples in each epoch.
"""
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
self.random_state = random_state
def fit(self, X, y):
"""Fit training data.
Parameters
-----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples and n_features is the number of features.
y : array-like, shpe = [n_examples]
Target values.
Returns
--------
self : obeject
"""
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):
"""Fit training data without reinitializing the weights"""
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):
"""Shuffle training data"""
r = self.rgen.permutation(len(y))
return X[r], y[r]
def _initialize_weights(self, m):
"""Initualize weights to small random numbers"""
self.rgen = np.random.RandomState(self.random_state)
self.w_ = self.rgen.normal(loc=0, scale=0.01, size=1 + m)
self.w_initialized = True
def _update_weights(self, xi, target):
""""Apply Adaline learning rule to update the weights"""
output = self.activation(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):
"""Calculate net input"""
return np.dot(X, self.w_[1:] + self.w_[0])
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0, 1, -1)
代码中的_shuffle方法时通过调用np.random钟的permutation函数来生成范围在0~100中的唯一数组成的随机序列,再以这些数字作为索引来对特征矩阵和分类标签向量进行洗牌。
最后我们调用fit方法来训练AdalineSGD分类器,并用plot_decision_regions把训练结果画出来,代码如下:
ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada_sgd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_sgd)
plt.title('Adaline - Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
plt.plot(range(1, len(ada_sgd.cost_) +1), ada_sgd.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.tight_layout()
plt.show()
执行代码得到下图:
附全文代码
#面向对象的感知器API
import numpy as np
class Perceptron(object):
"""Perceptron classifier
Parameters
-------------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-------------
w_ : id-array
Weights after fitting.
errors_ : list
Number of misclassifications (updates) in each epoch.
"""
def __init__(self,eta=0.01, n_iter=50,random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self,x,y):
"""Fit training data,
Parameters
----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples ans n_features is the number of features.
Y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : object
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0.0, scale=0.01,
size=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)
#在鸢尾花数据集上训练感知器模型
import os
import pandas as pd
df = pd.read_csv('D:/AAAAAuseful/wzu/课程/机器学习/《Python机器学习(原书第3版)》_源代码/Chapter02/iris.data',header=None, encoding='utf-8')
print(df.tail())
import matplotlib.pyplot as plt
import numpy as np
#select setosa and versicolor
y = df.iloc[0:100,4].values
y = np.where(y == 'Iris-setosa',-1,1)
#extract sepal length and petal length
X = df.iloc[0:100,[0,2]].values
#plot data
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('sepal length [cm]')
plt.ylabel('petal length [cm]')
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 updates')
plt.show()
#可视化决策边界
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, resolution=0.02):
# setup marker generator and color map
markers = ('s', 'x', 'o', '^', 'v')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
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.3, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot class examples
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.8,
c=colors[idx],
marker=markers[idx],
label=cl,
edgecolor='black')
plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')
plt.show()
#修改fit方法以实现Adaline规则
class AdalineGD(object):
"""ADAptive LInear NEuron classifier
Parameters
------------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passes over the training dataset.
random_state : int
Random number generator seed for random weight initialization.
Attributes
------------
w_ : 1d-array
Weights after fitting.
cost_ : list
Sum-of-squares cost function value in each epoch.
"""
def __init__(self, eta=0.01, n_iter=50, random_state=1):
self.eta = eta
self.n_iter = n_iter
self.random_state = random_state
def fit(self, X, y):
"""Fit training data.
Parameters
-----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples and n_features is the number of features.
y : array-like, shape = [n_examples]
Target values.
Returns
-------
self : obeject
"""
rgen = np.random.RandomState(self.random_state)
self.w_ = rgen.normal(loc=0, scale=0.01, size=1 + X.shape[1])
self.cost_ = []
for i in range(self.n_iter):
net_input = self.net_input(X)
output = self.activation(net_input)
errors = (y - output)
self.w_[1:] += self.eta * X.T.dot(errors)
self.w_[0] += self.eta * errors.sum()
cost = (errors**2).sum() / 2
self.cost_.append(cost)
return self
def net_input(self, X):
"""Calculate net input"""
return np.dot(X, self.w_[1:]) + self.w_[0]
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0, 1, -1)
#选择不同的eta画出代价函数与迭代次数的关系图
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 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),ada2.cost_, marker='o')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Sum-squared-error')
ax[1].set_title('Adaline - Learning rate 0.0001')
plt.show()
#实现标准化
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()
#再次训练Adaline
ada_gd = AdalineGD(n_iter=15, eta=0.01)
ada_gd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_gd)
plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
plt.plot(range(1, len(ada_gd.cost_) + 1), ada_gd.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Sum-squared-error')
plt.tight_layout()
plt.show()
#调整算法实现随机梯度下降
class AdalineSGD(object):
"""ADAptive LInear NEuron classifier.
Parameters
-----------
eta : float
Learning rate (between 0 and 1)
n_iter : int
Passea over the training dataset.
shuffle : bool (default: True)
Shuffles training data every epoch if True to prevent cycles.
random_state : int
Random number generator seed for random weight initialization.
Attributes
-----------
w_ : 1d-array
Weights after fitting.
coat_ : list
Sum-of-squares cost function value averaged over all training examples in each epoch.
"""
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
self.random_state = random_state
def fit(self, X, y):
"""Fit training data.
Parameters
-----------
X : {array-like}, shape = [n_examples, n_features]
Training vectors, where n_examples is the number of examples and n_features is the number of features.
y : array-like, shpe = [n_examples]
Target values.
Returns
--------
self : obeject
"""
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):
"""Fit training data without reinitializing the weights"""
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):
"""Shuffle training data"""
r = self.rgen.permutation(len(y))
return X[r], y[r]
def _initialize_weights(self, m):
"""Initualize weights to small random numbers"""
self.rgen = np.random.RandomState(self.random_state)
self.w_ = self.rgen.normal(loc=0, scale=0.01, size=1 + m)
self.w_initialized = True
def _update_weights(self, xi, target):
""""Apply Adaline learning rule to update the weights"""
output = self.activation(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):
"""Calculate net input"""
return np.dot(X, self.w_[1:] + self.w_[0])
def activation(self, X):
"""Compute linear activation"""
return X
def predict(self, X):
"""Return class label after unit step"""
return np.where(self.activation(self.net_input(X)) >= 0, 1, -1)
#训练AdalineSGD分类器,画出训练结果
ada_sgd = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada_sgd.fit(X_std, y)
plot_decision_regions(X_std, y, classifier=ada_sgd)
plt.title('Adaline - Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
plt.plot(range(1, len(ada_sgd.cost_) +1), ada_sgd.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.tight_layout()
plt.show()