2020-08-19--梯度下降法01

  • 梯度下降法简介
  • 多元线性回归中的梯度下降法
  • 随机梯度下降法
  • 梯度下降法 的调试

1.梯度下降法简介

  • 不是一个机器学习算法
  • 是一种基于搜索的最优化方法
  • 作用:最小化损失函数 (最小值的点)
  • 梯度上升法:最大化一个效用函数(最大值的点 )

1.1一元线性回归梯度下降原理分析

首先我们一元线性回归为例:

一元线性回归的目标:

我们的目标是求a和b的值,以作到损失函数最小。

在上式中X(i)和y(i)为训练集数据,我们假设这个直线最终结果是一个没有截距过原点的直线,也就是将b看作一个常数(或者将b直接去掉),对公式进行变换:

对于第i个数据的损失函数表达式为:

那么这样看的话这是一个关于a的一元二次方程。
总共有m个数据,他们每一个损失函数的值相加,最后相加的结果还是一个一元二次方程。

所以我们的目标就是找到该方程的最小值。

参数theta对应的损失函数J的值对应的示例图,我们需要找到使得损失函数值J取得最小值对应的theta(这里是二维平面,也就是我们的参数只有一个)

1.我们要找的就是图中的theta(m)值。
  • 在直线方程中,导数代表斜率。
  • 在曲线方程中,导数代表切线斜率。斜率越大,变化越快。
  • theta越靠近theta(m),J的变化越慢
2.怎么理解图中的导数可以代表方向,对应J增大的方向以及公式?

我们对图中任意一点求导(切线),这个切线都是有方向的

  • 在最小值左侧,切线的斜率是负的,对应途中的(1),它的指向为J增大的方向
  • 在最小值的右侧,切线斜率是正的,对应图中的(2),它的指向也为J增大的方向
  • 而我们的目标是向最小值的为晃着靠近,所以对每一个点求导之后,取相反数,那么这样切线就指向J减小的方向。
3.对于学习率n,接下来介绍:

η太小,会减慢收敛学习速度:

η太大,甚至导致不收敛:

那么这样的话表达式:

我们在知道下边的原理就可以理解了。

寻找最小点theta(m)的原理过程:

在图中的线上随机寻找一个点,我们要不断的更新theta的值,而这个更新值与原值之间的区间就是上边的公式(我们暂且将公式值即为p),那么下一次更新的值就为(theta+p)。由于这个更新值一定指向J缩小的方向,所以theta不断靠近theta(m),直到某一次theta对应的J比上一次的theta值对应的J小于某一个很小的值(越靠近theta(m),J的变化速度越小),说明我们找到了该点theta。

4.其他注意事项

并不是所有函数都有唯一的极值点

5.解决方案:
  • 多次运行,随机化初始点
  • 梯度下降法的初始点也是一个超参数
6.有b时的情况

在之前的分析中我们把b看作一个常数/0。

如果加上b的话,那么损失函数就随着a,b两个参数而做改变,这就是一个三维的空间,如果做图的话,x为theta,y为b,z为J:

那么画出来就类似于倒过来的大钟。

1.2模拟梯度下降法

接下来我们使用代码来实现上边的分析过程。

1.创建数据集,画图:
import numpy as np
import matplotlib.pyplot as plt

# 生成等差数列[-1,6],个数为141个
plot_x = np.linspace(-1,6,141)
print(plot_x)
# print(len(plot_x))

# 生成一元二次方程
plot_y = (plot_x-2.5)**2 - 1
print(plot_y)

# 画图
plt.plot(plot_x,plot_y)
plt.show()

运行:

2.实现梯度下降:
# 学习率n
eta = 0.1
# 最小差距
epsilon = 1e-8

# 定义生成J值得函数
def J(theta):
    return (theta-2.5)**2 - 1

# 定义d(J)/d(theta)得值
def DJ(thate):
    return 2*(thate-2.5)

# 设置初始theta
theta = 0.0
while True:
    # 记录当前位置,后边用于判断是否跳出
    last = theta

    # 计算下一个点的位置,直接赋给循环变量theta
    theta = theta-eta * DJ(theta)

    if abs(J(theta) - J(last)) < epsilon:
        break

print(theta)
print(J(theta))
# 2.499891109642585
# -0.99999998814289

