☰
基于知识图谱的Flask智能推荐系统源码解析与实战
2026/10/7 16:35:33 网站建设 项目流程

简介:本资源为基于知识图谱的智能推荐系统毕业设计完整源码包,面向计算机相关专业学生及需要课程设计、毕业设计参考的开发者。项目以Python 3.6.8与Flask框架搭建后端,MySQL 5.7存储数据,采用B/S架构,支持通过歌名、电影名或书名检索并推荐相关信息,并融入深度学习扩展内容应用,适合作为推荐系统与知识图谱方向的实战学习案例。压缩包共388个文件,约297.51MB,包含20个py源码、15个html页面、50个js与32个css前端资源、18个pkl模型文件、1个sql数据库脚本,以及说明文档、设计文档和知识图谱数据文件,覆盖前后端与数据层完整结构。目前已有283人学习下载。读者可获得可运行源码、数据库文件、项目说明文档与开发文档,便于快速理解系统架构、复现推荐流程并在此基础上进行二次开发与功能扩展。

1. 从一份 Flask 知识图谱推荐源码说起:它到底解决了什么问题

很多同学做毕业设计时,一提到“推荐系统”第一反应就是协同过滤,用 MovieLens 数据集跑个皮尔逊相似度,最后算个 RMSE 就交差。但真正拿到一份基于知识图谱的智能推荐系统源码时,你会发现它的思路完全不同:它不只看“用户和用户像不像”,而是先构建一张包含用户、物品、属性、关系的图谱,再沿着关系路径去推理“为什么给你推这个”。这套 Flask + MySQL 的方案,核心价值在于把推荐结果从“黑匣子打分”变成“可解释的路径”。它适合两类人:一是毕设选题想做点差异化、不想再堆协同过滤的同学;二是想搞明白知识图谱怎么从 Neo4j 或内存图结构落到 Web 系统里的开发者。你拿到源码后,最该先跑通的是数据层和推荐接口,而不是急着改前端样式。

2. 知识图谱推荐系统的骨架:从三元组到 Flask 接口

2.1 为什么用知识图谱做推荐,而不是纯协同过滤

协同过滤的硬伤是冷启动和稀疏性。一个新用户没有历史行为,系统就废了;一个物品只有两三个人买过,相似度矩阵全是零。知识图谱的思路是引入侧信息:用户注册时填的标签、物品的类别、物品之间的替代关系,这些都可以作为图谱里的边。推荐时,系统从用户节点出发,沿着“用户-兴趣-物品-属性-相似物品”这样的路径游走,把路径末端的物品作为候选。这样做的好处是,即使某个用户没有行为数据,只要他填了兴趣标签,图谱里就有路径可走。

常见做法是构建一个异构图,节点类型包括 User、Item、Category、Tag,边类型包括 INTEREST、BELONG、SIMILAR、VIEW。存储上,毕设项目为了降低部署门槛,通常不会强制上 Neo4j,而是用 MySQL 的关系表模拟图结构:一张节点表、一张边表,查询时用多表 JOIN 或者递归 CTE 来模拟路径搜索。这样做性能不如原生图数据库,但胜在 MySQL 安装配置教程满地都是,答辩时老师也能看懂。

2.2 用 MySQL 关系表模拟图谱的建表语句

下面这套表结构是我在多个 Flask 毕设里反复用过的,字段不多,但足够支撑路径查询。注意kg_edge表里的weight字段,它决定了推荐时路径的优先级。

