ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python和MySQL的协同过滤商品推荐系统源码与数据库设计

基于Python和MySQL的协同过滤商品推荐系统源码与数据库设计 简介这是一套面向计算机相关专业在校生与项目实战学习者的毕业设计级商品推荐系统资料核心采用Python实现协同过滤算法配套数据库、论文与演示视频适合作为毕设、课程设计、期末大作业或比赛初期立项参考。压缩包共712个文件约30.94MB涵盖39个py源码、36个vue前端组件、51个css与162个js等前端资源以及2个sql数据库脚本、1个docx论文文档、1个mp4演示视频和多个bat启动脚本前后端与文档结构完整。目前已有147人学习浏览。项目经本地运行与功能测试评审分98.5分读者可据此理解协同过滤推荐流程、数据库表设计与前后端交互逻辑并借助论文与录屏快速梳理实现思路也便于在现有代码基础上进行二次开发与功能扩展。1. 从一份「能跑起来」的协同过滤商品推荐系统说起电商后台的订单表堆到几十万行之后运营最常问的一句话是能不能给每个用户推几个他大概率会买的商品这个问题听起来像算法岗的活但真正落到课程设计、小型项目或者内部工具上往往就是一份 Python 源码加一个 MySQL 数据库的事。协同过滤商品推荐系统核心逻辑并不复杂拿用户对商品的评分或行为算出「和你口味相似的人还买了什么」或者「和你买过的这个东西相似的东西还有哪些」。它不需要深度学习框架一台普通笔记本、装好 Python 和 MySQL 就能跑通全流程。这份标题里的「源码数据库论文演示视频」组合本质是一套完整的交付物源码负责算法和界面数据库负责存用户、商品、评分三张核心表论文负责把原理和实验讲清楚演示视频负责让别人三分钟看懂你怎么用。适合的人群很明确——做课程设计的学生、想快速验证推荐逻辑的初级开发、需要给内部小系统加个「猜你喜欢」模块的工程师。下面我按实际落地的顺序把选型、建库、写算法、调参、避坑一条线讲透你照着做就能得到一个能演示、能改、能写进文档的系统。2. 协同过滤的两条路线与数据表设计为什么先定 UserCF 还是 ItemCF2.1 UserCF 和 ItemCF 的适用边界别一上来就选错协同过滤分两条路基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。UserCF 的思路是「找到和你评分相似的一批用户把他们喜欢但你没看过的商品推给你」ItemCF 的思路是「找到和你历史喜欢的商品相似的物品推给你」。两者没有绝对优劣但落地场景差别很大。UserCF 适合用户数量远小于商品数量、且用户兴趣变化快的场景比如新闻推荐、社交动态。它的缺点是用户数一多用户相似度矩阵会膨胀得很快而且新用户进来没有历史行为就完全没法推。ItemCF 适合商品数量相对稳定、用户兴趣比较持久的场景比如电商、视频网站。它的优势是商品相似度矩阵可以离线算好线上响应快而且可解释性强——「因为你买了 A所以推 B」这种话运营和用户都能听懂。我一般做商品推荐系统默认选 ItemCF原因很实际商品表通常比用户表稳定相似度矩阵更新频率低演示的时候解释起来也顺。但如果你做的场景是「给一批用户推他们可能感兴趣的帖子」用户量不大UserCF 反而更直接。选型这一步定错后面调参和写论文都会别扭。2.2 三张核心表加一张相似度表MySQL 建库脚本直接抄不管选哪条路线数据层最少需要三张表用户表、商品表、评分表。评分表是核心它记录「谁给哪个商品打了多少分」。如果只有行为没有显式评分可以用浏览、收藏、购买加权成一个隐式评分。下面这份建库脚本可以直接在 MySQL 里执行字段类型和索引都按推荐系统的查询习惯调过。-- 创建数据库字符集用 utf8mb4 避免中文商品名乱码 CREATE DATABASE IF NOT EXISTS rec_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE rec_system; -- 用户表只存最基础的标识和昵称 CREATE TABLE users ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 商品表category 用于后续做品类维度的兜底推荐 CREATE TABLE items ( item_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, category VARCHAR(50), price DECIMAL(10,2) DEFAULT 0.00, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 评分表user_id item_id 建联合唯一索引防止重复评分 CREATE TABLE ratings ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, item_id INT NOT NULL, score TINYINT NOT NULL COMMENT 评分 1-5, rated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_item (user_id, item_id), KEY idx_item (item_id), KEY idx_user (user_id) ) ENGINEInnoDB; -- 相似度表离线算好的 item-item 相似度线上直接查 CREATE TABLE item_similarity ( item_id INT NOT NULL, sim_item_id INT NOT NULL, similarity FLOAT NOT NULL, PRIMARY KEY (item_id, sim_item_id), KEY idx_sim (item_id, similarity DESC) ) ENGINEInnoDB;这段脚本的关键点有三个。第一ratings表的uk_user_item联合唯一索引保证一个用户对一个商品只有一条评分避免后续算相似度时出现重复计数。第二idx_item和idx_user两个普通索引是为了加速「查某个商品的所有评分」和「查某个用户的所有评分」这两类高频查询。第三item_similarity表把离线计算结果落库线上推荐时只需要一条ORDER BY similarity DESC LIMIT 20就能拿到候选集不用每次实时算矩阵。提示如果数据量在十万条评分以内相似度也可以不落库直接在内存里用 pandas 算。落库的意义在于线上服务重启后不用重算以及方便你观察相似度分布。2.3 用 Python 造一批可复现的测试数据真实数据往往拿不到课程设计里更常见的是自己造数据。造数据不能纯随机否则相似度算出来全是噪声演示效果很差。我的做法是先定义几个「兴趣簇」比如簇 A 的用户偏爱数码簇 B 的用户偏爱图书然后让同一簇内的用户对同类商品的评分有较高重叠。下面这段脚本生成 200 个用户、100 个商品、约 8000 条评分直接写入 MySQL。import random import pymysql # 连接数据库charset 必须指定否则中文写入报错 conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databaserec_system, charsetutf8mb4 ) cursor conn.cursor() random.seed(42) # 固定种子保证每次生成的数据一致方便复现 categories [数码, 图书, 服饰, 食品] # 每个品类下挂 25 个商品共 100 个 items [] for cate in categories: for i in range(25): items.append((f{cate}商品{i1}, cate, round(random.uniform(10, 500), 2))) cursor.executemany( INSERT INTO items (title, category, price) VALUES (%s, %s, %s), items ) conn.commit() # 生成用户并给每个用户分配一个主兴趣品类 users [(fuser_{i1},) for i in range(200)] cursor.executemany(INSERT INTO users (username) VALUES (%s), users) conn.commit() # 每个用户对主兴趣品类商品评分偏高对其他品类评分偏低或缺失 ratings [] for uid in range(1, 201): main_cate categories[uid % len(categories)] for iid in range(1, 101): cate items[iid - 1][1] if cate main_cate: if random.random() 0.7: # 主品类 70% 概率产生评分 ratings.append((uid, iid, random.randint(4, 5))) else: if random.random() 0.1: # 其他品类 10% 概率产生评分 ratings.append((uid, iid, random.randint(1, 3))) cursor.executemany( INSERT INTO ratings (user_id, item_id, score) VALUES (%s, %s, %s), ratings ) conn.commit() print(f生成评分 {len(ratings)} 条) cursor.close() conn.close()这段脚本的逻辑说明random.seed(42)是复现的关键不固定种子的话每次生成的数据不同你调参时无法判断效果变化是参数引起的还是数据引起的。主兴趣品类 70% 评分概率、其他品类 10% 概率这个比例让同一品类内的用户有足够重叠ItemCF 才能算出有意义的相似度。如果所有品类概率一样相似度矩阵会接近均匀分布推荐结果就没有区分度。参数方面pymysql.connect里的charsetutf8mb4必须写否则中文商品名会变成问号。executemany比循环单条execute快一个数量级八千条评分一秒内能写完。如果你本地 MySQL 版本较老把utf8mb4_general_ci换成utf8_general_ci也能跑但中文支持不如前者。3. 用 Python 实现 ItemCF从评分矩阵到 TopN 推荐3.1 余弦相似度算物品相似度为什么不用欧氏距离ItemCF 的核心是算物品之间的相似度。常见做法有两种余弦相似度和皮尔逊相关系数。余弦相似度看的是两个物品的评分向量方向是否一致对评分量纲不敏感皮尔逊相关系数在此基础上做了中心化能消除用户评分偏好的影响。我一般用调整后的余弦相似度也就是先把每个用户的评分减去他的平均分再算余弦。这样能缓解「有人习惯打 5 分、有人习惯打 3 分」带来的偏差。欧氏距离在这个场景里不常用因为它对绝对分值敏感。两个物品如果都被同一批用户喜欢但一个评分普遍是 5 分、另一个普遍是 4 分欧氏距离会认为它们不相似而实际上它们的用户群体高度重叠。余弦相似度只看方向不受这个影响。下面这段代码从 MySQL 读评分构建用户-物品评分矩阵算物品相似度并写入item_similarity表。import pandas as pd import numpy as np import pymysql from sklearn.metrics.pairwise import cosine_similarity conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databaserec_system, charsetutf8mb4 ) # 读评分表pivot 成 用户 x 物品 的矩阵缺失值填 0 df pd.read_sql(SELECT user_id, item_id, score FROM ratings, conn) matrix df.pivot_table( indexuser_id, columnsitem_id, valuesscore, fill_value0 ) # 中心化每个用户评分减去该用户平均分只对非零项减 user_mean matrix.replace(0, np.nan).mean(axis1) centered matrix.sub(user_mean, axis0).fillna(0) # 转置成 物品 x 用户算物品间余弦相似度 item_matrix centered.T.values sim cosine_similarity(item_matrix) # 把自己和自己置 0避免推荐时推回原商品 np.fill_diagonal(sim, 0) # 取每个物品最相似的 Top20 写入数据库 item_ids matrix.columns.tolist() rows [] for i, iid in enumerate(item_ids): top_idx np.argsort(sim[i])[::-1][:20] for j in top_idx: if sim[i][j] 0: rows.append((iid, item_ids[j], float(sim[i][j]))) cursor conn.cursor() cursor.execute(TRUNCATE TABLE item_similarity) cursor.executemany( INSERT INTO item_similarity (item_id, sim_item_id, similarity) VALUES (%s, %s, %s), rows ) conn.commit() print(f写入相似度 {len(rows)} 条) cursor.close() conn.close()逻辑说明pivot_table把长表转成宽表fill_value0表示没评过分的填 0。中心化那一步用replace(0, np.nan)先把 0 变成 NaN算完均值再fillna(0)这样不会把「没评分」当成「评了 0 分」参与均值计算。cosine_similarity接收的是物品×用户的矩阵算出来是物品×物品的相似度矩阵。np.fill_diagonal(sim, 0)把对角线置零否则每个物品最相似的一定是它自己。参数方面Top20 这个数字可以调。商品总数少的时候取 10 就够商品多的时候取 50 甚至 100但写入数据库的条数会线性增长。sim[i][j] 0这个过滤条件是为了不把负相似度写进去负相似度意味着两个物品的用户群体相反推了反而有害。3.2 根据相似度生成推荐列表评分加权与去重有了物品相似度给某个用户生成推荐就三步找出他评分过的物品查这些物品的相似物品按相似度加权打分排序。加权公式是预测分 Σ(用户对已评物品的评分 × 物品相似度) / Σ(相似度)。这个公式比简单取相似物品的平均分更合理因为它让相似度高的物品贡献更大。def recommend_for_user(user_id, top_n10): cursor conn.cursor(pymysql.cursors.DictCursor) # 1. 查该用户评分过的物品和分数 cursor.execute( SELECT item_id, score FROM ratings WHERE user_id %s, (user_id,) ) rated {row[item_id]: row[score] for row in cursor.fetchall()} if not rated: return [] # 冷启动用户走兜底逻辑 # 2. 查这些物品的相似物品 placeholders ,.join([%s] * len(rated)) cursor.execute( fSELECT item_id, sim_item_id, similarity FROM item_similarity fWHERE item_id IN ({placeholders}), tuple(rated.keys()) ) sim_rows cursor.fetchall() # 3. 加权打分排除用户已经评过的物品 scores {} for row in sim_rows: iid row[sim_item_id] if iid in rated: continue scores[iid] scores.get(iid, 0) row[similarity] * rated[row[item_id]] # 4. 排序取 TopN ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return ranked逻辑说明第一步拿到用户的历史评分如果为空直接返回空列表后面要接兜底推荐。第二步用IN查询批量拿相似物品placeholders是为了防止 SQL 注入不能直接拼字符串。第三步的if iid in rated: continue是去重用户已经评过的物品不再推荐。第四步排序取前 N 个。参数方面top_n默认 10演示时够用。如果要做分页可以在排序后切片。scores字典的 key 是物品 IDvalue 是加权分这个分数不是真实评分只用于排序不要直接展示给用户看。3.3 冷启动兜底没有历史行为时推什么新用户没有评分记录ItemCF 完全失效。这时候需要兜底策略。常见做法有三种推热门商品、推新品、推品类多样性高的商品。我一般用「热门 随机品类」的组合既保证有东西可推又不会所有新用户看到一模一样的结果。def fallback_recommend(top_n10): cursor conn.cursor(pymysql.cursors.DictCursor) # 按评分人数排序取热门再随机打散品类 cursor.execute( SELECT i.item_id, i.title, COUNT(r.id) AS cnt FROM items i LEFT JOIN ratings r ON i.item_id r.item_id GROUP BY i.item_id ORDER BY cnt DESC LIMIT %s , (top_n * 3,)) candidates cursor.fetchall() random.shuffle(candidates) return [(c[item_id], c[cnt]) for c in candidates[:top_n]]逻辑说明先取评分人数最多的前top_n * 3个商品作为候选池然后random.shuffle打散最后取前top_n个。这样不同新用户看到的推荐列表有差异不会显得太机械。LEFT JOIN保证即使某个商品没人评分也能出现在结果里COUNT(r.id)统计的是评分条数。参数方面候选池放大三倍是为了给随机打散留空间放大倍数太小随机效果不明显太大则热门商品可能被挤出。如果系统里商品本身就不多直接取全部商品打散也行。4. 避坑与排查协同过滤落地时最容易翻车的五个地方4.1 相似度全是 0 或者全是 1现象跑完相似度计算发现item_similarity表里要么没有记录要么所有 similarity 都是 1.0。原因通常是评分矩阵太稀疏或者中心化那一步把数据洗没了。如果用户-物品矩阵里大部分是 0余弦相似度算出来会接近 0如果每个用户只评了一个物品中心化后每个用户只有一个非零值物品向量方向完全一致相似度就是 1。解决先查评分密度SELECT COUNT(*) / (用户数 * 商品数) FROM ratings低于 1% 就要考虑换数据或降低相似度阈值。中心化那步确认replace(0, np.nan)写对了如果直接对含 0 的矩阵减均值会把没评分的物品也拉进计算。演示数据用第 2 章那段脚本生成密度控制在 5% 到 10% 之间比较合适。4.2 推荐结果里出现用户已经买过的商品现象给用户推的列表里赫然躺着他上周刚评过 5 分的商品。原因是在加权打分那一步忘了排除rated里的物品或者相似度表里sim_item_id和item_id的对应关系搞反了。解决检查recommend_for_user里的if iid in rated: continue是否生效。另外确认item_similarity表写入时item_id和sim_item_id没有写反——item_id是源物品sim_item_id是相似物品查询时用WHERE item_id IN (用户评过的物品)取sim_item_id作为候选。写反了就会推出用户没评过的物品的相似物品逻辑上没错但结果会很怪。4.3 MySQL 连接报 1366 错误中文商品名写不进去现象插入商品数据时报Incorrect string value: \xE6\x95\xB0\xE7\xA0\x81... for column title。原因是数据库或表的字符集不是 utf8mb4或者 Python 连接时没指定 charset。解决三步确认。第一建库时DEFAULT CHARACTER SET utf8mb4写了没有。第二pymysql.connect里charsetutf8mb4写了没有。第三如果表已经建好了用ALTER TABLE items CONVERT TO CHARACTER SET utf8mb4;改一下。三个地方都对了就不会再报这个错。4.4 相似度计算太慢跑一次要十几分钟现象评分数据到几十万条之后cosine_similarity算一次要很久内存也吃紧。原因是物品数量大时物品×物品的相似度矩阵是 O(n²) 的空间复杂度10000 个物品就是 1 亿个浮点数。解决两个方向。一是降维用 TruncatedSVD 把物品向量降到 50 到 100 维再算相似度速度能快一个数量级精度损失可接受。二是只算 TopK用sklearn.neighbors.NearestNeighbors的kneighbors方法指定n_neighbors20它内部用树结构避免全量计算。如果数据量再大就上 Spark 或者分片计算但课程设计级别用前两种就够了。4.5 演示时推荐结果每次刷新都不一样现象同一个用户刷新页面推荐列表就变了。原因是相似度表被重复写入没有清空或者推荐时用了随机排序。TRUNCATE TABLE item_similarity那一步如果漏了每次跑脚本都会追加新记录查询时ORDER BY similarity DESC可能取到不同批次的重复数据。解决每次重算相似度前先TRUNCATE保证表里只有最新一批结果。推荐排序用sorted的确定性排序不要用random.shuffle。如果确实想要多样性可以在 TopN 里做品类打散但打散逻辑要固定种子否则演示时每次结果不同别人会以为系统不稳定。5. 把推荐系统跑成可演示的完整流程从建库到出结果的一条命令5.1 用一份配置文件管住所有参数前面几章的脚本里数据库密码、TopN、相似度阈值这些参数散落在各处改一个要翻好几个文件。实际交付时我习惯用一个config.py集中管理源码包里的其他脚本都从这里读。# config.py DB_CONFIG { host: localhost, user: root, password: your_password, database: rec_system, charset: utf8mb4 } TOPN_SIMILAR 20 # 每个物品保留的相似物品数 TOPN_RECOMMEND 10 # 给用户推荐的商品数 MIN_SIMILARITY 0.1 # 低于这个相似度不写入 RANDOM_SEED 42 # 数据生成和打散的随机种子这样调参时只改这一个文件论文里写实验设置也方便引用。MIN_SIMILARITY这个阈值我一般设 0.1低于它的相似度基本是噪声写进去只会让推荐列表变长但质量下降。5.2 一条命令跑通全流程把建库、造数据、算相似度、生成推荐串成一个入口脚本演示时只需要执行一次。# 1. 建库建表在 MySQL 客户端里执行 schema.sql mysql -u root -p schema.sql # 2. 生成测试数据 python generate_data.py # 3. 计算物品相似度并落库 python compute_similarity.py # 4. 给指定用户生成推荐 python recommend.py --user_id 1 --top_n 10recommend.py里用argparse接收参数输出格式化成「商品ID - 商品名 - 预测分」三列方便直接贴进演示视频或者论文截图。如果要做成 Web 界面Flask 起一个路由读recommend_for_user的结果渲染成表格就行不需要额外的前端框架。5.3 验证推荐效果的三个土办法没有标注数据的情况下怎么判断推荐结果靠不靠谱我常用三个办法。第一抽样看品类随机抽 10 个用户看推荐列表里主兴趣品类的商品占比如果超过 60%说明相似度算得合理。第二交叉验证把用户评分按 8:2 切分用 80% 算相似度在 20% 上算命中率命中率高于随机推荐就说明有效。第三人工看找几个典型用户看推出来的商品是不是「像那么回事」这个方法最土但最直接演示前一定要做一遍。注意交叉验证时测试集的评分不能参与相似度计算否则就是数据泄漏命中率会虚高。切分要在算相似度之前做。5.4 论文和演示视频里该放什么论文部分重点写三块数据表设计附 E-R 图、相似度公式推导把中心化余弦的公式写清楚、实验结果命中率对比随机推荐。演示视频控制在三到五分钟开头展示数据库里的数据量中间跑一遍推荐脚本结尾展示推荐结果和品类分布。不要花时间讲 Python 安装那是环境准备不是系统本身。我自己的习惯是每次交付前把config.py里的RANDOM_SEED固定重新跑一遍全流程确认结果可复现。这个习惯帮我省过很多次「演示时结果和论文里对不上」的尴尬。协同过滤这套东西不复杂但细节多把参数管住、把随机性管住它就能稳定跑起来。希望帮到你。本文还有配套的精品资源点击获取
返回列表