分析:

  1. 首先定义超参数n和最小差距,然后定义两个函数J和DJ,计算某一点的J值,以及计算某一点J对theta的导。
  2. 设置超参数--初始点的theta,进入无限迭代中,先记录本次点的theta值定义last,然后计算下一个点的theta,判断本次theta和上一次theta点的J值差距是否小于最小差距,若是则跳出,否则继续循环。

3.生成梯度下降寻找最小值的过程展示:

import matplotlib.pyplot as plt
import numpy as np

# 设置初始theta
theta = 0.0
# 记录寻找过程中的点
theta_list = []
while True:
    # 记录当前位置,后边用于判断是否跳出
    last = theta
    theta_list.append(theta)
    # 计算下一个点的位置,直接赋给循环变量theta
    theta = theta-eta * DJ(theta)

    if abs(J(theta) - J(last)) < epsilon:
        break
print(len(theta_list))      # 45

# 画图
# J函数的x,y
plot_x = np.linspace(-1,6,141)
plot_y = J(plot_x)

plt.plot(plot_x,plot_y)
plt.plot(theta_list,J(np.array(theta_list)),color='r',marker='+')
plt.show()

运行:

由图:
从左到右的过程中,+号越来月稀疏。
总共循环了45次,找到了这个theta最小值。
这个循环次数是由学习率n,初始点,和最小差距决定的。
例如:
1.将学习率调小

将学习率n为0.000001
那么,循环次数达到了1956011次

运行:

我们可以多次改变初始点的位置以保证准确性。

2.调大学习率

将学习率n为0.8
那么,循环次数为21次

运行:

学习率不能调的过大,结果会过大,会报错。

修改J函数:

def J(theta):
    try:
        return (theta-2.5)**2 - 1
    except:
        return float('inf')       # 返回一个很大的float数

J()在返回值进入下方循环中时,能保证不报错,但是会陷入死循环中,因为在学习率很大时,在x轴上的跳跃很快,当循环次数达到一定程度,就会超出范围。

所以在下边我们封装为函数后,可以加一个循环次数的参数,就可以查看其超出范围是怎么回事了。

3.封装为函数
def gradient_descent(init_theta,eta,n_iters=1e4,epsilon=1e-8):
    '''

    :param init_theta: 初始点的theta值
    :param eta:  学习率
    :param n_iters: 循环次数,默认为1e4
    :param epsilon:  最小差距值,默认为1e-8
    :return:
    '''
    # 将初始化的init_theta赋给theta
    theta = init_theta
    i_iter = 0       # 初始化循环次数
    theta_history = []      #  初始化路径列表

    while i_iter < n_iters:        # 当循环次数小于参数n_iter时,执行循环
        last = theta             # 记录本次的theta
        theta_history.append(theta)          # 传进列表
        theta = theta - eta * DJ(theta)        # 计算下次的theta
        # 判断上次的J值和本次的J值差距是否小于最小值
        if abs(J(theta) - J(last)) < epsilon:
            break
        # 循环次数+1 
        i_iter += 1
    return theta,theta_history      # 最佳theta值和寻找theta值路径列表

调用:

# 调用函数
eta = 1.1
result,result_list = gradient_descent(init_theta=0.0,eta=eta,n_iters=10)
print(result)
print(result_list)

# 画图
plt.plot(plot_x,plot_y)
plt.plot(result_list,J(np.array(result_list)),color='r')
plt.show()

将eta设置为1.1后,循环次数设为9次,运行:

开始值很小,慢慢的J值变化越来越快,就会溢出。

2. 线性回归中的梯度下降法

上述的过程实现了简单的线性回归关于y = ax的实现,而y = ax+b以及多元线性回归中的y = a(0)+a(1)x(1)+a(2)x(2)+...a(n)x(n)并没有实现。

来看看多元线性回归:

y轴还是J函数,x轴变为一个数据集---(theta(0),theta(1),...theta(n))。为了表达清除,化成平面图,其实这个模型图是一个n维的模型图,我们画不出来,但是我们可以画一个三维的来看一下:

一个三维空间中的梯度下降法(x,y为系数,z为损失函数):

