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

[复制链接]
发表于 2025-9-18 06:24:22 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
代码部分
  1. package com.ruoyi.system.service.impl;
  2. import com.ruoyi.system.domain.Book;
  3. import com.ruoyi.system.domain.MyOrder;
  4. import com.ruoyi.system.mapper.BookMapper;
  5. import com.ruoyi.system.mapper.MyOrderMapper;
  6. import com.ruoyi.system.service.IBookRecommendService;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.data.redis.core.RedisTemplate;
  11. import org.springframework.scheduling.annotation.Scheduled;
  12. import org.springframework.stereotype.Service;
  13. import org.springframework.transaction.annotation.Transactional;
  14. import javax.annotation.PostConstruct;
  15. import java.util.*;
  16. import java.util.concurrent.TimeUnit;
  17. import java.util.stream.Collectors;
  18. @Service
  19. public class BookRecommendServiceImpl implements IBookRecommendService {
  20.     private static final Logger log = LoggerFactory.getLogger(BookRecommendServiceImpl.class);
  21.     @Autowired
  22.     private MyOrderMapper orderMapper;
  23.     @Autowired
  24.     private BookMapper bookMapper;
  25.     @Autowired
  26.     private RedisTemplate<String, Object> redisTemplate;
  27.     private static final String USER_SIMILARITY_KEY = "recommend:user:similarity";
  28.     private static final double SIMILARITY_THRESHOLD = 0.000001; // 相似度阈值
  29.     /**
  30.      * 应用启动时初始化推荐数据
  31.      */
  32.     @PostConstruct
  33.     public void init() {
  34.         log.info("检查推荐数据初始化状态...");
  35.         try {
  36.             if(!hasRecommendationData()) {
  37.                 log.info("未检测到推荐数据,开始初始化计算...");
  38.                 preComputeUserSimilarities();
  39.             } else {
  40.                 log.info("推荐数据已存在,跳过初始化计算");
  41.             }
  42.         } catch (Exception e) {
  43.             log.error("推荐数据初始化失败", e);
  44.         }
  45.     }
  46.     /**
  47.      * 检查是否存在推荐数据
  48.      */
  49.     private boolean hasRecommendationData() {
  50.         Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");
  51.         return keys != null && !keys.isEmpty();
  52.     }
  53.     @Override
  54.     @Transactional(readOnly = true)
  55.     public List<Book> recommendBooksByUserCF(Long userId, int limit) {
  56.         if (userId == null || limit <= 0) {
  57.             return Collections.emptyList();
  58.         }
  59.         try {
  60.             // 1. 从Redis获取用户相似度数据
  61.             Map<Object, Object> similarityScoresObj = redisTemplate.opsForHash()
  62.                     .entries(USER_SIMILARITY_KEY + ":" + userId);
  63.             if (similarityScoresObj == null || similarityScoresObj.isEmpty()) {
  64.                 log.debug("用户 {} 无相似用户数据", userId);
  65.                 return Collections.emptyList();
  66.             }
  67.             // 2. 转换数据类型
  68.             Map<Long, Double> similarityScores = convertSimilarityMap(similarityScoresObj);
  69.             // 3. 获取最相似的N个用户
  70.             List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);
  71.             if (similarUserIds.isEmpty()) {
  72.                 return Collections.emptyList();
  73.             }
  74.             // 4. 获取推荐图书
  75.             return generateRecommendations(userId, similarUserIds, limit);
  76.         } catch (Exception e) {
  77.             log.error("为用户 {} 生成推荐时发生错误", userId, e);
  78.             return Collections.emptyList();
  79.         }
  80.     }
  81.     /**
  82.      * 转换相似度Map数据类型
  83.      */
  84.     private Map<Long, Double> convertSimilarityMap(Map<Object, Object> rawMap) {
  85.         return rawMap.entrySet().stream()
  86.                 .collect(Collectors.toMap(
  87.                         e -> Long.parseLong(e.getKey().toString()),
  88.                         e -> Double.parseDouble(e.getValue().toString())
  89.                 ));
  90.     }
  91.     /**
  92.      * 获取最相似的用户ID列表
  93.      */
  94.     private List<Long> getTopSimilarUsers(Map<Long, Double> similarityScores, int topN) {
  95.         return similarityScores.entrySet().stream()
  96.                 .filter(e -> e.getValue() >= SIMILARITY_THRESHOLD)
  97.                 .sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
  98.                 .limit(topN)
  99.                 .map(Map.Entry::getKey)
  100.                 .collect(Collectors.toList());
  101.     }
  102.     /**
  103.      * 生成推荐图书列表
  104.      */
  105.     private List<Book> generateRecommendations(Long targetUserId, List<Long> similarUserIds, int limit) {
  106.         // 1. 获取相似用户订单
  107.         List<MyOrder> similarUserOrders = orderMapper.selectCompletedOrdersByUserIds(similarUserIds);
  108.         // 2. 获取目标用户已购图书
  109.         Set<Long> purchasedBooks = getPurchasedBooks(targetUserId);
  110.         // 3. 计算图书推荐分数
  111.         Map<Long, Double> bookScores = calculateBookScores(similarUserOrders, purchasedBooks);
  112.         // 4. 获取推荐图书
  113.         return getTopRecommendedBooks(bookScores, limit);
  114.     }
  115.     /**
  116.      * 获取用户已购图书ID集合
  117.      */
  118.     private Set<Long> getPurchasedBooks(Long userId) {
  119.         List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);
  120.         if (orders == null || orders.isEmpty()) {
  121.             return Collections.emptySet();
  122.         }
  123.         return orders.stream()
  124.                 .map(order -> order.getBookId())
  125.                 .collect(Collectors.toSet());
  126.     }
  127.     /**
  128.      * 计算图书推荐分数
  129.      */
  130.     private Map<Long, Double> calculateBookScores(List<MyOrder> similarUserOrders, Set<Long> purchasedBooks) {
  131.         Map<Long, Double> bookScores = new HashMap<>();
  132.         for (MyOrder order : similarUserOrders) {
  133.             Long bookId = order.getBookId();
  134.             if (!purchasedBooks.contains(bookId)) {
  135.                 bookScores.merge(bookId, (double) order.getQuantity(), Double::sum);
  136.             }
  137.         }
  138.         return bookScores;
  139.     }
  140.     /**
  141.      * 获取评分最高的推荐图书
  142.      */
  143.     private List<Book> getTopRecommendedBooks(Map<Long, Double> bookScores, int limit) {
  144.         if (bookScores.isEmpty()) {
  145.             return Collections.emptyList();
  146.         }
  147.         List<Long> recommendedBookIds = bookScores.entrySet().stream()
  148.                 .sorted(Map.Entry.<Long, Double>comparingByValue().reversed())
  149.                 .limit(limit)
  150.                 .map(Map.Entry::getKey)
  151.                 .collect(Collectors.toList());
  152.         return bookMapper.selectBookByIds(recommendedBookIds);
  153.     }
  154.     @Override
  155.     @Transactional
  156.     public void preComputeUserSimilarities() {
  157.         log.info("开始计算用户相似度矩阵...");
  158.         long startTime = System.currentTimeMillis();
  159.         try {
  160.             // 1. 清空旧数据
  161.             clearExistingSimilarityData();
  162.             // 2. 获取所有用户ID(有完成订单的)
  163.             List<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();
  164.             log.info("找到{}个有订单的用户", userIds.size());
  165.             if (userIds.isEmpty()) {
  166.                 log.warn("没有找到任何用户订单数据!");
  167.                 return;
  168.             }
  169.             // 3. 构建用户-图书评分矩阵
  170.             Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);
  171.             // 4. 计算并存储相似度
  172.             computeAndStoreSimilarities(userIds, ratingMatrix);
  173.             long duration = (System.currentTimeMillis() - startTime) / 1000;
  174.             log.info("用户相似度矩阵计算完成,耗时{}秒", duration);
  175.         } catch (Exception e) {
  176.             log.error("计算用户相似度矩阵失败", e);
  177.             throw e;
  178.         }
  179.     }
  180.     /**
  181.      * 清空现有相似度数据
  182.      */
  183.     private void clearExistingSimilarityData() {
  184.         Set<String> keys = redisTemplate.keys(USER_SIMILARITY_KEY + ":*");
  185.         if (keys != null && !keys.isEmpty()) {
  186.             redisTemplate.delete(keys);
  187.             log.info("已清除{}个旧的用户相似度记录", keys.size());
  188.         }
  189.     }
  190.     /**
  191.      * 构建用户-图书评分矩阵
  192.      */
  193.     private Map<Long, Map<Long, Integer>> buildRatingMatrix(List<Long> userIds) {
  194.         Map<Long, Map<Long, Integer>> ratingMatrix = new HashMap<>();
  195.         for (Long userId : userIds) {
  196.             List<MyOrder> orders = orderMapper.selectCompletedOrdersByUserId(userId);
  197.             if (orders == null || orders.isEmpty()) {
  198.                 continue;
  199.             }
  200.             Map<Long, Integer> userRatings = new HashMap<>();
  201.             for (MyOrder order : orders) {
  202.                 if (order == null || order.getBookId() == null) {
  203.                     continue;
  204.                 }
  205.                 Long bookId = order.getBookId();
  206.                 Integer quantity = Math.toIntExact(order.getQuantity() != null ? order.getQuantity() : 0);
  207.                 userRatings.merge(bookId, quantity, (oldVal, newVal) -> oldVal + newVal);
  208.             }
  209.             ratingMatrix.put(userId, userRatings);
  210.         }
  211.         return ratingMatrix;
  212.     }
  213.     /**
  214.      * 计算并存储用户相似度
  215.      */
  216.     private void computeAndStoreSimilarities(List<Long> userIds, Map<Long, Map<Long, Integer>> ratingMatrix) {
  217.         int computedPairs = 0;
  218.         for (int i = 0; i < userIds.size(); i++) {
  219.             Long userId1 = userIds.get(i);
  220.             Map<Long, Integer> ratings1 = ratingMatrix.get(userId1);
  221.             Map<String, String> similarities = new HashMap<>();
  222.             // 只计算后续用户,避免重复计算
  223.             for (int j = i + 1; j < userIds.size(); j++) {
  224.                 Long userId2 = userIds.get(j);
  225.                 Map<Long, Integer> ratings2 = ratingMatrix.get(userId2);
  226.                 double similarity = computeCosineSimilarity(ratings1, ratings2);
  227.                 if (similarity >= SIMILARITY_THRESHOLD) {
  228.                     similarities.put(userId2.toString(), String.valueOf(similarity));
  229.                     computedPairs++;
  230.                 }
  231.             }
  232.             if (!similarities.isEmpty()) {
  233.                 String key = USER_SIMILARITY_KEY + ":" + userId1;
  234.                 redisTemplate.opsForHash().putAll(key, similarities);
  235.                 redisTemplate.expire(key, 7, TimeUnit.DAYS);
  236.             }
  237.             // 定期打印进度
  238.             if (i % 100 == 0 || i == userIds.size() - 1) {
  239.                 log.info("已处理 {}/{} 用户", i + 1, userIds.size());
  240.             }
  241.         }
  242.         log.info("共计算{}对用户相似关系", computedPairs);
  243.     }
  244.     /**
  245.      * 计算余弦相似度
  246.      */
  247.     private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {
  248.         // 获取共同评价的图书
  249.         Set<Long> commonBooks = new HashSet<>(ratings1.keySet());
  250.         commonBooks.retainAll(ratings2.keySet());
  251.         if (commonBooks.isEmpty()) {
  252.             return 0.0;
  253.         }
  254.         // 计算点积和范数
  255.         double dotProduct = 0.0;
  256.         double norm1 = 0.0;
  257.         double norm2 = 0.0;
  258.         // 计算共同图书的部分
  259.         for (Long bookId : commonBooks) {
  260.             int r1 = ratings1.get(bookId);
  261.             int r2 = ratings2.get(bookId);
  262.             dotProduct += r1 * r2;
  263.             norm1 += r1 * r1;
  264.             norm2 += r2 * r2;
  265.         }
  266.         // 计算各自独有的部分
  267.         norm1 += ratings1.keySet().stream()
  268.                 .filter(bookId -> !commonBooks.contains(bookId))
  269.                 .mapToDouble(bookId -> Math.pow(ratings1.get(bookId), 2))
  270.                 .sum();
  271.         norm2 += ratings2.keySet().stream()
  272.                 .filter(bookId -> !commonBooks.contains(bookId))
  273.                 .mapToDouble(bookId -> Math.pow(ratings2.get(bookId), 2))
  274.                 .sum();
  275.         if (norm1 == 0 || norm2 == 0) {
  276.             return 0.0;}
  277.         return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
  278.     }
  279.     /**
  280.      * 定时任务:每天凌晨3点更新相似度矩阵
  281.      */
  282.     @Scheduled(cron = "0 0 3 * * ?")
  283.     public void scheduledSimilarityUpdate() {
  284.         log.info("定时任务:开始更新用户相似度矩阵...");
  285.         try {
  286.             preComputeUserSimilarities();
  287.             log.info("定时任务:用户相似度矩阵更新完成");
  288.         } catch (Exception e) {
  289.             log.error("定时任务:更新用户相似度矩阵失败", e);
  290.         }
  291.     }
  292. }
