ARTICLE DETAIL

资讯详情

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

基于Python机器学习与BERT的在线招聘欺诈检测系统实现

基于Python机器学习与BERT的在线招聘欺诈检测系统实现 简介文本分类是自然语言处理中应用最广泛的技术方向之一核心在于将非结构化文本转化为可计算的语义特征。传统词频统计方法难以捕捉复杂语境中的隐含关联而基于深度学习的预训练模型通过大规模语料学习能够生成更精准的句向量表达。将文本向量与经典机器学习算法结合可以在资源有限的环境下构建高效分类器。这一技术路线在招聘安全、反欺诈风控等场景具有重要的工程价值。本文从爬虫数据采集、BERT语义向量化、SKlearn模型训练到Flask服务发布的完整流程出发解析如何搭建一个在线招聘欺诈检测平台帮助开发者在真实业务中快速落地文本甄别能力。1. 从“投了三个月简历才发现是骗局”说起这个招聘欺诈检测平台值得拆把“基于Python机器学习的在线招聘欺诈检测平台设计”这份资源完整跑通之后我想先给你一个结论这套东西不是那种只能截图展示的毕业设计样板而是一条从爬虫抓取、BERT文本向量化、SKlearn分类到Flask后端和前端可视化的完整闭环。压缩包里论文、环境部署说明、项目源码三部分齐全你从零开始按说明装环境大概一个下午能把系统跑起来。它适合谁两类人——正在做招聘信息检测方向毕业设计的学生以及想在公司内部搭一个简易风控Demo的工程师。下面几章我按“架构原理→核心实现→环境部署→避坑→进阶”的顺序把这个项目拆给你看。2. 平台架构与检测流程spider、orfd、website三条线怎么咬合2.1 数据采集线spider爬虫的角色与边界在拆这个项目时我第一眼看到的是SourceCode目录下三个独立工程spider、orfd、website。很多毕设项目会把爬虫、后端、前端揉在一个工程里结构乱成一团这套资源把它拆成三个目录反而更像真实的生产系统。spider这条线的职责很明确就是负责从招聘网站采集原始职位信息然后存入数据库。它用到的核心工具是requests加BeautifulSoup4两个库搭配是Python爬虫最经典的组合。requests负责发HTTP请求BeautifulSoup4在拿到HTML之后做DOM解析提取职位名称、薪资范围、公司主体、工作地点、职位描述这些你之后要用来判别的字段。采集层有一个值得注意的边界它不负责判别欺诈。spider抓到任何一条数据无论真伪都先落库。这个设计是对的因为欺诈检测的核心判断逻辑在后面的文本特征和分类器环节爬虫过早过滤反而会污染训练样本。干净的原始数据、独立的采集模块是这个平台里我认为最合理的一处设计。实际抓取时爬虫还要处理两种常见情况一是招聘列表页和详情页是两个不同URL列表页通常能拿到几十条职位摘要详情页才有完整描述不翻页、不跟进详情你拿到的数据量根本不够训练模型二是很多网站会做编码处理返回的HTML可能是gbk或gb2312直接按utf-8解析必乱码。2.2 特征表达为什么偏偏选Google BERT中文预训练模型整个平台最有含金量的决定是文本特征这一层用了Google的BERT中文预训练模型做句向量而不是常见的TF-IDF或Word2Vec。你如果做过文本分类就知道TF-IDF在短文本场景下还行但招聘欺诈这类问题恰恰是长文本语义问题。诈骗招聘有很强的语义特征职位描述写“月薪30K、无经验要求、急招、高额补贴”公司信息却是一个新注册的皮包公司岗位内容和薪资范围之间出现语义矛盾。TF-IDF只能统计词频看不到“高薪”和“无经验”这两个词之间的关联Word2Vec虽然能处理语义关系但对一句话的整体语境捕捉能力有限。这个项目引入BERT中文预训练模型是想把每个职位描述通过BERT编码成一个固定维度的语义向量。向量里包含的是整句话的上下文语义而不是孤立的词表。训练好的分类器在这个向量基础上学习“什么样的文本表述更像诈骗招聘”这才是这套平台的核心竞争力。实现上项目没有直接用TensorFlow硬钢BERT接口而是用bert-serving-server和bert-serving-client在中间包了一层。为什么会这么设计因为直接调BERT底层的输入处理非常繁琐分词、掩码、segment标记、attention mask都要你自己拼。bert-serving-server把模型加载到内存里起一个gRPC服务客户端只需要把原始文本传进去向量结果就返回出来把复杂的预处理细节全屏蔽了。2.3 分类与后端SKlearn模型训练与Flask API发布当你把文本转换成向量之后模型训练这件事反而变得简单。这部分用的全是SKlearn提供的成熟算法。项目里用pandas做数据加载和清洗用sklearn的train_test_split划分训练集和测试集然后训练一个文本分类模型判断一条招聘信息是否涉嫌欺诈。选SKlearn而不选深度学习模型这里有个务实考量项目运行环境是Windows 10GPU不一定有深度学习模型的训练和推理在CPU上非常吃力。SKlearn里的逻辑回归或随机森林拿来训练一个几千条样本的分类任务几秒钟就能完成实际做推理时一条文本转成BERT向量后给分类器打分毫秒级就能返回结果。毕设、Demo场景里这条路径的性价比远高于再叠一层深度学习分类器。后端工程orfd做的是把这些模块串起来的事从数据库中取数据、用BERT服务做特征转换、用训练好的模型做预测、最后通过Flask框架对外暴露HTTP接口。Flask是Python轻量级Web框架里最适合这种场景的选择不用像Django那样建一整套工程结构单文件就能起一个API服务。项目中orfd目录里的main.py、setting.py、core.py分别承担启动入口、配置管理和核心逻辑的职责。前端website工程则是通过页面调用后端接口把检测结果展示给用户。3. 核心实现拆解从爬虫到分类器每一步怎么落地3.1 爬虫抓取实战requests与BeautifulSoup协作先看spider这部分最关键的一小段代码。我按项目里典型的抓取流程给你写一个精简但能用的样例它把职位列表页抓下来后用BeautifulSoup解析出核心字段。import requests from bs4 import BeautifulSoup import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9 } def fetch_job_page(url): resp requests.get(url, headersheaders, timeout10) # 招聘网站常返回gbk编码这里做个兼容判断 if resp.encoding.lower() in (gbk, gb2312): resp.encoding gbk return resp.text def parse_job_items(html): soup BeautifulSoup(html, html.parser) items [] # 不同的站点选择器不同需要按目标网站实际调整 for job in soup.select(.job-list-item): title job.select_one(.job-title) salary job.select_one(.salary) company job.select_one(.company-name) if title and salary: items.append({ title: title.get_text(stripTrue), salary: salary.get_text(stripTrue), company: company.get_text(stripTrue) if company else , detail_url: job.get(href, ) }) return items if __name__ __main__: html fetch_job_page(https://example.com/jobs) for item in parse_job_items(html): print(item) time.sleep(1) # 控制抓取频率降低被封风险这套代码里有两个参数值得你实际去调。第一是User-Agent很多站点对常见的默认UA直接拦你换成真实浏览器UA后通过率会明显提升。第二是time.sleep(1)这个延时看起来碍事但直接影响你的爬虫寿命——抓太快导致IP被封比慢十分钟严重得多。项目里spider的数据量不用特别大几千条就够训练初版模型慢一点没关系。另外select那一行不同招聘网站的结构差别很大拿到包里的源码后你要先看人家已经写好的选择器再按自己目标站点改。抓回来的数据要落库。项目用的数据库是MySQL相关的配置都能在setting.py里找到核心就是把抓到的文本字段存成UTF-8格式避免后续模型训练时读出来出现乱码。这里有个习惯我建议你保留原始HTML里的隐藏字段别丢弃有些诈骗招聘的描述里会有联系电话、微信号这类在页面上不直接展示的信息后续做特征扩展时有用。3.2 BERT特征提取bert-serving-client的正确调用方式当文本数据进了数据库下一步就是调BERT服务做向量化。这一段代码是项目能跑通的核心我给你写一个完整的调用示例。from bert_serving.client import BertClient # 连接BERT服务注意ip和port要和bert-serving-server启动参数一致 bc BertClient(ip127.0.0.1, port5555, port_out5556) def text_to_vector(text, max_len128): # 超长文本截断BERT对输入长度有上限 if len(text) max_len: text text[:max_len] # 返回的是句向量shape为(1, 768) vec bc.encode([text]) return vec[0] test_text 诚聘兼职刷单日结300元无需经验工资日结 vec text_to_vector(test_text) print(向量维度:, vec.shape) print(向量前10个值:, vec[:10])这里有几个容易被忽略的细节。端口对不上你后端起服务时客户端这边就报连接失败项目里bert-serving-server默认监听5555输出端口5556两个端口在代码里都要指定。max_len截断参数很关键BERT中文模型对输入序列长度有限制超过512直接报错但大多数学分类任务里职位描述核心语义在头部128个字内就能表达清楚直接截断不会损失太多信息。vec.shape这个结果应该是(768,)如果你看到的是(1, 768)说明你没有取出第一维后面往分类器里填数据时维度会不匹配。另一点是批量处理。在实际训练时你不可能一条一条地调bc.encode那太慢了。bert-serving-client支持直接传一个list进去一次返回多维数组texts [ 诚聘兼职刷单日结300元, 急招电话销售底薪高提成, 某互联网公司招聘Java开发年薪30万 ] vecs bc.encode(texts) # shape为(3, 768) print(vecs.shape)一次请求处理多条文本速度提升非常明显。项目里训练集如果是5000条分成每批500条去编码十次请求就能全部转成向量这个批量大小的参数记忆里调得太大会遇到单个请求超时我这边的经验是一次200到500条比较稳。3.3 训练分类模型pandas清洗与SKlearn流水线拿到向量之后训练代码就清爽多了。我按项目里典型的处理流程给你一个完整示例包含数据读入、标签处理、划分和训练。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report from bert_serving.client import BertClient bc BertClient(ip127.0.0.1, port5555, port_out5556) # 假设你已经准备好一张表text列是职位描述label列是0/11代表欺诈 df pd.read_csv(labeled_jobs.csv, encodingutf-8) df df.dropna(subset[text]) print(样本量:, len(df), 正样本比例:, df[label].mean()) # 批量转换为BERT向量 texts df[text].tolist() vecs bc.encode(texts) # 划分训练测试集stratify保证正负样本比例一致 X_train, X_test, y_train, y_test train_test_split( vecs, df[label].values, test_size0.2, random_state42, stratifydf[label].values ) # 用逻辑回归做分类max_iter调大避免不收敛 model LogisticRegression(max_iter2000, class_weightbalanced) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[正常, 欺诈]))这段代码里最值得说的参数是class_weightbalanced。招骗样本在真实数据中比例通常不超过10%如果不做处理模型会学成“全都预测正常”准确率看着90%以上但欺诈检测的召回率是0完全没有意义。class_weightbalanced会自动给少数类加权重是处理这个问题的常规手段。逻辑回归的max_iter也要注意BERT向量是768维特征维度不算低默认的100次迭代经常收敛不了我改成2000次后没有这个报错。当然你不用非选逻辑回归换成随机森林也行只是逻辑回归在这里有一个优势它能输出预测概率而不仅仅是0/1标签你后端判断欺诈时可以根据概率设置不同阈值。3.4 前端结果展示website如何呈现检测结果检测模型有了后端API也起了前端website工程承担的任务是让人看得见结果。我按项目里最可能的实现方式给你一个Flask接口加前端Ajax调用的示意后端返回预测概率前端把它渲染成表格。from flask import Flask, request, jsonify import joblib from bert_serving.client import BertClient import numpy as np app Flask(__name__) model joblib.load(fraud_model.pkl) bc BertClient(ip127.0.0.1, port5555, port_out5556) app.route(/predict, methods[POST]) def predict(): data request.get_json() text data.get(text, ) if not text: return jsonify({error: text is required}), 400 vec bc.encode([text[:128]]) prob model.predict_proba(vec)[0][1] label 欺诈 if prob 0.5 else 正常 return jsonify({ text: text, label: label, probability: round(float(prob), 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)你在项目里找model.load那一段时要注意模型文件的保存和读取要对应。project里训练模型的脚本一般会调用joblib.dump把训练结果存成.mdl或.pkl文件后端加载这个文件的路径要和你保存路径一致。路径写错是最常见的启动报错。flask启动后前端用fetch或axios发POST请求。你打开website的页面输入一条职位描述页面会把文本POST到/predict拿到返回的label和probability显示出来。我建议你把probability打印在页面上而不是只显示“欺诈/正常”两个词因为0.49和0.5之间的差在演示时往往是最能说明模型有效性的细节。4. 环境部署与参数配置Windows下从零跑通这套系统4.1 Python 3.6.8与PyCharm版本选择的理由项目环境部署说明文档里明确写的是Windows 10专业版、PyCharm Professional 2019.1.1、Python 3.6.8。这里有个很多人容易忽视的关键点为什么锁定Python 3.6.8而不是最新的3.12因为TensorFlow和BERT服务这套组件栈对Python版本有严格兼容上限。TensorFlow 1.x系列在老版本Python上表现得最稳BERT官方代码的很多依赖也只保证兼容3.6到3.7。你如果装Python 3.9以上安装tensorflow时往往没有匹配的wheel包装到一半就报错。我自己的习惯是看到这种项目的第一件事不是去下载最新版Python而是严格按照部署说明里的版本锁死。用PyCharm新建项目时在Project Interpreter里选择已经安装好的3.6.8解释器别用默认的新版本。另外一个部署细节是整个项目跑起来至少要同时开两个进程BERT服务进程和Flask后端进程。BERT服务是常驻内存的它加载的是Google的中文预训练模型模型文件几百兆启动时间在几十秒级别。你每次改后端代码重启Flask时BERT服务不用跟着重启这让开发体验好很多。部署说明文档把它写得很清楚照着一步步来不会有遗漏。4.2 第三方依赖安装清单与顺序依赖安装是整个部署过程中最磨人的一环。项目里需要的第三方库我在摘要里数了一遍flask、bert-serving-server、bert-serving-client、requests、BeautifulSoup4、pandas、numpy、sklearn、tensorflow。安装顺序有讲究必须先装tensorflow再装bert-serving-server否则bert-serving-server在安装时检测不到tensorflow会跳过对它的依赖校验。下面是我实际的安装命令pip install tensorflow1.13.1 pip install bert-serving-server bert-serving-client pip install flask requests beautifulsoup4 pandas numpy scikit-learn这里tensorflow版本我建议你锁定在1.13到1.14之间这是和bert-serving-server兼容性最好的区间。如果直接pip install tensorflow装出来的是2.x甚至3.x版本bert-serving-server会有兼容报错。scikit-learn这个包在Python 3.6环境下的最新版本是0.22左右直接装也没问题。所有包装完后用下面这段代码做一次快速验证import tensorflow as tf import sklearn import bert_serving from bert_serving.server.helper import get_args_parser print(tensorflow:, tf.__version__) print(sklearn:, sklearn.__version__) print(bert_serving OK)如果三个版本号都能正常打印出来说明依赖层没问题可以进下一步。这里报错的话十有八九是tensorflow和bert-serving-server的版本不匹配。排查思路很简单先卸载tensorflow再按我上面的版本号重装。4.3 BERT服务启动与model路径配置BERT服务是整个平台的“黑匣子”启动它也是门槛最高的部分。你需要先下载Google的BERT中文预训练模型把压缩包解压到ExecutableFiles目录或你自己的model目录下确保文件夹里有三个文件bert_config.json、vocab.txt、bert_model.ckpt.index、bert_model.ckpt.data-00000-of-00001。前三个缺一个服务都起不来。启动命令如下这里参数值要和你项目里代码调用的端口严格保持一致bert-serving-start -model_dir D:/workspace/chinese_L-12_H-768_A-12 \ -num_worker1 \ -port5555 \ -port_out5556 \ -max_seq_len128 \ -cpu-model_dir后面是模型文件解压后的绝对路径路径中不要有中文和空格否则Windows下容易出诡异问题。num_worker是并行工作进程数如果你没有GPU建议设1个有GPU可以设到4但显存至少需要6G。端口号5555和5556是项目的默认配置和你代码里的BertClient参数要对上。加-cpu参数是强制CPU模式在没有CUDA环境下防止启动时报找不到GPU。服务启动成功的标志是你的控制台出现类似“ready and serving”的日志然后你打开另一个终端执行上面第3章里的client代码能打印出向量维度就说明服务完全正常。5. 避坑记录这几条血泪经验能帮你省三天时间5.1 BERT服务启动后一直报“graph is not ready”这个坑我猜至少有一半的人会踩。现象是bert-serving-start命令敲下去之后日志卡在加载模型阶段很长时间最后报错里带“graph”和“not ready”关键字。原因有两类。第一是模型文件不完整缺了bert_model.ckpt.data文件模型无法加载成Graph。第二是tensorflow的版本问题用了2.0以上版本后BERT服务的底层兼容性崩掉模型文件能读但图构建失败。解决的方法是先确认模型目录下四个文件齐全再从自己的环境里执行“pip list | grep tensorflow”查版本如果大于1.14就卸载重装1.13.x。两个版本之间选择版本是最快的路径。我在这卡过一下午最后发现是之前另一项目装的tensorflow 2.x污染了环境。5.2 sklearn训练时维度对不上expected 2D array, got 1D array这个报错常出现在你从BERT接口拿到向量后直接喂给fit方法。现象是训练代码一执行fit就抛出“expected 2D array, got 1D array instead”。原因是BertClient.encode在只传一条文本时返回的是(1, 768)的二维数组但你如果做了类似vecs vecs[0]的操作向量变成768维的一维数组sklearn会把它当作一个样本而不是一个特征矩阵。这个操作在很多教程里都有因为大家习惯上把单条向量拿出来打印。解决了这一点也很简单你确保喂给fit和predict的都是二维数组。如果对维度没把握可以在fit之前主动reshape一下(n_samples, 768)。务必要保证第一位是样本数第二位是特征维度。5.3 爬虫抓回来的文本全部变成乱码这个现象很直观用BeautifulSoup解析出来的职位名称全是“锟斤拷”“烫烫烫”这类常见的乱码字符。原因是很多招聘网站页面是GBK编码但requests库在未指定编码的情况下会先从响应头里的charset字段判断判断失败就默认用ISO-8859-1那解析中文文本必定乱词。解决方案就是在代码里强制指定编码也就是我在第3章示例中写的resp.encoding gbk那两行。如果目标网站是utf-8就改成utf-8。这是一行代码就能解决的问题但如果你没经验会在清洗数据环节浪费大量时间。5.4 正负样本比例失控模型准确率99%却是废物这个坑很隐蔽。现象是你训练完模型后打印classification_report看到整体准确率高达99%但precision和recall在欺诈类上非常低甚至为0。这种模型部署上去形同虚设。原因是招聘欺诈在真实场景里本来就是极少数样本如果把数据采集后直接拿来训练正样本占比可能不足5%模型学到的就是“全部预测成正常”这个最优策略。解决方法是两条路同时走。一是在数据采集阶段要刻意多收集欺诈样本用关键词比如“刷单”“日结”“无需经验”去定向搜索。二是在训练时加class_weightbalanced或者设置样本权重数组。项目里的正样本配比你也要在代码里去确认一下不合理时先扩充标注数据模型效果才会真实。5.5 Flask后端一启动就端口冲突现象是启动Flask时报“Address already in use”提示5000端口被占用。这个在开发机上很常见。原因可能是IDE里上一次的运行进程没有被杀掉还占着端口。再就是另一个项目也在用Flask默认端口。解决方法是先把占用端口的进程清掉。Windows下执行“netstat -ano | findstr :5000”查PID然后“taskkill /PID xxx /F”强杀。后面我一般会在setting.py里把端口改成一个不常用的比如8012省得每次和别的项目打架。6. 进阶用法与效果验证样本扩充、阈值调整与API扩展模型跑通只是第一步真正要交付一个能用的平台还得做三件事扩充样本、调阈值、扩展验证方式。这三件事不需要改架构只是在你现有代码上做增量。第一个动作是扩充样本。项目自带的标注数据量有限训练出来的模型在演示时看着不错但遇到真实世界的新话术预测能力会打折。你可以用第3章的爬虫代码去更大的招聘网站抓数据然后手动标注一部分补进原始数据集。我自己的经验是标注8000条以上并且欺诈样本占比控制在不低于20%逻辑回归的泛化能力会明显上一个台阶。标注工作虽然枯燥但它对模型质量的提升比换任何模型都见效。第二个动作是调阈值。项目里的逻辑回归模型输出的是概率你用0.5作为欺诈判断的阈值这只是默认值。真实场景中你可以根据漏判和误判的代价去移动这个阈值如果欺诈造成的损失大就把阈值降到0.3宁可误判一些正常职位也不要放过可疑信息如果误判严重干扰正常招聘就往上调到0.6。这个调参过程用下面的代码很直观from sklearn.metrics import precision_recall_curve import numpy as np probs model.predict_proba(X_test)[:, 1] precision, recall, thresholds precision_recall_curve(y_test, probs) for t in [0.3, 0.4, 0.5, 0.6, 0.7]: pred (probs t).astype(int) p precision_recall_curve(y_test, probs) # 简略计算当前阈值下的精确率和召回率 tp ((pred 1) (y_test 1)).sum() fp ((pred 1) (y_test 0)).sum() fn ((pred 0) (y_test 1)).sum() print(f阈值{t}: 精确率{tp/(tpfp):.3f}, 召回率{tp/(tpfn):.3f})这段代码跑完你会看到一条清晰权衡曲线然后选定你要的阈值把后端predict接口里的0.5替换成实测最优值。第三个动作是扩展API的能力。项目里现在的Flask接口只能接收一条文本返回一个结果你完全可以加一个批量检测接口接收一个职位列表返回每一个的欺诈概率。上游的爬虫每抓到一批新职位就调用一次批量接口检测结果自动落库。这样就把一个“演示平台”变成了一个“能用起来的风控工具”。最后提醒你一件事BERT服务加载后模型权重和词表都占在内存里它吃的是CPU资源你和Flask跑在同一台机器上时如果机器配置是4核8G以下建议先做一些数据的小量测试再跑全量。这个项目我从下载到跑通前后踩过上面那些坑尤其是环境版本和BERT模型路径两关最费时间。从那以后我每次部署类似的项目都强制自己先写一遍环境版本清单再动代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表