《模型不玄学》第14章 标签、损失与样本权重

小白一句话

模型不是"看一眼就蹦出 0 或 1",它其实一直在心里记一笔账:我猜的概率和后来实际发没发生差多远,差得越远罚得越狠。这笔账叫损失 ;而样本权重决定"谁的账更该被看重"。标签给了目标,损失给了尺子,权重决定尺子往哪边偏。

这一章在流水线里的位置

第6章把"一条样本长什么样"定下来了:左边是特征,右边是标签 active_7d(评分日之后 7 天有没有至少再来领 1 次)。第7章把左边 10 维特征讲清。第12章让 GBDT 学"从特征到概率"的映射,并验证它真的能区分 U0036(几乎天天来,概率 0.99)和 U0009(在掉,概率 0.22)。

本章往回退一步,把第12章"模型到底在优化什么"讲透:

  • 那个 0/1 标签是怎么变成模型心里的目标的;
  • 模型用哪把尺子量自己猜得准不准(损失);
  • 训练表里正负样本差很多、同一用户还出现好几回,这两件事分别该怎么处理(样本权重)。

三件事串起来,才算真正看懂第12章那棵 GBDT 在干什么。

标签怎么变成目标

先复习标签本身。第6章定下的口径:

对一个 (uid, 评分日) 快照,看评分日之后 7 天(不含当天)有没有至少再来领取 1 次。有 → label=1,没有 → label=0

第12章说过,我们要的不是硬猜 0/1,而是一个 0~1 的概率 p,表示"这个用户未来 7 天回来的概率"。所以标签在模型眼里是这样用的:

  • label=1 这一行,模型的目标是让 p 尽量靠近 1;
  • label=0 这一行,模型的目标是让 p 尽量靠近 0。

标签本身是个"是/否",但目标是个"连续概率"。这中间靠的就是下面这把尺子。

损失:模型心里的那把尺

模型每做一个预测,都记一笔账。这笔账叫损失(loss)。把所有样本的损失平均,就是模型在训练集上的"总扣分"。训练 = 想办法把这笔总扣分降下来。

本项目用的尺子叫对数损失(log loss,也常叫交叉熵)。它的直觉特别朴素:

顺带一句:这把尺子不是树模型的专利。神经网络算完最后一层,也是拿同一个公式量自己猜得准不准(第15章拿神经网络跑对比时,用的还是这把尺子)。

  • 真实发生了 (y=1):你给的概率 p 越接近 1,罚得越轻(逼近 0);你若给得很低,比如 0.1,等于你几乎断定"不会发生"却发生了,罚得极重 ------ -log(0.1) ≈ 2.30
  • 真实没发生 (y=0):你给的概率越接近 0 罚得越轻;你若给 0.9(几乎断定"会发生"却没发生),同样罚 2.30

用第12章两个老熟人感受一下:

  • U0036 (y=1,真实回来了):模型给 0.99,损失 -log(0.99) ≈ 0.01,几乎不罚。模型很确信他会回来,而且说对了。
  • 设想一个"要凉却被高估"的人(y=0,真实没回来):模型若给他 0.9,损失 -log(0.1) = 2.30,重罚。模型很确信他会回来,却错了------这种错正是损失最想抓住的。

为什么是损失而不是准确率

新手容易想:直接数"猜对几个"不就行了?问题是准确率有硬伤:

  • 它是"对/错"二选一,不连续。模型从"给 0.50"微调到"给 0.51",预测结论没变(都算"会回来"),准确率纹丝不动,模型没法顺着它一点点改进。
  • 训练表 162 行里 134 行本来就会回来。一个最懒的模型"一律猜 1",准确率已经有 82.7%,看起来不错,实则对"谁要凉"毫无判断------而这恰恰是我们最想提前发现的。

对数损失给每个样本一个连续的"后悔程度",错得离谱就重罚、错得轻微就轻罚、猜对了就几乎不罚。GBDT 正是靠这个连续信号去算"我上一堆树还差多少"(第12章说的"拟合残差"),才能一棵一棵把错补掉。

