机器学习中监督学习算法的一个核心在于:在学习阶段定义一个待优化的目标函数,然后将这个目标函数做最小化的处理的代价函数,以便寻找出最优的权重(参数)w。在求解权重w的优化算法时,使用较多的就是梯度下降算法(Gradient Descent, GD)。
梯度下降算法的优点在于,在梯度下降算法求解中只需要求解损失函数的一阶导数,计算成本比较小,这样梯度下降算法可以在大规模数据集上的到应用。
梯度下降的原理:
梯度下降原理可以形象的描述为下山,直到走到一个局部或者全局最优解。在每次梯度迭代中,根据给定的学习速率和梯度的斜率,能够确定每次移动的步幅,按照步幅沿梯度的方向前进。
假设我们有两个权重θ0,θ1要处理,则我们只需要找到代价函数在局部或全局最小时θ0,θ1的值:
算法实现:
梯度下降的算法原理为如下公式:
θj :=θj - α * (∂/∂θj) * J(θ)
公式解释:
θj:表示第j个权重(参数),α:表示学习速率,J(θ):代价函数,:= 表示赋值
特别说明:为了简化代价函数我用J(θ)表示,一般用J(θ0,θ1,…,θn)。
算法步骤:
- 为权重θ设置一个初始值(一般初始权重都设置为0就行)。
- 不断更新权重θ,使J(θ)变小,直到J(θ)得到一个全局最小值或局部最小值。
总结如下:
1、每一次迭代都同步更新所有权重(在使用过程中要先将导数项解出来)
θ0 = θ0 - α * (∂/∂θ0) * J(θ)
θ1 = θ1 - α * (∂/∂θ1) * J(θ)
……
θn = θn - α * (∂/∂θn) * J(θ)
2、计算J(θ)的值
3、重复步骤1,2
关于学习速率α:
对于学习速率α,若果选择太小,会导致收敛的速度比较慢。如果选择太大则会出现跳过最优解,在最优解附件徘徊。假设只有一个权重值,太小或太大会出现以下情况。
具体示例:
还是鸢尾花数据,假设我们还选取两个特征x1,x2。并假设函数为h(x)=θ1*x1+θ2*x2+θ0。设x0 = 1则h(x)=θ1*x1+θ2*x2+θ0*x0。对于线性回归我们设代价函数为J(θ)=1/2 * ∑(h(xi) - yi)²。
求导(粘个图片):
得到权重更新规则:
Python实现梯度下降算法求解权重:
鸢尾花数据集是机器学习的一个经典示例,它包含Setosa,Versicolor,Virginica三个品种,现在我们用鸢尾花的前两个品种并选取两个特征做分类。
首先我们拿出数据进行预处理,并进行可视化:
import pandas as pd
import matplotlib.pylab as plt
df = pd.read_csv('iris.data', header=None)
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()
得到鸢尾花的散点图:
梯度下降求解权重:
import numpy as np
#梯度下降
class AdalineGD():
'''
梯度下降算法求权重,当代价函数的值收敛到最优解或局部最优解时得到合适的权重W
'''
def __init__(self, eta=0.01, n_iter=50):
self.eta = eta
self.n_iter = n_iter
def fit(self, X, y):
#w_:权重并增加初始权重w0,cost_:存放每次迭代后代价函数的值
self.w_ = np.zeros(X.shape[1] + 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)
绘制两个不同的学习速率下,代价函数与迭代次数的图像:
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('Epoches')
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('Epoches')
ax[1].set_ylabel('log(Sum-squared-error)')
ax[1].set_title('Adaline - Learning rate 0.0001')
plt.show()
我们得到:
上图可以看出,我们面临的两种问题。左图显示学习速率过大会出现的问题:没有使代价函数值尽可能低反而跳过了全局最优解,导致误差随着迭代次数增加而增大。在右图,虽然代价函数的值在逐渐减小,但是对于α=0.0001的值太小,以导致为了达到算法收敛的目标而需要更多的迭代次数。
在这里我们可以将特征进行缩放以优化算法的性能:
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()
在进行标准我们再学习速率α=0.01进行训练:
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.4, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
#plot class samples
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)
ada = AdalineGD(eta=0.01, n_iter=15)
ada.fit(X_std,y)
plot_decision_regions(X_std, y, classifier=ada)
plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [stadardizsed]')
plt.ylabel('petal length [stardarizsed]')
plt.legend(loc='upper left')
plt.show()
plt.plot(range(1, len(ada.cost_)+1), ada.cost_, marker='o')
plt.xlabel('Epoches')
plt.ylabel('Sum-squared-error')
plt.show()
得到决策区域以及代价函数逐渐收敛的图:
在这里梯度下降算法解决了感知器的问题,感知器面临的算法的收敛的问题。在感知器算法中,如果两个类别无法通过线性判定边界完全正确划分,则权重会不断更新。为了证明这个我们更改训练的样本特征
import pandas as pd
import matplotlib.pylab as plt
df = pd.read_csv('iris.data', header=None)
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()
得到新的鸢尾花数据的散点图:
我们还以上面的学习速率α=0.01进行训练,得到新的决策边界和代价函数收敛图:
如上图,可以见到算法得到了正确的决策边界,而且代价函数也成功的收敛了。
随机梯度下降:
在梯度下降中还存在一个问题,在梯度下降中权重每一次更新都需要用整个训练集来进行评估。假设我们有一个包含几百万条数据的巨大训练集,这样对于每一次的权重更新我们将消耗巨大的计算代价,这种情况下梯度下降算法需要的计算成本非常高。为了解决这个问题我们引用新的梯度下降的优化算法,随机梯度下降或者在线梯度下降。与基于所有样本的累积误差更新权重策略不同,我们每一次使用一个训练样本进行更新权重。
θj := α * (h(x) - y) * x
虽然随机梯度下降可以看做是多梯度下降的近似,但是由于权重更新更频繁,通常它会更快收敛。由于梯度的计算是基于单个训练样本来完成的,因此其误差曲面不及梯度下降的平滑,这样使得梯度下降更容易跳出小范围的局部最优点。为了通过随机梯度下降得到更加准确的结果,可以让数据以随机的方式提供给算法,这也是每次迭代都要打乱训练集防止进入循环的原因。随机梯度下降不一定会得到全局最优解,但是会趋近它,借助于自适应学习速率,我们可以更近一步趋于全局最优解。
自适应学习速率:
通常使用随着时间变化的自适应学习速率大代替固定学习速率,例如
α = c1 / ([迭代次数] - c2) (c1和c2都是常数)
随机梯度下降的另一个优势是可以将其用于在线学习。通过在线学习,当新的数据输入时会被实时训练。这在对于面临海量数据是特别有效,例如针对Web中的用户信息。使用在线学习,系统可以及时的适应变化,同时如果考虑存储成本的话也可以将训练数据在完成对模型更新后丢弃。
小批次学习:
小批次学习是介于梯度下降和随机梯度下降的一种技术。可以将小批次学习理解为在相对较小的训练集上应用梯度下降。例如,一次使用50个样本。与梯度下降相比,由于权重更新更加频繁,因此收敛速度更快。以下实现小批次学习。
在小批次学习算法中,增加了一个partial_fit的方法,对于在线学习,此方法不会更新权重。为了检验算法在训练后是否收敛,将每次迭代后计算出的代价值作为训练集的平均消耗。此外,增加一个shuffle训练数据的选项,在每次迭代前重排训练数据避免在优化时陷入循环。通过random_state参数指定随机数种子以保持多少次训练的一致性。
在算法中_shuffle的工作原理如下:通过numpy.random的permutation的函数生成一个包含0-100的不重复随机序列。这些数字可以打乱特征矩阵和类标向量。
import numpy as np
from numpy.random import seed
class AdalineSGD():
def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
self.eta = eta
self.n_iter = n_iter
self.shuffle = shuffle
self.w_initialized = False
if random_state:
seed(random_state)
def fit(self,X,y):
self._initialized_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._initialized_weights(X.shape[1] + 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) #如果传入参数是X那么更新权重计算是要将X转置计算
return self
#将训练样本随机打乱
def _shuffle(self, X, y):
r = np.random.permutation(len(y))
return X[r],y[r]
#生成权重
def _initialized_weights(self, m):
self.w_ = np.zeros(m+1)
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 = error ** 2 / 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, 1, -1)
通过fit方法训练算法:
ada = AdalineSGD(eta=0.01, n_iter=30, random_state=1)
ada.fit(X_std,y)
plot_decision_regions(X_std, y, classifier=ada)
plt.title('Adaline - Gradient Descent')
plt.xlabel('sepal length [stadardizsed]')
plt.ylabel('petal length [stardarizsed]')
plt.legend(loc='upper left')
plt.show()
plt.plot(range(1, len(ada.cost_)+1), ada.cost_, marker='o')
plt.xlabel('Epoches')
plt.ylabel('Sum-squared-error')
plt.show()
得到的决策边界和代价函数的收敛情况:
如图,可见进过迭代后最终的分界线也成功的将两种数据分割。如果用于处理在线学习可以对单个样本简单调用parital_fit方法,如:ada.parital_fit(X_std[0, :], y[0])。