ARTICLE DETAIL

资讯详情

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

基于 Django 与协同过滤的电影个性化推荐系统开发实战

基于 Django 与协同过滤的电影个性化推荐系统开发实战 1. 项目源起为什么电影推荐系统成了大数据毕设的“常青树”我见过不少同学在开题前把毕设标题定了七八个版本最后绕了一圈还是回到这个“基于Django协同过滤算法的电影个性化推荐系统”上来。为什么因为它的分工太明确了——爬虫负责采集数据协同过滤负责核心算法Django负责业务系统ECharts负责可视化展示整个链路覆盖了大数据项目的标准流程。你光看这个标题就知道答辩时教授会问哪些问题数据从哪来推荐结果怎么算的系统架构是什么性能瓶颈在哪里每一个问题都能接住不用临时憋答案。这个项目的本质其实是一个“数据闭环系统”爬虫从公开电影数据源抓取影片信息经过清洗后存入数据库用户通过Web界面评分、收藏、浏览系统再根据用户行为数据用协同过滤算法算出推荐结果最后把统计数据和推荐过程用可视化图表呈现给用户和管理员。技术栈很经典但正因为经典才有足够的参考案例和开源资料可查适合一到三个月左右毕设周期完成。适合谁参考一是计算机、大数据、软件工程相关专业的本科生和研究生二是准备找Web后端或数据分析岗位、需要简历项目的求职者三是想快速上手Django和推荐系统的人。这个题目的门槛不高但能讲出来的东西很多属于“下限低、上限高”的选择。在开始之前我建议先想清楚两件事。第一是数据源的选择不同数据源的结构差异很大决定了后面的清洗和建模方式第二是“推荐”的定位你是做一个纯学术层面的算法演示还是做一套贴近线上业务的可运行系统这两者之间的联系和区别要在心里有数。2. 系统整体设计与技术选型这套组合拳到底怎么打2.1 两层架构爬虫和推荐解耦整个系统我建议拆成两个独立模块加上一个Web应用层。独立模块之间用数据库和消息队列沟通而不是互相调用代码这样后续替换和排错都很方便。数据采集层负责从公开电影平台抓取电影名称、类型、地区、上映年份、评分、演员、简介这些基础字段同时还要抓取“热门标签”和用户的评分记录。这一层我建议单独写成爬虫脚本定时触发。数据存储层电影基础信息和用户行为数据用MySQL存热点数据用Redis做缓存比如首页的电影列表、排行榜、推荐结果这些频繁读少写的场景直接走缓存减轻数据库压力。推荐引擎层基于用户行为日志用协同过滤算法离线计算用户相似度或物品相似度生成“用户-推荐电影”映射表。这一层可以用Django命令定时执行也可以放到后台任务队列中。Web展示层Django负责注册登录、电影列表、电影详情、评分、收藏、个人中心、后台管理前端页面用常规的HTMLCSSJavaScript可视化部分用ECharts展示统计报表和推荐结果。模块之间解耦的好处我实际体会很深。有一次爬虫改版后字段结构变了我只动了数据清洗部分的代码推荐逻辑和前端页面完全不用碰整个系统照常运行。如果当初把所有代码写在一个文件夹里这一改动就能让人头大。2.2 技术选型为什么要用Django而不是Flask或直接前后端分离技术选型需要写在论文里的所以要讲清楚“为什么”不能只列技术名。我建议从这样几个维度论证第一是Django自带Admin后台对于毕设中的“后台管理”需求来说不需要再单独写一套管理界面直接在admin注册模型就能实现用户管理、电影管理、评分管理、数据统计演示时给老师看后台也是一种功能展示。第二是Django的ORM足够强大写原生SQL的机会少对于业务逻辑主要在推荐算法里、而不是复杂查询的系统来说效率更高。第三是Django的项目结构统一urls、views、models、templates各司其职写作论文时画架构图、写功能描述都方便。有人喜欢Flask说它轻量但如果你还要自己装ORM、装admin、装session管理、装表单验证加起来的工作量其实不比Django少。还有同学想用Vue做前后端分离系统显得更高级但代价是需要解决跨域、Token认证、Django只做后端API等一堆问题毕设题目是“电影个性化推荐系统”不是“高并发微服务架构”不要为了炫技给自己挖坑。Django模板渲染直接输出页面部署简单、演示顺畅对毕设来说是最稳的方案。2.3 大数据技术栈的“点缀”方式标题里有“大数据”三个字这在毕设中不代表你非要搭一套Hadoop集群。大数据是一个宏观概念落到这个项目里主要体现在几个方面数据集规模达到了万级以上、用户行为数据持续增长、系统需要利用缓存和索引来保证响应速度、可视化看板展示了海量数据的统计规律。这些内容在论文的需求分析和方案设计中写清楚就够了。如果你确实想加入更多“大数据”元素我建议引入Redis做实时推荐结果缓存、用Pandas对评分数据集做聚合分析、用爬虫多线程提高采集效率。这三个方向每个都能写出几百字的实现细节而且和工作岗位的技能要求对得上。2.4 Redis和WebSocket在系统里的真实定位热搜词里反复出现“redis可视化客户端”和“django websocket后台有数据前端推送”这些在实际项目里到底怎么用Redis在我的系统里主要承担两件事第一是缓存首页数据。电影列表、评分排行榜、推荐结果这些接口的查询频次高但数据并不需要实时更新我把它们序列化后扔进Redis设置五分钟过期数据库的压力立刻下来。第二是记录用户最近浏览记录。这个用Redis的List结构非常合适LTRIM控制长度为最近20条取的时候直接LRANGE返回比去MySQL里翻日志快得多。WebSocket则用在可视化大屏的实时刷新上。如果你只做静态图表用ECharts加定时器轮询也能实现但如果你想让页面在后台有新数据时主动推送到前端那就需要Django Channels。Django Channels是为Django增加WebSocket支持的官方方向它通过ASGI服务器代替传统的WSGI让Django具备长连接能力。具体的做法是把新增的电影数据格式化后推送到消息组中可视化大屏通过WebSocket客户端接收并刷新图表。这个功能虽然不是核心算法但在答辩演示时特别出效果算是一个加分项。3. 协同过滤算法核心代码怎么落地、相似度怎么算3.1 基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF这个系统标题直接点名了“协同过滤”所以算法这块必须写得明明白白不能只写一个函数就糊弄过去。协同过滤分为基于用户的协同过滤和基于物品的协同过滤两者在实际推荐中各有侧重。基于用户的协同过滤UserCF的核心思路是找出与你兴趣最相似的用户把那些用户喜欢的但你还没看过的电影推荐给你。这个算法侧重“社群发现”更适用于用户量较小、兴趣社区明显的场景。假设用户A和用户B都对《肖申克的救赎》《阿甘正传》《盗梦空间》打了高分那么系统认为A和B口味相似B给《星际穿越》打了高分A没看过就把《星际穿越》推荐给A。基于物品的协同过滤ItemCF的核心思路是找出物品之间的相似性推荐“与你看过的电影相似的其他电影”。这个算法侧重“物品关系”更适用于物品数相对稳定的场景电影正好符合这个特点。用户喜欢《流浪地球》系统发现《流浪地球》和《星际穿越》经常被同一批用户看完并评分就把《星际穿越》推荐给用户。3.2 相似度计算余弦相似度的推导和实现两个用户的相似度怎么量化最常用的是余弦相似度。把用户评分向量看成高维空间中的一个点夹角越小说明两个用户的方向越一致即兴趣越相似。余弦相似度公式写作similarity cos(θ) (A·B) / (|A|×|B|)A和B分别是两个用户在所有共同评分电影上的评分向量。为什么用余弦而不是直接用欧氏距离因为余弦看重的是“方向一致性”对用户的评分尺度不那么敏感。举个直观的例子一个用户习惯给所有电影打4-5分另一个用户习惯打2-3分两者在绝对值上差距很大但如果他们的排序规律一致余弦相似度仍然会给出较高结果这在推荐场景中更合理。下面是一个可直接运行的相似度计算实现片段使用了numpy加速矩阵运算import numpy as np def cosine_similarity(matrix): 计算用户评分矩阵中两两用户的余弦相似度 matrix: 用户-电影评分矩阵, shape(user_count, movie_count) 返回: user_count × user_count 的相似度矩阵 # 归一化去掉用户自己的评分尺度差异 norm np.linalg.norm(matrix, axis1, keepdimsTrue) # 避免除零将norm为0的位置置为1防止NaN norm[norm 0] 1 matrix_norm matrix / norm similarity np.dot(matrix_norm, matrix_norm.T) return similarity这里有个细节值得注意如果直接用原始评分做点积用户评分偏高或偏低都会造成偏差所以先做L2归一化再点积这在实际效果上往往比直接计算更稳定。我把这份代码放在推荐模块中配合一个稀疏矩阵的构造函数就能处理几千个用户、几千部电影的评分数据。3.3 预测评分和Top-N推荐相似度算完之后下一步是预测目标用户对未评分电影的分数。经典做法是取与目标用户最相似的K个用户按相似度加权平均这些用户对该电影的评分再加上目标用户的平均评分作为偏移矫正pred mean_user sum(sim * (rating - mean_similar_user)) / sum(sim)我用代码实现了这个过程def predict_rating(user_idx, movie_idx, similarity, rating_matrix): 预测用户user_idx对movie_idx的评分 user_ratings rating_matrix[user_idx] # 计算该用户的平均评分考虑偏移 rated_mask user_ratings 0 user_mean user_ratings[rated_mask].mean() if rated_mask.any() else 3.0 # 找对同一部电影也评过分、且相似度最高的前K个用户 sim_users [] for other_idx in range(rating_matrix.shape[0]): if other_idx user_idx: continue if rating_matrix[other_idx, movie_idx] 0: sim_users.append((similarity[user_idx, other_idx], rating_matrix[other_idx, movie_idx], rating_matrix[other_idx][rating_matrix[other_idx] 0].mean() if (rating_matrix[other_idx] 0).any() else 3.0)) if not sim_users: return user_mean top_k sorted(sim_users, keylambda x: x[0], reverseTrue)[:10] weight_sum 0 score_sum 0 for sim, rating, other_mean in top_k: if sim 0: # 相似度为0的用户不要引入噪声 continue score_sum sim * (rating - other_mean) weight_sum sim if weight_sum 0: return user_mean return user_mean score_sum / weight_sum3.4 稀疏矩阵和冷启动问题的处理协同过滤算法在实际数据上最头疼的问题就是稀疏矩阵。假设你有两万部电影用户只看过其中二三十部评分矩阵里绝大多数位置都是0这时候算出来的相似度参考价值有限。我的处理策略是把“用户评分缺失值”统一填成0然后把“只在少数几个共同电影上有评分的用户对”直接过滤掉不参与推荐。这个操作可以在相似度矩阵计算完成后加一个筛子——只有共同评分电影数大于等于5的用户对才保留相似度否则置0。这是我在实操中总结出来的关键技巧没有这一步推荐结果中经常出现只依据一部共同电影就强推的情况效果很差。冷启动问题也要在论文里多写几段。新用户没有行为数据新电影没有评分数据协同过滤完全无法工作。我的解决方法是增加一个“热度推荐”兜底策略新用户进入系统后默认推荐“综合评分最高且评论人数最多”的电影当用户产生了至少5条评分行为后才切换为协同过滤推荐。这就保证了系统在任何情况下都有内容可展示。4. 爬虫与可视化数据从哪来、图表怎么做4.1 爬虫的设计思路与实现注意事项这个系统里的电影数据建议通过公开的网页接口采集。爬虫模块不要写到Django项目里单独建一个spider/目录里面包含spider.py、clean.py和loader.py三个文件。spider.py负责请求网页并解析信息。我用requests库发送请求BeautifulSoup或者lxml解析HTML结构。为了不给目标站点造成压力也为了文明采集务必在代码中做三件事设置合理的User-Agent、每次请求之间随机延时1到3秒、控制单次采集总量。这是爬虫的基本素养也是保证长期稳定运行的经验。import requests import time import random from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_movie_list(start_page0): 从公开页面抓取电影列表这里以豆瓣Top250这类结构为例 movies [] for page in range(start_page, 10): url fhttps://example.com/top250?start{page * 25} try: resp requests.get(url, headersheaders, timeout8) soup BeautifulSoup(resp.text, html.parser) # 伪代码解析每个条目的标题、评分、主演、简介 # items soup.select(.item) # ... time.sleep(random.uniform(1, 3)) except Exception as e: continue return moviesclean.py做数据清洗。爬下来的数据里经常出现的问题包括空值、重复项、字符串中混入html标签、年份和地区字段格式不统一。我用Pandas的drop_duplicates、fillna和自定义正则统一格式清洗后的结果再写入MySQL。这一步建议单独写一个数据质量统计的脚本输出每条字段的缺失比例答辩时还能拿出来当作测试环节的证据。loader.py的作用是把清洗后的DataFrame批量写入数据库。不要一条条insert效率极低一条SQL插入几百条性能最好。我用pandas.DataFrame.to_sql方法配合MySQL的replace模式速度比逐条插入快几十倍。4.2 ECharts可视化的五个常用图表可视化页面是整个系统最直观的展示层也是很多同学最拿得出手的部分。我用ECharts实现了五个核心图表分别对应不同的数据分析主题词云图根据电影类型标签生成直观展示电影类型分布。数据来自数据库中的category字段按出现频次加权。评分分布直方图展示所有电影评分的段位分布通常呈现明显的偏态分布这在大数据统计分析中是一个现成的分析切入点。电影年份分布折线图统计每个年份的上映数量可以看出电影产业随时间的变化趋势。用户评分热度气泡图以用户等级为横轴、电影评分为纵轴、评分数为气泡大小直观展示用户活跃度和内容质量的关系。推荐结果关系图用ECharts的关系图展示“用户-相似用户-推荐电影”的路径这个图在答辩时可以作为核心算法的可视化实证。ECharts的option配置不复杂但有一个容易踩坑的地方是数据格式。ECharts要求词云图中的数据是[{name: 剧情, value: 123}]这种对象数组而从Django后端传来的QuerySet需要经过序列化。我封装了一个通用的图表数据接口用Django的JsonResponse统一返回前端只用去对应接口拿数据填进ECharts的series就行。4.3 大屏实时推送Django Channels与Redis协同可视化大屏是拿得出手的功能点。我做了两种模式第一种是静态加载打开页面后请求一次全部数据适合演示时快速看到图表第二种是实时模式后台爬虫每采集到一批新数据就通过WebSocket推送到前端前端图表自动更新看起来就像“数据在流动”。这个效果不需要真正庞大的数据量就能产生很强的视觉冲击力关键就在于WebSocket推送。用Django Channels时注意版本和依赖关系。channels 3.x对应Django 3.2以上版本需要在settings.py中把ASGI_APPLICATION设置为yourapp.asgi.application再配置channels_redis作为channel layer后端。消费者类里前端通过WebSocket连接后后端就把该客户端加入一个名为movie_updates的群组# consumers.py from channels.generic.websocket import AsyncWebsocketConsumer import json class MovieUpdateConsumer(AsyncWebsocketConsumer): async def connect(self): await self.channel_layer.group_add(movie_updates, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(movie_updates, self.channel_name) async def movie_message(self, event): await self.send(text_datajson.dumps(event[data]))在后台爬虫程序里每写入一批新电影就往Redis的channel group发一条消息消费者类收到后推送给所有已连接的前端页面。整套实时链路不需要任何定时轮询属于“事件驱动”的现代做法。5. 从零搭建数据库设计、Django项目骨架、部署说明5.1 数据库表结构和关系设计在我的项目中数据库表一共有六张分别构成用户、电影、评分、收藏、标签、推荐结果六个维度。关系如下用户表auth_user或自建的user_profile保存基础信息包括用户名、邮箱、注册时间。电影表movie保存电影基础字段字段设计时注意把“类型”和“地区”设计成多对多关系因为一部电影属于多个类型、可能在多个地区上映如果冗余成一个字符串字段后续基于类型的统计分析和相似度计算会非常别扭。评分表rating记录用户对电影的评分这是协同过滤算法的输入数据字段包括user_id、movie_id、score、timestamp必须对user_id和movie_id建联合索引。收藏表favorite记录行为逻辑上评分和收藏是两个不同的用户行为在论文里可以论证协同过滤是基于隐式反馈还是显式反馈。推荐结果表存储算法算好的推荐映射离线计算完成后直接查表返回。MySQL建表时我建议使用InnoDB存储引擎utf8mb4字符集——否则存emoji或者特殊字符时会报错这也是一个常见的坑。5.2 Django项目结构和核心models代码项目目录建议按照Django标准结构组织为了方便后面写论文我把核心模块起了直观的名称movie_reco/ ├── apps/ │ ├── users/ # 用户模块 │ ├── movies/ # 电影模块 │ ├── ratings/ # 评分模块 │ ├── recom/ # 推荐模块 │ └── visual/ # 可视化模块 ├── spider/ # 独立爬虫目录 ├── static/ # 静态资源 └── manage.py核心models的代码大致长这样from django.db import models from django.contrib.auth.models import User class Movie(models.Model): title models.CharField(max_length255) douban_id models.CharField(max_length32, uniqueTrue, nullTrue) category models.CharField(max_length255, help_text类型用逗号分隔) region models.CharField(max_length64, blankTrue) release_year models.IntegerField(nullTrue) rating models.FloatField(default0) rating_count models.IntegerField(default0) actors models.TextField(blankTrue) intro models.TextField(blankTrue) class Meta: db_table movie class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) movie models.ForeignKey(Movie, on_deletemodels.CASCADE) score models.FloatField() created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table rating unique_together [(user, movie)]Rating表的外键设置在数据库和ORM两个层面都有约束但注意unique_together保证了同一个用户对同一部电影只有一条评分记录这是防止脏数据的基本保障。5.3 推荐结果的计算和存储流程推荐流程上我建议在Django中创建一个自定义管理命令这样就能通过python manage.py run_recom手动触发推荐计算也可以挂到定时任务里。计算完成后把结果批量写入推荐结果表前端接口直接读取。计算过程中要加进度打印和日志因为这些数据量一旦上来肉眼盯着等进程是很容易慌的。这里还有一个重要的优化不能每次请求都实时算协同过滤。推荐计算是离线任务在线只做查表。我在论文里用一句话总结就是“离线计算、在线服务”这也是业界推荐系统的常规模式。5.4 部署时踩过的坑与解决办法部署这块最容易翻车我把常见问题列在下面Django静态文件404生产环境关闭DEBUG后Django不再自动处理静态文件需要配置whitenoise或让Nginx代理静态目录。很多同学在服务器上看到页面没有样式就是这一步没做。MySQL连接编码错误配置文件里数据库的OPTIONS要加上charset: utf8mb4否则存中文会出现乱码。gunicorn端口冲突如果服务器上已经跑了其他服务注意改用非80端口或者用Nginx反向代理时统一监听80。Redis连接失败检查Redis是否只绑定了127.0.0.1如果Django在容器里而Redis在宿主机要配置正确的host和port。爬虫和Django在同一进程里跑导致请求阻塞建议把爬虫独立成一个进程或放到Celery任务中不要直接在Django的views里调爬虫函数。6. 常见问题快速排查表问题现象可能原因解决方式数据分析时报“nan”错误评分矩阵中缺失值未处理用0填充缺失值或者只保留有评分记录的矩阵位置推荐结果全是同一类型电影协同过滤相似度计算时未排除共同评分为0的“无意义用户”增加共同观影数量阈值至少≥5部署后CSS全丢了DEBUGFalse后静态文件路由未处理用whitenoise或Nginx配置静态目录爬取的数据写不进MySQL字段长度不够、特殊字符无法存储统一使用utf8mb4文本字段用TEXT类型WebSocket连接总是断开网关超时或channel layer配置错误检查asgi配置确认redis是channel layer后端且运行正常大屏图表加载很慢后端实时聚合计算太重改成预热缓存图表数据先写入RedisDjango Admin无法上传图片缺少Pillow库pip install Pillow用pandas直接读数据库报连接错缺少mysqlclient或pymysql驱动安装对应驱动并在settings.py中配置好engine这些坑我几乎在初版项目里全都踩过一遍尤其是WebSocket的连接断开问题当时以为是代码问题调试了大半天才发现是redis的channel layer超时配置太短。后来把参数调大一切正常。7. 给要写论文的同学LW怎么和代码结合标题里提到“LW”多半指论文或设计文档。很多同学代码写完论文却不知从何下笔这是我见过最多的现象。其实代码和论文是互相呼应的论文的每一章都能在代码里找到对应实体。第一章绪论写“电影个性化推荐系统的背景和意义”可以从信息过载讲起讲用户在海量内容前无法选择所以需要推荐系统来缩小范围。然后引出现有系统的不足引出协同过滤方法的必要性。第二章相关技术介绍把Django、协同过滤、爬虫、可视化这四块各写一页左右特别是协同过滤部分要把数学公式推导过程单独列出来这是论文的含金量所在。第三章需求分析写系统功能需求和非功能需求功能需求分用户端和管理端描述用例图非功能需求写性能、安全性、易用性。第四章系统设计画总体架构图、数据库ER图、模块设计图。第五章系统实现按照模块把核心代码截图和说明放进去这一步要特别注意代码别贴太多贴关键片段即可否则查重风险高。第六章系统测试写测试用例和测试结果功能测试表、性能测试表各一页就够。论文中有一个重点必须突出协同过滤算法在本系统里是如何定制化的。不要只写“用了协同过滤”一定要写出你对标准算法的改进或者适配。例如我的系统里增加了“用户相似度筛选阈值”、“对冷启动用户的兜底推荐”以及“基于Redis的推荐结果缓存”这三个点都是区别于教科书案例的差异化内容也是答辩加分点。8. 我的实际体会这个项目做下来最容易忽略的三件事第一是“数据质量大于算法复杂度”。很多人在协同过滤上花费大量时间调参但忽略了爬虫采集的数据脏、评分数据太少、用户行为不真实这些根本问题。数据不过关再好的算法也出不了效果。我建议先把数据清洗做好评分数据量低于一千条时先不要急着上协同过滤而是把热度推荐做稳。第二是“可视化不只是花架子”。ECharts大屏演示虽然吸引眼球但图表本身要能推导出结论。我的系统里有一张电影评分分布直方图从图上能看出大部分用户评分集中在中高分段说明推荐系统倾向于推荐高分电影这是符合预期的协同过滤倾向答辩时可以顺着这个结论往下讲。图表和数据结论之间构建关联比单纯地“画出来”更有价值。第三是“能跑通和能演示是两回事”。很多项目在本人电脑上运行正常一旦换到答辩用的电脑或部署到服务器上就崩。这提醒你从开发第一天起就要保持项目环境可迁移性用虚拟环境管理依赖写一个清晰的requirements.txt数据库结构和初始数据导出成SQL文件部署文档里把每一步命令写清楚。做这个项目不可能一次成功我也是改了三版才跑通完整流程。第一版爬虫数据乱成一团第二版推荐结果全是乱推第三版把数据清洗和协作过滤阈值修好后推荐结果终于有了直觉上的合理性。项目本身的开发过程就是最好的学习过程别怕推倒重来。
返回列表