基于知识图谱与神经网络的简历推荐系统毕业设计实战

发布时间:2026/10/25 10:17:33
基于知识图谱与神经网络的简历推荐系统毕业设计实战

简介:这份资源是面向高校计算机相关专业学生的Python毕业设计完整项目,主题为基于知识图谱与人工神经网络的简历推荐系统,适合作为毕设、期末大作业或课程设计的高分参考方案。项目将知识图谱构建与深度神经网络模型相结合,实现简历解析、岗位匹配与智能推荐等核心功能,代码注释详尽,新手也能快速理解整体逻辑。压缩包共66个文件,约10.7MB,涵盖19个Python源码文件、8个HTML页面、6个CSS与6个JavaScript前端资源,以及模型权重、词向量语料、SQLite数据库和说明文档等,前后端结构完整,部署后即可运行。目前已有113人学习下载。项目经过严格调试,附带文档说明与数据样本,读者可据此掌握知识图谱构建、DNN模型训练、简历文本解析与推荐流程,并直接用于毕业设计答辩或二次开发,具有较高的实际应用与参考价值。

1. 简历推荐系统为什么值得做成毕业设计:从投递匹配到知识图谱的落地价值

招聘平台每天沉淀的简历和岗位描述,本质上是两堆非结构化文本。求职者抱怨"投了 200 份没回音",HR 抱怨"筛了 300 份没一个合适",问题不在数量,而在匹配环节缺少语义理解。一个只靠关键词if "Python" in resume的推荐逻辑,会把"熟悉 Python 数据分析"和"用 Python 做过量化交易策略代码"当成同一类人,也会把"知识图谱构建经验"和"知识图谱标注"混为一谈。这正是基于知识图谱与人工神经网络的简历推荐系统要解决的核心问题:用图谱把技能、项目、岗位之间的关联显式建模,再用神经网络学习人岗之间的非线性匹配关系。

这个题目适合计算机、软件工程、电子信息工程方向的毕业设计,因为它同时覆盖了数据清洗、图结构建模、深度学习训练和 Web 服务四个可展示的模块,答辩时既有算法深度又有工程完整度。下面按"数据怎么变成图 → 神经网络怎么学 → 系统怎么跑起来 → 哪里会翻车"的顺序,把一套可复现的方案讲清楚。

2. 从简历文本到知识图谱:实体抽取与三元组构建的完整链路

知识图谱不是把简历丢进数据库就完事了,它的核心是"实体—关系—实体"的三元组结构。简历里的实体包括技能(Python、知识图谱)、学历(本科、硕士)、岗位(算法工程师)、项目经历;关系包括"掌握""要求""属于""参与过"。这一章把从原始简历到可查询图谱的每一步拆开讲。

2.1 简历数据的结构化清洗与字段对齐

真实简历格式极其混乱,PDF、Word、纯文本都有,字段命名也不统一。第一步不是上模型,而是把非结构化文本压成统一 schema。常见做法是用pdfplumber抽文本,再用正则切分"教育经历/项目经历/技能"等段落。

import re import pdfplumber # 简历段落关键词,用于切分不同区块 SECTION_KEYS = ["教育经历", "项目经历", "技能", "实习经历", "自我评价"] def extract_text(pdf_path): """从 PDF 简历中抽取纯文本,按行拼接""" text = "" with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: page_text = page.extract_text() or "" text += page_text + "\n" return text def split_sections(text): """按关键词把简历切成字典,key 是段落名,value 是内容""" pattern = "|".join(SECTION_KEYS) parts = re.split(f"({pattern})", text) sections = {} for i in range(1, len(parts) - 1, 2): sections[parts[i]] = parts[i + 1].strip() return sections if __name__ == "__main__": raw = extract_text("resume_sample.pdf") sec = split_sections(raw) print(sec.keys()) # 查看切出了哪些段落

这段代码的逻辑是:先用pdfplumber把 PDF 转成文本流,再用正则按段落关键词做切分。参数上,SECTION_KEYS需要根据你拿到的简历模板调整,中文简历常见的就是这几个;如果简历是英文的,要换成Education/Projects/Skills。切分后每个段落单独处理,避免技能词和项目描述混在一起导致后续实体抽取串味。

提示:清洗阶段一定要保留原始文本和清洗后文本的映射关系,后面图谱出错时能回溯到是哪份简历的哪个字段出了问题,这是排查问题的后悔药。

2.2 技能实体抽取:规则词典 + 轻量模型的组合策略

技能实体是图谱里最关键的节点。纯规则词典覆盖不全,纯模型又需要标注数据,毕业设计阶段最稳的组合是"词典打底 + 模型补漏"。先维护一份技能词典,再用jieba分词和词性标注把候选词捞出来。

