原理

梯度下降是一种在机器学习和深度学习中广泛使用的优化方法,常用于回归和分类问题中。在函数表示的曲线上的一点,其梯度方向表示函数值上升最快的方向,由于在机器学习中的梯度是损失函数的梯度,因此我们想要损失函数最小,就要将参数往负梯度方向进行调整。
以一元线性回归为例,我们的数据是由
y= w_ref × x+b_ref
再加噪声生成,在这里我们假设w_ref=3,b_ref=2,因此原函数就是
y= 3x+2
我们在区间x∈[start,end]中等间距取n个点,并生成y值,然后再在此基础上加上e∈[-q,q]的随机浮点数噪声,最终的
y_ref = y+e。

在得到原始数据以后,就可以利用梯度下降算法,回归原来的直线了。梯度下降算法首先需要有数据,这里我们采用SGD方法求的梯度,就是每次都用一个样本。
设模型参数y = wx+b.这里的w和b就是待回归的参数,这里我们随机赋初始值令w=-2,b=4。首先获得在此模型参数下得到的y值
y = w × x+b
然后得到损失函数
function_error = (y-y_ref)^2
再根据损失函数计算梯度
Δw = x×(y-y_ref)
Δb = (y-y_ref)
最后将梯度乘系数加到当前参数上得到一次优化后的参数
w = w - α_w×Δw
b = b - α_b×Δb
到达设定的终止条件后停止训练。

代码

代码使用C++编写,将计算过程的各个部分封装成不同的函数,依次调用执行,有利于阅读和调试,并他们作为求解一元线性规划问题类的成员函数,进行调用。下面是类中使用的函数:

double w=-2.0;//初始参数
double b = 4;
double val(double x);//求模型输出y
vector<vector<double>> rand_val(double w_ref,double b_ref,double start,double end,int n);//生成样本集
vector<double> grad(double y,double y_ref,double x);//计算梯度
void train(vector<vector<double>> data,double alpha_w,double alpha_b);//训练函数

1、生成样本集

使用c++模板类vector构建二维数组,二维数组中每个一维数组是一个样本,其第一个元素是对应x坐标,第二个元素是对应的y_ref。x的取值范围是[start,end],总的采样个数是n,对应采样间隔是nuit_x。随机数据的取值范围为[-4,4]之间的浮点数。

vector<vector<double>> rand_val(double w_ref,double b_ref,double start,double end,int n){
    double e = 0.0;
    double unit_x = (end-start)/n;
    vector<vector<double>> val(n);
    while(n--){
        e = -4 + (double)(rand()) /RAND_MAX * (4 - (-4));
        val[n].push_back(start+n*unit_x);
        val[n].push_back(w_ref*val[n][0]+b_ref+e);
        //cout<<"x"<<val[n][0]<<"y_ref"<<val[n][1]<<endl;
    }
    return val;
}

2、计算梯度

梯度也是以数组的形式返回,第一个元素是关于w的导数,第二个元素是关于b的导数。这里函数直接返回的损失函数的梯度,没有显式得写出损失得表达。但是在计算梯度的时候同样需要当前模型输出y,因此首先要求解y。

//计算y
double val(double x){
    return w*x+b;
}
//计算梯度
vector<double> grad(double y,double y_ref,double x){
    vector<double> tmp;
    tmp.push_back(x*(y-y_ref));//w's grad
    tmp.push_back(y-y_ref);//d's grad
    //cout<<x<<"space"<<y<<"space"<<y_ref<<"space"<<tmp[0]<< "space"<<tmp[1]<<endl;
    return tmp;}

3、训练函数

训练函数中首先需要获取样本总数,因为是SGD得方式求梯度,因此每次在for循环中提取一个样本进行训练,还是以原理中解释得步骤,首先计算y值,然后计算梯度,最后对w和b分别以不同得学习率进行更新。

void train(vector<vector<double>> data,double alpha_w,double alpha_b){
    int num = data.size();
    vector<double> tmp;
    double x(0.0),y(0.0),y_ref(0.0);
    for(int i = 0;i<num;i++){
        x = data[i][0];
        y_ref = data[i][1];

        y = val(x);
        tmp = grad(y,y_ref,x);
        w = w-alpha_w*tmp[0];
        b = b-alpha_b*tmp[1];
    }
}

4、主函数

主函数中主要进行参数设置以及启动训练,设置w_ref和b_ref可以让我们拟合任意一个想要的函数,检验自己的算法,w_init和b_init是类中随机初始化的参数值,sample是采样的样本个数,start_sample以及end_sample是采样区间,alpha_w和alpha_b是学习率。

    double w_init = sol.w;
    double b_init = sol.b;
    double w_ref = 3.0;
    double b_ref = 2.0;
    double sample = 500;
    double start_sample = -10;
    double end_sample = 10.0;
    double alpha_w = 0.002;
    double alpha_b = 0.01;

5、结果

可以看到,经过500个样本的训练后,参数w由-2调整到2.92,离生成样本的参数w_ref仅相差0.08;参数b由4调整到2.06,离生成样本的参数b_ref仅相差0.06,检验了有效性。

在这里插入图片描述

总结

本文使用c++编写梯度下降算法实现一元线性回归,此代码仅为最简单的版本,后续可以继续完善功能,例如加入文本输入样本部分,加入由损失函数阈值判断是否终止,以及使用小批量更新等等。欢迎大家下载、使用、一共完善此代码。
githuv源码:
https://github.com/wangjunhe8127/linear-regression-by-Gradient-descent

_May the force be with you!_