梯度

对梯度的概念

梯度概念是建立在偏导数与方向导数概念基础上的。所谓偏导数,简单来说是对于一个多元函数,选定一个自变量并让其他自变量保持不变,只考察因变量与选定自变量的变化关系。数学上说,是指对于多元函数y=f(x_0,x_1,...x_n),假设其偏导数都存在,则该函数共有n个偏导数,可以表示为:

{f_{{x_0}}} = {{\partial y} \over {\partial {x_0}}},{f_{{x_1}}} = {{\partial y} \over {\partial {x_1}}}...{f_{{x_n}}} = {{\partial y} \over {\partial {x_n}}}

偏导数只能表示多元函数沿某个坐标轴方向的导数,如对于二元函数z=x^2+y^2{{\partial z} \over {\partial x}} = 2x表示函数沿X轴方向的导数,而{{\partial z} \over {\partial y}} = 2y表示函数沿Y轴方向的导数。

除开沿坐标轴方向上的导数,多元函数在非坐标轴方向上也可以求导数,这种导数称为方向导数。很容易发现,多元函数在特定点的方向导数有无穷多个,表示函数值在各个方向上的增长速度。一个很自然的问题是:在这些方向导数中,是否存在一个最大的方向导数,如果有,其值是否唯一?为了回答这个问题,便需要引入梯度的概念。

一般来说,梯度可以定义为一个函数的全部偏导数构成的向量(这一点与偏导数与方向导数不同,两者都为标量)。一般将函数f的梯度记为∇f,即:

\nabla f(x_0,x_1,...x_n)=\left[\begin{matrix}{{\partial f} \over {\partial x_0}}(x_0,x_1,...x_n)\ {{\partial f} \over {\partial x_1}}(x_0,x_1,...x_n)\ \vdots \end{matrix}\right]

直观理解

直观点说,梯度就是某一点函数值的变化趋势。既然是变化趋势,肯定就不是一个标量,而是一个具有方向和大小的向量。
从一元函数理解,一元函数的梯度就是导数。其导数的方向就是函数的变化方向。
对于二元函数y=f(x_0,x_1),由于有两个自变量,其变化趋势的方向就是对每个自变量求偏导数形成的一个方向向量。
举个例子。在一个凹凸不平的地面上放置一个小球,忽略摩擦力,小球总会有一个往下滚动的趋势。这个趋势是有方向和大小的。对于一个三维空间,小球的变化趋势就是一个三个维度的向量。而这个三维向量,就是那一点的梯度。

梯度下降算法

了解了什么是梯度,再理解梯度下降法就不难了。梯度下降法的作用就是搜索函数中的极值点。

算法思想

依然拿那个小球打比方,把小球放在凹凸不平的地面上,忽略摩擦力,小球总会有往下滚动的趋势。给小球一单位时间的时间,让其往下滚,小球就会滚向另一个位置。可以想象得到,坡度越陡,小球在这段时间内就会滚得越远,坡度越平缓,小球就滚得越近。时间结束后,让小球停止,重新开始计时并让其滚动。最终,小球就会滚到一个凹的地方。这个地方不一定是整个地面的最低点,但是一定是一个旁边高,中间地的一个凹坑。这个坑,就是三维空间中的一个极值点。

一元函数最小值问题

假定有一个一元函数F(x)=x^2-x+1。初始值x_0=-1,通过梯度下降法求最小化目标函数F的搜索轨迹。
首先,我们要求出 F(x) 的梯度函数\nabla F(x)=F'(x)=2x-1
定义一个步长eta=0.1,精度epsilon=0.01
计算x的变化趋势。当\nabla F(x) < epsilon 时接近极值点,停止搜寻。否则更新x得到值,使x向变化趋势方向前进一步。

第1次搜索:\nabla F(x_0)=-3
x_1=x_0-eta\times \nabla F(x_0)=-1-0.1\times -3=-0.7

第2次搜索:\nabla F(x_1)=-2.4
x_2=x_1-eta\times \nabla F(x_1)=-2.4-0.1\times -2.4=-0.46

............

第27次搜索: \nabla F(x_{26})=-0.0090 < epsilon搜索终止。

设计算法

import numpy as np
import matplotlib.pyplot as plt


def F(w):  # F(w) = w^2-2+1
    return w ** 2 - w + 1


def Fg(w):  # F'(w) = 2w-1
    return 2 * w - 1