import jieba.posseg as pseg # 技能词典,实际项目中会从岗位 JD 里统计高频词扩充 SKILL_DICT = {"Python", "知识图谱", "神经网络", "SQL", "机器学习", "数据分析"} def extract_skills(text): """从文本中抽取技能实体,词典命中优先,名词短语作为候选""" jieba.load_userdict(list(SKILL_DICT)) # 把技能词加入分词词典 hits, candidates = set(), set() for word, flag in pseg.cut(text): if word in SKILL_DICT: hits.add(word) elif flag in ("n", "nz", "eng") and len(word) > 1: candidates.add(word) return hits, candidates text = "熟悉 Python 和知识图谱构建,参与过机器学习项目" hits, cands = extract_skills(text) print("命中:", hits) # 词典直接命中的技能 print("候选:", cands) # 需要人工或模型二次确认的候选

逻辑说明:jieba.load_userdict把技能词强制加入分词词典,保证"知识图谱"不会被切成"知识"和"图谱"。命中集合是可信技能,候选集合是名词和英文词,后续可以用一个简单的分类模型或人工规则过滤。参数上,flag过滤条件n/nz/eng覆盖了普通名词、专有名词和英文词,如果你发现候选里噪音太多,可以加长度阈值或词频阈值。

2.3 三元组构建与 Neo4j 入库

实体抽出来后要变成三元组。简历场景里典型的三元组是(求职者)-[掌握]->(技能)、(岗位)-[要求]->(技能)、(求职者)-[参与]->(项目)。用py2neo写入 Neo4j 是最常见的做法。

from py2neo import Graph, Node, Relationship graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) def build_triples(resume_id, skills, job_skills): """构建求职者-技能、岗位-技能两类三元组并入库""" person = Node("Person", id=resume_id) graph.merge(person, "Person", "id") # merge 避免重复节点 for s in skills: skill_node = Node("Skill", name=s) graph.merge(skill_node, "Skill", "name") graph.merge(Relationship(person, "掌握", skill_node)) for s in job_skills: skill_node = Node("Skill", name=s) graph.merge(skill_node, "Skill", "name") job_node = Node("Job", name="算法工程师") graph.merge(job_node, "Job", "name") graph.merge(Relationship(job_node, "要求", skill_node)) build_triples("R001", {"Python", "知识图谱"}, {"Python", "神经网络"})

这里用merge而不是create,是因为同一技能会被多个求职者引用,merge保证节点唯一。参数上,auth里的密码要和 Neo4j 服务端配置一致;Relationship的关系名用中文在展示时直观,但如果后续要用 Cypher 做复杂查询,建议统一成英文关系名如MASTER、REQUIRE,避免编码问题。

图谱建好后,可以用一句 Cypher 验证连通性:

MATCH (p:Person)-[:掌握]->(s:Skill)<-[:要求]-(j:Job) RETURN p.id, s.name, j.name

这条查询返回的是"求职者掌握的技能正好被某岗位要求"的匹配路径,也是后续推荐召回的基础。如果返回为空,先检查技能名是否大小写不一致,这是最常见的翻车点。

3. 人工神经网络怎么做人岗匹配:特征工程与模型训练

图谱解决了"关联显式化",但匹配强度需要神经网络来学。这一章讲清楚输入特征怎么构造、网络结构怎么选、训练时看什么指标。

3.1 把图谱特征转成神经网络输入向量

神经网络吃的是数值向量,所以要把图谱里的关系转成特征。常见做法是构造一个"人—岗"特征向量,维度包括:技能重合度、图谱路径数、学历匹配、项目相似度。

import numpy as np def build_feature_vector(person_skills, job_skills, graph): """构造人岗匹配特征向量""" person_set, job_set = set(person_skills), set(job_skills) # 特征1:技能 Jaccard 相似度 jaccard = len(person_set & job_set) / len(person_set | job_set + 1e-6) # 特征2:图谱中两跳可达路径数 query = """ MATCH (p:Person {id:$pid})-[:掌握]->(s:Skill)<-[:要求]-(j:Job {name:$jid}) RETURN count(s) AS overlap """ overlap = graph.run(query, pid="R001", jid="算法工程师").evaluate() # 特征3:技能数量差 diff = abs(len(person_set) - len(job_set)) return np.array([jaccard, overlap, diff], dtype=np.float32) vec = build_feature_vector(["Python", "知识图谱"], ["Python", "神经网络"], graph) print(vec)

逻辑说明:jaccard衡量技能集合的整体重合度,overlap是图谱里实际存在的匹配路径数,diff反映技能数量差距。这三个特征拼成向量后送入网络。参数上,1e-6是防止除零;overlap查询里的$pid、$jid是参数化查询,避免字符串拼接注入。

