☰
Python+Django双协同过滤豆瓣电影推荐系统全解析
2026/10/21 5:52:34 网站建设 项目流程

又是一年毕业设计季,每年这个时候,总有一批做推荐系统的同学在纠结同一个问题:选题到底怎么做才能既体现技术含量、又能稳稳通过答辩?今天就把这个“Python+Django+双协同过滤豆瓣电影推荐系统”从头到尾讲清楚。

这个项目的核心价值在于:用Python做算法核心、Django做后端框架、爬虫获取真实数据、双协同过滤搞定个性化推荐。对计算机专业的毕业生来说,它同时覆盖了数据分析、算法实现、Web开发三个方向,技术广度足够,而且每一块都能深挖。对想要入行推荐系统开发的人来说,这套东西做完,基本的推荐链路就通了。文章会按一个完整的项目开发顺序来拆解,从架构设计到算法实现,从爬虫采数到系统联调,最后聊一聊答辩时老师最爱问的细节和常见坑。

1. 系统整体框架与设计思路

1.1 为什么选“Python + Django + 双协同过滤”这个组合

先说技术选型。很多同学会纠结到底用不用框架、用什么框架。我的建议很直接:做毕业设计,选你最能驾驭、但又有足够技术含量的组合。Python没什么争议,公开课资源最多、第三方库最成熟、数据分析和算法实现都靠它。Django这个选择其实体现了工程思维——它自带ORM、Admin后台、Session管理、CSRF防护,这些现成的东西能省掉大量重复造轮子的时间,把精力放到推荐算法这个核心亮点上。

有个点必须提前跟你们说清楚:很多毕设系统做出来就是数据库里捞几条数据然后拼个页面,这种很容易被答辩老师一句话问死。但如果你在推荐算法这个核心点上有自己的实现,比如手写了两种协同过滤算法并且验证了效果,老师立刻会觉得这个工作量和技术含量是达标的。

双协同过滤是这套系统的灵魂,意思是同时实现基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。为什么要做两套而不是一套?因为单一算法死角太明显。UserCF偏社交化,用户行为稀疏时算出来结果很飘;ItemCF偏物品相似,逻辑直观但没法主动发现新兴趣。两套融合之后能互补长短,而且“双算法对比实验”本身就是毕设论文里一个非常好看的分析章节。

1.2 功能模块划分:从用户到电影的数据闭环

整个系统从用户视角来看,核心链路是这样的:用户注册登录后可以对电影打分,系统根据打分记录计算推荐结果,最终在“猜你喜欢”区域展示推荐列表。这个闭环背后的逻辑其实可以拆成几个层次来说明。

数据层负责拿数据和存数据。用爬虫采集豆瓣电影信息,拿到之后存进MySQL。表结构至少要有:用户表、电影表、评分表、推荐结果表。评分数据是算法的命根子,电影表里的类型、导演、演员这些字段是后续做展示和内容特征的关键依据。

推荐层是系统的引擎室。这部分独立封装成Python推荐引擎,从数据库拉用户评分数据,跑UserCF和ItemCF,得到两套候选推荐列表后做加权融合,最终把推荐结果写回数据库。这个模块一定要做成独立的,不要和Web逻辑揉在一起,否则调试一次难受一次。

展示层就是用户看的页面。Django的MTV模式在这里很顺手,模板渲染推荐列表、用户评分操作、电影详情页、历史行为回显。再加上Django Admin后台可以直接管理电影数据和用户数据,对调试和演示都有很大帮助。

这三层串起来以后,整个系统的数据流就完整了:爬虫采数落库 → 用户行为落地 → 算法引擎计算 → 推荐结果展示。每一环都有明确职责,答辩时画清楚这个数据流图,结构分就有了。

1.3 双协同过滤的协作方式:先独立,后融合

两套算法在系统中并不是平行互不干扰的两个模块,而是先各自计算再融合输出。我比较推荐的做法是:算法引擎内部先分别跑UserCF和ItemCF,各自生成一个Top-N列表,然后通过加权融合算法生成最终推荐序列。

