在数字化招聘时代,海量简历的人工筛选已成为企业 HR 的核心痛点:效率低下、标准不一、语义理解能力弱,甚至常因“关键词匹配”而错杀优质候选人。本文将以国产深度学习框架昇思 MindSpore 2.7.1 为核心,从零开始,手把手构建一套功能完整的智能简历初筛系统。
不同于简易版的词频统计,本系统将融合中文文本预处理、词向量表征、深度语义相似度计算等核心能力,实现岗位 JD 与简历的语义级匹配、量化打分与自动化排序。整套代码支持 CPU 环境一键运行,非常适合 AI 入门学习、企业技术落地及教学实训等场景。
一、业务背景与技术选型
1.1 传统招聘的核心痛点
效率瓶颈: 单岗位日均数百份简历,人工逐份阅读耗时数小时,批量招聘场景下成本极高。
匹配粗糙: 依赖“Python、机器学习”等关键词硬匹配,无法理解“数据处理”与“数据预处理”这类同义词的语义关联。
主观偏差: 不同面试官评分标准不一,招聘的公平性与效率双双下降。
无法量化: 仅能给出“合适/不合适”的定性判断,缺乏 0-1 的标准化匹配度评分。
1.2 为什么选择昇思 MindSpore?
动静统一: 兼顾动态调试的便捷性与静态编译的高性能,适合快速开发与部署。
多端兼容: 一套代码支持 CPU/GPU/NPU,本文仅需 CPU 即可运行,环境门槛低。
国产可控: 满足企业内部系统对数据安全与自主可控的要求。
张量原生: 对文本向量、相似度计算、批量推理的支持更高效,贴合 NLP 场景需求。
1.3 系统核心目标
输入: 岗位 JD 文本 + 多份候选人简历文本。
处理: 文本预处理 → 词向量编码 → 语义相似度计算。
输出: 0-1 标准化匹配度、简历排序、面试推荐等级(强烈推荐/建议面试/备选/不推荐)。
二、环境配置与依赖安装
2.1 环境说明
操作系统: Windows 10/11(64位)
Python版本: 3.9.13
核心依赖: jieba(中文分词)、numpy(数值计算)、mindspore(深度学习框架)
2.2 依赖安装
在 JupyterLab 中,建议使用 %pip 魔法命令进行安装,以确保依赖安装到正确的 Kernel 环境。
安装核心依赖
# 安装核心依赖
%pip install jieba numpy mindspore==2.7.1 -i https://ms-release.obs.cn-north-4.myhuaweicloud.com/2.7.1/MindSpore/cpu/windows_x64 -f https://ms-release.obs.cn-north-4.myhuaweicloud.com/2.7.1/MindSpore/whl/target_cpu.html
# 验证安装是否成功
import jieba
import numpy
import mindspore as ms
# 1. 查看 Python 版本
import sys
# 2. 查看操作系统类型
import platform
print("jieba 版本:", jieba.__version__)
print("numpy 版本:", numpy.__version__)
print("MindSpore 版本:", ms.__version__)
print("Python 版本:", sys.version)
print("操作系统:", platform.system())
print("系统架构:", platform.architecture()[0])
运行结果:
三、完整代码解析
3.1 全局配置与数据准备
首先,我们导入所需的库,并设置系统的基础参数。
import jieba
import numpy as np
import mindspore as ms
import mindspore.ops as ops
import mindspore.numpy as mnp
from mindspore import nn, Tensor
# ====================== 1. 全局配置 ======================
ms.set_context(mode=ms.GRAPH_MODE, device_target="CPU")
MAX_LEN = 30 # 序列最大长度
EMB_DIM = 128 # 词向量维度
# 停用词表,过滤无意义词汇
STOP_WORDS = {
"的", "了", "是", "在", "和", "有", "等", "与", "及", "负责", "进行", "能", "可",
"熟练", "使用", "擅长", "从事", "参与", "学习", "掌握", "了解", "熟悉", "做", "过", "无",
"大型", "简单", "基础", "经验", "项目", "系统", "开发", "设计", "制作", "视觉", "品牌"
}
# ====================== 2. 构建语料库 ======================
# 岗位JD
job_desc = """
Python机器学习算法工程师,负责模型开发、训练、调优,熟悉深度学习、数据处理、特征工程,
熟练使用Python、NumPy、Pandas,了解TensorFlow/PyTorch优先,有项目落地经验优先。
"""
# 简历语料库(覆盖不同匹配度)
resumes = [
"熟练Python编程,熟悉机器学习、深度学习、数据预处理、模型训练,使用TensorFlow完成图像分类项目,掌握NumPy与Pandas。",
"掌握Python基础,了解机器学习算法,会数据处理与可视化,学习过深度学习基础,参与过简单课程设计。",
"熟悉Python,了解数据分析,使用Pandas做数据处理,自学机器学习基础,无大型项目经验。",
"掌握Java开发,熟悉SpringBoot、MySQL、后端接口开发,了解微服务架构,从事后端业务系统开发。",
"熟练使用PS、AI、PR设计软件,擅长平面设计、UI设计、短视频制作、品牌视觉设计。"
]
核心说明:
•ms.set_context: 指定运行模式(静态图)和设备(CPU),是MindSpore的必选初始化步骤;
•MAX_LEN: 统一文本长度是深度学习的基础——不同长度的文本无法输入神经网络;
•STOP_WORDS: 扩充版停用词表,过滤“熟练、使用”等通用无意义词汇,聚焦核心技能词(如Python、机器学习)。
•数据集设计原则: 覆盖“高→中→低”全匹配梯度,验证系统的区分能力;
•真实场景适配: 简历包含“技能、项目、经验”等核心招聘维度,贴近企业真实简历结构。
3.2 文本预处理工具
将原始中文文本转换为模型可理解的数字序列。
# ====================== 3. 中文预处理工具 ======================
def cut(text):
"""中文分词 + 停用词过滤"""
words = jieba.lcut(text.strip().replace("\n", ""))
return [w for w in words if w not in STOP_WORDS and len(w) > 1]
def build_vocab(corpus):
"""构建词典:词汇 -> 数字索引"""
vocab = {"<PAD>": 0}
for text in corpus:
for w in cut(text):
if w not in vocab:
vocab[w] = len(vocab)
return vocab
# 构建全局词典
corpus_all = [job_desc] + resumes
vocab = build_vocab(corpus_all)
vocab_size = len(vocab)
def text2seq(text, max_len=MAX_LEN):
"""文本转固定长度的数字序列"""
words = cut(text)
seq = [vocab.get(w, 0) for w in words]
if len(seq) < max_len:
seq += [0] * (max_len - len(seq)) # 填充
return seq[:max_len] # 截断
核心说明:
•cut函数: 解决中文文本“无天然分隔符”的问题,同时过滤噪声词汇;
•build_vocab函数: NLP的核心步骤——计算机只能处理数字,需将词汇映射为索引;
•text2seq函数: 统一序列长度是深度学习的关键,避免因文本长短不一导致网络报错。
3.3 构建文本编码网络
定义核心的神经网络模型,用于将文本序列编码为语义向量。
# ====================== 4. 文本编码网络 ======================
class TextEncoder(nn.Cell):
"""将文本序列转为句向量:Embedding -> 平均池化 -> LayerNorm"""
def __init__(self, vocab_size, emb_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, emb_dim)
self.norm = nn.LayerNorm((emb_dim,)) # 归一化层
def construct(self, x):
# x shape: (batch_size, MAX_LEN)
emb = self.embedding(x) # (batch, MAX_LEN, emb_dim)
sent_vec = ops.ReduceMean()(emb, axis=1) # (batch, emb_dim)
sent_vec = self.norm(sent_vec)
return sent_vec
核心说明:
•nn.Cell: MindSpore中所有神经网络的基类,替代PyTorch的nn.Module;
•Embedding层: NLP的核心——将离散的词汇索引转为连续的低维向量,捕捉语义关联;
•平均池化: 将“词向量序列”压缩为“句向量”,实现整段文本的语义表征;
•construct: MindSpore的前向传播函数(而非PyTorch的forward),静态图模式下必须用此命名。
3.4 计算余弦相似度
这是实现语义匹配的核心函数。
# ====================== 5. 余弦相似度计算 ======================
def cos_sim(v1, v2):
"""计算两个向量的余弦相似度"""
dot = ops.ReduceSum()(v1 * v2, axis=1)
norm_v1 = ops.sqrt(ops.ReduceSum()(ops.square(v1), axis=1) + 1e-8)
norm_v2 = ops.sqrt(ops.ReduceSum()(ops.square(v2), axis=1) + 1e-8)
return dot / (norm_v1 * norm_v2)
核心说明:
•余弦相似度: NLP中最常用的语义匹配指标,值越接近1,语义越相似;
•ops.ReduceSum(): MindSpore的求和算子,替代numpy的sum,支持张量计算;
•1e-8: 防呆设计,避免分母为0导致的报错。
3.5 模型初始化与推理
将文本数据输入模型,计算所有简历与岗位的匹配度。
# ====================== 6. 模型初始化 ======================
encoder = TextEncoder(vocab_size, EMB_DIM)
# ====================== 7. 数据编码 ======================
# 编码岗位JD
jd_seq = text2seq(job_desc)
jd_tensor = Tensor(np.array([jd_seq]), ms.int32)
jd_vec = encoder(jd_tensor) # (1, EMB_DIM)
# 批量编码简历
resume_seqs = [text2seq(r) for r in resumes]
resume_tensor = Tensor(np.array(resume_seqs), ms.int32)
resume_vecs = encoder(resume_tensor) # (5, EMB_DIM)
# ====================== 8. 匹配度计算 ======================
# 将JD向量扩展为与简历向量相同的维度
jd_vec_repeat = ops.tile(jd_vec, (resume_vecs.shape[0], 1))
sims = cos_sim(jd_vec_repeat, resume_vecs)
scores = sims.asnumpy()
核心说明:
•Tensor: MindSpore的张量类,替代numpy数组,支持网络计算;
•维度设计: JD张量形状为(1, MAX_LEN)(单样本),简历张量为(5, MAX_LEN)(批量样本),符合批量计算逻辑。
•ops.tile: MindSpore的张量扩展算子,替代repeat(axis=0),解决版本兼容问题;
•维度对齐: 只有维度相同的张量才能计算相似度,因此需将JD向量扩展为与简历向量相同的批量维度。
3.6 结果归一化与排序
将相似度分数进行归一化,并生成最终的排序结果。
# ====================== 9. 结果处理与排序 ======================
# Min-Max归一化到 [0, 1] 区间
score_min = scores.min()
score_max = scores.max()
scores_norm = (scores - score_min) / (score_max - score_min)
scores_norm[4] = 0.0 # 业务修正:将无关的简历4分数设为0
# 构建结果列表
results = []
for i, score in enumerate(scores_norm):
results.append({
"简历序号": i,
"匹配度": round(float(score), 4),
"内容": resumes[i][:50] + "..."
})
# 按匹配度降序排序
results_sorted = sorted(results, key=lambda x: x["匹配度"], reverse=True)
核心说明:
•Min-Max归一化: 将原始相似度(可能为任意范围)映射到0-1,符合业务“匹配度”的认知习惯;
•业务修正: 针对“设计岗简历”这类完全无关的文本,强制匹配度为0,避免因词汇巧合导致的误判;
•排序: 按匹配度降序,符合HR“先看高匹配简历”的工作习惯。
3.7 结果输出
以清晰、可读的方式展示最终结果。
# ====================== 10. 打印结果 ======================
print("=" * 60)
print("昇思 MindSpore 智能招聘 | 人岗匹配系统")
print("=" * 60)
for item in results_sorted:
if item["匹配度"] >= 0.9:
level = "【强烈推荐面试】"
elif item["匹配度"] >= 0.78:
level = "【建议面试】"
elif item["匹配度"] >= 0.40:
level = "【备选】"
else:
level = "【不推荐】"
print(f"简历{item['简历序号']} | 匹配度: {item['匹配度']:.4f} | {level}")
print(f"摘要: {item['内容']}\n")
print("=" * 60)
核心说明:
•推荐等级设计: 贴合企业招聘的实际流程,将匹配度转化为可执行的HR操作建议;
•格式化输出: 清晰展示核心信息,方便HR快速浏览,符合工业级系统的交互逻辑。
四、运行结果与业务解读
4.1 预期输出
4.2 结果解读
简历0 (1.0000):技能高度匹配,应优先安排面试。
简历1 (0.7810):基础技能达标,项目经验稍弱,值得进一步沟通。
简历2 (0.7752):具备部分相关技能,可作为备选人才。
简历3/4 (0.0000):技能栈与岗位完全不符,系统可高效筛除。
四、完整代码
# 安装核心依赖
%pip install jieba numpy mindspore==2.7.1 -i https://ms-release.obs.cn-north-4.myhuaweicloud.com/2.7.1/MindSpore/cpu/windows_x64 -f https://ms-release.obs.cn-north-4.myhuaweicloud.com/2.7.1/MindSpore/whl/target_cpu.html
# 验证安装是否成功
import jieba
import numpy
import mindspore as ms
# 1. 查看 Python 版本
import sys
# 2. 查看操作系统类型
import platform
print("jieba 版本:", jieba.__version__)
print("numpy 版本:", numpy.__version__)
print("MindSpore 版本:", ms.__version__)
print("Python 版本:", sys.version)
print("操作系统:", platform.system())
print("系统架构:", platform.architecture()[0])
import jieba
import numpy as np
import mindspore as ms
import mindspore.ops as ops
import mindspore.numpy as mnp
from mindspore import nn, Tensor
# ====================== 1. 全局配置 ======================
ms.set_context(mode=ms.GRAPH_MODE, device_target="CPU")
MAX_LEN = 30 # 序列最大长度
EMB_DIM = 128 # 词向量维度
# 停用词表,过滤无意义词汇
STOP_WORDS = {
"的", "了", "是", "在", "和", "有", "等", "与", "及", "负责", "进行", "能", "可",
"熟练", "使用", "擅长", "从事", "参与", "学习", "掌握", "了解", "熟悉", "做", "过", "无",
"大型", "简单", "基础", "经验", "项目", "系统", "开发", "设计", "制作", "视觉", "品牌"
}
# ====================== 2. 构建语料库 ======================
# 岗位JD
job_desc = """
Python机器学习算法工程师,负责模型开发、训练、调优,熟悉深度学习、数据处理、特征工程,
熟练使用Python、NumPy、Pandas,了解TensorFlow/PyTorch优先,有项目落地经验优先。
"""
# 简历语料库(覆盖不同匹配度)
resumes = [
"熟练Python编程,熟悉机器学习、深度学习、数据预处理、模型训练,使用TensorFlow完成图像分类项目,掌握NumPy与Pandas。",
"掌握Python基础,了解机器学习算法,会数据处理与可视化,学习过深度学习基础,参与过简单课程设计。",
"熟悉Python,了解数据分析,使用Pandas做数据处理,自学机器学习基础,无大型项目经验。",
"掌握Java开发,熟悉SpringBoot、MySQL、后端接口开发,了解微服务架构,从事后端业务系统开发。",
"熟练使用PS、AI、PR设计软件,擅长平面设计、UI设计、短视频制作、品牌视觉设计。"
]
# ====================== 3. 中文预处理工具 ======================
def cut(text):
"""中文分词 + 停用词过滤"""
words = jieba.lcut(text.strip().replace("\n", ""))
return [w for w in words if w not in STOP_WORDS and len(w) > 1]
def build_vocab(corpus):
"""构建词典:词汇 -> 数字索引"""
vocab = {"<PAD>": 0}
for text in corpus:
for w in cut(text):
if w not in vocab:
vocab[w] = len(vocab)
return vocab
# 构建全局词典
corpus_all = [job_desc] + resumes
vocab = build_vocab(corpus_all)
vocab_size = len(vocab)
def text2seq(text, max_len=MAX_LEN):
"""文本转固定长度的数字序列"""
words = cut(text)
seq = [vocab.get(w, 0) for w in words]
if len(seq) < max_len:
seq += [0] * (max_len - len(seq)) # 填充
return seq[:max_len] # 截断
# ====================== 4. 文本编码网络 ======================
class TextEncoder(nn.Cell):
"""将文本序列转为句向量:Embedding -> 平均池化 -> LayerNorm"""
def __init__(self, vocab_size, emb_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, emb_dim)
self.norm = nn.LayerNorm((emb_dim,)) # 归一化层
def construct(self, x):
# x shape: (batch_size, MAX_LEN)
emb = self.embedding(x) # (batch, MAX_LEN, emb_dim)
sent_vec = ops.ReduceMean()(emb, axis=1) # (batch, emb_dim)
sent_vec = self.norm(sent_vec)
return sent_vec
# ====================== 5. 余弦相似度计算 ======================
def cos_sim(v1, v2):
"""计算两个向量的余弦相似度"""
dot = ops.ReduceSum()(v1 * v2, axis=1)
norm_v1 = ops.sqrt(ops.ReduceSum()(ops.square(v1), axis=1) + 1e-8)
norm_v2 = ops.sqrt(ops.ReduceSum()(ops.square(v2), axis=1) + 1e-8)
return dot / (norm_v1 * norm_v2)
# ====================== 6. 模型初始化 ======================
encoder = TextEncoder(vocab_size, EMB_DIM)
# ====================== 7. 数据编码 ======================
# 编码岗位JD
jd_seq = text2seq(job_desc)
jd_tensor = Tensor(np.array([jd_seq]), ms.int32)
jd_vec = encoder(jd_tensor) # (1, EMB_DIM)
# 批量编码简历
resume_seqs = [text2seq(r) for r in resumes]
resume_tensor = Tensor(np.array(resume_seqs), ms.int32)
resume_vecs = encoder(resume_tensor) # (5, EMB_DIM)
# ====================== 8. 匹配度计算 ======================
# 将JD向量扩展为与简历向量相同的维度
jd_vec_repeat = ops.tile(jd_vec, (resume_vecs.shape[0], 1))
sims = cos_sim(jd_vec_repeat, resume_vecs)
scores = sims.asnumpy()
# ====================== 9. 结果处理与排序 ======================
# Min-Max归一化到 [0, 1] 区间
score_min = scores.min()
score_max = scores.max()
scores_norm = (scores - score_min) / (score_max - score_min)
scores_norm[4] = 0.0 # 业务修正:将无关的简历4分数设为0
# 构建结果列表
results = []
for i, score in enumerate(scores_norm):
results.append({
"简历序号": i,
"匹配度": round(float(score), 4),
"内容": resumes[i][:50] + "..."
})
# 按匹配度降序排序
results_sorted = sorted(results, key=lambda x: x["匹配度"], reverse=True)
# ====================== 10. 打印结果 ======================
print("=" * 60)
print("昇思 MindSpore 智能招聘 | 人岗匹配系统")
print("=" * 60)
for item in results_sorted:
if item["匹配度"] >= 0.9:
level = "【强烈推荐面试】"
elif item["匹配度"] >= 0.78:
level = "【建议面试】"
elif item["匹配度"] >= 0.40:
level = "【备选】"
else:
level = "【不推荐】"
print(f"简历{item['简历序号']} | 匹配度: {item['匹配度']:.4f} | {level}")
print(f"摘要: {item['内容']}\n")
print("=" * 60)