在这里中theta是一个向量,可将y = ax+b中的b看作a(0),那么对于这个一元线性回归来说,这个theta就是一个有两行一列的向量。多元线性方程是多行一列的向量。,其实本质上都是一样的。

原理:
原理与一元线性回归一样,随即设置一个点,不断的迭代更新该点,进而找出最优点。

  • 这里就牵扯到一个变量--更新量。也就是一元中的【-n(d(J)/d(theta))】
    在多元中,由于theta是一个向量,那么要不断更新该点的位置,那么这个更新量也必须是一个向量,该向量用【-n(J对每一个theta的偏导)】
  • 这样(广播操作)向量-向量后的结果还是向量,达到了移动该点的效果。对应图中的(1)(2)点的效果。
  • 根据新的theta求出该点的J,比较差距跳出即可。

1.损失函数求导的公式推导

对于多元的损失函数来说,他不想一元的J那样求导的话一眼能看出来,
例如:
J =(theta-2.5)2 -1
DJ = 2
(theta-2.5)
多元的J比较复杂:

不可能看出来答案,所以我们要对它进行推导。

推导:

推导思路与线性回归章节中的思想一样,将y_head表达为两个矩阵的点乘进行计算:

y_head = X(b) (*) theta

X(b)和theta的表示:

所以对J可以表示为:

对于J求导:

求导的依据【标量对列向量】的求导方法进行求导。

但是上面推导出的式子的大小是和样本数有关的,m越大,结果越大,这是不合理的,我们希望和m无关,所以对损失函数J除以m:

那么J导就变为:

这样的话J函数对theta向量求导后的数据就表示出来了。

2.在线性回归的梯度下降代码实现

1.J的函数定义

损失函数的公式:


根据公式得知参数有 测试数据:X_b,对应真实值y,以及theta值。

# 损失函数J
def J(theta,X_b,y):
    try:
        return np.sum((y - X_b.dot(theta))**2)/len(X_b)
    except:
        return float('inf')
2.DJ的实现

对DJ的公式进行分析:

DJ的公式结果是一个向量,其中数据的个数是由theta向量个数决定的,因为他每一项都是J/theta的导。所以y = ax+b其实就相当于只有theta(0)和theta(1)两个元素。

我们将它分为两个部分,第一部分是对theta(0)的导,其余部分是第二部分。这样我们便于计算。

参数列表:测试集数据X_b,对应真实值y,以及theta向量。

DJ函数:

def DJ(theta,X_b,y):
    res = np.empty(len(theta))
    # 第一个元素
    res[0] = np.sum(X_b.dot(theta) - y)
    # 第二个往后
    for i in range(1,len(theta)):
        res[i] = (X_b.dot(theta) - y).dot(X_b[:,i])

    return res *2 / len(X_b)

分析:

  1. 首先创建一个与theta向量等长的数组res,用于填充数据。这个原因上边说过。计算第一部分数据的值--计算括号内数据直接求和即可,赋给res[0]。
  2. 其他数据项使用for循环循环计算,为了便于计算,我们把它换位向量的点乘。
    首先分析这其中一项:
  • 该数据项是m个数据值之和,他的每一项数据值都是标量*标量,例如第二行分析:


所以就可以将公式看作(以第二行数据为例):

那么用就可以在代码用for循环给每一项赋值。

  1. 最后返回res * 2 / len(X_b)。
3.梯度下降函数

与之前的y = ax的函数大概一致,只是在函数中调用J和DJ函数时要使用的参数X_b和y要加在该函数的参数中。

gradient_descent_many:

def gradient_descent_many(X_b,y,init_theta,eta,n_iters=1e4,epsilon=1e-8):
    '''
    :param X_b: 数据集,经过改造(第0列都为1)
    :param y:  数据集对应正确值
    :param init_theta: 初始点的theta值,一个向量
    :param eta:  学习率
    :param n_iters: 循环次数,默认为1e4
    :param epsilon:  最小差距值,默认为1e-8
    :return: theta向量
    '''
    # 将初始化的init_theta赋给theta
    theta = init_theta
    i_iter = 0       # 初始化循环次数
    # theta_history = []      #  初始化路径列表

    while i_iter < n_iters:        # 当循环次数小于参数n_iter时,执行循环
        last = theta             # 记录本次的theta
        # theta_history.append(theta)          # 传进列表
        theta = theta - eta * DJ(theta,X_b,y)        # 计算下次的theta
        # 判断上次的J值和本次的J值差距是否小于最小值
        if abs(J(theta,X_b,y) - J(last,X_b,y)) < epsilon:
            break
        # 循环次数+1
        i_iter += 1

    return theta