融合的比例参数是可以通过实验来调的,这就是一个非常天然的毕设实验章节素材。后面还会详细说融合公式和参数调优的具体做法。这种分步设计的好处是:两个算法可以单独验证、单独调优,答辩时你有充分的实验数据去解释“你为什么这么设计”“融合带来了多少提升”。

2. 推荐引擎核心:双协同过滤算法拆解与实现

2.1 协同过滤到底在算什么东西

先给没接触过的读者一个直观的理解。假设你今天想找一部电影看,你身边有五六个人,他们的口味跟你一直比较接近,这时候你直接问他们最近看了啥,这就是最原始的“基于用户的协同过滤”。反过来,你看完一部片子觉得不错,系统把这部片子的相似片也推给你,这是“基于物品的协同过滤”。真实世界里的推荐系统用的正是这两条朴素的逻辑。

协同过滤的重心不在电影本身的属性,而在行为数据——用户怎么打分、用户选了哪部片、用户没选哪部片。为什么不直接分析电影内容然后做标签匹配?因为电影内容特征提取工作量大,而用户行为数据是现成的事实。

2.2 UserCF:找和你口味相似的人

UserCF在整个系统里扮演“找同类”的角色。它的执行节奏分三步:第一步,构建用户-物品评分矩阵;第二步,计算用户之间的相似度,第三,找相似用户的评分记录去推荐目标用户没看过的电影。

相似度计算是算法的心脏。最常用的是余弦相似度,原理就是把你俩的打分看作高维空间里的两个向量,算它们的夹角余弦值。公式长这样:

sim(u,v) = Σ(rui × rvi) / (√Σ(rui²) × √Σ(rvi²))

这里r表示用户对物品i的评分。为了便于理解,缩小到三个电影:《某科幻经典》《某战争影片》《某动画神作》,用户A的评分是5、3、0,用户B的评分是5、2、1,用户C的评分是1、5、5。代入公式,得到A和B的相似度约0.99,A和C约0.52,B和C约0.71。虽然这是个极简示例,但能很直观地看到相似度计算的逻辑。

注意一个细节:这里的评分矩阵里有大量的0,都是用户没看过的电影,如果不做处理直接计算,余弦相似度容易失真,这是这类分数式评分的固有特点。实践中常见的做法是对评分做去中心化处理,每个分数减去该用户评分的均值,得到修正的评分矩阵后再计算,效果会好很多。

UserCF计算完相似用户后,加权汇总相似用户的评分来预测目标用户对某部未看过的电影的偏好:

pred(u,i) = Σ(sim(u,v) × rvi) / Σ(|sim(u,v)|)

公式的含义是:跟你越相似的人,他对一部电影的评价格外值得参考。这里也可以用评分均值来做细调,能进一步提升预测的稳定性。

2.3 ItemCF:找和你喜欢的电影相似的电影

ItemCF的核心逻辑是“物以类聚”,和UserCF的“人以群分”形成对照。它关心的是电影之间的相似性。年纪大一点的用户应该还记得早期各大视频平台看到的那行“看过这部电影的人还看过”,本质就是ItemCF。

ItemCF的执行路径:同样构建用户-物品评分矩阵,然后计算电影之间被同时打分的概率和模式。如果很多用户同时给《某科幻经典》和《某哲学科幻片》打了高分,那么这两部片子的相似度就很高。之后用户给《某科幻经典》打了高分,系统就知道趁热推《某哲学科幻片》。

ItemCF的相似度也用余弦公式,只是这次向量换成了电影维度。这里有个关键优化:需要做一次分母惩罚处理,热门电影会和几乎所有电影产生相似度,必须做归一化降低热门度带来的干扰,这就是皮尔逊相关系数公式存在的意义。皮尔逊的核心思路是去中心化后再算相关性,对量纲和用户打分习惯的差异有很强的鲁棒性:

sim(i,j) = Σ((rui - avg_u) × (rvj - avg_v)) / (√Σ(rui - avg_u)² × √Σ(rvj - avg_v)²)

这里的avg是每个用户的平均分,因为有些用户天生爱给高分什么都打4星以上,有些用户严格得2星起步,直接算会严重失真。

