☰
电影推荐系统源码实战:协同过滤与矩阵分解全解析
2026/10/18 16:43:38 网站建设 项目流程

简介:基于机器学习的电影推荐系统源码与配套数据集,面向机器学习初学者、高校学生以及需要快速搭建演示项目的开发者,可用于课程设计、毕业设计或推荐系统入门实践,覆盖从数据清洗到推荐结果展示的完整流程。全套共2000个文件,压缩包约3.15MB,包含源码文件、编译缓存、评分与电影信息等数据集以及SQLite数据库,还配套大量前端页面样式、脚本和图片资源,可直接构建完整的交互界面,省去自行搭建页面的时间。已有745人学习下载。解压后即可运行,目录结构清晰,适合对照研究协同过滤或基于内容的推荐流程,并能将自带数据替换为自有评分数据进一步实验。源码涵盖数据预处理、候选集生成、评分预测及推荐展示等环节,便于按模块理解推荐系统整体链路,也适合在此之上二次开发。

1. 能跑通的机器学习项目长什么样:电影推荐系统源码包到底给了你什么

做机器学习这行最怕的不是算法难,是数据、代码、环境三样凑不齐。很多教程给你一段算法代码,结果数据集格式对不上,跑到一半不是缺库就是中文乱码。这次拆的这份资源是“下载即用”的电影推荐系统,Python源码加数据集打包在一起,后端有完整的协同过滤和矩阵分解训练流程,前端配好了Bootstrap和Layer弹层界面,rating和movie两张CSV表往里一放就能启动。适合三类人:正在做课设的在校生、刚接触推荐系统想少走弯路的开发者、想把推荐逻辑快速落地成演示Demo的从业者。第一件事不是看代码,是先搞懂它背后的推荐算法选型。

2. 两种核心推荐路线:基于用户的CF和基于物品的CF该怎么选

2.1 先从CSV里看到底有什么:movie.csv和ratings.csv的字段含义

这份资源的数据集是两张表。movie.csv存电影属性,ratings.csv存用户打分记录,这是推荐系统最经典的Rating-Item结构。按常见的字段设计,movie.csv里至少有movieId、title、genres,部分扩展版本还带releaseYear、runtime。ratings.csv至少是userId、movieId、rating、timestamp四列,timestamp用来做时间衰减或者划分训练集测试集很关键。

先把数据读进来看看分布:

import pandas as pd movies = pd.read_csv('movie.csv', encoding='utf-8') ratings = pd.read_csv('ratings.csv', encoding='utf-8') print(movies.info()) print(ratings.info()) # 看用户评分行为分布 print(ratings.groupby('userId')['rating'].count().describe())

逻辑说明:info()能快速看到每一列有没有空值、数据类型是什么,很多加载报错都是这里先暴露的。groupby后describe看的是每个用户评了多少部电影,如果平均数很低,说明数据稀疏,直接跑协同过滤效果会差。参数说明:encoding='utf-8'不一定对,如果读出来报解码错误,换成encoding='gbk'或'latin-1'再试,这是CSV相关项目最常见的翻车点。

2.2 基于用户CF的原理:相似的人看相似的东西

基于用户的协同过滤逻辑很简单:给A推荐B看过的电影,前提是A和B的历史评分高度相似。先按用户算两两相似度,再找最近邻,加权聚合出未看影片的预测分。核心公式是皮尔逊相关系数,比余弦相似度更适合评分数据,因为它做了均值中心化,能抵消用户打分尺度宽严不同的问题。

import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 构造用户-物品评分矩阵 user_item_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0) # 计算用户间余弦相似度 user_sim = cosine_similarity(user_item_matrix) user_sim_df = pd.DataFrame( user_sim, index=user_item_matrix.index, columns=user_item_matrix.index )

逻辑说明:pivot_table把长表转成宽表,行是用户,列是电影,值是评分,空位补0。余弦相似度在高维稀疏向量下数值会偏小,实际项目里更推荐这么改:先对每个用户减去自己的平均分再算相似度,相当于皮尔逊相关系数。参数说明:fillna(0)是权宜之计,它把“没看过”和“打了0分”混为一谈,严谨做法是只对共同评分过的电影算相似度。

2.3 基于物品的CF:冷启动不友好场景下的备选方案

基于物品的CF是给A推荐“和他看过的电影相似的其他电影”,前提是物品间有足够多的共同评分用户。它的优势是物品相似度比用户相似度稳定,离线算好以后线上查询速度快。缺点是完全没有评分记录的新用户依然推荐不出来,这属于冷启动问题,单纯靠CF解决不了,需要生成式内容推荐比如基于电影类型、导演做特征匹配兜底。

