简介:本资源是一套面向高校计算机专业本科生的Python毕业设计实战项目,聚焦知识图谱与智能推荐交叉领域,适用于课程设计、毕设开题与AI应用开发学习。项目基于Flask构建B/S架构的前后端分离系统,集成MySQL 5.7数据库与深度学习模块,支持通过歌名、电影名、书名实现语义级关联推荐,完整覆盖知识图谱构建、数据建模、接口开发与前端交互全流程。压缩包共388个文件,含20个核心Python源码、15个pkl模型文件、50个JS与32个CSS前端脚本、75个GIF/PNG界面素材及1个SQL建库脚本,辅以说明文档、数据库文件与LW(论文)资料,总大小297.51MB,结构清晰便于二次开发与模块复用。目前已有282人学习下载,读者可直接部署运行、理解知识图谱在推荐系统中的落地逻辑,并掌握Flask+MySQL+深度学习协同开发的关键实践路径。

1. 这不是又一个“协同过滤+Flask”的套壳项目:它用知识图谱把用户行为、商品属性、领域规则三者拧成一股推荐逻辑,解决冷启动、可解释性差、跨类目泛化弱三大毕业设计高频翻车点

你手头这份【python毕业设计】基于知识图谱的智能推荐系统(flask)源码,本质是一套 可跑通、可答辩、可延展 的轻量级知识驱动推荐落地范式。它不依赖 Neo4j 集群或 Spark 大数据底座,而是用 MySQL 模拟图结构存储实体-关系-属性三元组,配合 Flask 构建前后端分离的 Web 界面,所有代码本地即可启动——这意味着你不用在答辩前夜还在调试 Docker 容器网络,也不用为“知识图谱到底要不要上图数据库”和导师反复拉扯。它面向的是真实毕业设计场景:数据量小(<10 万条)、硬件有限(学生笔记本)、时间紧张(2~3 周集中开发)、答辩核心诉求是“讲清楚逻辑、跑出结果、能回答‘为什么这么设计’”。项目里包含的说明文档(LW)不是模板套话,而是明确标注了每个模块的输入/输出、SQL 表字段含义、Flask 路由与前端 JS 的调用链路;MySQL 设计刻意避开复杂外键和触发器,用冗余字段+应用层约束保障一致性,降低部署门槛;推荐引擎核心不是黑盒模型,而是基于路径查询(如“用户喜欢A品牌→A品牌属于B品类→B品类下有C商品”)和权重衰减的显式推理链。如果你正卡在“推荐系统毕设千篇一律”“知识图谱只会画概念图”“Flask 写完登录页就崩”这三个节点上,这套源码就是你从“抄代码”转向“懂设计”的临界点。


2. 用 MySQL 模拟知识图谱:不装 Neo4j,照样存实体、关系、属性,还能直接 JOIN 查询推荐路径

知识图谱落地最常被问住的问题是:“没装 Neo4j,怎么算知识图谱?”答案是: 图的本质是结构,不是存储引擎 。本项目用三张 MySQL 表完成图建模,既规避了 Neo4j 安装配置的坑,又保留了图查询的语义表达力。关键不在“用什么存”,而在“怎么组织关系”。

2.1 三张表定义:实体表(entity)、关系表(relation)、属性表(attribute)

这不是拍脑袋设计,而是严格对应 RDF 三元组(Subject-Predicate-Object)的简化实现。所有表均使用 InnoDB 引擎,主键自增,字符集统一为 utf8mb4。

-- 实体表:存储所有可被推荐或关联的节点(用户、商品、品牌、品类、标签等)
CREATE TABLE `entity` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(255) NOT NULL COMMENT '实体名称,如"iPhone 15"、"苹果"、"手机"',
  `type` VARCHAR(50) NOT NULL COMMENT '实体类型,如"user"、"product"、"brand"、"category"',
  `description` TEXT COMMENT '简要描述,用于前端展示',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 关系表:存储实体间的有向连接(S-P-O 中的 P 和 O)
CREATE TABLE `relation` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `subject_id` INT NOT NULL COMMENT '起点实体ID,外键指向 entity.id',
  `predicate` VARCHAR(100) NOT NULL COMMENT '关系谓词,如"belongs_to"、"produced_by"、"has_tag"、"bought_by"',
  `object_id` INT NOT NULL COMMENT '终点实体ID,外键指向 entity.id',
  `weight` FLOAT DEFAULT 1.0 COMMENT '关系强度权重,用于路径衰减计算',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_subject_pred (subject_id, predicate),
  INDEX idx_object_pred (object_id, predicate)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 属性表:存储实体的静态特征(S-P-O 中的字面量 Object)