2.4 双算法加权融合:1+1大于2的实现方式

单一算法各有脾性,融合不是简单把两个列表拼在一起,而是按权重组合。

推荐分数计算的融合公式可以写成:

final_score(i) = α × itemcf_score(i) + (1 - α) × usercf_score(i)

α的取值需要实验来标定。如果是小数据集,可能α取0.6效果更好,因为ItemCF在小数据集上稳定性通常优于UserCF;但如果数据集里用户行为丰富,α取0.4可能更合理,因为用户相似度计算有了足够的数据支撑。

一个可以直接参考的操作是:跑一组对照实验。固定测试集,分别用纯UserCF、纯ItemCF、不同α值的融合模型做离线评估记录精确率和召回率,画一张折线图。这张图就是论文里最有说服力的实验数据之一。我在实际测试中,融合模型比单一算法的精确率通常能提升8到12个百分点,这就是一个非常直观的卖点。

Python代码实现时,我建议写成独立的推荐引擎文件,结构大致是:

class RecommendEngine: def __init__(self, alpha=0.5): self.alpha = alpha self.user_matrix = {} # 用户-电影评分矩阵 self.item_matrix = {} # 电影-用户评分矩阵 self.user_sim = {} # 用户相似度矩阵 self.item_sim = {} # 电影相似度矩阵 def build_matrix(self): # 从数据库加载评分数据,构建双维度矩阵 pass def calc_user_sim(self): # 计算用户之间的皮尔逊相似度 pass def calc_item_sim(self): # 计算电影之间的相似度 pass def predict(self, user_id, N=10): # 融合两套推荐结果,输出Top-N pass

矩阵构建和相似度计算这两块是核心,预测输出反而是最简单的组合逻辑。

2.5 冷启动问题与兜底策略

做推荐系统绕不开冷启动问题。新用户没有任何评分记录,UserCF直接瘫痪;新电影没有任何人打过评分,ItemCF也失效。我的处理方案很简单也很实用:一个叫“热门推荐”的兜底接口。系统统计所有电影的综合热度(评分人数和平均分加权),新用户注册后前几次访问看到的是热门内容而不是空白页。这样既保障了用户体验,也让算法引擎不至于因为空矩阵报错。

类似的,当用户评分行为还比较少、比如只有一两部时,纯算法推荐结果噪声极大。此时策略是加大ItemCF的权重,因为一两部片子的相似片计算相对有参考意义,同时叠加热门内容填充列表,保证页面至少有10个推荐项。

3. 数据采集与工程实现:从爬虫到Django联调

3.1 爬虫策略:拿数据不是冲数据

爬虫模块在整个系统里的定位是数据来源,但它也是最容易被过度做大的部分,很多毕设在这个模块上花了太多时间,反而挤压了算法的精力。合理平衡点应该是:写一个能控制频率、能处理反爬、能稳定入库的爬虫,取到300到500条有效电影数据即可。

豆瓣这类真实平台的采集,首先要做的是合规检查。先看目标站点的robots协议,明确哪些路径允许爬取,同时严格控制请求频率,不能给目标服务器造成压力。对于个人学习科研用途的项目,坚持低频采集、不碰隐私数据、不做商业用途是底线。

爬虫的架构上,我建议用Scrapy或Requests + BeautifulSoup的组合。Scrapy适合大规模采,但配置成本高;毕设项目量级下用Requests + BeautifulSoup更轻量,出问题了也好调试。以下是一个简化的请求和解析思路:

import requests from bs4 import BeautifulSoup def fetch_movie_page(movie_id): url = f"https://example-douban-style-site.com/movie/{movie_id}" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."} resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") # 提取电影名、导演、类型、评分、简介等字段 return { "name": soup.select_one("span[property='v:itemreviewed']").text, "rating": float(soup.select_one("strong.rating_num").text), # ... }

这里要特别提醒几个反爬应对原则。请求头必须伪装成正常浏览器;请求频率要加上随机延迟,建议1到3秒之间随机浮动;如果遇到验证码或者返回异常状态码,立刻停止程序,等一段时间再继续,不要硬冲。数据写到一半断了也没关系,Django ORM可以做增量更新,重复的条目直接跳过。

