技术方案分享:基于pdf解析+特征工程+lightgbm的校招简历信息完整性检测思路与实现源码

1,102次阅读
没有评论

今天是2023年11月9日,星期四,北京,天气晴,我们来看看一个关于简历信息完整性检测的工作。
本文公开了”讯飞AI算法挑战大赛-校招简历信息完整性检测挑战赛“赛道的技术方案和代码,本次比赛主要采用pdf解析和特征工程的方法,通过使用lightgbm的树模型10折交叉验证进行二分类的任务。
供大家一起参考。

一、赛题任务

简历智能化判断,需要大量的数据集作为支撑,同时简历的半结构化数据特点和多变的简历风格给简历智能化判断带来了挑战,本次大赛将提供脱敏的学生中文简历数据集(pdf或docx格式)作为训练样本,参赛选手需要基于提供的样本数据构建模型,预测简历是否符合简历投递基本要求。

任务如下:

简历完整性检测。根据要求提取简历要素特征数据,并根据样本数据构建模型,预测简历是否符合简历投递基本要求,预测结果可分为两个类别:即完整(标签1)不完整(标签0)

技术方案分享:基于pdf解析+特征工程+lightgbm的校招简历信息完整性检测思路与实现源码

二、数据和评价指标

数据:脱敏后的学生简历数据集(pdf或docx格式)。训练数据提供脱敏后的数据集,共800余份。测试集不可见,由真实简历数据组成,共100余份。训练集全部为pdf格式。

注:数据集分为正样本负样本,其中正样本为完整性简历数据集,符合简历投递基本要求;负样本为不完整简历数据集,不符合简历投递基本要求。

评价指标:F1 score

三、方案

3.1.方案概述

本次比赛主要采用pdf解析和特征工程的方法,通过使用lightgbm的树模型10折交叉验证进行二分类的任务。

3.2.pdf2text解析

本次比赛主要实验了以下几种解析工具,最终最高分选择了pymupdf

  • pdfplumber
  • PyPDF2
  • pymupdf

3.3.特征工程

主要文本特征如下:

  • 页数

  • pdf2text的文本长度

  • 按行切分后的平均长度

  • 按行切分后的最大长度

  • 按行切分后的长度标准差

  • text字符集合的大小

  • pdf2text的文本长度-text字符集合的大小

  • text字符集合的大小/(pdf2text的文本长度+1)

  • text空格切分后的列表大小

  • text换行符切分后的列表大小

  • -的数量

  • x的数量

  • xxx的数量

  • 数字的数量

  • @的数量

  • .com的数量

  • *的数量

  • :的数量

  • ****的数量

  • 正则匹配电话号码的数量

特征提取对应的code

pattern = r"[D]+(1d{10})+(?!d)"

def extract_feature_from_pdf(path):
    doc = fitz.open(path)
    all_content = []
    page_nums = 0
    for i in doc.pages():
        page_nums += 1
        all_content.append(i.get_text())
    text = .join(all_content)
    text = .join(text.split(‘n’))

    feat = [
        page_nums,
        len(text),
        np.mean([len(x) for x in text.split(‘n’)]),
        np.max([len(x) for x in text.split(‘n’)]),
        np.std([len(x) for x in text.split(‘n’)]),

        len(set(text)),
        len(text) – len(set(text)),
        len(set(text)) / (len(text) + 1),

        len(text.split()),
        len(text.split(‘n’)),
        text.count(‘-‘),
        text.count(‘x’),
        text.count(‘xxx’),
        sum([text.count(x) for x in ‘0123456789’]),
        text.count(‘@’),
        text.count(‘.com’),
        text.count(‘*’),
        text.count(‘:’),
        text.count(‘****’),
        len(re.compile(pattern).findall(text)),
        1 if ‘正样本’ in path else 0,

    ]
    return feat

3.4.训练代码

本次比赛主要使用的是lightgbm的树模型,视为二分类任务,进行10折交叉验证的训练。

#!/usr/bin/env python
# _*_coding:utf-8_*_
# Author   :    Junhui Yu

import warnings

warnings.simplefilter(‘ignore’)

import gc

import pandas as pd

pd.set_option(‘display.max_columns’None)
pd.set_option(‘display.max_rows’100)

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, classification_report

import lightgbm as lgb

import glob

import pandas as pd
from tqdm import tqdm
import numpy as np
import re
import fitz

pattern = r”[D]+(1d{10})+(?!d)”

