机器学习-基于矩阵分解算法的梯度下降推荐系统 (C++实现)

N个用户,M个商品,分别有若干评分r_{ij},要求预测用户对未评分商品的评分值

定义每个用户为一维向量p,代表对某个商品各特征的重视程度,向量维数为K
定义每个商品为一维向量q,代表该商品各特征值,向量维数为K

其中向量维数K为可调超参数

某个用户对某个商品的预测打分值可定义为(向量内积):

\hat{r_{ij}}=p^{T}q=\sum _{k=1}^{K}\textrm{}p_{ik}q_{jk}

其中i代表第i个用户,j代表第j个商品,K代表向量的总维数,k代表遍历向量的某一维

损失函数定义为所有(标签-预测值)平方的总和:

loss=e_{ij}^{2}=\sum (r_{ij}-\hat{r{ij}})^{2}=\sum (r_{ij}-\sum _{k=1}^{K}\textrm{}p_{ik}q_{jk})^{2}

算法定义为损失函数分别对用户向量和商品向量求偏导(梯度下降),从而分别对用户向量和商品向量更新:

\frac{\partial e_{ij}^{2}}{\partial p_{ik}}=2e_{ij}(-q_{jk})

\frac{\partial e_{ij}^{2}}{\partial q_{jk}}=2e_{ij}(-p_{ik})

更新参数(更新向量的参数直至使其适应训练集)的公式如下:

p_{ik}=p_{ik}-\lambda\frac{\partial e_{ij}^{2}}{\partial p_{ik}}=p_{ik}+2\lambda e_{ij}q_{jk} (k=1,k=2,...,k=K)

q_{jk}=q_{jk}-\lambda\frac{\partial e_{ij}^{2}}{\partial q_{jk}}=q_{jk}+2\lambda e_{ij}p_{ik} (k=1,k=2,...,k=K)

其中\lambda代表步长,即学习率,为可调超参数

评价模型本代码使用以下两个值:

均方根误差:RMSE=\sqrt{\frac{\sum (r_{ij}-\hat{r{ij}})^{2}}{\left |T \right |}}

平均绝对误差:MAE=\frac{\sum \left |r_{ij}-\hat{r_{ij}} \right |}{\left |T \right |}

其中T代表预测样本的数量,即测试集的大小(行数)

通过引入正则化来防止模型的过拟合:

正则化的引入需要改变损失函数,通过让每一次损失函数的计算都有固定存在的小损失来实现

带有正则化的损失函数定义为所有(标签-预测值)平方再加上正则化项的总和:

loss=e_{ij}^{2}=\sum \left \{ (r_{ij}-\hat{r{ij}})^{2}+\frac{\beta }{2}[\sum _{k=1}^{K}\textrm{}(p_{ik}^{2}+q_{jk}^{2})] \right \}

与之前相比只是多了正则化项,其中\beta代表正则化参数,为可调超参数

算法定义为损失函数分别对用户向量和商品向量求偏导(梯度下降),从而分别对用户向量和商品向量更新:

\frac{\partial e_{ij}^{2}}{\partial p_{ik}}=-2e_{ij}q_{jk}+\beta p_{ik}

\frac{\partial e_{ij}^{2}}{\partial q_{jk}}=-2e_{ij}p_{ik}+\beta q_{jk}

更新参数(更新向量的参数直至使其适应训练集)的公式如下:

p_{ik}=p_{ik}-\lambda\frac{\partial e_{ij}^{2}}{\partial p_{ik}}=p_{ik}-\lambda (-2e_{ij}q_{jk}+\beta p_{ik}) (k=1,k=2,...,k=K)

q_{jk}=q_{jk}-\lambda\frac{\partial e_{ij}^{2}}{\partial q_{jk}}=q_{jk}-\lambda (-2e_{ij}p_{ik}+\beta q_{jk}) (k=1,k=2,...,k=K)

