简介:基于Java与协同过滤算法实现的购物电商系统源码,面向需要完成毕业设计、课程设计或期末大作业的Java Web学习者,适合掌握Java基础、希望了解推荐系统落地的初中级读者。压缩包共1249个文件,涵盖270个html页面、172个css样式、153个js脚本等前端资源,79个java类、22个jsp页面、26个xml配置及1个sql数据库脚本等后端代码,整体约77.96MB,目录结构按功能模块划分,便于对照学习。已有265人学习下载,项目已通过老师指导与评审,可作高分毕设参考。项目完整实现商品浏览、购物车与协同过滤推荐等模块,包含用户行为数据建模、相似度计算与推荐结果展示等关键流程;同时附有演示视频、架构设计图及上传处理、JSON配置等辅助文件,帮助读者快速从代码层面理解电商推荐系统的工程组织与算法实现。
1. 基于 Java 与协同过滤算法的购物电商系统源码:毕业设计里的商品推荐怎么落地
一份基于 Java 和协同过滤算法实现的购物电商系统源码,拿到的第一时间我就直接跑了一遍完整链路:用户登录、浏览商品、下单支付,后台离线计算相似度矩阵,再到首页“猜你喜欢”按 Top-N 给出商品推荐。这套流程覆盖了电商系统最常见的前后台闭环,也正好是许多 Java 毕业设计最容易被问倒的地方——推荐结果到底是怎么算出来的。对正在赶毕业设计和期末大作业的人来说,这份源码的价值在于协同过滤算法不是玩具 demo,而是嵌在真实订单表和商品表上的完整实现;对想快速了解推荐系统工程落地的 Java 工程师,它同样能作为一份简洁的参考骨架。
2. 协同过滤算法的选型逻辑:用户-物品矩阵与三种相似度计算怎么定
2.1 基于用户的协同过滤:从“和你像的人”推导商品
协同过滤算法的核心假设很朴素:相似的人会买相似的商品。基于用户的协同过滤(User-Based Collaborative Filtering)把这句话直接变成代码逻辑:先找到与当前用户历史行为最接近的 K 个用户,再把这 K 个用户买过、而当前用户还没碰过的商品作为候选集,按相似度权重汇总排序后输出推荐。这个思路在购物电商系统里解释成本很低,写进毕业论文里也容易讲清楚。
我在拆这份源码时注意到,它的评分数据并不依赖用户手动打分,而是从订单和行为日志折算出的隐式评分。项目里默认的折算规则是:购买记 5 分,加购记 3 分,浏览记 1 分。这样构造出的用户-物品矩阵虽然没有 MovieLens 那种显式评分精确,但胜在数据获取容易,电商场景里逻辑也完全自洽。答辩老师问“评分从哪来”的时候,你直接说这个折算口径,对方基本不会再追问。
矩阵的数据结构同样是关键。全量二维数组在用户数和商品数都在几千时还能撑住,商品一旦过万,内存就开始吃紧。这份源码采用稀疏 Map 结构存储矩阵,每个用户只保留有行为的商品列,相似度计算时也只遍历两个用户共同出现过的商品维度。相比二维数组,稀疏结构在内存占用上能省一个量级,实现稍繁一点,但扩展性好得多。
2.2 基于物品的协同过滤:相似度矩阵怎么建更省内存
基于物品的协同过滤(Item-Based Collaborative Filtering)和 User-Based 的差异不在算法难度,而在矩阵维度和可解释性。它计算的是商品与商品之间的相似度:买过商品 A 的用户,有多大比例也买了商品 B。把这个概率归一化到 0-1,就得到物品相似度矩阵。
为什么电商项目里 Item-Based 往往比 User-Based 更实用?因为商品数量通常比用户数量低一个数量级,物品相似度矩阵的空间占用更可控;而且“买过 A 的人还买了 B”这种推荐理由,放在商品详情页非常自然,用户也更容易接受。这份源码把两种模式都实现了,默认走 User-Based,但配置项里可以切到 Item-Based。我在验证阶段把两套结果各跑了一遍,差异主要集中在长尾商品上,头部商品的推荐结果重合度相当高。
物品相似度矩阵的更新策略会直接影响推荐质量。常见的做法是每天凌晨用定时任务重新计算一次,计算完写入缓存表或内存缓存。实时触发全量更新不是不行,但数据量稍大就容易把数据库连接池占满,反而拖垮正常订单流程。所以项目里的默认策略是:定时计算 + 缓存读取,接口层面不直接触发重算。
2.3 余弦相似度、皮尔逊、Jaccard:在 Java 项目里怎么选
相似度算法选了哪一种,直接决定矩阵计算的口径。源码里实际封装了三个实现类,但同一时刻只启用一个。下面这段是项目默认的余弦相似度实现,我保留了一些参数化空间便于调优:
public double cosineSimilarity(Map<Long, Double> vectorA, Map<Long, Double> vectorB) { if (vectorA == null || vectorB == null || vectorA.isEmpty() || vectorB.isEmpty()) { return 0.0; } // 只遍历较小的向量,减少循环次数 Map<Long, Double> small = vectorA.size() <= vectorB.size() ? vectorA : vectorB; Map<Long, Double> large = (small == vectorA) ? vectorB : vectorA; double dotProduct = 0.0; double normA = 0.0; double normB = 0.0; // 遍历小向量,在大向量中查找共同商品 for (Map.Entry<Long, Double> entry : small.entrySet()) { Double valueInLarge = large.get(entry.getKey()); if (valueInLarge != null) { dotProduct += entry.getValue() * valueInLarge; } normA += entry.getValue() * entry.getValue(); } for (Double value : large.values()) { normB += value * value; } if (dotProduct == 0.0 || normA == 0.0 || normB == 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }这段实现的关键细节有两个。第一,两个 Map 的遍历成本不一样,所以先固定遍历较小的向量,另一个向量只做 Map.get 查找,能省掉不少比较次数。第二,当点积为 0 或某一个模长为 0,直接返回 0,相当于两个用户没有任何共同行为时,相似度不生效,避免除零异常。
皮尔逊系数和 Jaccard 在这个项目里可以看作余弦的变种。皮尔逊先把每个维度减掉用户均值,再做向量点积,能消除用户打分习惯带来的系统性偏移;Jaccard 则完全不看分值,只统计共同行为数。三种算法在项目里的切换位置是一个配置项,叫cf.similarity.type,可选cosine、pearson、jaccard。我调参时发现一个规律:行为数据稀疏但行为类型丰富时,Jaccard 的推荐结果更稳;购买行为占比高时,余弦表现更好。这也是为什么建议把算法封装成可切换的类,而不是在 Service 里写死。
3. 数据底座与矩阵构建:MySQL 表设计、行为日志和 MyBatis 查询优化
3.1 用户、商品、订单、行为四类表怎么设计字段
推荐算法依赖的数据,本质上就是“谁在什么时间对什么商品做了什么操作”。电商系统里这些数据散落在用户表、商品表、订单表和操作日志里。源码的表结构设计比较规整,核心几张表大致如下:
-- 用户表 CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表 CREATE TABLE `tb_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '售价', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `image_url` varchar(500) DEFAULT NULL COMMENT '商品主图', `status` tinyint(4) DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 CREATE TABLE `tb_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '下单用户', `order_no` varchar(32) NOT NULL COMMENT '订单编号', `total_amount` decimal(10,2) DEFAULT 0.00, `status` tinyint(4) DEFAULT 0 COMMENT '0待支付 1已支付 2已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE `tb_order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `product_name` varchar(200) DEFAULT NULL COMMENT '下单时的商品快照', `price` decimal(10,2) NOT NULL, `quantity` int(11) DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单明细里的product_name和price保存的是下单时刻的快照,而不是实时关联商品表。这个设计在电商项目里很常见:商品改价或改名后,历史订单仍然保留原始信息,推荐算法按历史订单算相似度时也不会被商品信息变动干扰。两个索引idx_order_id和idx_product_id分别支撑按用户查订单、按商品查关联两个方向。
行为日志表是协同过滤计算的最直接数据源,我单独拆出来看:
CREATE TABLE `tb_user_action` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '操作用户', `product_id` bigint(20) NOT NULL COMMENT '被操作商品', `action_type` tinyint(4) NOT NULL COMMENT '1浏览 2加购 3购买', `action_score` int(11) DEFAULT NULL COMMENT '折算后的评分 1/3/5', `action_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_product` (`user_id`, `product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;action_type记录原始行为类型,action_score则是按规则折算后的数值。这里其实是做了一个反规范化处理:本可以把行为类型实时折算成分数,但项目选择在写入时就算好,推荐模块读取时零计算成本,SQL 也更好写。action_score允许为 NULL,原因是早期数据可能没有完整的行为来源,算法在构建矩阵时要对这种记录做跳过处理。
3.2 从行为记录构建用户-物品矩阵的 Java 实现
有了行为数据,下一步就是把数据库查询结果转换成推荐算法需要的 Map 结构。这一步的代码量不大,但细节决定后面相似度计算的准确性:
public class UserItemMatrixBuilder { /** * 从用户行为记录构建稀疏用户-物品矩阵 * * @param actionList 数据库查出的用户行为列表 * @return Map<用户ID, Map<商品ID, 行为评分>> */ public Map<Long, Map<Long, Double>> buildMatrix(List<UserAction> actionList) { Map<Long, Map<Long, Double>> matrix = new HashMap<>(); if (actionList == null || actionList.isEmpty()) { return matrix; } for (UserAction action : actionList) { if (action.getActionScore() == null || action.getActionScore() <= 0) { continue; // 没有有效评分的行为不进入矩阵 } Map<Long, Double> userVector = matrix.computeIfAbsent( action.getUserId(), k -> new HashMap<>()); // 同一用户对同一商品存在多次行为时,只保留最高分 Double oldScore = userVector.get(action.getProductId()); if (oldScore == null || action.getActionScore() > oldScore) { userVector.put(action.getProductId(), (double) action.getActionScore()); } } return matrix; } }computeIfAbsent的用法值得多说一句:它会在 Map 里不存在该用户 key 时自动创建一个新的 HashMap,存在时直接返回已有向量,等于把“判空 + 创建 + 取值”三行代码压缩成一行。同一用户对同一商品反复浏览或购买,oldScore == null || action.getActionScore() > oldScore这个条件保证矩阵里永远保留最高分,避免多次浏览把某个商品的权重刷得虚高。
我在实际跑这个系统时,把矩阵的行列数打印出来看了一眼:初始数据大约 300 个用户、800 个商品,矩阵里有效条目只有不到 4000 条,填充率不到 2%。如果按二维数组存,会有 24 万个格子,其中绝大多数是 0;用稀疏 Map 存,实际占用的内存只有有效条目数乘两个 Map.Entry 的开销,直观很多。
3.3 MyBatis 查询策略:避免全表扫描和 N+1 问题
数据准备好之后,查询效率会成为一个隐形坑。最典型的错误是在循环里逐条查行为数据,比如先查出所有用户,再循环每个用户去查他的行为列表,这会产生 N+1 次数据库查询。用户量到几千时,这种写法会让推荐接口慢得没法看。
推荐模块的做法是:一次性查出所有行为记录,按用户 ID 分组,再在 Java 内存里组装矩阵。SQL 大致是:
<select id="selectAllActions" resultType="com.example.recommend.entity.UserAction"> SELECT user_id, product_id, action_type, action_score, action_time FROM tb_user_action WHERE action_time >= #{startTime} AND action_time < #{endTime} ORDER BY user_id ASC </select>#{startTime}和#{endTime}是时间窗口参数,项目里通常按最近 180 天过滤。这个窗口设置很讲究:窗口太短,用户行为不足,矩阵稀疏;窗口太长,早期行为早就过期,反而干扰推荐。如果是课程设计,数据量不大,直接全表查询也能跑,但我会建议保留这个时间过滤逻辑,答辩时可以多提一句“推荐系统只关心近期行为”,这也是加分项。
查询结果集偏大时,还要注意 MyBatis 的fetchSize配置。MySQL 默认驱动是一次性把结果拉到内存,行为记录超过几十万条时容易触发 OOM。在数据源 URL 上追加?useCursorFetch=true,再在 mapper 查询上设置fetchSize="5000",可以让驱动分批从数据库取数据。这套配置不是毕设必须,但了解它对排查线上问题很有帮助。
4. 推荐功能的 Java 实现:相似度计算、Top-N 生成和接口对接
4.1 相似度计算的 Service 层实现与缓存
矩阵构建完成后,进入推荐模块的 Service 层。这一层不关心数据库和页面,只负责一件事:给定当前用户,返回一个按推荐分值排序的商品 ID 列表。基于用户的协同过滤推荐主流程可以写成这样:
@Service public class RecommendService { @Resource private SimilarityCalculator similarityCalculator; @Resource private RecommendCache recommendCache; /** * 为指定用户生成 Top-N 推荐商品 * * @param userId 当前用户ID * @param topN 期望返回的商品数量 * @return 推荐商品ID列表,按推荐分值降序 */ public List<Long> recommendForUser(Long userId, int topN) { // 1. 先查缓存,命中直接返回 List<Long> cached = recommendCache.get(userId); if (cached != null && !cached.isEmpty()) { return cached; } // 2. 获取当前用户的行为向量 Map<Long, Double> targetVector = userItemMatrix.get(userId); if (targetVector == null || targetVector.isEmpty()) { return Collections.emptyList(); // 冷启动用户交给兜底策略 } // 3. 遍历其他用户,累计相似度加权的商品分数 Map<Long, Double> scoreMap = new HashMap<>(); for (Map.Entry<Long, Map<Long, Double>> entry : userItemMatrix.entrySet()) { Long otherUserId = entry.getKey(); if (otherUserId.equals(userId)) { continue; // 跳过自己 } double similarity = similarityCalculator.calculate( targetVector, entry.getValue()); if (similarity <= 0.0) { continue; // 无相似性的用户不参与计算 } for (Map.Entry<Long, Double> itemEntry : entry.getValue().entrySet()) { Long productId = itemEntry.getKey(); if (targetVector.containsKey(productId)) { continue; // 用户买过/浏览过的商品不再推荐 } scoreMap.merge(productId, itemEntry.getValue() * similarity, Double::sum); } } // 4. 按分数排序,取前 Top-N return scoreMap.entrySet().stream() .sorted(Map.Entry.<Long, Double>comparingByValue().reversed()) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); } }这个主流程有三处值得细看。第一,缓存优先,这是推荐系统不被打垮的关键,后面的定时任务会把计算结果刷新进缓存;第二,相似度小于等于 0 的用户直接跳过,实际数据里大量用户之间没有共同行为,这一步能砍掉绝大多数无效计算;第三,targetVector.containsKey(productId)的过滤,保证推荐列表不会出现用户已经买过的商品。
scoreMap.merge(productId, itemEntry.getValue() * similarity, Double::sum)是累计推荐分值的核心:目标用户与当前用户的相似度越高,对方买过的商品得到的加权分就越大。累加完所有相似用户后,按分值降序取前 N 个,就是最终推荐列表。
4.2 Top-N 推荐列表生成:过滤已购商品与随机扰动
正常跑通之后,推荐列表还有一个常见毛病:每次返回的结果完全一样,用户刷新页面看到的东西永远不变。这在演示时不够真实,在真实电商里也会造成推荐疲劳。源码的做法是在 Top-N 生成之后加一个随机扰动参数:
/** * 对推荐列表做轻微随机扰动,避免每次刷新结果完全一致 * * @param recommendList 排序后的推荐商品ID列表 * @param topN 需要返回的数量 * @param shuffleFactor 0-1 之间的扰动系数,越大排序变化越明显 */ public List<Long> shuffleWithFactor(List<Long> recommendList, int topN, double shuffleFactor) { if (recommendList.size() <= topN) { return recommendList; } List<Long> result = new ArrayList<>(recommendList.subList(0, topN)); if (shuffleFactor > 0) { // 只对后半段做扰动,保住头部高置信度推荐 int startIndex = (int) (result.size() * (1 - shuffleFactor)); List<Long> subList = new ArrayList<>(result.subList(startIndex, result.size())); Collections.shuffle(subList); for (int i = 0; i < subList.size(); i++) { result.set(startIndex + i, subList.get(i)); } } return result; }shuffleFactor取 0.2 表示只对推荐列表末尾 20% 的商品做随机换序,头部 80% 保持稳定。这个参数在演示系统里很实用:既能让人看到推荐结果确实会变,又不会把最核心的推荐商品打散。我一般把这个值配置在 0.1 到 0.3 之间,太低没效果,太高会让用户觉得推荐混乱。
4.3 Controller 接口与页面渲染:把推荐结果送回前端
推荐模块最终要通过接口暴露给页面。Controller 层的设计决定了前端怎么调用、参数怎么传、异常怎么处理:
@RestController @RequestMapping("/api/recommend") public class RecommendController { @Resource private RecommendService recommendService; /** * 猜你喜欢接口 * * @param userId 当前登录用户ID * @param topN 推荐数量,默认 10 * @return 商品ID列表 */ @GetMapping("/guess") public Result<List<Long>> guess(@RequestParam("userId") Long userId, @RequestParam(value = "topN", defaultValue = "10") int topN) { if (userId == null || userId <= 0) { return Result.fail("用户ID不能为空"); } List<Long> productIds = recommendService.recommendForUser(userId, topN); return Result.ok(productIds); } }topN参数通过defaultValue = "10"给了默认值,前端可以不传。userId从请求参数拿,这在演示项目里足够用,因为登录态没有做 token 校验,简化了实现。真正企业级项目会从拦截器里取当前登录用户,而不是信任前端传入的 userId,这点在毕业设计答辩时可以主动说出来,能体现工程意识。
页面端我用 JSP 渲染推荐位,核心循环就十几行:
<div class="recommend-section"> <h3>猜你喜欢</h3> <div class="product-grid"> <c:forEach items="${recommendProducts}" var="product"> <div class="product-card"> <img src="${product.imageUrl}" alt="${product.name}" /> <p class="product-name">${product.name}</p> <p class="product-price">¥${product.price}</p> <a href="/product/detail/${product.id}">查看详情</a> </div> </c:forEach> </div> </div>推荐接口返回的是商品 ID 列表,页面拿到 ID 后再去查询商品详情,这是典型的“先算推荐、再查商品”两段式流程。如果商品数量不大,也可以让推荐接口直接返回商品对象,省一次查询;但商品表一旦超过几千条,分开查询的灵活度会高很多,推荐模块和商品模块的职责也更清晰。
5. 避坑与常见问题:协同过滤项目里的五个典型翻车场景
5.1 冷启动:新用户没有行为数据时推荐列表为空
现象:用一个刚注册的账号登录,首页“猜你喜欢”区域一片空白,接口返回空数组。
原因:协同过滤算法的前提是用户必须有历史行为。新用户没有任何浏览、加购、购买记录,用户-物品矩阵里就没有他的向量,相似用户无法计算,推荐结果自然为空。
解决:在recommendForUser方法开头加兜底分支——当目标用户向量为空时,直接返回热门商品排行榜,按销量和浏览量排序。这份源码里已经预留了hotProductService,我在实践里把兜底阈值设为库存充足且上架状态为 1 的商品,再加一个LIMIT topN,演示效果比空列表好得多。
5.2 数据稀疏:矩阵填充率太低导致相似度失真
现象:系统上线初期只有几十个用户和几百个订单,算出来的相似度矩阵里,不少用户之间的相似度异常偏高,甚至达到 1.0。
原因:用户-物品矩阵填充率不到 1% 时,两个用户只要有一个共同购买行为,余弦相似度就可能被拉得很高。共同行为数太少,相似度缺乏统计显著性。
解决:在相似度计算时加入最小共同行为数门槛。比如minCommonItems设为 3,只有共同行为数大于等于 3 才计算相似度,否则直接视为不相干。这个参数在项目配置里调,我会建议演示时把阈值设为 2 或 3,既能保证有推荐结果,又不至于让偶然的共同行为主导结果。
5.3 内存溢出:全量用户相似度计算 OOM
现象:数据导入到 5000 个用户后,执行定时推荐任务时堆内存报OutOfMemoryError: Java heap space。
原因:用户两两计算相似度,时间复杂度是 O(n²)。5000 个用户就是 1250 万对,每对都存一个 Double 分数,再叠加中间对象,默认 256M 堆内存根本扛不住。
解决:两个方向同时处理。第一,把 JVM 启动参数-Xmx调到 1024M 或更高,缓解内存压力;第二,从算法侧做裁剪,只对至少有一个共同行为商品的用户计算相似度,用倒排索引先筛出候选用户,能大幅减少无效计算对。这点源码里没有默认实现,但已经把相似度计算独立成接口,改起来不伤筋动骨。
5.4 热门商品霸榜:推荐列表失去个性
现象:推荐结果几乎全是销量最高的那几件商品,不同用户看到的推荐高度雷同,协同过滤的效果看起来和“热门推荐”差不多。
原因:多个相似用户买过同一件热门商品时,该商品的加权分会叠加得非常高,自然挤占长尾商品的位置。推荐系统被热门商品主导,是协同过滤常见的偏置问题。
解决:在排序完成后,对商品分数做一次平滑处理,或者按商品类别对推荐结果做多样性约束,同一分类最多出现 3 件。更简单的做法是在 SQL 查询商品详情时按category_id分类统计,超过限制就顺延下一件。这个优化我在项目里手动补了一层,演示时切换到不同账号看推荐列表,差别明显了。
5.5 用户评分习惯不同导致相似度假高
现象:用户 A 习惯给所有商品评 5 分,用户 B 也习惯评高分,两个人共同买过几件商品后,余弦相似度接近 1,但实际兴趣偏好可能完全不一样。
原因:余弦相似度对用户评分的绝对量级敏感,没有消除“有些人天生手松、有些人天生手紧”的习惯偏差。
解决:切换到皮尔逊相关系数。它先对评分向量做中心化处理,也就是每个维度减去用户自己的平均分,再去比较形状,评分习惯的影响就被抵消了。我在调优时把默认配置从 cosine 切到 pearson,项目里这个参数切换只需要改cf.similarity.type一处,验证成本很低。
6. 把“能跑”变成“能用”:验证推荐效果的三个离线检查动作
推荐系统跑通之后,最怕的就是“看似有推荐,实际没法用”。我从这个项目里总结出三个必做的离线检查动作,每次调完参数都会强制走一遍。
第一,检查推荐覆盖率。随机抽 50 个有行为数据的用户,逐个调用推荐接口,统计返回为空的比例。这个比例超过 20% 就说明兜底策略不到位,新用户或行为稀疏用户的体验会很差。我一般要求空结果率控制在 5% 以内,达不到就加强热门商品兜底逻辑。
第二,检查重复度。同一个用户连续调用两次推荐接口,比较两次结果的重叠比例;再抽几个不同用户,比较他们推荐列表的重叠比例。用户内重复度低说明随机扰动生效,用户间重叠度太高说明热门商品偏置严重。参考值:用户内重复度可以控制在 80% 左右,用户间重叠度最好不要超过 50%,超过就调低shuffleFactor的扰动区间,或者加大长尾商品的权重。
第三,做一次简单的人工判断。我习惯用离线小样本,随机选 10 个用户,人工看他的购买记录和推荐结果,评估“推荐的商品和买过的商品是不是同一类”。比如买过跑步鞋的人,系统推荐的是篮球鞋而不是手机壳,那相似度计算基本就是对的;如果推荐结果只按销量排,说明矩阵构建环节出了问题,行为数据没有真正进入计算链路。
这三个动作都不需要复杂的评估框架,跑一遍最多十几分钟,却能把算法层面的多数隐患提前暴露出来。我在后来的项目里把这三步做成了脚本:一条命令触发定时任务,一条命令抽取样本校验,一条命令打印重复率和覆盖率,已经成了我接手推荐系统的固定习惯。
希望这份基于 Java 和协同过滤算法的购物电商系统源码拆解,能帮你在毕业设计或课程设计里少走弯路,把推荐这个模块真正落到能演示、能讲清楚的程度。
本文还有配套的精品资源,点击获取