基于物品的协同过滤
题目完成情况向量:
对于一道题目完成了为0,这样就不会出现在前k大里,不会推荐该题目,未完成为1。
题目相似矩阵:
题目相似矩阵采用基于余弦的相似度计算,构造一道题目的向量$i$为每个用户对该道题的时间权重和难度权重的相关占比和。计算两个向量的相似度可用余弦公式计算:
$$
sim(i,j) = cos(i,j) = \frac{i * j}{|i|*|j|}= \frac{\sum_(x_i \cdot y_i)}{\sqrt{\sum_x_i2} \cdot \sqrt{\sum_y_i^2}}
$$
可以计算出任意$(i,j)$的相似度,从而得到相似矩阵。
假设用户数为$n$,题目数为$m$,得到该矩阵是时间复杂度为$O(nm^2)$
推荐列表
推荐列表 = 题目完成情况向量 * 题目相似矩阵
矩阵乘法$O(m^2)$计算出推荐列表,已完成题目用题目完成情况向量筛去,对于指定用户选取推荐列表中前k大值进行推荐。
时间效率
假设每个用户选择25道题,有$n$个用户,那么最坏情况下有$25n$道题目,即$m=25n$,假设计算上限$107$次,$n \cdot (25n)2 \le 10^7$,即$n \le 25$,那么就选取最近有过AC记录的25位用户,每位用户选最近AC的25道题进行计算。
将协同过滤应用于userinfo.php
中的Recommend
将需要推荐的题目放在/OJ/userinfo.php
里的$hznu_recommend_set
变量中,该变量是一个array
大致的步骤为:
- 查询最近有提交记录的25个不同的用户(包括当前用户在内)
- 查询每个用户最近AC的25道不同的题目
- 对当前用户建立题目完成情况01行向量
- 对每道题目按时间权重$10 / log(n+1)$,计算权值,构造向量(倒数第n次正确提交,若没有提交则n为100)
- $O(m^2)$枚举题目,计算余弦值得到相似矩阵
- 题目完成情况行向量和相似矩阵相乘得到推荐列表
1.查询最近有提交记录的25个不同的用户
CREATE index `idx_solution_judgetime` ON solution(judgetime); -- 添加BTREE索引
SELECT DISTINCT `user_id` FROM `solution` WHERE `result` = 4 ORDER BY `judgetime` DESC LIMIT 25;
2.查询每个用户最近AC的25道不同的题目
SELECT DISTINCT `problem_id` FROM `solution` WHERE `user_id` = '$tmp_user_id' AND `result` = 4 ORDER BY `judgetime` LIMIT 25
3.对当前用户建立题目完成情况01行向量
SELECT count(*) FROM solution WHERE problem_id=$recommand_problems[$i] AND result = 4 AND user_id = '$user'
$solved_vector[$i] = 1;
if (intval($mysqli->query($sql_accepted)->fetch_array()[0]) > 0) $solved_vector[$i] = 0;
这样完成了记为0,未完成记为1,最后将该向量和推荐列表的每一项相乘就可筛去已经AC的题目。
4.对每道题目构造向量
构造方法为$10 / log(n+1)$(只考虑时间权重,还需要调整),存在一个二维数组中可以方便构造
$problems_vector = array();
$problems_vector[$row_problems['problem_id']][$users_cnt] = 10.0 / log($j + 2);
其中前一维代表题目,后一维代表用户,值代表该用户对该道题的权值。
5.计算相似矩阵
枚举$(i,j)$后用公式计算余弦值即可。
6.得到推荐列表
矩阵乘法,将已AC题目筛去。使用php的arsort
函数按值排序(键会跟着变),可以很方便的得到topk个结果。
相关代码
/* 基于物品的协同过滤推荐算法 */
$sql_users = "SELECT DISTINCT `user_id` FROM `solution` WHERE `result` = 4 ORDER BY `judgetime` DESC LIMIT 25";
$result_users = $mysqli->query($sql_users);
$recommend_vis = array();
$problems_vector = array(); // 每道题的向量
$users_cnt = 0;
$user_hasvis = 0;
for ($i = 0; $row_users=$result_users->fetch_array(); ++$i) {
$tmp_user_id = $row_users['user_id'];
$sql_problems = "SELECT DISTINCT `problem_id` FROM `solution` WHERE `user_id` = '$tmp_user_id' AND `result` = 4 ORDER BY `judgetime` DESC LIMIT 25";
$result_problems = $mysqli->query($sql_problems);
for ($j = 0; $row_problems=$result_problems->fetch_array(); ++$j) {
if ($row_problems['problem_id']) {
$recommend_vis[$row_problems['problem_id']] = 1;
$problems_vector[$row_problems['problem_id']][$users_cnt] = 10.0 / log($j + 2); // 以时间作为权重
}
}
if ($user == $tmp_user_id) $user_hasvis = 1;
$users_cnt++;
}
if ($user_hasvis == 0) {
$sql_problems = "SELECT DISTINCT `problem_id` FROM `solution` WHERE `user_id` = '$user' AND `result` = 4 ORDER BY `judgetime` DESC LIMIT 25";
$result_problems = $mysqli->query($sql_problems);
for ($j = 0; $row_problems=$result_problems->fetch_array(); ++$j) {
if ($row_problems['problem_id']) {
$recommend_vis[$row_problems['problem_id']] = 1;
$problems_vector[$row_problems['problem_id']][$users_cnt] = 10.0 / log($j + 2);
}
}
$users_cnt++;
}
$recommand_problems = array_keys($recommend_vis);
$problem_cnt = count($recommand_problems);
$solved_vector = array(); // 题目完成情况向量
for ($i = 0; $i < $problem_cnt; ++$i) {
$solved_vector[$i] = 1;
$sql_accepted = "SELECT count(*) FROM solution WHERE problem_id=$recommand_problems[$i] AND result = 4 AND user_id = '$user'";
if (intval($mysqli->query($sql_accepted)->fetch_array()[0]) > 0) $solved_vector[$i] = 0;
}
$sim_matrix = array(); // 题目相似矩阵
for ($i = 0; $i < $problem_cnt; ++$i) {
for ($j = $i; $j < $problem_cnt; ++$j) {
if ($i == $j) $sim_matrix[$i][$j] = 1.0;
else {
$fz = 0.0;
$fmx = 0.0;
$fmy = 0.0;
for ($k = 0; $k < $users_cnt; ++$k) {
$xi = $problems_vector[$recommand_problems[$i]][$k];
$yi = $problems_vector[$recommand_problems[$j]][$k];
$fz += $xi * $yi;
$fmx += $xi * $xi;
$fmy += $yi * $yi;
}
$fm = sqrt($fmx) + sqrt($fmy);
$ret = $fz / $fm; // 两个向量的余弦值用于判断相似度
$sim_matrix[$i][$j] = $ret;
$sim_matrix[$j][$i] = $ret;
}
}
}
$recommend_list = array(); // 推荐列表
for ($i = 0; $i < $problem_cnt; ++$i) {
$recommend_list[$i] = 0.0;
for ($j = 0; $j < $problem_cnt; ++$j)
$recommend_list[$i] += $solved_vector[$j] * $sim_matrix[$j][$i];
}
for ($i = 0; $i < $problem_cnt; $i++) $recommend_list[$i] = $recommend_list[$i] * $solved_vector[$i];
arsort($recommend_list);
$recommend_list_key = array_keys($recommend_list);
$topk = 10; // 推荐权值最大的k道题目
$recommend_cnt = 0;
for ($i = 0; $i < $problem_cnt && $recommend_cnt < $topk; ++$i) {
if ($recommend_list[$recommend_list_key[$i]] > 0)
$hznu_recommend_set[$recommend_cnt++] = $recommand_problems[$recommend_list_key[$i]];
else break;
}