• 讯飞AI算法挑战大赛-校招简历信息完整性检测挑战赛-三等奖方案


    前言

    本文公开了”讯飞AI算法挑战大赛-校招简历信息完整性检测挑战赛“赛道的技术方案,本次比赛主要采用pdf解析特征工程的方法,通过使用lightgbm的树模型10折交叉验证进行二分类的任务,最终取得三等奖的成绩。

    一、赛题任务

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

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

    二、数据和评价指标

    数据:脱敏后的学生简历数据集(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]+(1\d{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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    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]+(1\d{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')
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159

    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]+(1\d{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('/')[-1] for x in test_paths],
        'label': predict_label.astype(int)
    }).to_csv('/work/output/result.csv', index=None)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    四、延伸

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

    结论

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

    参考文献

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

  • 相关阅读:
    idea spring boot java maven 依赖重复报错解决
    c#如何判断一个类型是否为数值类型,请给出通用方法
    使用腾讯云cos搭建图片服务器
    多测师肖sir_高级讲师_第2个月第4讲python之判断循环语句
    Halcon 第一章『Halcon语言』◆第5节:读取图像
    notepad++编辑多个位置
    matlab解方程组解析解
    yalmiptest问题
    用滑动条做调色板---cv2.getTrackbarPos(),cv2.creatTrackbar()
    DRF请求与响应
  • 原文地址:https://blog.csdn.net/yjh_SE007/article/details/134260888