def extract_feature_from_pdf(path):
    doc = fitz.open(path)
    all_content = []
    page_nums = 0
    for i in doc.pages():
        page_nums += 1
        all_content.append(i.get_text())
    text = .join(all_content)
    text = .join(text.split(‘n’))

    feat = [
        page_nums,
        len(text),
        np.mean([len(x) for x in text.split(‘n’)]),
        np.max([len(x) for x in text.split(‘n’)]),
        np.std([len(x) for x in text.split(‘n’)]),

        len(set(text)),
        len(text) – len(set(text)),
        len(set(text)) / (len(text) + 1),

        len(text.split()),
        len(text.split(‘n’)),
        text.count(‘-‘),
        text.count(‘x’),
        text.count(‘xxx’),
        sum([text.count(x) for x in ‘0123456789’]),
        text.count(‘@’),
        text.count(‘.com’),
        text.count(‘*’),
        text.count(‘:’),
        text.count(‘****’),
        len(re.compile(pattern).findall(text)),
        1 if ‘正样本’ in path else 0,

    ]
    return feat

train_paths = glob.glob(
    ‘../xfdata/校招简历信息完整性检测训练集/*/*.pdf’)

df_train = pd.DataFrame(
    columns=[
        ‘page_nums’,
        ‘text_len’,
        ‘text_len_mean’,
        ‘text_len_max’,
        ‘text_len_std’,
        ‘text_set_len’,
        ‘lentext-lenset’,
        ‘lenset_div_lentext’,
        ‘text_split_len’,
        ‘text_split_ent_len’,
        ‘-_nums’,
        ‘x_nums’,
        ‘xxx_nums’,
        ‘dig_sum’,
        ‘@_nums’,
        ‘.com_nums’,
        ‘*_nums’,
        ‘:_nums’,
        ‘****_nums’,
        ‘phone_nums’,
        ‘label’
    ])

for t_p in tqdm(train_paths):
    df_train.loc[len(df_train)] = extract_feature_from_pdf(t_p)

not_use_feats = [‘label’]
use_features = [col for col in df_train.columns if col not in not_use_feats]
print(len(use_features))
train = df_train[df_train[‘label’].notna()]

NUM_CLASSES = 2
FOLDS = 10
TARGET = ‘label’

def run_lgb(df_train, use_features):
    target = TARGET
    oof_pred = np.zeros((len(df_train), NUM_CLASSES))

    folds = StratifiedKFold(n_splits=FOLDS, shuffle=True, random_state=42)
    for fold, (tr_ind, val_ind) in enumerate(folds.split(train, train[TARGET])):
        print(f’Fold {fold + 1})
        x_train, x_val = df_train[use_features].iloc[tr_ind], df_train[use_features].iloc[val_ind]
        y_train, y_val = df_train[target].iloc[tr_ind], df_train[target].iloc[val_ind]
        train_set = lgb.Dataset(x_train, y_train)
        val_set = lgb.Dataset(x_val, y_val)

        params = {
            ‘learning_rate’0.1,
            ‘metric’‘multiclass’,
            ‘objective’‘multiclass’,
            ‘num_classes’: NUM_CLASSES,
            ‘feature_fraction’0.75,
            ‘bagging_fraction’0.75,
            ‘bagging_freq’2,
            ‘n_jobs’-1,
            ‘seed’1029,
            ‘max_depth’10,
            ‘num_leaves’100,
            ‘lambda_l1’0.5,
            ‘lambda_l2’0.8,
            ‘verbose’-1
        }

        model = lgb.train(params,
                          train_set,
                          num_boost_round=500,
                          early_stopping_rounds=100,
                          valid_sets=[train_set, val_set],
                          verbose_eval=100)
        oof_pred[val_ind] = model.predict(x_val)
        print(‘acc:’, accuracy_score(np.argmax(oof_pred, axis=1), df_train[‘label’]))
        del x_train, x_val, y_train, y_val, train_set, val_set
        gc.collect()

    return oof_pred, model

oof_pred, model = run_lgb(train, use_features)
print(classification_report(np.argmax(oof_pred, axis=1), df_train[‘label’]))

model.save_model(‘model.txt’)

3.5.推理代码

#!/usr/bin/env python
# _*_coding:utf-8_*_
# Author   :    Junhui Yu

import glob

import pandas as pd
import numpy as np
import re
import fitz

import lightgbm as lgb

patter = r”[D]+(1d{10})+(?!d)”

def extract_feature_from_pdf(path):
    doc = fitz.open(path)
    all_content = []
    page_nums = 0
    for i in doc.pages():
        page_nums += 1
        all_content.append(i.get_text())
    text = .join(all_content)
    text = .join(text.split(‘n’))

    feat = [
        page_nums,
        len(text),
        np.mean([len(x) for x in text.split(‘n’)]),
        np.max([len(x) for x in text.split(‘n’)]),
        np.std([len(x) for x in text.split(‘n’)]),

        len(set(text)),
        len(text) – len(set(text)),
        len(set(text)) / (len(text) + 1),

        len(text.split()),
        len(text.split(‘n’)),
        text.count(‘-‘),
        text.count(‘x’),
        text.count(‘xxx’),
        sum([text.count(x) for x in ‘0123456789’]),
        text.count(‘@’),
        text.count(‘.com’),
        text.count(‘*’),
        text.count(‘:’),
        text.count(‘****’),
        len(re.compile(patter).findall(text)),
        1 if ‘正样本’ in path else 0,

    ]
    return feat

df = pd.DataFrame(
    columns=[
        ‘page_nums’,
        ‘text_len’,
        ‘text_len_mean’,
        ‘text_len_max’,
        ‘text_len_std’,
        ‘text_set_len’,
        ‘lentext-lenset’,
        ‘lenset_div_lentext’,
        ‘text_split_len’,
        ‘text_split_ent_len’,
        ‘-_nums’,
        ‘x_nums’,
        ‘xxx_nums’,
        ‘dig_sum’,
        ‘@_nums’,
        ‘.com_nums’,
        ‘*_nums’,
        ‘:_nums’,
        ‘****_nums’,
        ‘phone_nums’,
        ‘label’
    ])

test_paths = glob.glob(‘/work/data/integrity-check-of-resume-test-set/*.pdf’)[:]

for t_f in test_paths:
    df.loc[len(df)] = extract_feature_from_pdf(t_f)

not_use_feats = [‘label’]
use_features = [col for col in df.columns if col not in not_use_feats]

model = lgb.Booster(model_file=‘model.txt’)

y_pred = model.predict(df[use_features])

predict_label = np.argmax(y_pred, axis=1)

pd.DataFrame({
    ‘ResumeID’: [x.split(‘/’)[-1for x in test_paths],
    ‘label’: predict_label.astype(int)
}).to_csv(‘/work/output/result.csv’, index=None)

3.6.特征重要度与f1-score

                feature  split       gain
16              *_nums     96  23.080862
15           .com_nums     68  15.428008
6       lentext-lenset    126  12.632440
7   lenset_div_lentext    222  10.997545
13             dig_sum    218   7.045122
1             text_len    110   4.449556
17              :_nums    179   4.178767
8       text_split_len    165   4.169549
10              -_nums    137   3.483447
5         text_set_len    184   3.018025
14              @_nums     13   2.870494
11              x_nums     94   2.141016
19          phone_nums     16   1.668496
18           ****_nums     12   1.608449
12            xxx_nums     24   1.249654
2        text_len_mean     31   1.066294
0            page_nums     31   0.803168
3         text_len_max      5   0.109109
9   text_split_ent_len      0   0.000000
4         text_len_std      0   0.000000

              precision    recall  f1-score   support

           0       0.75      0.84      0.79       105
           1       0.98      0.96      0.97       710

    accuracy                           0.94       815
   macro avg       0.86      0.90      0.88       815
weighted avg       0.95      0.94      0.94       815

四、延伸

本次比赛任务相对简单,如果真正做到应用级别还需要考虑更多维度综合对简历的完整性进行评价。比如:简历中核心字段的填充率、设计简历中核心字段的重要性权值等等多维度信息。涉及技术可能有基于实体识别的简历解析(从本文特征工程也可以看出)技术等。

结论

本文仅记录8月份参与该比赛思路,至于代码也很普通。该比赛任务由于比较简单,线下指标虚高,训练数据与线上评测数据较少(耐心做特征工程分数可以非常高),并且技术价值不高。因此,前前后后投入差不多一个小时左右时间速刷了一下,最后偶然获奖。

参考文献

【1】校招简历信息完整性检测挑战赛:https://challenge.xfyun.cn/topic/info?type=information-integrity&option=ssgy

关于我们

老刘,刘焕勇,NLP开源爱好者与践行者,主页:https://liuhuanyong.github.io。


老刘说NLP,将定期发布语言资源、工程实践、技术总结等内容,欢迎关注。


对于想加入更优质的知识图谱、事件图谱、大模型AIGC实践、相关分享的,可关注公众号,在后台菜单栏中点击会员社区->会员入群加入。

 

Read More 

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)

文心AIGC

2023 年 11 月
 12345
6789101112
13141516171819
20212223242526
27282930  
文心AIGC
文心AIGC
人工智能ChatGPT,AIGC指利用人工智能技术来生成内容,其中包括文字、语音、代码、图像、视频、机器人动作等等。被认为是继PGC、UGC之后的新型内容创作方式。AIGC作为元宇宙的新方向,近几年迭代速度呈现指数级爆发,谷歌、Meta、百度等平台型巨头持续布局
文章搜索
热门文章
潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026

潞晨尤洋:日常办公没必要上私有模型,这三类企业才需要 | MEET2026 Jay 2025-12-22 09...
“昆山杯”第二十七届清华大学创业大赛决赛举行

“昆山杯”第二十七届清华大学创业大赛决赛举行

“昆山杯”第二十七届清华大学创业大赛决赛举行 一水 2025-12-22 17:04:24 来源:量子位 本届...
MiniMax海螺视频团队首次开源:Tokenizer也具备明确的Scaling Law

MiniMax海螺视频团队首次开源:Tokenizer也具备明确的Scaling Law

MiniMax海螺视频团队首次开源:Tokenizer也具备明确的Scaling Law 一水 2025-12...
真正面向大模型的AI Infra,必须同时懂模型、系统、产业|商汤大装置宣善明@MEET2026

真正面向大模型的AI Infra,必须同时懂模型、系统、产业|商汤大装置宣善明@MEET2026

真正面向大模型的AI Infra,必须同时懂模型、系统、产业|商汤大装置宣善明@MEET2026 量子位的朋友...
最新评论
ufabet ufabet มีเกมให้เลือกเล่นมากมาย: เกมเดิมพันหลากหลาย ครบทุกค่ายดัง
tornado crypto mixer tornado crypto mixer Discover the power of privacy with TornadoCash! Learn how this decentralized mixer ensures your transactions remain confidential.
ดูบอลสด ดูบอลสด Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Pretty! This has been a really wonderful post. Many thanks for providing these details.
ดูบอลสด ดูบอลสด Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
Obrazy Sztuka Nowoczesna Obrazy Sztuka Nowoczesna Thank you for this wonderful contribution to the topic. Your ability to explain complex ideas simply is admirable.
ufabet ufabet Hi there to all, for the reason that I am genuinely keen of reading this website’s post to be updated on a regular basis. It carries pleasant stuff.
ufabet ufabet You’re so awesome! I don’t believe I have read a single thing like that before. So great to find someone with some original thoughts on this topic. Really.. thank you for starting this up. This website is something that is needed on the internet, someone with a little originality!
ufabet ufabet Very well presented. Every quote was awesome and thanks for sharing the content. Keep sharing and keep motivating others.
热评文章
小米大模型“杀”进第一梯队:代码能力开源第一,智商情商全在线

小米大模型“杀”进第一梯队:代码能力开源第一,智商情商全在线

小米大模型“杀”进第一梯队:代码能力开源第一,智商情商全在线 克雷西 2025-12-18 08:57:11 ...
ISC.AI 2025创新百强颁奖典礼落幕,首发智能体专家驱动产业升级

ISC.AI 2025创新百强颁奖典礼落幕,首发智能体专家驱动产业升级

ISC.AI 2025创新百强颁奖典礼落幕,首发智能体专家驱动产业升级 量子位的朋友们 2025-12-18 ...
具身智能的数据难题,终于有了可规模化的解法

具身智能的数据难题,终于有了可规模化的解法

具身智能的数据难题,终于有了可规模化的解法 思邈 2025-12-18 14:20:44 来源:量子位 成立4...
医生版ChatGPT,估值120亿美元

医生版ChatGPT,估值120亿美元

医生版ChatGPT,估值120亿美元 Jay 2025-12-18 13:45:12 来源:量子位 Jay ...
国产AI芯片看两个指标:模型覆盖+集群规模能力 | 百度智能云王雁鹏@MEET2026

国产AI芯片看两个指标:模型覆盖+集群规模能力 | 百度智能云王雁鹏@MEET2026

国产AI芯片看两个指标:模型覆盖+集群规模能力 | 百度智能云王雁鹏@MEET2026 西风 2025-12-...