深度学习

模型研究报告

作者:@Galaxyzeta + @DDDFaker

1 初步分析

由于一开始对于 Python 处理结构化数据不是很熟,再加上拿到规模较大的数据集不知该从何处入手,算法研究小组决定先用Excel进行数据探索。

拿到数据的时候,发现有四张表格,均为csv格式,其中两张表格为各企业的年度数据,另两张表格是各企业的基本概况,由此发现此题目需要参赛者对多表数据进行合并。

(问题定性)

首先观察训练集数据,发现 flag = 1 或者 NA,而验证集中 flag 存在0、1、NA、blank几种情况,由此想到NA、blank是数据缺失,而0、1是我们要预测的值,由此该问题可以定性为二分类。而训练集中的flag = 1或NA,没有0,因此不能直接拿来训练,而且验证集的数据条目数远远大于训练集,若按照原集训练显然是不合理的,因此考虑将验证集数据集合并,然后按照一定的比例进行划分。

(特征工程)

查阅相关资料,结合题目要求,我们对什么是僵尸企业,如何划分僵尸企业有了初步的认知。文献对僵尸企业的描述多半为 “连续亏损,扭亏无望,靠政府资助”,描述十分模糊,没有具体的定量描述。根据这些描述,我们首先重点考虑企业的收支、盈利、负债、所有者权益以及各类总额状况,认为这些特征与预测结果具有强相关性。

经过多表数据的 Excel 融合,继续通过 flag 列筛选,观察僵尸企业/非僵尸企业的共性特征,结果发现知识产权、控制人持股比例的分布过于随机,因此暂时认为它们与预测结果具有弱相关性。

由于给出的原始数据分布差异十分明显,不在同一数量级,再加上不同企业的行业分类、地域、服务种类、企业性质等存在差异,所以不同企业的收支、负债情况显然是存在明显差异的,因此我们认为不能直接把部分原始数据套到模型中训练,而是应该计算相应的比例,再行训练。

(缺失值处理)

经筛选,表格中存在大量缺失值,因此对缺失值的处理显得尤其重要,对于缺失值,初步提出若干处理思路:

  1. 考虑到训练的准确性,一条记录中不允许出现NA值,全部抛弃;
  2. 用一列的均值填充NA值,然后进行计算;
  3. 计算时若遇到NA值,计算结果仍然为NA,计算完毕后填充NA值;
  4. 根据不同企业的地域、性质、行业等填充相应的平均值。

(小结)

经过初步分析,我们大致确定了计划:先进行数据预处理/特征工程,然后建立简单的机器学习模型进行训练。

2 数据处理

一开始用的是 Excel 表格处理,但由于其面对大量数据的处理效率实在太低,再加上不是自动化的,后来全面转向 python pandas 数据处理,为了进一步提升效率和处理准确性,期间代码重写过多次。

为了方便地检验特征值相关性以及添加新地特征值,目前的数据处理是针对全数据的处理,为了特征值提取的方便,设计了填充/抛弃 NA 数据,仅提取 NA flag,任意获得输出的特征值列等功能。

函数定义:

其中,f1、f2、f3、f4是给出的 训练集/验证集csv,out 是输出csv,fillna 为 False 时,采取严格的数据筛选模式,即一遇到NA就舍弃,给定数据少于三年同样舍弃;fillna 为 True 时,采取尽可能准确的 NA 值填充策略。当flagNa 为 True 时,输出结果仅保留 Na 值 flag,供半监督学习使用;flagNa 为 False 时,输出结果舍弃 Flag = Na。

数据处理的步骤大致分为以下几点:

  1. 读取csv,多表合并
  2. 数据清洗,NA值填充
  3. 因某些未知原因,在处理过程中遗失的 ID 需要被重新处理(发现这是计算三年数据每两年之间差异导致的)。