3.2 用 PyTorch 搭一个可训练的人岗匹配网络

网络结构不需要太深,毕业设计阶段一个三层全连接就够用,重点是能跑通训练和验证流程。

import torch import torch.nn as nn class MatchNet(nn.Module): def __init__(self, input_dim=3, hidden=16): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1), nn.Sigmoid() # 输出 0~1 的匹配概率 ) def forward(self, x): return self.net(x) model = MatchNet() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.BCELoss() # 二分类:匹配/不匹配 # 模拟一批训练数据:特征 + 标签 X = torch.tensor([[0.8, 3, 1], [0.2, 0, 5], [0.6, 2, 2]], dtype=torch.float32) y = torch.tensor([[1.0], [0.0], [1.0]], dtype=torch.float32) for epoch in range(200): pred = model(X) loss = criterion(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 50 == 0: print(f"epoch {epoch}, loss {loss.item():.4f}")

逻辑说明:输入维度 3 对应上一节的特征向量,隐藏层 16 是经验值,数据量小的时候不要设太大否则过拟合。Sigmoid把输出压到 0~1 当作匹配概率,BCELoss是二分类标准损失。参数上,lr=1e-3是 Adam 的常用起点,如果 loss 震荡就降到1e-4;epoch数量根据数据规模调整,毕业设计数据量通常几百到几千条,200 轮足够观察收敛趋势。

注意:标签数据是毕业设计里最容易被忽略的环节。如果没有真实投递反馈,可以用"技能重合度超过阈值即视为匹配"来生成弱标签,但要在论文里说明这是弱监督,不能宣称是真实业务标签。

3.3 训练过程要看哪些指标,怎么判断模型没白训

只看 loss 下降是不够的,人岗匹配是排序问题,要看 Top-K 命中率。做法是把测试集里每个求职者的岗位按预测概率排序,看真实匹配岗位是否落在前 K 个。

def top_k_hit(model, features, labels, k=3): """计算 Top-K 命中率""" model.eval() with torch.no_grad(): scores = model(features).squeeze() # 按分数降序取前 k 个索引 topk_idx = torch.topk(scores, k).indices hit = labels[topk_idx].sum().item() return hit / k hit_rate = top_k_hit(model, X, y, k=2) print(f"Top-2 命中率: {hit_rate:.2f}")

逻辑说明:model.eval()关闭 dropout 等训练态行为,torch.no_grad()省显存。topk取分数最高的 K 个,统计其中有多少是真实匹配。参数上,K 的选择取决于业务场景,推荐列表一般看 Top-5 或 Top-10;如果命中率低于随机水平,说明特征没区分度,要回到第 2 章检查图谱质量。

4. 系统集成与本地跑通:从图谱服务到推荐接口

算法跑通不等于系统能用。这一章把 Neo4j、PyTorch 模型和 Web 接口串起来,给出一套能在本地复现的最小系统。

4.1 环境依赖与目录结构

先把依赖固定下来,避免"在我电脑上能跑"的玄学问题。建议用虚拟环境隔离。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install pdfplumber jieba py2neo torch flask numpy

目录结构建议按职责分层,答辩时也好讲:

resume_rec/ ├── data/ # 原始简历和清洗后数据 ├── graph/ # 图谱构建脚本 │ ├── extract.py │ └── build_graph.py ├── model/ # 神经网络定义与训练 │ ├── net.py │ └── train.py ├── service/ # Flask 接口 │ └── app.py └── requirements.txt

4.2 用 Flask 暴露推荐接口

接口只做一件事:收到求职者 ID,返回排序后的岗位列表。

from flask import Flask, jsonify, request from py2neo import Graph import torch from model.net import MatchNet app = Flask(__name__) graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) model = MatchNet() model.load_state_dict(torch.load("model/match_net.pth")) model.eval() @app.route("/recommend", methods=["GET"]) def recommend(): pid = request.args.get("pid") # 从图谱取该求职者的技能 skills = graph.run( "MATCH (p:Person {id:$pid})-[:掌握]->(s:Skill) RETURN s.name", pid=pid ).data() skill_names = [s["s.name"] for s in skills] # 这里简化处理:用技能数量构造特征,实际应查岗位库逐个打分 feat = torch.tensor([[len(skill_names) / 10, len(skill_names), 0]], dtype=torch.float32) score = model(feat).item() return jsonify({"pid": pid, "skills": skill_names, "match_score": round(score, 4)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

逻辑说明:接口从图谱查技能,构造特征后过模型得到分数。参数上,host="0.0.0.0"让局域网可访问,方便答辩演示;port=5000是 Flask 默认端口。实际系统里应该遍历岗位库逐个打分再排序,这里为了最小可运行做了简化,扩展时把岗位查询放进循环即可。

