图书推荐(协同过滤)算法的实现:基于订单购买实现相似用户的图书推荐

代码部分

package com.ruoyi.system.service.impl;import com.ruoyi.system.domain.Book;
import com.ruoyi.system.domain.MyOrder;
import com.ruoyi.system.mapper.BookMapper;
import com.ruoyi.system.mapper.MyOrderMapper;
import com.ruoyi.system.service.IBookRecommendService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;@Service
public class BookRecommendServiceImpl implements IBookRecommendService {private static final Logger log = LoggerFactory.getLogger(BookRecommendServiceImpl.class);@Autowiredprivate MyOrderMapper orderMapper;@Autowiredprivate BookMapper bookMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String USER_SIMILARITY_KEY = "recommend:user:similarity";private static final double SIMILARITY_THRESHOLD = 0.000001; // 相似度阈值/*** 应用启动时初始化推荐数据*/@PostConstructpublic void init() {log.info("检查推荐数据初始化状态...");try {if(!hasRecommendationData()) {log.info("未检测到推荐数据,开始初始化计算...");preComputeUserSimilarities();} else {log.info("推荐数据已存在,跳过初始化计算");}} catch (Exception e) {log.error("推荐数据初始化失败", e);}}/*** 检查是否存在推荐数据*/private boolean hasRecommendationData() {Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");return keys != null && !keys.isEmpty();}@Override@Transactional(readOnly = true)public List<Book> recommendBooksByUserCF(Long userId, int limit) {if (userId == null || limit <= 0) {return Collections.emptyList();}try {// 1. 从Redis获取用户相似度数据Map<Object, Object> similarityScoresObj = redisTemplate.opsForHash().entries(USER_SIMILARITY_KEY + ":" + userId);if (similarityScoresObj == null || similarityScoresObj.isEmpty()) {log.debug("用户 {} 无相似用户数据", userId);return Collections.emptyList();}// 2. 转换数据类型Map<Long, Double> similarityScores = convertSimilarityMap(similarityScoresObj);// 3. 获取最相似的N个用户List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);if (similarUserIds.isEmpty()) {return Collections.emptyList();}// 4. 获取推荐图书return generateRecommendations(userId, similarUserIds, limit);} catch (Exception e) {log.error("为用户 {} 生成推荐时发生错误", userId, e);return Collections.emptyList();}}/*** 转换相似度Map数据类型*/private Map<Long, Double> convertSimilarityMap(Map<Object, Object> rawMap) {return rawMap.entrySet().stream().collect(Collectors.toMap(e -> Long.parseLong(e.getKey().toString()),e -> Double.parseDouble(e.getValue().toString())));}/*** 获取最相似的用户ID列表*/private List<Long> getTopSimilarUsers(Map<Long, Double> similarityScores, int topN) {return similarityScores.entrySet().stream().filter(e -> e.getValue() >= SIMILARITY_THRESHOLD).sorted(Map.Entry.<Long, Double>comparingByValue().reversed()).limit(topN).map(Map.Entry::getKey).collect(Collectors.toList());}/*** 生成推荐图书列表*/private List<Book> generateRecommendations(Long targetUserId, List<Long> similarUserIds, int limit) {// 1. 获取相似用户订单List<MyOrder> similarUserOrders = orderMapper.selectCompletedOrdersByUserIds(similarUserIds);// 2. 获取目标用户已购图书Set<Long> purchasedBooks = getPurchasedBooks(targetUserId);// 3. 计算图书推荐分数Map<Long, Double> bookScores = calculateBookScores(similarUserOrders, purchasedBooks);// 4. 获取推荐图书return getTopRecommendedBooks(bookScores, limit);}/*** 获取用户已购图书ID集合*/private Set<Long> getPurchasedBooks(Long userId) {List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);if (orders == null || orders.isEmpty()) {return Collections.emptySet();}return orders.stream().map(order -> order.getBookId()).collect(Collectors.toSet());}/*** 计算图书推荐分数*/private Map<Long, Double> calculateBookScores(List<MyOrder> similarUserOrders, Set<Long> purchasedBooks) {Map<Long, Double> bookScores = new HashMap<>();for (MyOrder order : similarUserOrders) {Long bookId = order.getBookId();if (!purchasedBooks.contains(bookId)) {bookScores.merge(bookId, (double) order.getQuantity(), Double::sum);}}return bookScores;}/*** 获取评分最高的推荐图书*/private List<Book> getTopRecommendedBooks(Map<Long, Double> bookScores, int limit) {if (bookScores.isEmpty()) {return Collections.emptyList();}List<Long> recommendedBookIds = bookScores.entrySet().stream().sorted(Map.Entry.<Long, Double>comparingByValue().reversed()).limit(limit).map(Map.Entry::getKey).collect(Collectors.toList());return bookMapper.selectBookByIds(recommendedBookIds);}@Override@Transactionalpublic void preComputeUserSimilarities() {log.info("开始计算用户相似度矩阵...");long startTime = System.currentTimeMillis();try {// 1. 清空旧数据clearExistingSimilarityData();// 2. 获取所有用户ID(有完成订单的)List<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();log.info("找到{}个有订单的用户", userIds.size());if (userIds.isEmpty()) {log.warn("没有找到任何用户订单数据!");return;}// 3. 构建用户-图书评分矩阵Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);// 4. 计算并存储相似度computeAndStoreSimilarities(userIds, ratingMatrix);long duration = (System.currentTimeMillis() - startTime) / 1000;log.info("用户相似度矩阵计算完成,耗时{}秒", duration);} catch (Exception e) {log.error("计算用户相似度矩阵失败", e);throw e;}}/*** 清空现有相似度数据*/private void clearExistingSimilarityData() {Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);log.info("已清除{}个旧的用户相似度记录", keys.size());}}/*** 构建用户-图书评分矩阵*/private Map<Long, Map<Long, Integer>> buildRatingMatrix(List<Long> userIds) {Map<Long, Map<Long, Integer>> ratingMatrix = new HashMap<>();for (Long userId : userIds) {List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);if (orders == null || orders.isEmpty()) {continue;}Map<Long, Integer> userRatings = new HashMap<>();for (MyOrder order : orders) {if (order == null || order.getBookId() == null) {continue;}Long bookId = order.getBookId();Integer quantity = Math.toIntExact(order.getQuantity() != null ? order.getQuantity() : 0);userRatings.merge(bookId, quantity, (oldVal, newVal) -> oldVal + newVal);}ratingMatrix.put(userId, userRatings);}return ratingMatrix;}/*** 计算并存储用户相似度*/private void computeAndStoreSimilarities(List<Long> userIds, Map<Long, Map<Long, Integer>> ratingMatrix) {int computedPairs = 0;for (int i = 0; i < userIds.size(); i++) {Long userId1 = userIds.get(i);Map<Long, Integer> ratings1 = ratingMatrix.get(userId1);Map<String, String> similarities = new HashMap<>();// 只计算后续用户,避免重复计算for (int j = i + 1; j < userIds.size(); j++) {Long userId2 = userIds.get(j);Map<Long, Integer> ratings2 = ratingMatrix.get(userId2);double similarity = computeCosineSimilarity(ratings1, ratings2);if (similarity >= SIMILARITY_THRESHOLD) {similarities.put(userId2.toString(), String.valueOf(similarity));computedPairs++;}}if (!similarities.isEmpty()) {String key = USER_SIMILARITY_KEY + ":" + userId1;redisTemplate.opsForHash().putAll(key, similarities);redisTemplate.expire(key, 7, TimeUnit.DAYS);}// 定期打印进度if (i % 100 == 0 || i == userIds.size() - 1) {log.info("已处理 {}/{} 用户", i + 1, userIds.size());}}log.info("共计算{}对用户相似关系", computedPairs);}/*** 计算余弦相似度*/private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {// 获取共同评价的图书Set<Long> commonBooks = new HashSet<>(ratings1.keySet());commonBooks.retainAll(ratings2.keySet());if (commonBooks.isEmpty()) {return 0.0;}// 计算点积和范数double dotProduct = 0.0;double norm1 = 0.0;double norm2 = 0.0;// 计算共同图书的部分for (Long bookId : commonBooks) {int r1 = ratings1.get(bookId);int r2 = ratings2.get(bookId);dotProduct += r1 * r2;norm1 += r1 * r1;norm2 += r2 * r2;}// 计算各自独有的部分norm1 += ratings1.keySet().stream().filter(bookId -> !commonBooks.contains(bookId)).mapToDouble(bookId -> Math.pow(ratings1.get(bookId), 2)).sum();norm2 += ratings2.keySet().stream().filter(bookId -> !commonBooks.contains(bookId)).mapToDouble(bookId -> Math.pow(ratings2.get(bookId), 2)).sum();if (norm1 == 0 || norm2 == 0) {return 0.0;}return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));}/*** 定时任务:每天凌晨3点更新相似度矩阵*/@Scheduled(cron = "0 0 3 * * ?")public void scheduledSimilarityUpdate() {log.info("定时任务:开始更新用户相似度矩阵...");try {preComputeUserSimilarities();log.info("定时任务:用户相似度矩阵更新完成");} catch (Exception e) {log.error("定时任务:更新用户相似度矩阵失败", e);}}
}