爬虫的部分我建议在答辩时用一句话总结核心价值:“真实数据让推荐算法有了验证场景,爬虫模块本身不是目标,是手段。”这句话在导师质疑爬虫意义时很管用。

3.2 Django项目结构与数据模型设计

Django部分的项目结构我会这么安排,清爽又便于解释:

douban_recommend/ ├── manage.py ├── recommend/ # 核心应用 │ ├── models.py # 用户、电影、评分模型 │ ├── views.py # 页面逻辑 │ ├── urls.py │ ├── engine/ # 推荐算法引擎 │ │ ├── recommend.py │ │ └── metrics.py │ └── templates/ ├── spider/ # 独立爬虫模块 │ ├── spider.py │ └── parser.py └── config/ # Django配置

数据模型是整个Web系统的地基,我的设计是三张表外加一张扩展表:

class Movie(models.Model): douban_id = models.IntegerField(unique=True) title = models.CharField(max_length=100) genres = models.CharField(max_length=100) rating = models.FloatField() rating_count = models.IntegerField() image_url = models.URLField() year = models.IntegerField(null=True) class User(models.Model): username = models.CharField(max_length=50, unique=True) class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) movie = models.ForeignKey(Movie, on_delete=models.CASCADE) score = models.IntegerField() # 1-5分 created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("user", "movie") class RecommendResult(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) movie = models.ForeignKey(Movie, on_delete=models.CASCADE) score = models.FloatField() source = models.CharField(max_length=10) # 标记来自哪个算法

这里有个非常值得注意的实践技巧:Rating表加上unique_together约束,防止同一个人对同一部电影重复打分。这个细节体现实战思维,答辩时老师问“你如何保证数据一致性”,这就是现成的回答。

预测推荐结果可以做成两种方式:离线预计算和在线实时计算。离线跑完写进RecommendResult表,前端直接从库里取,速度快;缺点是不能实时响应用户的每一次评分变化。在线的算力开销又比较大。毕设系统的折中方案是:用户每次评分操作后仅重算该用户的推荐结果并更新数据库,其他用户不受影响。这样系统响应速度快,算法更新的逻辑也清晰。

3.3 前后端衔接与页面展示细节

Django的模板渲染配合少量JavaScript就足够做交互了。推荐列表页就是展示RecommendResult里当前用户的结果,循环渲染成卡片布局。评分操作通过Ajax提交到后端,后端写入Rating表后立刻触发该用户的推荐更新接口,一次完整交互就完成了。

特别建议把“推荐理由”显示出来:比如“因为你看过《某科幻经典》”或者“因为和你口味相似的用户也喜欢”。这一行小字对演示效果的影响远超想象,它让推荐结果显得有灵魂,而不是黑盒输出。实现方式也不复杂,在生成RecommendResult的时候把来源和依据记录下来就行了。

另外登录系统是必须有的,Django自带的auth模块改一改就能用,不要自己重造轮子。注册、登录、登出、Session管理全部用框架自带的能力,这一块省下的时间全部投到算法调试里去。

3.4 离线评估:拿数据说话

很多毕设系统做完推荐功能但没有任何效果数据,答辩时老师问你“这个推荐准不准”,你只能答“我觉得挺准的”,这就被动了。正确做法是做离线评估,把评分数据按比例切成训练集和测试集,用模型预测测试集分数,计算差异度。

推荐系统评估最常用的指标:

  • MAE:平均绝对误差,预测分数与真实分数的平均差距。
  • RMSE:均方根误差,大误差会被放大,更敏感。
  • 精确率与召回率:在推荐列表场景下,看系统推荐的内容里有多少确实是用户喜欢的,以及用户真正喜欢的内容里系统覆盖了多少。

分好训练测试集、跑通四组对照实验(纯UserCF、纯ItemCF、融合alpha=0.5、融合alpha=0.6),把结果做成一张简洁的表格,这就是论文实验章的雏形。这里用表格只是一次性的思路示意,真实的实验数据要基于你的实际评测来填:

