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

资讯详情

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

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

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_length=50, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user' class Anime(models.Model): title = models.CharField(max_length=200, db_index=True) # 番名,加索引方便搜索 genre = models.CharField(max_length=200, blank=True) # 类型,多个用逗号分隔 year = models.IntegerField(null=True, blank=True) # 年份 score = models.FloatField(default=0) # 站内均分 cover_url = models.URLField(blank=True) # 封面图 summary = models.TextField(blank=True) # 简介 class Meta: db_table = 'anime' class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) anime = models.ForeignKey(Anime, on_delete=models.CASCADE) score = models.FloatField() # 评分 1-10 created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'rating' # 联合唯一索引,一个用户对一部番只能有一条评分 unique_together = ('user', 'anime') indexes = [ models.Index(fields=['user', 'anime']), ]

字段说明:score用 FloatField 而不是 IntegerField,是因为后面算余弦相似度时浮点精度更稳;unique_together是防重复评分的硬约束,别指望前端拦;db_index=True加在title上,因为搜索页会频繁按番名模糊查。迁移命令就两条:

python manage.py makemigrations python manage.py migrate

2.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( cnt=Count('id') ).filter(cnt__gte=5).values_list('user', flat=True) # 删除无效评分(分数为 0 或超过 10 的脏数据) Rating.objects.filter(score__lte=0).delete() Rating.objects.filter(score__gt=10).delete()

这里有个参数要留意:cnt__gte=5这个阈值不是拍脑袋定的。阈值太低,相似度矩阵噪声大;太高,能参与推荐的用户太少。一般从 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( index='user_id', columns='anime_id', values='score', fill_value=0 ) return matrix def item_similarity(matrix): # 转置后行是动漫,算动漫之间的余弦相似度 item_matrix = matrix.T sim = cosine_similarity(item_matrix) return pd.DataFrame( sim, index=item_matrix.index, columns=item_matrix.index )

逻辑说明:pivot_table的fill_value=0是关键——协同过滤里没评分就是 0,不能填均值,否则相似度会被拉偏。cosine_similarity返回的是对称矩阵,对角线是 1。参数上,如果动漫数量超过几千,这个稠密矩阵会吃光内存,这时候要换稀疏矩阵方案(scipy.sparse),后面避坑章节会讲。

3.3 生成推荐列表的完整函数

有了相似度矩阵,给某个用户推荐就是:找他评分高的番,看这些番和哪些番最像,加权求和排序。

def recommend_for_user(user_id, matrix, item_sim, top_n=10): # 该用户的评分向量 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(ascending=False)[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(), key=lambda x: x[1], reverse=True) 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 = f'rec_{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_n=10) # 推荐结果缓存 10 分钟 cache.set(cache_key, result, 600) return JsonResponse({'list': result, 'from_cache': False})

cache.set的第三个参数是过期秒数,600 秒是个折中——太短缓存没意义,太长新评分反映不进来。用户产生新评分时,记得主动cache.delete(f'rec_{user_id}'),这就是「后悔药」,不然用户打完分刷新页面推荐没变,体验很差。

4.2 推荐接口的 URL 配置与前端对接

# urls.py from django.urls import path from app import views urlpatterns = [ path('api/recommend/', views.recommend_view, name='recommend'), path('api/rating/', views.rating_view, name='rating'), ]

前端拿到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( user=request.user, anime_id=anime_id, defaults={'score': score} ) # 清掉该用户的推荐缓存,下次请求重新算 cache.delete(f'rec_{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(user=u, anime=a).delete()清一遍,或者用bulk_create时加ignore_conflicts=True。数据库层的唯一索引是最后防线,但别只靠它。

5.5 现象:换了数据库(SQLite 转 MySQL)后中文番名乱码

原因:MySQL 建库时字符集没设 utf8mb4,Django 连接配置里也没指定。解决:建库语句用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,Django 的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, k=10): 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_n=k) 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」变成「能上线给人用」,这套路径是最短的那条。希望帮到你。

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

返回列表