☰
Django与协同过滤实战:动漫推荐系统从算法到部署
2026/9/25 4:55:31 网站建设 项目流程

简介:这是一套面向高校学生与Python/Django开发者的动漫推荐系统毕业设计完整源码包,以协同过滤算法为核心,解决个性化动漫推荐与用户行为分析的实现问题。资源包含用户管理、动漫信息展示、用户行为交互三大模块:用户端支持注册登录、偏好问卷、评分收藏与社交关注;动漫端提供多维度分类检索、专题合集与详情展示;行为端采集显式评分与隐式停留时长,并实现评论、弹幕与话题讨论。压缩包共585个文件,约19.98MB,以159个svg图标、99个vue组件、63个js脚本、60个py源码及48个pyc编译文件为主,另含png/jpg图片、css样式、sql建表脚本与bat启动脚本,前后端分离结构清晰。已有58人学习下载,适合需要完整赛题方案、协同过滤算法落地代码与数据库文档参考的读者,可据此快速搭建推荐系统并理解工程目录组织。

1. 动漫推荐系统遇上协同过滤:为什么用户总在第三集弃番

做动漫站的人都有一个共同的痛:用户注册后前三天活跃,之后流失率陡增。你以为是内容不够多,其实是推荐没做对。一个刚看完《进击的巨人》的用户,首页推给他《小猪佩奇》,他不跑才怪。动漫推荐系统要解决的核心问题,就是在海量番剧里找到用户真正想看的那几部,而协同过滤正是这个场景下最成熟、最容易落地的算法路径。

这套方案用 Django 做后端框架,Python 实现协同过滤算法,SQLite 或 MySQL 存数据,最终交付一个能跑起来的推荐系统加配套数据库文档。适合谁?适合正在做课程设计的学生、想从零搭一个推荐模块的后端新手、以及需要快速验证推荐效果的产品开发。你不需要机器学习科班背景,但得会基本的 Python 语法和 Django 的 MTV 模式。接下来我会把选型理由、数据建模、算法实现、接口对接、踩坑记录全部拆开讲,每一步都能照着复现。

2. 技术选型与数据建模:Django 和协同过滤为什么是当前最优解

2.1 为什么选 Django 而不是 Flask 或 FastAPI

推荐系统本质上是一个数据密集型的 Web 应用,需要 ORM、Admin 后台、用户认证、表单验证这些基础设施。Django 自带这些东西,开箱即用。Flask 更轻,但你要自己拼 SQLAlchemy、Flask-Login、Flask-Admin,拼完发现跟 Django 差不多重,还多了一堆兼容性问题。FastAPI 异步性能好,但推荐系统的瓶颈在算法计算和数据库查询,不在 Web 层的并发,用 FastAPI 属于杀鸡用牛刀。

我一般会这样判断:如果你的项目需要后台管理界面给运营人员录入动漫信息、查看用户行为日志,Django Admin 能省掉至少两天的前端开发量。这是实打实的效率优势。

Django 的 ORM 还有一个隐性好处:它天然适合做推荐系统的数据聚合。比如你要统计某个用户对某类标签的偏好权重,用 Django 的annotate和aggregate几行代码就能搞定,换成原生 SQL 写起来又长又容易出错。

2.2 协同过滤的两种路线:基于用户 vs 基于物品

协同过滤分两个方向。User-Based 是找跟你口味相似的人,把他们喜欢的番剧推给你。Item-Based 是找你之前喜欢的番剧,找跟它们相似的番剧推给你。

动漫推荐场景下,我强烈建议用 Item-Based。原因很直接:动漫的数量远小于用户数量,Item 之间的相似度矩阵更稳定,不会因为一个新用户进来就大幅变动。而且 Item-Based 的推荐结果可解释性强,你可以告诉用户「因为你看了《鬼灭之刃》,所以推荐《咒术回战》」,用户能理解。User-Based 你没法解释「因为你跟某个匿名用户相似所以推荐这个」。

相似度计算用余弦相似度就够了。调整余弦相似度在评分数据稀疏时表现更好,但动漫推荐场景下用户的评分行为很少,大部分是「看过/没看过」的隐式反馈,所以用余弦相似度处理 0-1 矩阵更合适。

2.3 数据库表结构设计:五张核心表撑起整个系统

数据库文档是这个项目交付物的重要组成部分。核心表就五张,但每张表的字段设计都有讲究。