4.生成数据,测试梯度下降
'''生成数据'''
np.random.seed(666)
x = 2 * np.random.random(size=100)
# 修改为100行1列的二维数组,也就是100个数据,每个数据1个特征
X = x.reshape(-1,1)
# 生成100个靠近y = 3x + 4 直线的点
y = x * 3. + 4. + np.random.normal(size=100)
# 将X修改其中数据值为X_b
ones = np.ones((len(X),1))
X_b = np.hstack([ones,X])
print(X_b[:5])
# [[1.         1.40087424]
#  [1.         1.68837329]
#  [1.         1.35302867]
#  [1.         1.45571611]
#  [1.         1.90291591]]

# 设置init_theta,一个列向量,其长度与数据集X_b的维度(列数)相同
# 不能是X,因为theta向量包含theta(0)
init_theta = np.zeros(X_b.shape[1])
print(init_theta)
# [0. 0.]

eta = 0.01

如果梯度下降法寻找出来的theta(0)接近4,theta(1)接近3,那么就说明找到了。

theta = gradient_descent_many(X_b,y,init_theta,eta)
print(theta)
# [4.02145786 3.00706277]

成功!!!

5.整体代码
import numpy as np
import matplotlib.pyplot as plt

# 损失函数J
def J(theta,X_b,y):
    try:
        return np.sum((y - X_b.dot(theta))**2)/len(X_b)
    except:
        return float('inf')

def DJ(theta,X_b,y):
    res = np.empty(len(theta))
    # 第一个元素
    res[0] = np.sum(X_b.dot(theta) - y)
    # 第二个往后
    for i in range(1,len(theta)):
        res[i] = (X_b.dot(theta) - y).dot(X_b[:,i])

    return res *2 / len(X_b)

def gradient_descent_many(X_b,y,init_theta,eta,n_iters=1e4,epsilon=1e-8):
    '''

    :param X_b: 数据集,经过改造(第0列都为1)
    :param y:  数据集对应正确值
    :param init_theta: 初始点的theta值,一个向量
    :param eta:  学习率
    :param n_iters: 循环次数,默认为1e4
    :param epsilon:  最小差距值,默认为1e-8
    :return:
    :return:
    '''
    # 将初始化的init_theta赋给theta
    theta = init_theta
    i_iter = 0       # 初始化循环次数
    # theta_history = []      #  初始化路径列表

    while i_iter < n_iters:        # 当循环次数小于参数n_iter时,执行循环
        last = theta             # 记录本次的theta
        # theta_history.append(theta)          # 传进列表
        theta = theta - eta * DJ(theta,X_b,y)        # 计算下次的theta
        # 判断上次的J值和本次的J值差距是否小于最小值
        if abs(J(theta,X_b,y) - J(last,X_b,y)) < epsilon:
            break
        # 循环次数+1
        i_iter += 1

    return theta

'''生成数据'''
np.random.seed(666)

x = 2 * np.random.random(size=100)
# 修改为100行1列的二维数组,也就是100个数据,每个数据1个特征
X = x.reshape(-1,1)
# 生成100个靠近y = 3x + 4 直线的点
y = x * 3. + 4. + np.random.normal(size=100)
# 将X修改其中数据值为X_b
ones = np.ones((len(X),1))
X_b = np.hstack([ones,X])
print(X_b[:5])
# [[1.         1.40087424]
#  [1.         1.68837329]
#  [1.         1.35302867]
#  [1.         1.45571611]
#  [1.         1.90291591]]

# 设置init_theta,一个列向量,其长度与数据集X_b的维度(列数)相同
# 不能是X,因为theta向量包含theta(0)
init_theta = np.zeros(X_b.shape[1])
print(init_theta)
# [0. 0.]
eta = 0.01

theta = gradient_descent_many(X_b,y,init_theta,eta)
print(theta)
# [4.02145786 3.00706277]

3. 封装梯度下降函数-->线性回归类

