大白话 5 分钟带你走进人工智能 - 第十一节梯度下降之手动实现梯度下降和随机梯度下降的代码(6)

第十一节梯度下降之手动实现梯度下降和随机梯度下降的代码(6)

我们回忆一下,之前咱们讲什么了? 梯度下降 ,那么梯度下降是一种什么算法呢?函数最优化算法。那么它做的是给我一个函数,拿到这个函数之后,我可以求这个函数的导函数,或者叫可以求这个函数的梯度。 导函数是一个数儿,梯度是一组数 ,求出来梯度之后怎么用?把你瞎蒙出来的这组θ值,减去α乘以梯度向量,是不是就得到了新的θ,那么往复这么迭代下去的,是不是越来越小, 越来越小,最后达到我们的最优解的数值解? 你拿到数值解之后,我们实际上就得到了我们的一组最好的θ。

回到我们继续学习的场景来说,我们想要找到一组能够使损失函数最小的θ,那么我原来是通过解析解方式能够直接把这θ求出来,但是求的过程太慢了,有可能当你参数太多的时候,所以我们通过梯度下降法可以得到我们损失函数最小那一刻对应的 W 值,也就是完成了一个我们这种参数型模型的训练。其实这个东西虽然咱们只讲了一个线性回归,但是逻辑回归 svm,岭回归,学了之后,你本质就会发现它是不同的损失函数,还是同样使用函数最优化的方法达到最低值,因为都是参数型模型,只要它有损失函数,最终你就能通过梯度下降的方式找到令损失函数最小的一组解,这组解就是你训练完了,想要拿到手,用来预测未来,比如放到拍人更美芯片里面的模型。哪怕深度神经网络也是这样。

在讲梯度下降背后的数学原理之前,我们上午只是从直觉上来讲,梯度为负的时候应该加一点数,梯度为正的时候应该减一点数,而且梯度越大证明我应该越多加一点数,只是这么来解释一下梯度下降,那么它背后实际上是有它的理论所在,为什么要直接把梯度拿过来直接乘上一个数,就能达到一个比较快的收敛的这么一个结果,它有它的理论所在的。

在讲这个之前我们还是先来到代码,我们手工的实现一个 batch_gradient_descent。批量梯度下降,还记得批量梯度下降和 Stochastic_ gradient_descent 什么关系吗?一个是随机梯度下降,一个是批量梯度下降, 那么随机跟批量差在哪了?就是计算负梯度的时候,按理说应该用到所有数据, 通过所有的数据各自算出一个结果,然后求平均值. 现在咱们改成了直接抽选一条数据,算出结果就直接当做负梯度来用了,, 这样会更快一点,这是一个妥协。理论向实际的妥协,那么我们先看看实现批量梯度下降来解决。

import numpy as np
#固定随机种子
np.random.seed(1)
#创建模拟训练集
X = 2 * np.random.rand(10000, 1)
y = 4 + 3 * X + np.random.randn(10000, 1)
X_b = np.c_[np.ones((10000, 1)), X]
# print(X_b)

learning_rate = 0.1
n_iterations = 500
#有100条样本
m = 10000

#初始化θ
theta = np.random.randn(2, 1)
count = 0


for iteration in range(n_iterations):
    count += 1
    gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
    theta = theta - learning_rate * gradients

print(count)
print(theta)

X = 2 * np.random.rand(10000, 1)
y = 4 + 3 * X + np.random.randn(10000, 1)

上这两行代码仍然是生成一百个 X,模拟出对应了一百个 Y,这个代码里边我们不掉现成的包,不像在这用了一个 sklearn 了,我们不用 sklearn 的话,还有什么包帮你好心的生成出一个截距来,是不是没有了?所以我们是不是还是要手工的,在 X 这里面拼出一个全为 1 的向量作为 X0,现在 X_b 是一个什么形状呢?100 行 2 列,第一列是什么?是不是全是 1,第二列是什么?随机生成的数。我们解释下上面代码:


learning_rate = 0.1
n_iterations = 500

我们做梯度下降的时候,是不是有一个α?我们命名它为学习率,拿一个变量给它接住,learning_rate=0.1。那么 iterations 什么意思? 迭代 是吧,n_iterations 就是说我最大迭代次数是 1 万次,通常这个超参数也是有的,因为在你如果万一你的学习率设的不好,这个程序是不是就变成死循环了?它一直在走,如果你不设一个终止条件的话,你这个东西一训练你有可能就再也停不下来了。所以通常都会有一个最大迭代次数的。当走了 1 万次之后没收敛,也给我停在这,最后给我报个警告说并没有收敛,说明你这个东西有点问题。你应该增加点最大次数,或者你调一调你的学习率是不是太小了或者太大了,导致它还没走到或者说走过了震荡震荡出去了,你需要再去调整。M 是这个数据的数量,这一会再说它是干嘛的。


