ARTICLE DETAIL

资讯详情

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

基于Django与协同过滤的儿童图书推荐系统实战

基于Django与协同过滤的儿童图书推荐系统实战 简介本资源为基于Python与Django框架、融合协同过滤算法的儿童图书推荐系统完整开发资料包面向计算机相关专业毕业设计学生及推荐系统入门开发者帮助解决从算法选型到工程落地的全流程问题。压缩包共1121个文件约61.61MB涵盖106个Python源码文件、4个SQL数据库脚本、204个Vue前端组件、126个JavaScript脚本及318个SVG图标资源另含bat启动脚本、json配置与doc说明文档前后端与数据库结构一应俱全。已有59人学习下载。资料完整呈现用户基与物品基协同过滤的相似度计算逻辑、用户行为数据表设计及推荐列表生成流程读者可据此掌握Django项目搭建、数据库建模与算法实现要点并参考目录结构快速定位核心模块适合作为毕设参考或推荐系统练手项目。1. 儿童图书推荐系统从协同过滤到 Django 落地的完整路径给儿童选书这件事看起来简单实际做起来比给成年人推荐电影要难得多。成年人推荐错了大不了浪费一张电影票儿童图书推荐错了轻则孩子翻两页就扔一边重则家长觉得这个系统不靠谱直接卸载。我接过一个类似的需求对方是做儿童绘本租赁的手里有几千个家庭的借阅记录想做一个「猜你喜欢」的功能。一开始想得很简单不就是协同过滤吗UserCF 跑一遍就完事了。结果真正落地的时候发现儿童图书的推荐和电商推荐完全是两码事——数据稀疏得可怕一个孩子半年可能只借了十几本书而且年龄段、阅读能力、家长偏好这三个维度互相纠缠不是简单打个分就能解决的。这篇文章要聊的就是基于 Python Django 协同过滤算法搭建一套儿童图书推荐系统从数据准备、算法选型、Django 接口设计到线上踩坑把整条链路走一遍。适合两类人看一是手里有儿童图书借阅数据、想快速搭一个推荐原型的开发者二是正在做 Django 项目实战、想找一个有真实业务复杂度的练手项目的新手。我不会只讲算法公式也不会只贴 Django 配置而是把「为什么这么选」和「具体怎么写」揉在一起讲。源码和数据库文档是起点但真正值钱的是中间那些翻车经验。2. 协同过滤在儿童图书场景的选型UserCF 还是 ItemCF2.1 为什么儿童图书推荐不能直接套用电商那套电商推荐的核心假设是「用户行为量大且密集」一个活跃用户一个月能产生几百条点击、加购、购买记录。儿童图书借阅场景完全不是这样。我拿到的那份数据里平均每个孩子只有 12 条借阅记录最少的只有 3 条。这种稀疏程度下UserCF 找相似用户会非常困难——两个用户共同借过的书可能只有一两本算出来的相似度噪声极大。另一个区别是消费周期。电商冲动消费多今天推明天买儿童图书借阅有明确的年龄阶段一个 5 岁孩子的借阅偏好和 7 岁孩子差异巨大但和同班同学高度重合。这意味着时间衰减因子在儿童图书场景里要比电商更激进半年前的借阅记录参考价值已经很低了。还有一个容易被忽略的点儿童图书的「评分」不是显式打分而是借阅行为本身。借了没还说明喜欢借了三天就还说明一般续借了说明很喜欢。这些隐式反馈需要做加权处理不能简单地把「借过」当成「喜欢」。2.2 UserCF 和 ItemCF 在儿童图书场景的对比先看一张对比表这是我实际跑过两版之后总结的维度UserCFItemCF相似度计算对象用户之间图书之间数据稀疏敏感度高用户共同借阅少时失效低图书被共同借阅的概率更高可解释性差「和你相似的小朋友也借了」好「借过这本书的人还借了」新用户冷启动差没有历史行为无法找相似用户较好可以基于图书标签做兜底新图书冷启动较好新书可以被相似用户借阅差没有借阅记录无法算相似度计算复杂度用户数平方级图书数平方级儿童场景适用性用户量少时可用更推荐图书数量通常远小于用户数结论很明确儿童图书推荐优先选 ItemCF。原因有三。第一图书数量通常比用户数量少一个数量级计算相似度矩阵更省资源。第二ItemCF 的可解释性对家长很重要「借过《猜猜我有多爱你》的人还借了《逃家小兔》」这种推荐理由家长一看就懂。第三新用户冷启动可以用热门图书 年龄段标签兜底不需要等用户积累行为。但 ItemCF 也有一个坑如果两本书被同一个孩子借过但这两本书一个是 3 岁绘本、一个是 10 岁章节书那这个共现关系其实是噪声。所以计算相似度之前必须按年龄段做分桶只在同一年龄段内计算图书相似度。2.3 相似度计算的具体实现与参数调整ItemCF 的核心是算图书之间的相似度。常用的有余弦相似度、皮尔逊相关系数、改进的余弦相似度。儿童图书场景我推荐用改进的余弦相似度因为它能惩罚热门图书的共现噪声。import numpy as np from collections import defaultdict def build_item_similarity(borrow_records, age_bucket, top_k20): 构建图书相似度矩阵 borrow_records: list of (user_id, book_id, age_bucket, weight) age_bucket: 当前计算的年龄段只在该年龄段内计算 top_k: 每本书保留最相似的 top_k 本 # 1. 筛选当前年龄段的记录 filtered [r for r in borrow_records if r[2] age_bucket] # 2. 构建图书-用户倒排表 item_users defaultdict(set) for user_id, book_id, _, weight in filtered: item_users[book_id].add(user_id) # 3. 计算共现矩阵 item_count len(item_users) cooccur defaultdict(int) for book_id, users in item_users.items(): for u in users: for v in users: if u ! v: cooccur[(book_id, v)] 1 # 4. 计算改进余弦相似度 # 改进点分母用 log(1 |N(i)|) 惩罚热门图书 similarity {} for (i, j), count in cooccur.items(): if i j: continue ni len(item_users[i]) nj len(item_users[j]) # 改进余弦相似度公式 sim count / np.sqrt(ni * nj) * (1 / np.log(1 ni)) similarity[(i, j)] sim similarity[(j, i)] sim # 5. 每本书保留 top_k 相似 item_sim_list defaultdict(list) for (i, j), sim in similarity.items(): item_sim_list[i].append((j, sim)) result {} for book_id, sim_list in item_sim_list.items(): sim_list.sort(keylambda x: x[1], reverseTrue) result[book_id] sim_list[:top_k] return result这段代码有几个关键参数需要说明。top_k20表示每本书只保留最相似的 20 本这个值不能太大否则推荐结果会发散也不能太小否则召回率不够。我实测下来 15 到 25 之间比较合适图书总量少的时候可以调到 30。age_bucket是年龄段分桶我一般按 0-3 岁、3-6 岁、6-9 岁、9-12 岁分四档分得太细会导致每个桶里数据不够分得太粗又起不到过滤噪声的作用。改进余弦相似度里的1 / np.log(1 ni)是惩罚项ni 是图书 i 被借阅的人数。热门图书比如《不一样的卡梅拉》可能被几百个孩子借过如果不惩罚它会和几乎所有书都产生高相似度推荐结果里全是热门书失去个性化意义。2.4 推荐结果生成与 Django 接口对接相似度矩阵算好之后推荐结果生成就简单了拿用户最近借过的 N 本书从每本书的相似列表中取 top_k加权合并去掉已经借过的按分数排序返回。def recommend_for_user(user_id, user_history, item_sim, top_n10): 为用户生成推荐列表 user_history: list of (book_id, weight)按时间倒序 item_sim: build_item_similarity 返回的相似度字典 top_n: 返回推荐数量 # 1. 取用户最近借阅的 5 本书作为种子 recent_books [b for b, _ in user_history[:5]] borrowed_set set(b for b, _ in user_history) # 2. 加权合并相似图书 scores defaultdict(float) for book_id in recent_books: if book_id not in item_sim: continue for sim_book, sim_score in item_sim[book_id]: if sim_book in borrowed_set: continue scores[sim_book] sim_score # 3. 排序返回 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [book_id for book_id, _ in ranked[:top_n]]在 Django 里这个逻辑通常放在recommend/services.py里视图层只负责取参数和返回 JSON。接口设计上我建议用GET /api/recommend/?user_idxxxlimit10返回结果里带上推荐理由比如「因为您借过《好饿的毛毛虫》」这样前端展示更友好。提示相似度矩阵不要每次请求都实时计算图书数量上千之后计算一次要好几秒。常见做法是离线跑批每天凌晨更新一次存到 Redis 或数据库表里接口只做查询和合并。3. Django 项目结构设计与数据库建模3.1 儿童图书推荐系统的数据表设计数据库设计是这套系统的地基地基没打好后面全是坑。核心表有六张用户表、图书表、借阅记录表、图书标签表、用户画像表、推荐结果缓存表。用户表不用多说Django 自带的AbstractUser扩展一下就行加上age、grade、reading_level三个字段。图书表除了基本信息一定要有age_min和age_max两个字段表示这本书适合的年龄段这是后续做分桶和兜底推荐的关键。借阅记录表是核心字段包括user_id、book_id、borrow_date、return_date、renew_count其中renew_count是续借次数用来做隐式评分加权。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): age models.IntegerField(nullTrue, blankTrue) grade models.CharField(max_length20, blankTrue) reading_level models.CharField(max_length20, blankTrue) class Book(models.Model): title models.CharField(max_length200) author models.CharField(max_length100, blankTrue) isbn models.CharField(max_length20, uniqueTrue) age_min models.IntegerField(default0) age_max models.IntegerField(default12) category models.CharField(max_length50, blankTrue) cover_url models.URLField(blankTrue) class BorrowRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) book models.ForeignKey(Book, on_deletemodels.CASCADE) borrow_date models.DateField() return_date models.DateField(nullTrue, blankTrue) renew_count models.IntegerField(default0) class Meta: indexes [ models.Index(fields[user, borrow_date]), models.Index(fields[book, borrow_date]), ]借阅记录表上必须建联合索引user borrow_date用于查用户历史book borrow_date用于算图书相似度。没有索引的话数据量到十万级查询就会明显变慢。3.2 Django app 划分与协同过滤模块的接入方式我一般把项目拆成四个 appusers管用户和认证books管图书和标签borrow管借阅记录recommend管推荐算法和接口。这样拆的好处是推荐模块可以独立测试不依赖 Web 层。协同过滤的计算逻辑放在recommend/services.py对外暴露三个函数build_item_similarity、recommend_for_user、refresh_similarity_cache。refresh_similarity_cache是一个管理命令通过python manage.py refresh_similarity调用内部从数据库拉数据、算相似度、写回缓存表。# recommend/management/commands/refresh_similarity.py from django.core.management.base import BaseCommand from recommend.services import build_item_similarity, save_similarity_to_cache from borrow.models import BorrowRecord class Command(BaseCommand): help 刷新图书相似度缓存 def handle(self, *args, **options): records BorrowRecord.objects.filter( return_date__isnullFalse ).values_list(user_id, book_id, book__age_min, renew_count) # 按年龄段分桶计算 for age_bucket in [0-3, 3-6, 6-9, 9-12]: sim build_item_similarity(records, age_bucket) save_similarity_to_cache(age_bucket, sim) self.stdout.write(f{age_bucket} 相似度计算完成共 {len(sim)} 本书)这个管理命令建议放到 crontab 里每天凌晨 3 点跑一次。注意return_date__isnullFalse这个过滤条件只算已经归还的记录正在借阅中的记录行为意图不明确不适合作为正样本。3.3 冷启动兜底策略新用户和新图书怎么推新用户没有借阅历史ItemCF 完全失效。这时候需要兜底策略我一般用三层第一层按年龄段推热门图书第二层按同班同学借阅最多的书推第三层推编辑精选。def cold_start_recommend(user, top_n10): 新用户冷启动推荐 # 第一层年龄段热门 age user.age or 6 hot_books Book.objects.filter( age_min__lteage, age_max__gteage ).annotate( borrow_countmodels.Count(borrowrecord) ).order_by(-borrow_count)[:top_n] if len(hot_books) top_n: return hot_books # 第二层同班同学借阅 classmates User.objects.filter(gradeuser.grade).exclude(iduser.id) class_books Book.objects.filter( borrowrecord__user__inclassmates ).annotate( cntmodels.Count(borrowrecord) ).order_by(-cnt)[:top_n] # 合并去重 result list(hot_books) for book in class_books: if book not in result: result.append(book) if len(result) top_n: break return result[:top_n]新图书的冷启动反过来没有借阅记录算不了相似度。我的做法是给新书打标签用基于内容的推荐兜底找和这本书标签最相似的老书把老书的相似列表借过来用。等新书积累到一定借阅量我设的阈值是 20 次再切换到 ItemCF。4. 推荐系统落地避坑数据稀疏、实时性与评估指标4.1 数据稀疏导致推荐结果重复率过高现象推荐列表里翻来覆去就是那几本热门书用户反馈「怎么老是这几本」。原因儿童图书借阅数据稀疏ItemCF 算出来的相似图书数量不够很多书只有一两个相似邻居推荐时只能反复用热门书填充。解决三个措施一起上。第一降低相似度阈值从 0.1 降到 0.05让更多弱相似关系进入候选池。第二引入基于内容的相似度做补充用图书的标签、作者、出版社算一个内容相似度和协同过滤相似度加权融合权重我一般设 0.7 协同 0.3 内容。第三推荐结果做多样性打散同一作者或同一系列的书最多出现两本。4.2 实时推荐与离线计算的取舍现象用户刚借了一本书推荐列表没有变化家长觉得系统「不智能」。原因相似度矩阵是离线算的推荐结果也是离线生成的实时性差。解决不要试图做全实时推荐成本太高。我的方案是「离线相似度 实时合并」。相似度矩阵每天更新一次但推荐结果在接口层实时生成用户请求时取用户最新的借阅记录作为种子从离线相似度矩阵里查相似图书实时合并排序。这样用户刚借完书下一次请求推荐列表就会变化而计算量只有一次查表和排序响应时间在 50ms 以内。4.3 评估指标选错导致优化方向跑偏现象离线评估 AUC 很高上线后点击率却很低。原因离线评估用的是「预测用户下一本借什么」但线上推荐展示的是 top10 列表用户只看前几个。AUC 衡量的是整体排序能力和 top10 的命中率不是一回事。解决离线评估用 Recall10 和 NDCG10这两个指标直接衡量 top10 列表的质量。另外一定要做在线 A/B 测试离线指标只做粗筛最终决策看线上的点击率和借阅转化率。我踩过的坑是离线 Recall10 提升了 5%上线后点击率反而降了 2%原因是推荐结果太集中用户审美疲劳。4.4 年龄段分桶边界处理不当引入噪声现象给 5 岁孩子推荐了 8 岁才适合的章节书家长投诉。原因年龄段分桶用的是age_min和age_max的硬边界但很多书的年龄标注本身就不准确而且孩子的阅读能力个体差异很大。解决分桶时用软边界一本书如果age_min6那它在 3-6 岁桶里的权重设为 0.3在 6-9 岁桶里权重为 1.0。推荐结果生成后再用孩子的reading_level做一次过滤阅读水平低于平均的孩子推荐结果里低龄书权重上调。4.5 缓存更新与数据库不一致现象图书信息改了但推荐结果里还是旧封面、旧标题。原因相似度缓存表里存了图书 ID 和相似度但推荐接口返回时又从缓存里取了图书基本信息图书更新后缓存没刷新。解决缓存里只存图书 ID 和相似度分数图书详情每次从数据库查。如果 QPS 高可以加一层 Redis 缓存图书详情但设置较短的过期时间比如 5 分钟并在图书更新时主动删除对应缓存。5. 推荐效果验证与线上调优的实用技巧推荐系统上线只是开始真正的功夫在调优。我一般用「离线评估 → 小流量 A/B → 全量」三步走每一步都有具体的操作技巧。离线评估阶段除了 Recall10 和 NDCG10我还会看一个指标叫「推荐覆盖率」就是推荐列表里出现过的图书占总图书的比例。这个指标低于 30% 说明推荐太集中需要增加多样性。计算方式很简单def coverage(all_recommendations, total_books): 推荐覆盖率 recommended_books set() for rec_list in all_recommendations: recommended_books.update(rec_list) return len(recommended_books) / total_books小流量 A/B 阶段我一般切 10% 的用户做实验组对照组用热门推荐。观察周期至少两周因为儿童图书借阅周期长一周内的数据波动太大。核心看两个指标推荐位点击率和借阅转化率。点击率提升但转化率没变说明推荐标题或封面有问题两个都提升才算真正有效。线上调优有几个实用技巧。第一推荐理由要具体不要写「猜你喜欢」写「借过《好饿的毛毛虫》的小朋友还借了这本」点击率能差一倍。第二推荐列表不要一次给太多top10 足够了给 20 个反而稀释了点击。第三新用户前三次推荐一定要准这是建立信任的关键期我一般对新用户用编辑精选 同班热门混合不用协同过滤。最后说一个我自己的习惯每次调整算法参数我都会在数据库里记一条调优日志包括调整时间、调整内容、离线指标变化、线上指标变化。这个习惯帮我避免了很多次「改了参数但忘了为什么改」的尴尬。推荐系统调优是个长期活没有一劳永逸的参数只有持续观察和迭代。希望帮到你。本文还有配套的精品资源点击获取
返回列表