☰
协同过滤推荐系统原理与Python实现:从Django到相似度算法全解析
2026/10/10 9:45:11 网站建设 项目流程

简介:基于协同过滤推荐算法的电影推荐系统源码,是一份面向高校计算机专业学生的课程设计与期末大作业参考项目。项目采用Python与Django框架搭建,内置协同过滤算法模块、用户评分矩阵数据、SQLite数据库和前端模板,涵盖数据处理、相似度计算、结果展示等完整环节。压缩包共72个文件,以15个Python源码文件、12个HTML模板、12个JS脚本为主,另含CSS样式、图片、CSV数据与Notebook说明文件,整体约1MB,目录结构紧凑,便于按模块阅读和二次开发。作为已获导师指导并通过的97分高分作业,项目运行逻辑完整,下载解压后配合快速启动脚本即可使用,无需额外修改,适合需要快速搭建电影推荐系统或理解协同过滤原理的学习者。目前已有477人学习下载,可助力期末大作业、课程设计及推荐算法入门实验。

1. 协同过滤电影推荐系统:源码到底能帮你解决什么

拿到这套基于 Python 的协同过滤电影推荐系统源码,我没先翻数据库,而是直接打开了recommend_algos.py——算法文件拆得越干净的项目,越容易二次加工,也越适合课程设计和期末大作业场景。这套资源用 Django 搭 Web 层,用 SQLite 存业务数据,核心推荐逻辑单独放在算法模块里,用户评分数据预处理好后以umatrix.csv的形式供算法读取,整个链路从数据加载、相似度计算到 Top-N 推荐都有完整落地的代码。适合两类人:一是正在做推荐系统课程设计、需要快速跑通并看懂算法的学生;二是想了解协同过滤工程实现的 Python 开发者,可以参考它如何把 pandas、numpy 和 Django 串成一个可演示的系统。先说结论:这份源码的架构层次清楚,算法独立成模块,意味着你不仅可以交作业,还能替换成自己的实现。下面按我的拆解习惯,从数据链路、启动方式、算法细节到踩坑记录逐一讲透。

2. 项目结构与数据链路:Django 应用层和推荐算法怎么分工

2.1 Django 应用结构与各模块职责

解压后先看目录,这套项目是标准 Django 工程结构,根目录下有manage.py、db.sqlite3、templates、static,业务代码集中在movie_recommend应用目录下,配置相关的是recommend_system目录。这种“业务应用 + 配置项目”分离是 Django 的默认约定,比把路由、配置全堆在一个文件夹里的写法规范得多,导师抽查时好感度会高很多。

真正干活的文件拆开看:

文件职责
movie_recommend/views.py处理浏览器请求,调用算法模块并渲染页面
movie_recommend/recommend_algos.py协同过滤算法核心:相似度计算、评分预测、推荐生成
movie_recommend/load_data.py读取 CSV 文件,构建用户-电影评分矩阵
movie_recommend/models.py定义数据库表结构,与 SQLite 里的表对应
movie_recommend/admin.py后台管理注册,方便在 Django Admin 里查看数据
manage.pyDjango 命令行入口,迁移、启动都必须走它
umatrix.csv用户-电影评分矩阵的原始数据文件
plots.csv绘图数据,通常用于展示评分分布或推荐结果统计
快速启动.batWindows 下一键安装依赖并启动服务

load_data.py、recommend_algos.py被单独拆出来,而不是把算法写在views.py里,这是我认为这套源码最值得借鉴的地方。视图层只做三件事:接收参数、调用算法、构造上下文传给模板。算法层不关心请求从哪来,只接收矩阵和用户 ID,返回推荐列表。这样职责边界清楚,答辩时解释项目分层会特别顺畅。

2.2 算法选型:为什么默认走基于用户的协同过滤

协同过滤分两大类:基于用户(User-Based)和基于物品(Item-Based)。基于用户的核心假设是“品味相似的人,喜好也趋同”——先找到与当前用户评分行为最接近的 k 个邻居,再把这 k 个邻居喜欢过且当前用户没看过的电影推荐出来。基于物品则反过来,计算电影之间的相似度,推荐与用户看过的电影相近的内容。