协同过滤解析

协同过滤推荐算法原理与实现分析

这个代码实现了一个基于用户的协同过滤(User-based Collaborative Filtering)推荐系统,下面我将结合代码详细解析协同过滤算法的原理和实现方式。

1. 协同过滤基本概念

协同过滤(CF)是推荐系统中最经典的算法之一,核心思想是:
“相似用户喜欢的物品,你也可能喜欢”

1.1 两种主要类型

  • 基于用户的协同过滤(User-CF):代码中实现的就是这种
  • 基于物品的协同过滤(Item-CF)

2. 代码实现的User-CF流程

2.1 整体流程

  1. 离线计算阶段

    • 构建用户-物品评分矩阵
    • 计算并存储用户相似度
  2. 在线推荐阶段

    • 查找相似用户
    • 基于相似用户的偏好生成推荐

2.2 核心代码解析

(1) 离线计算阶段 (preComputeUserSimilarities)
public void preComputeUserSimilarities() {// 1. 清空旧数据clearExistingSimilarityData();// 2. 获取所有用户IDList<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();// 3. 构建评分矩阵Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);// 4. 计算并存储相似度computeAndStoreSimilarities(userIds, ratingMatrix);
}

评分矩阵构建

  • 用户为行,图书为列
  • 值为购买数量(作为评分)