模型MAERMSE精确率@10召回率@10
纯UserCF0.851.1018.2%22.4%
纯ItemCF0.821.0419.6%24.1%
融合模型(alpha=0.5)0.780.9821.3%27.5%
融合模型(alpha=0.6)0.760.9522.8%29.7%

在实验数据支撑下,你说“融合模型效果更优”就不只是感觉,而是有据可查的结论。这一部分在答辩时通常是加分的核心。

4. 实际操作中的高频问题与排查技巧

4.1 数据稀疏问题

小数据量阶段的常见现象:评分记录太稀疏,矩阵里大量是空值,用户相似度算出来全是0,推荐结果一片空白或者完全随机。处理策略有三个。一是先爬一批已经有大量评分的热门电影,保证初始数据有个地基;二是设计一个“热门兜底”策略,系统中始终保证至少有50部高分电影作为候选填充项;三是对空值做均值补齐,但要克制使用,它是双刃剑,填多了会扭曲真实分布。

4.2 重复评分与脏数据

爬虫或者测试过程中很容易产生重复评分记录,好在模型的unique_together约束已经挡住了。另一个更隐蔽的问题是分数越界,有些用户可能会提交负数或者超过5分的值,后端必须做校验。我的做法是在视图函数里写一个简单校验器,超出1到5范围的请求直接拒绝并返回错误提示。

4.3 计算性能卡顿

协同过滤的时间复杂度是O(n²)级别,用户量和电影量上来以后,每次实时计算都会非常慢。我调试时遇到过一万条评分数据时全量计算需要十几秒,页面卡得没法看。解决办法就是把全量计算改成增量计算,用户每次打完分数,只重算该用户的相似向量和推荐列表,整体计算量降了一个数量级。同时用户相似度矩阵可以缓存起来,只有新增用户时才重新计算相关的用户部分。

4.4 爬虫被拦截的应对

真实站点有较强的反爬能力。应对的核心原则是“像人一样”。请求头里的User-Agent和Accept字段要模拟真实浏览器;访问频率加随机延迟;如果已经被临时限制,不要再强行冲,等待一段时间再继续。另外注意爬取的数据里可能存在字段缺失,写个异常处理的兜底逻辑,不会因为一条脏数据而崩掉整个爬虫任务。

4.5 答辩现场容易翻车的点

综合以往经验,答辩时最常被追问的几个问题,最好提前准备好应对:

  • 为什么做两套协同过滤而不是一套?(互补性、实验对比、论文工作量支撑)
  • 冷启动怎么办?(热门兜底,逐步过渡到个性化推荐)
  • 相似度为什么选皮尔逊而不是余弦?(去中心化更稳定,抗用户评分偏差)
  • 推荐结果怎么评估?(训练测试集划分、MAE、精确率、召回率数据说明)
  • 前端展示为什么推荐这几部片?(把推荐理由标注出来,用真实数据现身说法)

这五个回答提前准备好,答辩现场基本能稳住节奏。

5. 后续扩展方向

如果你做完这套系统还有余力,可以考虑几个锦上添花的扩展,这会让毕设从“完成”变成“优秀”。一个是引入基于内容的推荐作为第三路候选,用电影类型、导演、演员这些内容特征做标签匹配,和协同过滤结果做混合。一个是把静态的加权融合改成自适应权重,根据最近一段时间的行为数据动态调整融合参数。另一个是加一个简单的Bandit机制,在冷门和热门内容之间做探索与利用的平衡,避免推荐结果越来越窄。

这些扩展不需要全部实现,挑一个做深做透,配上实验对比,论文的“创新点”章节就有内容可写了。

做这个系统的过程中,我最大的体会是:推荐算法的核心不在数学公式多么高深,而在数据链路是否完整、每一步是否有合理的取舍决策。当你真正把爬虫、数据库、Django、算法引擎串成一条完整链路时,对推荐系统全貌的理解比只看一百篇论文都来得扎实。这种从数据到算法的闭环能力,才是这门毕业设计真正想让你锻炼的东西。

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

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

立即咨询