-
Notifications
You must be signed in to change notification settings - Fork 0
/
report.typ
396 lines (315 loc) · 16.5 KB
/
report.typ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
#import "template.typ": *
#show: project.with(
header_text: "2024 暑期大数据推荐系统课程",
title: "基于隐式反馈的 Top-10 推荐列表预测",
authors: (
(
name: "吴建军",
organization: [学号2024140899],
email: "[email protected]"
),
),
abstract: "本实验通过使用提供的训练集构建了推荐模型,并使用测试集中的数据对推荐系统进行了Top-10推荐列表的预测。完成了数据处理、模型构建、训练与验证的过程,并生成了每个用户的推荐结果。通过实验验证了推荐算法的效果和可行性,并对推荐系统的性能进行了评估。",
keywords: (
"推荐系统",
"Top-10推荐",
"隐式反馈",
),
)
= 实验概述 <introduction>
training.txt是用户-物品-隐式反馈的交互对,一共有四万多条交互信息。在代码中将其拆分为训练集和验证集。
test.txt是真实的测试集,只有用户ID,我们最终需要在该测试集上进行Top-N推荐任务。
result.txt是算法得到的结果,即对test.txt中的用户一一进行Top-10推荐。
== 实验设计 <setting>
本实验包括以下几个步骤,每个部分都有明确的功能,最终实现了一个基于矩阵分解的推荐系统。
+ 数据加载与预处理。这部分代码加载训练数据,进行负采样来生成负样本,并将正负样本结合后打乱顺序。
+ 数据集划分和稀疏度计算。计算了数据的稀疏度,并划分了训练集和验证集,计算了平均评分。
+ 初始化矩阵参数。初始化了用户和电影的特征矩阵及其增量矩阵。
+ 训练模型。实现了模型的训练过程,包括特征矩阵的更新、训练误差和验证误差的计算,并绘制了误差曲线。
+ Precision@10和Recall@10计算。这部分代码计算了Precision@10和Recall@10两个评价指标。
+ 推荐结果生成与保存。这部分代码对测试集用户生成推荐结果,并将结果保存为txt文件。
== 代码清单
本项目的代码目录如下:
```python
project/
│
├── data/
│ └── training.txt
│ └── test.txt
│ └── result.txt
│
├── images/
│ └── num_feats_6.png
│ └── num_feats_8.png
│ └── num_feats_10.png
│ └── baseline.png
│
├── baseline.py
└── main.py
```
data目录包含输入、输出数据文件。
images目录包含本报告中所有的图。
baseline.py文件为基准模型的实现代码,用于与主模型进行比较。
main.py文件为主程序文件,包含项目的核心逻辑。
== 实验环境 <environment>
本实验使用的环境如下:
- Ubuntu 22.04 LTS
- Python 3.11.9
= 实现细节 <details>
== 数据加载与预处理
加载训练数据并设定列名为"user_id", "item_id", "click",方便后续处理。将所有点击行为设为5,简化了评分机制,使模型更关注于交互关系。通过负采样生成负样本(即用户未点击的项目),并将正负样本结合,打乱顺序以增加数据的多样性。
这样做确保数据格式正确,同时通过引入负样本,提升模型的泛化能力,使其在预测未见数据时表现更好。
```Python
# 读取训练数据
train_data = pd.read_csv("data/training.txt", sep=" ", header=None, names=["user_id", "item_id", "click"])
train_data["click"] = 5
rating = train_data["click"]
movie = train_data["item_id"]
user = train_data["user_id"]
data_num = len(rating)
movie_num = movie.nunique() # 电影的数量
user_num = user.nunique() # 用户的数量
# 负采样
negative_sample = np.random.choice(user_num * movie_num, data_num, replace=True)
record_user = []
record_movie = []
record_rating = [1] * data_num
for i in negative_sample:
movie_i = i // user_num
user_i = i % user_num + 1
record_user.append(user_i)
record_movie.append(movie_i)
negative_data = pd.DataFrame({"user_id": record_user, "item_id": record_movie, "click": record_rating})
# 数据结合
data = pd.concat([train_data, negative_data])
rating = data["click"]
movie = data["item_id"]
user = data["user_id"]
data_num = len(rating)
movie_num = movie.nunique() # 使用.nunique()确保电影数量的准确性
user_num = user.nunique() # 使用.nunique()确保用户数量的准确性
data = data.sample(frac=1).reset_index(drop=True) # 打乱数据集
```
== 数据集划分和稀疏度计算
计算数据的稀疏度,通过用户和物品的数量以及评分数据的数量来确定数据的稀疏程度。这一步有助于理解数据的特点,选择合适的模型和优化方法。随后将数据划分为训练集和验证集,训练集用于训练模型,验证集用于评估模型在未见过的数据上的表现。
```Python
# 稀疏度
sparsity = data_num / (movie_num * user_num)
print(f"Sparsity: {sparsity:.6f}")
# 划分训练集和验证集
train_num = 80000
train_vec = data.iloc[:train_num]
probe_vec = data.iloc[train_num:84000]
mean_rating = train_vec["click"].mean()
pairs_tr = len(train_vec)
pairs_pr = len(probe_vec)
numbatches = 8
num_m = movie_num
num_p = user_num
```
== 初始化矩阵参数
初始化用户特征矩阵w1_P1和电影特征矩阵w1_M1,以及它们的增量矩阵w1_P1_inc和w1_M1_inc,这些矩阵用于动量优化。这些特征矩阵以随机小数初始化,确保模型在开始训练时有一个合理的初始状态。增量矩阵用于记录前几次更新的趋势,动量优化有助于加速训练过程,提高模型的收敛速度和稳定性,减少训练过程中的震荡。
```Python
# 初始化矩阵参数
w1_M1 = 0.1 * np.random.rand(num_m, num_feat) # 电影特征矩阵
w1_P1 = 0.1 * np.random.rand(num_p, num_feat) # 用户特征矩阵
w1_M1_inc = np.zeros((num_m, num_feat)) # 电影特征矩阵增量
w1_P1_inc = np.zeros((num_p, num_feat)) # 用户特征矩阵增量
```
== 训练模型
通过多次epoch和batch迭代更新特征矩阵。在每个batch中,提取当前批次的用户和物品ID,计算预测评分、误差平方和、梯度,并更新特征矩阵。分批次训练使得模型可以处理大规模数据,避免内存溢出。梯度更新和动量优化可以加速模型训练,提高模型的准确性和稳定性。每个epoch结束时计算训练误差和验证误差,有助于监控模型的训练过程,及时调整策略。
```Python
# 训练模型
for epoch in range(maxepoch):
for batch in range(numbatches):
# 计算当前批次的起始和结束索引
start = batch * N
end = start + N
if end > pairs_tr: # 防止越界
end = pairs_tr
# 提取当前批次的用户和物品ID,并将它们的索引减1以匹配矩阵
aa_p = train_vec.iloc[start:end]["user_id"].values - 1
aa_m = train_vec.iloc[start:end]["item_id"].values - 1
rating = train_vec.iloc[start:end]["click"].values.astype(float) # 确保rating为浮点数
rating -= mean_rating # 减去平均评分
# 计算预测评分
pred_out = np.sum(w1_M1[aa_m] * w1_P1[aa_p], axis=1)
f = np.sum((pred_out - rating) ** 2) # 计算误差平方和
# 计算梯度
IO = 2 * (pred_out - rating)
IO = np.tile(IO[:, None], num_feat) # 将IO扩展到特征数量的维度
Ix_m = IO * w1_P1[aa_p] # 对电影特征的梯度
Ix_p = IO * w1_M1[aa_m] # 对用户特征的梯度
# 初始化梯度增量矩阵
dw1_M1 = np.zeros((movie_num, num_feat))
dw1_P1 = np.zeros((user_num, num_feat))
# 累加每个样本的梯度
for ii in range(N):
dw1_M1[aa_m[ii]] += Ix_m[ii]
dw1_P1[aa_p[ii]] += Ix_p[ii]
# 更新特征矩阵
w1_M1_inc = momentum * w1_M1_inc + epsilon * dw1_M1 / N
w1_M1 -= w1_M1_inc
w1_P1_inc = momentum * w1_P1_inc + epsilon * dw1_P1 / N
w1_P1 -= w1_P1_inc
# 计算训练误差
pred_out = np.sum(w1_M1[aa_m] * w1_P1[aa_p], axis=1)
f_s = np.sum((pred_out - rating) ** 2)
err_train[epoch] = np.sqrt(f_s / N) # 计算训练集上的RMSE
# 在验证集上进行预测并计算误差
aa_p = probe_vec["user_id"].values - 1
aa_m = probe_vec["item_id"].values - 1
rating = probe_vec["click"].values
pred_out = np.sum(w1_M1[aa_m] * w1_P1[aa_p], axis=1) + mean_rating
pred_out = np.clip(pred_out, 1, 5) # 将预测评分限制在1到5之间
err_valid[epoch] = np.sqrt(np.sum((pred_out - rating) ** 2) / pairs_pr) # 计算验证集上的RMSE
# 打印每个epoch的训练和验证误差
print(f"Epoch {epoch + 1}/{maxepoch}, Train RMSE: {err_train[epoch]:.4f}, Test RMSE: {err_valid[epoch]:.4f}")
# 绘制Loss曲线
plt.plot(range(1, maxepoch + 1), err_train, label="Train Error", color="blue")
plt.plot(range(1, maxepoch + 1), err_valid, label="Validation Error", color="red")
plt.xlabel("Epoch")
plt.ylabel("Error")
plt.legend()
plt.show()
```
== 计算Precision@10和Recall@10
计算两个关键的推荐系统评估指标:Precision@10和Recall@10。Precision@10衡量推荐结果中前10个推荐项中相关项的比例,反映推荐结果的准确性。Recall@10衡量前10个推荐项中覆盖了所有相关项的比例,反映推荐结果的全面性。
```Python
# 定义常量
j = 10
# 计算Precision@10
precisions = []
for i in range(user_num):
user_i_rating_real = probe_vec[probe_vec["user_id"] == i + 1]
user_i_rating_real = user_i_rating_real.sort_values(by="click", ascending=False)
user_i_rating = (
np.dot(w1_P1[i, :], w1_M1[user_i_rating_real["item_id"].values - 1].T)
+ mean_rating
)
if len(user_i_rating_real) > j:
top_j_pred = np.argsort(-user_i_rating)[:j]
precision = np.sum(np.isin(top_j_pred, np.arange(j))) / j
precisions.append(precision)
else:
ti = np.sum(user_i_rating_real["click"] >= 4)
if ti != 0:
precision = np.sum(user_i_rating[:ti] >= 4) / ti
precisions.append(precision)
Pre = np.mean(precisions)
# 计算Recall@10
recalls = []
for i in range(user_num):
user_i_rating_real = probe_vec[probe_vec["user_id"] == i + 1]
user_i_rating_real = user_i_rating_real.sort_values(by="click", ascending=False)
user_i_rating = (
np.dot(w1_P1[i, :], w1_M1[user_i_rating_real["item_id"].values - 1].T)
+ mean_rating
)
if len(user_i_rating_real) > j:
user_i_rating[user_i_rating < 4] = 0
user_i_rating[user_i_rating >= 4] = 1
ti = np.sum(user_i_rating)
if ti != 0:
bigerthan4 = np.sum(user_i_rating_real["click"] >= 4)
tinri = np.sum(user_i_rating[:bigerthan4])
recall = tinri / ti
recalls.append(recall)
else:
ti = np.sum(user_i_rating_real["click"] >= 4)
if ti != 0:
recall = np.sum(user_i_rating[:ti] >= 4) / ti
recalls.append(recall)
Re = np.mean(recalls)
print(f"Recall@10: {Re:.4f}, Precision@10: {Pre:.4f}")
```
== 推荐结果生成与保存
读取测试集数据,对每个测试用户生成推荐结果。通过计算用户对每个物品的预测评分,筛选出评分最高的前10个物品作为推荐结果。将推荐结果与测试集用户结合,生成推荐结果的DataFrame,并保存为txt文件。
```Python
# 读取测试集数据
test = pd.read_csv("data/test.txt", header=None, sep=" ")
test.columns = ["user_id"] # 设置列名为'user_id'
# 定义常量
k = 10 # 推荐的数量
# 初始化变量
record = [] # 存储推荐结果的列表
# 对每个测试用户进行推荐
for i in test["user_id"]:
user_i_rating = np.dot(w1_P1[i - 1, :], w1_M1.T) + mean_rating
used = data[data["user_id"] == i]["item_id"].values - 1
user_i_rating[used] = 0 # 将已评分的电影的评分设为0
top_k_movies = np.argsort(user_i_rating)[-k:][::-1] + 1
record.append((i, top_k_movies)) # 将用户ID和推荐结果加入列表
# 将推荐结果转换为指定格式的字符串
result_lines = []
for user_id, movies in record:
movies_str = ",".join(map(str, movies))
result_lines.append(f"{user_id}: {movies_str}")
# 将结果保存到TXT文件
with open("data/result.txt", "w") as file:
for line in result_lines:
file.write(line + "\n")
```
= 结果分析 <example>
== 参数设定
本实验的所用的参数如下,学习率(epsilon=50)控制每次参数更新的步长,动量参数(momentum=0.7)加速收敛并减少震荡;初始epoch(epoch=1)和总训练次数(maxepoch=50)决定训练迭代的轮数;训练误差和验证误差数组(err_train和err_valid)分别用于记录每个epoch结束时的训练和验证误差,以监控模型表现;隐因子数量(num_feat=8)决定特征向量的维度,影响模型复杂度和推荐效果;每次训练三元组的数量(N=10000)设定了每个batch的大小,确保有效利用内存处理大规模数据。
```python
epsilon = 50 # Learning rate 学习率
momentum = 0.7 # Momentum parameter 动量优化参数
epoch = 1 # Initial epoch 初始化epoch
maxepoch = 50 # Total number of training epochs 总训练次数
err_train = np.zeros(maxepoch) # Training error 训练误差
err_valid = np.zeros(maxepoch) # Validation error 验证误差
err_random = np.zeros(maxepoch) # Random error 随机误差
num_feat = 8 # Number of latent factors 隐因子数量
N = 10000 # Number of training triplets per epoch 每次训练三元组的数量
```
== 训练集表现
分别设定num_feat的值为6,8,10来探究训练集上的表现,如@nf6、@nf8、@nf10。可以看到训练误差(Train Error)随着训练次数的增加,训练误差逐渐下降并趋于稳定,说明模型在训练数据上的拟合效果越来越好。但是随着隐因子数量的增加,验证误差(Validation Error)在初期也随训练次数的增加而下降,但在某个点之后开始趋于平稳甚至略有上升。这表明增加隐因子数量虽然可以提高训练数据上的拟合效果,但对验证数据的泛化能力提升有限,甚至可能导致过拟合。
#figure(image("images/num_feats_6.png"),
caption: [
num_feat = 6 的 Loss 曲线
],
)<nf6>
#figure(image("images/num_feats_8.png"),
caption: [
num_feat = 8 的 Loss 曲线
],
)<nf8>
#figure(image("images/num_feats_10.png"),
caption: [
num_feat = 10 的 Loss 曲线
],
)<nf10>
如@table1 所示,在五个随机种子下进行了不同测试以消除偶然性。隐因子数量为6时,模型在Pre@10和Re@10两个指标上的表现最好,表明此时模型的推荐效果较优。增加隐因子数量到8或10并未显著提升模型性能,反而有所下降。
#figure(
table(
columns: (auto, 1fr, 1fr),
inset: 10pt,
align: horizon,
[隐因子数量], [Pre@10], [Re@10],
"6", "0.5876", "0.5903",
"8", "0.5394", "0.5415",
"10","0.5669", "0.5689"
),
caption: "不同隐因子的Pre@10和Re@10"
)<table1>
== 与其他模型的对比
采用ALS与BPR模型作为baseline,与本模型进行对比。从@baseline 中可以观察到ALS(Alternating Least Squares)和BPR(Bayesian Personalized Ranking)两种模型在Precision@10和Recall@10指标上的表现。
#figure(image("images/baseline.png"),
caption: [
ALS与BPR模型的表现
],
)<baseline>
=== Pre@10比较
- 本模型:隐因子数量为6时的Pre@10最高,为0.5876;隐因子数量为10时,Pre@10也较高,为0.5669。
- ALS模型:Pre@10值较低,约为0.025。
- BPR模型:Pre@10值较高,约为0.04,但仍远低于本模型的表现。
=== Re@10比较
- 本模型:隐因子数量为6时的Re@10最高,为0.5903;隐因子数量为10时,Re@10也较高,为0.5689。
- ALS模型:Re@10值较低,约为0.01。
- BPR模型:Re@10值较高,约为0.022,但仍远低于本模型的表现。
从对比中可以明显看出,本模型在不同隐因子数量下的Pre@10和Re@10指标均显著高于ALS和BPR模型。这表明在推荐系统的前10个推荐项中,本模型的推荐准确性和召回率远高于ALS和BPR模型,特别是在隐因子数量为6和10时表现最佳。
= 实验总结
在本次实验中,我对比了不同推荐算法和模型在隐性反馈数据上的表现,重点评估了本模型、ALS(Alternating Least Squares)模型和BPR(Bayesian Personalized Ranking)模型在Precision@10和Recall@10两个指标上的表现。
从实验结果中可以明显看出,本模型在不同隐因子数量配置下的Precision@10和Recall@10指标均显著优于ALS和BPR模型。特别是在隐因子数量为6和10时,本模型的推荐准确性和召回率表现最佳。这表明本模型在处理隐性反馈数据时具有较强的优势,能够提供更为精准和全面的推荐结果。