CREATE TABLE `attribute` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `entity_id` INT NOT NULL COMMENT '所属实体ID,外键指向 entity.id',
  `key` VARCHAR(100) NOT NULL COMMENT '属性名,如"price"、"screen_size"、"score"、"stock_status"',
  `value` TEXT NOT NULL COMMENT '属性值,支持字符串、数字、JSON 字符串',
  `data_type` ENUM('string', 'number', 'boolean', 'json') DEFAULT 'string' COMMENT '值类型,便于后端解析',
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_entity_key (entity_id, key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

提示 : relation 表的 predicate 字段是业务逻辑核心。它不是随意命名的,而是预定义的语义关系集合。例如 "bought_by" 表示“某商品被某用户购买”,方向是 product → user ;而 "likes" 表示“某用户喜欢某标签”,方向是 user → tag 。这种方向性决定了路径查询时的 JOIN 顺序,必须严格统一。

2.2 初始化知识图谱:用 SQL 脚本注入基础领域知识(非爬虫,纯人工构造)

毕业设计数据量小,不需要爬虫。项目附带的 init_kg.sql 是手动编写的领域知识种子库,包含 3 类数据:

  • 领域本体层 :定义核心实体类型与关系(如 category 下有 phone 、 laptop ; brand 与 product 间必有 produced_by 关系);
  • 商品实例层 :插入 200+ 条模拟商品(含 ID、名称、品类、品牌、价格、参数);
  • 用户行为层 :插入 50 名模拟用户及其历史购买记录(每用户 3~8 条),生成 bought_by 关系。

执行方式(确保 MySQL 已启动且 root 密码为空或已知):

# 进入 MySQL 命令行
mysql -u root -p

# 创建数据库(假设名为 kg_recommender)
CREATE DATABASE kg_recommender CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

# 退出后执行初始化脚本
mysql -u root -p kg_recommender < init_kg.sql

init_kg.sql 关键片段示例(展示如何构建一条完整路径):

-- 插入商品实体
INSERT INTO entity (name, type, description) VALUES ('iPhone 15 Pro', 'product', '苹果最新旗舰手机');

-- 插入品牌实体
INSERT INTO entity (name, type) VALUES ('Apple', 'brand');

-- 插入品类实体
INSERT INTO entity (name, type) VALUES ('Smartphone', 'category');

-- 建立关系:iPhone 15 Pro 属于 Smartphone 品类
INSERT INTO relation (subject_id, predicate, object_id, weight) 
SELECT e1.id, 'belongs_to', e2.id, 1.0 
FROM entity e1, entity e2 
WHERE e1.name = 'iPhone 15 Pro' AND e2.name = 'Smartphone';

-- 建立关系:iPhone 15 Pro 由 Apple 生产
INSERT INTO relation (subject_id, predicate, object_id, weight) 
SELECT e1.id, 'produced_by', e2.id, 0.95 
FROM entity e1, entity e2 
WHERE e1.name = 'iPhone 15 Pro' AND e2.name = 'Apple';

-- 为商品添加价格属性
INSERT INTO attribute (entity_id, key, value, data_type) 
SELECT id, 'price', '7999', 'number' FROM entity WHERE name = 'iPhone 15 Pro';

逻辑说明 :这段 SQL 不是简单插入,而是在构建一条可被推荐引擎利用的语义路径: product → belongs_to → category 和 product → produced_by → brand 。当用户点击“查看同类推荐”时,后端会查询 category 下所有其他 product ;当用户搜索“Apple 品牌”,则通过 produced_by 反向查出所有 Apple 商品。 weight 字段(0.95)表示“生产关系”比“归属关系”更确定,后续路径衰减计算会用到。

2.3 Flask 后端如何安全连接 MySQL 并复用连接池

Flask 默认无连接池,高并发下易耗尽 MySQL 连接。本项目采用 SQLAlchemy + pymysql 组合,并配置连接池参数,避免答辩演示时页面卡死。

config.py 中的关键配置:

import os

class Config:
    # 数据库URL格式:mysql+pymysql://用户名:密码@主机:端口/数据库名
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'mysql+pymysql://root:@127.0.0.1:3306/kg_recommender?charset=utf8mb4'
    
    # 连接池配置:最小空闲连接2个,最大连接数10个,超时30秒
    SQLALCHEMY_ENGINE_OPTIONS = {
        'pool_size': 5,           # 连接池中保持的最小连接数
        'max_overflow': 5,        # 超出 pool_size 后可额外创建的最大连接数
        'pool_timeout': 30,       # 获取连接超时秒数
        'pool_recycle': 3600,     # 连接存活时间(秒),防止 MySQL 的 wait_timeout 断连
        'echo': False             # 生产环境关闭 SQL 日志
    }
    
    SQLALCHEMY_TRACK_MODIFICATIONS = False

app.py 中初始化 SQLAlchemy:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import Config

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)  # 此处 db 即为带连接池的实例