顺带一句:scikit-learn 的 GradientBoostingClassifier 二分类默认用的就是对数损失(loss='log_loss')。我们不手写它,理解"模型在最小化平均对数损失"就够用了。

对数损失还有个隐藏好处,给下一章埋个伏笔:它在数学上会"鼓励"模型输出真概率------也就是说,当模型说"这批人里有 82.7% 会回来"时,真实比例最好也真是 82.7%。这个"分"和"真实比例"对不对得上,就是第16章要讲的概率校准

标签平滑:给 0/1 标签放点水

硬标签是 0/1,非此即彼。可模型要输出的是概率------被一堆"只能到 1"和"只能到 0"的标签推着走,输出就容易往极端去(动不动 0.99 / 0.01)。这正是第16章讲"树模型过自信"的根源之一:目标本身就是极端的,模型自然学会输出极端

**标签平滑(Label Smoothing)**就是把训练目标从硬 0/1 换成软目标:

复制代码
硬标签:y = 0 或 1
软标签:y = 0.05 或 0.95(拿一点"概率"放水,别把目标设死)

意思很简单:模型不用把每个样本都背到极端,"这个人基本会回来"就够了,不必逼到 0.999。示例数据上演示一下(scikit-learn 的 GBDT 分类器不吃软标签,用回归器拟合软目标做近似):

训练目标 概率 >0.9 概率 <0.1 落在 0.1~0.9 ROC-AUC
硬标签 0/1 46 人 9 人 2 人 0.689
软标签 0.05/0.95 42 人 5 人 10 人 0.655

硬标签把 57 人里 55 个推到极端区间,中间只剩 2 人;软标签的分布明显温和(10 人落在中间)。代价也摆出来:ROC-AUC 从 0.689 略降到 0.655(57 人上属噪声)------平滑用一点排序损失,换概率不那么极端

和第16章校准的关系一句话说清:标签平滑是训练时的预防,校准是训练后的修正------一个在损失里就把目标软化,一个把训出来的概率掰回真实比例。两条路都冲着"概率别太极端"去,平滑管前面,校准管后面。真实项目里神经网络的训练常用平滑;树模型里它不如加权常用,但理解"目标别设死"这个思路,看别的模型的训练配置就不陌生了。

类别不均衡与样本权重

训练表的实际分布第12章报过:162 行,正样本 134(未来 7 天回来)、负样本 28(没回)。这一节只补一个马上要用的比例:正负比 ≈ 4.79 : 1

负样本(要凉的人)是少数。而我们恰恰最想提前揪出这批人去干预。

如果不加任何处理,模型会发现"一律猜 1"在训练集上对数损失也低------因为 134/162 都对。它会变懒,对"识别要凉的人"毫无动力。这正是类别不均衡的坑。

样本权重怎么补这个坑

样本权重(sample_weight) 就是给每行样本挂一个重要性 w,损失从"简单平均"变成"加权平均":

复制代码
加权平均分 = Σ( wᵢ × 该样本损失 ) / Σ( wᵢ )

把少数类的 w 调高,模型就不得不认真对待它们犯的错。

办法一:class_weight='balanced'(自动按反比)

scikit-learn 给了个现成开关:正样本权重 = n / (2 × n_pos),负样本权重 = n / (2 × n_neg)。代入真实数字:

  • 正样本权重 = 162 / (2 × 134) = 0.604
  • 负样本权重 = 162 / (2 × 28) = 2.893
  • 负 ÷ 正 = 4.79(正好等于正负比的倒数)

含义很直白:每错判一个"要凉的人",惩罚相当于错判近 5 个"常来的人"。少数类被拉回到和多数类平等的话语权。

办法二:手动 sample_weight(按业务代价)