为什么这类课程设计项目默认选基于用户?原因有三:第一,用户-电影评分矩阵天然适合演示“找邻居”的过程,蜂群效应和口碑传播场景解释起来直观;第二,项目数据规模通常只有几百到几千个用户,双重循环计算相似度矩阵的性能压力可以忽略,不需要一上来就上矩阵分解;第三,基于用户的推荐结果有明确解释路径——“和你有相似观影习惯的用户也喜欢这部电影”,这句话放到页面上,比简单列个预测分数据更有说服力。

数据流向可以概括成一条直线:umatrix.csv→load_data.py加载并填充评分矩阵 →recommend_algos.py计算相似度 → 预测未评分电影的分数 →views.py取 Top-N 传给模板渲染。整个链路里面,umatrix.csv是数据入口,也是最容易出问题的地方,下面单独讲。

2.3 umatrix.csv 与邻接矩阵:数据进算法前的最后一步

umatrix.csv的本质是一个用户-电影评分矩阵,行是用户 ID,列是电影 ID,表格交叉点的数字代表评分(通常 1~5),0 或空值代表用户没看过这部电影。用这种格式存数据,其实就是在用 Python 构建邻接矩阵——同行同列的交点值就是用户与电影之间的“连接权重”。

我一般会先读一下这个 CSV 长什么样:

import pandas as pd # 读取评分矩阵,第一列是用户ID,作为行索引 df = pd.read_csv("umatrix.csv", index_col=0) # 缺失值填 0,表示未评分 rating_matrix = df.fillna(0).astype("float32") print(rating_matrix.shape) # 期望输出 (用户数, 电影数) print(rating_matrix.head())

这段代码的逻辑:index_col=0让第一列变成 DataFrame 的索引而不是普通数据列;fillna(0)把空值统一补成 0,因为后续相似度计算里我们用“是否大于 0”来判断一个用户是否看过某部电影;astype("float32")是为了节省内存——纯 Python float 是 64 位,矩阵越大差距越明显。

参数说明里重点提一下encoding,如果 CSV 是 UTF-8 编码但在 Windows 下用记事本改过,很可能变成 GBK,这时要显式传encoding="utf-8"或encoding="gbk"才能读出来。这个坑我在第 5 章里还会展开,因为它实在是太常见了。

3. 环境搭建与快速启动:跑通推荐页面的完整操作

3.1 依赖安装与版本兼容:先把 Python 环境理顺

在解压源码之前,先确认机器上 Python 版本。这套项目基于 Django,而 Django 各版本对 Python 版本要求不同,老项目用 Python 3.10+ 跑旧 Django 偶尔会遇到不兼容警告。最稳妥的做法是先用命令看看环境:

python --version pip --version

如果没有报错,说明 Python 和 pip 都已经在系统 PATH 里;如果提示 “python 不是内部或外部命令”,先到 Python 官网装一个 3.7 以上的版本,安装时记得勾选 “Add Python to PATH”。这是整套源码能跑起来的第一道关卡,也是最容易在开始时就被卡住的点。

依赖安装用 pip 一把梭:

pip install django pandas numpy

我还习惯顺手装一个matplotlib,因为 plots.csv 通常需要配合图表展示。如果网络比较慢,可以加国内镜像源:

pip install django pandas numpy -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后别急着启动,先验证一下 Django 版本,避免项目里的代码用了新版本 API 而装到旧版本:

python -m django --version

3.2 快速启动.bat 解析:一键脚本做了什么

Windows 用户直接依赖这个脚本起步。快速启动.bat内部通常是这样的逻辑:

@echo off echo ==================================== echo 电影推荐系统快速启动脚本 echo ==================================== where python >nul 2>&1 if %errorlevel% neq 0 ( echo [错误] 未检测到 Python,请先安装 Python 3.7+ 并加入 PATH pause exit /b ) echo [1/3] 安装依赖包... pip install django pandas numpy -i https://pypi.tuna.tsinghua.edu.cn/simple echo [2/3] 数据库迁移... python manage.py migrate echo [3/3] 启动服务... python manage.py runserver 127.0.0.1:8000 pause