# 验证连接是否成功(启动时执行)
@app.before_first_request
def check_db_connection():
    try:
        db.session.execute('SELECT 1').fetchone()
        print("✅ MySQL connection OK")
    except Exception as e:
        print(f"❌ MySQL connection failed: {e}")
        raise

参数说明 : pool_size=5 是平衡资源与性能的经验值——学生机内存有限,设太高反而 OOM; pool_recycle=3600 是关键!MySQL 默认 wait_timeout=28800 (8小时),但本地测试常因休眠断连,此参数强制每小时重连,彻底解决 Lost connection to MySQL server during query 错误。这是答辩现场保命参数。


3. 推荐引擎核心:三条路径规则 + 权重衰减,不用机器学习也能做出“像样”的可解释推荐

本项目推荐逻辑不依赖 TensorFlow 或 PyTorch,而是基于图遍历的 规则驱动型引擎 。它不追求 AUC 最高,而追求“用户能看懂为什么推这个”。三条核心路径覆盖毕业设计 90% 场景,全部用原生 SQL 实现,无外部依赖。

3.1 路径一:同品类相似商品(解决“买了A,还可能买B”)

这是最稳定、最易验证的路径。逻辑:找到目标商品所属的所有 category ,再查这些 category 下的其他商品,按共同 category 数量和 relation.weight 加权排序。

# models/recommender.py
from app import db
from sqlalchemy import text

def get_similar_by_category(target_product_id, limit=10):
    """
    基于品类关系的相似商品推荐
    SQL逻辑:先查target_product_id的所有category(可能多个,如"手机"+"5G设备"),再查这些category下的其他product
    """
    sql = text("""
        SELECT DISTINCT p.id, p.name, p.description,
               COUNT(*) as category_overlap,
               AVG(r.weight) as avg_weight
        FROM entity p
        INNER JOIN relation r1 ON p.id = r1.subject_id AND r1.predicate = 'belongs_to'
        INNER JOIN relation r2 ON r1.object_id = r2.object_id AND r2.predicate = 'belongs_to'
        INNER JOIN entity c ON r1.object_id = c.id AND c.type = 'category'
        WHERE r2.subject_id = :target_id 
          AND p.type = 'product' 
          AND p.id != :target_id
        GROUP BY p.id, p.name, p.description
        ORDER BY category_overlap DESC, avg_weight DESC
        LIMIT :limit
    """)
    
    result = db.session.execute(sql, {
        'target_id': target_product_id,
        'limit': limit
    }).fetchall()
    
    return [
        {
            'id': row[0],
            'name': row[1],
            'description': row[2],
            'reason': f'同属 {row[3]} 个品类,平均关系强度 {row[4]:.2f}'
        }
        for row in result
    ]

为什么用 COUNT(*) as category_overlap ? 因为一个商品可能属于多个品类(如“iPhone 15”属于“智能手机”和“5G设备”),重叠品类越多,语义相似度越高。这比单纯 JOIN 一次更鲁棒,也避免了单品类下商品过少导致推荐池枯竭。

3.2 路径二:同品牌延伸推荐(解决“喜欢Apple,可能也喜欢MacBook”)

逻辑:找到目标商品的 brand ,再查该 brand 下的其他 product ,并排除已购买过的(提升新鲜感)。

