写在之前
这是本人第一次编写博客,技术不是很娴熟,若造成阅读困扰十分抱歉。其次,编写博客的内容是基于研究生第一年所学课程作业而言的,部分时间已经比较久远,引用和转载上可能找不到当初借鉴的原文了,十分抱歉!编写博客的目的一方面是对自己所学有个总结和归处,另一方面在完成某些作业时受很多博客的影响,向他们学习、致敬!最后就是代码的正确性在当时是可以得到保证的,但不一定适用于所有场景,且效率上可能存在一定问题,因此仅供参考,谢谢!
实验目的
有一组数据记录了学生复习小时数h和通过考试与否Y,如图1所示。
现欲找出通过考试的概率与复习时长之间的关系,即用Logistic函数(sigmod函数)模拟p(x),其中
Logistic函数如图2所示,其函数表示见公式(1),构造函数p(x)如公式(2)所示,实验目的转化为通过图1中的数据学习出参数θ0,θ1,得到最终的h与Y的关系函数Y = f (h)。
实验步骤
求公式(2)中的参数θ,在数理统计里问题转化为给定样本,对参数进行估计,常用方法为极大似然估计。按以下步骤构造似然函数:
对似然函数L(θ)取自然对数并化简得到公式(5)。
现在问题转化为求公式(5)的最大值,即最优化问题,如公式(6)所示。
该问题的等价优化问题如公式(7)所示。
终于,将最开始的回归问题转换成了求目标函数(公式(7))的最小值问题,可以用到我们优化理论与方法所学的梯度下降算法。让公式(7)分别对θ0,θ1求偏导,得到公式(8)(9),整个似然函数的梯度如公式(10)所示。
基本实现原理
梯度下降的思想是其基本思想是从某一个初始点x0出发,每次沿着函数值下降最快(即负梯度方向)的方向前进一小步,随着执行次数不断增加,xn 不断逼近局部极小点x*,如图3所示。
此图里的n有个小问题,图中算法步骤的第3步的Σ符号上的的n代表数据集的记录数,其他的n代表迭代次数。另外值得注意的是,此时的循环终止条件用的是梯度的二范数小于给定阈值ε。
实现代码
代码如下,也可观看我GitHub。
# -*- coding: utf-8 -*-
"""
Created on Sat Oct 27 12:09:23 2018
@author: YLC
"""
import numpy as np #用于矩阵运算
import matplotlib.pyplot as plt #用于画图
import math #用于指数运算
h = np.array([0.5,0.75,1,1.25,1.5,1.75,1.75,2,2.25,2.5,2.75,3,3.25,3.5,4,4.25,4.5,4.75,5,5.50]) #学习小时数
Y = np.array([0,0,0,0,0,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1]) #是否通过标记
matrix = np.vstack((h,Y)) #原始数据
print("原始数据如下:")
for i in range(0,len(h)):
print("h="+str(matrix[0][i])+"\t p="+str(matrix[1][i]))
n = 0 #迭代次数
arr_len = len(h) #数据量大小
theta0 = 0.5 #第一个要学习的参数
theta1 = 0.5 #第二个要学习的参数
theta = np.array([[theta0],[theta1]])#将两个参数放到参数矩阵,不保留参数的历史记录,只存当前参数值
epsilon = 0.001 # 定义阈值
print("学习前,初始参数:theta0="+str(theta[0])+", theta1="+str(theta[1]))
def p(x,theta): #定义Logistic回归函数
return 1/(1+math.exp(-theta[0]-theta[1]*x))
def grad(x,y,theta): #定义梯度
sum1 = sum2 = 0
for i in range(0,arr_len):
sum1 = sum1 + y[i]-p(x[i],theta) #相当于theta0参数沿当前梯度的变化量
sum2 = sum2 + (y[i]-p(x[i],theta))*x[i] #相当于theta1参数沿当前梯度的变化量
return np.array([[sum1],[sum2]])
#梯度的二范数大于或等于阈值或小于迭代次数则进入循环,小于跳出循环
while (np.linalg.norm(grad(h,Y,theta),ord=2)>=epsilon):#梯度的范数可理解为二维中y=kx+b的斜率,高维中的下降最快的坡度
if(n >= 1000) : break #迭代超过阈值
alpha = 0.08 #初始步长通常在0.01~0.3之间取值
thetaT = theta #临时存放参数
n = n + 1 #迭代次数+1
theta = thetaT + alpha * grad(h,Y,theta) #更新参数
print("误差(梯度的二范数)为:",np.linalg.norm(grad(h,Y,theta),ord=2))
#print("第"+str(n)+"次迭代,梯度的二范数为:"+str(np.linalg.norm(grad(h,Y,theta),ord=2)))
print("迭代"+str(n)+"次后结束,学习后,theta0="+str(theta[0])+",theta1="+str(theta[1]))
print("模型为:y = 1/(1+exp("+str(float(-theta[0]))+str(float(-theta[1]))+"*x))")
pp = np.array([])
for i in range(0,len(h)):
pp = np.append(pp,p(h[i],theta))
pmatrix = np.vstack((h,pp))
print("将学习小时数进行输入,获得课程通过概率为:")
for i in range(0,len(h)):
print("h="+str(pmatrix[0][i])+"\t p="+str(pmatrix[1][i]))
plt.scatter(h,pp,c = 'r',marker = 'o')
print("\n其散点图为:")
plt.show()
''' 整个模型的图像 '''
print("整个模型的图像")
x=np.linspace(0,5,1000) #这个表示在-5到5之间生成1000个x值
y=[1/(1+np.exp(-theta[0]-theta[1]*i)) for i in x] #对上述生成的1000个数循环用sigmoid公式求对应的y
plt.plot(x,y) #用上述生成的1000个xy值对生成1000个点
plt.show() #绘制图像
在具体实现时,循环条件用的时梯度的二范数与给定最大迭代次数同用,满足其一跳出循环。
代码的主要输出结果如下:
学习前,初始参数:theta0=[0.5], theta1=[0.5]
迭代252次后结束,学习后,theta0=[-4.07451178],theta1=[1.50356165]
模型为:y = 1/(1+exp(4.0745117791005-1.503561654356568*x))。
将原始数据的学习时长h代入模型里,得到散点图如图6所示,将最终模型的图像全部画出来如图7。
发现与收获
做完整个实验后,实验的前半部分与所学的工程数学里数理统计的参数估计相应印证,实验的后半部分让我对梯度下降的原理有了更深的掌握。然而,优化理论博大精深,还是有许多一时难以消化的地方。
起初的难点在于理解循环终止条件,特别是梯度的二范数作为终止条件的具体含义不能有深刻的理解,后来想到其可理解为二次平面中的斜率,或者三维图像里的坡度,沿着最陡的地方走,走着走着变平缓了,说明就找到了一个局部极小值,如果是凸函数,那就找到了最小值。
实际上后面考虑过自适应步长,使用回溯线搜索时,一直无法收敛,这让我对调参产生了苦恼,应该有可以机器调参的方法,只是我还没学到,就没在代码上放出来,省得误导别人。在设置固定步长时,考虑迭代次数最短,改变固定步长,然后就随缘调到了0.08,多加一点,少加一点都不合适,是粗鲁的最优了。