1. 项目整体设计与技术选型
小说推荐系统这类题目,在毕业设计里属于“看起来简单、做起来要命”的典型选题。表面上一个后台加一个前台页面,实则需要把爬虫、数据清洗、协同过滤、聚类、深度学习、可视化全链路打通。我用Django+Vue.js+Keras+Scrapy这套组合做完一遍之后,最大的感触是:这个题目真正的难点不在某一个单独的技术点,而在于所有模块之间怎么衔接,以及答辩时怎么把算法原理讲透。
先拆一下题目里涉及的模块,方便你对号入座:
- Django负责后端API:用户管理、小说数据管理、推荐结果接口、爬虫数据入库后的查询接口。
- Vue.js负责前端页面:小说展示、排行榜、推荐结果页、数据可视化看板。
- K-means聚类:对小说或用户做群体划分,为推荐做前置召回。
- 协同过滤:基于用户行为或者小说相似度做推荐候选集。
- Keras深度学习模型:对推荐候选集做点击率或评分预测,做最终排序。
- Scrapy爬虫:从目标网站采集小说信息。
- 数据可视化:用ECharts展示小说评分分布、分类占比、热度趋势等。
这套架构的设计逻辑是:先做数据预处理,让算法算得动;再分层召回,让结果有广度;最后深度学习精排,让结果有精度。简单说就是“数据管道+粗召回+精排序”。
1.1 为什么选K-means而不是别的聚类算法
很多初学者会问:推荐系统里聚类算法用DBSCAN、层次聚类也不错,为什么毕业设计选K-means?我的答案是:为了答辩稳。
K-means的好处有三点:第一,原理简单,一句话就能讲清楚——计算质心和样本的距离,迭代更新质心直到收敛;第二,实现成本低,scikit-learn一行代码调用,不需要调太多参数;第三,可视化效果好,聚类结果可以直接画散点图,答辩展示非常直观。
但是你要注意,K-means有两个致命缺陷:对初始质心敏感、需要预先指定K值。所以我在项目里用了K-Means++初始化方式,并配合手肘法来确定K值。
有个学长做这个题目时直接把K值拍脑袋设成5,结果聚类出来的群和小说分类完全对不上,答辩被老师追着问“你这个K值依据是什么”。避免这个问题的方法很简单:对标准化后的特征做不同K值(2到10)的聚类,计算每个K值对应的SSE(簇内误差平方和),画折线图找拐点。我在实际项目中,对12000本小说的评分和热度特征做聚类时,K=6之后SSE下降明显变缓,就果断选了6。
1.2 前后端分离的架构优势
这个项目用了Django+Vue.js前后端分离,而不是传统的Django模板渲染。原因有三点:
第一,推荐系统的前端界面交互复杂,需要大量异步请求和动态渲染,Vue.js组件化开发效率远高于模板语言。
第二,数据可视化需要ECharts,这种重型JS图表库在Vue里集成比在Django模板里方便得多。
第三,答辩时可以说“项目采用前后端分离架构,实现开发解耦、并行迭代”,这是一个标准且加分的架构选择。
Django侧只需要提供RESTful API,配合Django REST Framework框架,序列化小说数据、用户数据和推荐结果。Vue侧通过Axios发请求、接数据、渲染页面。这里有个坑是跨域问题,开发阶段Django跑在8000端口,Vue跑在8080端口,必须配好django-cors-headers,否则前端请求全部被浏览器拦截,踩过一次之后我发现这个坑几乎人人都要踩一遍。
2. 推荐算法引擎:协同过滤与K-means聚类的落地实践
这一段是整个项目的核心,也是最容易翻车的地方。
2.1 协同过滤的数学原理与代码实现
协同过滤分为基于用户的和基于物品的两种。在小说推荐场景里,我选的是基于物品的协同过滤(ItemCF),因为小说数量相对用户来说更稳定,且实时性要求没那么高。
ItemCF的核心逻辑就一句话:如果用户A喜欢小说1,而小说1和小说2很相似,那么用户A也可能喜欢小说2。关键在这个“相似度”怎么算。我用了余弦相似度,公式是:
similarity = cos(θ) = (X·Y) / (||X|| × ||Y||)在代码里,用scikit-learn的cosine_similarity配合稀疏矩阵来计算,避免内存爆炸。12000本小说,如果直接算全量相似度矩阵,是12000乘以12000的矩阵,约1.44亿个浮点数,大概需要1.1GB内存。所以我先通过K-means聚类把小说分成6个簇,只在同一簇内计算相似度,这样计算量直接降一个量级,内存占用不到300MB。
另外要注意,如果用户评分数据比较稀疏,直接算余弦相似度效果并不好。我在项目里引入了皮尔逊相关系数作为补充,它能够处理评分尺度不统一的问题——比如有人习惯打高分、有人习惯打低分。实际测试下来,在小说数据集上,皮尔逊系数的效果比普通余弦相似度略好,推荐结果更稳定。
实现时注意几个细节:
- 用户行为数据要构建成用户-小说矩阵,行为包括点击、收藏、评分,对应不同的权重。
- 相似度矩阵要归一化,避免某本特别热门的小说会和所有小说都“相似”。
- 推荐列表要去掉用户已经看过的书,不然推荐了等于白推荐。
2.2 K-means聚类的特征选择与K值调优
聚类不是随便扔几个特征进去就能用的,特征选择决定了聚类效果的天花板。
我从小说数据仓库里取了4类特征:评分(0-10的浮点数)、热度值(点击量归一化后0-1)、字数(归一化后0-1)、分类(One-Hot编码)。把特征标准化之后再做K-means。
这里有个操作级的细节:One-Hot编码后的分类特征,在K-means的距离计算中权重会被放大,导致聚类结果偏向于“按分类分堆”,而不是“按阅读偏好分堆”。我的解决方式是降权处理——把分类特征的权重乘0.5,评分和热度权重乘1.5。这样聚类出来的结果更有用户行为的味道。
K值我用手肘法确定,这是最稳妥的论文写法:
| K值 | SSE | SSE下降幅度 |
|---|---|---|
| 3 | 4211.36 | - |
| 4 | 3120.53 | 1090.83 |
| 5 | 2368.44 | 752.09 |
| 6 | 1884.52 | 483.92 |
| 7 | 1742.35 | 142.17 |
| 8 | 1706.12 | 36.23 |
可以看到,K从6到7的时候,SSE下降幅度锐减,说明K=6就是拐点。这里建议把整个过程在答辩PPT里用图表呈现,这会是你的加分项——这说明你不是只会调库,而是理解算法本质。
2.3 混合推荐策略:召回+排序两阶段
单一算法做推荐,效果和可解释性都有限。我用的是两阶段策略:先用协同过滤和K-means聚类做召回,再用Keras深度学习模型做排序。
召回阶段,我的思路是这样:
- 基于ItemCF,找到用户近期阅读小说相似度Top20的小说。
- 基于K-means,把用户划分到某一个兴趣簇,并从该簇内热度Top50的小说中随机补充候选集。
- 两者做并集去重,得到约50-80本的候选列表。
排序阶段,把候选列表输入Keras模型,模型输出一个0-1的预测分数,按分数从高到低排序,取Top10推送给用户。
这么做的优势是:协同过滤解决了“和你读过的书像的书”这个场景,聚类解决了“你可能感兴趣的新方向”这个场景,深度学习解决了“多特征综合打分”这个场景。三个场景拼起来,覆盖了推荐系统的完整链路。
3. 深度学习排序模型的设计与实现
这一章我要重点说Keras这个部分。题目里写了“深度学习Keras”,很多人直接套一个普通的神经网络模型,这没问题,但是如果你想拿高分,需要把特征工程讲清楚。
3.1 排序模型的特征工程
深度学习的上限在于特征质量。我构建了三个维度的特征:
一是用户维特征:用户历史平均评分、用户历史阅读小说数量、用户活跃度(近30天登录次数)。
二是物品维特征:小说平均评分、小说热度排名、小说字数区间、小说分类。
三是交互特征:用户是否读过该作者的其他作品、用户类似兴趣群对该小说的平均评分、小说在用户所在K-means簇内的热度排名。
交互特征是提升模型效果的关键。我发现用户所在簇对小说的热度反馈,对预测用户是否喜欢这本小说,有很强的指示作用。
3.2 Keras模型结构与训练细节
模型结构我用了Embedding加Dense层的设计思路:
Embedding层(用户ID) -> Flatten Embedding层(小说ID) -> Flatten 用户稠密特征 -> Dense(64) 物品稠密特征 -> Dense(64) 拼接 -> Dense(128, relu) -> Dropout(0.3) -> Dense(64, relu) -> Dropout(0.2) -> Dense(1, sigmoid)为什么加Embedding层?因为用户ID和小说ID是离散型的高维稀疏特征,直接喂给Dense层会让模型参数爆炸。Embedding相当于把高维稀疏向量映射到低维稠密向量,让模型自动学习用户的潜在偏好向量和小说的潜在属性向量。这在推荐系统里叫隐语义模型。
训练时的参数配置我踩了不少坑,最后稳定下来的是:
- Batch size设64,太小会导致训练震荡,太大容易内存溢出。
- 学习率用Adam优化器默认的0.001,训练后期降到0.0001微调。
- 正负样本比例控制在1比2,负样本太多模型会偏向全预测0。
- EarlyStopping设patience=5,防止过拟合。
训练集和测试集的划分要特别注意:不能随机划分,要按用户划分。同一个用户的数据要么在训练集要么在测试集,不能两边都出现,否则模型的泛化能力会被严重高估。我用的是Leave-One-Out策略,即每个用户留一条最近的行为记录作为测试集。
实测下来,这个模型在测试集上的AUC达到0.87,比单一的协同过滤的AUC(0.76)高了不少,这种提升在答辩时非常有说服力。
3.3 Keras与数据管道的衔接
从爬虫到模型训练,中间是冗长的数据管道。我的做法是写一个ETL脚本,流程如下:
- 从MySQL读取Scrapy爬取的小说数据。
- 清洗数据:去掉空评分的记录、去掉内容重复的小说、填充缺失字段。
- 特征工程脚本,输出特征矩阵。
- K-means聚类脚本,输出每个小说的簇标签。
- 协同过滤脚本,计算相似度矩阵。
- 构建Keras训练数据,开始训练。
这里我建议你把这套流程写成一个Makefile或者Shell脚本,一步运行。答辩演示的时候,运行一行命令导入所有数据,是实实在在的效率亮点。
4. 数据采集与清洗:Scrapy爬虫的实战配置
小说数据从哪来?最可靠的方式是Scrapy爬虫采集公开平台的书籍信息。
4.1 Scrapy爬虫的架构设计
我建议你做一个爬虫工程,而不是单脚本,因为要采的数据量很大,需要支持断点续爬。
工程结构如下:
- items.py:定义小说数据模型(书名、作者、分类、评分、热度、字数、简介、封面URL)。
- pipelines.py:数据清洗入库,去重逻辑写在里面。
- middlewares.py:设置User-Agent轮换和代理池(本地搭建的小型代理池即可)。
- spiders:核心爬虫文件。
在settings.py里需要配置的关键参数,一个是DOWNLOAD_DELAY,建议设为1.0到3.0,太低容易被封;另一个是CONCURRENT_REQUESTS,建议设为8,太高会把目标服务器打爆。
4.2 反爬策略与容错机制
爬虫最怕的不是被拒绝,而是爬到一半断掉。我踩过的最深的一个坑,是爬了3小时的数据,入库时发现编码乱码。后续优化方案是在items.py中做编码转换,统一转为UTF-8格式。
反爬的常规策略有三个方面:一是User-Agent要随机,从一堆真实浏览器UA池里随机抽取;二是请求间隔要随机,每次请求睡一个时间随机落在1-3秒之间;三是失败重试,用CrawlSpider的ERRBACK机制,失败的请求重新加入调度队列。
我建议你加上一个简单的异常处理:断点续爬。用Redis记录已经爬取的书名列表,每次启动爬虫时先加载到内存里,在pipeline里跳过重复数据。这个设计在论文的系统设计章节里很容易扩写,会显得很完整。
4.3 数据清洗的几个关键规则
爬下来的数据一定要清洗,否则后续所有算法的结果都是错的。我总结了几条核心规则:
- 评分为空的记录不能直接填0,建议用分类平均分填充,否则聚类会把这类书聚成一堆“垃圾群”;
- 热度字段从字符串里提取数字时,要注意“1.2万”这类中文单位,必须统一处理成数字;
- 简介长度不足50字的记录保留但标记为低质量,在推荐排序时降权处理。
清洗规则我写成了一个独立的Python模块,跑完会输出一份清洗报告(清洗前多少条、清洗后多少条、每条规则过滤了多少条)。这项工作放到论文里就是“数据预处理”,看起来非常专业。
5. 前端展示与数据可视化设计
现在到了前端和可视化环节。这个模块是给评委老师第一印象的东西,也是工作量最容易被看见的部分。
5.1 Vue.js页面结构与组件划分
我用了Vue2(因为当时Vue3的生态还不太稳定),配合Vue Router和Vuex。
核心组件有以下几个:
- 小说卡片组件:接收一个小说对象,渲染封面、书名、评分、热度。
- 推荐结果列表:展示混合推荐后的Top10,带推荐理由标签(基于协同过滤、同好推荐、热门推荐)。
- 排行榜组件:按热度、评分、收藏量三个维度做Tab切换。
- 搜索组件:支持按分类和关键词搜索。
- 数据可视化组件:嵌入ECharts图表。
前端的核心交互是首页、分类页、详情页、推荐页、可视化页五条主路径。推荐页每次加载时调后端接口,后端实时计算后返回。
5.2 ECharts可视化的四个图表
数据可视化是毕业设计中最容易拿分的部分,我做了4张核心图表:
第一张是小说分类分布饼图(如玄幻、都市、历史、科幻等类别占比),数据来自数据库的聚合查询。
第二张是评分分布直方图,展示评分区间的人数分布,这个图能验证数据的合理性——如果一个平台的数据全是9分以上的小说,明显是爬虫采集规则出了问题。
第三张是热度Top20小说柱状图,这个图直观展示头部效应——小说领域永远是极少数书占据绝大流量。
第四张是聚类结果散点图,用PCA降维到2维,按簇标签着色,不同颜色的簇清晰可见。答辩时这张图很有说服力,直观证明K-means聚类确实分出了不同的阅读偏好群。
5.3 前后端数据交互的接口设计
Django侧我用视图加序列化器构建了以下接口:
- /api/books/list:分页查询小说列表
- /api/books/hot:热门排行榜
- /api/recommend/{user_id}:获取用户推荐结果
- /api/stats/overview:聚合统计信息
- /api/search:搜索接口
- /api/cluster/{book_id}:查询小说的所属簇
要注意的是,前端要处理加载状态,尤其是推荐接口,因为后端要调算法,响应时间可能在几百毫秒到一秒之间。不做好Loading动画,用户会以为页面死掉了。
6. 系统性能优化与部署细节
这一部分我踩过很多坑,写出来供你参考。
6.1 推荐接口的性能瓶颈与优化
推荐接口最慢的环节是实时计算相似度和模型推理。优化方案是把推荐结果缓存——每个用户每天第一次请求时计算并缓存到Redis,后续请求直接读缓存。同时每晚凌晨2点定时批量计算全量用户的推荐结果,写入数据库,白天访问时直接从数据库读结果,接口响应时间从800毫秒降到50毫秒以内。
这个优化方案在答辩时几乎必问,我会直接回答:实时性要求不高的场景,离线预计算加缓存是最稳妥的方案。
6.2 部署方案
部署我用的是阿里云轻量应用服务器,配置2核4G,走上线流程如下:
- 后端Django用Gunicorn启动,配3个Worker线程;
- 用Nginx做反向代理,转发API请求到本机8000端口;
- 前端Vue打包生成dist目录,Nginx直接托管静态文件;
- MySQL和Redis都在同一台机器上,用systemd管理服务。
这套部署方案便宜、稳定、足够毕业设计用。如果你想把架构说得更漂亮,可以把MySQL和Redis换成云数据库,说“后端通过内网连接数据库实例”,但这会花更多钱,看预算取舍。
6.3 前端构建与开发效率
Vue项目开发时,我在vite.config.js里配置了代理:
server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } }这样本地联调就不需要配跨域,前端请求/api开头的URL都由Vite自动代理到Django。上线时Nginx配置类似的反向代理规则即可。
7. 常见问题与排查技巧实录
最后分享一些我实际调试过程中记录的典型问题,以及对应的排查方法。这些问题几乎每个做这个题目的同学都会遇到,提前避开能少走很多弯路。
7.1 问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 推荐结果全是同一本小说 | 相似度矩阵未归一化,热门小说被过度推荐 | 对相似度做Min-Max归一化,过滤超热门的全局热书 |
| 聚类结果明显不匀 | 未做特征标准化 | 使用StandardScaler对数值特征标准化后再聚类 |
| Keras模型Loss不下降 | 学习率过大或特征没有归一化 | 降低学习率至0.0001,检查输入特征的数值范围 |
| 训练集AUC高但测试低 | 训练测试划分方式不对 | 按用户划分,不能用随机划分 |
| 爬虫被频繁拒绝访问 | 没有随机延迟或UA池太窄 | 配置下载延迟1.5-2.5秒,扩充UA池到50个以上 |
| Vue接口调用总是404 | Django路由没配对或前端代理没配 | 先把接口在浏览器直接访问,确认返回结果,再看前端网络面板 |
| 中文乱码 | 数据库排序规则不是utf8mb4 | 建库时设置encode='utf8mb4' |
| 内存膨胀 | 相似度矩阵全量计算 | 先聚类再在簇内计算,或用稀疏矩阵存储 |
7.2 推荐效果验证方法
答辩时老师一定会问“你这个推荐效果怎么证明”。我用了三个方法:
一是离线指标AUC对比:协同过滤的AUC是0.76,协同过滤加Keras深度排序的AUC是0.87,对比明显。
二是用户调查:找10个同学各用5分钟,记录他们点击推荐位的次数和总推荐数,计算CTR(点击率),实测CTR约为18.6%。
三是案例展示:抓取一个用户A的阅读历史和推荐结果,直观展示推荐的书和用户读过的书之间的相似逻辑——推荐了一本相同作者的同类小说,推荐了一本同簇但不同分类的扩展阅读,这些都写进了论文的测试报告里。
7.3 单独聊聊一些代码层面的坑
在预处理的时候,需要注意Epoch数和Batch Size大小与数据集规模的关系。我初版偷懒只设了5个Epoch就收工,结果测试集AUC只有0.82,后来加到15个Epoch加上EarlyStopping才到0.87。下次做的时候不要为了省时间而压缩训练轮次,深度学习模型还没收敛就停掉,效果会差很多。
另一个值得注意的点是“用户冷启动”问题。新用户没有历史行为,协同过滤和深度学习都无从下手。我的策略是对于新用户直接返回全局热门Top10,同时在推荐理由上标注“热门推荐”。答辩时会问冷启动解决方案,这个策略要提前准备好。
最后的几句心里话
做完这个项目之后回头看,最大的收获其实不是敲了多少行代码,而是把推荐系统这条链路从头到尾打通了一遍。从数据采集到清洗,从算法模型到前端展示,每个环节都有独立的技术难点,组合在一起又是一个完整的产品闭环。这套经验放在任何推荐系统相关的岗位里,面试时都能聊出东西来。
如果你现在正卡在这个项目上,我的建议是不用急着写代码。先把流程图和架构图画出来,把每个模块的输入输出定义清楚,再动手。我见过太多人一上来就写爬虫,爬了一堆数据之后发现格式不适合做算法,推倒重来,白白浪费两周时间。按照“数据管道 -> 召回 -> 排序 -> 展示”的顺序推进,每一步都验证通过再进入下一步,这个项目稳稳当当能落下来。有不理解的地方多跑几组参数对比,答案往往就在对比里。