1. 引言
线性回归是机器学习的基础,在日常使用中,我们经常调用函数来求解参数,这样虽然方便,但在学习初期不能加深对模型及代码的理解。本文则讲述了梯度下降法(包括Adagrad法)求解线性回归的理论以及如何用Python实现,最后应用到 diabetes 数据集,并将结果与 sklearn 中的API进行对比。
2. 理论基础
2.1 最小二乘法
线性回归模型可表示为:
(严格地讲,这里的$w$应该用$\hat{w}$)
其中
是
维列向量。
是
维的列向量,之所以多了一个维度,是因为我们把常数项参数
也放在了
里。即
。相应的,
也多了一个维度,该矩阵的第一列全为1。如果无法理解这种形式的表达,可以将矩阵在草稿纸上写出来,就看得明白了,这样表达能方便后续求解。
求解线性回归最简单的算法是最小二乘法,即找到一组参数,使模型的损失函数(残差平方和)最小化,用数学模型表示就是:
写成矩阵的形式能够方便求梯度,可能下面这种形式是大家更熟悉的最小二乘法形式:
其中
是第
个个体的自变量向量,
是向量的内积。这种形式是大家更为熟悉的最小二乘法形式。
2.2 梯度
上文提到,最小二乘法的损失函数为:
这是一个标量,而
是一个向量,标量对向量求导,其实就是该标量对向量每一个元素求导,再组合成列向量的形式。用数学表达就是:
如何求解
呢?将损失函数
展开:
利用矩阵求导法则:
此时,如果我们令
就可以求得
的解析解,即:
但是在梯度下降法中,我们当然不能直接带这个公式算,因为这个公式只对线性回归模型成立,对其他模型不成立,而梯度下降则是对各种模型都能使用。下面讲述梯度下降法的原理。
2.3 梯度下降
什么是梯度?从几何上解释,沿着梯度方向,函数上升的最快。可以类比二次函数的导数,二次函数在某一点上升最快的方向就是那一点切线的方向。这段话另一面就是说逆着梯度方向,函数下降的最快。梯度下降法的逻辑就是:我任意给定一个初始参数向量
,算出损失函数值,这个值肯定不是最小的,但是如果我算出
这一点的梯度
,那么我反着这个梯度走,就可以减小损失函数值,如果我不停重复这个过程,就能达到一个最小值。
这一段可以结合台湾大学李宏毅老师的机器学习课程理解。也可以参考这个博文。 用数学过程表示为:
其中
是学习率,该值是手动设置的参数,如果过大,可能会学习过快错过最低点,如果过小,会使学习速度太慢。实际操作中,当梯度等于零的时候循环结束不太现实,因为不可能达到零,因此更多地是大量地迭代,去逼近解析值。
这就是最经典的梯度下降法,可以看到梯度下降法的一大缺点就是固定的学习率,但一个常识是:通常是随着参数的更新越来越小的。这很容易理解,学习越好的同学越可以少努力学习,学习越差的同学则应该更努力学习。有没有什么办法能让我们随着迭代更新
呢?接下来我们介绍Adagrad。
2.4 Adagrad
Adagrad的过程表示如下:
其中
是参数向量,
表示第$t$次更新参数,
。
是之前所有梯度均方根。
表示损失函数对
的偏微分,即梯度
。
需要注意,由于
是一个向量,式子中
或者
都是对每向量每一个元素进行计算。
与
都有
,因此可以约分,化解后的更新过程为:
在计算机编程中,为了防止出现分母为零的情况,更新过程下的分母一般会加上一个非常小的值
,即:
3. 代码实现
本部分展示梯度下降法求解多元线性回归的Python代码,结合sklearn中的diabetes数据集为例进行简单的训练,同时与用sklearn提供的linear_model得到的结果进行对比。
import numpy as np
import pandas as pd
def grad_desc(X, y, learning_rate=0.01, epochs=1000, eps=0.0000000001, measure='gd'):
'''This funchtion is used to calculate parameters of linear regression by gradient descend method\n:param X: sample matrix, shape(n_samples, n_features)\n:param y: matrix-like, shape (n_samples, 1)\n:param learning_rate: learning rate, 0.01 by default\n:param epochs: times of iteration, 1000 by default\n:param eps: the small positive number used in Adagrad prevent zero denominator\n:param return: the parameters of linear regression\n:param gd_type: measures to do gradient descend, can choose 'gd' or 'Adagrad'\n'''
n = X.shape[0] # 样本数量
dim = X.shape[1] + 1 # 特征数量,+1是因为有常数项
x = np.concatenate((np.ones([n, 1]), X), axis = 1).astype(float)
# 同样由于有常数项,x矩阵需要多加一列1
y = np.matrix(y).reshape(-1, 1).astype(float) # y转化为列向量,方便矩阵运算
w = np.zeros([dim, 1]) # 初始化参数
## 常规的梯度下降法
if measure == 'gd':
for i in range(epochs):
loss = np.sum(np.power(np.dot(x, w) - y, 2))/n
if (i % 100 == 0):
print(str(i) + ":" + str(loss))
gradient = 2 * np.dot(x.transpose(), np.dot(x, w) - y)/n
w = w - learning_rate * gradient
## Adagrad法
if measure == 'Adagrad':
adagrad = np.zeros([dim, 1])
for i in range(epochs):
loss = np.sum(np.power(np.dot(x, w) - y, 2))/n
if (i % 100 == 0):
print(str(i) + ":" + str(loss))
gradient = 2 * np.dot(x.transpose(), np.dot(x, w) - y)/n
adagrad += np.square(gradient)
w = w - learning_rate * gradient / np.sqrt(adagrad + eps)
return w
def predict(w, test_X, test_y):
'''This function is use to calculate the MSE of a given linear regression model\n:param w: matrix-like, shape (n_features+1, 1)\n:test_X: test sample matrix, shape(n_samples, n_features)\n:test_y: true targets of test set, matrix-like, shape (n_samples, 1)\n'''
test_X = np.concatenate((np.ones([test_X.shape[0], 1]), test_X), axis = 1).astype(float)
test_y = np.matrix(test_y).reshape(-1, 1).astype(float)
predict_y = np.dot(test_X, w)
mse = np.sqrt(np.average(np.square(predict_y - test_y)))
return mse, predict_y
if __name__ == '__main__':
# 导入deabetes数据集
from sklearn.datasets import load_diabetes
from sklearn.utils import shuffle
diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target
# 划分训练集和测试集
offset = int(X.shape[0] * 0.9)
X_train, y_train = X[:offset], y[:offset]
X_test, y_test = X[offset:], y[offset:]
y_train = y_train.reshape((-1,1))
y_test = y_test.reshape((-1,1))
print('X_train=', X_train.shape)
print('X_test=', X_test.shape)
print('y_train=', y_train.shape)
print('y_test=', y_test.shape)
# 使用grad_desc()函数求解
w = grad_desc(X_train, y_train, learning_rate=0.03,epochs=10000, measure='gd')
print('使用grad_desc()函数的MSE:', predict(w, X_test, y_test)[0])
# 利用sklearn的线性回归api来做对比
import sklearn.linear_model as sl
import sklearn.metrics as sm
model = sl.LinearRegression()
model.fit(X_train, y_train)
pred_y = model.predict(X_test)
print('使用sklearn线性模型函数的MSE:', sm.mean_absolute_error(y_test, pred_y))
最终得到结果:
使用grad_desc()函数的MSE:41.768875313581226
使用sklearn线性模型函数的MSE: 32.085174947765125
可以看到,我们手动实现的梯度下降法表现明显不如api提供的好,这也是很正常的事情,因为本文所采用的梯度下降法是最简单的两种:普通的GD法和Adagrad。而梯度下降的更新方法有许多提高措施,如随机梯度下降法(SGD),使用动量的随机梯度下降法,RMSProp法等,详见博文梯度下降。有兴趣读者可在本文基础上加上这些提高措施,改善性能。
4. 总结
本文主要讲解了梯度下降法求解线性回归模型的理论与Python代码实现,最终效果一般,但是梯度下降的思想和方法是最重要的,因为深度学习的基础就是梯度下降。
作为一个金融科班出身的学生,我深知光学金融学或者经济学的知识是远远不够的,现在各个学科都在鼓励交叉,最近几年金融科技也逐渐收到社会的关注,因此我在硕士课程之余经常自学机器学习、深度学习的内容。一开始是只企图掌握API的使用(即掉包、调参侠),但我逐渐发现,许多有创造的论文都是对经典的模型有一些改动,加上在金融领域的应用,才能成为优秀的论文。因此我又从头开始,试图能从源码上理解基础模型,既能加深对模型的印象,也会对未来创作有帮助。
希望这篇博文能帮助到,欢迎对深度学习、机器学习感兴趣的金融、经济学领域同学与我交(P)流(Y)。