balanced 只是"数量上平等"。现实中两类错判的代价往往不等:把"要凉的人"误判成"会回来"(真实 0 却预测 1,等于放跑了一个本可挽回的用户),代价通常比"把常来的人误判成要凉"高得多。这时可以手工再调:比如认定前者的代价是后者的 3 倍,就把负样本权重再乘 3(2.893 → 约 8.68)。

一个原则先说清:加权不改动任何标签,也不改变"模型在算概率"这件事,它只改变"模型更怕在哪些样本上犯错"。 标签还是那个 0/1,只是少数类的账被记重了。

配套脚本 附件/05_活跃度预测篇/activity_loss_weights.py 里放了个最偷懒的基线对照:一个啥也不学的模型,永远输出经验频率 p = 134/162 ≈ 0.827。它的平均对数损失:

  • 未加权 = 0.460
  • 加权(balanced)= 0.973

加权后损失接近翻倍。原因就是少数类一旦被错判,惩罚被放大,逼着模型不能靠"一律猜会回来"躺赢。这比任何口头解释都直观。

另一条路:改样本(过采样)

改权重不是唯一的路,还有一条:改样本------把少数类补足,让正负比例拉平。

  • 简单过采样:把少数类的样本复制几份,凑到和多数类一样多。最朴素,但复制品终归是复制品,复制多了模型会在少数类的重复形态上过拟合。
  • SMOTE :更进一步,在少数类样本之间插值------挑两个相近的少数类样本,在它们连线上造出一个"没见过的"新样本。比复制"新鲜",但也更难实现、更耗内存。

示例数据(训练集正 92 / 负 13)上,三路对比:

路线 PR-AUC ROC-AUC
A 基线(不处理) 0.827 0.689
B balanced 加权 0.872 0.708
C 简单过采样(负 ×7) 0.888 0.729

C 看着最高,但和 B 的差距在 57 人测试集上仍是噪声级。两条路的取舍,比单点分数实在:

  • 改权重:不造假、不膨胀样本。少数类只有十几个也能用,训练快、内存省;
  • 改样本:造出"假"数据把比例拉平,但少数类样本少到一定程度,造再多也是在原来的几个点上打转。

怎么选一句话:少数类样本还不少(几千个)时,过采样能帮上忙;少数类就十几个时,加权更实在。本例负样本 13 行,复制 7 倍也是 13 个人的复制品------三路分数分不出高下,正是"样本这么少,怎么折腾都有限"的又一例证。

同一个用户的多个样本该怎么加权

第6、8章讲过:一个用户在多个评分日可以各贡献一行,因为同一人在不同时间状态不同。训练表里这件事是实打实发生的:

  • 去重用户 57 个 ,平均每人在训练表里出现 2.84 次
  • 出现最多的 3 次,最少的 1 次;
  • U0036 在 07-08、07-15、07-22 各一行,标签 [1, 1, 1]
  • U0009 同样三行,标签 [1, 1, 0](前两次还行,第三次真凉了);
  • U0025 只在 07-22 出现 1 次(早期还没记录,冷启动),标签 [0]

这些行不是互相独立的样本------它们共享同一用户的历史,长得高度相似(第8章叫"近重复样本")。如果一视同仁,样本多的老用户等于给自己额外投了好几票,模型会偏向他们的行为模式。两种加权思路来处理:

思路一:按用户去重降权

同一用户多行,每行权重 = 1 / 该用户样本数,把"用户级"和"快照级"拉平:

  • U0036 三行,每行权重 1/3 ≈ 0.333
  • U0009 三行,每行权重 1/3 ≈ 0.333
  • U0025 只有一行,权重 1/1 = 1.000

这样 U0036 的三次快照加起来还是 1 票,不会因为他出现得多就多说话。

思路二:时间衰减(越近越重要)

越近的评分日越代表用户"当前状态",对未来预测更有代表性。可以给近期样本更高权重。配套脚本里用的示意规则是"每早 7 天权重减半" 0.5^(距今天数/7),以最近评分日 07-22 为基准,U0036 三行变成:

  • 07-08(距今 14 天)→ 0.250
  • 07-15(距今 7 天)→ 0.500
  • 07-22(距今 0 天)→ 1.000