# 物品相似度矩阵(转置即可) item_sim = cosine_similarity(user_item_matrix.T) item_sim_df = pd.DataFrame( item_sim, index=user_item_matrix.columns, columns=user_item_matrix.columns )

两者怎么选,我的经验是:看数据集行数比例。用户量远大于电影量,就选基于物品CF;电影量远大于用户量,选基于用户CF。一般评分数据都是用户多、电影少,所以这套源码里大概率核心逻辑是围绕基于物品的相似度展开。

3. 矩阵分解实战:SVD怎么把稀疏评分矩阵补完整

3.1 降维推荐和CF的本质区别

协同过滤的缺陷是物品和用户都多时,相似度矩阵会膨胀得没法看。一台普通笔记本电脑,几千个用户乘几千部电影,稀疏矩阵存下来可能占几个GB内存。矩阵分解的思路完全不同:把用户-物品矩阵拆成两个低维稠密矩阵,一个代表用户潜在因子偏好,一个代表物品潜在因子属性。预测评分就是两者内积。训练时迭代逼近已有评分,同时隐式填充缺失值。

3.2 用surprise库快速实现SVD训练

这份源码的资源设计围绕典型案例展开,判断里面应该是用Python标准库加少量第三方依赖实现训练。不过无论源码怎么实现,落地时我推荐用surprise库再验证一遍结果,因为它封装了完整的SVD、NMF和交叉验证工具:

from surprise import SVD, Dataset, Reader from surprise.model_selection import cross_validate reader = Reader(line_format='user item rating timestamp', sep=',') data = Dataset.load_from_file('ratings.csv', reader=reader) algo = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02) cv_results = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)

逻辑说明:Reader负责告诉surprise每列是什么含义,line_format按顺序声明四个字段,sep指定分隔符是逗号,这直接对应ratings.csv的标准结构。SVD里n_factors是潜在因子数量,越大模型拟合能力越强但更容易过拟合;n_epochs是训练轮数;lr_all是学习率;reg_all是正则化系数。常见调参组合是n_factors在20到100之间,学习率0.005左右。参数说明:cv=5表示五折交叉验证,如果数据集太小可以改成3折。

3.3 不依赖surprise的手写SVD核心循环

源码里如果要自己实现SVD,核心逻辑是随机梯度下降,每一步只抽一个评分样本,更新对应的用户向量和物品向量。关键代码框架如下:

import numpy as np def train_svd(ratings, n_factors=20, lr=0.01, reg=0.05, epochs=10): user_ids = ratings['userId'].unique() movie_ids = ratings['movieId'].unique() user_map = {u: i for i, u in enumerate(user_ids)} movie_map = {m: i for i, m in enumerate(movie_ids)} # 初始化因子向量 user_vec = np.random.randn(len(user_ids), n_factors) * 0.1 movie_vec = np.random.randn(len(movie_ids), n_factors) * 0.1 user_bias = np.zeros(len(user_ids)) movie_bias = np.zeros(len(movie_ids)) global_bias = ratings['rating'].mean() for epoch in range(epochs): for _, row in ratings.iterrows(): u, m, r = user_map[row['userId']], movie_map[row['movieId']], row['rating'] pred = global_bias + user_bias[u] + movie_bias[m] pred += np.dot(user_vec[u], movie_vec[m]) error = r - pred user_bias[u] += lr * (error - reg * user_bias[u]) movie_bias[m] += lr * (error - reg * movie_bias[m]) u_factor = user_vec[u].copy() user_vec[u] += lr * (error * movie_vec[m] - reg * user_vec[u]) movie_vec[m] += lr * (error * u_factor - reg * movie_vec[m]) return user_bias, movie_bias, user_vec, movie_vec

逻辑说明:全局均值加用户偏置加物品偏置是预测基线,因子内积负责捕捉个性化差异,这三部分加在一起就是完整预测。训练时用.copy()保存旧用户向量,防止更新用户向量后马上影响到物品向量更新。参数说明:随机初始化标准差0.1是关键经验,太大会让初始误差爆炸,太小会让梯度消失;正则化系数控制偏置和向量的复杂度,越大越不容易过拟合。

注意:如果你的数据集是稀疏的,全量遍历DataFrame会非常慢。实际工程会先把每行转成三元组list,再打乱顺序做负采样。但课设规模几百MB以内,上述方案足够。