具体阐述

  1. 表头手动替换成英文(后面我会把这个过程改成自动进行)、表中中文数据(交通运输业、有限责任公司等等)自动替换英文,这样可以避免中文处理导致的编码问题。

  2. 若存在ID不确定,直接去除(事实上没有这种情况的发生)。

  3. 若 flag = NA,不论是在训练集还是验证集,由于其不可用于全监督学习,故直接去除(函数设计了只保留NA值的方法,用于半监督提取数据用)。

  4. 根据企业属性的不同组合计算出平均值参考表,根据此表填充年报表格的 NA 值,知识产权表格默认填充0,融资额度/数值 表格的数据暂不填充,在计算后,若存在NA则填充平均值。

  5. 分组计算比例数据,主要有:

    • 融资成本除以融资额度
    • 负债比例=负债/总资产,主收入比例=主收入/总收入,纳税比例=纳税/总资产,所有者权益比例=所有者权益/总资产
    • 三年数据中,每两年之间的比例数据差异及人口差异。(此操作虽然能增加标签数,但会导致数据损失,如果比赛后期最终确定不需要使用这些新增的特征值,这一步可以不要)
  6. 为表中的枚举属性值建立One hot编码列。

  7. 人工选择需要抛弃的特征值,且根据传入的参数,选择是否仅保留 NA Flag。

  8. 经过处理的数据与给定的原企业ID信息比对,找出在处理过程中丢失的ID,并将其写入文件,以便后续处理。(目前处理方式是将其flag直接定为0,在预测后将缺失的数据与已经预测的数据合并,然后再计算预测精度。)

关于数据预处理的效率

对于10000条记录~50000条记录的单次处理可以在大约2秒内完成,效率还算不错。

3 模型设计

研究模型时提出了多种方向

  1. 最初设想用多层感知机模型,这符合二分类的一般做法。
  2. 观察到给出的年份大多为3年,具备时序,想过把数据转变为图片进行处理,但由于转化过的图片过小(宽度只有3),且其中的关系可以直接用新增标签来实现,显然通过CNN来解决问题是不合适的。
  3. 想过RNN,但给定的时序太短,且主键不是时间,是ID,所以RNN是不适合的。
  4. 半监督学习,通过已知模型,给flag = NA 的数据打伪标签,从而达到扩充数据集的作用,但增强效果不明显。
  5. Sklearn 封装模型,尝试多个模型后发现 LinearSVC 和 SGDClassifier 对于特征较少且准确的情况下,效果相对较好。

 

多层感知机模型及其超参数设置:

重点研究和调整的是多层感知机模型,该模型可以表述如下:

Input -- BN -- Dense(X, relu) -- BN -- Dense(Y, relu) -- Dense(1, Sigmoid)

输入数据阶段,用 normalize() 进行 L1 正则化,目的有二:

  1. 减小数据之间的分布差异。
  2. 防止大量epoch训练产生过拟合。

此过程直接对数据进行,不包括在模型内。

(以下不保证准确,我不太清楚其中原理)

其中,BN 代表 Batch Normalization,批规范化。引入批规范化的优势在于:

  1. 可以有效防止过拟合。
  2. 可以加速模型训练的收敛速度。
  3. 减小数据数值分布差异过大带来的权重调节问题。

批规范化不宜过多,尤其是这种层数不多的模型,批正则化过度会导致训练精度上不去。

Dense是中间隐层,可以根据需要增加或减少。增加 Dense 层深度和特征维度能加快模型收敛速度,同时增加模型复杂度和过拟合风险,实验结果表明两层中间隐层的效果是相对最好的。X设置为4,Y设置为8是在有BN层的前提下,否则收敛速度过慢。

使用 Relu 函数有对抗梯度消失、加速收敛、计算效率高等诸多好处。

最终层采用 Sigmoid 激活函数输出分类预测概率,理论上,二分类问题采用 sigmoid 和 softmax 是等价的,但根据网友实践,二分类问题采用 sigmoid 实际效果更好。而且 sigmoid 作为输出层激活函数,只需要1个输出层神经元,比起 softmax 的两个神经元,减少了少量不必要的计算。

损失函数选择了均方损失误差(mean squared error),选择二分类交叉熵(binary cross entrophy)也可,但经过测试,感觉MSE更可靠。