复制代码
协同过滤解析

协同过滤保举算法原理与实现分析

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

协同过滤(CF)是保举体系中最经典的算法之一,核心思想是:
“相似用户喜好的物品,你也大概喜好”
1.1 两种主要范例


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

2.1 团体流程


  • 离线盘算阶段:

    • 构建用户-物品评分矩阵
    • 盘算并存储用户相似度

  • 在线保举阶段:

    • 查找相似用户
    • 基于相似用户的偏好天生保举

2.2 核心代码解析

(1) 离线盘算阶段 (preComputeUserSimilarities)
  1. public void preComputeUserSimilarities() {
  2.     // 1. 清空旧数据
  3.     clearExistingSimilarityData();
  4.    
  5.     // 2. 获取所有用户ID
  6.     List<Long> userIds = orderMapper.selectAllUserIdsWithCompletedOrders();
  7.    
  8.     // 3. 构建评分矩阵
  9.     Map<Long, Map<Long, Integer>> ratingMatrix = buildRatingMatrix(userIds);
  10.    
  11.     // 4. 计算并存储相似度
  12.     computeAndStoreSimilarities(userIds, ratingMatrix);
  13. }
复制代码
评分矩阵构建:

  • 用户为行,图书为列
  • 值为购买数目(作为评分)