4. 数据清洗和特征工程:评分表里那几类坑最容易被忽略

4.1 稀疏度、重复评分、时间戳异常检查

很多人在数据预处理这一步翻车,最大原因是评分表本身就不干净。有三类问题最常见:同一用户对同一电影出现多条评分记录(代表数据采集端没做过去重);评分不在1到5闭区间内(超出边界的脏数据);时间戳缺失或为负数。这部分源码里的评分数据如果是官方公开的数据集,一般干净度还行,但保不齐是爬下来的,建议还是做一遍:

# 检查重复记录 dup_count = ratings.duplicated(subset=['userId', 'movieId']).sum() # 检查评分范围 invalid_rating = ratings[ (ratings['rating'] < 1) | (ratings['rating'] > 5) ].shape[0] # 时间戳转换完整性检查 ratings['ts_datetime'] = pd.to_datetime(ratings['timestamp'], unit='s') print(ratings['ts_datetime'].describe())

逻辑说明:duplicated能查到同一用户对同一电影重复评分的情况,发现后取mean或最新一次都行,建议保留最新一次,因为那代表用户最新偏好。invalid_rating过滤掉异常区间的数据,否则会影响后续SVD训练的误差收敛。参数说明:pd.to_datetime里unit='s'表示时间戳单位是秒,如果用毫秒单位会得到离谱年份,检查时看年份分布就能发现问题。

4.2 电影侧的特征构造:从类型字符串拆出可计算属性

movie.csv里的genres字段通常是用竖线分隔的字符串,比如Action|Adventure|Sci-Fi。直接当字符串没法进模型,要做one-hot展开。同时可以构造出辅助特征:电影被评次数、平均评分、平均评分与中位数的偏差。

# 提取电影特征 genre_dummies = movies['genres'].str.get_dummies(sep='|') movie_features = pd.concat([movies[['movieId']], genre_dummies], axis=1) # 电影被评统计量 rating_stats = ratings.groupby('movieId')['rating'].agg(['count', 'mean', 'std']) rating_stats.columns = ['rating_count', 'rating_mean', 'rating_std'] movie_feature_final = movie_features.merge(rating_stats, on='movieId', how='left')

逻辑说明:get_dummies(sep='|')是处理这个字段最快的方法,直接展开成多列二值变量,Action列填1就代表该电影包含动作类型。rating_count和rating_mean这两个特征特别有用,它们能识别“什么是热门电影”——如果Mean高、Count也高,那是口碑好且热度高的电影;Mean高但Count低,可能是小众冷门佳作。参数说明:dropna策略上,对合并后没有评分统计的冷门电影,rating_count补0,rating_mean补全局平均分,这是业界常见做法。

4.3 训练集测试集切分不能随机切

推荐系统切数据不能像普通分类任务那样随机切。随机切会造成同一用户出现在训练集和测试集里,出现信息泄露。正确做法是按用户切分:每个用户拿出一部分评分当测试,剩下的当训练,保证测试用户完全没在训练里见过。

from sklearn.model_selection import train_test_split train_list, test_list = [], [] for uid, group in ratings.groupby('userId'): if len(group) < 2: train_list.append(group) continue t1, t2 = train_test_split(group, test_size=0.2, random_state=42) train_list.append(t1) test_list.append(t2) train_df = pd.concat(train_list, ignore_index=True) test_df = pd.concat(test_list, ignore_index=True)

逻辑说明:按组切分后,测试集里每个用户都还有至少一条训练评分,模型能用他们的历史行为推荐,同时测试评分又不会泄漏。参数说明:random_state固定成42,保证每次跑出来结果一致,方便对比调参效果。test_size=0.2是常见的80/20划分,数据量小的话可以改成0.3。

5. 避坑手册:从运行到出结果最容易翻车的五个点

5.1 数据路径问题:文件读不出来全是绝对路径的锅

现象:源码在别人环境里跑得好好的,自己一运行就FileNotFoundError。原因:代码里写了固定的绝对路径,比如C:/Users/某用户/Desktop/项目/data/ratings.csv,换电脑路径全变了。解决:把读取逻辑改成相对路径,基于脚本所在目录动态拼接:

import os base_dir = os.path.dirname(os.path.abspath(__file__)) ratings_path = os.path.join(base_dir, 'data', 'ratings.csv')

从那以后我每次拿到一套新源码,第一件事就是全局搜.csv路径,把绝对路径全部换掉。希望帮到你。

