ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的简历智能推荐算法:从TF-IDF到余弦相似度的实践指南

基于Python的简历智能推荐算法:从TF-IDF到余弦相似度的实践指南 简介基于Python实现简历智能推荐算法是一套面向课程设计、毕业设计以及NLP与机器学习初学者的完整项目资源。该项目聚焦招聘场景中的简历与职位描述自动匹配涵盖文本清洗、去停用词、词形还原等NLP预处理步骤以及TF-IDF、词嵌入等特征工程方法模型部分对比朴素贝叶斯、SVM、决策树、CNN、Bi-LSTM等常用算法并给出超参数调优、交叉验证的实践思路同时融入在线学习与协同过滤以提升推荐准确性。压缩包共337个文件含19个Python源码、17个pkl模型文件、hdf5与checkpoint训练权重以及HTML结果展示页、Markdown说明文档和毕业论文Word稿资源目录按源码、模型、文档分层整体大小127.43MB方便从数据处理到模型部署全流程参考。已有485人学习浏览适合希望快速搭建简历推荐原型、理解NLP与机器学习落地细节的读者。1. 简历智能推荐的第一课先让机器“看明白”简历再谈算法一个很反直觉的结论简历智能推荐算法这类项目真正决定成败的往往不是推荐模型而是前面那段没人愿意仔细做的文本解析和词库建设。我见过太多同学装好 scikit-learn调了两天参最后发现中文分词把“Java开发”切成了“Java / 开发”把“Vue”当成一句英文滤掉了推荐结果自然一言难尽。这里说的基于 Python 的简历智能推荐算法本质上是一个“岗位 JD 当查询、简历库当文档”的检索排序问题输入一份岗位描述从几百几千份简历里找出最匹配的 Top N并且让 HR 一眼知道“为什么推荐他”。适合正在做招聘系统、内部人才库的开发者也适合拿来做实训课设的中级 Python 学员。这篇会把链路拆开从语料准备讲到相似度计算附上能直接跑的最小代码和参数边界。2. 整体链路与技术选型为什么简历推荐的核心是“职位语义空间”2.1 先把推荐问题拆成“召回、粗排、精排”的轻量版商品推荐系统的经典三段式在简历推荐里完全可以做轻量映射。召回阶段的任务是把明显不合适的简历过滤掉比如岗位要求在北京简历期望城市却在杭州这一层用简单规则就能完成没必要让算法介入。粗排阶段才是算法的主场对通过召回的简历做文本相似度计算给出一个初步分数。精排阶段要叠加业务字段比如工作年限、学历、当前状态等把算法分数和业务规则混合排序。这个拆法有一个现实原因简历库的规模通常不会太大。内部招聘系统里几千份简历、招聘平台里几十万份简历单机内存都能扛得住不需要一上来就上 Spark、上向量数据库。最朴素的方案往往是维护成本最低的。你在做技术选型时先问自己一个问题这份简历推荐是给谁用的HR 在招聘后台点开一个岗位系统要在几百毫秒内返回候选名单这就决定了初版算法必须够快、够简单、够容易解释。2.2 为什么第一版选“TF-IDF 余弦相似度”而不是神经网络简历推荐常见的算法路线有这么几条纯规则匹配、TF-IDF 向量加余弦相似度、Word2Vec 平均向量、BERT/BGE 这类预训练向量模型。纯规则匹配最简单简历里出现“Python”就给 2 分出现“Java”就给 3 分但它的天花板很低因为简历文本的措辞千变万化“熟悉 Linux 脚本”和“掌握 Shell 编程”词面完全不同意思却一样。Word2Vec 需要语料训练几十份简历根本训不出像样的词向量用公开词向量又经常遇到未登录词。BERT/BGE 这类模型效果确实好但需要 GPU 推理服务普通开发机跑批量打分延迟和内存都吃紧第一版就上重模型会让项目周期拉长好几倍。所以 TF-IDF 加余弦相似度是简历推荐最靠谱的基线方案几行代码能跑通结果可解释后期要升级也只是把向量化那一层替换成深度模型上层排序逻辑不用改。有个点需要提前说明TF-IDF 解决的是“词面相似”它分不清“三年 Java 后端”和“三年 Java 前端”的差别。这个问题要靠后文的技能命中特征和业务字段精排来补不是换模型能一步到位的。2.3 五段式流水线从原始简历到推荐列表的完整路径我一般会把简历推荐系统拆成五个阶段解析、清洗、特征化、相似度计算、精排输出。解析解决“读文件”的问题把 PDF、DOCX、纯文本里的内容抽出来清洗解决“脏数据”的问题去掉电话、邮箱、无意义的换行符特征化解决“变成向量”的问题包括中文分词、停用词过滤、TF-IDF 向量化相似度计算负责给每一份简历打分精排负责叠加经验年限、城市这些业务规则。搭建环境这一步没有太多玄学用 Python 3.9 以上的版本在虚拟环境里把必要依赖装好即可。如果你还在纠结 IDE用 VSCode 把 Python 环境配置好能跑通import jieba就够了不一定要用 PyCharm 全家桶。# 创建虚拟环境并安装依赖 python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install jieba scikit-learn pandas pypdf说明一下jieba负责中文分词scikit-learn提供 TF-IDF 向量化和余弦相似度计算pypdf用来解析 PDF 简历文本pandas用于后续的数据回看和结果分析。安装时如果遇到网络慢的问题可以换成国内镜像源但不要把依赖版本锁得太死这几个库的接口在常用版本里差别不大。装完后先跑一个最小的冒烟测试# 验证环境是否可用的冒烟测试 import jieba from sklearn.feature_extraction.text import TfidfVectorizer print(jieba.lcut(熟悉Python和爬虫开发)) vec TfidfVectorizer() X vec.fit_transform([熟悉Python, 熟悉Java]) print(X.shape) # 正常应该输出 (2, 3)这里如果输出(2, 3)说明环境和两个核心库都正常。不要小看这一步环境问题占简历推荐项目初期踩坑的三分之一提前确认能省很多时间。3. 简历语料与技能词库把原始简历变成可计算的文档集3.1 数据从哪里来合规收集与字段设计是第一步简历推荐的第一步不是写算法而是准备好一批能用的简历文本。这里要特别强调合规问题简历属于个人敏感信息不能从公开网络随意抓取也不能拿公司真实简历库直接做实验。常见做法有三种。第一用自己或同事脱敏后的模拟数据字段全部替换成假名第二用公开的简历样例数据集第三和业务方确认后使用内部投递库中已经授权用于算法实验的简历子集。我见过不少项目一上来就想做采集、做可视化大屏其实这些都是后话。先把数据通路打通才有资格谈效果。简历数据在进入算法之前至少要包含下面这些字段字段含义示例是否进入算法特征resume_id简历唯一 IDR0001否name姓名脱敏张**否phone电话脱敏138****0000否email邮箱脱敏zh**xx.com否skill_text技能描述原文熟悉Python、Flask是exp_year工作年限3是精排city期望城市北京是精排intent求职意向岗位Python后端是召回过滤字段设计的原则是“算法字段和展示字段分离”。skill_text是给向量化用的原始文本exp_year和city是给精排用的结构化字段。不要在早期就把学历、薪资期望、到岗时间全部塞进特征里特征越少越容易排查问题。脱敏处理建议在数据入库时就完成正则替换是最快的方式# 简历脱敏把手机号和邮箱替换成占位符 import re def desensitize(text: str) - str: text re.sub(r(?!\d)1[3-9]\d{9}(?!\d), [手机号], text) text re.sub(r[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}, [邮箱], text) return text sample 联系电话13812340000邮箱zhangsanexample.com print(desensitize(sample))正则里(?!\d)和(?!\d)是负向断言避免把 12 位号码从中间截断也避免把身份证号里的连续数字误判成手机号。这一步处理完的数据才能进入后续的解析流程。真实项目中脱敏逻辑比这个复杂可能还要处理微信号、QQ 号、座机等但核心思路是一致的先替换敏感信息再做任何计算。3.2 技能词库构建按“岗位族”组织而不是按原始字符串匹配技能词库是简历推荐里最容易被低估的部分。很多初版方案直接把技能做成一个列表简历文本里in一下就完事。这样做的直接问题是词面变体太多一个简单的“前端框架”可以有“Vue”“vue.js”“Vue3”“vue3”四种写法靠字符串匹配要么漏掉要么误伤。我的做法是给技能词库分三层。第一层是岗位族比如python、java、frontend、data第二层是技能主词比如Python、Spring Boot、Vue第三层是别名表把常见写法归并到主词下。这样做的好处是推荐结果可以带出“这个候选人的技能属于哪个岗位族”而不仅仅是“他命中了一个词”。实际构建词库时有三条路径可以并行。第一从历史岗位 JD 里提取高频技术词人工复核后入库第二从公开的技术栈清单里整理一份常用词表第三从推荐结果的反例里回收比如某次误推是因为把“docker”当成了“doc”这个词就该进词库修正。词库本身不追求大而全覆盖率 80% 比准确率 99% 重要因为漏词会导致简历特征稀疏误词会把推荐结果带偏。3.3 用脚本快速造模拟简历保证可复现的实验基础没有正式数据时可以写一个小的数据生成脚本造出 200 份结构可控的简历。这样做的目的不是骗自己而是让后续每一个模块都有确定的输入输出能跑单元测试。# 生成模拟简历数据保证实验可复现 import json import random SKILL_POOL { python: [python, flask, fastapi, pandas, numpy], java: [java, spring, mybatis, kafka], frontend: [vue, react, webpack, typescript], data: [sql, spark, hive, clickhouse], } def make_resume(idx: int) - dict: major random.choice(list(SKILL_POOL.keys())) skills set(SKILL_POOL[major]) # 每个候选人再随机搭配 1~2 个其他岗位族技能制造关联噪音 for _ in range(random.randint(1, 2)): other random.choice(list(SKILL_POOL.keys())) skills | set(random.sample(SKILL_POOL[other], k1)) return { resume_id: fR{idx:04d}, skill_text: , .join(sorted(skills)), exp_year: random.randint(0, 12), city: random.choice([北京, 上海, 广州, 深圳, 杭州]), intent: major, } random.seed(20240701) # 固定种子保证每次生成结果一致 resumes [make_resume(i) for i in range(200)] with open(mock_resumes.json, w, encodingutf-8) as f: json.dump(resumes, f, ensure_asciiFalse, indent2) print(f已生成 {len(resumes)} 份模拟简历)参数说明random.seed固定下来保证每次跑出的数据集完全一致否则后续调参时换了一批数据很难判断效果变化是来自代码还是来自随机波动。SKILL_POOL里故意让技能之间有一定交叉比如 Python 岗位的人可能也会一点 SQL这更接近真实场景——纯粹的一对一技能匹配反而是最容易做的情况。intent字段记录真实求职意向后面可以用来评估推荐结果的准确率。4. 从简历文本到特征向量解析、分词、技能命中与 TF-IDF 向量化4.1 先把简历读成干净的纯文本PDF、DOCX 与边界情况简历文件格式五花八门第一版建议只支持纯文本格式有限但能快速打通流程。如果你需要用真实简历做实验优先处理 TXT 和 DOCXPDF 留到后面再说因为 PDF 解析的坑尤其多。下面这段代码负责把原始文本洗成干净的语料# 简历文本清洗去空行、去噪声、统一换行 import re def clean_text(raw: str) - str: # 按行处理去掉首尾空白过滤掉只剩下标点的空行 lines [] for line in raw.splitlines(): line line.strip() if len(line) 1: # 过滤单个字符的无意义行 lines.append(line) return \n.join(lines) raw 张三 熟悉 Python、Flask有 3 年后端开发经验。 电话13812340000 print(clean_text(raw))逻辑说明先按splitlines()切分原始文本再逐行strip()去掉首尾空格最后把长度小于等于 1 的行直接丢掉因为简历里经常出现单独的空格行、制表符行这些内容进向量化只增加噪音。len(line) 1这个阈值不能设得太高中文简历里有的标题就两个字比如“教育”“项目”过滤掉了会影响语义完整度。PDF 解析建议单独封装一个函数用pypdf的extract_text()方法提取文本。但要记住两个边界。第一扫描版 PDF 没有文本层提取结果是空字符串这时候必须走 OCR成本高且容易出错第二双栏排版的 PDF 提取出来的文本顺序是错乱的左栏读一半跳到右栏语义完全断裂。所以初版对 PDF 的策略是能提取就用提取后检查字符数少于 200 字的直接打标人工补录不要硬喂给算法。4.2 中文分词的两个关键坑专有名词保护与自定义词典分词是简历文本特征化的核心环节。jieba默认词典对通用中文支持尚可但遇到技术名词就很吃力。我见过最典型的例子是“Three.js”被切成“Three / js”而“js”是停用词直接丢掉技术栈信息就没了。解决这种问题只有一个可靠路径把技能词库灌进jieba的用户词典。# 加载技能词库为 jieba 用户词典 import jieba def build_userdict(skills: list[str], out_path: str skill_dict.txt) - None: 把技能词写成语料格式加载进 jieba。 with open(out_path, w, encodingutf-8) as f: for skill in skills: # 格式词 词频 词性词频给 10 表示保持词的独立性 f.write(f{skill} 10 nz\n) jieba.load_userdict(out_path) skills [python, flask, three.js, vue, spring boot] build_userdict(skills) text 熟悉three.js和spring boot开发 print(jieba.lcut(text)) # 正确输出应包含 [three.js, spring boot]而不是被切开词频参数给 10 是一个经验值表示这个词在语料中很常见强制保留。词性标注用nz代表“其他专名”。这里的关键是技能的写法要和简历里的写法完全一致技能词库里有“three.js”简历里写“threejs”就失效了所以别名表很重要。另外要注意jieba.load_userdict必须在第一次分词之前调用如果你在交互式环境里已经分过一次词需要重启内核再加载。分词之后技能命中就是一次普通的字符串匹配了。我习惯在向量化之前先算一套技能命中向量用来给后续精排做证据支撑# 基于技能词库做命中检测返回命中列表 def hit_skills(text: str, skills: set[str]) - list[str]: text_lower text.lower() hits [] for skill in skills: # 统一转小写再判断避免大小写不一致导致漏匹配 if skill.lower() in text_lower: hits.append(skill) return hits print(hit_skills(熟悉 Flask 和 Vue, {flask, vue, django}))这个函数看起来简单但很容易写错忘记转小写是常见翻车点用而不是in是另一个高频翻车点会把“Python开发”里的“python”漏掉。hit_skills的输出要保留到后续步骤它既是推荐结果的解释材料也是技能特征的一个维度。4.3 文本向量化的三种方案全文本 TF-IDF、技能 OneHot、两者拼接分词完成后进入特征向量化环节。处理文本向量的主流思路是把整份简历文本作为特征来源走在前面做结构化的会先抽取技能再字典匹配。这里把两条路径放一起看更适合初版对比选型。方案特征来源优点缺点适用阶段全文本 TF-IDF分词后的完整简历不需要额外标注常见语义能体现同义词、否定词处理差第一版基线技能 OneHot手工构造技能命中向量可解释性强召回准确漏词即漏特征无泛化配合基线使用向量拼接TF-IDF 叠加技能命中兼顾语义与业务知识向量维度大调参成本高数据量上来后TF-IDF 在简历推荐里有一个必须开的参数sublinear_tfTrue因为它能把词频的爆炸式增长压缩成对数增长。一份简历里“Python”出现 10 次和出现 3 次对语义的重要性差别远没有 10 比 3 那么大。from sklearn.feature_extraction.text import TfidfVectorizer jd_texts [招聘Python后端工程师熟悉Flask优先] resume_texts [熟悉Python和Flask开发, 三年Java开发经验] # 两份简历一份 JD组合后统一 fit保证同一套词表 all_texts jd_texts resume_texts vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), sublinear_tfTrue, stop_words[熟悉, 开发, 经验], ) X vectorizer.fit_transform(all_texts) jd_vec X[0] resume_vecs X[1:] print(特征矩阵形状:, X.shape)参数选择逻辑如下。max_features5000限制词表大小防止简历库大起来后内存爆炸ngram_range(1, 2)把相邻两个词的组合也纳入特征比如“大数据开发”这种三字词在 unigram 下会被切碎但在 bigram 下能保留“开发”stop_words用于去掉“熟悉”“开发”这类对区分度没有帮助的通用词。技能命中向量的做法是直接用MultiLabelBinarizer把每份简历命中的技能列表转成向量和 TF-IDF 向量横向拼接。拼接的时候用scipy.sparse.hstack注意保持稀疏矩阵格式不要转成 dense否则简历量过万之后内存直接拉满。5. 用余弦相似度实现 Top-N 推荐最小可运行的推荐代码5.1 相似度计算与最近邻从 JD 到简历的余弦距离简历推荐的核心计算其实很简单把 JD 的向量和每份简历的向量做余弦相似度取 Top-N。scikit-learn提供了cosine_similarity直接对矩阵操作不用自己写循环。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # jd_vec: 1 x D 矩阵, resume_vecs: N x D 矩阵 sim_matrix cosine_similarity(jd_vec, resume_vecs) sim_scores sim_matrix[0] # 拿到一份 JD 对应所有简历的分数 top_n 10 min_score 0.10 # 相似度低于阈值的简历不推避免冷启动硬凑 top_indices np.argsort(sim_scores)[::-1] # 从高到低排序 candidates [] rank 0 for idx in top_indices: if sim_scores[idx] min_score: break # 后面的分数只会更低直接截断 candidates.append((idx, float(sim_scores[idx]))) rank 1 if rank top_n: break for idx, score in candidates: print(f简历 {idx}: 相似度 {score:.4f})这段代码里要重点理解np.argsort()返回的是索引数组而不是分数本身所以[::-1]是把索引按分数降序排列。min_score阈值要有否则系统没有任何可推荐简历时会硬推一份相似度只有 0.02 的候选人HR 看到推荐质量差对整个系统的信任度会大幅下降。对于全零向量余弦相似度定义为 0这时候它也会排到队伍后面不会误导排序。这里有个性能细节cosine_similarity一次计算全部 pairwise 相似度。简历量上万时这个矩阵会占很大内存可以在计算前先用技能召回粗筛一遍只对粗筛后的几百份简历计算相似度。5.2 排序融合把相似度、工作年限、城市意愿组合成最终排序纯相似度排行的最大问题是把业务规则全部丢弃了。一份简历技术栈和 JD 完全匹配但候选人已经 12 年经验且期望在成都JD 要求 3 年经验且在北京这种简历排第一就是笑话。我的习惯是把相似度作为基础分再叠加经验匹配分和城市匹配分。# 最终排序相似度 业务字段加权 def final_score(sim: float, exp_year: float, jd_exp: float, city_match: bool, w_sim: float 0.7, w_exp: float 0.2, w_city: float 0.1) - tuple: # 经验分0~1JD 要求 3 年候选人 3 年得满分超过 6 年不再加分 if jd_exp 0: exp_score 1.0 else: exp_score max(0.0, 1 - abs(exp_year - jd_exp) / max(jd_exp, 1)) exp_score min(exp_score, 1.0) city_score 1.0 if city_match else 0.0 score w_sim * sim w_exp * exp_score w_city * city_score return score, exp_score, city_score # 假设某候选人 score, exp_score, city_score final_score( sim0.45, exp_year2, jd_exp3, city_matchTrue ) print(f综合分{score:.4f}, 经验分{exp_score:.2f}, 城市分{city_score:.2f})参数说明w_sim0.7、w_exp0.2、w_city0.1是一组常见初始值具体权重应该根据业务反馈调整。如果岗位对城市要求极严格干脆把w_city提到 0.3如果岗位是远程办公w_city可以直接置零。经验分这里用的是1 - 相对误差意思是候选人经验越接近 JD 要求得分越高超出的部分会受到惩罚。这里有一个容易踩坑的点abs(exp_year - jd_exp)对经验超过要求的候选人不友好10 年经验对 3 年经验的岗位也许不是减分项。所以在真实项目里我经常把经验打分改成阶梯式0 jd_exp 之间线性加分超过 jd_exp 之后封顶为 1.0超出再多也不加分、不扣分。需要实际跑一次大批量推荐再决定用哪种经验打分函数不要拍脑袋定。5.3 结果解释给 HR 一个“为什么推这个人”的回放推荐系统的信任度取决于解释能力。黑匣子式推荐在商品场景还能忍在招聘场景完全不行——HR 需要向业务部门说明推荐理由。所以每一条推荐结果要能回放出匹配了哪些技能、相似度多少、经验分多少、城市是否匹配。# 生成推荐结果的解释信息 def explain_match(jd_text: str, resume_text: str, sim: float, exp_year: int, jd_exp: int, city_ok: bool) - dict: hits hit_skills(resume_text, {flask, vue, python, java}) _, exp_score, city_score final_score(sim, exp_year, jd_exp, city_ok) return { similarity: round(sim, 4), hit_skills: hits, exp_score: round(exp_score, 2), city_match: city_ok, reason: f技能匹配{、.join(hits) or 无}经验分{exp_score:.2f} } print(explain_match( 招聘Flask后端, 熟悉Flask和Python三年经验, 0.52, 3, 3, True ))逻辑说明explain_match把命中的技能、经验分、城市匹配情况汇总成一个 JSON 字典前端可以直接展示。这也是为什么在特征工程阶段要保留hit_skills的输出——它既是特征也是解释材料。推荐系统做到这里才算从“能跑”变成“能用”。6. 简历推荐调参避坑5 个高频翻车现场与排查清单6.1 “小程序开发”被分词器切得七零八落技能命中率直线下降现象简历里写“熟悉小程序开发”分词结果是“小 / 程序 / 开发”hit_skills里预设的“小程序”永远匹配不上。原因jieba默认词表里没有“小程序”这个词把“小程序”切成“小”和“程序”。技术名词只要不在词表里就一定会被切碎。解决把“小程序”“小程序开发”一起加进用户词典词频设 10。更稳妥的做法是把技能词库和jieba用户词典统一管理技能词库里新增一个词时自动同步到用户词典不要人工维护两份表。6.2 PDF 简历提取出来是乱码或整段空白喂进算法全是噪音现象用pypdf提取某些 PDF 简历得到的文本要么是乱码要么是空白要么是顺序错乱的段落。原因PDF 有两种形态文本型和扫描型。文本型 PDF 如果用了非标准字体编码extract_text()会返回乱码扫描型 PDF 本身没有文本层只能 OCR。双栏排版的简历即使提取成功阅读顺序也是错的。解决在解析层直接做体检提取字数少于 200 的文本直接标注“解析失败”不进推荐队列。后续有三条路可以走一是用带 OCR 能力的解析库二是要求候选人上传 PDF 的同时填一份结构化表单三是把解析失败的简历转入人工补录流程。初版不要为了兼容所有 PDF 而把链路搞复杂。6.3 相似度推荐被热门简历霸榜冷门候选人永远排不进去现象每次推荐 Top 10 里总是那么几份简历换一个 JD 结果也差不多这些简历的共同点是技能词特别全、文本特别长、关键词密度特别高。原因TF-IDF 本身偏向长文本和热词一份写了 50 个技能的简历天然比只写 5 个核心技能的简历更容易和任意 JD 相似。这不是算法 bug是数据分布的问题。解决推荐前先做粗筛召回只对符合“技术栈主方向”的简历计算相似度而不是全库遍历。技能命中数少于 1 的直接跳过热门简历单日推荐次数超过上限后降权。实现逻辑很简单在候选池里加一个hit_count判断即可。6.4 “3 年 Java 后端”被推给“3 年 Java 前端”岗位现象文本相似度把“后端”和“前端”当成相关词因为两份简历都大量出现“Java”“三年”“开发”推荐结果和技术方向南辕北辙。原因TF-IDF 无法理解“后端”和“前端”是互斥关系它只知道这两个词经常同时出现在同一批文档里。解决岗位族硬过滤不能省。JD 明确要求后端岗位族时通过技能词库判断简历的主要方向如果主要方向是前端直接不进候选池。这一步必须发生在相似度计算之前不要指望模型自己学会这个行业常识。数据量足够大之后可以把“后端”从背景词改成判别词但那需要大量标注数据不是第一版要干的事。排查项检查方法判断标准分词结果随机抽 20 条简历打印分词列表核心技能词保持完整相似度分布打印全部相似度的直方图大多数集中在 0~0.3个别 0.5技能命中覆盖率统计命中技能的简历占比低于 60% 说明词库覆盖不足推荐结果重复率统计 10 个 JD 的 Top 10 重复度重复率过高需要检查粗筛逻辑6.5 改一次技能词库要重启整个服务线上验证迭代慢现象词库里加了一个新技能词要改代码、重启服务、重新加载数据一顿操作下来小半天没了。原因把技能词库写死在 Python 常量里或者加载逻辑放在模块顶层导致每次改动都要动代码。解决把技能词库抽成独立配置文件用 JSON 或 YAML 存放服务启动时加载并提供接口支持定时热刷新。加词不需要改算法代码只改配置然后触发一次缓存的失效。这个改动看起来很基础但它决定了后续迭代速度值得在最开始就做对。7. 上线前怎么验证推荐质量NDCG 与历史回看推荐系统最怕的是自我感觉良好。相似度看着挺高候选人好像也对口但 HR 是否真的愿意邀约机器不知道。所以在写第一版推荐算法的时候就应该把评估指标也写出来。我常用的是 NDCG它能同时衡量“推荐的人对不对”和“对的人排得靠不靠前”。计算它需要一份标注数据从历史投递记录里把 HR 标记过“已邀约”或“已入职”的简历当成正样本把没被处理的简历当成负样本。然后对比推荐系统的排序结果和真实结果。import numpy as np def ndcg_at_k(ranked_ids: list[str], relevant_ids: set[str], k: int 10) - float: 计算前 k 个推荐结果的 NDCG。 dcg 0.0 for i, rid in enumerate(ranked_ids[:k]): rel 1 if rid in relevant_ids else 0 dcg (2 ** rel - 1) / np.log2(i 2) ideal_rels min(len(relevant_ids), k) idcg sum(1.0 / np.log2(i 2) for i in range(ideal_rels)) return dcg / idcg if idcg 0 else 0.0 # 示例推荐序列中第 0 位命中真实相关简历有 2 份 print(ndcg_at_k([R001, R002, R003], {R001, R004}, k3))这段代码里ranked_ids是推荐算法输出的结果序列relevant_ids是由 HR 行为数据标记出的正样本集合。NDCG 的值域是 0 到 1越接近 1 说明又准又靠前。我自己的习惯是每次改完分词、词库、权重之后固定跑一遍同一份测试集看 NDCG 是否下降如果下降就要回滚不要相信“这次改动应该没问题”的直觉。这类项目的价值不在于用了多前沿的模型而在于能稳定地产出可解释的推荐结果。一个几百份简历的团队内部招聘工具用 TF-IDF 加余弦相似度加业务规则的精排效果已经足够上场等简历量真的到了几十万再换向量模型也不迟——上一层的排序接口不用动只替换特征提取这一层。做简历推荐算法这几年来我最大的收获是凡是没评估指标就上线的推荐系统最终都会变成黑匣子出了问题谁也说不清是数据的问题还是权重的问题。我自己每次改完参数都要用同一份数据跑回归就怕哪天线上悄悄翻车还不自知希望帮到你。本文还有配套的精品资源点击获取
返回列表