比赛链接:https://js.dclab.run/v2/cmptDetail.html?id=457 本文章代码链接:https://github.com/yanqiangmiffy/One-City-Challenge/tree/master/code
1 竞赛背景
本届OneCity编程大赛主题围绕智慧城市OneCity赋能智慧政务,实现政务数据管理更智慧化、智能化展开。政务数据智能算法包括分类与标签提取,根据政府表格文件标题与内容,按照一定的原则将杂乱无章的文件自动映射到具体的类目上,加速数据归档的智能化与高效化。本次比赛旨在通过抽取政务表格文件中的关键信息,来实现表格数据自动化分类的目标。优胜者还可获得价值万元的苹果电脑,华为手机等丰厚大奖,欢迎大家踊跃参与!
PC端详情页:https://www.dcjingsai.com/v2/cmptDetail.html?id=457
2 任务
比赛任务本质上其实是文本分类,同时稍微对数据预处理要求高些,尤其能够正确读出每个表格的内容。
选手需要建立模型,针对政务表格文件实现自动化分类。允许使用一些常见的开源预训练模型,如bert。 数据智能分类按照行业领域,将政务数据分为以下20个基础大类,分别是:生态环境、资源能源、信息产业、医疗卫生、文化休闲、财税金融、经济管理、教育科技、交通运输、工业、农业畜牧业、政法监察、城乡建设、商业贸易、旅游服务、气象水文测绘地震地理、外交外事、文秘行政、民政社区、劳动人事。
3 数据
备注:报名参赛或加入队伍后,可获取数据下载权限。 数据是从政府开放数据平台收集的真实数据,共有9万多个表格文件,包括xls、xlsx、csv三种格式,其中csv文件编码格式统一为utf-8。 文件被分为三个部分,训练集、测试集1和测试集2。其中训练集(6万个文件,含标签)和测试集1(8000个文件,不含标签)于初赛阶段开放给选手下载,测试集2(不含标签)于复赛阶段开放给选手下载。
- 注意1:有些文件的内容为空。
- 注意2:有些文件的扩展名与文件格式不匹配,比如有些扩展名为xls的文件实际上是csv文件,有些扩展名为csv的文件其实是html文件。
- 注意3:在复赛阶段,有约50%的文件名会被更名为纯数字(这些文件的内容非空),请选手注意调整模型。
4【硬件资源】
CPU: Intel(R) Xeon(R) Gold 5118 CPU @ 2.30GHz GPU:Tesla V100
评分标准
采用分类的准确率Accuracy。示例代码如下:
代码语言:javascript复制from sklearn.metrics import accuracy_score
y_true = [’工业’,’文秘行政’,’劳动人事’,’信息产业’,’医疗卫生’,’文化休闲’,’旅游服务’]
y_pred = [’工业’,’ 工业’,’ 工业’,’信息产业’,’ 信息产业’,’ 信息产业’,’ 信息产业’]
score = accuracy_score(y_true, y_pred)
5 初赛top2思路
5.1 数据预处理
对于这部分,初赛期间下了不少功夫,我的目标就是能够正确读出每个表格的内容:包括表头(columns),表格单元内容(content),以及表格名称(sheet_names).
基本思路: 对表格文本数据进行提取,文本由三部分组成:文件名 表格名字 表头列名 文件名:直接用文件名,利用baseline代码可以达到一个不错的基线成绩,0.977 表格名字:有的xls内容包含多个表格,可以利用这部分表格的名字提供额外的信息,后来发现这部分文本缺陷比较大,主要是test,少部分有中文文本,另外就是为空 表头列名:这部分信息作用比较大,因为这些数据其实从政府职能网站爬去下来,数据比较脏,但是网站的html或者格局是固定的,所以表头可以提供比较有效的信息 三段文本拼接方式不同,效果也会不同
数据预处理代码如下:
代码语言:javascript复制import multiprocessing as mp
import re
import warnings
import pandas as pd
from pandarallel import pandarallel
warnings.simplefilter('ignore')
pandarallel.initialize(progress_bar=False, nb_workers=16)
def clean_text(x):
text = x.replace('train/', '').replace('.xls', '').replace('.csv',
'').replace('_', ' ').replace('test1/', '')
return text
def process_text(text):
r1 = '[a-zA-Z0-9’!"#$%&'())* -./:;,<=>?@。?★、…【】《》?“”‘’![\]^_`{|}~] '
text = re.sub(r1, '', text)
text = text.replace('NaN', '').replace('n', '')
text = text.replace("\", "")
# print(text)
text = "".join(text.split())
return text
def get_file_content_v1(filename):
"""
直接将表格内容进行拼接
"""
table_path = 'data/' filename
r1 = '[0-9’!"#$%&'())* -./:;,<=>?@。?★、…【】《》?“”‘’![\]^_`{|}~] '
if filename.endswith('xls'):
try:
with open(table_path, 'r', encoding='utf-8') as f:
text = "".join(f.read().split())
text = re.sub(r1, '', text)
print("读取xls方式[open]成功", table_path)
return text[:300]
except UnicodeDecodeError as e:
try:
df = pd.read_excel(table_path)
print("读取xls方式[read_excel]成功", table_path)
if len(df) == 0:
data = pd.DataFrame()
tmp_xls = pd.ExcelFile(table_path)
sheet_names = tmp_xls.sheet_names
for name in sheet_names:
d = tmp_xls.parse(name)
data = pd.concat([data, d])
text = data.to_string()
text = "".join(text.split())
text = re.sub(r1, '', text)
text = text.replace('NaN', '').replace('n', '')
# print(text)
return text[:300]
else:
text = df.to_string()
text = "".join(text.split())
text = re.sub(r1, '', text)
text = text.replace('NaN', '').replace('n', '')
# print(text)
return text[:300]
except Exception as e:
try:
df = pd.read_html(table_path)
print("读取xls方式[read_html]成功", table_path)
text = df.to_string()
text = "".join(text.split())
text = re.sub(r1, '', text)
text = text.replace('NaN', '').replace('n', '')
# print(text)
return text[:300]
except Exception as e:
print(e)
print("读取xls失败", table_path)
return ''
elif filename.endswith('csv'):
try:
df = pd.read_csv(table_path, error_bad_lines=False, warn_bad_lines=False, lineterminator='n')
text = df.to_string()
text = "".join(text.split())
text = re.sub(r1, '', text)
text = text.replace('NaN', '').replace('n', '')
return text[:300]
except Exception as e:
return ''
def get_file_content_v2(filename):
"""
按照表格 文件名 表格sheet文本 表格列名 文本进行拼接
"""
table_path = 'data/' filename
if filename.endswith('xls'):
try:
data = pd.DataFrame()
tmp_xls = pd.ExcelFile(table_path)
sheet_names = tmp_xls.sheet_names
# print("表格名字:",sheet_names," ".join(sheet_names))
sheet_name_text = " ".join(sheet_names)
col_names = []
for name in sheet_names:
d = tmp_xls.parse(name)
try:
col_names.extend(d.columns.tolist())
except Exception as e:
col_names.extend([])
data = pd.concat([data, d])
# print("表头名字", col_names)
col_name_text = " ".join(col_names)
table_content_text = data.to_string(header=False, show_dimensions=False, index=False, index_names=False,
sparsify=False)
print("处理成功", table_path)
text = sheet_name_text ' ' col_name_text ' ' table_content_text
text = " ".join(text.split())
text = process_text(text)
return text[:500]
except Exception as e:
# print(e, table_path)
try:
data = pd.read_csv(table_path, error_bad_lines=False, warn_bad_lines=False, lineterminator='n')
sheet_name_text = ''
try:
col_name_text = " ".join(data.columns.tolist())
except Exception as e:
col_name_text = ''
table_content_text = data.to_string(header=False, show_dimensions=False, index=False, index_names=False,
sparsify=False)
print("处理成功", table_path)
text = sheet_name_text ' ' col_name_text ' ' table_content_text
text = " ".join(text.split())
text = process_text(text)
return text[:500]
except Exception as e:
print(e, table_path)
sheet_name_text = ''
col_name_text = ''
table_content_text = ''
text = sheet_name_text ' ' col_name_text ' ' table_content_text
text = " ".join(text.split())
return text[:500]
if filename.endswith('csv'):
try:
data = pd.read_csv(table_path, error_bad_lines=False, warn_bad_lines=False, lineterminator='n')
sheet_name_text = ''
try:
col_name_text = " ".join(data.columns.tolist())
except:
col_name_text = ''
table_content_text = data.to_string(header=False, show_dimensions=False, index=False, index_names=False,
sparsify=False)
print("处理成功", table_path)
text = sheet_name_text ' ' col_name_text ' ' table_content_text
text = " ".join(text.split())
text = process_text(text)
return text[:500]
except Exception as e:
print(e, table_path)
sheet_name_text = ''
col_name_text = ''
table_content_text = ''
text = sheet_name_text ' ' col_name_text ' ' table_content_text
return text
if __name__ == '__main__':
label_index = {
'工业': 0,
'文化休闲': 1,
'教育科技': 2,
'医疗卫生': 3,
'文秘行政': 4,
'生态环境': 5,
'城乡建设': 6,
'农业畜牧业': 7,
'经济管理': 8,
'交通运输': 9,
'政法监察': 10,
'财税金融': 11,
'劳动人事': 12,
'旅游服务': 13,
'资源能源': 14,
'商业贸易': 15,
'气象水文测绘地震地理': 16,
'民政社区': 17,
'信息产业': 18,
'外交外事': 19}
train = pd.read_csv('data/answer_train.csv')
test = pd.read_csv('data/submit_example_test1.csv')
train['label'] = train['label'].map(label_index)
with mp.Pool(8) as pool:
train['content'] = pool.map(get_file_content_v1, train['filename'])
# train['content'] = train['filename'].parallel_apply(get_file_content).values
print("over")
train['text'] = train['text'].astype(str) ' ' train['content'].astype(str)
with mp.Pool(mp.cpu_count()) as pool:
test['content'] = pool.map(get_file_content_v1, test['filename'])
# test['content'] = test['filename'].parallel_apply(get_file_content).values
test['text'] = test['text'].astype(str) ' ' test['content'].astype(str)
train_df = train[['text', 'label']]
test_df = test[['text', 'label']]
train_df.to_csv('data/train_set_v3.csv', index=None)
test_df.to_csv('data/test_set_v3.csv', index=None)
5.2 模型训练
思想很简单:多模型融合,分类问题本质就是要减少模型差异化
代码语言:javascript复制import re
import numpy as np
import pandas as pd
from simpletransformers.classification import ClassificationModel, ClassificationArgs
from sklearn.metrics import accuracy_score
def get_clean(text):
r1 = '[a-zA-Z] '
text = re.sub(r1, '', text)
text = text.replace('\', '')
return text[:300]
train_df = pd.read_csv('data/train_set_v3.csv')
test = pd.read_csv('data/test_set_v3.csv')
train_df['text'] = train_df['text'].apply(lambda x: get_clean(x))
test['text'] = test['text'].apply(lambda x: get_clean(x))
print(test['text'])
print(train_df.shape, test.shape)
print(train_df.head())
train_tmp = pd.read_csv('data/answer_train.csv')
train_df = train_df.sample(frac=1., random_state=1024)
eval_df = train_df[54000:]
train_df = train_df[:54000]
label_index_inverse = {
0: '工业',
1: '文化休闲',
2: '教育科技',
3: '医疗卫生',
4: '文秘行政',
5: '生态环境',
6: '城乡建设',
7: '农业畜牧业',
8: '经济管理',
9: '交通运输',
10: '政法监察',
11: '财税金融',
12: '劳动人事',
13: '旅游服务',
14: '资源能源',
15: '商业贸易',
16: '气象水文测绘地震地理',
17: '民政社区',
18: '信息产业',
19: '外交外事'}
models = [
('bert', 'hfl/chinese-roberta-wwm-ext'),
('xlnet', 'hfl/chinese-xlnet-base'),
('bert', 'schen/longformer-chinese-base-4096'),
('bert', 'voidful/albert_chinese_base'),
('bert', 'clue/roberta_chinese_base'),
('electra', 'hfl/chinese-electra-base-discriminator'),
]
for i in range(len(models)):
print("training {}".format(models[i][1]))
model_args = ClassificationArgs()
model_args.max_seq_length = 150
model_args.train_batch_size = 32
model_args.num_train_epochs = 5
model_args.fp16 = False
model_args.evaluate_during_training = True
model_args.overwrite_output_dir = True
model_args.cache_dir = './caches'
model_args.output_dir = './outputs'
model_type = models[i][0]
model_name = models[i][1]
model = ClassificationModel(
model_type,
model_name,
num_labels=len(label_index_inverse),
args=model_args)
model.train_model(train_df, eval_df=eval_df)
result, _, _ = model.eval_model(eval_df, acc=accuracy_score)
data = []
for i, row in test.iterrows():
data.append(row['text'])
predictions, raw_outputs = model.predict(data)
sub = pd.read_csv('data/submit_example_test1.csv')[['filename']]
sub['label'] = predictions
sub['label'] = sub['label'].map(label_index_inverse)
print(sub.shape)
print(sub.head(10))
result_name = models[i][1].split('/')[1]
np.save('result/{}_{}.npy'.format(i, result_name), raw_outputs)
sub.to_csv('result/{}}_{}.csv'.format(i, result_name), index=False)
5.3 模型融合
直接将不同模型的预测概率相加平均
代码语言:javascript复制import numpy as np
import pandas as pd
import os
label_index_inverse = {
0: '工业',
1: '文化休闲',
2: '教育科技',
3: '医疗卫生',
4: '文秘行政',
5: '生态环境',
6: '城乡建设',
7: '农业畜牧业',
8: '经济管理',
9: '交通运输',
10: '政法监察',
11: '财税金融',
12: '劳动人事',
13: '旅游服务',
14: '资源能源',
15: '商业贸易',
16: '气象水文测绘地震地理',
17: '民政社区',
18: '信息产业',
19: '外交外事'}
pred = None
for file in os.listdir('result/'):
if file.endswith('.npy'):
if pred is None:
pred = np.load('result/{}'.format(file))
else:
pred = np.load('result/{}'.format(file))
predictions = np.argmax(pred, axis=1)
sub = pd.read_csv('data/submit_example_test1.csv')[['filename']]
sub['label'] = predictions
sub['label'] = sub['label'].map(label_index_inverse)
print(sub.head(10))
sub.to_csv('result/ensemble_mean.csv', index=False)
5.4 数据增强
对于样本少的类别,使用表格内容进行扩充数据,数据增强之后初赛线上效果稍微有些提升
代码语言:javascript复制import pandas as pd
from tqdm import tqdm
def aug_df(df=None, selected=[15, 16, 17, 18, 19], text_len=100):
"""
工业 8542
文化休闲 7408
教育科技 7058
医疗卫生 5750
文秘行政 5439
生态环境 4651
城乡建设 3722
农业畜牧业 2703
经济管理 2516
交通运输 2250
政法监察 2159
财税金融 1784
劳动人事 1759
旅游服务 1539
资源能源 1209
商业贸易 652
气象水文测绘地震地理 375
民政社区 349
信息产业 108
外交外事 27
label label_n
0 工业 0
1 文化休闲 1
2 教育科技 2
3 医疗卫生 3
4 文秘行政 4
5 生态环境 5
6 城乡建设 6
7 农业畜牧业 7
8 经济管理 8
9 交通运输 9
10 政法监察 10
11 财税金融 11
12 劳动人事 12
13 旅游服务 13
14 资源能源 14
15 商业贸易 15
16 气象水文测绘地震地理 16
17 民政社区 17
18 信息产业 18
19 外交外事 19
:param df:
:return:
"""
# 外交
# tmp0 = df[df.label == 15]
# tmp1 = df[df.label == 16]
# tmp2 = df[df.label == 17]
# tmp3 = df[df.label == 18]
# tmp4 = df[df.label == 19]
data_list = []
data = df[df.label.isin(selected)]
for index, row in tqdm(data.iterrows()):
# print(index,row)
text=row.text
if len(text) > 300:
text=text[:1500]
# print("====" * 2000)
filename = text.split(' ')[0]
candidate_text = text.replace(filename, '')
# print([filename, candidate_text])
for text_index in range(0, len(candidate_text), text_len):
# print(text_index)
data_list.append([file_name ' ' candidate_text[text_index:text_index 70], row.label])
result = pd.DataFrame(data_list, columns=['text', 'label'])
result = pd.concat([df, result], axis=0)
return result
初赛自己的思路其实蛮简单,无非数据预处理部分下了点功夫,我们通过复赛总结来看看可以学习到什么?
6 复赛总结-失败的原因
6.1 自身的原因:
由于复赛大量表格中没有文件名,导致初赛的基于title的融合方案效果有限,这个其实由于初赛前排之后没有花时间去尝试只基于content的实验,权重没有保存以及复赛基于simpletransfors训练卡顿等问题,最终崩盘。
6.2 对手的实力
非常感谢大佬分享的方案,我们在这篇文章中可以找到不少亮点:
文章链接:https://mp.weixin.qq.com/s/LgrnMtIvsUTLHzVeT3sx5g
- top6思路:https://github.com/jackhuntcn/onecity2020_6th
同时通过对比「答案」,发现有文件名的部分,准确度已经相当高了,单折也只有 8 个错误,所以可以将精力放在如何提高无文件名模型的精度上。
因为训练集中的文件内容有很多重复的(但是文件名并不重复,甚至 label 也不同),所以仅使用文档内容进行训练时需要先去重处理,训练集经过清理后只剩下 20000 个样本
增大文本输入长度为 512,使用 sliding windows 对文本做切割成 192 seq len 的文本,训练及推断的结果使用投票机制,线上 0.933 ( 0.002);
尝试不清理非中文字符,线下 acc 比只使用中文字符下降 0.01 左右,所以放弃了
文档内容模型增加一折(十折中的两折),线上 0.937 ( 0.004);再增加两折(十折中的四折),线上 0.939 ( 0.002)
最终我的提交是 十折中的两折全文本模型 十折仅文档内容模型 后处理,线上 0.93934,排名第六。
- 第一名 @挥霍的人生
分为文件名模型和文档内容模型
花了很多时间在预处理
模型为 TextCNN,seq len 7000 (char 级别),没有清理非中文字符
TextCNN softmax 之前拼接了文本、文件的统计特征,如文本总长度、文件的大小(指占了磁盘多少的文件大小)
半监督,利用初赛模型去判定初赛测试集,做伪标签
- 第二名 @第二次打比赛-小迷弟
利用规则直接找到测试集 17000 个样本的答案
剩下的 8000 个样本只采用仅文档内容模型
文本增强,训练集 初赛测试集 复赛测试集所有中文表头转化为拼音首字母,再找复赛测试集中的拼音首字母转化为中文,等
seqlen 192 的 BERT base
- 第四名 亿万少年的梦
分为文件名模型和文档内容模型
BERT / TextCNN / TF-IDF 做 stacking
开源整理
- 第二名 (https://github.com/ymcdull/onecity)
- 第四名(https://github.com/Mandule/OneCity-2020)
- 第六名 (本方案)(https://github.com/jackhuntcn/onecity2020_6th)
- Top 4%(https://github.com/DLLXW/data-science-competition/tree/main/dc竞赛/one-city编程大赛
7 总结
- 对于NLP、ML等常规化比赛进行代码复盘总结,形成自己的一套体系:包括代码规范和思路,相比模型带来的收益,其中强调一点代码写得好真的可以节省很多时间,尤其对于复赛时间紧凑的比赛更为重要
- 敢于尝试,要不断的尝试,尝试次数越多,你就能发现要优化的思路和方向。