N个用户,M个商品,分别有若干评分,要求预测用户对未评分商品的评分值
定义每个用户为一维向量p,代表对某个商品各特征的重视程度,向量维数为K
定义每个商品为一维向量q,代表该商品各特征值,向量维数为K
其中向量维数K为可调超参数
某个用户对某个商品的预测打分值可定义为(向量内积):
其中i代表第i个用户,j代表第j个商品,K代表向量的总维数,k代表遍历向量的某一维
损失函数定义为所有(标签-预测值)平方的总和:
算法定义为损失函数分别对用户向量和商品向量求偏导(梯度下降),从而分别对用户向量和商品向量更新:
更新参数(更新向量的参数直至使其适应训练集)的公式如下:
其中代表步长,即学习率,为可调超参数
评价模型本代码使用以下两个值:
均方根误差:
平均绝对误差:
其中T代表预测样本的数量,即测试集的大小(行数)
通过引入正则化来防止模型的过拟合:
正则化的引入需要改变损失函数,通过让每一次损失函数的计算都有固定存在的小损失来实现
带有正则化的损失函数定义为所有(标签-预测值)平方再加上正则化项的总和:
与之前相比只是多了正则化项,其中代表正则化参数,为可调超参数
算法定义为损失函数分别对用户向量和商品向量求偏导(梯度下降),从而分别对用户向量和商品向量更新:
更新参数(更新向量的参数直至使其适应训练集)的公式如下:
其中仍代表步长,即学习率,为可调超参数
评价模型仍然使用以下两个值:
均方根误差:
平均绝对误差:
其中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