也就是模型更信他"最近这次"的样子,稍远一点的快照只当参考。

两类权重可以叠着用

实务里常把两者合起来:先按用户去重降权(避免老用户霸榜),再乘时间衰减(让近期快照更响)。顺序无所谓,效果都是"既公平对待每个用户,又更看重当前状态"。

有一点要和第8章的"防泄露"对上:时间衰减只用到每个样本自己的评分日,而训练集里所有评分日都是过去------不牵扯任何未来信息,不会把标签泄露给特征。放心用。

为什么不去重到"一人一行"

有人会想:既然同一用户多行不独立,干脆每个用户只留最近一行,162 行缩到 57 行不就干净了?代价是丢掉"同一用户随时间怎么变"的信号------而第6章讲过,恰恰是这个变化(比如 U0009 从"还行"滑到"凉了")才是监督模型比无监督群分更准的关键。所以去重降权粗暴去重更稳:既压了老用户的票数,又保住了变化轨迹。

风险样本:第三种"不太信"的权重

前面两类权重管的是"样本多不多"和"样本新不新"。还有一类样本,两者都不是,但同样不想让它说话太响------风险样本:行为看着极端、疑似机器或数据有问题的样本。第13章准入评估是"训练前"给整批数据体检,这里讲的是"训练中"给个别样本压低话语权。

先定一个示例数据上能复现的风险规则(真实项目对应 risk_score / 风险标记,这里简化):单日领取次数 ≥ 12 记为爆发日(正常用户一天大多 5~10 次,12 次以上是明显的高强度日);训练表里特征期(评分日及以前)含爆发日的样本,标为风险样本。规则一跑:

  • 162 行里命中 11 行(6.8%),训练期 6 行、测试期 5 行,涉及 5 个用户;
  • 有意思的是这 11 行的标签全是 1------都回来了。说明风险样本不等于坏样本,它只是"特征看着极端":一个人平时一天领几次,某两天突然领 12~14 次,像脚本在跑,但该回来还是回来。

为什么只降权、不硬剔除

第一反应可能是"这种样本直接删掉不就行了"。三个理由说不行:

  1. 删 = 丢信息,小样本更痛。本来训练集才 105 行,删掉 6 行风险样本变 99 行。示例数据无所谓,真实项目样本紧张时,每删一行都是给模型少一次学习机会。
  2. 风险判定是启发式的,可能误伤。单日领 12 次,可能是脚本,也可能是用户那天就是兴致高。规则猜错了,删了就找不回来;降权至少把"这号人存在"的信号留在数据里。
  3. 模型得"见过"才能"会应对"。把风险样本全删了,模型从没见过这种极端形态,线上真遇到会懵;软降权让模型见过、但不全信。

所以做法是:不删,给一个很小的权重(比如 ×0.2),让它"听见但不做主"。

敏感性对照:开这个权重到底改变了什么

光说"应该降权"不够,得拿数据回答"降权前后模型差多少"------这叫敏感性对照:同一个模型、同一份切分,只改权重,看指标动不动。配套脚本在测试集(07-22,57 行)上跑出三档:

处理 训练样本 PR-AUC Brier ROC-AUC 0.5 阈值判回
A 关闭风险权重(全 1) 105 0.805 0.178 0.698 51/57
B 硬剔除(删 6 行) 99 0.821 0.176 0.694 54/57
C 软降权(风险 ×0.2) 105 0.813 0.177 0.698 51/57

A 那行就是第 14/16 章的老数字(PR-AUC 0.805、Brier 0.178、AUC 0.698),说明脚本没算错。三档差多少?Brier 差 0.002、AUC 差 0.004,A 和 C 在 0.5 阈值下判得不一样的人数是 0 个------基本都在噪声里。