theta = np.random.randn(2, 1)
count = 0

接下来θ,我们上来梯度下降,是不是要先蒙出两个θ来,于是我生成两个随机数,np.random.randn(2, 1) 这个的意思是生成一个两行一列的 -1 到 +1 之间的正态分布的随机数。接下来我就进入 for 循环:


for iteration in range(n_iterations):
count += 1
gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
theta = theta - learning_rate * gradients

在 python2 中,如果你输入 range(10000),会得到一个列表,从 0 一直到 9999,就实实在在的是一个列表,你把这列表搁进去,进行 for 循环,会得到什么?第一次循环的时候,这次此时的 iteration 等于 0,第二次等于 1,因为列表中的每一个元素要赋值到这个变量里面去。那么在 python3 里面 range 就不再直接给你实实在在的生成一个列表了,而生成一个 python 里面独一无二的类型叫 Generator,生成器。生成器是一个什么?你只是想借用这个东西去循环意思,循环一次,你有必要先放一个列表,在那占你的内存空间,没有必要?实际上它是一个懒加载的这么一个东西,你每次迭代第一次返回 0 第二次返回 1,每次迭代就给你返回一个数,生成器本质它就变成一个函数了,你第一次调用它返回零,第二次掉他返回一,第三次调用它返回二,它里边记录了自己当前到哪了,并且生成规则,这样就没有必要去占用你的内存空间了,这个是 range 通常是用来做 for 循环用的,就为了有一个序号。 还有另一种高级的用法是 enumerate,假如你用一个 enumerate(list),它会给你返回两个数,第一个是索引号,第二个是 list 中的本身的元素。 在这用逗号可以把这两个变量分别复制给你指定的两个变量名。


for i,a in enumerate(list):
print(i,a)
  

那么 i 在此时实际上就是 li1 里面的第一个元素的索引号是零,第二个元素就是本身是什么什么,这个是一个很方便的技巧,能够帮你在 for 循环体内部既需要索引号来计算它的位置,又需要这个数据本身的时候可以直接用 enumerate 一次性的把它取出来,很简单的一个技巧。


gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
theta = theta - learning_rate * gradients

那么在这我们只需要 n_iterations 给他做个计数器就好了,所以我在这儿只用它。那么我们看这 count 默认是零,他是一个计数器,每次循环会自己加 1,+= 大家都应该能看懂,自己自加 1,那么此时的 gradients 实际上就是 X_b 的转置。乘以它,再乘以 1/m。gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)。这一步里面有没有加和? 是批量梯度下降带加和的版本,还是随机梯度下降,不带加和的版本? 因为 X_B 是个矩阵,X_b^{\mathrm{T}} \cdot(X_b \bullet \theta-\mathrm{Y})实际上你看 X_b 是不是一个矩阵,Y 是一个向量,我们需要看 Y 是个行向量还是列向量, X 是一百行一列的,Y 是一百行一列的,它是一个列向量。然后用最开始的 X 转置去乘以列向量,实际上矩阵乘以一个向量的时候,本身就拿第一行乘以第一列加第二行乘以第一列,本身就把加和融在矩阵乘法里面去了。所以实际上 1/m * X_b.T.dot(X_b.dot(theta)-y) 这个东西本身就已经是带加和的了,而且如前面所说是不是一定要加一个 M 分之一?接下来实现梯度下降是不是非常简单,拿上一代的θ减去 learnrate,乘以你计算出来的梯度就完事了! 也就是


theta = theta - learning_rate * gradients

我们看梯度下降,虽然讲了半天感觉很复杂,其实四行代码结束了,那么在执行完了这 1 万次迭代之后,我们把θ给打印出来,看看结果,我没有实现那个 tolerance,没有判定。假如这样我每一步都让它打印θ。

for iteration in range(n_iterations):
    count += 1
    gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
    theta = theta - learning_rate * gradients
    if count+==0:
        print(theta)

我们看看θ随着更新,它很早你发现是不是都已经收敛了其实?你看她通过多少次收敛的,上来是 3.27,3.52,下次慢慢在变化变化,每一步都走的还比较稳健,到 4.00,4.04,

20190318203941223.png
20190406171856702.png

是不是越走越慢了,你发现。你看第一部时候从 3.27 到 3.51,到后来 4.04,4.07 是不是越走越慢了?4.10,4.12,为什么会越走越慢,学过梯度下降,你们现在是不是应该知道了?因为 越接近谷底,它的梯度值怎么样?越大还是越小? 越小,它自然就越走越慢,越小走的越慢。 那么最后到 4.18,收敛了再也不动,但是我们循环是不是还在往下一直继续,只不过每次加的梯度都是怎么样?零。