在之前我们写的线性回归的类中。我们的fit方法是通过公式推导的方式计算theta的,而梯度下降法是通过不断迭代寻找最好的theta的。

fit_gd():梯度下降法函数,在该函数中封装了三个函数:

  • J():计算损失函数
  • DJ():计算损失函数的导
  • gradient_descent():寻找最佳theta
  • fit_gd()的参数为原始训练数据,而这三个内部函数的参数数据为经过处理的数据,所以在内部函数外要进行数据整理(X_b)以及参数(init_theta,eta)的设置。
class LinearRegression:

    def __init__(self):
        """初始化Linear Regression模型"""

        ## 系数向量(θ1,θ2,.....θn)
        self.coef_ = None
        ## 截距 (θ0)
        self.interception_ = None
        ## θ向量
        self._theta = None

    '''线性回归公式训练模型,计算theta'''
    def fit_normal(self, X_train, y_train):
        # 拼接为X(b)格式的数据,-----在每行的第一列之前加上1.
        ones_vector = np.ones((len(X_train), 1))
        X_b = np.hstack([ones_vector, X_train])

        # 根据X_b带入公式计算w
        # arr.dot(arr):点乘
        # np.linalg.inv(arr):矩阵的逆
        self._theta = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y_train)

        self.coef_ = self._theta[1:]
        self.interception_ = self._theta[0]

        return self

    '''使用梯度下降寻找theta'''
    def fit_gd(self, X_train, y_train, eta=0.01, n_iters=1e4):
        """根据训练数据集X_train, y_train, 使用梯度下降法训练Linear Regression模型"""
        assert X_train.shape[0] == y_train.shape[0], \
            "the size of X_train must be equal to the size of y_train"

        def J(theta, X_b, y):
            try:
                return np.sum((y - X_b.dot(theta)) ** 2) / len(y)
            except:
                return float('inf')

        def dJ(theta, X_b, y):
            res = np.empty(len(theta))
            res[0] = np.sum(X_b.dot(theta) - y)
            for i in range(1, len(theta)):
                res[i] = (X_b.dot(theta) - y).dot(X_b[:, i])
            return res * 2 / len(X_b)

        def gradient_descent(X_b, y, initial_theta, eta, n_iters=1e4, epsilon=1e-8):

            theta = initial_theta
            cur_iter = 0

            while cur_iter < n_iters:
                gradient = dJ(theta, X_b, y)
                last_theta = theta
                theta = theta - eta * gradient
                if (abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon):
                    break

                cur_iter += 1

            return theta

        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])
        self._theta = gradient_descent(X_b, y_train, initial_theta, eta, n_iters)

        self.interception_ = self._theta[0]
        self.coef_ = self._theta[1:]

        return self


    '''测试集'''
    def perdict(self, X_perdict):
        ones_vector = np.ones((len(X_perdict), 1))
        X_b = np.hstack([ones_vector, X_perdict])

        y_head = X_b.dot(self._theta)

        return y_head

    '''MSE'''
    def mean_squared_error(self, y_true, y_predict):
        """计算y_true和y_predict之间的MSE"""
        assert len(y_true) == len(y_predict), \
            "the size of y_true must be equal to the size of y_predict"

        return np.sum((y_true - y_predict) ** 2) / len(y_true)

    '''R^2'''
    def r2_score(self, y_true, y_perdict):
        r2_score = 1 - self.mean_squared_error(y_true, y_perdict) / np.var(y_true)
        return r2_score

    def __repr__(self):
        return 'LinearRegression'

调用:

'''生成数据'''
np.random.seed(666)

x = 2 * np.random.random(size=100)
# 修改为100行1列的二维数组,也就是100个数据,每个数据1个特征
X = x.reshape(-1,1)
# 生成100个靠近y = 3x + 4 直线的点
y = x * 3. + 4. + np.random.normal(size=100)

# 实例化算法模型
lin = LinearRegression()
lin.fit_gd(X,y)
print(lin.coef_)        # [3.00706277]
print(lin.interception_)        # 4.021457858204859
print(lin._theta)      # [4.02145786 3.00706277]

训练结果大概正确。

4.向量化

我们在计算多元线性回归的的损失函数J对每个theta值的导DJ时略显麻烦,先计算第一个,在循环计算其他得数据项,如果把这个DJ用向量的点乘表示出来,那么就轻松了。