这就是"敏感性对照"的结论,而且是个好结论:差异小,说明规则温和、风险样本和正常样本长得像,开着软降权也不怕带偏模型,可以放心开。 反过来,哪天你跑这个对照发现差异突然变大,先别急着关权重------那通常是规则的信号,回去查是不是阈值太松、误伤了一大片正常用户。真到风险样本又多又坏、能明显带偏模型的时候,软降权才真正显身手。

最后别忘了,这类权重可以和前面两类叠着用:先 balanced 补少数类、再按用户去重、乘时间衰减、再乘风险权重------每一类权重解决一个担心,最终权重是它们乘在一起。

风险程度也能当特征

上面那个风险标记只有"有/没有"两档。可风险还有个"多严重"的维度------爆发过一次和爆发过三次,显然不是一个量级。更细的做法是把风险程度做成特征,喂给模型,让模型自己学"什么样的风险值得警惕"。这里造三个脱敏简化的风险特征(真实项目对应 anomaly_score / 风险字段那一路):

特征 定义 训练表 162 行上命中 其中标签 0(凉了)
f_risk_burst 特征期内"单日领取 ≥ 12 次"的天数(爆发过几次) 11 行 0
f_risk_overlap 特征期内"五种任务全领且次数 ≥ 10"的天数(扫荡式领取) 24 行 0
f_risk_ratio 单日最大领取次数 ÷ 自己单日中位数(爆发有多猛) ≥3 的有 7 行 0

先对个暗号:f_risk_burst >= 1 的那 11 行,正是前面被 0.2 折权重的那批------二值标记是"有没有"(f_risk_burst 取 0/1 就是它),连续版记的是"几次"。

把它们加进模型(第 15 章同一套切分和权重)比一比:

特征 PR-AUC ROC-AUC Brier
原 10 维 0.805 0.698 0.178
+ 3 个风险特征 0.787 0.672 0.179

不升反降?都在 0.01 量级的噪声里,说明在示例数据上,风险特征没带来增益 。原因值得说透:示例数据是随机生成的,那批"爆发日"样本标签全是 1(爆发的人后来都回来了),它们根本不是"要凉的风险"。真实项目里机器行为、刷单用户才是风险特征的用武之地------那种"领得多、还在涨,但最后凉了"的人,光靠爆发日抓不出来,得靠 f_trend(第12章 U0009 就是被趋势特征读成 0.22 判对的)。

所以风险特征有两条出路,不冲突:一条当软降权依据 (本节能用),一条当特征喂模型(本节);喂不喂,先跑一版对照看增量,别拍脑袋。

它和前后章节的关系

到这儿,第12章那棵 GBDT 干的三件事就完整了:

  1. 目标:学 f(特征) → P(未来7天回来),标签是 active_7d
  2. 尺子:最小化平均对数损失,靠它算残差、一棵棵补树(第12章);
  3. 偏重:用样本权重对付类别不均衡、用去重/衰减对付同用户多样本、用软降权压低风险样本。

下一章(第15章)把这三件事写成一份完整的训练代码,逐行对着看它们落在哪一步。之后第16章讲概率校准------为什么模型说"82.7% 会回来"时,真实比例最好也真是 82.7%,以及怎么把分校准准;第17章再把"训练 → 评估 → 每日评分"三步串起来真跑通。

