十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python图书推荐系统:协同过滤与文本相似度融合实践

Python图书推荐系统:协同过滤与文本相似度融合实践 简介推荐系统是缓解信息过载的核心技术其价值在于从海量物品中精准匹配用户需求。协同过滤通过挖掘用户行为模式实现个性化推荐但面临新物品无行为记录的冷启动问题文本相似度则利用物品内容特征构建画像能有效弥补行为数据的缺失。将两者结合采用双通道召回与加权融合排序可在冷启动阶段用内容相似度兜底在行为数据充足时用协同过滤提升精度。这一思路在图书推荐场景中尤为适用新书入库、新用户注册等场景均能获得稳定推荐效果。基于Python实现的图书推荐系统源码正是融合了用户行为协同过滤与图书文本相似度完整展示了从向量化、相似度计算到混合排序的工程落地流程适合开发者快速搭建可演示的推荐闭环。1. 图书推荐系统为什么需要协同过滤和文本相似度夹击把“猜你喜欢”从一句口号变成可复现的代码最常见的数据缺口不是算法不够新而是冷启动。新书入库没有点击量新用户注册没有历史行为纯协同过滤会直接失效反过来只做文本相似度又会让热门书永远霸榜。这套Python实现的图书推荐系统源码选择的是“用户行为协同过滤 图书文本相似度”双通道方案文本相似度解决物品冷启动协同过滤解决用户个性化两者在召回阶段先并行在排序阶段再融合。适合正在做课程设计、毕业设计或者想在本地搭建一个可演示的推荐业务闭环的开发者。下文所有代码和参数都基于该源码的业务模块拆解不依赖外部大型框架用的也是Python生态里最常见的库。2. 用户冷启动基于文本相似度的图书内容画像2.1 图书信息的向量化与TF-IDF加权图书详情页里能用的文本字段通常包括书名、作者、简介、分类、出版社。如果直接拼接成字符串再算相似度分词噪声会很大。常见做法是先对文本做去停用词和分词再用TF-IDF把每一本书转成向量。TF-IDF的精髓在于某个词在一本书里出现频率高但在整个图书语料里很少出现这个词就更能代表这本书的主题。源码里图书模块包含“根据分类展示”“搜索图书”“图书详情展示”这些功能共用同一份文本向量。实施时我用到的字段权重大概是书名 3.0简介 1.0分类 2.0作者 0.5。作者权重低是因为同名作者可能写完全不同主题的书而分类权重要拉高因为用户对类别的感知最直接。分词工具用 jieba停用词表覆盖常见虚词和“本书”“内容简介”这类页面噪音词。2.1.1 向量化流程与参数选择下面是核心的向量构造代码适用于把数据库里图书表导出成 CSV 后进行离线计算import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer STOP_WORDS {本书, 内容, 简介, 以及, 可以, 一个, 我们, 他们} def load_books(csv_path): df pd.read_csv(csv_path) # 合并文本字段书名和分类加大权重 df[weighted_text] ( df[book_name].apply(lambda x: (x ) * 3) df[category] df[intro] df[author] ) return df def tokenize_with_stopwords(text): words jieba.lcut(text) return .join([w for w in words if w.strip() and w not in STOP_WORDS]) df load_books(books.csv) vectorizer TfidfVectorizer(tokenizertokenize_with_stopwords, ngram_range(1, 2)) tfidf_matrix vectorizer.fit_transform(df[weighted_text])这段代码做了三件事首先把书名重复三次变相提高权重然后把分类、简介、作者拼接成带权重文本最后用 TfidfVectorizer 生成稀疏矩阵。ngram_range(1, 2)可以把“数据挖掘”这类双字词也纳入特征避免单纯单字切分丢语义。2.1.2 相似度矩阵的存储方式得到 tfidf_matrix 后直接计算两两余弦相似度会得到 n x n 稠密矩阵一万本书就是 1 亿个浮点数内存扛不住。源码里推荐的是用 scipy 的 k 近邻搜索只保留每本书最相似的 TopK 本from sklearn.metrics.pairwise import cosine_similarity from scipy.sparse import csr_matrix import numpy as np def build_similarity_topk(tfidf_matrix, k10): # 归一化后做矩阵乘法余弦距离等价于归一化后的内积 normed tfidf_matrix / np.sqrt(tfidf_matrix.multiply(tfidf_matrix).sum(axis1)) sim normed normed.T sim sim.toarray() if hasattr(sim, toarray) else sim result {} for i in range(sim.shape[0]): # 排除自己取前k个相似图书 idx np.argsort(-sim[i])[1:k1] result[i] [(int(j), float(sim[i][j])) for j in idx if sim[i][j] 0.05] return result这里先做 L2 归一化再矩阵相乘是余弦相似度的高效替代方案。阈值 0.05 用来过滤几乎无关的边TopK 取 10 意味着每本书只保留 10 个近邻。注意argsort后跳过索引 0 自己如果出现某本书和所有书相似度过低保留空列表即可下游推荐时自然忽略。2.2 用相似度实现“被推荐图书展示”源码前台有个“被推荐图书展示”区块入口就是上面生成的 result 字典。当用户打开一本书详情后端取这本书的相似图书列表再和运营手动推荐的书籍做一次并集去重。排序时不能只按相似度降序我会额外乘一个热度系数log(浏览量 1) / 5避免冷门书相似度虚高。原因是简介文本可能大量雷同比如“经典必读”这类套话导致相似度失真。具体查询伪代码如下def get_related_books(book_id, sim_dict, hot_map, limit8): related sim_dict.get(book_id, []) scored [] for bid, score in related: if bid in hot_map: # 相似度为主热度做微调 adjusted score * 0.85 min(hot_map[bid] / 10000, 0.15) else: adjusted score * 0.85 scored.append((bid, adjusted)) scored.sort(keylambda x: -x[1]) return [bid for bid, _ in scored[:limit]]这个公式是一个工程折衷相似度占比 85%热度占比 15%。热度上限封顶 0.15防止超级大热门冲掉真正的语义相关。如果你想做中性内容可以直接去掉热度项纯按相似度排。3. 行为数据变现协同过滤召回与评分预测3.1 UserCF与ItemCF的选择逻辑有了阅读、收藏、点赞、评论、历史记录这些行为后就可以上协同过滤。这套源码里既有“用户历史数据推荐的图书”也有“查看个人点赞、收藏、评论的书籍”说明行为数据是分类型的。我在拆包时看到登录用户会在浏览图书时产生隐性行为打开详情显性行为包括点赞、收藏、评论、评分。UserCF 适合用户少、物品多、兴趣变化快的场景ItemCF 适合物品少、用户多、兴趣稳定的场景。图书属于后者——图书数量远小于用户数且用户喜欢某本书的意图比较持久。源码混合了两种离线批处理用 ItemCF 生成候选在线实时模块用 UserCF 补足用户刚发生的点击行为。下表是选型对比维度UserCFItemCF计算规模受用户数量影响用户多则矩阵爆炸受图书数量影响适合图书库规模中等实时性新行为立即可见能反映兴趣漂移要等到物品相似度更新后才生效可解释性“和你相似的人也在看”“看过这本的人还在看”冷门物品容易被忽略相似度高的冷门书也有机会3.2 评分矩阵构建与ItemCF核心代码源码里没有显式的 1-5 星评分按钮所以我把“评分”隐式化浏览 1 分、点赞 2 分、收藏 3 分、评论 4 分。构造评分矩阵时每个用户对每本书的总分是这些行为分值的累加然后减去用户平均分做中心化消掉不同用户打分尺度的差异。import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity # user_id, book_id, behavior 组成行为流 df pd.read_csv(user_behavior.csv) behavior_score {view: 1, like: 2, favorite: 3, comment: 4} df[score] df[behavior].map(behavior_score) # 累加后中心化 pivot df.pivot_table(indexuser_id, columnsbook_id, valuesscore, aggfuncsum, fill_value0) mean_user pivot.mean(axis1) centered pivot.sub(mean_user, axis0) # 图书相似度用每本书的评分列做余弦相似度 book_sim cosine_similarity(centered.T) np.fill_diagonal(book_sim, 0) book_sim_df pd.DataFrame(book_sim, indexpivot.columns, columnspivot.columns)这里故意把行为分值映射写成字典是为了让你能调参。比如你认为“收藏”的意图远强于“点赞”可以把收藏调到 4、点赞降到 1。中心化处理是协同过滤中容易忽略的细节不做中心化爱给高分和只给低分的用户会被同等对待相似度计算会产生偏差。3.2.1 给用户召回图书候选基于物品相似度矩阵推荐分数是用户历史Rating过的物品的加权和。公式常见做法是R(u, i) sum(score(u, j) * sim(i, j)) / sum(|sim(i, j)|)除法是为了防止热门历史物品主导结果。def recommend_for_user(uid, pivot, book_sim_df, top_n10): user_rated pivot.loc[uid] rated_books user_rated[user_rated 0].index.tolist() if not rated_books: return [] # 留给文本相似度兜底 scores {} for book in rated_books: w user_rated[book] sim_bucket book_sim_df[book].drop(indexrated_books) # 排除已读 for cand, sim_val in sim_bucket.items(): if sim_val 0: continue scores[cand] scores.get(cand, 0) w * sim_val # 归一化 for cand in scores: hist_w sum([user_rated[b] for b in rated_books]) scores[cand] / hist_w ranked sorted(scores.items(), keylambda x: -x[1])[:top_n] return [book_id for book_id, _ in ranked]注意drop(indexrated_books)是工程上的关键一步——不给用户推荐他已经看过的书。归一化分母用历史行为总分而非单纯的行为数因为加权分数会随历史量级膨胀。如果用户只点了一本书那本书的相似图书会被放大这是 ItemCF 的固有特性需要通过第 5 章的阈值调优来缓解。3.3 混合推荐的权重融合与排序源码的“系统根据用户历史数据推荐的图书”最终展示需要把文本相似度和协同过滤结果合并。我采用线性加权具体比例为协同过滤 0.6、文本相似度 0.4。在实际运行中这个比例需要看数据集协同过滤冷启动时文本相似度占比要升到 0.7。def hybrid_rank(user_id, content_scores, cf_scores, alpha0.6): all_ids set(content_scores.keys()) | set(cf_scores.keys()) merged {} for bid in all_ids: score 0.0 if bid in cf_scores: score alpha * (cf_scores[bid] / max(cf_scores.values())) if bid in content_scores: score (1 - alpha) * (content_scores[bid] / max(content_scores.values())) merged[bid] score return sorted(merged.items(), keylambda x: -x[1])这段代码先把两种分数各自归一化到 0~1 区间再按 alpha 加权相加。如果不归一化协同过滤的浮点数往往比相似度小几个数量级加权就失去意义。这也是常见误用直接把原始分数相加导致某一通道永远占优。4. 前后端模块落地从登录注册到后台管理4.1 用户模块与等级体系对推荐的影响源码里用户有三种等级普通用户、会员、认证作者。积分和等级不只是装饰它们直接影响推荐权重会员行为分值在协同过滤中乘以 1.5认证作者的书籍在文本相似度中有额外的曝光加权。登录注册模块采用 Django 自带认证机制时用户表要注意扩展积分字段和等级字段。4.1.1 用户注册与登录的密码处理实际开发中不要存明文密码用 Django 的make_password或者 werkzeug 的generate_password_hash都行。源码中注册流程我在本地复现时精简为from werkzeug.security import generate_password_hash, check_password_hash # 注册时 hashed_pwd generate_password_hash(password, methodpbkdf2:sha256, salt_length16) # 登录时 if check_password_hash(user.password_hash, input_pwd): # 更新登录时间、积分 1 user.points 1 user.last_login_at datetime.now() db.commit()pbkdf2是当前成本可控且被广泛接受的方案salt_length保持 16 字节足够。登录后建议把用户 ID 放入 session不要存完整对象。积分加 1 的规则要和推荐计算部分解耦否则每次登录都触发一次矩阵更新。4.2 图书管理批量导入、上下架与审核后台图书管理支持批量导入 CSV、编辑、删除、上下架、审核。批量导入最容易出错的是编码和字段对齐。我通常用 pandas 读取并做校验再逐条写入数据库。下架的逻辑不是删除而是把status字段改为 0推荐查询时强制过滤status1。-- 图书状态1 上架0 下架2 待审核 SELECT book_id, book_name, category, intro, author FROM books WHERE status 1 AND category %s ORDER BY view_count DESC LIMIT 50;4.2.1 批量导入的校验流程import pandas as pd def import_books(file_path): df pd.read_csv(file_path, dtype{isbn: str}) required [book_name, author, category, intro] for col in required: if col not in df.columns: raise ValueError(f缺少必填列: {col}) # 去重和校验 df df.drop_duplicates(subset[isbn], keepfirst) df df[df[book_name].notna() df[book_name].str.strip().ne()] for _, row in df.iterrows(): create_book( namerow[book_name].strip(), authorrow[author].strip(), categoryrow[category].strip(), introrow.get(intro, ), status1 if row.get(status) 上架 else 0 )这里isbn用字符串读取是为了防止长数字被科学计数法截断。重复 ISBN 默认保留第一条如果你想保留最新数据可以改成keeplast。批量导入后必须调用一次第 2 章的文本向量更新否则新书不会出现在推荐里——这是源码里最容易漏掉的步骤。4.3 评论、点赞、收藏与历史续读用户可以在图书详情页评论、修改评论也可以点赞和收藏。这些行为都会写入用户行为日志表成为协同过滤的输入。历史阅读记录用last_read_at和read_progress百分比两个字段存储用户下次进入详情页时前端根据进度唤起“续读”提示。评论模块最重要是防重复提交。前端重选提交按钮是基础后端还要做唯一约束class Comment(db.Model): __table_args__ ( db.UniqueConstraint(user_id, book_id, nameuix_user_book_comment), ) id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) book_id db.Column(db.Integer, db.ForeignKey(book.id)) content db.Column(db.Text, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.now)这样设计保证一个用户对一本书只能有一条评论后续修改走 UPDATE。如果产品要求允许多次评论把唯一约束去掉但要在查询时取最新一条。4.4 后台统计与数据备份恢复系统管理模块包含运行日志、数据备份恢复、统计图。备份恢复最稳妥的方式是数据库本身的功能而不是在应用层做快照。以 MySQL 为例常见做法是每天凌晨定时mysqldump恢复时只需要source文件。日志模块则要把“推荐结果日志”单独记录——用户看到哪些推荐、点击了哪个这是评估推荐系统的唯一事实来源。# 备份全部数据排除缓存表 mysqldump -u root -p --databases book_recommend --ignore-tablebook_recommend.cache_tb backup_$(date %Y%m%d_%H%M%S).sql # 恢复 mysql -u root -p backup_20250623_120000.sql统计模块的“时间段内新增图书和新增用户”用一条 SQL 就能完成SELECT DATE(created_at) AS day, COUNT(DISTINCT book_id) AS new_books, COUNT(DISTINCT user_id) AS new_users FROM audit_log WHERE created_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(created_at) ORDER BY day;这里audit_log是源码里记录新增操作的统一表用 DISTINCT 防止同一本书多次编辑导致重复计数。展示给管理员时前端用 ECharts 折线图即可满足。5. 推荐系统的参数调优与线上验证技巧5.1 相似度阈值、K值和融合权重的调参顺序不要一上来就瞎调 alpha。我调这套源码的顺序是先定文本相似度 TopK再定协同过滤行为分值最后才动混合权重。具体做法是——取一个已知图书的相似列表人为标出“明显相关”和“明显无关”用精确率衡量。比如取 500 本书每本检查 Top10 里真正的相关书占比这比看单个案例可靠。K 值对结果影响如下表以 2 万本图书规模估算K 值召回变化精度变化计算耗时5低候选少高噪声少最快10中等中等快20较高下降明显中等50最高严重下降慢源码默认 TopK10我的经验是图书简介长度小于 50 字时建议增大到 15利用更多文本特征简介都很长时10 以内更安全否则长简介图书会霸占相似列表。5.2 离线评估与A/B测试的最小验证方案离线评估采用简单的留一法从用户历史行为里随机隐藏 10% 的收藏记录用剩余 90% 训练看隐藏的收藏是否出现在推荐 TopN 中。命中率即recall10这个指标已经能说明问题。import random def evaluate(data_loader, recommend_fn, hidden_ratio0.1, topn10): hits 0 total 0 for user in data_loader.users(): history data_loader.get_history(user) if len(history) 2: continue hidden random.sample(history, max(1, int(len(history) * hidden_ratio))) train [b for b in history if b not in hidden] recs recommend_fn(user, train, topntopn) hits len(set(recs) set(hidden)) total len(hidden) return hits / total if total else 0注意recommend_fn的入参需要改成接收训练行为列表不能内部去读全量数据库否则测试会数据泄漏。线上 A/B 测试则更简单把用户按用户 ID 哈希分流到对照组纯协同过滤和实验组混合方案比较 CTR 和次日回访率。样本量达到每日 1 万请求时跑一周基本能判断差异是否显著。最后一个小技巧把推荐结果落库。每次用户请求推荐时将推荐列表和当时用的参数版本写入recommend_log排错时直接回放当时的参数不用猜测是哪次配置改动导致了线上效果回落。这一点在源码的日志模块里预留了接口补上后你这套系统的可持续维护性就能超过大多数课程设计项目。本文还有配套的精品资源点击获取
返回列表