ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Django协同过滤动漫推荐系统源码实战:从算法到部署

Django协同过滤动漫推荐系统源码实战:从算法到部署 简介这是一套面向高校计算机专业毕业设计与课程实践的动漫推荐系统完整源码包基于Python与Django框架开发核心采用协同过滤算法实现个性化推荐。资源涵盖用户管理、动漫信息展示与用户行为交互三大模块用户端支持注册登录、偏好问卷、历史记录与社交关注动漫端提供多维度分类检索、专题合集与详情展示行为端则通过评分、收藏、弹幕、评论等显式与隐式反馈采集偏好数据适合作为推荐算法入门与Web全栈开发的综合练手项目。压缩包共585个文件约19.98MB包含159个svg图标、99个vue组件、63个js脚本、60个py源码及48个pyc编译文件另有png、jpg图片素材与sql建表脚本、bat启动脚本等前后端分离结构清晰。目前已有58人学习下载可帮助读者快速理解协同过滤在真实业务中的落地方式并参考数据库文档完成环境搭建与二次开发。1. 从一份 Django 协同过滤动漫推荐系统源码说起它到底能跑出什么结果如果你正在做推荐系统方向的毕业设计或者想找一个能直接跑起来的 Django 实战项目这套「基于 Python 协同过滤的动漫推荐系统」大概率能省掉你从零搭架子的一大半时间。它不是那种只丢一个算法脚本、连数据库都不给的半成品而是把 Django 后端、协同过滤推荐逻辑、数据库文档和前端展示串成了一条完整链路。你拿到手之后能直接看到用户对动漫的评分数据怎么进库、相似度怎么算、推荐列表怎么渲染到页面上。适合的人群很明确一是需要交毕设、要有完整代码和数据库设计文档的学生二是想拿一个真实项目练手 Django 和推荐算法的开发者。它解决的核心问题就一个——把「协同过滤」这个听起来玄乎的东西落成一个你能打开浏览器点进去、能看到推荐结果的系统。2. 协同过滤推荐引擎从评分矩阵到相似度计算的落地拆解2.1 为什么选 User-Based 协同过滤而不是上深度学习这套系统用的是基于用户的协同过滤User-Based CF不是神经协同过滤那套。原因很实际毕设场景下数据量通常就几百个用户、几百部动漫评分矩阵稀疏度还没到必须上 embedding 的程度。User-Based CF 的逻辑链条短每一步都能在代码里找到对应函数答辩的时候老师问「相似度怎么算的」你能直接翻到那一行。常见做法是先用皮尔逊相关系数算用户之间的相似度再取 Top-N 邻居的评分做加权预测。选皮尔逊而不是余弦相似度是因为皮尔逊会减去用户平均分能抵消一部分「有人习惯打高分、有人习惯打低分」的偏差。这个选择在数据量小的时候效果差异很明显我一般会建议先跑皮尔逊如果推荐结果明显偏向热门动漫再换余弦对比一下。2.2 评分矩阵构建与相似度计算的核心代码推荐引擎的入口通常是一个recommend.py或者放在utils目录下的算法模块。下面这段是典型的评分矩阵构建加相似度计算流程我按实际项目里常见的写法还原import numpy as np from math import sqrt def build_rating_matrix(ratings): ratings: list of (user_id, anime_id, score) 返回: user_ids, anime_ids, matrix user_ids sorted(set(r[0] for r in ratings)) anime_ids sorted(set(r[1] for r in ratings)) user_index {uid: i for i, uid in enumerate(user_ids)} anime_index {aid: j for j, aid in enumerate(anime_ids)} matrix np.zeros((len(user_ids), len(anime_ids))) for uid, aid, score in ratings: matrix[user_index[uid]][anime_index[aid]] score return user_ids, anime_ids, matrix def pearson_similarity(matrix, user_a, user_b): 计算两个用户之间的皮尔逊相关系数 只计算两人都评过分的动漫 a matrix[user_a] b matrix[user_b] # 取共同评分项 mask (a 0) (b 0) if mask.sum() 2: return 0 # 共同评分太少相似度置零 a_common a[mask] b_common b[mask] a_mean a_common.mean() b_mean b_common.mean() numerator np.sum((a_common - a_mean) * (b_common - b_mean)) denominator sqrt(np.sum((a_common - a_mean) ** 2)) * sqrt(np.sum((b_common - b_mean) ** 2)) if denominator 0: return 0 return numerator / denominator这段代码有两个关键参数需要留意。第一个是mask.sum() 2这个阈值共同评分少于 2 项时直接返回 0避免算出无意义的相似度。第二个是矩阵用 0 表示未评分所以后面做加权预测时必须把 0 过滤掉否则会把「没看过」当成「打了 0 分」来处理推荐结果会直接翻车。我见过不少新手在这里踩坑推荐出来的动漫全是没人看过的冷门货排查半天才发现是 0 值没处理。2.3 加权评分预测与 Top-N 推荐生成相似度算完之后下一步是给目标用户没看过的动漫打分。逻辑是找和目标用户最相似的 K 个邻居用他们的评分做加权平均。代码大概长这样def predict_score(matrix, user_index, target_user, anime_id, k10): 预测 target_user 对 anime_id 的评分 k: 取相似度最高的前 k 个邻居 target_vec matrix[user_index[target_user]] similarities [] for other_user in range(matrix.shape[0]): if other_user user_index[target_user]: continue sim pearson_similarity(matrix, user_index[target_user], other_user) if sim 0: # 只取正相关邻居 similarities.append((other_user, sim)) similarities.sort(keylambda x: x[1], reverseTrue) neighbors similarities[:k] numerator 0 denominator 0 for neighbor, sim in neighbors: score matrix[neighbor][anime_id] if score 0: numerator sim * score denominator abs(sim) if denominator 0: return 0 return numerator / denominator这里的k10是邻居数量实际调的时候可以试 5、10、20 三档。k 太小推荐结果不稳定k 太大又会把不相关的用户拉进来稀释信号。我一般会先固定 k10 跑一遍看推荐列表里有没有明显不相关的动漫有的话就往下调。另外sim 0这个过滤条件也很重要负相关的用户直接排除不然会出现「因为你们口味相反所以推荐这个」的诡异逻辑。3. Django 后端集成模型设计、视图路由与推荐结果渲染3.1 数据库模型设计User、Anime、Rating 三张核心表Django 的 ORM 让数据库设计变得很直观但推荐系统的表结构有几个容易忽略的点。核心就三张表用户表直接用 Django 自带的User动漫表存动漫元信息评分表存用户对动漫的评分记录。下面是一个典型的models.py写法from django.db import models from django.contrib.auth.models import User class Anime(models.Model): title models.CharField(max_length200, verbose_name动漫名称) genre models.CharField(max_length100, verbose_name类型) description models.TextField(blankTrue, verbose_name简介) cover_url models.URLField(blankTrue, verbose_name封面地址) release_year models.IntegerField(default2020, verbose_name年份) class Meta: db_table anime verbose_name 动漫 def __str__(self): return self.title class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) anime models.ForeignKey(Anime, on_deletemodels.CASCADE, verbose_name动漫) score models.FloatField(verbose_name评分) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table rating unique_together (user, anime) # 防止重复评分 verbose_name 评分记录unique_together这个约束是血泪经验。没有它的话同一个用户对同一部动漫可以反复提交评分推荐矩阵里就会出现重复行相似度计算直接乱掉。另外score用FloatField而不是IntegerField是因为有些评分场景会用 0.5 分制整数存不下。如果你确定只用 1-5 分整数改成IntegerField也行但迁移的时候记得把已有数据转一下。3.2 视图层把推荐结果塞进 Django 的上下文视图层要做的事情很明确接收请求、调推荐算法、把结果传给模板。下面是一个基于函数视图的写法比类视图更直白from django.shortcuts import render from django.contrib.auth.decorators import login_required from .models import Anime, Rating from .recommend import build_rating_matrix, predict_score login_required def recommend_list(request): # 1. 从数据库拉全部评分记录 ratings list(Rating.objects.values_list(user_id, anime_id, score)) user_ids, anime_ids, matrix build_rating_matrix(ratings) # 2. 当前用户在所有用户中的索引 current_uid request.user.id if current_uid not in user_ids: # 新用户没有评分记录返回热门动漫兜底 hot_animes Anime.objects.all()[:12] return render(request, recommend/list.html, {animes: hot_animes}) user_index {uid: i for i, uid in enumerate(user_ids)} # 3. 对当前用户没看过的动漫逐个预测评分 rated_anime_ids set(Rating.objects.filter(userrequest.user).values_list(anime_id, flatTrue)) candidates [] for anime in Anime.objects.exclude(id__inrated_anime_ids): idx anime_ids.index(anime.id) if anime.id in anime_ids else -1 if idx -1: continue pred predict_score(matrix, user_index, current_uid, idx) candidates.append((anime, pred)) # 4. 按预测分排序取前 12 个 candidates.sort(keylambda x: x[1], reverseTrue) top_animes [c[0] for c in candidates[:12]] return render(request, recommend/list.html, {animes: top_animes})这段代码里有个性能隐患predict_score在循环里被反复调用每次都要重新算一遍相似度。动漫数量少的时候没问题一旦超过几百部页面加载会明显变慢。常见优化做法是先把当前用户和其他所有用户的相似度算一次存成一个字典循环里直接查。这个改动不大但响应时间能从好几秒降到几百毫秒。3.3 URL 路由与模板渲染的衔接路由配置没什么特别的但要注意把推荐页面的 URL 放在合适的位置。通常是在urls.py里加一行from django.urls import path from . import views urlpatterns [ path(, views.index, nameindex), path(recommend/, views.recommend_list, namerecommend), path(anime/int:anime_id/, views.anime_detail, nameanime_detail), path(rate/int:anime_id/, views.submit_rating, namesubmit_rating), ]模板里渲染推荐列表的时候我一般会用一个简单的卡片布局每张卡片显示封面、标题和预测评分。这里不展开前端细节但有一个点要注意如果cover_url为空模板里要做兜底不然页面上会出现裂图。Django 模板里可以用{% if anime.cover_url %}判断或者用default过滤器给一个占位图地址。4. 数据库文档与数据初始化让系统第一次跑起来就有数据4.1 数据库文档里到底该写什么这套资源里附带的数据库文档通常包含表结构说明、字段类型、索引设计和 ER 图。很多人拿到文档只看表名其实有几个地方值得细看。第一是索引rating表的user_id和anime_id上有没有建索引直接决定推荐页面的查询速度。第二是字符集如果动漫标题里有日文或特殊符号数据库字符集必须是utf8mb4不然存进去会变问号。第三是外键约束on_delete的行为要确认清楚是CASCADE还是SET_NULL这决定了删用户的时候评分记录会不会跟着消失。下面是一个典型的表结构对照你可以拿它跟文档里的设计对一下表名关键字段类型说明animetitlevarchar(200)动漫名称建议加索引animegenrevarchar(100)类型用于分类筛选ratinguser_idint外键关联 auth_userratinganime_idint外键关联 animeratingscorefloat评分值范围 1-5ratingcreated_atdatetime评分时间可用于时间衰减4.2 用 Django fixture 或管理命令灌入初始数据系统第一次跑起来最尴尬的就是数据库是空的推荐页面什么都出不来。常见做法有两种一是用 Django 的fixtures机制把初始数据写成 JSON 文件用loaddata命令导入二是写一个自定义管理命令用代码批量生成测试评分。我一般推荐第二种因为推荐系统需要的是「有规律的评分数据」手动编的 JSON 很难保证用户和动漫之间的评分矩阵有足够的重叠度。# management/commands/seed_ratings.py from django.core.management.base import BaseCommand from django.contrib.auth.models import User from anime.models import Anime, Rating import random class Command(BaseCommand): help 生成测试评分数据 def handle(self, *args, **options): users list(User.objects.all()) animes list(Anime.objects.all()) if not users or not animes: self.stdout.write(请先创建用户和动漫数据) return count 0 for user in users: # 每个用户随机评 10-30 部动漫 sample random.sample(animes, min(len(animes), random.randint(10, 30))) for anime in sample: Rating.objects.get_or_create( useruser, animeanime, defaults{score: random.choice([1, 2, 3, 4, 5])} ) count 1 self.stdout.write(f已生成 {count} 条评分记录)跑的时候用python manage.py seed_ratings就行。这里用get_or_create是为了防止重复执行命令时产生重复评分配合模型里的unique_together一起用数据一致性有保障。评分用random.choice从 1-5 里取虽然不够真实但足够让推荐算法跑出结果。如果你想让数据更真实可以给不同用户设定不同的评分偏好比如某些用户偏向打高分这样皮尔逊相似度的效果会更明显。4.3 数据库迁移与常见报错处理Django 的迁移命令就两条python manage.py makemigrations和python manage.py migrate。但实际跑的时候经常遇到几个报错。一个是「No changes detected」通常是因为 app 没有加到INSTALLED_APPS里或者模型文件没保存。另一个是「Table already exists」说明数据库里已经有同名表了要么删表重来要么用--fake参数跳过。还有一个比较隐蔽的SQLite 数据库文件路径不对Django 会默默创建一个新的空库你以为是迁移成功了其实数据全在另一个文件里。我一般会在settings.py里把DATABASES的NAME写成绝对路径避免这种玄学问题。5. 避坑与排查协同过滤推荐系统最容易翻车的五个地方5.1 推荐结果全是热门动漫冷门好番一个不出现象推荐列表里翻来覆去就是那几部评分人数最多的动漫新用户和老用户看到的推荐几乎一样。原因评分矩阵太稀疏大部分动漫只有个位数评分相似度计算时这些动漫的共同评分项太少直接被过滤掉了。热门动漫因为评分人数多跟谁都「相似」自然霸榜。解决在预测评分时加一个热度惩罚项对评分人数超过阈值的动漫做降权。或者改用基于物品的协同过滤Item-Based CF物品相似度受热门影响小一些。另一个办法是设置最低评分人数门槛低于 5 人评分的动漫不进入推荐池但这样会牺牲覆盖率看你的取舍。5.2 新用户登录后推荐页面空白现象刚注册的用户点进推荐页什么都没有或者报 500 错误。原因新用户在评分表里没有任何记录build_rating_matrix返回的user_ids里不包含他后续代码直接索引越界。解决在视图层加一个判断如果当前用户不在评分矩阵里返回热门动漫或者随机动漫作为兜底。上面 3.2 节的代码里已经写了这个逻辑关键是别忘了加。另外可以在用户注册后引导他至少评 3 部动漫这样推荐才有依据。5.3 相似度计算耗时过长导致页面超时现象用户数量超过 200 之后推荐页面加载要十几秒有时候直接 502。原因每次请求都重新计算当前用户和其他所有用户的相似度时间复杂度是 O(n²)用户越多越慢。解决把相似度矩阵缓存起来用 Django 的 cache 框架或者直接存到 Redis 里。用户评分更新时再失效缓存。如果不想引入 Redis可以退而求其次把相似度计算放到管理命令里离线跑视图层只读结果。我一般会先用django.core.cache的本地内存缓存顶一下开发阶段够用上线再换 Redis。5.4 评分数据重复导致推荐结果偏移现象同一个用户对同一部动漫的评分在数据库里有好几条推荐结果明显偏向这部动漫。原因提交评分的视图没有做去重或者前端表单重复提交了。解决模型层加unique_together视图层用update_or_create代替create。前端加一个提交按钮的防抖处理。如果数据库里已经有重复数据写个脚本按user_id anime_id分组只保留最新一条其余的删掉。5.5 数据库字符集不对导致动漫标题乱码现象动漫标题里的日文假名或者特殊符号存进去变成问号页面上显示乱码。原因MySQL 数据库或者表的字符集是utf8而不是utf8mb4utf8最多只支持 3 字节存不下某些特殊字符。解决建库的时候指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。已经建好的库可以用ALTER DATABASE和ALTER TABLE改。Django 的settings.py里DATABASES配置加OPTIONS: {charset: utf8mb4}。SQLite 默认就是 UTF-8不用操心这个。6. 进阶技巧用离线预计算把推荐响应压到 200ms 以内系统能跑起来之后下一步就是让它跑得快。协同过滤的瓶颈在相似度计算而相似度矩阵在用户评分没有大变动的时候是稳定的没必要每次请求都重算。我一般会写一个管理命令离线把用户相似度矩阵算好存到数据库或者缓存里视图层直接查。# management/commands/precompute_similarity.py from django.core.management.base import BaseCommand from django.core.cache import cache from anime.models import Rating from anime.recommend import build_rating_matrix, pearson_similarity class Command(BaseCommand): help 离线预计算用户相似度矩阵 def handle(self, *args, **options): ratings list(Rating.objects.values_list(user_id, anime_id, score)) user_ids, anime_ids, matrix build_rating_matrix(ratings) sim_cache {} for i, uid_a in enumerate(user_ids): for j, uid_b in enumerate(user_ids): if i j: continue sim pearson_similarity(matrix, i, j) if sim 0: sim_cache.setdefault(uid_a, {})[uid_b] sim sim_cache.setdefault(uid_b, {})[uid_a] sim cache.set(user_similarity, sim_cache, timeout3600 * 6) self.stdout.write(f已缓存 {len(sim_cache)} 个用户的相似度数据)这个命令跑一次大概几秒钟缓存有效期设 6 小时。视图层改成从缓存里读相似度只做加权预测响应时间能从秒级降到 200ms 以内。注意缓存失效的时机有新评分提交时要么删掉整个缓存要么只更新受影响用户的相似度。我一般图省事直接删整个缓存下次请求时重新触发预计算命令反正用户量不大几秒钟的事。验证方法也很简单打开 Django shell手动调一下cache.get(user_similarity)看看返回的字典里有没有数据。再对比一下预计算前后的推荐列表如果 Top-12 的动漫基本一致说明缓存没算错。如果差异很大检查一下pearson_similarity的输入索引是不是对上了。从那以后我每次部署推荐系统都会先把离线预计算命令挂到定时任务里跑完再切流量。这个习惯帮我省掉了至少三次线上页面超时的故障排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表