相似度盘算:

  • 利用余弦相似度(Cosine Similarity)
  • 只存储相似度高于阈值(SIMILARITY_THRESHOLD)的关系
(2) 相似度盘算 (computeCosineSimilarity)
  1. private double computeCosineSimilarity(Map<Long, Integer> ratings1, Map<Long, Integer> ratings2) {
  2.     // 获取共同评价的图书
  3.     Set<Long> commonBooks = new HashSet<>(ratings1.keySet());
  4.     commonBooks.retainAll(ratings2.keySet());
  5.    
  6.     // 计算点积和范数
  7.     double dotProduct = 0.0;
  8.     double norm1 = 0.0;
  9.     double norm2 = 0.0;
  10.    
  11.     // 计算余弦相似度
  12.     return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
  13. }
复制代码
余弦相似度公式:
  1. similarity = (A·B) / (||A|| * ||B||)
复制代码
(3) 在线保举阶段 (recommendBooksByUserCF)
  1. public List<Book> recommendBooksByUserCF(Long userId, int limit) {
  2.     // 1. 获取用户相似度数据
  3.     Map<Long, Double> similarityScores = getSimilarityScores(userId);
  4.    
  5.     // 2. 获取最相似的N个用户
  6.     List<Long> similarUserIds = getTopSimilarUsers(similarityScores, 10);
  7.    
  8.     // 3. 生成推荐
  9.     return generateRecommendations(userId, similarUserIds, limit);
  10. }