其中\lambda仍代表步长,即学习率,为可调超参数

评价模型仍然使用以下两个值:

均方根误差:RMSE=\sqrt{\frac{\sum (r_{ij}-\hat{r{ij}})^{2}}{\left |T \right |}}

平均绝对误差:MAE=\frac{\sum \left |r_{ij}-\hat{r_{ij}} \right |}{\left |T \right |}

其中T代表预测样本的数量,即测试集的大小(行数)

运行平台与环境:Windows11,VisualStudio2022

下面放代码qwq:

//基于矩阵分解算法的梯度下降推荐系统模型 (C++实现) 2023.3.30
#pragma GCC optimize(2)
#include<bits/stdc++.h>
#include<fstream>
#define ll long long int

using namespace std;
constexpr ll N = 1e6 + 10;

int K = 10; //权重参数向量的维数(用户向量的维数/商品向量的维数)
double a = 0.0001; //学习率
double b = 0.005; //正则化系数

char letter[N] = { '\0' };
string words;

list<int> trains[800010]; //读入的训练集
list<int> tests[300010]; //读入的测试集

vector<double> w_p[8010]; //用户权重参数
vector<double> w_q[6010]; //商品权重参数

void initR() //初始化读入数据集
{
	ifstream readFile;
	readFile.open("train.txt", ios::in);

	words.clear();
	for (int i = 0; i <= 1e6; ++i) letter[i] = '\0';
	
	int sep = 0;
	while (readFile >> letter)
	{
		words += letter;
		if (sep != 2) { ++sep; words += " "; }
		else { sep = 0; words += "\n"; }
	}

	readFile.close();

    int cnt = 1, div = 0;
    string ins1, ins2, ins3;
    for (int i = 0; words[i] != '\0'; ++i)
    {
        if (div != 3)
        {
            if (words[i] != ' ' && words[i] != '\n')
            {
                if (div == 0) ins1 += words[i];
                if (div == 1) ins2 += words[i];
                if (div == 2) ins3 += words[i];
            }
            if (words[i] == ' ' || words[i] == '\n')
            {
                ++div;
            }
        }
        if (words[i] == '\n')
        {
			trains[cnt].push_back(stoi(ins1));
			trains[cnt].push_back(stoi(ins2));
			trains[cnt].push_back(stoi(ins3));

			++cnt;
            div = 0;
            ins1.clear();
            ins2.clear();
            ins3.clear();
        }
    }


	readFile.open("test.txt", ios::in);

	words.clear();
	for (int i = 0; i <= 1e6; ++i) letter[i] = '\0';

	sep = 0;
	while (readFile >> letter)
	{
		words += letter;
		if (sep != 2) { ++sep; words += " "; }
		else { sep = 0; words += "\n"; }
	}

	readFile.close();

	cnt = 1, div = 0;
	ins1.clear(), ins2.clear(), ins3.clear();
	for (int i = 0; words[i] != '\0'; ++i)
	{
		if (div != 3)
		{
			if (words[i] != ' ' && words[i] != '\n')
			{
				if (div == 0) ins1 += words[i];
				if (div == 1) ins2 += words[i];
				if (div == 2) ins3 += words[i];
			}
			if (words[i] == ' ' || words[i] == '\n')
			{
				++div;
			}
		}
		if (words[i] == '\n')
		{
			tests[cnt].push_back(stoi(ins1));
			tests[cnt].push_back(stoi(ins2));
			tests[cnt].push_back(stoi(ins3));

			++cnt;
			div = 0;
			ins1.clear();
			ins2.clear();
			ins3.clear();
		}
	}

	return;
}

