ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Bagging集成深度学习模型提升财务造假预测AUC的完整实践

Bagging集成深度学习模型提升财务造假预测AUC的完整实践 简介面向机器学习与财务风控从业者这套基于Bagging与深度学习的上市公司财务数据造假预测方案完整覆盖从制造业、其他行业特征工程到二分类模型训练与预测的全流程适合数据挖掘竞赛、毕业设计或企业财务异常识别场景的复现与二次开发。包内共33个文件以15个CSV数据文件、9个Python源码脚本、3张效果图片为主另附Excel附件、PDF说明与运行文档整体约35.46MB目录按数据、特征、模型、预测等模块组织便于按需取用。已有191人学习。资源实现了SMOTE过采样与四种树模型的特征重要性集成筛选并给出多层感知机、残差网络与Cross网络组成的DCRN模型及Bagging集成代码包含特征榜单、ROC曲线、训练好的模型文件与运行说明可帮助读者理解特征筛选、交叉组合、梯度消失缓解和降方差集成的完整思路快速迁移到真实财务异常检测场景。1. 财务造假预测为什么是 Bagging 深度学习的舞台一个基模型不够用的真实原因先说结论单靠一个深度学习模型去识别上市公司财务造假在公开数据集上通常只能做到 0.750.78 的 AUC而用 Bagging 把 812 个结构相同但训练子集不同的基模型集成起来同一个测试集上 AUC 能抬到 0.85 以上预测分数的方差还能缩减一半左右。这个差距不是玄学而是财务造假数据本身的分布特征决定的正样本占比通常不到 3%造假样本和正常样本在指标空间里高度重叠造假手段还会随年份和市场规则漂移。这些恰好是单个高容量深度模型最容易过拟合、最容易把记忆当成泛化的场景。本文要拆解的是一套“源码 数据集 模型 运行说明”式的完整方案。我会从监管处罚公告和三大报表出发一步步构造监督学习数据集训练一个以深度神经网络为基学习器的 Bagging 集成模型给出可直接复现的基线参数并把最容易翻车的五个位置写透。适合手里握着审计线索或风控诉求、准备实际投入这个方向的从业者如果你是第一次接触财务造假预测这篇也能让你少走一个月的弯路。2. 把造假预测切成监督问题标签、特征与时间线设计2.1 造假标签的生成处罚公告、追溯期与窗口对齐构造数据集的第一步是从监管处罚公告里提取“谁、从哪一年、到哪一年”。我既用过手工整理版也写过解析公告的脚本核心逻辑是相通的公告里通常有一句“涉嫌信息披露违法违规——在 XXXX 年至 XXXX 年定期报告中存在虚假记载”这句话就决定了正样本的区间。需要特别注意的是一个处罚决定覆盖多个年度时要为每个年度各生成一条正样本而不是只给一条。import pandas as pd # penalties.csv 字段stock_code, begin_dt, end_dt # 该表来自处罚公告的结构化结果不是原始公告文本 penalties pd.read_csv(penalties.csv, parse_dates[begin_dt, end_dt]) records [] for _, row in penalties.iterrows(): # 造假区间覆盖到的每一个财年各生成一个正样本 for year in range(row[begin_dt].year, row[end_dt].year 1): records.append({ stock_code: row[stock_code], fiscal_year: year, target: 1, }) positives pd.DataFrame(records)这段代码的关键是窗口对齐。处罚公告公布的造假年份和模型真正能看到的报表数据之间存在时间差比如一家公司 2018 年和 2019 年造假2021 年才被处罚那么模型在 2019 年底并不知道这个事实只能用截至 2019 年年报披露日的数据做特征。后续合并特征时必须把披露日期作为 join 的锚点而不是把标签和特征简单地按年份对齐否则就会出现用自己的“未来”去预测自己的“过去”这种标签泄漏——这是造假预测项目里最常见、也最隐蔽的错误。正样本构造完负样本就是样本池里所有未被处罚的(stock_code, fiscal_year)组合。一个容易忽视的问题是有些公司造假没被发现不等于没有造假它们被当成负样本只是给定数据集约束下的近似。这个问题没有完美解法但对模型的影响有限因为漏网的“假阴性”样本在全部负样本里占比很小而且单个噪声样本对 Bagging 集成的干扰远小于它对单模型的干扰。2.2 特征工程从三大报表和审计意见里抽取 40 个指标标签定好后就要把每个(stock_code, fiscal_year)变成定长的数值向量。我的经验是不要直接把原始会计科目喂给深度网络哪怕深度模型理论上能自己学出组合特征。原因有两个一是原始科目分布长尾极重个别公司营收几十亿上百亿多数公司只有几亿不取对数或不做比率归一化网络注意力会被头部样本带走二是原始科目之间线性相关性太高深度模型在这种输入上容易学到表层规律而不是造假行为的结构性信号。常见做法是构造比率特征和差分特征。我习惯把基线特征集控制在 40 个左右核心包括应收账款 / 营业收入、存货 / 营业收入、经营现金流净额 / 净利润、非经常性损益 / 净利润、应收账款增速与营收增速之差、毛利率的跨年波动、存货周转天数变化、资产负债率、流动比率、审计意见类型虚拟变量、高管是否变更、是否收到过问询函。import numpy as np def build_features(statements: pd.DataFrame, external: pd.DataFrame) - pd.DataFrame: # statements: 长表每行是 (stock_code, report_date, item, value) # external: 审计意见、高管变更、问询函数字表 pivoted statements.pivot_table( index[stock_code, report_date], columnsitem, valuesvalue, aggfuncfirst ).reset_index() eps 1e-6 pivoted[ar_to_revenue] ( pivoted[accounts_receivable] / (pivoted[revenue] eps) ) pivoted[inventory_to_revenue] ( pivoted[inventory] / (pivoted[revenue] eps) ) pivoted[op_cashflow_to_profit] ( pivoted[op_cash_flow] / (pivoted[net_profit] eps) ) pivoted[nonrecurring_ratio] ( pivoted[nonrecurring_gain_loss] / (pivoted[net_profit] eps) ) pivoted[gross_margin] ( (pivoted[revenue] - pivoted[operating_cost]) / (pivoted[revenue] eps) ) pivoted[debt_ratio] ( pivoted[total_liabilities] / (pivoted[total_assets] eps) ) pivoted[quick_ratio] ( (pivoted[current_assets] - pivoted[inventory]) / (pivoted[current_liabilities] eps) ) return pivoted.merge( external, on[stock_code, report_date], howleft ).fillna(0.0)这段代码背后的逻辑是把绝对数值换成比值消除公司规模的影响。eps 1e-6是为了分母为零时不会直接出inf这是财务数据处理里最常见的细节问题。所有比率列拼接后还要做一步跨年差分——例如 T 年应收账款增速与 T-1 年营收增速的差这个差在真实造假样本上往往显著为正因为它捕捉的是“收入没怎么涨、应收账款先涨了”的异常信号。这一步常用groupby(stock_code).diff()实现建议在训练集和测试集上分别执行避免把全局统计量混入训练过程。2.3 样本划分与类别不平衡先定时间线再讲算法财务数据预测最忌讳随机划分训练集和测试集。同一家公司的两个年度样本高度相关——2018 年的报表数据一定是 2019 年样本的输入一部分随机划分会把这种序列相关性带进验证结果造成 AUC 虚高。常见做法是按自然年切分某年之前的所有样本进训练之后的进测试并且保证同一个股票代码的所有样本只出现在一侧。train_year_max 2017 train_df samples[samples[fiscal_year] train_year_max] test_df samples[samples[fiscal_year] train_year_max]这个切分看起来简单实际上是最重要的一道防线。造假预测场景里正样本质量远比数量重要几千个正常样本加上 80 个真实造假样本信息量远大于加上 800 个疑似但不干净的样本。类别不平衡的处理我一般分两步走先让训练集里正样本的比重控制在 10%20%通过负样本欠采样再用集成中每个基模型使用不同随机子集的方式保留多样性。我不太会在数据层做 SMOTE 这种插值因为财务指标的造假信号很稀薄插值容易生成“跨界样本”骗得过模型但骗不过真实场景。3. 用 Bagging 包装深度模型训练、集成与预测的最小实现3.1 为什么 Bagging 能压住深度模型在财务数据上的方差单个深度神经网络是高方差估计器同样的数据、换个随机种子训练出来的预测分数可以差 0.05 个 AUC。这在风控场景里很要命因为风控需要一个稳定的分数不是“这次碰巧跑出来的好成绩”。Bagging 的核心思想是构造多个分布近似的子数据集分别训练基模型再把预测结果做平均或投票最终预测的方差随基模型数量近似线性下降。财务造假数据正样本少、数据分布不对称这种场景下 Bagging 比 Boosting 更稳Boosting 会在异常样本上不断放大权重而造假样本本身噪声很大权重放大到最后往往拟合的是处罚边界的不确定部分而不是造假行为本身。从另一个角度看Bagging 也天然适配深度网络的“随机性”本质。每个基模型用不同子集启动相当于在模型空间里做了一次采样最后平均时每个模型的偏置被保留方差被抵消。对于造假预测这种需要给出稳定概率分数的任务集成后的输出排序更稳定阈值也更跟手。3.2 构造子训练集自助采样与分层采样实现 Bagging 深度模型最常见有两种构造子集的方式纯自助采样和分层采样。纯自助每次从原始训练集随机有放回地抽 n 个样本正样本占比会随抽样波动基模型之间差异大但有些基模型可能正样本占比过低学不到造假信号。我一般用分层采样每次固定从正样本里抽 70%80%负样本按正样本数量的 46 倍随机抽取这样每个基模型都有足够且相似的正样本。from sklearn.utils import resample n_models 10 base_sample_ratio 0.8 n_positive int(train_df[target].sum()) n_negative_per_model n_positive * 5 sub_sets [] for i in range(n_models): pos train_df[train_df[target] 1] neg train_df[train_df[target] 0] pos_sub resample( pos, replaceFalse, n_samplesint(len(pos) * base_sample_ratio), random_state42 i ) neg_sub resample( neg, replaceFalse, n_samplesmin(n_negative_per_model, len(neg)), random_state100 i ) sub_sets.append(pd.concat([pos_sub, neg_sub]))这段代码有两个参数值得单独说。base_sample_ratio控制每个基模型看到了多少比例的正样本选 0.8 是平衡精度和召回率的常用值n_negative_per_model控制类别不平衡程度设成正样本的 5 倍是为了让每个基模型不会被少数几个异常样本牵制也不会因为负样本太多把训练时间拖到不可接受。random_state42 i这个细节最容易被忽略——如果不给每次抽样不同的随机种子十个基模型拿到的是完全相同的子集Bagging 就会退化成训练了十个一样的模型预测平均之后毫无增益。这不仅浪费算力还会让模型的稳定性和单体模型完全一致等于白绕了一圈。3.3 基分类器设计用 MLP 和 TCN 各做一版再融合基学习器选型上财务造假预测最常用的是多层感知机MLP因为特征已经是定长向量不需要卷积或序列模型。MLP 的优势在于实现简单、训练稳定、超参数敏感度低缺点是忽略了特征之间的时间结构。如果你想更进一步可以把连续三年的特征拼成序列用 TCN时间卷积网络作为基学习器但 TCN 的训练时间会翻好几倍在小样本上并不一定比 MLP 更好。这里给出一个完整的 MLP 基模型定义包含两个连续的线性层、批归一化、Dropout 和输出层。import tensorflow as tf from tensorflow.keras import layers, models, regularizers def build_mlp(input_dim: int) - tf.keras.Model: inputs layers.Input(shape(input_dim,), namefinancial_features) x layers.BatchNormalization()(inputs) x layers.Dense( 128, activationrelu, kernel_regularizerregularizers.l2(1e-5) )(x) x layers.Dropout(0.3)(x) x layers.Dense( 64, activationrelu, kernel_regularizerregularizers.l2(1e-5) )(x) x layers.Dropout(0.3)(x) outputs layers.Dense(1, activationsigmoid)(x) model models.Model(inputs, outputs) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossbinary_crossentropy, metrics[tf.keras.metrics.AUC(nameauc)] ) return model结构上没有特别花哨的地方但每个组件都有针对性。BatchNormalization 放在输入端是因为财务特征经过标准化后仍可能有小批量内的分布偏移它能避免模型在训练初期被少数极端样本带偏。两个 Dropout 层分别放在中间层之后是为了在基模型内部做正则化让 Bagging 之外再叠加一层抗过拟合能力。L2 正则的系数只给 1e-5不能太大否则在造假样本占比极低的情况下模型会倾向于把所有样本预测成 0网络结构再好也学不到有效信号。3.4 训练循环、模型保存与投票/平均策略Bagging 训练循环比较直接依次在子集上调用基模型 fit然后存到一张列表里最终预测时对所有模型输出的概率做算术平均。需要强调的细节是在下采样后的子集上训练时每个基模型的验证集必须用原始测试集不能用子集里分出来的验证集否则验证分数会严重高估。models [] for i, sub in enumerate(sub_sets): sub sub.sample(frac1.0, random_state1000 i) X_sub sub.drop(columns[target]).values.astype(float32) y_sub sub[target].values.astype(float32) base_model build_mlp(input_dimX_sub.shape[1]) base_model.fit( X_sub, y_sub, validation_data(X_val, y_val), epochs200, batch_size64, callbacks[ tf.keras.callbacks.EarlyStopping( monitorval_auc, patience15, modemax, restore_best_weightsTrue ) ], verbose0 ) models.append(base_model) base_model.save(fmodels/bagging_mlp_{i:02d}.h5)训练时用了早停监控的是验证集 AUC 而不是 loss。原因是binary_crossentropy在严重不平衡的数据上数值上偏向多数类loss 下降不直接等于分类能力提升AUC 对类别比例不敏感更适合作为早停的监控量。patience15是财务小样本上比较常用的值数据量足够大时可以缩小到 10。注意每个基模型都用不同的 shuffle 种子虽然 MLP 对样本顺序不敏感但复现时多一重随机性控制就少一个不确定性来源。预测阶段的常见做法是对每个模型输出的概率取平均。这里用mean而不是硬投票因为财务造假打分场景要的是概率意义上的排序硬投票会把许多打分 0.5 附近的样本直接判给多数类平均概率保留连续分数方便之后设定阈值做不同风险等级分类。import numpy as np def ensemble_predict(models, X): preds np.column_stack([m.predict(X, verbose0).ravel() for m in models]) return preds.mean(axis1)4. 参数应该怎么设一组可直接复现的基线参数4.1 网络结构与正则化Dropout、早停、BatchNorm我的调参习惯是先把网络结构固定下来再调 Bagging 层面的参数避免两层超参互相干扰。基线网络固定为输入层 - 128 个神经元 - 64 个神经元 - 输出层激活函数用 ReLU输出层用 Sigmoid。128 这个宽度是兼顾表达能力和过拟合风险的常用起点财务特征通常只有 40 维左右128 个神经元已经能拟合很复杂的非线性边界再增加宽度对 AUC 的提升非常有限训练时间倒是近似线性增长。正则化三件套是 BatchNormalization、Dropout(0.3)、L2(1e-5)。如果训练集正样本数量只有几十个Dropout 建议从 0.3 提高到 0.5L2 保持不变如果发现验证集 AUC 在小样本下波动很大优先调整 BatchNormalization 的 momentum——默认 0.99 在小批量上不稳定可以降到 0.9。训练轮数和早停配合使用patience 设置在 1520 比较合适太小的 patience 在财务数据上太敏感经常因为一个 mini-batch 的波动过早停止。# 正则化参数基线速查作者常用值 # -------------------------------------------- # 隐藏层2 层128 - 64 个神经元 # Dropout 率0.3正样本极少时提到 0.5 # L2 系数1e-5 # BatchNorm momentum0.9默认 0.99 小批量不稳 # 学习率1e-3 # 早停 patience15监控 val_auc # batch_size64数据量小可用 32 # --------------------------------------------4.2 Bagging 轮数与采样率算算你的训练成本Bagging 轮数从 10 起步先小规模跑通流程再往 30 加。多数公开实验结果表明基模型数量超过 1520 个后预测分数平均的方差下降幅度逐渐趋缓继续增加模型主要带来训练时间和磁盘占用。考虑到财务造假数据集通常只有几千到几万行样本单个 MLP 在 CPU 上 30 秒内就能训练完跑 30 个模型也就是 15 分钟左右如果运行环境有 GPU这个时间会缩短到一两分钟。采样率直接决定每个基模型看到的正样本数量和负样本比例。前面用了base_sample_ratio0.8、正负比例 1:5这个组合适合正样本数量在 80200 之间的场景。正样本少于 50 时建议把采样比例调到 0.9正负比例放宽到 1:3因为此时每个基模型能看到的正样本太少即使有 Bagging单个基模型也很容易把仅有的正样本当成噪声记下来。正负比例在 1:5 到 1:7 之间通常是最稳定的区间过高的负样本比例会让每个基模型的训练时间变长AUC 提升有限而且会让阈值选择阶段过度偏向精确率漏掉真正的造假公司。4.3 决策阈值怎么定召回率优先还是精确率优先模型输出是一个 01 之间的概率拿多少作为“疑似造假”的分界点需要在验证集上单独选。选阈值之前先明确业务目标审计场景宁可多一些疑似线索把阈值压低一点用召回率换人工核查成本投资风控场景则相反建议优先精确率避免公开名单出现过多误报造成声誉成本。这两种目标对应不同的阈值选择方法。from sklearn.metrics import precision_recall_curve probs_val ensemble_predict(models, X_val) precision, recall, thresholds precision_recall_curve(y_val, probs_val) f1_scores 2 * precision * recall / np.maximum(precision recall, 1e-6) best_idx np.argmax(f1_scores) best_threshold thresholds[best_idx]用 F1 选阈值是中性默认值如果业务画像更匹配审计场景可以在 F1 阈值基础上向低阈值移动 0.050.1让召回率优先。这里用precision_recall_curve而不是 ROC是因为财务造假标签占比极低ROC 曲线在假阳性率很低的一段非常乐观无法反映少数类预测的真实困境PR 曲线对正样本的预测质量更敏感也更符合审计复核的直觉。5. 复现过程中最容易踩的五个坑现象、根因与解决5.1 标签泄漏把未来信息混进特征现象训练 AUC 0.99、测试 AUC 0.98漂亮得可疑一换到新年度数据上预测AUC 直接掉到 0.65。原因特征构造时把处罚公告日期之后的报表数据也放进特征了或者用全样本均值做了标准化。模型学到的是“处罚后调整过的报表长什么样”而不是“造假行为长什么样”。解决严格按披露日期拼接数据所有特征必须在样本截止日之前可见。用时间切分能挡住一部分泄漏但泄漏的产生点在特征工程前端必须在构造特征时就控制住不能只靠样本划分兜底。5.2 类别不平衡下模型全部预测为正常现象验证集 AUC 有 0.82但预测分数分布明显压在 0.3 以下没有任何一家公司被判定为疑似造假检查每个基模型发现输出概率的均值连 0.5 都不到。原因欠采样后正负比例还是 1:10负样本绝对量太大交叉熵损失主导了训练方向模型学到的最优解就是全部输出低概率。解决把正负采样比调到 1:5 以内并给损失函数增加class_weight。经验上class_weight{0: 1.0, 1: 3.0}是比较不容易过头的起点同时确保早停监控的是val_auc而不是val_loss。val_loss在负样本主导的验证集上会误导早停时机。5.3 时间穿越标准化参数用全量样本拟合现象测试集 AUC 很高但模型上线后每个月分数都在漂方向越来越差。原因归一化时用了全样本的均值和标准差把未来几个月的分布信息泄漏进了历史预测。解决StandardScaler 或任何归一化器只能用训练集 fit再 transform 验证集和测试集。比较稳妥的做法是把归一化步骤写进 sklearn Pipeline用同一个 pipeline 处理训练、验证、测试三份数据。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val) X_test_scaled scaler.transform(X_test)5.4 财务数据的分布漂移旧模型在新年份失效现象模型在 20082017 年数据上 AUC 有 0.87在 20182021 年数据上只有 0.71进一步看错判的样本集中在某些行业或新会计政策下的公司。原因财务造假手法和会计准则都在演化。比如新收入准则实施后收入确认方式变化导致多个收入和应收类比率的统计分布整体下移旧模型学到的是旧准则下的特征边界。解决训练集里固定加入最近 N 年数据的滚动窗口不要用全部历史数据一次训完。我一般把窗口设为 810 年每年重新训练一次并保留最近一年的 Bagging 模型作为线上版本。这是维护这类模型最朴素也最有效的方案。5.5 复现失败随机种子与依赖版本现象按说明跑源码每次结果都对不上甚至同一台机器上两次运行AUC 都有明显差异。原因train_test_split、resample、numpy 操作、TensorFlow 初始化每一处都引入随机性。最常见的复现失败是保存了模型但没有同步保存每个基模型的采样索引。解决在全流程入口设置三个种子random.seed(42)、np.random.seed(42)、tf.random.set_seed(42)每次resample固定 random_state。如果已经保存了模型但没保存子集索引基线很难完全复现。所以源码包里的模型、数据集、运行说明必须一起交付缺了任何一部分结果都可能变成另一套行为。依赖版本上TensorFlow 2.x 在 GPU 下有非确定性算法问题如果必须精确保留训练过程说明里冻结tensorflow-cpu和numpy的具体版本号比只写“TensorFlow 2.x”靠谱得多。6. 进阶验证滚动回测、可解释性与部署注意点6.1 用滚动时间窗口回测替代随机划分随机划分对财务时序数据来说基本不可信。更接近线上真实表现的验证方式是滚动窗口回测第一次用 20102014 训练、2015 做测试第二次用 20102015 训练、2016 做测试依此类推把每年的预测分数拼在一起画 PR 曲线。这种方式得到的指标通常会低于随机划分但它更接近模型上线后的真实水平。滚动回测还能暴露一个隐藏问题在造假处罚率高的年份模型指标表现往往更好到了处罚率低的年份PR 曲线会剧烈下探。这不是模型退化而是样本先验概率变化导致的。汇报效果时把每年的正样本数量一起报告接手的人就不会被单年指标误导。6.2 从黑匣子到可解释用 SHAP 找到关键造假特征财务造假预测在审计场景落地时办案人员不会接受一个完全不能解释的黑匣子。我一般会在 Bagging 预测概率上做 SHAP 分析输出每个样本的特征贡献排序。因为 Bagging 集成了多个模型SHAP 值要传模型列表逐个计算再取平均这个做法理论上只是近似实践上却非常好用。import shap import numpy as np def shap_for_bagging(models, X_reference, X_explain): all_shap [] for m in models: explainer shap.Explainer(m, X_reference) all_shap.append(explainer(X_explain).values) return np.mean(all_shap, axis0) shap_values shap_for_bagging(models, X_train_scaled, X_test_scaled)SHAP 结果常见的呈现方式是 beeswarm 图和单样本 force plot。结合我实际复核过的项目排名靠前的特征通常包括经营现金流净额 / 净利润、应收账款增速与营收增速之差、非经常性损益占比这跟审计实务里“利润与现金流长期背离”的关注点吻合。这种一致性反过来也能验证模型学到的确是真信号而不是某种偶然模式。6.3 部署与运行说明里必须写清楚的三件事其一是依赖清单。TensorFlow、numpy、pandas、scikit-learn 的版本要逐项锁死requirements.txt里用而不是。其二是目录结构。源码、数据集、模型、运行说明四个部分要互相独立模型文件单独放——深度学习模型常有.h5和.keras两种版本混在源码里容易在版本迁移时踩坑。其三是数据检查脚本。主流程运行前先做缺失值比例、异常日期、跨年重复计数、目标列占比统计这些跟模型无关的小事决定了一个方案能不能顺利交接给别人长期维护。交付一份工程方案最负责的方式不是把数据和模型打包了事而是把上面三件事写进运行说明的开头让接手的人按说明从原始数据到预测结果完整跑通一遍再把预测结果与说明里的示例输出逐位对比。能对得上才说明这个方案真正可复现。这也是我自己交付财务模型时反复踩坑换来的习惯希望帮到你。本文还有配套的精品资源点击获取
返回列表