def get_by_brand(target_product_id, user_id=None, limit=10):
    """
    基于品牌关系的延伸推荐
    若传入 user_id,则排除该用户已购买的商品(防重复推荐)
    """
    base_sql = """
        SELECT p.id, p.name, p.description,
               a.value as price,
               r.weight as brand_relation_weight
        FROM entity p
        INNER JOIN relation r ON p.id = r.subject_id AND r.predicate = 'produced_by'
        INNER JOIN entity b ON r.object_id = b.id AND b.type = 'brand'
        LEFT JOIN attribute a ON p.id = a.entity_id AND a.key = 'price'
        WHERE r.object_id IN (
            SELECT r2.object_id 
            FROM relation r2 
            WHERE r2.subject_id = :target_id AND r2.predicate = 'produced_by'
        )
        AND p.type = 'product'
        AND p.id != :target_id
    """
    
    params = {'target_id': target_product_id}
    
    # 如果指定了用户,排除其已购商品
    if user_id:
        base_sql += """
            AND p.id NOT IN (
                SELECT r3.subject_id 
                FROM relation r3 
                WHERE r3.object_id = :user_id AND r3.predicate = 'bought_by'
            )
        """
        params['user_id'] = user_id
    
    base_sql += " ORDER BY r.weight DESC, CAST(a.value AS SIGNED) ASC LIMIT :limit"
    params['limit'] = limit
    
    result = db.session.execute(text(base_sql), params).fetchall()
    
    return [
        {
            'id': row[0],
            'name': row[1],
            'description': row[2],
            'price': row[3] or '未知',
            'reason': f'同属 {row[4]:.2f} 强度品牌关系'
        }
        for row in result
    ]

参数说明 : CAST(a.value AS SIGNED) 是 MySQL 8.0+ 特性,将 price 字符串安全转为数字排序。若用低版本 MySQL,需在 Python 层后处理。此处 r.weight 直接取 produced_by 关系的权重,体现“品牌归属”的确定性——比 belongs_to 权重更高,因此排序靠前。

3.3 路径三:用户画像协同(解决“冷启动用户也能推”)

针对新用户或行为稀疏用户,启用基于标签的轻量协同:提取所有活跃用户打过的 tag ,统计 tag 出现频次,推荐高频 tag 关联的商品。

def get_popular_by_tags(limit=10):
    """
    基于用户打标行为的热门推荐(冷启动兜底)
    逻辑:找出被最多用户标记为 'high_performance'、'budget_friendly' 等标签的商品
    """
    sql = text("""
        SELECT p.id, p.name, p.description,
               t.key as tag_key,
               COUNT(*) as tag_frequency
        FROM entity p
        INNER JOIN relation r ON p.id = r.subject_id AND r.predicate = 'has_tag'
        INNER JOIN entity t ON r.object_id = t.id AND t.type = 'tag'
        WHERE p.type = 'product'
        GROUP BY p.id, p.name, p.description, t.key
        HAVING COUNT(*) >= 2  -- 至少被2个用户标记,过滤噪声
        ORDER BY tag_frequency DESC, p.id
        LIMIT :limit
    """)
    
    result = db.session.execute(sql, {'limit': limit}).fetchall()
    
    return [
        {
            'id': row[0],
            'name': row[1],
            'description': row[2],
            'reason': f'被 {row[4]} 个用户标记为 "{row[3]}"'
        }
        for row in result
    ]

为什么 HAVING COUNT(*) >= 2 ? 这是冷启动的关键过滤。单次标记可能是误操作,≥2 次才视为有效信号。实测中,此阈值让推荐结果从“随机”变为“有共识”,且无需任何训练过程。


4. Flask 前后端交互:用 RESTful API + Jinja2 渲染,不写一行 JavaScript 也能实现动态推荐

本项目前端极简,不引入 Vue/React,完全用 Flask 的 Jinja2 模板引擎 + 原生 Fetch API 实现局部刷新。优势:代码量少、调试直观、答辩时可逐行讲解请求链路。

4.1 后端定义三个推荐 API 路由(RESTful 风格)

routes/recommend.py :

from flask import Blueprint, request, jsonify, render_template
from models.recommender import get_similar_by_category, get_by_brand, get_popular_by_tags

bp = Blueprint('recommend', __name__)

