多元线性回归的数据集有多个特征。程序经过相应的优化算法使模型拟合训练数据集。本次练习使用C语言编写波士顿房价预测的多元线性回归程序。本次所用的数据集将会上传到优快云。
程序代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<math.h>
double w[13];//参数+偏置
double Max[12];
double Min[12];//训练集各维数据的最大值和最小值,为数据归一化做准备。
double Distance[12];//Max-Min
double x[506][13];//训练集
double y[506];//标签
int T_num;//训练集大小
int Valid_num;//验证集大小
double Model(double *x, double *w);
double Select_Maxin()//查找各维最大最小值
{
int i, j, k, l, m;
for (i = 0; i < 12; i++)
{
for (j = 0; j < 505; j++)
{
if (x[j][i] > x[j + 1][i])
Min[i] = x[j + 1][i];
if (x[j][i] < x[j + 1][i])
Max[i] = x[j + 1][i];
}
Distance[i] = Max[i] - Min[i];
}
}
double Valid_Loss()//损失函数
{
int i;
double L = 0;
for (i = 400; i < Valid_num+400; i++)
{
L += pow((y[i] - Model(x[i], w)), 2);
}
L = L / Valid_num;
return L;
}
double Loss()//损失函数
{
int i, j, k;
double L = 0;
for (i = 0; i < T_num; i++)
{
L += pow((y[i] - Model(x[i], w)), 2);
}
L = L / T_num;
return L;
}
double Gradient_Descent_Optimization(double a)//梯度下降优化函数
{
int i, j, k, m;
double opt[13];
double W[13];
for (i = 0; i < 13; i++)
{
opt[i] = 0;
for (j = 0; j < T_num; j++)
opt[i] += x[j][i] * (Model(x[j], w) - y[j]);
opt[i] = opt[i] / T_num;
}
//开始更新参数
for (m = 0; m < 13; m++)
{
w[m] = w[m] - a * opt[m];
}
}
double Model(double *x, double *w)//模型
{
double Y = 0;
int i;
for (i = 0; i < 12; i++)
{
Y += ((x[i] - Min[i]) / Distance[i]) * w[i];
}
Y += w[12];
return Y;
}
double Gaussrand_Normal()
{
static float V1, V2, S;
static int phase = 0;
double X;
if (phase == 0)
{
do
{
double U1 = (float)rand() / RAND_MAX;
double U2 = (float)rand() / RAND_MAX;
V1 = 2 * U1 - 1;
V2 = 2 * U2 - 1;
S = V1 * V1 + V2 * V2;
} while (S >= 1 || S == 0);
X = V1 * sqrt(-2 * log(S) / S);
}
else
X = V2 * sqrt(-2 * log(S) / S);
phase = 1 - phase;
return X;
}
double gaussrand(float mean, float stdc)
{
return mean + Gaussrand_Normal()*stdc;
}
//主函数;
//模型训练过程
//定义重要参数
//读取训练数据
int main()
{
FILE *fp = NULL;
char Delimiter;//分隔符
int i, j, k;//循环标志
double a;//学习率
double Epoch;//训练轮次
double L;//损失函数值
double L_Valid;
double Detect;//结果验证值
//读取文件
for (i = 0; i < 506; i++)
{
x[i][12] = 1;
}
if ((fp = fopen("C:/Users/西邮吴彦祖/Desktop/data/boston.csv", "at+")) != NULL)
{
fseek(fp, 63L, SEEK_SET);
for (j = 0; j < 506; j++)
{
fscanf(fp, "%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf,%lf", &x[j][0], &x[j][1], &x[j][2], &x[j][3], &x[j][4], &x[j][5], &x[j][6], &x[j][7], &x[j][8], &x[j][9], &x[j][10], &x[j][11], &y[j]);
}
}
fclose(fp);
//初始化训练集大小
T_num = 400;
//初始化验证集大小
Valid_num = 100;
Select_Maxin();
//初始化超参数
a = 0.00129;
Epoch = 200000;
for (i = 400; i < 506; i++)
printf("y:%lf\n", y[i]);
//按照正态分布随机初始化参数
for (k = 0; k < 13; k++)
{
w[k] = gaussrand(0, 1);
printf("w:%lf\n", w[k]);
}
for (j = 0; j < Epoch; j++)
{
Gradient_Descent_Optimization(a);
L = Loss();
L_Valid = Valid_Loss();
printf("第%d轮训练:Training Loss:%lf , Valid Loss:%lf\n", j + 1, L, L_Valid);
}
printf("最终参数值:\n");
for (i = 0; i < 13; i++)
{
printf("第%d个参数:%lf\n", i, w[i]);
}
for (k = 500; k < 506; k++)
{
Detect = Model(x[k], w);
printf("预测结果:%lf\n实际结果:%lf\n\n", Detect, y[k]);
}
system("pause");
}
代码讲解
代码整体框架
整个程序包含7个自定义函数,
double Select_Maxin()//查找各维最大最小值
double Valid_Loss()//验证集损失函数
double Loss()//训练集损失函数
double Gradient_Descent_Optimization(double a)//梯度下降优化函数
double Model(double *x, double *w)//模型
double Gaussrand_Normal()
double gaussrand(float mean, float stdc)//生成正态分布数组
下面根据调用顺序讲解代码:
特征数据归一化
进入主函数后读取训练集,将训练集中的特征存放在二维数组x[506][13]中,标签则被存放在一维数组y[506]中。
为了使梯度下降法可以较快地收敛,特征数据需要进行归一化:(特征值-特征值最小值)/(特征值最大值-特征值最小值)。
double Select_Maxin()//查找各维最大最小值
{
int i, j, k, l, m;
for (i = 0; i < 12; i++)
{
for (j = 0; j < 505; j++)
{
if (x[j][i] > x[j + 1][i])
Min[i] = x[j + 1][i];
if (x[j][i] < x[j + 1][i])
Max[i] = x[j + 1][i];
}
Distance[i] = Max[i] - Min[i];
}
}
Select_Maxin()函数可以找出数据集中各维数据中的最大值与最小值并将最大值、最小值与两者的差值分别存放在Max、Min、Distance数组中。
参数初始化
之后初始化参数与超参数。该模型有13个参数,在初始化参数时按照正态分布随机初始化模型中的十三个参数,正态分布数组生成函数为:
double Gaussrand_Normal()
{
static float V1, V2, S;
static int phase = 0;
double X;
if (phase == 0)
{
do
{
double U1 = (float)rand() / RAND_MAX;
double U2 = (float)rand() / RAND_MAX;
V1 = 2 * U1 - 1;
V2 = 2 * U2 - 1;
S = V1 * V1 + V2 * V2;
} while (S >= 1 || S == 0);
X = V1 * sqrt(-2 * log(S) / S);
}
else
X = V2 * sqrt(-2 * log(S) / S);
phase = 1 - phase;
return X;
}
double gaussrand(float mean, float stdc)
{
return mean + Gaussrand_Normal()*stdc;
}
调用函数生成正态分布随机数组:
//按照正态分布随机初始化参数
for (k = 0; k < 13; k++)
{
w[k] = gaussrand(0, 1);
printf("w:%lf\n", w[k]);
}
梯度下降优化
接下来开始使用批量梯度下降法训练模型,优化函数为:
double Gradient_Descent_Optimization(double a)//梯度下降优化函数
{
int i, j, k, m;
double opt[13];
double W[13];
for (i = 0; i < 13; i++)
{
opt[i] = 0;
for (j = 0; j < T_num; j++)
opt[i] += x[j][i] * (Model(x[j], w) - y[j]);
opt[i] = opt[i] / T_num;
}
//开始更新参数
for (m = 0; m < 13; m++)
{
w[m] = w[m] - a * opt[m];
}
}
其方法为,在训练集的范围内分别对损失函数中的各变量求偏导并令求得的结果相加求其平均值,之后使用上一次迭代得到的参数值减去学习率a与上一步得到的结果opt得到更新后的参数值。
损失函数
在这里损失函数使用均方差损失函数:
double Loss()//损失函数
{
int i, j, k;
double L = 0;
for (i = 0; i < T_num; i++)
{
L += pow((y[i] - Model(x[i], w)), 2);
}
L = L / T_num;
return L;
}
经过参数更新后再次计算最新的模型在训练集和验证集上的损失。
经过Epoch次迭代后,得到最新模型。并使用测试集测试模型的准确度。至此,程序运行完毕。
总结
踩过的坑
未划分数据
在第一版代码中,我将所有数据都用做训练集。在训练完毕得出结果后,虽然损失被降得很低但对比之前Python编写的多元线性回归程序得出的结果发现模型参数的差别很大。于是我在所有506个数据中划分出400个数据作为训练集,100个数据作为验证集,其他数据则用来测试模型,用来验证模型的正确性。
数据读取出错
使用Notepad++打开.csv文件时可以看到各数据之间使用逗号间隔,所以我在读取数据时将数据存放至数组中,将数据间的逗号度进一个char型变量中,但是在程序运行时数组中的值总是为零。经过多次调试程序后我发现,读取.csv文件时需要忽略逗号。
心得
在整个程序调试过程中最痛苦的莫过于调整学习率,如果学习率太大在程序一开始就跑飞了,最后的出的结果只会是一堆“nan”。如果学习率太小则需要更多次迭代才会有满意的结果。所以后续可以引入相应的超参数优化机制来完善代码。在该版本程序中我分别编写了两个损失函数一个用于计算模型在训练集上的损失,另一个用于计算模型在测试机上的损失。可以在下次改进时只用一个损失函数计算两个损失。
下一次将会使用C++实现对数据的二分类,即可以锻炼自己的C++使用,又可以验证自己对分类问题的理解。目前正在找可用的数据集。相对于线性回归问题,分类问题用到了激活函数,并且可以增加网络的层数,我相信随着学习的深入机器学习将会变得越来越有趣。