在 Transformer 结构中,由于模型没有内置的序列信息(不像 RNN 那样有时间步的顺序依赖),需要通过**位置编码(Positional Encoding, PE)**来提供位置信息,使得模型能够区分不同 token 的相对位置。
由于 Transformer 采用的是自注意力机制(Self-Attention),它对输入序列的排列顺序不敏感,因此需要显式地向输入中添加位置信息。位置编码的主要作用包括:
Vaswani 等人在 “Attention is All You Need” 论文中提出了一种固定的位置编码方法,使用不同频率的正弦(sin)和余弦(cos)函数来编码位置:
P E ( p o s , 2 i ) = sin ( p o s 1000 0 2 i / d ) P E ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 2 i / d ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d}}\right) \\ \ \\ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d}}\right) PE(pos,2i)=sin(100002i/dpos) PE(pos,2i+1)=cos(100002i/dpos)
其中:
以下是使用 PyTorch 实现的位置编码模块:
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, max_len: int = 5000):
"""
:param d_model: 词向量维度 (Embedding 维度)
:param max_len: 序列最大长度
"""
super(PositionalEncoding, self).__init__()
# 创建位置编码矩阵(shape: [max_len, d_model])
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # [max_len, 1]
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # [d_model/2]
# 计算 sin 和 cos 位置编码
pe[:, 0::2] = torch.sin(position * div_term) # 偶数索引
pe[:, 1::2] = torch.cos(position * div_term) # 奇数索引
# 增加 batch 维度以便广播
pe = pe.unsqueeze(0) # shape: [1, max_len, d_model]
# 注册为 buffer,使其不会被模型参数更新
self.register_buffer('pe', pe)
def forward(self, x):
"""
:param x: 输入张量,形状为 [batch_size, seq_len, d_model]
:return: 加入位置编码后的张量
"""
return x + self.pe[:, :x.size(1), :]
见【搜广推校招面经二十六】
在广告投放中,**广告位置(Ad Placement)会对点击率(CTR)、转化率(CVR)等关键指标产生影响,导致模型学习到的特征偏向某些广告位,而不是用户真实的兴趣或广告的质量。这种位置偏差(Position Bias)**可能导致:
Bias Net 主要用于建模广告位置对点击率的影响,并将其从主模型(如 CTR 预估模型)中剥离,使主模型学习到去除位置影响后的广告真实效果。
Bias Net 主要采用 “双塔网络”(Two-Tower Model) 的思想:
BiasNet 通过 MLP(多层感知机)或 Embedding 方式 学习广告位置的偏置。以下是 BiasNet 的常见结构:
position_id
Embedding
层将广告位转换为向量,再通过 MLP
学习其对点击率的影响。position_id
预测 CTR
,然后归一化,使其成为位置偏置因子。import torch
import torch.nn as nn
class MainNet(nn.Module):
def __init__(self, ad_feature_dim, user_feature_dim):
super(MainNet, self).__init__()
self.fc = nn.Sequential(
nn.Linear(ad_feature_dim + user_feature_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid()
)
def forward(self, ad_features, user_features):
x = torch.cat([ad_features, user_features], dim=1)
return self.fc(x)
在去除广告位置(Position, pos
)的影响时,使用除法而不是加法的主要原因是 归一化 和 比例调整。
假设我们要预测广告的点击率(CTR),模型的原始预测值是:
P ( click ∣ ad , user , pos ) P(\text{click} | \text{ad}, \text{user}, \text{pos}) P(click∣ad,user,pos)
但由于广告位置会影响点击率(例如,广告展示在页面顶部时点击率更高),我们需要去除 pos
的影响,使得模型的预测结果更加公正,仅反映广告内容和用户兴趣。
BiasNet 是专门用于学习 pos
对点击率影响的网络,它的输出是:
P ( click ∣ pos ) P(\text{click} | \text{pos}) P(click∣pos)
最终,我们想要的去偏点击率应该是:
P debiased ( click ∣ ad , user ) = P ( click ∣ ad , user , pos ) P ( click ∣ pos ) P_{\text{debiased}}(\text{click} | \text{ad}, \text{user}) = \frac{P(\text{click} | \text{ad}, \text{user}, \text{pos})}{P(\text{click} | \text{pos})} Pdebiased(click∣ad,user)=P(click∣pos)P(click∣ad,user,pos)
在广告点击率(CTR)建模中,广告通常是按一定策略排序展示的。通常情况下,用户的注意力随广告位(Position)下降而减少,因此 CTR 也应该遵循 “自上而下递减” 的规律。然而,在实际建模过程中,CTR 可能会受到其他因素(如广告内容、用户兴趣等)的干扰,导致某些下方广告的 CTR 反而比上方广告高。
如果模型未能正确学习广告位的影响,CTR 预测可能出现异常。
pos
作为一个重要特征,并确保其影响符合期望:pos_embedding = nn.Embedding(n_pos, embedding_dim)
,让模型学习广告位的影响。pos / n_pos
作为一个数值特征输入。为了确保 P ( click ∣ pos ) P(\text{click} | \text{pos}) P(click∣pos) 递减,可以对模型进行约束:
位置影响单调递减:
pos
越大(排名越靠后),CTR 预测值不会上升。使用单调递减的变换函数:
class MonotonicLayer(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.linear = nn.Linear(input_dim, 1)
def forward(self, x):
return -torch.relu(self.linear(x)) # 保证单调递减
pos_1
在 pos_2
之上,则 P(\text{click} | \text{pos_1})
应该大于 P(\text{click} | \text{pos_2})
:如果数据中广告位的 CTR 没有遵循递减规律,可以通过以下方法调整:
分层归一化(Layer-wise Normalization):
pos
计算均值 CTR μ_pos
,然后对 y
进行调整:逆倾向采样(Inverse Propensity Weighting, IPW):
P(\text{pos})
作为权重,对样本进行重新加权: loss = torch.mean(weight * loss_function(y_pred, y_true))
如果模型仍然预测出了不符合递减规律的 CTR,可以在后处理时进行 排序约束调整:
CTR_pred
进行 Sort
,确保 P(\text{click} | \text{pos})
递减:ctr_pred_sorted, _ = torch.sort(ctr_pred, descending=True)
def smooth_ctr(ctr_pred):
for i in range(1, len(ctr_pred)):
ctr_pred[i] = min(ctr_pred[i], ctr_pred[i-1])
return ctr_pred