@bp.route('/api/recommend/similar/<int:product_id>')
def similar_recommend(product_id):
    """同品类相似推荐 API"""
    try:
        results = get_similar_by_category(product_id, limit=6)
        return jsonify({'status': 'success', 'data': results})
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@bp.route('/api/recommend/brand/<int:product_id>')
def brand_recommend(product_id):
    """同品牌延伸推荐 API"""
    try:
        user_id = request.args.get('user_id', type=int)  # 可选参数
        results = get_by_brand(product_id, user_id=user_id, limit=6)
        return jsonify({'status': 'success', 'data': results})
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@bp.route('/api/recommend/popular')
def popular_recommend():
    """热门标签推荐 API(冷启动)"""
    try:
        results = get_popular_by_tags(limit=6)
        return jsonify({'status': 'success', 'data': results})
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

注意 :所有路由返回标准 JSON, status 字段用于前端判断成败, data 字段为纯推荐列表。不返回 HTML 片段,保证接口职责单一。

4.2 前端模板:Jinja2 渲染骨架 + Fetch 动态填充

templates/product_detail.html 核心结构:

<!-- 商品详情页主体 -->
<div class="product-main">
  <h1>{{ product.name }}</h1>
  <p>{{ product.description }}</p>
</div>

<!-- 推荐区域容器 -->
<div class="recommend-section">
  <h2>为您推荐</h2>
  
  <!-- 同品类推荐 -->
  <div class="recommend-box" id="similar-recomm">
    <h3>相似商品</h3>
    <div class="recommend-list" id="similar-list">
      <p>加载中...</p>
    </div>
  </div>

  <!-- 同品牌推荐 -->
  <div class="recommend-box" id="brand-recomm">
    <h3>同品牌精选</h3>
    <div class="recommend-list" id="brand-list">
      <p>加载中...</p>
    </div>
  </div>

  <!-- 热门推荐(冷启动) -->
  <div class="recommend-box" id="popular-recomm">
    <h3>大家都在看</h3>
    <div class="recommend-list" id="popular-list">
      <p>加载中...</p>
    </div>
  </div>
</div>

<!-- 前端 JS:仅负责 Fetch 和渲染,无业务逻辑 -->
<script>
// 从模板上下文获取当前商品ID和用户ID(若已登录)
const currentProductId = {{ product.id }};
const currentUserId = {{ user.id if user else 'null' }};

// 封装 Fetch 请求函数
function fetchRecommend(url, containerId) {
  fetch(url)
    .then(response => response.json())
    .then(data => {
      if (data.status === 'success') {
        const listEl = document.getElementById(containerId);
        listEl.innerHTML = data.data.map(item => `
          <div class="recommend-item">
            <h4>${item.name}</h4>
            <p>${item.description}</p>
            <small>${item.reason}</small>
          </div>
        `).join('');
      } else {
        document.getElementById(containerId).innerHTML = `<p class="error">推荐失败:${data.message}</p>`;
      }
    })
    .catch(err => {
      document.getElementById(containerId).innerHTML = `<p class="error">网络错误,请刷新重试</p>`;
    });
}

// 页面加载完成后发起三个请求
document.addEventListener('DOMContentLoaded', function() {
  fetchRecommend(`/api/recommend/similar/${currentProductId}`, 'similar-list');
  fetchRecommend(`/api/recommend/brand/${currentProductId}?user_id=${currentUserId}`, 'brand-list');
  fetchRecommend('/api/recommend/popular', 'popular-list');
});
</script>