第一段where python是检查 Python 是否在 PATH 里,%errorlevel%不等于 0 说明没找到;第二段安装依赖,用清华源是为了避免默认源超时;第三段执行迁移保证数据库可用;最后一段用127.0.0.1:8000绑定本机访问。注意脚本最后不能直接写pause在runserver后面,因为runserver是阻塞进程,停不下来;通常放在启动失败分支即可。

双击运行后,看到 “Starting development server at http://127.0.0.1:8000/” 就说明成功了。

3.3 手动启动与页面验证:启动、迁移、访问三步走

不用 bat 脚本,手动操作更能理解每一步在干什么。Linux 或 macOS 环境下完整流程:

# 1. 安装依赖 pip install django pandas numpy # 2. 数据库迁移,首次运行必须执行 python manage.py makemigrations python manage.py migrate # 3. 启动开发服务器 python manage.py runserver 127.0.0.1:8000

makemigrations扫描models.py中的模型变化生成迁移文件,migrate把这些迁移落到db.sqlite3里。如果压缩包已经带了可用的数据库,直接migrate也行,但建议makemigrations和migrate都跑一遍,保证模型与数据库状态一致。

启动后浏览器访问http://127.0.0.1:8000,首页应该是电影展示或推荐入口。选择一个用户(或注册新用户)后,系统会调用协同过滤算法,返回针对该用户的电影推荐列表。有的系统可能还需要先登录 Django Admin 创建用户,这取决于views.py是直接读取矩阵索引还是从数据库取用户;正常情况页面里会有入口。

提示:runserver是 Django 自带开发服务器,只建议本地演示和课程答辩使用,不能用于生产环境部署。

4. 协同过滤算法实现:recommend_algos.py 核心代码走读

4.1 相似度计算:皮尔逊相关系数与 mask 过滤

协同过滤的核心第一步是算“用户之间有多像”。最常用的相似度度量是皮尔逊相关系数,公式并不复杂:对两个用户的共同评分项,分别减去各自平均分,再算协方差与标准差的比值。为什么要中心化?因为不同用户的打分尺度不一样——有人习惯给 3~4 星,有人喜欢打 5 星,直接比绝对值会失真。

import numpy as np def pearson_similarity(user_a, user_b): """ 计算两个用户评分向量之间的皮尔逊相关系数 user_a, user_b: 一维 numpy 数组,下标对应电影,0 表示未评分 """ # 1. 找出双方都评过分的电影下标 mask = (user_a > 0) & (user_b > 0) if mask.sum() == 0: return 0.0 # 2. 只保留共同评分项 a_common = user_a[mask] b_common = user_b[mask] # 3. 各自中心化 a_mean = a_common.mean() b_mean = b_common.mean() a_centered = a_common - a_mean b_centered = b_common - b_mean # 4. 计算皮尔逊相关系数 numerator = np.dot(a_centered, b_centered) denominator = np.sqrt(np.sum(a_centered ** 2) * np.sum(b_centered ** 2)) if denominator == 0: return 0.0 return numerator / denominator

这里最关键的一步是mask过滤。0 在矩阵里代表“没看过”,如果不做 mask,直接把 0 当真实评分参与计算,两个没有任何共同观影记录的用户也可能因为大量 0 值而算出一个虚假的高相似度。用mask.sum()检查共同评分数量,如果没有共同项直接返回 0,是协同过滤里最基本的防御性写法。

4.2 构建用户相似度矩阵:双重循环与复杂度边界

算完单个用户对的相似度,接下来要构建整个用户集合的相似度矩阵,方便后续查询“和用户 u 最相似的 k 个人”。

def build_similarity_matrix(rating_matrix): """ 构建用户-用户相似度矩阵 rating_matrix: DataFrame, 行是用户, 列是电影, 值 0 表示未评分 返回: 二维 numpy 数组, sim[u][v] 表示用户 u 和 v 的相似度 """ n_users = rating_matrix.shape[0] sim_matrix = np.zeros((n_users, n_users)) for i in range(n_users): for j in range(i + 1, n_users): score = pearson_similarity( rating_matrix.iloc[i].values, rating_matrix.iloc[j].values ) sim_matrix[i][j] = score sim_matrix[j][i] = score return sim_matrix