模型优化器采用SGD,这种优化器是所有优化器中收敛速度相对最慢的,但好处是结果准确可靠,以下是一些超参数调节:

  1. 为适当提升收敛速度,采用业界标准0.9动量加速因子
  2. 由于训练会持续数十个/数百个epoch,将学习率调低,且改为动态衰减,这样有利于训练精度已经较高的情况下更好地提升精度,也在梯度较大处更好地逼近最优解。经过多次尝试,将学习率设为0.001,每epoch衰减0.00002。
  3. 为了减轻梯度下降方向的错误,采用nesterov方法,与动量配合,使梯度下降方向得到修正。
  4. batchsize 设置为 32,这是由供训练的数据集规模不大决定的,也是BN正常工作的基础,即 Mini-batch。

 

训练

暂时采取四特征值作为训练依据,即:

参与训练的数据为原训练集全体Flag = 1的数据,加上等量从验证集获得的Flag = 0的数据(取出之后对应的行会被删除,保证训练与验证的数据是完全不同的),训练之前数据经过了随机排列。这样安排主要是考虑到验证集中完整的数据多余经过NA值填充的数据,用完全 “干净” 的数据训练比拿 “经过少量污染” 的数据训练效果更好。

训练epoch预设1000,实际上大约50epoch,模型就已经收敛,在训练过程中自动记录 Accuracy 最高的模型,作为最终训练的模型。训练的数据集为经过严格筛选的数据(3年完整不含NA数据),训练过程中取经过NA值填充的数据记录准确度。

最终四特征值下得到的模型在划分的测试集下,得到98.7%的准确度,随机抽样测试多次,准确度几乎不变,从而初步检验了模型的稳定性和准确性

 

关于半监督学习

由于训练集中存在大量NA值数据。验证集中也存在部分NA值数据,由此想到可以将这些数据提取出来,用已有模型对其打 “伪标签”,然后再将这些数据添加到训练集中重新训练,这属于 “数据增强” 的一种手段。但经过尝试,增强效果几乎没有体现。

 

模型的打标签效率

万级别数据打标签大约2s即可完成,其中不包括 tensorflow 预加载的过程。

 

4 调参过程

(这部分真不知道该怎么写,调参靠感觉,我不记得有一个明确的调参过程)

一开始已经确定要用多层感知机模型,调参主要集中在特征工程,模型修改,超参数调节三个方面:

特征工程调节

一开始尝试把给出的所有标签数据全部传入模型进行训练,但效果非常不好。后来根据数据之间的关系,分别衍生出更多看似合理的标签,逐渐减少特征,最终发现四特征值下模型的表现是最好的。

有时候训练 loss 不降反升,就是数据预处理出现问题,其中可能包含 inf / NA 等数据;有时训练 loss 波动特别的大,是 NA 值填充错误导致的。

个人认为在本场景下,数据的处理比模型的调节更为重要。

 

模型调节

主要体现在是否使用 Batch Normalization,需要多少 dense Layer,每个dense Layer 怎么设置神经元的个数这几个问题。

把Batch Normalization 去掉,将会严重影响训练收敛速度,通过增加每层神经元个数,虽然速度有一定程度提高,但训练到准确度较高时,损失函数几乎不下降,精度也难以提升。

想到增加 Batch Normalization 主要是之前在 CNN 图片分类时使用过,效果很明显。增加 BN 后,每层神经元个数可以大量下调,但训练速度依旧非常快,且不会影响最终训练的精度。

关于dense Layer的选择:尝试过1、2、3、4层中间隐层,4层中间隐层收敛速度极快,但一段时间后训练过拟合,验证集 loss 快速上升。1层中间隐层因不能很好表达特征,最后结果没有2层中间隐层好。

关于每层应该设置几个神经元:和dense Layer选择同理,主要考虑收敛速度和模型复杂度之间的关系。

 

超参数调节

采取吴恩达机器学习视频中的3倍率二分法调参,每次调参结束后绘制 loss - acc 曲线,对比后确定了合适的学习率。由于恒定学习率对于模型后期的收敛是不利的,因此考虑设定动态变化的学习率,即经过一个 epoch 学习率衰减指定值。测试发现 2e-5 到 3e-5 的衰减是比较好的。