ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Django实战:基于协同过滤的动漫推荐系统从数据到接口全流程

2026/10/4 1:07:29 拓冰建站 浏览量
Django实战:基于协同过滤的动漫推荐系统从数据到接口全流程 简介本资源是一套基于Django与Python协同过滤算法实现的动漫推荐系统完整项目面向计算机相关专业毕业设计学生及推荐算法入门开发者帮助解决从零搭建推荐系统、理解协同过滤落地流程的问题。压缩包共585个文件约19.98MB包含60个py后端源码、99个vue前端组件、63个js脚本、159个svg图标及43张png、42张jpg界面素材另有2个sql数据库脚本与数据库文档前后端分离结构清晰。系统覆盖用户管理、动漫信息展示与用户行为交互三大模块支持邮箱手机号及第三方登录、偏好问卷引导、评分与收藏等显式隐式反馈采集并据此驱动协同过滤推荐动漫模块提供类型、年代、评分、声优等多维检索与专题合集交互模块含短评长评、弹幕与话题讨论。已有78人学习下载读者可获得可运行的完整工程、数据库设计文档与推荐算法实现思路适合作为毕设参考或二次开发基础。1. 动漫推荐系统遇上协同过滤一个 Django 项目从数据到推荐的完整落地路径很多人做推荐系统第一反应是上深度学习但真正在动漫场景里跑起来基于用户的协同过滤往往才是性价比最高的起点。原因很直接动漫用户的评分行为高度聚集一部番的受众画像比电影更集中用户之间的相似度信号非常强。这个项目要解决的核心问题是——给定一批用户对动漫的评分数据如何找到口味相近的人把对方喜欢而你没看过的番推给你。技术栈选 Django Python 协同过滤数据库用 MySQL 或 SQLite 都行。适合谁适合正在找 Django 项目实战练手的新手也适合想理解推荐算法工程化落地而非只跑 notebook 的开发者。下面从数据建模一路讲到推荐接口和避坑。2. 数据模型与数据库设计动漫、用户、评分三张表怎么建2.1 为什么协同过滤的数据模型不能随便建协同过滤的输入本质上是一个「用户-物品评分矩阵」。矩阵的稀疏度、评分粒度、时间戳的有无直接决定了后面算法能不能跑、跑出来准不准。很多新手上来就建一张大宽表用户 ID 做列、动漫 ID 做行结果数据一多表就炸了。正确做法是拆成三张核心表加若干辅助表用关系型数据库的范式来存查询时再拼成矩阵。动漫表存番剧元信息用户表用 Django 自带的 AbstractUser 扩展评分表是核心——它记录谁给哪部番打了多少分、什么时候打的。评分表上必须建联合唯一索引防止同一用户对同一部番重复评分这是后面算相似度时数据干净的前提。2.2 三张核心表的字段设计与建表代码# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): # 扩展用户表加个昵称和注册时间就够了 nickname models.CharField(max_length50, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table user class Anime(models.Model): title models.CharField(max_length200, db_indexTrue) # 番名加索引方便搜索 genre models.CharField(max_length200, blankTrue) # 类型多个用逗号分隔 year models.IntegerField(nullTrue, blankTrue) # 年份 score models.FloatField(default0) # 站内均分 cover_url models.URLField(blankTrue) # 封面图 summary models.TextField(blankTrue) # 简介 class Meta: db_table anime class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) anime models.ForeignKey(Anime, on_deletemodels.CASCADE) score models.FloatField() # 评分 1-10 created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table rating # 联合唯一索引一个用户对一部番只能有一条评分 unique_together (user, anime) indexes [ models.Index(fields[user, anime]), ]字段说明score用 FloatField 而不是 IntegerField是因为后面算余弦相似度时浮点精度更稳unique_together是防重复评分的硬约束别指望前端拦db_indexTrue加在title上因为搜索页会频繁按番名模糊查。迁移命令就两条python manage.py makemigrations python manage.py migrate2.3 评分数据的导入与清洗真实场景里评分数据往往来自爬虫或 CSV 导入。导入前必须做三件事去掉评分为空的记录、把评分统一到 1-10 区间、剔除评分次数少于 5 次的用户冷启动用户对相似度计算是噪声。用 Django shell 或 management command 都行# 清洗评分数据的核心逻辑 from app.models import Rating, User from django.db.models import Count # 找出评分少于 5 条的用户他们的数据先不参与相似度计算 active_users Rating.objects.values(user).annotate( cntCount(id) ).filter(cnt__gte5).values_list(user, flatTrue) # 删除无效评分分数为 0 或超过 10 的脏数据 Rating.objects.filter(score__lte0).delete() Rating.objects.filter(score__gt10).delete()这里有个参数要留意cnt__gte5这个阈值不是拍脑袋定的。阈值太低相似度矩阵噪声大太高能参与推荐的用户太少。一般从 5 开始试数据量大就提到 10。清洗完记得把结果落库别每次推荐都重算。3. 协同过滤算法实现从评分矩阵到相似度计算3.1 基于用户还是基于物品动漫场景怎么选协同过滤分两大流派UserCF 和 ItemCF。UserCF 找和你口味像的人推他们喜欢的番ItemCF 找你喜欢的番相似的番。动漫场景我一般选 ItemCF 为主、UserCF 为辅。原因是动漫的「物品」数量远小于用户数量ItemCF 的相似度矩阵更小、更稳定而且新番上线时只要有人看过就能算相似度不像 UserCF 那样依赖用户活跃度。但 UserCF 在「发现冷门好番」上更强所以两个都实现前端给个切换。3.2 用 Python 构建评分矩阵并算余弦相似度核心思路把 Rating 表拉出来用 pandas 透视成用户-动漫矩阵缺失值填 0然后算余弦相似度。代码不复杂但内存和稀疏性是坑。import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity from app.models import Rating def build_matrix(): # 拉取所有评分values 里只取需要的字段减少内存 qs Rating.objects.values(user_id, anime_id, score) df pd.DataFrame(list(qs)) # 透视成矩阵行是用户列是动漫 matrix df.pivot_table( indexuser_id, columnsanime_id, valuesscore, fill_value0 ) return matrix def item_similarity(matrix): # 转置后行是动漫算动漫之间的余弦相似度 item_matrix matrix.T sim cosine_similarity(item_matrix) return pd.DataFrame( sim, indexitem_matrix.index, columnsitem_matrix.index )逻辑说明pivot_table的fill_value0是关键——协同过滤里没评分就是 0不能填均值否则相似度会被拉偏。cosine_similarity返回的是对称矩阵对角线是 1。参数上如果动漫数量超过几千这个稠密矩阵会吃光内存这时候要换稀疏矩阵方案scipy.sparse后面避坑章节会讲。3.3 生成推荐列表的完整函数有了相似度矩阵给某个用户推荐就是找他评分高的番看这些番和哪些番最像加权求和排序。def recommend_for_user(user_id, matrix, item_sim, top_n10): # 该用户的评分向量 user_ratings matrix.loc[user_id] # 只看他评过分的番 rated user_ratings[user_ratings 0] if rated.empty: return [] # 冷启动用户走热门兜底 scores {} for anime_id, rating in rated.items(): # 取这部番最相似的 20 部 sim_row item_sim[anime_id].sort_values(ascendingFalse)[1:21] for sim_anime, sim_score in sim_row.items(): if sim_anime in rated.index: continue # 已经看过的跳过 scores[sim_anime] scores.get(sim_anime, 0) sim_score * rating # 按加权分排序取前 N ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]参数说明[1:21]是取相似度最高的 20 部去掉自己这个数字影响推荐多样性和计算量一般 10-30 之间调。sim_score * rating是加权策略也可以用sim_score * (rating - user_mean)做去偏效果更稳但代码稍复杂。冷启动用户直接返回空由上层走热门推荐兜底别硬算。4. Django 接口与推荐结果落地把算法接进 Web 项目4.1 推荐结果要不要缓存怎么缓存协同过滤的计算成本主要在相似度矩阵这个矩阵不需要每次请求都重算。我的做法是相似度矩阵离线算好存到 Redis 或数据库的缓存表里推荐接口只做查表和加权排序。Django 里可以用cache框架也可以用一张RecommendCache表。数据量不大时直接存 pickle 到文件也行但生产环境别这么干。# views.py from django.core.cache import cache from django.http import JsonResponse from app.services import get_item_similarity, recommend_for_user, build_matrix def recommend_view(request): user_id request.user.id cache_key frec_{user_id} cached cache.get(cache_key) if cached: return JsonResponse({list: cached, from_cache: True}) matrix build_matrix() item_sim get_item_similarity() # 从缓存或文件加载 result recommend_for_user(user_id, matrix, item_sim, top_n10) # 推荐结果缓存 10 分钟 cache.set(cache_key, result, 600) return JsonResponse({list: result, from_cache: False})cache.set的第三个参数是过期秒数600 秒是个折中——太短缓存没意义太长新评分反映不进来。用户产生新评分时记得主动cache.delete(frec_{user_id})这就是「后悔药」不然用户打完分刷新页面推荐没变体验很差。4.2 推荐接口的 URL 配置与前端对接# urls.py from django.urls import path from app import views urlpatterns [ path(api/recommend/, views.recommend_view, namerecommend), path(api/rating/, views.rating_view, namerating), ]前端拿到list后里面是(anime_id, score)的列表再调一个批量查动漫详情的接口渲染卡片。注意别在推荐接口里直接返回完整动漫对象那样响应体太大而且推荐逻辑和展示逻辑耦合了。分开查前端用anime_id批量请求。4.3 评分提交接口与实时更新def rating_view(request): if request.method POST: anime_id request.POST.get(anime_id) score float(request.POST.get(score)) Rating.objects.update_or_create( userrequest.user, anime_idanime_id, defaults{score: score} ) # 清掉该用户的推荐缓存下次请求重新算 cache.delete(frec_{request.user.id}) return JsonResponse({ok: True})update_or_create保证重复评分是更新而不是插入配合前面的唯一索引双保险。清缓存这一步别省这是推荐系统「活」起来的关键。5. 避坑与排查协同过滤在 Django 里最容易翻车的 5 个点5.1 现象推荐结果永远是那几部热门番原因评分矩阵太稀疏大部分动漫之间相似度为 0只有热门番有足够评分支撑相似度计算导致推荐被热门霸榜。解决在加权分里加一个热度惩罚项或者对相似度做归一化。简单做法是final_score weighted_score / (1 log(1 anime_popularity))让热门番的分数被压一压。5.2 现象接口响应越来越慢最后超时原因每次请求都重新build_matrix()数据量上来后 pandas 透视和余弦相似度计算是 O(n²) 级别。解决相似度矩阵离线定时任务算celery 或 cron存 Redis推荐接口只做查表和排序。矩阵超过 5000×5000 就上 scipy 稀疏矩阵别用稠密矩阵硬扛。5.3 现象新用户进来推荐接口报错或返回空原因冷启动用户没有评分记录matrix.loc[user_id]直接 KeyError。解决推荐函数入口先判断用户是否有评分没有就走热门榜或让用户选几个喜欢的类型做初始画像。别让接口抛异常返回空列表前端也要有兜底 UI。5.4 现象评分数据里有重复记录相似度算出来不对原因前端没拦重复提交或者导入数据时没去重unique_together在批量导入时可能被绕过。解决导入前用Rating.objects.filter(useru, animea).delete()清一遍或者用bulk_create时加ignore_conflictsTrue。数据库层的唯一索引是最后防线但别只靠它。5.5 现象换了数据库SQLite 转 MySQL后中文番名乱码原因MySQL 建库时字符集没设 utf8mb4Django 连接配置里也没指定。解决建库语句用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ciDjango 的DATABASES配置里加OPTIONS: {charset: utf8mb4}。SQLite 默认 utf-8 所以本地没事一上 MySQL 就翻车这个坑我踩过不止一次。6. 进阶技巧用混合推荐和离线评估把效果再拉一档纯协同过滤跑通之后想再进一步两个方向最实在混合推荐和离线评估。混合推荐的做法是把 ItemCF 的推荐结果和基于内容的推荐按 genre、year 算相似按权重融合。比如final 0.7 * cf_score 0.3 * content_score权重用 A/B 测试调。内容相似度计算很简单把 genre 做 one-hot算余弦就行。这样能缓解协同过滤对冷门番不友好的问题。离线评估是很多人忽略的一步。别凭感觉说「推荐准了」用留一法把每个用户最后一条评分藏起来用剩下的数据训练看推荐列表里有没有命中藏起来的那部番。指标用 Hit Rate 和 NDCG。def evaluate_hit_rate(matrix, item_sim, k10): hits 0 total 0 for user_id in matrix.index: user_ratings matrix.loc[user_id] rated user_ratings[user_ratings 0] if len(rated) 2: continue # 藏最后一条 holdout rated.index[-1] train rated.iloc[:-1] # 用 train 生成推荐简化版实际要传 train 矩阵 recs recommend_for_user(user_id, matrix, item_sim, top_nk) rec_ids [r[0] for r in recs] if holdout in rec_ids: hits 1 total 1 return hits / total if total else 0这个评估函数跑一次可能要几分钟但值得。Hit Rate 低于 0.1 就说明推荐基本没效果得回去查数据质量或调相似度阈值。我一般会把评估脚本做成 management command每次改完算法跑一遍心里有数。最后一个习惯所有推荐相关的参数相似度取 top 几、缓存多久、混合权重都写进 settings 或数据库配置表别硬编码在代码里。调参时改配置重启就行不用翻代码。这个项目从数据建模到推荐接口再到评估整条链路跑通大概两三天但调参和优化能磨一周。值不值得做如果你想把推荐系统从「跑个 demo」变成「能上线给人用」这套路径是最短的那条。希望帮到你。本文还有配套的精品资源点击获取