这段代码用的是对称矩阵存储,只计算上三角,sim_matrix[j][i]镜像填充,省掉近一半的重复计算。两层循环的时间复杂度是 O(n²),n 是用户数;每个用户对再按电影维度算相似度,总耗时接近 O(n²m)。课程设计的数据规模通常几百个用户,秒级出结果完全够用,不要再往复杂了做。

4.3 Top-N 推荐生成:预测评分、排序与冷启动问题

有了相似度矩阵,推荐过程分三步:找出最相似的 k 个邻居、预测当前用户未看电影的分数、按分数降序取前 N 部。

def recommend_for_user(user_id, rating_matrix, sim_matrix, top_n=10, k=10): """ 给指定用户生成电影推荐列表 rating_matrix: 评分矩阵 sim_matrix: 用户-用户相似度矩阵 top_n: 返回多少部电影 k: 参与预测的最近邻用户数 """ user_ratings = rating_matrix.iloc[user_id].values # 1. 取出与当前用户最相似的 k 个用户 sim_scores = list(enumerate(sim_matrix[user_id])) sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True) neighbors = [(uid, s) for uid, s in sim_scores if uid != user_id and s > 0][:k] # 2. 预测未看过的电影评分 scores = {} for movie_idx in range(rating_matrix.shape[1]): if user_ratings[movie_idx] > 0: continue # 已看过,跳过 weighted_sum = 0.0 sim_sum = 0.0 for uid, sim in neighbors: if rating_matrix.iloc[uid, movie_idx] > 0: weighted_sum += sim * rating_matrix.iloc[uid, movie_idx] sim_sum += sim if sim_sum > 0: scores[movie_idx] = weighted_sum / sim_sum # 3. 按预测分数排序,取前 top_n ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:top_n]

预测公式是“邻居对该电影评分的加权平均”,权重就是相似度。top_n和k是两个最值得调的参数:top_n控制推荐列表长度,页面展示用 10 个比较合适;k控制参与投票的邻居数,太小推荐结果随机性大,太大会把低相似度用户也拉进来稀释精度,数据集不大时取 10~20 都是常见范围。

这一套逻辑跑下来,用户收到的是“被相似用户喜欢但是自己没看过”的电影列表。要注意冷启动问题:如果某个用户没有评分记录,sim_scores里全是 0,neighbors为空,scores也为空,最后推荐列表是空的。遇到这种情况,常见做法是回退到热门榜——把全局评分最高的几部电影垫底补上。这是协同过滤的典型边界,答辩时主动讲出来反而加分。

5. 常见问题与避坑记录:运行期最容易翻车的五件事

5.1 启动脚本闪退,窗口一闪而过

现象:双击快速启动.bat,黑窗口刚弹出来就消失,服务没起来,人也懵了。

原因:绝大多数情况是 Python 不在系统 PATH 里,where python判断失败,脚本走到exit /b直接退出,错误信息根本来不及显示。

解决:不要直接双击,先在命令行确认python --version能执行。如果不行,重装 Python 时勾选 “Add Python to PATH”;如果系统装了多个 Python 版本,脚本里改python为python3或完整的 Python 路径。我习惯在脚本开头第一行加一个cmd /k方式保留窗口,或者干脆先手动跑一遍命令,别把时间耗在猜上。

5.2 CSV 编码问题导致数据加载失败

现象:运行load_data.py或启动服务后,控制台报UnicodeDecodeError,提示utf-8无法解码。

原因:umatrix.csv和plots.csv在 Windows 下被记事本或 Excel 改过,保存成了 GBK/ANSI 编码,而 pandas 默认按 UTF-8 读取,中文注释和字段值就解码失败了。

解决:在pd.read_csv()里显式指定编码:

df = pd.read_csv("umatrix.csv", index_col=0, encoding="utf-8")

