ARTICLE DETAIL

资讯详情

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

基于Python的小说推荐系统实战:从数据清洗到协同过滤与Flask部署

基于Python的小说推荐系统实战:从数据清洗到协同过滤与Flask部署 简介基于Python实现的小说推荐系统源码附超详细注释专为课程设计、毕业设计及入门学习场景打造。压缩包共16个文件以4个Python脚本为核心覆盖爬虫采集、数据处理、推荐算法、交互展示等关键模块搭配多个CSV/TXT格式的小说数据集及README说明配合XML项目配置文件整体仅130KB轻量且结构清晰便于直接运行和二次开发。已有998人下载学习代码经测试运行成功适合计算机、人工智能、通信工程等专业学生参考也适合动手能力较强的小白进阶。通过逐行研读源码可掌握基于内容的推荐、协同过滤等算法思路学习爬虫数据清洗与界面设计流程还可替换数据源或扩展功能快速完成课程设计或毕业设计项目演示。1. 基于Python实现小说推荐系统数据准备比算法更决定成败经历过课程设计的人大概都有同感推荐系统听起来是“算法”的事可真正打开工程一看最常见的结局是协同过滤代码只写了十几行数据清洗却占据了80%的改动量。小说推荐系统更是如此——它的样本不是现成的用户“怎么看小说”这件事往往散落在书评、阅读时长、收藏和打分记录里得有人先把这些乱七八糟的日志整理成一张干净的评分矩阵后面的推荐才能成立。所以做这个选题的第一步不是import numpy而是先把“用户-小说-评分”三元组定死。只要这几行数据结构说得清楚推荐算法用什么都是锦上添花。这篇文章就按照课程设计最常见的交付形态——Python pandas scikit-learn Flask——把数据到接口的完整链路走一遍新手照着能跑通有经验的人也能拿到几个值得抠的参数细节。2. 小说推荐系统的数据准备从行为日志到评分矩阵2.1 先定数据结构用户ID、小说ID、评分值推荐系统无论表面多复杂输入层永远收敛成一个稀疏矩阵。矩阵的行是用户列是小说交叉点是用户对小说的“反馈强度”。在小说阅读场景里反馈可以是明确的1到5星评分也可以是隐式的“看了前30页就不看了”这类行为转换来的分值。常见做法是准备一个CSV文件至少包含三列user_id、novel_id、rating。为了课程设计的好看程度还可以加上read_time、genre做辅助特征但第一版只需要这三列。字段类型说明user_idstr/int用户唯一标识建议统一为字符串novel_idstr/int小说唯一标识同样建议字符串ratingfloat评分1.0 - 5.0隐式反馈可以换算成0/1如果手头没有现成数据常见做法是拿爬虫去抓公开的读书评论页面或者用Python脚本自己生成一份仿真数据——随机分配用户和小说再按正偏分布给评分。课程设计阶段建议先跑通流程再换真实数据。import pandas as pd import numpy as np np.random.seed(42) user_ids [fu{i:03d} for i in range(1, 201)] novel_ids [fn{i:03d} for i in range(1, 501)] rows [] for uid in user_ids: rated_novels np.random.choice(novel_ids, sizenp.random.randint(5, 30), replaceFalse) for nid in rated_novels: rating np.random.randint(1, 6) rows.append([uid, nid, rating]) df pd.DataFrame(rows, columns[user_id, novel_id, rating]) df.to_csv(novel_ratings.csv, indexFalse, encodingutf-8-sig)这段代码会生成200个用户、500本小说、每个人读过5到30本的仿真评分数据。encodingutf-8-sig是特意加的不然Windows下用Excel打开中文列名会乱码。生成数据的随机逻辑放在课程设计文档里就是“模拟用户阅读行为”答辩时能少被问一句“数据哪来的”。2.2 用pandas做类型转换与缺失值清洗原始数据几乎不可能干干净净进矩阵。评分为空、用户ID格式不一致、重复记录、超出评分区间这四类问题是课程设计中出现频率最高的。处理顺序建议固定成一条流水线import pandas as pd # 读取原始数据 df pd.read_csv(novel_ratings.csv, encodingutf-8-sig) # 统一列名并做类型转换 df.columns [user_id, novel_id, rating] df[rating] pd.to_numeric(df[rating], errorscoerce) # 去掉评分缺失的行 df df.dropna(subset[rating]) # 去掉完全重复的记录 df df.drop_duplicates(subset[user_id, novel_id], keeplast) # 把评分钳制在1到5之间 df[rating] df[rating].clip(1, 5) # 检查最终规模 print(df.shape) print(df[rating].describe())这里每个操作都有针对性。pd.to_numeric比直接astype(float)稳它会把无法转换的值变成NaN而不是直接抛异常errorscoerce就是指“遇到不认识的值就置空”。.drop_duplicates里的keeplast表示如果同一用户对同一本书评了两次保留最新的一条。.clip(1, 5)处理那些手滑填了0分或10分的脏数据。这一步做完用户ID和小说ID最好再做一次编码把字符串映射成连续整数。推荐算法和PyTorch里的Embedding都偏爱这种从0开始的整数索引尤其是后面要画热力图或做矩阵分解时连续索引能省掉一堆这种转换user_index {u: i for i, u in enumerate(sorted(df[user_id].unique()))} novel_index {n: i for i, n in enumerate(sorted(df[novel_id].unique()))} df[user_idx] df[user_id].map(user_index) df[novel_idx] df[novel_id].map(novel_index)2.3 构造用户-物品评分矩阵清洗完成后最核心的一步是把长表转成宽表。pandas的pivot_table在这里是主力import pandas as pd import numpy as np from scipy.sparse import csr_matrix # 长表转宽表 matrix df.pivot_table(indexuser_idx, columnsnovel_idx, valuesrating) print(矩阵形状:, matrix.shape) print(密度: {:.2%}.format(np.count_nonzero(matrix.values) / matrix.size)) # 转成CSR稀疏矩阵节省内存 sparse_matrix csr_matrix(matrix.fillna(0).values)matrix.shape如果打印出来是(200, 500)说明200个用户、500本小说都在矩阵里了。这里的密度是“非零元素的比例”——仿真数据每个人只读过不到30本密度通常低于5%。真实场景下这个数字可能低到0.1%以下所以后面所有相似度计算都要基于稀疏矩阵否则内存会先撑不住。有个细节容易被忽略pivot_table会自动把缺失值填成NaN直接用会报错所以要fillna(0)。但要注意填0在这里含义是“用户没读过这本书”不代表“用户打了0分”这个理解要在课程设计文档里单独写清楚。2.4 数据稀疏性与冷启动的处理边界矩阵建好以后第一个要面对的问题就是稀疏性。常用处理方案对比方案原理优点缺点适合场景直接补零用0替代缺失值简单直接引入大量虚假负样本现状代码演示够用均值填充用用户或物品的平均分补缺失保留部分偏好信息扭曲方差效果不稳定数据量大、初始状态SVD降维矩阵分解后重构稠密矩阵能发现潜在因子分解耗时解释性差离线定期训练矩阵分解ALS / SGD 学习用户与物品隐向量效果最好要调超参数数据量稍大的课程设计冷启动和数据清洗是两回事——数据清洗处理的是“脏数据”冷启动处理的是“没有数据”。新用户没有阅读记录协同过滤必然失效这时候基于内容的推荐第3.3节才有意义。课程设计里最简单的一种降级策略是没有评分记录的用户就返回全局热门小说列表。def cold_start_recommend(df, top_n10): 冷启动对没有行为记录的用户推荐全局热门小说 popular df[novel_id].value_counts().head(top_n).index.tolist() return popular这个函数放在API层调用当判断用户不在评分矩阵里时走这条路。3. 小说推荐的核心算法协同过滤与内容相似度3.1 基于用户的协同过滤用余弦相似度找“口味相似的人”基于用户的协同过滤UserCF的思路是找到和目标用户读过相同小说的人再把这些人读过但目标用户没读过的小说推荐出去。第一步是计算用户相似度矩阵。常用余弦相似度from sklearn.metrics.pairwise import cosine_similarity # 基于用户向量计算相似度shape: (user_count, user_count) user_sim_matrix cosine_similarity(sparse_matrix)sparse_matrix是上一节的CSR矩阵行是用户列是小说。cosine_similarity会把每一行当成一个向量计算两两夹角余弦。值越接近1说明两个用户的口味越像越接近0说明越不像。第二步是写推荐函数def recommend_by_user_cf(user_idx, user_sim_matrix, sparse_matrix, K10): 基于用户的协同过滤推荐 :param user_idx: 目标用户的索引 :param user_sim_matrix: 用户相似度矩阵 :param sparse_matrix: 用户-物品评分矩阵 :param K: 取前K个相似用户 # 获取当前用户的相似度向量 sim_scores user_sim_matrix[user_idx] # 按相似度排序排除自己取前K个 similar_users np.argsort(sim_scores)[::-1][1:K1] # 候选物品分 相似用户评分 * 相似度 的加权和 score np.zeros(sparse_matrix.shape[1]) for sim_user_idx in similar_users: sim_score sim_scores[sim_user_idx] user_ratings sparse_matrix[sim_user_idx].toarray().ravel() score sim_score * user_ratings # 排除当前用户已经看过的内容 watched sparse_matrix[user_idx].toarray().ravel() 0 # 推荐得分最高的前10本 score[watched] 0 recommended np.argsort(score)[::-1][:10] return recommended.tolist()这段代码的加权求和逻辑是关键不是简单地“朋友看了就给推荐”而是用相似度给每个人的评分加权。K参数的直观理解是“参考多少个邻居”K太小会过拟合K太大会把不相关的人拉进来一般课程设计取5到20之间调试。3.2 基于物品的协同过滤先算小说相似度再给用户推荐“类似读物”基于物品的协同过滤ItemCF在小说场景里比UserCF更实用理由是用户兴趣会变但小说之间的相似关系相对稳定而且ItemCF能提前把“相似小说表”算好在线推荐时延迟更低。物品相似度的计算同样用余弦相似度只不过这次对矩阵转置# 物品相似度矩阵shape: (novel_count, novel_count) item_sim_matrix cosine_similarity(sparse_matrix.T)注意这里.T转置了矩阵相当于把小说当样本、用户当特征。两本小说被同一批用户读过它们的向量就越接近相似度就越高。推荐的逻辑也和UserCF对称def recommend_by_item_cf(user_idx, item_sim_matrix, sparse_matrix, top_n10): 基于物品的协同过滤推荐 :param user_idx: 目标用户索引 :param item_sim_matrix: 物品相似度矩阵 :param sparse_matrix: 评分矩阵 :param top_n: 最终推荐数量 user_ratings sparse_matrix[user_idx].toarray().ravel() # 找出用户评分最高的3本书作为“种子” seed_items np.argsort(user_ratings)[::-1][:3] score np.zeros(item_sim_matrix.shape[0]) for seed in seed_items: sim_vector item_sim_matrix[seed] score user_ratings[seed] * sim_vector # 过滤掉已经看过的书 watched user_ratings 0 score[watched] 0 return np.argsort(score)[::-1][:top_n].tolist()这段代码里有个微妙之处种子书的选择会影响最终结果。如果用户什么都看过一点按评分最高的3本做种子推荐出来的反而太集中。一个改进是降低种子书权重或者干脆用全部有评分的书加权求和代价是计算量变大。课程设计阶段可以两种都实现然后在答辩时对比输出差异。3.3 基于内容的小说相似度用TF-IDF把小说变成向量协同过滤解决了“看到别人读过什么”但它绕不开冷启动和新书推荐的问题。基于内容推荐不需要评分记录它只看小说自身的属性——标题、简介、标签、章节名。常见做法是用jieba分词 TF-IDF向量化构建“小说画像”import jieba from sklearn.feature_extraction.text import TfidfVectorizer novel_docs [ # 这里假设每本小说有一段简介文本实际从数据文件读取 主角重生在玄幻大陆手握系统一路逆袭, 都市职场新人获得读心术看穿老板心思, 修仙废柴转世重修升级打怪一路碾压, ] def segment(text): 分词函数jieba默认精确模式 return .join(jieba.cut(text)) # 对每本小说简介做分词 segmented_docs [segment(doc) for doc in novel_docs] # TF-IDF向量化 tfidf TfidfVectorizer() doc_vectors tfidf.fit_transform(segmented_docs) # 计算小说间的余弦相似度 novel_sim cosine_similarity(doc_vectors)TfidfVectorizer输出的是稀疏矩阵novel_sim[i][j]就是第i本和第j本小说的内容相似度。这里分词必须提前做否则TF-IDF会把连续句子当成一个词条效果非常差。基于内容的推荐入口是“用户收藏了哪本小说”def recommend_by_content(novel_id, novel_sim, top_n10): 基于内容相似度的推荐 sim_scores novel_sim[novel_id] recommended np.argsort(sim_scores)[::-1][1:top_n1] return recommended.tolist()内容推荐的最大优点是可解释性强——给用户展示“因为你看过《XXX》所以推荐《YYY》”这句话在课程设计Demo里比协同过滤输出结果更有说服力。3.4 混合推荐把三种算法加权融合三种算法单独用都有短板课程设计如果想要更高的完整度可以在最后一层做加权融合。常见做法是先各自返回候选集和得分再归一化后加权求和def hybrid_recommend(user_idx, novel_id): 混合推荐协同过滤 内容推荐 热门兜底 result {} # 用户有历史行为才做协同过滤 if user_idx is not None: for nid in recommend_by_user_cf(user_idx, user_sim_matrix, sparse_matrix): result[nid] result.get(nid, 0) 0.4 # 基于内容的推荐 for nid in recommend_by_content(novel_id, novel_sim): result[nid] result.get(nid, 0) 0.3 # 热门小说保底 for nid in cold_start_recommend(df): result[nid] result.get(nid, 0) 0.1 return sorted(result.items(), keylambda x: x[1], reverseTrue)[:10]0.4、0.3、0.1这三个权重是可以调的。课程设计报告里如果能画一个“不同权重组合下的推荐效果对比”表格是很加分的——说明你不是只会写代码还做了实验。4. 用Flask把小说推荐系统发布成Web服务4.1 模型训练结果持久化推荐系统的离线训练和线上推荐是两套逻辑。课程设计通常的做法是训练时把相似度矩阵算好用pickle存到磁盘Flask启动时直接加载。这样用户请求进来不需要重新计算几百本小说之间的相似度。import pickle # 训练阶段保存 with open(model/user_sim.pkl, wb) as f: pickle.dump(user_sim_matrix, f) with open(model/item_sim.pkl, wb) as f: pickle.dump(item_sim_matrix, f) with open(model/novel_meta.pkl, wb) as f: pickle.dump({id_to_name: id_to_name, novel_sim: novel_sim}, f) # Flask启动时加载 with open(model/user_sim.pkl, rb) as f: user_sim_matrix_loaded pickle.load(f).pkl文件有个坑它依赖Python版本和依赖库版本在A机器保存的文件在B机器上可能加载失败。课程设计最后一次演示前务必在同一台机器上训练和启动或者改用np.save和np.load这种更稳的格式。4.2 设计推荐API的路由与返回格式Flask的接口设计不需要复杂两个路由就够一个是“给用户推荐”一个是“给小说找相似”。返回格式统一用JSON前端才好处理。from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/recommend/user/string:user_id, methods[GET]) def api_recommend_user(user_id): 根据用户ID推荐小说 k request.args.get(k, default10, typeint) user_idx user_index.get(user_id) if user_idx is None: # 冷启动返回全局热门 rec cold_start_recommend(df, k) return jsonify({user_id: user_id, recommendations: rec, strategy: hot}) rec recommend_by_item_cf(user_idx, item_sim_matrix, sparse_matrix, top_nk) return jsonify({user_id: user_id, recommendations: rec, strategy: item_cf}) app.route(/api/recommend/novel/string:novel_id, methods[GET]) def api_recommend_similar_novel(novel_id): 根据小说ID推荐相似小说 novel_idx novel_index.get(novel_id) if novel_idx is None: return jsonify({error: novel not found}), 404 rec recommend_by_content(novel_idx, novel_sim) return jsonify({novel_id: novel_id, similar_novels: rec}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)几个细节值得在注释里写明。request.args.get(k, default10, typeint)是在GET请求的URL参数里取数量例如/api/recommend/user/u001?k5。host0.0.0.0是让局域网内其他机器也能访问不是只绑本机回环地址。演示时如果只在自己电脑上跑host写默认的127.0.0.1也行。4.3 代码注释与异常兜底课程设计评审很看重一个行为代码里有没有覆盖“用户不存在”“小说不存在”“输入K值不合法”这三种情况。光有推荐逻辑远远不够接口层要主动拦掉异常输入。app.errorhandler(404) def not_found(e): return jsonify({error: resource not found}), 404 app.errorhandler(500) def internal_error(e): return jsonify({error: internal server error}), 500加上这两段以后即使推荐函数内部抛了异常前端拿到的也是一个结构化的JSON错误而不是一坨黑白的traceback页面。课程设计文档里可以把这解释成“面向用户的服务端错误处理”。4.4 性能优化把重复计算结果放到内存一个常见的错误写法是每次请求进来都重新计算一遍相似度。当数据量到几百本小说时可能还看不出来一旦评分矩阵扩大到几千行Flask会明显卡顿。常见的优化手段是启动时预计算一次后续请求只做矩阵查表。另一个更细的优化是用局部变量引用全局对象减少字典查找# 启动时加载模型 with open(model/user_sim.pkl, rb) as f: user_sim_matrix pickle.load(f) # 压测时能快些但代码可读性略差 # 不推荐在课程设计里过度优化课程设计阶段不建议引入Redis或消息队列。把服务做成内存加载、直接查询已经是超过大部分同组作业的水平。5. 课程设计答辩时的推荐效果验证与参数调优5.1 离线评估准确率与召回率推荐系统不能只用“看起来像不像”来证明自己。常见做法是把评分矩阵按8:2切成训练集和测试集再用召回率、准确率做量化对比。from sklearn.model_selection import train_test_split from sklearn.metrics import precision_score, recall_score # 把每个用户读过的书拆成训练集和测试集 def split_by_user(df, test_ratio0.2): train_list, test_list [], [] for uid, group in df.groupby(user_idx): test_idx np.random.choice(group.index, sizeint(len(group) * test_ratio), replaceFalse) test_list.append(group.loc[test_idx]) train_list.append(group.drop(indextest_idx)) return pd.concat(train_list), pd.concat(test_list) train_df, test_df split_by_user(df) # 对每个测试用户生成推荐检查命中情况 hits 0 total_rec 0 for uid, group in test_df.groupby(user_idx): recs recommend_by_item_cf(uid, item_sim_matrix_train, sparse_matrix_train, top_n10) hits len(set(recs) set(group[novel_idx])) total_rec 10 print(precision{10}:, hits / total_rec) print(recall: {:.4f}.format(hits / len(test_df)))注意这里必须用训练集生成的相似度矩阵去推荐不能用全量数据否则结果会虚高到看起来“完美”但答辩时一追问就露馅。5.2 参数对效果的敏感度K值、种子书数量参数推荐值调高影响调低影响K相似用户数/物品数10 - 20更平滑但可能引入噪音更精准但覆盖窄种子书数量3 - 5推荐更多样但偏离主偏好个性化强但结果太集中评分阈值4.0只看高分项多样性降低参考更多行为噪音变多加权权重0.4/0.3/0.3按场景调无绝对最优同上一个实用技巧是每调一个参数跑一次评估函数记录结果到表格里。答辩时拿出“K5的时候准确率是0.21K20时降到0.17”这类现象比“我们的系统效果很好”有力得多。5.3 提升答辩完成度的3个细节一是可视化。用matplotlib画出用户评分分布直方图、相似度热力图甚至只是推荐结果的柱状图都能让评分老师觉得工作量饱满。import matplotlib.pyplot as plt df[rating].value_counts().sort_index().plot(kindbar) plt.title(用户评分分布) plt.xlabel(评分) plt.ylabel(次数) plt.savefig(report/rating_distribution.png, dpi150)二是README。把启动步骤写清楚pip install pandas scikit-learn flask jieba然后先跑train.py再跑app.py。不要默认评审老师会看代码能一条命令跑起来才是交付。三是日志。Flask里加一个简单的logging.info记录每次请求的用户ID和返回条数演示时打开终端窗口评审能看到系统实时输出体验会生动很多。推荐系统的边界远不止课程设计里这些内容矩阵分解、图神经网络、大模型做语义推荐都在往前走但评价一个系统是否可靠的标准没有变——数据质量、评估指标、可解释性这三样东西永远比花哨模型更值钱。本文还有配套的精品资源点击获取
返回列表