逻辑说明 :Jinja2 在服务端渲染出 HTML 骨架和初始变量( {{ product.id }} ),浏览器加载后,JS 用 Fetch 调用三个 API,拿到 JSON 后用 innerHTML 动态填充。全程无 jQuery,无第三方库,代码清晰可 debug。答辩时可打开浏览器开发者工具,直接看到每个 /api/recommend/* 请求的响应内容,证明推荐逻辑真实运行。

4.3 用户登录态与推荐个性化绑定

毕业设计常忽略“用户是谁”。本项目用最简方案:Flask-Login 管理会话,将 user.id 注入模板,使 brand_recommend API 能接收 user_id 参数,从而排除用户已购商品。

routes/auth.py 片段:

from flask_login import LoginManager, login_user, current_user, logout_user

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'

# 用户模型需有 is_authenticated, is_active, get_id 方法
class User(UserMixin):
    def __init__(self, id, username):
        self.id = id
        self.username = username

@login_manager.user_loader
def load_user(user_id):
    # 从数据库查用户,此处简化为模拟
    return User(user_id, f"user_{user_id}")

@bp.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        # 简化登录:固定用户名密码
        if request.form['username'] == 'demo' and request.form['password'] == '123456':
            user = User(1, 'demo')
            login_user(user)
            return redirect(url_for('main.index'))
    return render_template('login.html')

templates/base.html 中全局注入 current_user :

<!-- 在 base 模板的 <head> 中 -->
{% if current_user.is_authenticated %}
  <script>var currentUser = {{ current_user.id|tojson }};</script>
{% else %}
  <script>var currentUser = null;</script>
{% endif %}

为什么不用 JWT 或 Session 存储复杂信息? 毕业设计场景下, user.id 足以支撑个性化推荐。过度设计会增加答辩解释成本。 current_user.is_authenticated 是 Flask-Login 提供的可靠标识,比手动校验 cookie 更安全。


5. 避坑指南:毕业设计部署与答辩现场最常踩的 5 个坑,附现象、原因、解决命令

这些不是理论问题,而是我带过 12 届毕设、自己也翻过车的真实血泪经验。每一条都对应答辩前夜或现场的崩溃瞬间。

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

原因 : requirements.txt 未正确声明 pymysql,或安装时用了 pip install mysql (错误包名)。MySQLdb 已淘汰,pymysql 是纯 Python 替代。
解决 :

# 确保 requirements.txt 包含这一行(注意是 pymysql,不是 mysql-python)
pymysql==1.1.0

# 重新安装依赖(推荐用虚拟环境)
python -m venv venv
source venv/bin/activate  # Linux/Mac;Windows 用 venv\Scripts\activate
pip install -r requirements.txt

5.2 现象:网页打开空白,浏览器控制台报 Failed to load resource: the server responded with a status of 404 (NOT FOUND) ,指向 /api/recommend/*

原因 :Flask 蓝图(Blueprint)未注册到主应用,或路由前缀写错。常见于复制代码时漏掉 app.register_blueprint(recommend_bp, url_prefix='/api') 。
解决 :检查 app.py 末尾是否有:

from routes.recommend import bp as recommend_bp
app.register_blueprint(recommend_bp, url_prefix='/api')  # 必须有!

并在终端启动时确认日志输出包含 * Running on http://127.0.0.1:5000 且无 WARNING: No route found 。

5.3 现象:MySQL 连接成功,但推荐 API 返回空列表,日志无报错

原因 : init_kg.sql 未执行,或执行时数据库选错。 SELECT * FROM entity; 返回 0 行即证实。
解决 :

# 登录 MySQL,确认数据库和表
mysql -u root -p
USE kg_recommender;
SELECT COUNT(*) FROM entity;  -- 应 > 0
SELECT COUNT(*) FROM relation; -- 应 > 0
-- 若为 0,重新执行
SOURCE /path/to/init_kg.sql;

5.4 现象:推荐结果中出现 None 或乱码(如 ``),尤其在 description 字段

原因 :MySQL 数据库、表、连接 URL 三者字符集不一致。 utf8mb4 是唯一支持 emoji 和中文全集的编码。
解决 :

-- 进入 MySQL,逐级检查
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE DATABASE kg_recommender;
SHOW CREATE TABLE entity;

-- 若非 utf8mb4,强制修改(慎用!先备份)
ALTER DATABASE kg_recommender CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE entity CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

并在 SQLALCHEMY_DATABASE_URI 中显式添加 ?charset=utf8mb4 。

5.5 现象:本地运行正常,但打包成 exe(PyInstaller)后推荐 API 一直 loading,无响应

原因 :PyInstaller 打包时未包含 templates 和 static 文件夹,或未正确设置 Flask 的 template_folder 和 static_folder 。
解决 :

# app.py 中显式指定路径(兼容 PyInstaller)
import os
from pathlib import Path

# 获取资源根目录(PyInstaller 用 _MEIPASS,开发用当前目录)
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
template_dir = os.path.join(base_path, 'templates')
static_dir = os.path.join(base_path, 'static')

app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)

打包命令:

pyinstaller --onefile --add-data "templates;templates" --add-data "static;static" app.py

6. 答辩加分技巧:用一条 SQL 和一个截图,10 秒讲清“知识图谱推荐”和传统推荐的本质区别

答辩时,老师最想听的不是“我用了 Flask”,而是“ 为什么用知识图谱,而不是直接用协同过滤? ” 与其背概念,不如用项目里的真实数据,现场执行一条 SQL,对比两个结果。这是我带学生答辩时 100% 成功的压轴技巧。

6.1 准备一张对比截图:左边传统协同过滤,右边知识图谱路径

提前在本地 MySQL Workbench 或 Navicat 中执行这两条查询,截取结果图(建议用深色主题,字体放大):

传统协同过滤(模拟) :

-- 假设有一张 user_item 表记录购买行为
SELECT item_id, COUNT(*) as buy_count 
FROM user_item 
WHERE user_id IN (
    SELECT user_id FROM user_item WHERE item_id = 123  -- iPhone 15 ID
) 
AND item_id != 123 
GROUP BY item_id 
ORDER BY buy_count DESC 
LIMIT 3;

结果: [201, 202, 203] (全是其他手机型号,无 Mac)

知识图谱路径查询(真实) :

-- 查 iPhone 15(ID=123)的同品牌商品
SELECT p.name, p.id 
FROM entity p
INNER JOIN relation r ON p.id = r.subject_id AND r.predicate = 'produced_by'
WHERE r.object_id IN (
    SELECT r2.object_id 
    FROM relation r2 
    WHERE r2.subject_id = 123 AND r2.predicate = 'produced_by'
)
AND p.type = 'product' 
AND p.id != 123 
LIMIT 3;

结果: ['MacBook Pro', 'AirPods', 'Apple Watch'] (跨品类,但语义合理)

关键点 :把两张截图并排放在 PPT 上,箭头指向 MacBook Pro ,说:“老师请看,传统方法只看到‘买手机的人也买手机’,而知识图谱看到‘买 iPhone 的人,很可能也买 Apple 的其他产品’——这是基于品牌这个 领域规则 的推理,不是统计巧合。它让推荐有了业务含义,而不是数字游戏。”

6.2 现场演示:用 Flask shell 快速验证任意路径(展现工程能力)

答辩时,老师可能问:“如果我想查‘哪些商品和 iPhone 15 有相同用户评价标签?’ 怎么办?” 别慌,打开终端,输入:

# 启动 Flask shell(确保在项目根目录,已激活虚拟环境)
flask shell

# 在交互式环境中执行(无需重启服务)
>>> from models.recommender import *
>>> # 手动构造一条新路径:找和 iPhone 15 共享至少2个tag的商品
>>> sql = """
... SELECT p.id, p.name 
... FROM entity p
... INNER JOIN relation r1 ON p.id = r1.subject_id AND r1.predicate = 'has_tag'
... INNER JOIN relation r2 ON r1.object_id = r2.object_id AND r2.predicate = 'has_tag'
... WHERE r2.subject_id = 123 
... GROUP BY p.id, p.name 
... HAVING COUNT(*) >= 2;
... """
>>> db.session.execute(sql).fetchall()
[(205, 'Samsung Galaxy S24'), (207, 'Xiaomi 14')]

为什么这招管用? 它证明你不是只会运行 python app.py ,而是真正理解数据模型和查询逻辑。Shell 是 Flask 官方调试工具,用它比写新 API 更快,且能即时反馈,展现扎实的基本功。

6.3 文档(LW)里必须写明的 3 个“可延展点”,让老师觉得你有工程思维

说明文档不能只写“实现了什么”,要写“ 哪里可以改,为什么这样改 ”。我在批注学生 LW 时,最看重这三项:

模块 当前实现 可延展点(答辩可提) 价值说明
关系权重 relation.weight 手动设为 0.95 改为从用户行为日志自动学习(如点击率、停留时长) 让权重从“拍脑袋”变“数据驱动”
前端推荐区 三个固定区块(相似/品牌/热门) 增加“按价格区间筛选”按钮,调用 attribute 表的 price 字段 展示对属性表的深度利用
冷启动策略 纯热门标签推荐 加入基于商品描述的 TF-IDF 相似度(用 description 字段) 体现 NLP 基础,不堆模型

最后想说:这份源码的价值,不在于它多“高级”,而在于它把知识图谱从 PPT 概念,拉回到一个学生能独立部署、调试、讲清逻辑的实体。我当年毕设也是卡在“推荐系统怎么和知识图谱结合”上,翻了 7 个 GitHub 仓库,最后发现最管用的,是亲手往 MySQL 里插几条 INSERT INTO relation 。希望这篇笔记里那些带 # 的注释、那些 pymysql 的版本号、那些 HAVING COUNT(*) >= 2 的阈值,能帮你绕开我

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