如果已经报错,先用记事本打开 CSV 另存为 UTF-8,再跑。临时应急可以改encoding="gbk",但提交代码时整体统一成 UTF-8 才是长久之计,否则 Linux 服务器上又会踩一遍。

5.3 迁移提示表已存在,migrate 直接报错

现象:执行python manage.py migrate,报django.db.utils.OperationalError: table already exists。

原因:项目压缩包里自带了db.sqlite3,数据库结构已经初始化过;再次 migrate 时,Django 发现已有表,但没有对应的迁移记录,双方对不上就炸了。

解决:看需求。不需要保留原数据的话,直接删除db.sqlite3再重新执行makemigrations和migrate,数据库会从零重建;想保留数据,执行python manage.py migrate --run-syncdb,Django 只补缺失的表,不重建已有表。课程设计场景我一般选前者,干净利落。

5.4 页面打开样式全丢,控制台 404

现象:项目能启动,首页也有内容,但 CSS 和图片全部不加载,控制台一堆/static/xxx报 404。

原因:settings.py里STATICFILES_DIRS配置的路径不对,或者DEBUG=False时 Django 默认不托管静态文件。

解决:先把DEBUG保持为 True,开发环境下 Django 会自动处理静态资源。如果为了演示把DEBUG关了,就执行:

python manage.py collectstatic

把static目录文件收集到STATIC_ROOT下。检查templates里模板开头是否加了{% load static %},这也是新手最容易漏的一步。

5.5 端口被占用,runserver 起不来

现象:执行runserver提示[Errno 10048] error while attempting to bind on address ('127.0.0.1', 8000)。

原因:上一次服务没关干净,或者电脑上另一个项目占了 8000 端口。

解决:最快的方式换端口启动:

python manage.py runserver 127.0.0.1:8001

课程设计演示场景没有固定端口依赖,换个端口不影响任何功能,比花时间找进程杀掉更省事。如果一定要清端口,Windows 下用netstat -ano | findstr 8000找到 PID 再结束进程。

6. 进阶技巧:给推荐结果加解释,让协同过滤从黑匣子变白盒

这套源码跑通以后,默认推荐页只展示电影标题和海报。如果能给每部推荐电影下面加一行解释——“因为用户 A、B 和你口味相似,他们也喜欢这部电影”,演示效果会立刻不一样,而且这比抽象调参更容易打动导师。本质上是把predict_score阶段用到的邻居信息带出来展示。

实现思路并不复杂,在推荐函数里同时返回相似用户列表:

def recommend_with_explain(user_id, rating_matrix, sim_matrix, top_n=10, k=10): user_ratings = rating_matrix.iloc[user_id].values sim_scores = list(enumerate(sim_matrix[user_id])) sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True) neighbors = [(uid, s) for uid, s in sim_scores if uid != user_id and s > 0][:k] results = [] for movie_idx in range(rating_matrix.shape[1]): if user_ratings[movie_idx] > 0: continue weighted_sum, sim_sum = 0.0, 0.0 used_neighbors = [] for uid, sim in neighbors: if rating_matrix.iloc[uid, movie_idx] > 0: weighted_sum += sim * rating_matrix.iloc[uid, movie_idx] sim_sum += sim used_neighbors.append(uid) if sim_sum > 0: predicted = weighted_sum / sim_sum results.append((movie_idx, predicted, used_neighbors[:3])) results.sort(key=lambda x: x[1], reverse=True) return results[:top_n]

改动点就一处:每个打分结果额外携带used_neighbors,取前 3 个相似用户作为解释来源。views.py拿到推荐结果后,再查一下用户表把 ID 映射成昵称,模板里渲染成“因为 xxx、xxx 也喜欢它,且你们相似度达到 xx”就行。这个做法没有改变任何算法逻辑,只是把协同过滤过程中原本就存在的邻居信息暴露出来,但用户体验和报告演示的观感完全不一样。

从那以后,我每次拆推荐系统项目都会强制自己先把“推荐理由”补上,再谈调参优化。评分预测数值本身很难被用户感知,推荐理由却能让别人一眼看到系统“为什么推荐”,这个习惯在答辩和实际项目里都帮了我很多,希望也能帮到你。

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

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

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

立即咨询