再有一个问题,刚才我的数据集里面并没有做归一化,那么实际上它需要做最大最小值归一化吗?本质上不太需要,因为它只有一个 W,只有一个 W 的时候自然做归一化是无所谓的,如果你有多个 W 的情况下,你对每一个 X 在预先处理之前做都需要做归一化,其实也是两行代码的事。我们加入最大最小值归一化的方式:


X_b[:,1]=X_b[:,1]-X_b[:,1].mean()

X_b[:,1] 这个冒号什么意思?这个冒号就是一个索引方式,我要取所有的行,我就打一个冒号:,你取所有的行的第一列,就写个 1,X_b[:,1] 其实是一个一维数组,就是那一堆一百个随机数,那么 X_b[:,1].mean()加一个.mean,你可以看到它的平均是多少?0.95。 如果你用 X_b[:,1]=X_b[:,1]-X_b[:,1].mean(),会自动的帮我们对位相减,每一个数都减减去它。然后我们此时在看我们剪完了之后的结果,是不是就变成有正有负的了?

20190318203941223.png

此时在做梯度下降,按理说速度应该更快一些,我们看刚才我们迭代次数是多少,取决于你初始化的点,如果你初始化的点好的话是没有区别的,如果初始化的点不好的话就有区别。我们运行一下,你看一下,我期待它会有变化。需要多少次?331. 刚才有多少次?500 多次,现在只需要 300 多次,就证明刚才随机那个点,实际上对于能不能正负一起优化,是不是有实际的敏感度的,你现在做了一个均值归一化,实际上发生了什么问题? 迭代次数变得更好了,更少了,就更快地达到了收敛的值。但是你发现它 W1 和 W0 也变了,

20190318203941223.png

肯定会变,因为你的整个数值全都变了,但变了没关系。怎么样才能继续用起来?你这变过之后的 W,对你新预测的数据拿回来之后先做同样的归一化,你再去预测结果也是正确的。

有了批量梯度下降的代码,我们再看随机梯度下降的话,就简单多了。先上代码:

import numpy as np
from sklearn.linear_model import SGDRegressor

#固定随机种子
np.random.seed(1)
#生成训练集
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
X_b = np.c_[np.ones((100, 1)), X]
print(X_b)

n_epochs = 1000#迭代次数
t0, t1 = 5, 50

m = 100

#设置可变学习率
def learning_schedule(t):
    return t0 / (t + t1)

#初始化θ
theta = np.random.randn(2, 1)


#随机梯度下降
learning_rate = 0.1
for epoch in range(n_epochs):
    for i in range(m):
        random_index = np.random.randint(m)
        # 为了解决维度问题,在索引屈指
        xi = X_b[random_index:random_index+1]
        yi = y[random_index:random_index+1]
        gradients = 2*xi.T.dot(xi.dot(theta)-yi)
        learning_rate = learning_schedule(epoch*m + i)#随着迭代次数增加,学习率逐渐减小。
        theta = theta - learning_rate * gradients

print(theta)

SGDRegressor

解释下:np.random.seed(1),确定随机种子,这是不是万年不变的老三样,没有什么变化,然后我们还是 n_iterations,总共迭代一千次。n_epochs = 1000。为什么是双重 for 循环?我细致的给大家说,首先我是不是随机梯度下降,我要有一个随机,我要随机选出一条数据来,我在这一部分


 random_index = np.random.randint(m)
 xi = X_b[random_index:random_index+1]
 yi = y[random_index:random_index+1]

都是在随机的选 X 和 Y,randint(m)代表从 0 到 99 随机选出一个数字来作为 index,作为索引,选出来之后,我的 X 是不是要从 X 里边把这个随机位的索引给取出来,所以我取出来 X 的索引。那么 Y 是不是为了随机取出来索引,这两个 xi = X_b[random_index:random_index+1], yi = y[random_index:random_index+1] 就是把对应的那一条 X 和对应的 Y 给搞出来。那么梯度就通过那一个 X 乘以了一个 Y, gradients = 2*xi.T.dot(xi.dot(theta)-yi) 得到了单个计算出来的梯度,你说这个表达式怎么没变,表达式是不用变,只不过原来的 X 矩阵是一百行两列,现在 X 矩阵变成一行两列,你表达式是不用变的,一行自动指出来一个数就不再是一个向量了,那么此时用 learning_rate,我原来是不是定死了就是 0.1,而现在我的 learning_rate 变成了 learning_schedule 返回的一个结果,我看我定义的 learning_schedule 是什么?


def learning_schedule(t):
return t0 / (t + t1)