推导:

由于X(b)的第0列的所有数据都是1,所以上式成立。

  1. 观察上式右边的式子,先不看 2/m:
    它是一个n+1 * 1 格式的向量,其每一项都是m个标量求和,那么去掉求和符号的话:所以其每一项必然是一个行向量(1 * m)(点乘)一个列向量(m * 1),并且他们每一个行式子的区别都是右边向量X_b的列数变化
  • 那么我们呢可以将左边看作一个列向量(m * 1),右边看作一个矩阵,该矩阵就是X_b矩阵的转置矩阵(n+1 * m)。

展开来写:

这样计算出来就是DJ了。

修改DJ函数:

def DJ(theta,X_b,y):
    return X_b.T.dot(X_b.dot(theta) - y)* 2. / len(X_b)

使用boston房价进行测试:

from sklearn import datasets
from KNN.knn_iris import train_test_split
from Tidudown.manyTidu.tuduClass import LinearRegression

data = datasets.load_boston()
X = data.data
y = data.target
X = X[y<50]
y = y[y<50]

X_train,y_train,x_test,y_test = train_test_split(X,y)

lr = LinearRegression()

lr.fit_gd(X_train,y_train,eta = 0.000001,n_iters=1e6)
y_perdict = lr.perdict(x_test)
z = lr.r2_score(y_test,y_perdict)
print(z)
# 0.6908063505166206

使用公式推导的方式计算theta训练模型:

# 使用公式推导的方式训练模型
lr.fit_normal(X_train,y_train)
y_perdict = lr.perdict(x_test)
z = lr.r2_score(y_test,y_perdict)
print(z)
# 0.784704968177089

在使用真实的数据,调整eta和n_iters,

  • 要么由于eta太小导致无法得出真实的结果,导致学习不好,正确率低
  • 要么由于eta太大导致训练时间加长,这是由于数据的规模在不同的特征上不同,所以我们需要对数据进行归一化。

5.数据归一化

为了防止数据中某一列数据的值过大导致结果发生偏差,要进行数据归一化操作。

from sklearn import datasets
from KNN.knn_iris import train_test_split
from Tidudown.manyTidu.tuduClass import LinearRegression
from sklearn.preprocessing import StandardScaler

data = datasets.load_boston()
X = data.data
y = data.target
X = X[y<50]
y = y[y<50]

# 分割数据
X_train,y_train,x_test,y_test = train_test_split(X,y)

# 数据归一化
ss = StandardScaler()
ss.fit(X_train)
X_train_standard = ss.transform(X_train)
x_test_standard = ss.transform(x_test)

# 开始学习
lin = LinearRegression()
# 梯度下降
lin.fit_gd(X_train_standard,y_train)
y_perdict = lin.perdict(x_test_standard)
re = lin.r2_score(y_test,y_perdict)
print(re)
# 0.7876525859547368
# 公式推导
lin.fit_normal(X_train,y_train)
y_perdict = lin.perdict(x_test)
z = lin.r2_score(y_test,y_perdict)
print(z)
# 0.7876600935022398

可以看到使用两种方式学习后的正确率基本都差不多了。

6.梯度下降法的优势

自己定义一个训练集,theta向量,以及他们之间的线值y。

m = 1000
n = 5000

big_X = np.random.normal(size=(m, n))
true_theta = np.random.uniform(0.0, 100.0, size=n+1)
big_y = big_X.dot(true_theta[1:]) + true_theta[0] + np.random.normal(0., 10., size=m)

big_reg1 = LinearRegression()
%time big_reg1.fit_normal(big_X, big_y)

# CPU times: user 18.8 s, sys: 899 ms, total: 19.7 s
# Wall time: 10.9 

big_reg2 = LinearRegression()
%time big_reg2.fit_gd(big_X, big_y)

# CPU times: user 9.51 s, sys: 121 ms, total: 9.63 s
# Wall time: 5.76 s

如果样本数非常多,那么即使使用梯度下降法也会导致速度比较慢,因为在梯度下降法中,J的值需要每一个样本都要参与运算。这时候需要采用随机梯度下降法。

你可能感兴趣的:(2020-08-19--梯度下降法01)