表名作用关键字段注意事项
anime动漫信息id, title, genre, tags, cover_url, descriptiontags 用逗号分隔存储,方便后续做基于内容的补充推荐
user用户信息id, username, password, email, created_at直接用 Django 自带 User 模型扩展
rating用户评分id, user_id, anime_id, score, created_atscore 范围 1-5,加联合唯一索引防止重复评分
watch_history观看记录id, user_id, anime_id, watched_at, progressprogress 记录看到第几集,用于判断是否弃番
recommendation推荐结果缓存id, user_id, anime_id, score, generated_at定期更新,避免每次请求都实时计算

建表的时候有一个血泪经验:rating 表的(user_id, anime_id)一定要加联合唯一约束。我见过不止一个项目因为没加这个约束,用户重复提交评分导致数据污染,最后相似度矩阵全乱掉。

# models.py 核心模型定义 from django.db import models from django.contrib.auth.models import User class Anime(models.Model): title = models.CharField(max_length=200, verbose_name='番剧名称') genre = models.CharField(max_length=100, verbose_name='类型') tags = models.CharField(max_length=500, blank=True, verbose_name='标签,逗号分隔') cover_url = models.URLField(blank=True, verbose_name='封面地址') description = models.TextField(blank=True, verbose_name='简介') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'anime' verbose_name = '动漫' class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) anime = models.ForeignKey(Anime, on_delete=models.CASCADE) score = models.IntegerField(verbose_name='评分1-5') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'rating' # 联合唯一约束,防止重复评分 unique_together = ('user', 'anime') indexes = [ models.Index(fields=['user', 'anime']), ]

unique_together这行是必须加的,Django 会在数据库层面生成唯一索引,比在应用层做判断可靠得多。indexes那行是为了加速后续的相似度查询,数据量上万条之后效果明显。

2.4 数据准备:从零构建评分矩阵

协同过滤的输入是一个用户-物品评分矩阵。实际项目中,这个矩阵的构建方式决定了推荐质量的上限。

# 构建用户-物品评分矩阵 import numpy as np from django.contrib.auth.models import User from .models import Rating, Anime def build_rating_matrix(): users = list(User.objects.all().values_list('id', flat=True)) animes = list(Anime.objects.all().values_list('id', flat=True)) # 建立 id 到索引的映射 user_index = {uid: i for i, uid in enumerate(users)} anime_index = {aid: i for i, aid in enumerate(animes)} # 初始化矩阵,0 表示未评分 matrix = np.zeros((len(users), len(animes))) ratings = Rating.objects.all().values_list('user_id', 'anime_id', 'score') for uid, aid, score in ratings: if uid in user_index and aid in anime_index: matrix[user_index[uid]][anime_index[aid]] = score return matrix, users, animes

这段代码的逻辑很直白:把所有用户和动漫的 ID 映射成矩阵的行列索引,然后遍历评分记录填充矩阵。np.zeros初始化为 0 表示未评分,这是隐式反馈的常见处理方式。

参数说明:matrix的形状是(用户数, 动漫数),如果用户 1000 人、动漫 500 部,矩阵就是 1000×500,内存占用约 4MB,完全没问题。但如果用户上百万,这个稠密矩阵就撑不住了,需要用 scipy 的稀疏矩阵。课程设计级别的项目,稠密矩阵够用。

3. 协同过滤算法实现:从相似度计算到 Top-N 推荐

3.1 余弦相似度计算与物品相似度矩阵

Item-Based 协同过滤的核心是算出物品之间的相似度矩阵。这个矩阵是对称的,对角线为 1,只需要算上三角。

from sklearn.metrics.pairwise import cosine_similarity import numpy as np def compute_item_similarity(matrix): # matrix 形状: (n_users, n_items) # 转置后形状: (n_items, n_users),每行代表一个物品的所有用户评分 item_matrix = matrix.T # 余弦相似度计算,结果形状 (n_items, n_items) similarity = cosine_similarity(item_matrix) # 对角线置零,避免推荐自己 np.fill_diagonal(similarity, 0) return similarity

cosine_similarity接收的矩阵每一行是一个样本,所以要把原始矩阵转置,让每一行代表一部动漫。计算出来的相似度矩阵中,similarity[i][j]表示第 i 部动漫和第 j 部动漫的相似度,值域 0 到 1。