复制代码
3. 关键技术点

3.1 数据存储优化


  • Redis存储:用户相似度矩阵存储在Redis中,键格式为recommend:user:similarity:{userId}
  • 定期更新:通过@Scheduled天天破晓3点更新
3.2 性能优化


  • 只盘算上三角矩阵:避免重复盘算(A-B和B-A的相似度雷同)
  • 阈值过滤:只存储故意义的相似关系(SIMILARITY_THRESHOLD)
  • 分批处置惩罚:每处置惩罚100个用户打印进度
3.3 保举天生逻辑


  • 清除用户已购买的图书
  • 根据相似用户的购买记录加权盘算保举分数
  • 按分数排序返回TopN保举
4. 协同过滤的优缺点

长处:


  • 不必要物品内容信息:仅依靠用户举动数据
  • 能发现潜伏爱好:大概保举出意想不到的物品
  • 表明性强:可以告诉用户"由于和你相似的用户也喜好"
缺点:


  • 冷启动题目:新用户/新物品缺乏数据
  • 希奇性题目:用户-物品矩阵通常非常希奇
  • 盘算复杂度高:用户增长时相似度盘算本钱呈平方级增长
5. 代码中的工程化实践


  • 定时使命:@Scheduled定期更新相似度矩阵
  • 启动初始化:@PostConstruct检查数据是否必要初始化
  • 非常处置惩罚:美满的try-catch和日记记录
  • 性能监控监控:记录关键步调的实行时间
  • 资源清算:盘算前先打扫旧数据
这个实现是一个典范的生产级协同过滤保举体系,均衡了算法效果和工程实践,得当中等规模的电商平台利用。对于更大规模的体系,大概必要思量更复杂的优化如分布式盘算、近似算法等。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表