void initW(int K) //初始化权重参数
{
	int r;
	srand((unsigned int)time(NULL));
	for (int i = 1; i <= 8000; ++i)
	{
		w_p[i].resize(K); //每个用户为长度K的向量,最多8000个用户
		for (int j = 0; j < K; ++j)
		{
			r = rand() % 9 + 1; //随机[1, 9]以内的整数
			w_p[i][j] = (double)r / 10; //每个权重参数初始化为[0.1, 0.9]之间的浮点数
		}
	}
	for (int i = 1; i <= 6000; ++i)
	{
		w_q[i].resize(K); //每个商品为长度K的向量,最多6000个商品
		for (int j = 0; j < K; ++j)
		{
			r = rand() % 9 + 1; //随机[1, 9]以内的整数
			w_q[i][j] = (double)r / 10; //每个权重参数初始化为[0.1, 0.9]之间的浮点数
		}
	}

	return;
}

double calc(int i, int j) //计算内积(第i个用户向量和第j个商品向量)
{
	double res = 0;
	for (int k = 0; k < K; ++k) res += w_p[i][k] * w_q[j][k];
	return res;
}

void TRAIN(int EPOCH)
{
	double loss = 0.0; //当前轮的loss
	for (int epoch = 1; epoch <= EPOCH; ++epoch)
	{
		loss = 0.0;
		for (int i = 1; i <= 800000; ++i)
		{
			if (trains[i].size() == 0) break;

			int userid = 0, goodid = 0, score = 0;
			for (auto it = trains[i].begin(); it != trains[i].end(); ++it)
			{
				if (userid == 0) userid = *it;
				else if (goodid == 0) goodid = *it;
				else if (score == 0) score = *it;
			}

			double e; //预测值与标签的差值
			for (int k = 0; k < K; ++k)
			{
				//不带正则化的权重更新
				e = score - calc(userid, goodid);
				w_p[userid][k] = w_p[userid][k] + 2.0 * a * e * w_q[goodid][k];
				loss += e * e;

				e = score - calc(userid, goodid);
				w_q[goodid][k] = w_q[goodid][k] + 2.0 * a * e * w_p[userid][k];
				loss += e * e;


				/*//带正则化的权重更新
				e = score - calc(userid, goodid);
				w_p[userid][k] = w_p[userid][k] - a * (-2 * e * w_q[goodid][k] + b * w_p[userid][k]);
				loss += e * e;

				e = score - calc(userid, goodid);
				w_q[goodid][k] = w_q[goodid][k] - a * (-2 * e * w_p[userid][k] + b * w_q[goodid][k]);
				loss += e * e;*/
			}
		}

		cout << fixed << setprecision(6);
		cout << "epoch" << epoch << "->loss=" << loss << "\n";
	}
}

void TEST()
{
	int T = 0; //测试集中样本数据的数量(测试集行数)
	double mae = 0; //平均绝对误差
	double rmse = 0; //均方根误差

	for (int i = 1; i <= 300000; ++i)
	{
		if (tests[i].size() == 0) break;

		int userid = -1, goodid = -1, score = -1;
		for (auto it = tests[i].begin(); it != tests[i].end(); ++it)
		{
			if (userid == -1) userid = *it;
			else if (goodid == -1) goodid = *it;
			else if (score == -1) score = *it;
		}

		double e; //预测值与标签的差值
		e = score - calc(userid, goodid);
		mae += e;
		e = e * e;
		rmse += e;

		++T;
	}

	rmse /= T;
	rmse = sqrt(rmse);
	mae /= T;

	cout << fixed << setprecision(6);
	cout << "均方根误差RMSE=" << rmse << "\n";
	cout << "平均绝对误差MAE=" << mae << "\n";
}

int main()
{
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);

	initR();
	initW(K);

	TRAIN(300);
	TEST();

	return 0;
}

训练集train.txt 和 测试集test.txt 的下载路径如下:

链接: https://pan.baidu.com/s/1077UD5BhAiqiiCTI4SbaAw 提取码: stet 

训练集和测试集记事本中数据的格式:
每行三个整数,分别代表 <用户编号> <商品编号> <某用户对某商品的评分>

记得把两个记事本放在工程根目录下哦qwq

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值