这里有个容易翻车的地方:如果某个物品只有一个人评分,它跟所有其他物品的相似度都会是 0 或 NaN。处理方式是在计算前过滤掉评分数少于阈值的物品,或者在结果中把 NaN 替换为 0。

3.2 基于物品相似度的评分预测

有了相似度矩阵,就可以预测用户对未评分动漫的感兴趣程度。公式是:预测分 = 相似动漫的评分加权平均,权重就是相似度。

def predict_ratings(user_id, matrix, similarity, users, animes, top_n=10): if user_id not in users: return [] user_idx = users.index(user_id) user_ratings = matrix[user_idx] # 该用户对所有动漫的评分 # 已评分的动漫索引 rated_indices = np.where(user_ratings > 0)[0] if len(rated_indices) == 0: return [] # 预测评分 = 已评分动漫的相似度加权和 / 相似度之和 scores = np.zeros(len(animes)) for i in rated_indices: scores += similarity[i] * user_ratings[i] sim_sum = np.sum(similarity[rated_indices], axis=0) # 避免除零 sim_sum[sim_sum == 0] = 1 predicted = scores / sim_sum # 排除已评分的动漫 predicted[rated_indices] = 0 # 取 Top-N top_indices = np.argsort(predicted)[::-1][:top_n] results = [(animes[i], predicted[i]) for i in top_indices if predicted[i] > 0] return results

这段代码的逻辑分四步:先拿到用户已有的评分向量,然后对每个已评分动漫,用它的相似度向量乘以评分值累加,再除以相似度之和做归一化,最后排序取前 N 个。

参数说明:top_n控制推荐数量,一般设 10 到 20。predicted[i] > 0这个过滤条件是为了排除那些跟用户已看动漫完全不相似的候选,避免推荐无关内容。

3.3 冷启动处理:新用户和新动漫怎么办

协同过滤最大的软肋就是冷启动。新用户没有评分记录,算不出相似度;新动漫没人看过,也进不了推荐池。

新用户的处理策略我一般用三种组合:第一,注册时让用户勾选感兴趣的标签,基于标签做内容推荐兜底;第二,推荐当前全站评分最高的 Top 20 动漫,至少不会推烂片;第三,用户产生 3 条以上评分后,切换到协同过滤。

新动漫的处理更简单:在推荐结果中预留 10% 到 20% 的位置给新上架的动漫,按编辑推荐权重排序。这样既保证了推荐的相关性,又给了新内容曝光机会。

def hybrid_recommend(user_id, matrix, similarity, users, animes, top_n=10): """混合推荐:协同过滤 + 热门兜底 + 新番曝光""" cf_results = predict_ratings(user_id, matrix, similarity, users, animes, top_n) # 如果协同过滤结果不足,用热门动漫补齐 if len(cf_results) < top_n: from .models import Anime from django.db.models import Avg hot_animes = Anime.objects.annotate( avg_score=Avg('rating__score') ).filter(avg_score__isnull=False).order_by('-avg_score')[:top_n] existing_ids = {aid for aid, _ in cf_results} for anime in hot_animes: if anime.id not in existing_ids and len(cf_results) < top_n: cf_results.append((anime.id, 0.0)) return cf_results

这个混合策略的关键在于:协同过滤结果优先,不足时用热门补齐。Avg('rating__score')是 Django ORM 的聚合查询,一行代码算出每部动漫的平均分。注意avg_score__isnull=False这个条件,排除掉没有任何评分的动漫。

3.4 推荐结果缓存与定时更新

每次请求都实时计算推荐结果,用户量一上来数据库就扛不住了。正确做法是把推荐结果缓存到 recommendation 表,定时任务更新。

# management/commands/update_recommendations.py from django.core.management.base import BaseCommand from django.contrib.auth.models import User from recommend.models import Recommendation from recommend.services import build_rating_matrix, compute_item_similarity, predict_ratings class Command(BaseCommand): help = '批量更新所有用户的推荐结果' def handle(self, *args, **options): matrix, users, animes = build_rating_matrix() similarity = compute_item_similarity(matrix) # 清空旧推荐 Recommendation.objects.all().delete() batch = [] for user in User.objects.all(): results = predict_ratings(user.id, matrix, similarity, users, animes, top_n=20) for anime_id, score in results: batch.append(Recommendation( user_id=user.id, anime_id=anime_id, score=score )) # 批量插入,比逐条 save 快 10 倍以上 Recommendation.objects.bulk_create(batch, batch_size=500) self.stdout.write(f'更新完成,共 {len(batch)} 条推荐记录')