动手

  1. 附件/05_活跃度预测篇/activity_loss_weights.py,对照正文看:标签分布是不是 134/28、balanced 权重是不是 0.604/2.893、U0036 和 U0009 是不是各出现 3 次。重点看最后那个基线对照------加权后损失从 0.460 涨到 0.973,这就是"加权逼模型不能偷懒"的意思。
  2. 打开 附件/05_活跃度预测篇/activity_train_table.csv,找到 U0009 的三行(as_of 分别是 07-08、07-15、07-22),看标签 [1, 1, 0]。想想若按用户去重降权,这三行每行只算 1/3 票,而一个只出现过一次的冷启动用户(如 U0025)算整 1 票------为什么这样更公平。
  3. 思考题:如果完全按用户去重到"一人一行",训练表从 162 行缩到 57 行,会丢掉 U0009 那种"同一个人从还行滑到凉了"的变化信号。这和第6章"为什么一个用户要有多个样本"是不是同一件事的两面?
  4. 附件/05_活跃度预测篇/activity_soft_weight.py,看风险样本怎么被标出来(单日 ≥12 次)、三档对照差多少。重点看:A 和 C 在 0.5 阈值下判得不一样的是 0 个人------差异小本身就是"敏感性对照"的结论,说明规则温和、开着放心。
  5. 附件/05_活跃度预测篇/activity_risk_features.py,看三个风险特征在训练表上的命中,以及加进模型后指标变多少------对照正文那句"示例数据的风险样本标签全是 1,所以没增量"。
  6. activity_risk_features.pyf_risk_burst 的阈值从 12 改成 8(单日 ≥8 次就算爆发),重跑看命中行数涨多少、加进模型后指标动不动------阈值放宽后"风险"就不那么极端了。
  7. 附件/05_活跃度预测篇/activity_oversample.py,对照"另一条路:改样本"那节:基线 0.827、加权 0.872、过采样 0.888 三路数字,以及"负样本 13 行复制 7 倍也是复制品"的说明。
  8. 附件/05_活跃度预测篇/activity_label_smoothing.py,对照"标签平滑"那节:硬标签把 57 人里 55 个推到极端区间(中间只剩 2 人)、软标签中间 10 人,ROC-AUC 从 0.689 略降到 0.655。把软标签从 0.05/0.95 改成 0.2/0.8 重跑,看概率分布怎么更温和。

本章配套脚本 附件/05_活跃度预测篇/activity_loss_weights.py(标签分布 / balanced 权重 / 去重与时间衰减演示)、附件/05_活跃度预测篇/activity_soft_weight.py(风险软降权三档敏感性对照)、附件/05_活跃度预测篇/activity_risk_features.py(三个风险衍生特征 + 加进模型的对照)、附件/05_活跃度预测篇/activity_oversample.py(过采样 vs 加权的三路对比)、附件/05_活跃度预测篇/activity_label_smoothing.py(硬标签 vs 软标签的输出分布对比,用 scikit-learn,已在第4章 附件/00_公共/requirements.txt 列出)。只用标准库的部分,环境搭建见第4章。训练表由第12章的 附件/05_活跃度预测篇/activity_train_table.py 生成,标签口径、特征口径与本章一致。

相关推荐
derekwang851 小时前
驾驭 AI · AI Harness Engineering · 契约层设计
人工智能·ai编程
黄金龙PLUS1 小时前
SPARKLE置换算法的优缺点
算法·网络安全·密码学·哈希算法·同态加密
不会就选b1 小时前
算法日常・每日刷题--<贪心>3
数据结构·算法·leetcode
DPS普轩·真空灌胶机专家1 小时前
普轩科技亮相第四届西安军工展 | 真空灌胶专家,展位 2-003
大数据·人工智能·科技·机器人·pcb工艺
台风护盾1 小时前
视频怎么自动翻译成中文字幕?AI 视频翻译的三道坎和一条捷径
人工智能·机器翻译·音频处理·ai工具·视频翻译
@嵌入式扫地僧1 小时前
嵌入式环境下麦克风阵列远场语音降噪的快速实现
人工智能·语音识别·嵌入式语音降噪·麦克风阵列·ai 降噪轻量化
行者全栈架构师1 小时前
WorkBuddy 实战:把一份 200 页年报变成可上台的投资分析 PPT
人工智能·算法·全栈
ReleaseU1 小时前
数据库 MCP:让 Agent 自己查数据、写报表
人工智能·大模型
deepseek231 小时前
Tenable联合OpenAI做AI Inspector:第三方Agent、Skill与MCP组件如何过供应链验收
人工智能·ai agent·mcp