定义了两个超参数,分别是 t0 和 t1, 你丢进一个 t 来,你看 t 越大,return 这个值会怎么样? 越小,那么我们看 t 总共能到多少?是不是 epoch*m+i, epoch 从哪来的?是不是从 n_epochs 来的,也就是上来循环第一次的时候它是多少?0, 此时你看 return 结果这算算是多少,是不是就是零?那么你看此时的 t 是零,此时的 learning_schedule 返回的是一个多大的数,是不是 0.1?也就是第一次执行的时候学习率是多少?0.1。当我内层循环第 101 次执行的时候此时 epoch 等于多少? 等于 1。epoch*m+i 越来越大,那么此时的学习率 learning_schedule 是上升了还是下降了?变大了还是变小了? 变小了一点,也就是说随着迭代的加深,epoch 是不是越来越大?传到这里边数也越来越大,学习率是越来越小的,所以这个也是梯度下降的一种变种。它会把学习率随着迭代的次数推进,让学习率越来越小,这样保证你就可以设置一个初始的时候比较大的学习率,这样你学习率万一设大了,它也不会一直震荡越远,因为随着迭代越多,梯度越来越小。在我们 sklearn 里面的 SGDRegressor 函数是有相关的超参数可以设置的。

    def __init__(self, loss="squared_loss", penalty="l2", alpha=0.0001,
                 l1_ratio=0.15, fit_intercept=True, max_iter=None, tol=None,
                 shuffle=True, verbose=0, epsilon=DEFAULT_EPSILON,
                 random_state=None, learning_rate="invscaling", eta0=0.01,
                 power_t=0.25, warm_start=False, average=False, n_iter=None):
        super(SGDRegressor, self).__init__(loss=loss, penalty=penalty,
                                           alpha=alpha, l1_ratio=l1_ratio,
                                           fit_intercept=fit_intercept,
                                           max_iter=max_iter, tol=tol,
                                           shuffle=shuffle,
                                           verbose=verbose,
                                           epsilon=epsilon,
                                           random_state=random_state,
                                           learning_rate=learning_rate,
                                           eta0=eta0, power_t=power_t,
                                           warm_start=warm_start,
                                           average=average, n_iter=n_iter)

loss="squared_loss",这个是什么? 就是说 SGDRegressor 是一个通用的机器学习的方式,你可以自己告诉他我的损失函数是什么,你甭管损失函数是什么,我给你通过 SG 的方向一直下降得到一个结果,你要把 MSE 传给我,你得到就是一个线性回归的结果,你要把一个 mse 加 L2 正则的损失函数给我,我得到的就是一个岭回归的结果,就损失函数不同,你的算法其实就改变了,那么在 SGD 它是不捆绑算法本身的,你给我什么损失函数执行出来什么样,我就是一个帮你下降做计算题的机器。那么默认的就是 squared_loss,alpha 实际上是指的 l1 跟 l2 中间的一个超参数,l1_ratio 也一样,这两个超参数是依附在 penalty 之上的一个超参数,咱们讲完了正则化之后,你就明它什么意思了。

fit_intercept=True,这个什么意思,截距是不是要帮你搞出来。max_iter=None,什么意思?最大迭代次数,它没设。tol 是什么意思,收敛次数。shuffle=True,shuffle 什么意思?shuffle 实际上把数据乱序。你上来不是给了我一堆 X 吗?我帮你先洗个牌乱一下序再进行训练,对于咱们这种算法来说是否乱序不影响最终计算结果。random_state=None 就是随机种子.learning_rate="invscaling", learning_rate 是学习率,那么看 learning_rate 都有哪些可以选择的地方?


learning_rate : string, optional
The learning rate schedule:
- 'constant': eta = eta0
- 'optimal':  eta = 1.0 / (alpha * (t + t0)) [default]
- 'invscaling': eta = eta0 / pow(t, power_t)

constant 什么意思?常数,那么此时的 eta 学习率通常也用 eta 表示就等于 eta0,后边是不是还有一个 eta0,如果你在这写一个 learning_rate=constant,后边 eta0 赋一个值,实际上就是一个定值的学习率。然后它有 两种变种的,一个叫 optimal ,优化的,用 1.0/(alpha*(t+t0))。另外一个是什么?T 的 power T 次方,就是 T 的 N 次方,这种方式叫 invscaling 。它们都是差不多,它们都是想让这学习率越往下走越小,这么一种可变学习率的调整方式,那么默认是使用 invscaling 倒置缩放的这种方式来做的,也就是说实际上我们现在就了解到,在 sklearn 中并没有使用这种定值的学习率,默认的实现里面,并不是使用固定的学习率来做梯度下降的,而是使用这种可变学习率来做的。

聊了这么多梯度下降的逻辑和过程,有没有对其底层原理感兴趣,所以下一节我们将讲解梯度下降的底层原理。