4.3 端到端验证:一条命令确认系统通了

启动 Neo4j 和 Flask 后,用 curl 验证接口:

curl "http://localhost:5000/recommend?pid=R001"

返回 JSON 里能看到技能列表和匹配分数,说明图谱查询、模型推理、接口响应三个环节都通了。如果返回空技能列表,先查 Neo4j 里Person节点的id属性是否和请求参数一致;如果分数异常,检查模型加载的权重文件是否和当前网络结构匹配。

5. 避坑与排查:简历推荐系统最容易翻车的 5 个地方

这一章是我踩过的坑,每条按"现象 → 原因 → 解决"写,照着排查能省不少时间。

现象一:图谱里技能节点爆炸,同一个技能出现几十个重复节点。原因是大小写和空格没统一,"Python""python""Python "被当成三个实体。解决方式是在入库前做标准化:统一转小写、去首尾空格、全角转半角,再merge入库。

现象二:模型训练 loss 一直不降,准确率跟随机猜差不多。原因通常是特征没归一化,技能数量差这个特征取值范围是 0~20,而 Jaccard 是 0~1,量纲差距让网络学不动。解决方式是对每个特征做 min-max 归一化,或者用BatchNorm层。

现象三:Neo4j 查询越来越慢,推荐接口响应超过 3 秒。原因是没建索引,每次查询都在全图扫描。解决方式是对Person.id、Skill.name、Job.name建唯一约束和索引:

CREATE CONSTRAINT FOR (p:Person) REQUIRE p.id IS UNIQUE; CREATE INDEX FOR (s:Skill) ON (s.name);

现象四:简历 PDF 解析出来全是乱码或空行。原因是部分 PDF 是扫描件或用了特殊编码。解决方式是先判断文本抽取长度,低于阈值就标记为"需 OCR 处理",不要硬塞进图谱污染数据。

现象五:答辩演示时接口报错,本地却正常。原因是 Neo4j 密码或端口写死在代码里,换环境就失效。解决方式是把连接信息抽到配置文件或环境变量,演示前用os.getenv读取并做一次连通性自检。

6. 进阶技巧:用图谱路径特征提升推荐可解释性

到这一步系统能跑了,但毕业设计想拿高分,还得解决一个痛点:神经网络是黑匣子,答辩老师问"为什么推荐这个岗位"时不能只说"模型算出来的"。知识图谱的最大优势就是可解释性,把图谱路径作为推荐理由输出,既提升说服力又体现图谱价值。

具体做法是在推荐接口里加一段路径查询,返回"求职者—技能—岗位"的匹配链路:

def explain_recommendation(pid, job_name): """返回人岗匹配的可解释路径""" query = """ MATCH path = (p:Person {id:$pid})-[:掌握]->(s:Skill)<-[:要求]-(j:Job {name:$jid}) RETURN [node IN nodes(path) | coalesce(node.name, node.id)] AS chain LIMIT 5 """ result = graph.run(query, pid=pid, jid=job_name).data() return [r["chain"] for r in result] chains = explain_recommendation("R001", "算法工程师") print(chains) # 例如 [['R001', 'Python', '算法工程师'], ['R001', '知识图谱', '算法工程师']]

这段 Cypher 返回的是路径上所有节点的名称序列,前端可以渲染成"你因为掌握 Python 和知识图谱,匹配到算法工程师岗位"。参数上,LIMIT 5控制返回路径数量,避免路径太多刷屏;coalesce(node.name, node.id)兼容技能节点有name、求职者节点有id的情况。

再进一步,可以把路径长度和路径数量作为特征回灌到神经网络,让模型在学匹配分数的同时利用图结构信息。常见做法是统计 1 跳、2 跳、3 跳路径的数量,拼成向量追加到第 3 章的特征向量里。这样模型既有数值特征又有结构特征,Top-K 命中率通常能提升几个百分点。

路径长度含义对推荐的贡献
1 跳求职者直接掌握技能强信号,直接匹配
2 跳求职者技能被岗位要求核心匹配路径
3 跳技能通过中间技能关联弱信号,用于扩展召回

验证可解释性是否有效,可以做一个简单对比:随机抽 20 个推荐结果,人工判断路径理由是否合理,合理率超过 80% 就说明图谱质量过关。如果合理率低,回到第 2 章检查实体抽取的准确率,尤其是技能词典的覆盖度。

我自己的习惯是每改一次图谱构建逻辑,就重新跑一遍这 20 条抽样,把合理率记在实验日志里。这个习惯帮我抓出过好几次"技能词典把'熟悉'也当成技能"的低级错误。图谱和神经网络结合的系统,坑往往不在模型本身,而在数据进入模型之前的那几步。希望帮到你。

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