基于TensorFlow与Django的个性化电影推荐系统项目实战解析

发布时间:2026/9/11 16:41:45
基于TensorFlow与Django的个性化电影推荐系统项目实战解析 简介面向计算机相关专业学生与毕业设计开发者资源包以PythonDjangoTensorflow构建了带前端界面的个性化电影推荐系统涵盖从数据处理、模型训练到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。排行榜当然也是一种推荐但它是“所有人看到同一份结果”没有个性也谈不上算法。这份基于PythonDjangoTensorflow的个性化电影推荐系统项目核心价值在于把“用户历史行为”变成“用户向量”再与电影向量做匹配最终给不同用户返回不同的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, headerNone, 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(indexuserId, columnsmovieId, valuesrating).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分只要两部电影上的评分趋势一致相似度依然会很高。实际项目里很多初学者直接用原始评分矩阵算结果被用户的评分习惯带偏效果明显变差。字段示例值说明userId196用户唯一标识映射到Django的User表movieId242电影唯一标识映射到Movie表rating3评分区间1-5整数或半整数timestamp881250949Unix时间戳用于划分训练集与验证集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_keyTrue) title models.CharField(max_length200) genres models.CharField(max_length100) class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) movie models.ForeignKey(Movie, on_deletemodels.CASCADE) rating models.FloatField() timestamp models.IntegerField() class Meta: constraints [ models.UniqueConstraint(fields[user, movie], nameuser_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_dim64, **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, axis1) return interaction tf.squeeze(self.user_bias(user_id)) tf.squeeze(self.movie_bias(movie_id))embed_dim64是经验值对MovieLens 100K这个量级的数据来说64维足够表达用户的兴趣分布调成128效果提升有限训练时间却几乎翻倍。tf.reduce_sum按最后一维把64维逐元素乘积求和得到用户与电影向量的内积内积越大表示越匹配。用户偏置与电影偏置捕捉用户的打分习惯和电影的热门程度预测值等于交互项加两个偏置相当于一个完整的打分估计。编译与训练配置如下model MFModel(num_users943, num_movies1682, embed_dim64) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.MeanSquaredError() ) history model.fit( [train_user_ids, train_movie_ids], train_ratings, batch_size1024, epochs30, validation_split0.1 ) model.save(recommend_model.h5)超参数取值调整说明embed_dim64太小欠拟合太大容易过拟合且推理变慢batch_size1024内存够用可上调到2048加速收敛learning_rate0.001Adam默认值loss震荡时降到0.0005epochs30训练集较小30轮足够收敛lossMSE评分是连续值用均方误差做回归训练过程中主要看验证集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_iduser_id ).values_list(movie_id, flatTrue) candidates Movie.objects.exclude( movie_id__inlist(watched_ids) ).values_list(movie_id, flatTrue) user_arr np.full(len(candidates), user_id, dtypenp.int32) movie_arr np.array(list(candidates), dtypenp.int32) scores model.predict([user_arr, movie_arr], batch_size512).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 idmovieSelect classselect2 stylewidth: 300px option value/option /select div idrecommendCards classrow/divfetch(/api/recommend?userId1topN8) .then(response response.json()) .then(data { const container document.getElementById(recommendCards); container.innerHTML data.movies.map(movie div classcol-md-3 div classcard div classcard-body h5${movie.title}/h5 span classbadge预测评分 ${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_n10): if Rating.objects.filter(user_iduser_id).exists(): return None popular Movie.objects.annotate( avg_ratingAvg(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管理系统。本文还有配套的精品资源点击获取