用bulk_create而不是循环save,这是 Django 批量操作的常识。batch_size=500是经验值,太大容易撑爆内存,太小又体现不出批量优势。这个命令通过python manage.py update_recommendations调用,配合 crontab 每天凌晨跑一次。

4. 接口对接与前端展示:让推荐结果真正被用户看到

4.1 Django 视图与 API 设计

推荐结果算出来了,得通过接口暴露给前端。用 Django 的 JsonResponse 就够了,不需要上 DRF。

# views.py from django.http import JsonResponse from django.contrib.auth.decorators import login_required from .models import Recommendation, Anime @login_required def api_recommendations(request): """获取当前用户的推荐列表""" recs = Recommendation.objects.filter( user=request.user ).select_related('anime').order_by('-score')[:20] data = [{ 'anime_id': r.anime.id, 'title': r.anime.title, 'cover_url': r.anime.cover_url, 'genre': r.anime.genre, 'score': round(r.score, 2), } for r in recs] return JsonResponse({'code': 0, 'data': data})

select_related('anime')是必须加的,它会把关联的 Anime 对象一起查出来,避免 N+1 查询问题。不加的话,20 条推荐会触发 21 次数据库查询,加了之后只有 1 次。

4.2 前端渲染与交互细节

前端拿到 JSON 数据后渲染成卡片列表。这里不展开前端框架的选择,用原生 JavaScript 就能搞定。

// 推荐列表渲染 async function loadRecommendations() { const resp = await fetch('/api/recommendations/'); const result = await resp.json(); if (result.code !== 0) { console.error('获取推荐失败'); return; } const container = document.getElementById('recommend-list'); container.innerHTML = result.data.map(item => ` <div class="anime-card"># signals.py from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth.models import User from .models import Rating, Recommendation from .services import build_rating_matrix, compute_item_similarity, predict_ratings @receiver(post_save, sender=Rating) def update_user_recommendation(sender, instance, created, **kwargs): """用户评分后,异步更新该用户的推荐结果""" if not created: return user_id = instance.user_id matrix, users, animes = build_rating_matrix() similarity = compute_item_similarity(matrix) results = predict_ratings(user_id, matrix, similarity, users, animes, top_n=20) # 删除旧推荐,写入新推荐 Recommendation.objects.filter(user_id=user_id).delete() Recommendation.objects.bulk_create([ Recommendation(user_id=user_id, anime_id=aid, score=score) for aid, score in results ])

这段代码挂在 Rating 的 post_save 信号上,用户每提交一次评分就触发一次推荐更新。created参数用来判断是新建还是更新,只有新建评分才触发,避免重复计算。

但这里有个性能陷阱:每次评分都重新构建整个评分矩阵和相似度矩阵,用户量大了之后每次评分都要等好几秒。优化方向是把矩阵和相似度缓存到 Redis 或者内存中,评分后只更新变化的部分。对于课程设计级别的项目,用户量不大,直接算也能接受,但要在文档里注明这个限制。

另一个技巧是给推荐结果加时间衰减权重。用户三个月前看的番剧,对当前推荐的影响应该比上周看的小。实现方式是在评分值上乘以一个时间衰减因子:

import math from datetime import datetime, timedelta def time_decay_weight(created_at, half_life_days=30): """时间衰减权重,半衰期默认30天""" days = (datetime.now() - created_at).days return math.pow(0.5, days / half_life_days)

half_life_days=30表示 30 天前的评分权重降为一半。这个参数根据你的业务调整,动漫这种内容消费周期长的场景可以设 60 天,短视频场景设 7 天。把衰减权重乘到评分值上再参与相似度计算,推荐结果会更贴近用户当前兴趣。

验证推荐效果的方法很简单:找 10 个真实用户,记录他们一周内的点击行为,看推荐列表的点击率跟热门列表的点击率差多少。如果推荐列表点击率没有明显高于热门列表,说明算法没起作用,需要回头检查数据质量和参数设置。

我自己做这类项目最大的教训是:不要一上来就追求算法复杂度,先把数据质量和工程链路跑通。评分表的唯一约束、推荐结果的缓存、接口的查询优化,这些工程细节对最终效果的影响,远比换个相似度公式大得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询