简介:面向银行风控、普惠金融与信贷科技方向的从业者及研究人员,这份606页文档围绕小微企业信用评估展开,以税务数据与经营信息的交叉验证为主线,串联从数据采集、标准化处理到模型落地的完整链路。全文共61个大章节,先讲税务票据非结构化数据的结构化提取、经营信息规则与统计双维度清洗去噪、数据链路低延迟传输,再展开DeepSeek-R1在信用评估场景的适配性分析与税务、经营两维特征工程;后半部分深入到自注意力与交叉注意力融合机制、信用标签体系与多源标注一致性校验、分布式标注框架搭建,以及增量预训练、数据集划分、混合精度训练与梯度累积等训练优化细节。压缩包为1个PDF,约16.12MB,支持目录章节跳转与阅读器书签大纲定位,结构清晰便于按模块查阅。目前已有97人学习。
1. 从两份对不上的报表说起:小微企业信用评估为什么必须做交叉验证
一家做建筑辅材的小微企业,增值税申报的年销售额 480 万,对公账户全年贷方发生额只有 210 万,开票系统里却躺着 620 万的记录。三个数字谁真谁假,单看任何一份报表都看不出问题。这就是普惠金融风控最真实的起点:不是缺数据,而是数据之间互相打架,而打架的地方恰恰是风险最先露头的地方。
交叉验证的思路很朴素。税务申报、开票流水、资金往来、社保缴纳、水电消耗,这几路数据的产生动机各不相同,企业要同时把所有口径粉饰到自洽,成本极高。把它们的偏离程度量化成一致性信号,申报、开票、资金三个口径越靠近,说明经营越规范;偏离越大,就越值得人工多看两眼。
这套方案对应的是银行小微条线、消金和助贷机构的风控与数据团队。606 页的方案文档拆开之后其实就是三件事:字段口径对齐、矛盾点量化、结果接进评分卡和人工复核工单。
2. 税务数据与经营信息的字段口径对齐:交叉验证的地基
2.1 税务侧取哪些字段才够用
税务数据不是拿到申报表就完事。真正能支撑交叉验证的,是四组东西:增值税申报的销售额与进项税额、企业所得税的年报利润与纳税调整、个人所得税申报人数与工资总额、纳税信用等级与申报连续性。前两组解决“企业说自己做了多少生意”,第三组解决“企业说自己雇了多少人”,第四组解决“企业愿不愿意按规矩出现”。
这里有个容易被忽略的坑:增值税申报销售额是不含税口径,开票金额通常是含税口径,银行流水是全额口径。三者在比对前必须先做口径归一,否则算出来的偏离度全是假信号。我在项目里一般统一压到不含税口径,按行业主税率还原,制造业按 13%、生活服务按 6%,混合经营的就按开票明细里的税率字段逐笔还原。
纳税信用等级虽然只有 A/B/C/D/M 几档,但它的价值在于时间序列。连续 12 个月按时申报、且信用等级稳定在 B 以上的企业,和一年内出现三次逾期的企业,即使财务指标一样,风险也应该分开处理。所以信用等级不要当静态标签用,要拉成按月对齐的状态序列,把“降级”这个动作本身当成信号。
申报连续性则更直接:连续零申报、突然中断、季度末集中补报,这三种模式在税务数据里一眼可见,而且很难通过经营信息的粉饰来掩盖。常见做法是统计近 24 个月的零申报月数、最大连续零申报月数、以及申报金额的环比波动系数。
2.2 经营信息的采集边界与更新频率
经营信息比税务数据散得多,采集时要有边界,否则数据接进来也进不了模型。我会按四类收:资金类(对公账户贷方发生额、月度日均余额)、交易类(开票金额、前五大客户集中度)、用工类(社保缴纳人数、公积金缴纳基数)、运营类(用电量、用水量、场地租赁合同)。
| 数据源 | 代表字段 | 更新频率 | 典型盲区 |
|---|---|---|---|
| 增值税申报 | 销售额、进项税额 | 月/季 | 不含税口径,需还原 |
| 开票数据 | 开票金额、客户名称 | 日 | 含税口径,红冲未剔除 |
| 对公流水 | 贷方发生额、日均余额 | 日 | 含关联方内部转账 |
| 社保缴纳 | 缴纳人数、缴费基数 | 月 | 存在最低基数缴纳 |
| 水电消耗 | 月度用电量 | 月 | 分表计量、合租共用 |
这张表里最需要警惕的是流水里的关联方转账。同一实控人名下的两家公司互相对开,贷方发生额能翻一倍,但真实经营没有任何变化。所以资金类字段在入模前要先做关联方识别,把同一实控人、同一地址、同一联系电话的多主体标记出来,内部往来单独统计,不计入经营性流入。
更新频率决定了模型能看到什么。开票和流水按日更新,适合做实时预警;社保和水电按月更新,适合做月度复评;税务申报按季更新,只适合做季度重检。把不同频率的数据塞进同一个实时决策流,是很多项目上线后指标抖动的根源。
2.3 用统一社会信用代码做主轴的时间对齐
对齐是整条链路上最容易出错、也最值得写清楚的一步。主键用统一社会信用代码,时间轴统一到 YYYYMM,税务按季申报的要把季度值按合理规则分摊到月,或者干脆把整个模型都建在季度粒度上。我的选择是后者:小微企业本身经营波动大,月度口径噪声太多,季度粒度反而更稳。
import pandas as pd import numpy as np def align_sources(tax_df: pd.DataFrame, biz_df: pd.DataFrame) -> pd.DataFrame: """按统一社会信用代码 + 所属期,把税务口径与经营口径对齐到一张宽表""" tax = tax_df.rename(columns={ "nsrsbh": "uscc", # 纳税人识别号统一成统一社会信用代码 "sbsq": "period", # 申报所属期,统一成 YYYYMM "xse": "tax_sales", # 申报销售额(不含税口径) })[["uscc", "period", "tax_sales"]] biz = biz_df.rename(columns={ "invoice_amt": "inv_amt", # 开票金额(含税口径) "bank_in": "bank_inflow", # 对公账户贷方发生额 })[["uscc", "period", "inv_amt", "bank_inflow"]] # one_to_one 会在任一侧出现重复主键时直接报错,比静默笛卡尔积安全得多 df = tax.merge(biz, on=["uscc", "period"], how="outer", validate="one_to_one") # 开票金额还原为不含税口径,13% 只是默认值,实际应按行业主税率替换 df["inv_amt_ex_tax"] = df["inv_amt"] / 1.13 # 税票偏离度:申报口径与开票口径的相对差 df["dev_tax_inv"] = (df["tax_sales"] - df["inv_amt_ex_tax"]).abs() / \ df[["tax_sales", "inv_amt_ex_tax"]].max(axis=1).replace(0, np.nan) # 税银偏离度:申报口径与资金口径的相对差 df["dev_tax_bank"] = (df["tax_sales"] - df["bank_inflow"]).abs() / \ df[["tax_sales", "bank_inflow"]].max(axis=1).replace(0, np.nan) return df这段代码有三个关键设计。一是validate="one_to_one",主键重复时直接抛错,避免多对多合并把行数放大后算出错误的偏离度,这是线上事故的高发点。二是偏离度用相对值而非绝对值,分母取两个口径的较大值并做零值保护,防止新设企业申报为零时除出无穷大。三是所有口径换算集中在一个函数里,税率参数后续要按行业拆开时只改一处。
注意:偏离度是方向无关的,480 万对 210 万和 210 万对 480 万会算出同一个值。风控上这两者含义完全不同,前者疑似少报流水,后者疑似虚增申报。所以除了偏离度,还要额外保留一个有符号的差值比率,供后续的语义判断环节使用。
3. DeepSeek 在交叉验证里的角色:非结构化信息的结构化与矛盾发现
3.1 别让 DeepSeek 直接给授信结论
一个常见的错误用法,是把企业资料一股脑丢给模型,让它输出“建议授信 300 万”。这条路走不通,原因是三方面。第一,大模型的输出不可复现,同一份材料换个时间问,结论可能不一致,而授信决策必须可追溯、可申诉。第二,模型没有经过本行的坏样本训练,它给的分数分布和你们的评分卡对不上,两个分数混在一起没法解释。第三,监管对授信决策的可解释性有明确要求,一句“模型认为风险高”不足以支撑拒绝理由。
真正适合 DeepSeek 做的是三件事:把非结构化的经营描述、合同摘要、征信报告文本抽成结构化字段;发现跨数据源的语义矛盾;把矛盾翻译成风控人员能直接读的复核提示。它解决的是“信息从哪来”和“矛盾怎么读”,不是“给不给额度”。
所以架构上,DeepSeek 的输出不是分数,而是一份矛盾清单。清单进规则引擎,由规则引擎折算成扣分项,扣分项再进评分卡。这样整条链路每一环都可解释、可回溯、可单独替换。
3.2 用 DeepSeek API 把经营信息抽成结构化矛盾清单
调用 DeepSeek 开放平台的方式和主流 API 协议兼容,最小可用代码如下,重点在结构化输出和参数约束上。
import json import os from openai import OpenAI client = OpenAI( api_key=os.environ["DEEPSEEK_API_KEY"], base_url=os.environ["DEEPSEEK_BASE_URL"], # 从环境变量读,不写死在代码里 ) SYSTEM_PROMPT = """你是银行小微企业授信复核助手。只输出 JSON,不要任何解释性文字。 输出结构:{"conflicts":[{"type":"矛盾类型","evidence":"原文依据","severity":1}]} severity 取值:1 轻微可解释;2 需要补充材料;3 直接触发人工复核。""" def extract_conflicts(profile: dict, timeout: int = 60) -> dict: resp = client.chat.completions.create( model=os.environ.get("DEEPSEEK_MODEL", "deepseek-chat"), messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(profile, ensure_ascii=False)}, ], temperature=0, # 风控场景不要发散 max_tokens=1024, response_format={"type": "json_object"}, # 强制 JSON,省掉解析兜底 timeout=timeout, ) return json.loads(resp.choices[0].message.content)参数上有几处值得说明。temperature=0是硬要求,风控抽取不需要任何创造性,温度调高只会让同一份材料抽出不同的矛盾项。response_format指定 JSON 后,返回内容可以直接反序列化,但生产环境仍要加一层 try/except 和重试,把解析失败率压到千分之几以下。timeout单独暴露出来,是因为批量跑几千户企业时,个别长文本会拖慢整批任务,宁可单条超时重试,也不要整批卡死。
profile这个入参的结构决定了抽取质量。我一般会把它组织成几个固定块:企业基本情况、税务汇总、开票汇总、流水汇总、征信文本摘要。每块字段名保持一致,模型在不同企业之间看到的格式统一,抽取结果的稳定性会明显好于每次自由拼装。
提示:把抽取结果做一次抽样人工核对再上线。抽 200 户,人工标一遍真实矛盾项,和模型输出做交集比对,算出召回率和误报率。这一步花两天,能省掉上线后一个月的返工。
3.3 本地化部署 DeepSeek 时的显存、并发与上下文取舍
涉及税务明细、流水、征信原文这类材料,不少机构会选择本地化部署 DeepSeek,把推理放在行内。这条路技术上可行,但参数取舍和云端 API 完全不同,三个维度要一起算。
显存决定模型规模上限。权重的显存占用大致是参数量乘以每个参数的字节数,FP16 下 1B 参数约 2GB,再做 4bit 量化能压到约 0.5GB 每 B。但权重只是一半,KV Cache 随并发数和上下文长度线性增长。处理征信报告这类长文本时,上下文开到 8K 以上,KV Cache 往往比权重更吃显存,这是很多人第一次部署时估算漏掉的部分。
并发决定吞吐。本地单实例的并发上限不是靠调大参数解决的,主要取决于显存余量和推理引擎的批处理策略。批量复评场景其实不需要高并发,用队列串行跑、拉长任务窗口,比硬扛并发更划算。只有接实时决策流时才需要认真调,而前面说过,DeepSeek 的输出本身不进实时决策,所以大多数情况下并不需要高并发。
上下文长度决定能塞多少材料。这里有个务实的做法:不要把整份征信原文丢进去,先在本地做规则化的字段提取和文本截断,只把摘要和关键段落送给模型。上下文从 32K 压到 4K,单条推理成本能降一个数量级,抽取质量反而更稳,因为模型的注意力没有被无关段落稀释。
# 以 OpenAI 兼容协议启动本地推理服务,供上面那段 Python 代码直接切换 base_url python -m vllm.entrypoints.openai.api_server \ --model /data/models/deepseek-weights \ --served-model-name deepseek-local \ --max-model-len 8192 \ # 够用即可,调大直接吃显存 --gpu-memory-utilization 0.90 \ # 留 10% 余量给 KV Cache 波动 --tensor-parallel-size 2 \ # 张量并行数要和实际卡数一致 --port 8000这套参数里,gpu-memory-utilization是最容易踩坑的。设成 0.98 看似榨干了显存,但一旦并发上来,KV Cache 申请失败会直接让请求报错。留一成的余量,换来的稳定性远超过那点吞吐损失。tensor-parallel-size必须等于实际使用的卡数,设错了启动阶段就会报维度不匹配。
3.4 数值规则与语义判断的合成
DeepSeek 抽出的矛盾清单和第二章算出的数值偏离度,最后要合成到一个决策口径上。我的做法是分层:数值偏离度负责定量,超阈值就直接进硬规则;语义矛盾负责定性,只做加权扣分和复核提示。
| 层级 | 输入 | 处理方式 | 输出 |
|---|---|---|---|
| 硬规则层 | 税票偏离度、税银偏离度 | 超阈值直接拒绝或转人工 | 布尔标记 |
| 加权层 | 指标 WOE 分箱值 | 加权求和进评分卡 | 信用分 |
| 语义层 | DeepSeek 矛盾清单 | 按 severity 折算扣分 | 扣分项 + 复核提示 |
这样分子之后有个好处:任何一层出问题都能单独替换。DeepSeek 换版本、换部署方式,只影响语义层;税务数据源换接口,只影响硬规则层和加权层。
4. 普惠金融评分卡落地:指标构造、权重分配与阈值校准
4.1 三类核心指标的构造公式
交叉验证出来的原始偏离度不能直接进模型,要先加工成有业务含义的指标。我一般构造三类。
第一类是口径一致性指标,包括税票偏离度、税银偏离度、开票与流水的方向一致性。前两个第二章已经算出来了,第三个是指开票金额和对公流入的同比增速是否同向,一个涨一个跌说明增长可能来自非经营性因素。
第二类是经营稳定性指标,包括近 24 个月零申报月数、连续零申报最大月数、开票金额环比波动系数、前五大客户集中度。集中度这个指标在小微场景里特别有效,前五大客户占比超过 80% 的企业,一旦丢掉一个大客户,现金流断得比想象中快。
第三类是用工与运营匹配度,包括社保人数与个税申报人数的差值、人均产值、单位用电量对应的销售额。社保人数多于个税申报人数,通常意味着存在最低基数缴纳或者劳务派遣;人均产值显著偏离行业中位数,要么是真实的高效率,要么是申报口径有问题,这两种情况都需要人工介入判断。
4.2 权重与阈值的确定:专家规则打底,WOE 分箱校准
权重不要一上来就用逻辑回归跑。样本量不够的时候,跑出来的系数不稳定,换一批数据就翻号。稳妥的路径是先用专家规则定一版权重上线,跑三到六个月积累坏样本,再用 WOE 分箱校准。
分箱的作用有两个:把连续变量转成单调的离散值,以及把非线性关系显式暴露出来。计算 WOE 和 IV 的代码如下。
import numpy as np import pandas as pd def woe_iv(df: pd.DataFrame, col: str, target: str, q: int = 10): """等频分箱后计算各箱 WOE 与整体 IV,target=1 表示违约""" tmp = df[[col, target]].dropna().copy() tmp["bin"] = pd.qcut(tmp[col], q=q, duplicates="drop") # 重复边界自动合并 g = tmp.groupby("bin", observed=True)[target].agg(["count", "sum"]) g.columns = ["total", "bad"] g["good"] = g["total"] - g["bad"] # 平滑 0.5,避免某箱坏样本为 0 时 log 发散 g["bad_rate"] = (g["bad"] + 0.5) / (g["bad"].sum() + 0.5 * len(g)) g["good_rate"] = (g["good"] + 0.5) / (g["good"].sum() + 0.5 * len(g)) g["woe"] = np.log(g["good_rate"] / g["bad_rate"]) g["iv"] = (g["good_rate"] - g["bad_rate"]) * g["woe"] return g, g["iv"].sum()qcut用等频分箱而不是等距,是因为税银偏离度这类指标严重右偏,等距分箱会把 90% 的样本塞进第一个箱里,后面几个箱没有统计意义。平滑项 0.5 是标准做法,但样本量小于 1000 时,平滑会明显改变 WOE 的绝对值,这时候更适合把分箱数降到 5 到 6 箱,而不是继续加平滑。
IV 值用来筛变量,经验区间是:小于 0.02 基本没有区分度,0.1 到 0.3 属于正常可用,超过 0.5 要警惕,通常是变量里混进了标签泄漏,比如用未来的逾期状态反推当前的偏离度。
注意:WOE 分箱必须在训练集上做,然后把这套分箱边界冻结下来,在验证集和线上样本上原样应用。每次重新分箱都会让线上分数发生不可解释的漂移。
4.3 矛盾清单折算成扣分项
DeepSeek 输出的 severity 只有三档,直接当扣分会太粗。我的做法是把它和矛盾类型组合成一张扣分表,用频率和严重度双维度加权。
| 矛盾类型 | severity=1 | severity=2 | severity=3 |
|---|---|---|---|
| 税票口径偏离 | 扣 3 分 | 扣 8 分 | 转人工 |
| 用工规模不符 | 扣 3 分 | 扣 8 分 | 转人工 |
| 上下游异常 | 扣 5 分 | 扣 10 分 | 转人工 |
| 经营描述矛盾 | 扣 2 分 | 扣 5 分 | 扣 12 分 |
同一类型在一个季度内重复出现的,扣分按次累加但设上限,一般不超过该类型单次扣分的两倍。这个上限的作用是防止材料质量差的企业被矛盾项刷到负分,掩盖了其他指标的真实表现。
扣分表要和生产、风控两边一起定,定完之后至少三个月不要动。频繁调整扣分表会让回溯分析失去基准,你再也说不清通过率的变化是策略调整造成的还是客群变化造成的。
5. 上线后的排错与效果验证:三张表定位模型漂移
5.1 通过率、KS 与 PSI:三张表的分工
上线之后,最先要看的不是坏账率,因为它滞后太久。前三个月应该盯三张表。
第一张是通过率表,按周统计申请量、通过率、转人工率,按渠道和客群分层。通过率突然跳变,八成是数据源接口出了问题,而不是客群变了。
第二张是区分度表,按月计算 KS,看评分对好坏样本的区分能力有没有衰减。KS 掉超过 20% 就要查变量,通常是个别指标的分布发生了突变。
第三张是稳定性表,计算每个入模变量和总分数的 PSI。
| 监控指标 | 计算口径 | 告警阈值 | 处置动作 |
|---|---|---|---|
| 通过率 | 周维度,分渠道 | 周环比波动超 15% | 查数据源接口 |
| KS | 月维度,滚动 6 个月 | 相对基准下降超 20% | 逐变量排查 |
| PSI | 月维度,对比训练集 | 超过 0.25 | 触发重训练评估 |
PSI 超过 0.1 就要开始关注,超过 0.25 说明分布已经明显偏移,这时候不要急着重训,先确认是客群真实变化还是数据口径变化。数据口径变化导致的重训,训出来的模型会学到错误的东西。
5.2 一个具体技巧:用冻结样本做回溯对账
排错时最有用的一个手段,是保留一批冻结样本。做法是每个月随机抽 500 户已经出过决策的申请,把这批样本的原始数据和当时的模型输出一起存档,不再参与任何训练。半年后拿这批样本回跑当前模型,对比两次输出的分布差异。
这个对比能直接回答一个关键问题:分数变化是模型变了,还是数据变了。如果原始数据回放后分数和存档分一致,说明模型本身稳定,线上分数的漂移来自数据侧;如果不一致,说明模型文件或者依赖特征的计算逻辑被改动过。
冻样本的选取要覆盖三个维度:通过的、拒绝的、转人工的各占一定比例,否则你只看得到通过客群的稳定性。转人工那部分尤其重要,因为它是策略边界最模糊的区域,也是最容易积累出优质坏样本的地方。
这套对账机制跑顺之后,重训练的节奏会变得有依据。不是按日历定期重训,而是等到冻样本对比、PSI、KS 三张表里至少两张同时告警,才启动一次,每次重训都能对应到一个明确的、可记录的原因。
本文还有配套的精品资源,点击获取