简介:面向计算机相关专业学生与毕业设计开发者,资源包以Python+Django+Tensorflow构建了带前端界面的个性化电影推荐系统,涵盖从数据处理、模型训练到Web展示的完整闭环,既可用于毕设/课设,也适合作为推荐系统入门到实战的参考项目。压缩包共2000个文件,大小约31.92MB,其中以Python源码(1170个py)、字节码文件(375个pyc)、Django模板(122个html)、国际化翻译文件(91个po/92个mo)及前端样式脚本(85个js/23个css)为主,同时提供sqlite3数据库、预训练模型pth与详细PDF文档,方便直接运行、查看推荐效果并进一步调参。目前已有128人学习下载,说明其具备不错的参考价值。资源内代码经测试运行成功,目录结构清晰,附有完整项目文档和数据集,能帮助读者快速理解协同过滤或深度推荐等核心实现思路,并在此基础上扩展功能、完成论文撰写或项目演示。
1. 从“排行榜”到“千人千面”:这个电影推荐为什么不只靠SQL
做了几年后端,再看毕业设计项目,发现一个很普遍的问题:很多人把推荐系统做成了SELECT title FROM movie ORDER BY rating DESC LIMIT 10。排行榜当然也是一种推荐,但它是“所有人看到同一份结果”,没有个性,也谈不上算法。这份基于Python+Django+Tensorflow的个性化电影推荐系统项目,核心价值在于把“用户历史行为”变成“用户向量”,再与电影向量做匹配,最终给不同用户返回不同的TopN结果。项目带前端页面、评分数据集和一份较完整的文档,适合计算机相关专业的学生拿来做毕设或课设起步,也适合刚入门推荐系统的后端开发看一遍真实的协同过滤落地链路。下文从数据预处理、相似度计算、TensorFlow模型训练、Django接口到前端联调,把整条实现路径拆开讲。
2. 协同过滤数据链路:从评分表到相似度矩阵
2.1 评分数据集的字段差异与读取方式
推荐系统项目里最常见的数据集是MovieLens系列,它会提供ratings.dat或u.data这类评分文件。不同版本的数据格式差异很大:早期版本用::做分隔符,字段顺序是用户ID、电影ID、评分、时间戳;后期一些版本则直接用CSV格式。这个项目的数据集保留了原始评分记录,读取时不能假设所有文件都是逗号分隔。
import pandas as pd ratings = pd.read_csv( "ml-100k/u.data", sep="\t", header=None, names=["userId", "movieId", "rating", "timestamp"] ) print(ratings.head()) print(ratings.shape)这里用sep="\t"明确指定制表符分隔,names参数补齐四列字段名。不要依赖表头,因为u.data这类文件本身就没有header。pandas读取后应能看到约10万行评分记录,对应900多个用户与1600多部电影,这个规模对协同过滤和TensorFlow训练来说都非常合适,单机CPU也能跑完。读取后先做一次空值检查和评分分布统计,常见做法是ratings["rating"].describe(),如果发现评分列缺失或超出1-5区间,需要先清洗再进模型。
2.2 构造用户-物品稀疏矩阵
协同过滤的输入通常是用户-物品矩阵:行是用户,列是电影,交叉点是评分。直接用pandas的pivot就能得到这个矩阵,但要注意两点:一是用户没有看过的电影位置是NaN,计算相似度前要统一填充为0;二是这个矩阵会非常稀疏,用户平均只对几十部电影评过分,剩余位置全是0。数据量再大一些时,直接用稠密矩阵做相似度计算会非常浪费内存,所以要用稀疏矩阵来包一层。
from scipy.sparse import csr_matrix from sklearn.metrics.pairwise import cosine_similarity user_item = ratings.pivot(index="userId", columns="movieId", values="rating").fillna(0) sparse_matrix = csr_matrix(user_item.values) user_sim = cosine_similarity(sparse_matrix) print(user_sim.shape)csr_matrix只存储非零位置的值和索引,10万条评分在这个矩阵里占用空间很小。cosine_similarity计算用户与用户之间的余弦相似度,输出矩阵的[i][j]位置表示用户i和用户j在评分方向上的接近程度。余弦相似度的特点是只看方向不看长度:一个用户习惯给3分,另一个用户习惯给5分,只要两部电影上的评分趋势一致,相似度依然会很高。实际项目里很多初学者直接用原始评分矩阵算,结果被用户的评分习惯带偏,效果明显变差。
| 字段 | 示例值 | 说明 |
|---|---|---|
| userId | 196 | 用户唯一标识,映射到Django的User表 |
| movieId | 242 | 电影唯一标识,映射到Movie表 |
| rating | 3 | 评分区间1-5,整数或半整数 |
| timestamp | 881250949 | Unix时间戳,用于划分训练集与验证集 |
2.3 评分归一化与时间戳切分
拿到稀疏矩阵后,不要急着算相似度。一个容易被忽视的细节是评分归一化:把每个用户对电影的评分减去该用户自己的平均分,能显著提升协同过滤在冷热不均场景下的表现。习惯打高分的用户和习惯打低分的用户,经过中心化后才有可比性。实现上就是pandas的transform操作。
ratings["rating_centered"] = ( ratings.groupby("userId")["rating"].transform(lambda x: x - x.mean()) )这里groupby("userId")以用户分组,transform把每个用户的评分转换成“相对自己平均分的偏差值”。后续计算相似度或训练矩阵分解模型时,使用中心化后的分数作为标签,模型学习的是用户偏好偏离程度,而不是绝对分数。另一个关键点是数据划分:不要把数据集随机切分为训练集和测试集。推荐场景有天然的时间属性,必须用时间戳排序后按前80%训练、后20%验证,否则会出现用未来数据预测过去的穿越问题。这个错误很隐蔽,写在论文里也容易被答辩老师一眼看穿。
最终把处理好的用户ID、电影ID和中心化评分保存成三份numpy数组,一份喂给后续的TensorFlow模型,一份留作验证。数据链路到这里就走通了一大半,剩下的是如何把用户与电影映射成模型可学习的整数索引。
3. Django 后端与 TensorFlow 双塔模型实现
3.1 Django 应用的 ORM 模型设计
推荐系统的数据最终要落到Django的模型里。这个项目采用了典型的ORM设计:用户沿用Django自带的auth.User,电影和评分单独建表。电影表字段不宜过多,但movieId必须独立存在,因为它要与数据集中原始ID保持映射关系。
from django.db import models from django.contrib.auth.models import User class Movie(models.Model): movie_id = models.IntegerField(primary_key=True) title = models.CharField(max_length=200) genres = models.CharField(max_length=100) class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) movie = models.ForeignKey(Movie, on_delete=models.CASCADE) rating = models.FloatField() timestamp = models.IntegerField() class Meta: constraints = [ models.UniqueConstraint(fields=["user", "movie"], name="user_movie_unique") ]ForeignKey建立用户与电影的多对多关系,UniqueConstraint保证同一个用户对同一部电影只会保留一条评分记录。timestamp字段用IntegerField存Unix时间戳,比DateTimeField更适合快速排序与切分。为什么不直接用自增id关联MovieLens的ID?因为模型训练时要把movieId连续编码,主键按自增走更省事;但movie_id作为原始ID仍保留,用来在推荐结果里反查电影标题和海报。
3.2 TensorFlow 双塔结构与训练参数
推荐模型这块,项目用的是TensorFlow实现的双塔结构,本质上是矩阵分解的神经网络写法。一个塔编码用户ID,一个塔编码电影ID,两层Embedding输出向量做点积,再加上用户偏置和电影偏置。这个设计的好处是:预测阶段可以预计算所有电影向量,实时只算用户向量,接口延迟能压到毫秒级。
import tensorflow as tf class MFModel(tf.keras.Model): def __init__(self, num_users, num_movies, embed_dim=64, **kwargs): super().__init__(**kwargs) self.user_embedding = tf.keras.layers.Embedding(num_users, embed_dim) self.movie_embedding = tf.keras.layers.Embedding(num_movies, embed_dim) self.user_bias = tf.keras.layers.Embedding(num_users, 1) self.movie_bias = tf.keras.layers.Embedding(num_movies, 1) def call(self, inputs): user_id, movie_id = inputs user_vec = self.user_embedding(user_id) movie_vec = self.movie_embedding(movie_id) interaction = tf.reduce_sum(user_vec * movie_vec, axis=1) return interaction + tf.squeeze(self.user_bias(user_id)) + tf.squeeze(self.movie_bias(movie_id))embed_dim=64是经验值,对MovieLens 100K这个量级的数据来说,64维足够表达用户的兴趣分布;调成128效果提升有限,训练时间却几乎翻倍。tf.reduce_sum按最后一维把64维逐元素乘积求和,得到用户与电影向量的内积,内积越大表示越匹配。用户偏置与电影偏置捕捉用户的打分习惯和电影的热门程度,预测值等于交互项加两个偏置,相当于一个完整的打分估计。
编译与训练配置如下:
model = MFModel(num_users=943, num_movies=1682, embed_dim=64) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.MeanSquaredError() ) history = model.fit( [train_user_ids, train_movie_ids], train_ratings, batch_size=1024, epochs=30, validation_split=0.1 ) model.save("recommend_model.h5")| 超参数 | 取值 | 调整说明 |
|---|---|---|
| embed_dim | 64 | 太小欠拟合,太大容易过拟合且推理变慢 |
| batch_size | 1024 | 内存够用可上调到2048,加速收敛 |
| learning_rate | 0.001 | Adam默认值,loss震荡时降到0.0005 |
| epochs | 30 | 训练集较小,30轮足够收敛 |
| loss | MSE | 评分是连续值,用均方误差做回归 |
训练过程中主要看验证集loss是否持续下降。如果出现验证loss上升而训练loss继续下降,说明模型过拟合,优先调低embed_dim或加L2正则。这里TensorFlow和PyTorch在这个量级的数据集上差距并不大,选TensorFlow更看重的是model.save与Django加载的生态衔接,加tensorflow关键词的项目在检索时更容易命中同类资源。
3.3 Django 视图层封装推荐接口
模型训完之后,在Django里封装一个推荐接口。核心逻辑是:根据用户ID拿到向量,遍历所有电影计算预测分,过滤掉用户已经看过的电影,按分数倒序取TopN。
import numpy as np import tensorflow as tf from django.http import JsonResponse from django.views import View from .models import Movie, Rating _model = None def load_model(): global _model if _model is None: _model = tf.keras.models.load_model("recommend_model.h5") return _model class RecommendView(View): def get(self, request, user_id): model = load_model() watched_ids = Rating.objects.filter( user_id=user_id ).values_list("movie_id", flat=True) candidates = Movie.objects.exclude( movie_id__in=list(watched_ids) ).values_list("movie_id", flat=True) user_arr = np.full(len(candidates), user_id, dtype=np.int32) movie_arr = np.array(list(candidates), dtype=np.int32) scores = model.predict([user_arr, movie_arr], batch_size=512).flatten() top_indices = np.argsort(scores)[::-1][:10] top_movies = [candidates[i] for i in top_indices] return JsonResponse({"user_id": user_id, "movies": top_movies})这段代码有两点要注意。Values_list只取评分表中的电影ID,构造一个已看集合;exclude在SQL层面执行了NOT IN,保证推荐结果不会包含用户已经看过并评过分的电影,Django ORM会把它翻译成子查询。model.predict一次传入所有候选电影,而不是在循环里逐条预测,充分利用GPU或CPU向量化计算。如果候选集很大,可以先用一个粗略规则筛选掉明显不相关的电影,再做全量预测,否则响应时间会线性增长。
4. Bootstrap 前端页面与推荐接口的联调
4.1 静态资源布局与页面骨架
项目的前端部分提供了完整的静态资源,包括bootstrap.css、bootstrap.min.css、responsive.css、select2.css、base.css、style2.css等。这套组合对应的是一个后台管理风格的界面:Bootstrap负责栅格布局与基础组件,Select2用于电影下拉搜索框,自定义的style2.css和responsive.css做移动端适配。页面整体分为三个区域:顶部导航、电影选择区、推荐结果卡片区。
| 静态文件 | 作用 |
|---|---|
| bootstrap.css | 全局布局、按钮、卡片、栅格系统 |
| select2.css | 电影搜索下拉框样式 |
| responsive.css | 适配平板和手机屏幕 |
| style2.css | 推荐结果卡片的自定义样式 |
| base.css | 通用基础样式重置 |
Django模板里把静态文件统一放到static目录,模板头部用{% load static %}加载,然后逐个引用。Bootstrap这类依赖先引入,自定义样式放在它后面,避免覆盖关系错乱。Select2需要额外的jQuery依赖,页面底部记得先加载jquery后加载select2的js文件,顺序反了会导致下拉框不可用。
4.2 Ajax 请求推荐接口并渲染结果
前端的交互不采用表单整页提交,而是通过fetch请求Django的推荐接口,拿到JSON后动态渲染卡片。这样做的体验更好,也符合现在尖feel的分离式开发习惯。
<select id="movieSelect" class="select2" style="width: 300px"> <option value=""></option> </select> <div id="recommendCards" class="row"></div>fetch("/api/recommend?userId=1&topN=8") .then(response => response.json()) .then(data => { const container = document.getElementById("recommendCards"); container.innerHTML = data.movies.map(movie => ` <div class="col-md-3"> <div class="card"> <div class="card-body"> <h5>${movie.title}</h5> <span class="badge">预测评分 ${movie.score}</span> </div> </div> </div> `).join(""); });fetch请求的URL写成相对路径/api/recommend而不是完整域名,这是同源部署下最简单可靠的方式。项目采用Django模板渲染页面,Django同时提供API接口,不存在跨域问题,因此不需要额外配置CORS。如果后续决定把前端拆成独立的Vue项目,那就需要在Django的settings.py里加django-cors-headers并配置白名单,否则浏览器会拦截跨域响应。这里也顺便回应了Django前后端分离场景下的一个常见误区:同源时可以不配CORS,分离时才必须配置,面试里常问的跨域问题其实就落在这点上。
4.3 同源部署与前后端分离的取舍
这个项目选择的是同源部署方案:Django既渲染页面又提供接口,代码都在同一个工程里。好处是开发调试不用启动两个服务,部署只需一个runserver或gunicorn进程。缺点是和前端的职责边界不够清晰,如果后续要换前端框架,Django模板层需要整体改掉。
| 对比项 | 同源部署 | 前后端完全分离 |
|---|---|---|
| 开发复杂度 | 低 | 高,需要维护两套工程 |
| CORS配置 | 不需要 | 必须配置 |
| 部署流程 | 单个服务 | 前端静态资源+Nginx接口代理 |
| 适合场景 | 毕业设计、中小型系统 | 多人协作、需独立迭代前端的项目 |
对于毕业设计来说,同源部署更容易演示,也更容易在答辩时讲清楚完整链路。前端代码本身用了Bootstrap + Select2,这套组合展示效果好,依赖也稳定,比手写原生HTML要耐看得多。资源包里style2.css和responsive.css两个自定义文件已经把轮播图、卡片间距和响应式断点处理过了,不建议新手再大改样式,重点应放在前后端数据交互的调试上。
5. 模型加载、冷启动与推荐结果的交叉验证
实际运行这个项目时,有一个最常见的性能坑:不要在视图函数里每次都load_model。TensorFlow加载h5模型文件需要解析图结构和权重,耗时通常在几百毫秒到数秒之间,如果每次请求都加载一次,接口会慢到让人怀疑机器性能。上面代码里用模块级全局变量_model做缓存,首次请求时加载,之后所有请求复用同一个模型实例。但要注意,Django开发服务器默认是单进程,生产环境如果用gunicorn多worker部署,每个worker进程会各自加载一份模型,内存占用约为模型体积乘以worker数。模型在100MB以下时可以接受,但要在部署文档里注明这个特性。
冷启动问题在这个项目里表现得比较明显:新用户没有任何评分记录,协同过滤无法算出他的用户向量,双塔模型也预测不了。此时接口的兜底策略要单独处理,常见的做法是把全局热门电影榜作为默认推荐结果。
def get_top_rated_fallback(user_id, top_n=10): if Rating.objects.filter(user_id=user_id).exists(): return None popular = Movie.objects.annotate( avg_rating=Avg("rating__rating") ).order_by("-avg_rating")[:top_n] return list(popular)这里用Django ORM的annotate按电影聚合平均评分,order_by("-avg_rating")降序排。新用户先看到热门榜单,交互几条后再切换为个性化结果,这种“先热门后个性化”的渐进策略在很多线上系统里也在用。注意Avg("rating__rating")的字段路径容易写错,rating__是Rating表的外键反向引用,后面的rating才是评分字段。
交叉验证推荐效果时,可以写一个对拍脚本:
python manage.py shell -c "from recommender.views import RecommendView; print(RecommendView().get(None, 1))"脚本的作用是直接测试接口输出,但这个方式不够直观。更有效的验证方法是取用户历史评分最高的5部电影,看模型预测结果里是否出现了同类型或同演员的作品;再取一个未注册的新用户,确认接口返回的是热门榜而不是报错。这两条路走通,推荐系统在主流程上才算真正闭环。
如果要进一步提升推荐效果,可以在双塔模型上加入time特征做时间衰减,或者把电影类型genres字段映射成多热向量拼接进电影塔。这个项目提供的数据集字段有限,不建议盲目堆模型复杂度。TensorFlow Playground式的调参思路在这里依然适用:先固定其他参数,只调整embed_dim,观察验证集loss变化,找到当前数据规模下的最优维度。
最终记得把评分数据重新导入后,重新执行一遍训练脚本。推荐系统不是训练一次就结束的项目,数据更新、模型重训、结果验证三个步骤需要定期循环。把这条链路跑通,这份毕业设计的完成度和含金量都会明显高于普通的CRUD管理系统。
本文还有配套的精品资源,点击获取