第十一节梯度下降之手动实现梯度下降和随机梯度下降的代码 (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 是不是一个矩阵, 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%20==0:
- print(theta)
我们看看θ随着更新, 它很早你发现是不是都已经收敛了其实? 你看她通过多少次收敛的, 上来是 3.27,3.52, 下次慢慢在变化变化, 每一步都走的还比较稳健, 到 4.00,4.04,
是不是越走越慢了, 你发现. 你看第一部时候从 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(), 会自动的帮我们对位相减, 每一个数都减减去它. 然后我们此时在看我们剪完了之后的结果, 是不是就变成有正有负的了?
此时在做梯度下降, 按理说速度应该更快一些, 我们看刚才我们迭代次数是多少, 取决于你初始化的点, 如果你初始化的点好的话是没有区别的, 如果初始化的点不好的话就有区别. 我们运行一下, 你看一下, 我期待它会有变化. 需要多少次? 331. 刚才有多少次? 500 多次, 现在只需要 300 多次, 就证明刚才随机那个点, 实际上对于能不能正负一起优化, 是不是有实际的敏感度的, 你现在做了一个均值归一化, 实际上发生了什么问题? 迭代次数变得更好了, 更少了, 就更快地达到了收敛的值. 但是你发现它 W1 和 W0 也变了,
肯定会变, 因为你的整个数值全都变了, 但变了没关系. 怎么样才能继续用起来? 你这变过之后的 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 都有哪些可以选择的地方?
constant 什么意思? 常数, 那么此时的 eta 学习率通常也用 eta 表示就等于 eta0, 后边是不是还有一个 eta0, 如果你在这写一个 learning_rate=constant, 后边 eta0 赋一个值, 实际上就是一个定值的学习率. 然后它有两种变种的, 一个叫 optimal, 优化的, 用 1.0/(alpha*(t+t0)). 另外一个是什么? T 的 power T 次方, 就是 T 的 N 次方, 这种方式叫 invscaling. 它们都是差不多, 它们都是想让这学习率越往下走越小, 这么一种可变学习率的调整方式, 那么默认是使用 invscaling 倒置缩放的这种方式来做的, 也就是说实际上我们现在就了解到, 在 sklearn 中并没有使用这种定值的学习率, 默认的实现里面, 并不是使用固定的学习率来做梯度下降的, 而是使用这种可变学习率来做的.
聊了这么多梯度下降的逻辑和过程, 有没有对其底层原理感兴趣, 所以下一节我们将讲解梯度下降的底层原理.
来源: https://www.cnblogs.com/LHWorldBlog/p/10709813.html