-- 节点表:统一存储用户、物品、分类、标签 CREATE TABLE kg_node ( node_id INT PRIMARY KEY AUTO_INCREMENT, node_type VARCHAR(20) NOT NULL COMMENT 'user/item/category/tag', node_name VARCHAR(100) NOT NULL, extra JSON COMMENT '存放物品描述、用户画像等扩展字段', INDEX idx_type (node_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 边表:存储节点之间的关系 CREATE TABLE kg_edge ( edge_id INT PRIMARY KEY AUTO_INCREMENT, source_id INT NOT NULL, target_id INT NOT NULL, relation VARCHAR(30) NOT NULL COMMENT 'interest/belong/similar/view', weight FLOAT DEFAULT 1.0 COMMENT '关系权重,用于推荐排序', INDEX idx_source (source_id), INDEX idx_target (target_id), INDEX idx_relation (relation) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户行为日志表,用于动态更新边权重 CREATE TABLE user_action ( action_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, item_id INT NOT NULL, action_type VARCHAR(20) COMMENT 'view/collect/buy', action_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表逻辑说明:kg_node用node_type区分实体类型,避免为每种实体单独建表导致 JOIN 爆炸。kg_edge的relation字段是推荐路径的核心,比如interest表示用户对标签的兴趣,belong表示物品属于某分类,similar表示物品间相似。weight默认 1.0,后续可以根据用户行为次数动态调整。user_action表不直接参与图谱查询,但用来离线更新similar边的权重。参数上,VARCHAR(30)对 relation 足够,如果你的关系类型超过 10 种,建议改成枚举或单独的关系字典表。

2.3 Flask 后端推荐接口的最小实现

推荐接口的核心逻辑是:给定用户 ID,先找到他的兴趣标签,再通过标签找到关联物品,最后按路径权重排序。下面这段代码放在app/recommend.py里,依赖 Flask 和 SQLAlchemy。

from flask import Blueprint, request, jsonify from sqlalchemy import text from app import db recommend_bp = Blueprint('recommend', __name__) @recommend_bp.route('/api/recommend/<int:user_id>') def recommend(user_id): # 第一步:查用户直接关联的兴趣标签 tag_sql = text(""" SELECT target_id FROM kg_edge WHERE source_id = :uid AND relation = 'interest' """) tags = db.session.execute(tag_sql, {'uid': user_id}).fetchall() tag_ids = [t[0] for t in tags] if not tag_ids: # 冷启动:返回热度最高的物品 hot_sql = text(""" SELECT node_id FROM kg_node WHERE node_type = 'item' ORDER BY node_id DESC LIMIT 10 """) hot_items = db.session.execute(hot_sql).fetchall() return jsonify({'user_id': user_id, 'items': [i[0] for i in hot_items], 'reason': 'hot_fallback'}) # 第二步:通过标签找物品,按边权重累加排序 placeholders = ','.join([str(t) for t in tag_ids]) item_sql = text(f""" SELECT e2.target_id, SUM(e1.weight * e2.weight) AS score FROM kg_edge e1 JOIN kg_edge e2 ON e1.target_id = e2.source_id WHERE e1.source_id IN ({placeholders}) AND e1.relation = 'interest' AND e2.relation = 'belong' GROUP BY e2.target_id ORDER BY score DESC LIMIT 10 """) items = db.session.execute(item_sql).fetchall() result = [{'item_id': row[0], 'score': float(row[1])} for row in items] return jsonify({'user_id': user_id, 'items': result, 'reason': 'kg_path'})

逻辑说明:先查interest边拿到用户兴趣标签,如果为空则走热度兜底,避免新用户看到空白页。有标签时,用两次 JOIN 模拟“用户→标签→物品”的两跳路径,SUM(e1.weight * e2.weight)是路径得分,权重相乘表示路径越强得分越高。参数上,LIMIT 10控制返回数量,毕设演示够用;如果要做分页,把 LIMIT 换成 OFFSET。注意placeholders直接拼接有 SQL 注入风险,生产环境要用参数化查询,但毕设本地跑问题不大,答辩时如果老师问起,你可以说“后续会换成绑定变量”。

3. 把源码跑起来:环境、数据库和前后端联调

3.1 Python 3.8 环境与依赖安装的避坑顺序

拿到源码后,第一步不是急着pip install -r requirements.txt,而是先确认 Python 版本。很多 Flask 毕设代码写于 2020 年前后,用的还是 Flask 1.x 和 SQLAlchemy 1.3,如果你本地是 Python 3.11,某些依赖会编译失败。我一般会建一个 3.8 的虚拟环境,命令如下:

# 创建虚拟环境,指定 Python 3.8 python3.8 -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 先升级 pip,避免旧版 pip 解析依赖出错 pip install --upgrade pip # 再装依赖,如果 requirements.txt 里有版本冲突,逐个装 pip install flask==1.1.4 pip install flask-sqlalchemy==2.5.1 pip install pymysql==1.0.2 pip install cryptography==3.4.8

参数说明:flask-sqlalchemy2.5.1 对应 Flask 1.x,如果你装了 Flask 2.x,接口会报_app_ctx_stack错误。pymysql是 MySQL 驱动,cryptography是 pymysql 连接 MySQL 8.0 时需要的加密库,不装会报RuntimeError: cryptography is required。这一步的坑在于,有些 requirements.txt 里写的是mysqlclient,它在 Windows 上需要 Visual C++ 构建工具,新手很容易卡住,换成pymysql并在配置里改SQLALCHEMY_DATABASE_URI为mysql+pymysql://即可。

3.2 MySQL 8.0 建库与导入初始数据

MySQL 安装教程网上很多,这里只说和本项目相关的配置。建库时字符集必须用utf8mb4,否则中文标签会乱码。

CREATE DATABASE kg_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE kg_recommend; -- 创建专用用户,避免用 root 跑应用 CREATE USER 'kg_user'@'localhost' IDENTIFIED BY 'Kg@123456'; GRANT ALL PRIVILEGES ON kg_recommend.* TO 'kg_user'@'localhost'; FLUSH PRIVILEGES;

然后在 Flask 的config.py里配置连接串:

SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://kg_user:Kg%40123456@localhost:3306/kg_recommend?charset=utf8mb4' SQLALCHEMY_TRACK_MODIFICATIONS = False

注意密码里的@要转义成%40,否则连接串解析会出错。这是血泪经验,很多人卡在这里一下午。导入初始数据时,如果源码提供了.sql文件,用source命令导入;如果没有,就根据第 2 章的建表语句自己插几条测试数据,至少保证kg_node里有 5 个用户、20 个物品、10 个标签,kg_edge里有对应的 interest 和 belong 边。

3.3 前后端联调:Flask 模板渲染与接口分离

这套源码的前端通常是 Jinja2 模板加 Bootstrap,也有部分用 Vue 做前后端分离。如果是模板渲染,启动 Flask 后直接访问http://127.0.0.1:5000就能看到页面;如果是分离的,前端一般在static或单独的frontend目录,需要用npm run serve启动。我一般会先跑后端,用 curl 测接口:

curl http://127.0.0.1:5000/api/recommend/1

如果返回 JSON 且 items 不为空,说明图谱查询通了。如果返回 500,先看 Flask 控制台的报错,最常见的是数据库连接失败或表不存在。前端联调时,跨域问题用flask-cors解决:

from flask_cors import CORS CORS(app, resources={r"/api/*": {"origins": "*"}})

参数说明:origins在开发阶段可以写*,上线要改成具体域名。如果前端请求一直 pending,检查 Flask 是否监听了0.0.0.0,默认127.0.0.1只能本机访问。

4. 推荐效果调优:权重、路径长度和冷启动兜底

4.1 边权重怎么设:从行为日志反推

第 2 章的weight默认是 1.0,但实际推荐时,用户“购买”比“浏览”的权重要高。常见做法是离线跑一个脚本,根据user_action表更新similar边的权重。比如两个物品被同一用户连续浏览,它们之间的相似度加 0.1;被购买则加 0.5。

# update_weight.py from app import db from sqlalchemy import text def update_similar_weight(): # 找出被同一用户交互过的物品对 sql = text(""" SELECT a.item_id, b.item_id, COUNT(*) AS cnt FROM user_action a JOIN user_action b ON a.user_id = b.user_id AND a.item_id < b.item_id GROUP BY a.item_id, b.item_id HAVING cnt >= 2 """) pairs = db.session.execute(sql).fetchall() for item_a, item_b, cnt in pairs: weight = min(1.0, cnt * 0.1) # 封顶 1.0 # 更新或插入 similar 边 upsert = text(""" INSERT INTO kg_edge (source_id, target_id, relation, weight) VALUES (:a, :b, 'similar', :w) ON DUPLICATE KEY UPDATE weight = :w """) db.session.execute(upsert, {'a': item_a, 'b': item_b, 'w': weight}) db.session.commit()

逻辑说明:HAVING cnt >= 2过滤掉偶然共现,min(1.0, cnt * 0.1)防止权重无限增长。参数上,0.1 是步长,你可以根据数据量调整,数据少就调大,数据多就调小。这个脚本建议每天凌晨跑一次,不要放在请求里实时算。

4.2 路径长度控制在 2 到 3 跳

知识图谱推荐的一个常见误区是路径越长越好。实际上,超过 3 跳后,路径得分会指数衰减,而且查询性能急剧下降。我一般把路径限制在 2 跳(用户→标签→物品)或 3 跳(用户→标签→物品→相似物品)。在 SQL 里就是多一层 JOIN,但要注意加LIMIT防止中间结果爆炸。

-- 3 跳查询示例:用户兴趣标签 -> 物品 -> 相似物品 SELECT e3.target_id, SUM(e1.weight * e2.weight * e3.weight) AS score FROM kg_edge e1 JOIN kg_edge e2 ON e1.target_id = e2.source_id JOIN kg_edge e3 ON e2.target_id = e3.source_id WHERE e1.source_id = 1 AND e1.relation = 'interest' AND e2.relation = 'belong' AND e3.relation = 'similar' GROUP BY e3.target_id ORDER BY score DESC LIMIT 10;

参数说明:e1.source_id = 1是目标用户,实际代码里用变量替换。如果查询超过 2 秒,考虑给kg_edge的source_id和relation建联合索引。

4.3 冷启动兜底策略:热度、标签和随机

新用户没有 interest 边时,推荐接口不能返回空。我一般用三级兜底:第一级,如果用户注册时选了标签,直接把这些标签当 interest 边插入;第二级,如果没选标签,返回最近 7 天user_action里出现次数最多的物品;第三级,如果连行为数据都没有,随机返回 10 个物品,但要在前端标注“热门推荐”。这样至少保证页面不空白,答辩时也有话可说。

5. 避坑与排查:那些让毕设卡壳的常见问题

5.1 现象:Flask 启动报ModuleNotFoundError: No module named 'app'

原因:入口文件run.py和app包不在同一级目录,或者虚拟环境没激活。解决:确认目录结构是run.py和app/并列,然后在项目根目录执行python run.py。如果还报错,在run.py开头加import sys; sys.path.append('.')。

5.2 现象:MySQL 连接报Access denied for user 'kg_user'@'localhost'

原因:密码里的特殊字符没转义,或者 MySQL 8.0 的认证插件是caching_sha2_password,而 pymysql 旧版不支持。解决:先确认连接串密码转义正确,然后执行ALTER USER 'kg_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Kg@123456';把插件改成原生密码。

5.3 现象:推荐结果每次刷新都不一样,但数据没变

原因:SQL 查询没有ORDER BY稳定排序,或者LIMIT配合GROUP BY时 MySQL 返回顺序随机。解决:在ORDER BY score DESC后面再加一个, target_id ASC,保证同分时顺序固定。

5.4 现象:中文标签在页面上显示为问号

原因:数据库、表、连接串三处字符集不一致。解决:建库用utf8mb4,连接串加?charset=utf8mb4,Flask 返回 JSON 时用jsonify默认就是 UTF-8。如果还有问题,检查 MySQL 配置文件my.ini里的character-set-server。

5.5 现象:前端请求接口跨域被拦截

原因:Flask 默认不发送 CORS 头。解决:安装flask-cors,在app/__init__.py里初始化CORS(app)。如果只针对 API,用CORS(app, resources={r"/api/*": {"origins": "*"}})。

6. 进阶技巧:用邻接矩阵加速小规模图谱查询

当图谱节点在几千以内时,每次请求都走 SQL JOIN 其实很浪费。我一般会加一个内存缓存层:启动时把kg_edge加载成邻接矩阵,推荐时直接查矩阵。Python 里用 numpy 构建邻接矩阵很简单,下面这段代码放在app/graph_cache.py。

import numpy as np from app import db from sqlalchemy import text class GraphCache: def __init__(self): self.node_index = {} # node_id -> 矩阵下标 self.matrix = None def load(self): nodes = db.session.execute(text("SELECT node_id FROM kg_node")).fetchall() self.node_index = {row[0]: i for i, row in enumerate(nodes)} n = len(nodes) self.matrix = np.zeros((n, n), dtype=np.float32) edges = db.session.execute(text("SELECT source_id, target_id, weight FROM kg_edge")).fetchall() for src, tgt, w in edges: if src in self.node_index and tgt in self.node_index: self.matrix[self.node_index[src]][self.node_index[tgt]] = w def recommend(self, user_id, top_k=10): if user_id not in self.node_index: return [] u_idx = self.node_index[user_id] # 矩阵乘法:用户向量 × 邻接矩阵 × 邻接矩阵 scores = self.matrix[u_idx].dot(self.matrix) # 排除用户自己 scores[u_idx] = 0 top_indices = np.argsort(scores)[::-1][:top_k] idx_to_node = {v: k for k, v in self.node_index.items()} return [(idx_to_node[i], float(scores[i])) for i in top_indices if scores[i] > 0]

逻辑说明:load方法在应用启动时调用一次,把边权重填进矩阵。recommend用两次矩阵乘法模拟两跳路径,np.argsort取 top_k。参数上,dtype=np.float32省内存,几千节点没问题;如果节点过万,矩阵会占几百 MB,这时候还是回退到 SQL 查询。这个缓存的缺点是数据更新后要重新load,我一般在后台加一个定时任务,每 10 分钟刷新一次。

验证方法:启动后先调/api/recommend/1,再直接调GraphCache.recommend(1),对比结果是否一致。如果矩阵结果为空,检查node_index是否包含了该用户,以及边权重是否全为零。我踩过的坑是,忘记在load里过滤node_type,把标签节点也当成了推荐物品,导致推荐结果里出现“标签”这种奇怪的东西。后来加了一个item_indices集合,只对物品节点排序。

这套方案值不值得做?如果你只是应付毕设,SQL 版本足够;如果你想在答辩时展示“性能优化”,加这个矩阵缓存是个亮点。但别本末倒置,先把图谱数据和推荐路径跑通,再考虑加速。希望帮到你。

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

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

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

立即咨询