相似度计算

  • 使用余弦相似度(Cosine Similarity)
  • 只存储相似度高于阈值(SIMILARITY_THRESHOLD)的关系
(2) 相似度计算 (computeCosineSimilarity)
private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {// 获取共同评价的图书Set<Long> commonBooks = new HashSet<>(ratings1.keySet());commonBooks.retainAll(ratings2.keySet());// 计算点积和范数double dotProduct = 0.0;double norm1 = 0.0;double norm2 = 0.0;// 计算余弦相似度return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}

余弦相似度公式:

similarity = (A·B) / (||A|| * ||B||)
(3) 在线推荐阶段 (recommendBooksByUserCF)
public List<Book> recommendBooksByUserCF(Long userId, int limit) {// 1. 获取用户相似度数据Map<Long, Double> similarityScores = getSimilarityScores(userId);// 2. 获取最相似的N个用户List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);// 3. 生成推荐return generateRecommendations(userId, similarUserIds, limit);
}

3. 关键技术点

3.1 数据存储优化

  • Redis存储:用户相似度矩阵存储在Redis中,键格式为recommend:user:similarity:{userId}
  • 定期更新:通过@Scheduled每天凌晨3点更新

3.2 性能优化

  • 只计算上三角矩阵:避免重复计算(A-B和B-A的相似度相同)
  • 阈值过滤:只存储有意义的相似关系(SIMILARITY_THRESHOLD)
  • 分批处理:每处理100个用户打印进度

3.3 推荐生成逻辑

  1. 排除用户已购买的图书
  2. 根据相似用户的购买记录加权计算推荐分数
  3. 按分数排序返回TopN推荐

4. 协同过滤的优缺点

优点:

  • 不需要物品内容信息:仅依赖用户行为数据
  • 能发现潜在兴趣:可能推荐出意想不到的物品
  • 解释性强:可以告诉用户"因为和你相似的用户也喜欢"

缺点:

  • 冷启动问题:新用户/新物品缺乏数据
  • 稀疏性问题:用户-物品矩阵通常非常稀疏
  • 计算复杂度高:用户增长时相似度计算成本呈平方级增长

5. 代码中的工程化实践

  1. 定时任务@Scheduled定期更新相似度矩阵
  2. 启动初始化@PostConstruct检查数据是否需要初始化
  3. 异常处理:完善的try-catch和日志记录
  4. 性能监控:记录关键步骤的执行时间
  5. 资源清理:计算前先清除旧数据

这个实现是一个典型的生产级协同过滤推荐系统,平衡了算法效果和工程实践,适合中等规模的电商平台使用。对于更大规模的系统,可能需要考虑更复杂的优化如分布式计算、近似算法等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/904712.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JMeter快速指南:命令行生成HTML测试报告(附样例命令解析)

一、核心命令解析 jmeter -g Dash_CapacityTest_01_AllModules_1000.jtl -o report/ 参数 作用 示例文件说明 -g 指定.jtl结果文件路径 -o 指定报告输出目录 自动创建report文件夹 二、操作步骤&#xff08;Windows/Linux/Mac通用&#xff09; 进入JMe…

2025年渗透测试面试题总结-渗透岗位全职工作面试(附回答)(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 一、通用基础类问题 1. 自我介绍 2. 职业动机与规划 3. 加班/出差接受度 二、安全技术类问题 1. 漏…

使用DEEPSEEK快速修改QT创建的GUI

QT的GUI&#xff0c;本质上是使用XML进行描述的&#xff0c;在QT CREATOR的界面编辑处&#xff0c;按CTRL2 切换到代码视图&#xff0c;CTRL3切换到编辑器视图。 CTRL2 切换到代码视图 CTRL3 切换到编辑器视图 鼠标左键点击代码视图中&#xff0c;按CTRLA → CTRLC复制XML代码…

draw.io流程图使用笔记

文章目录 图形较少的问题安装版好还是非安装版好业务系统嵌入的draw.io如何导入呢?如何判断组合和取消组合如何快速选中框里面的内容有时候选不到文本怎么办连接线如何不走直角 航点和取消航点支持多少种图形多个连接点?多个图形对齐双向箭头如何画图形的大小 其他流程图图标…

音频相关基础知识

主要参考&#xff1a; 音频基本概念_音频和音调的关系-CSDN博客 音频相关基础知识&#xff08;采样率、位深度、通道数、PCM、AAC&#xff09;_音频2通道和8ch的区别-CSDN博客 概述 声音的本质 声音的本质是波在介质中的传播现象&#xff0c;声波的本质是一种波&#xff0c;是一…

MySQL中隔离级别那点事

引言 在MySQL中&#xff0c;事务隔离级别和二进制日志&#xff08;binlog&#xff09;的格式密切相关&#xff0c;直接影响数据的一致性和复制的正确性。尤其是在“已提交读”&#xff08;Read Committed&#xff09;隔离级别下&#xff0c;由于没有使用间隙锁&#xff0c;某些…

LeetCode 热题 100 238. 除自身以外数组的乘积

LeetCode 热题 100 | 238. 除自身以外数组的乘积 大家好&#xff0c;今天我们来解决一道经典的算法问题——除自身以外数组的乘积。这道题在 LeetCode 上被标记为中等难度&#xff0c;要求在不使用除法的情况下&#xff0c;计算数组中每个元素的乘积&#xff0c;其中每个元素的…

【网络编程】三、TCP网络套接字编程

文章目录 TCP通信流程Ⅰ. 服务器日志类实现Ⅱ. TCP服务端1、服务器创建流程2、创建套接字 -- socket3、绑定服务器 -- bind&#x1f38f;4、服务器监听 -- listen&#x1f38f;5、获取客户端连接请求 -- acceptaccept函数返回的套接字描述符是什么&#xff0c;不是已经有一个了…

STM32的SysTick

SysTick介绍 定义&#xff1a;Systick&#xff0c;即滴答定时器&#xff0c;是内核中的一个特殊定时器&#xff0c;用于提供系统级的定时服务。该定时器是一个24位的递减计数器&#xff0c;具有自动重载值寄存器的功能。当计数器到达自动重载值时&#xff0c;它会自动重新加载…

【Java项目脚手架系列】第一篇:Maven基础项目脚手架

【Java项目脚手架系列】第一篇:Maven基础项目脚手架 前言 在Java开发中,一个好的项目脚手架可以大大提高开发效率,减少重复工作。本系列文章将介绍各种常用的Java项目脚手架,帮助开发者快速搭建项目。今天,我们先从最基础的Maven项目脚手架开始。 什么是项目脚手架? …

Kafka的消息保留策略是怎样的? (基于时间log.retention.hours或大小log.retention.bytes,可配置删除或压缩策略)

Kafka 消息保留策略详解 1. 核心保留机制 # Broker 基础配置示例&#xff08;server.properties&#xff09; log.retention.hours168 # 默认7天保留时间 log.retention.bytes1073741824 # 1GB 大小限制2. 策略类型对比 策略类型配置参数执行逻辑适用场景时间删除log.re…

五一の自言自语 2025/5/5

今天开学了&#xff0c;感觉还没玩够。 假期做了很多事&#xff0c;弄了好几天的路由器、监控、录像机&#xff0c;然后不停的出现问题&#xff0c;然后问ai&#xff0c;然后解决问题。这次假期的实践&#xff0c;更像是计算机网络的实验&#xff0c;把那些交换机&#xff0c;…

安卓基础(静态方法)

静态方法的特点​​ ​​无需实例化​​&#xff1a;直接用 类名.方法名() 调用。 ​​不能访问实例成员​​&#xff1a;只能访问类的静态变量或静态方法。 ​​内存中只有一份​​&#xff1a;随类加载而初始化&#xff0c;生命周期与类相同。 // 工具类 MathUtils publi…

EasyRTC嵌入式音视频通话SDK驱动智能硬件音视频应用新发展

一、引言 在数字化浪潮下&#xff0c;智能硬件蓬勃发展&#xff0c;从智能家居到工业物联网&#xff0c;深刻改变人们的生活与工作。音视频通讯作为智能硬件交互与协同的核心&#xff0c;重要性不言而喻。但嵌入式设备硬件资源受限&#xff0c;传统音视频方案集成困难。EasyRT…

《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》封面颜色空间一图的选图历程

禹晶、肖创柏、廖庆敏《数字图像处理&#xff08;面向新工科的电工电子信息基础课程系列教材&#xff09;》 学图像处理的都知道&#xff0c;彩色图像的颜色空间很多&#xff0c;而且又是三维&#xff0c;不同的角度有不同的视觉效果&#xff0c;MATLAB的图又有有box和没有box。…

Flutter 异步原理-Zone

前言 Zone 是 Dart 异步模型中的核心机制&#xff0c;主要用于&#xff1a; 隔离异步上下文&#xff0c;形成逻辑上的执行环境。捕获未处理的异步异常&#xff0c;保证系统稳定。自定义异步任务的调度行为&#xff08;比如微任务、Timer&#xff09;。 什么是 Zone&#xff1…

聊一聊自然语言处理在人工智能领域中的应用

目录 一、智能交互与对话系统 二、 信息提取与文本分析 三、机器翻译与跨语言应用 四、内容生成与创作辅助 五、 搜索与推荐系统 六、垂直领域的专业应用 七、关键技术支撑 自然语言处理NLP属于AI的一个子领域&#xff0c;专注于让机器理解和生成人类语言&#xff0c;比…

Redis的过期设置和策略

Redis设置过期时间主要有以下几个配置方式 expire key seconds 设置key在多少秒之后过期pexpire key milliseconds 设置key在多少毫秒之后过期expireat key timestamp 设置key在具体某个时间戳&#xff08;timestamp:时间戳 精确到秒&#xff09;过期pexpireat key millisecon…

vite:npm 安装 pdfjs-dist , PDF.js View 预览功能示例

pdfjs-dist 是 Mozilla 的 PDF.js 库的预构建版本&#xff0c;能让你在项目里展示 PDF 文件。下面为你介绍如何用 npm 安装 pdfjs-dist 并应用 pdf.js 和 pdf.worker.js。 为了方便&#xff0c;我将使用 vite 搭建一个原生 js 项目。 1.创建项目 npm create vitelatest pdf-v…

【Android】动画原理解析

一,基础动画 基础动画,有四种,分别是平移(Translate)、缩放(Scale)、Rorate(旋转)、Alpha(透明度),对应Android中以下四种。 1,Animation基类 1,基本概念 1,插值器 插值器的作用,是控制动画过程的参数,可以理解为 时间(t)与动画进程(d)的函数,动画仅…