5.2 编码问题:gbk和utf-8来回试

现象:pandas读取movie.csv时报UnicodeDecodeError,或者读出中文全是乱码。原因:CSV文件保存时是gbk编码,代码里硬编码了utf-8,两边对不上。解决:先用记事本打开CSV另存为UTF-8 BOM,或者读取时自动探测:

import chardet with open('movie.csv', 'rb') as f: det = chardet.detect(f.read(10000)) df = pd.read_csv('movie.csv', encoding=det['encoding'])

5.3 SVD训练不收敛:学习率太高,loss直接变成NaN

现象:训练日志里loss先是下降,突然变成NaN,然后所有预测结果全是0。原因:学习率设置过大,梯度更新时数值溢出。解决:把学习率调小到原来的十分之一,或者检查评分数据里是否存在极大异常值,比如rating列混入一个999。也常因为用户向量初始化标准差过大,改成0.05到0.1之间。

5.4 相似度矩阵内存爆炸:几百部电影就占几个GB

现象:程序运行到计算item_sim时,内存占用一路飙升然后被杀进程。原因:构建了稠密的物品相似度矩阵,平方级增长。解决:改成稀疏矩阵存储,或者用csr_matrix代替DataFrame:

from scipy.sparse import csr_matrix sparse_matrix = csr_matrix(user_item_matrix.values) item_sim = np.dot(sparse_matrix.T, sparse_matrix)

5.5 前端页面数据对不上:后端推荐的和前端展示的不是同一份

现象:两个模型训练出来的结果互相覆盖,或者缓存了旧模型,页面老是显示上一次的结果。原因:前后端共享同一个模型文件路径,但训练完成后没有刷新缓存。解决:每次训练结束强制把模型序列化到固定路径,模型加载模块里加一个时间戳判断:

model_path = 'svd_model.pkl' if os.path.exists(model_path): last_mtime = os.path.getmtime(model_path) else: last_mtime = 0

6. 把推荐结果搬到页面上:Bootstrap和Layer弹层怎么组装一个能看的界面

源码里带了bootstrap.css、bootstrap-theme.css、layer.css、star-rating.min.css,这一套前端栈本身就暗示了这个项目不是纯命令行,而是一个带交互的页面系统。最常见的组装方式:首页展示电影海报列表,用户点击星级打分,点击提交后后端返回推荐结果,用layer弹出层展示评分成功提示,再用Bootstrap栅格系统展示新的推荐卡片。

我一般会把推荐后端封装成一个最小可用的Flask接口,输出JSON给前端渲染:

from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/recommend', methods=['POST']) def recommend(): user_id = request.json.get('userId') watched_ids = request.json.get('watchedIds', []) rec_list = get_recommendations(user_id, watched_ids) # 调用算法模块 return jsonify({'code': 0, 'data': rec_list}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

逻辑说明:这里返回的是标准JSON结构,前端拿到后直接拼卡片。get_recommendations内部调用已经训练好的模型,对候选电影预测评分,按分数降序截取前N个。参数说明:watchedIds的用途是过滤用户已经看过的电影,避免重复推荐,这是推荐系统上线的基本礼仪。

配合Bootstrap的栅格布局,代码大概长这样:

<div class="row"> <div class="col-md-3" v-for="movie in recMovies"> <div class="thumbnail"> <img src="{{ movie.poster }}" alt=""> <div class="caption"> <h4>{{ movie.title }}</h4> <p><span class="rating">{{ movie.pred_score }}</span> / 5</p> </div> </div> </div> </div>

逻辑说明:这里用了Vue的语法示意,如果不用前端框架,直接用jQuery的$.each拼接字符串输出到容器里也完全可行。col-md-3的含义是一行四列,宽屏下面一排四部电影,另外两个细节要注意:一是豆瓣图片有防盗链,如果poster字段是外链,页面会显示裂图,需要用后端抓取后转存或加Referer绕过;二是评分显示用star-rating.min.css那一套的话,需要引入对应的图标字体文件,漏了则星星全变成乱码方块。

这套前端组装的时长大概占整个项目的三成。很多人以为推荐系统的难点全在模型,实际到做可演示课设时,前端能正常展示推荐结果才是验收重点。从那以后我碰任何带界面源码,都会先确认CSS里的字体资源相对路径是否完整,确认数据接口的字段名是否匹配页面模板里的变量名,确认弹层组件layer的初始化位置在DOM加载完成后,再动手跑整个流程。希望帮到你。

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

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

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

立即咨询