X, y = [], []  # 用于记录搜寻轨迹
eta, epsilon = 0.1, 0.01  # 步长和精度
w = -1  # 初始值
while True:
    X.append(w)  # 记录w的值
    y.append(F(w))  # 记录F(w)的值
    g = Fg(w)  # 求F'(w)
    w = w - eta * g  # 更新w
    if abs(g) < epsilon:
        break  # 小于搜寻精度 跳出

W = np.linspace(-1, 2, 100).reshape(100, 1)
U = F(W)
plt.plot(W, U)  # 画曲线
plt.scatter(X, y, s=15)  # 画搜寻过程中的每个点
plt.show()

运行结果:

梯度和梯度下降算法
可以看出,从x=-1开始搜索,顺着梯度下降的方向,最终就能找到极值点x=0.5。
多元函数也是同样的思想。

线性回归问题

用梯度下降算法求解线性回归问题,设目标函数为均方误差:

F(w)={1\over m}||Xw-y||^2

求出目标函数的最小值,就找到了线性回归的最优解。

F(w)的梯度为:

\nabla F(x)={2\over m}X^T(Xw-y)

同一元函数的最小值求法,设计算法:

定义梯度算法模型:
LinearRegression

import numpy as np


class LinearRegression:
    def fit(self, X, y, eta, epsilon):
        m, n = X.shape
        w = np.zeros((n, 1))  # w初始值是n行1列的零矩阵
        while True:
            g = 2 * X.T.dot(X.dot(w) - y) / m  # F(w)的梯度
            w = w - eta * g
            if np.linalg.norm(g, 2) < epsilon:  # 梯度g的L2范数 <epsilon 结束
                break
        self.w = w

    def predict(self, X):
        return X.dot(self.w)

使用模型处理糖尿病数据

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from machine_learning.search_algorithms.lib.linear_regression_gd import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
from sklearn.datasets import load_diabetes
import matplotlib.pyplot as plt


def process_features(X):  # 特征预处理
    scaler = StandardScaler()
    X = scaler.fit_transform(X)  # 标准化
    m, n = X.shape
    X = np.c_[np.ones((m, 1)), X]  # 首位加1
    return X


diabetes = load_diabetes()  # 加载糖尿病数据
X = diabetes.data
y = diabetes.target.reshape(-1, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)  # 分割训练集和测试集
X_train = process_features(X_train)
X_test = process_features(X_test)

model = LinearRegression()  # 定义梯度下降算法模型
model.fit(X_train, y_train, eta=0.01, epsilon=0.01)  # 训练数据
y_pred = model.predict(X_test)  # 预测数据
mse = mean_squared_error(y_test, y_pred)  # 计算均方误差mse
score = r2_score(y_test, y_pred)  # 计算决定系数
print("mse = {}, r2 = {}".format(mse, score))

m, n = y_pred.shape
horizon_axis = np.arange(0, m, 1)

font = {'family': 'SimHei',
        'weight': 'normal',
        'size': 23,
        }
plt.figure(figsize=(18, 6), )  # 画出每组数据的预测值和真实值
plt.xlabel("数据编号", font)
plt.ylabel("观测值", font)
plt.plot(horizon_axis, y_pred)
plt.scatter(horizon_axis, y_pred, s=15)
plt.plot(horizon_axis, y_test, color="red")
plt.scatter(horizon_axis, y_test, s=15)
plt.legend(('Pred', 'Test'), loc='upper right', prop=font)
plt.show()

plt.figure(figsize=(18, 6))  # 画出每组数据的预测值和真实值的差异
plt.xlabel("数据编号", font)
plt.ylabel("预测值与真实值的差异", font)
plt.plot(horizon_axis, y_pred - y_test, color="blue")
plt.scatter(horizon_axis, y_pred - y_test, s=15)
ax = plt.gca()  # get current axis 获得坐标轴对象
ax.spines['right'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.spines['top'].set_position(('data', 0))  # 指定 data  设置的bottom(也就是指定的x轴)绑定到y轴的0这个点上
plt.show()

预测结果:

mse = 3115.5158566159726, r2 = 0.4304028198845443

梯度和梯度下降算法
梯度和梯度下降算法
可以看到,部分预测值和真实值差异还是挺大的,可能是因为数据维度太高,训练数据集太少的原因。

梯度上升算法

梯度上升算法和下降算法只是变化趋势方向相反而已,梯度下降算法是为了找最小值,梯度下降算法则是找最大值。
只需把梯度下降算法中的w = w - eta * g 中的减号改成加号,就成了梯度上升算法。


我一直在开辟我的天空