- 梯度下降法简介
- 多元线性回归中的梯度下降法
- 随机梯度下降法
- 梯度下降法 的调试
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
分析:
- 首先定义超参数n和最小差距,然后定义两个函数J和DJ,计算某一点的J值,以及计算某一点J对theta的导。
- 设置超参数--初始点的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)
分析:
- 首先创建一个与theta向量等长的数组res,用于填充数据。这个原因上边说过。计算第一部分数据的值--计算括号内数据直接求和即可,赋给res[0]。
- 其他数据项使用for循环循环计算,为了便于计算,我们把它换位向量的点乘。
首先分析这其中一项:
-
该数据项是m个数据值之和,他的每一项数据值都是标量*标量,例如第二行分析:
所以就可以将公式看作(以第二行数据为例):
那么用就可以在代码用for循环给每一项赋值。
- 最后返回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,所以上式成立。
- 观察上式右边的式子,先不看 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的值需要每一个样本都要参与运算。这时候需要采用随机梯度下降法。