3. 打分函数类型与应用:线性打分、非线性打分、多维度打分
聊到打分函数,很多新手容易把它想成“一个公式打天下”。但我做了这么多年,发现事情没那么简单。
说白了,你得根据你的业务场景和数据特性,选择合适的打分函数。选错了,模型再复杂也是白搭。
这一节我们就来拆开看看,这三种最主流的打分函数分别长什么样,什么时候用,怎么用。
核心观点:打分函数没有绝对的好坏,只有合不合适。线性打分是基线,非线性打分处理复杂关系,多维度打分解决多目标冲突。
3.1 线性打分:最朴实,也最稳定
线性打分,就是最基础的加权求和。你想想看,把每个特征乘上权重,再加一个偏置,完事。
score = w1*x1 + w2*x2 + ... + wn*xn + b
这个形式简单吧?但简单不代表没用。我个人习惯,每次做新项目,先拿线性打分跑一遍基线。为什么?
- 可解释性强:每个特征的权重w就是它的贡献度。你能跟业务方说,“这个特征的权重是0.3,它的影响力占了三成。”
- 训练稳定:梯度计算简单,几乎没有收敛问题。我在项目中遇到过,用线性模型做CTR预估的初始版本,线上效果稳定,没有震荡。
- 推理极快:没有激活函数,没有矩阵乘法,就是几个乘法加法,适合高并发场景。
不过它也有天花板。线性打分假设特征之间是独立的,不会自动捕捉交叉。所以当你发现线性模型效果不升反降时,就需要考虑非线性能力了。
我的经验:如果你的数据已经是经过深度特征工程(比如做了很多交叉特征、多项式扩展),线性打分往往就够用了。别急着上复杂模型,它可能只是一个简单的线性问题。
举个例子,一个简单的点击率预估场景。使用Python快速实现:
class LinearScorer:
def __init__(self, weights, bias=0.0):
self.weights = weights
self.bias = bias
def predict(self, features):
score = self.bias
for i, w in enumerate(self.weights):
score += w * features[i]
return score
# 假设3个特征:用户活跃度、广告质量分、出价
weights = [0.4, 0.35, 0.25]
scorer = LinearScorer(weights, bias=-0.1)
score = scorer.predict([0.8, 0.6, 0.9])
print(f"预估点击率: {score:.4f}")
3.2 非线性打分:更能打,也更危险
当线性打分不够用时,就该上非线性了。说白了,就是在线性求和外面套一个“激活函数”。
score = activation_function( w1*x1 + w2*x2 + ... + wn*xn + b )
常见的激活函数就那么几种:Sigmoid、ReLU、Tanh。每个都有自己的脾气。
| 激活函数 | 输出范围 | 适合场景 | 注意问题 |
|---|---|---|---|
| Sigmoid | (0, 1) | 概率化、二分类 | 梯度消失,饱和区 |
| ReLU | [0, +∞) | 深度网络隐层 | 神经元死亡 (Dead ReLU) |
| Tanh | (-1, 1) | 需负值输出场景 | 梯度消失依然存在 |
我记得有一次做推荐系统的重排序,直接用线性打分输出不做限制,结果分数超出预期范围,导致排序逻辑混乱。后来换成了tanh做非线性变换,把分数缩放到(-1,1)之间,才稳定下来。
看个实际代码例子,非线性打分的实现:
import math
def sigmoid(x):
return 1.0 / (1.0 + math.exp(-x))
def relu(x):
return max(0.0, x)
class NonLinearScorer:
def __init__(self, weights, bias, activation='sigmoid'):
self.weights = weights
self.bias = bias
self.activation = activation
def predict(self, features):
linear_sum = self.bias
for i, w in enumerate(self.weights):
linear_sum += w * features[i]
if self.activation == 'sigmoid':
return sigmoid(linear_sum)
elif self.activation == 'relu':
return relu(linear_sum)
else:
return linear_sum
scorer = NonLinearScorer([0.4, 0.35, 0.25], -0.1, 'sigmoid')
score = scorer.predict([0.8, 0.6, 0.9])
print(f"软打分 (概率): {score:.4f}")
避坑指南:我曾经接过一个case,同学用ReLU做打分层的激活,结果所有负样本的分数都变成0。原因很简单:ReLU把所有负输入砍掉了。如果你的训练数据里有大量负样本,建议用sigmoid或leaky ReLU。
3.3 多维度打分:把鸡蛋放在多个篮子里
很多真实场景,你关心的不只是“这个点击率高不高”,你还关心“用户是否愿意付费”、“会不会投诉”、“是不是新用户”。你想想看,一个维度打分解决不了这种多目标权衡。
多维度打分,就是把复杂目标拆成几个独立的维度,每个维度有自己的打分函数(可以是线性也可以是非线性),最后加权融合。
final_score = α₁ * score₁ + α₂ * score₂ + ... + αₘ * scoreₘ
我在项目中遇到过电商推荐场景,需要同时考虑:点击概率、购买概率、好评概率、发货时效。每个维度各自用一个模型打分,最后人工定权重组合。
看一个典型的多维度打分结构:
class MultiDimScorer:
"""
多维度打分器,每个维度有独立的打分函数
"""
def __init__(self, dim_weights, scorers):
self.dim_weights = dim_weights # 维度权重列表
self.scorers = scorers # 维度打分器列表
def predict(self, features_dict):
"""
features_dict: 每个维度对应的特征字典
"""
final_score = 0.0
for i, (weight, scorer) in enumerate(zip(self.dim_weights, self.scorers)):
features = features_dict.get(f'dim_{i}', [])
dim_score = scorer.predict(features)
final_score += weight * dim_score
print(f"维度{i}: 权重={weight:.2f}, 分数={dim_score:.4f}")
return final_score
# 实例化:点击率、购买率、满意度三个维度
scorers = [
NonLinearScorer([0.5, 0.3], 0.0, 'sigmoid'), # 点击率
NonLinearScorer([0.7, 0.2], -0.1, 'sigmoid'), # 购买率
LinearScorer([0.4, 0.4, 0.2], 0.3) # 满意度
]
multi_scorer = MultiDimScorer(
dim_weights=[0.4, 0.4, 0.2],
scorers=scorers
)
# 模拟特征输入
features = {
'dim_0': [0.9, 0.7],
'dim_1': [0.6, 0.8],
'dim_2': [0.7, 0.5, 0.6]
}
final = multi_scorer.predict(features)
print(f"最终多维度分数: {final:.4f}")
关键点:多维度打分的权重αⱼ不是学出来的,是业务定的。你在上线前,一定要和产品、运营对齐权重。我就经历过,明明技术指标最优,但业务方觉得不对,反复调了几轮权重才稳住。
3.4 三种打分的对比总结
一下子讲了三种,我们用一个表格收一下。这样你以后看项目代码,一眼就知道该用哪个。
| 对比维度 | 线性打分 | 非线性打分 | 多维度打分 |
|---|---|---|---|
| 数学复杂度 | 低 | 中 | 高(组合) |
| 可解释性 | 高 | 中(注意激活函数) | 中(维度可解释) |
| 处理特征交叉 | 弱(需人工交叉) | 强(自动捕捉) | 中(依赖维度拆分) |
| 训练稳定性 | 极高 | 中(受激活影响) | 中(各维度独立训练) |
| 典型应用 | LR、FM模型输出 | DNN、GBDT输出 | 多目标重排序 |
说到这,你是不是觉得豁然开朗了?其实挑选打分函数,核心就是看你的目标和数据。没有银弹,但你知道了这些差异,至少知道第一步该往哪个方向走。