Python - list、dict、set常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# coding=utf-8


def list_demo():
print('创建')
list_0 = []
list_1 = [0, 1, 2, 3, 4]
list_2 = [x for x in range(10)]
print('list_0 = {}'.format(list_0))
print('list_1 = {}'.format(list_1))
print('list_2 = {}'.format(list_2))

print('按下标索引')
print('list_1[0] = {}'.format(list_1[0]))

print('按下标、切片修改')
list_1[0] = 999
list_2[0:3] = [6, 66, 666]
print('list_1 = {}'.format(list_1))
print('list_2 = {}'.format(list_2))

print('增加元素、拓展列表')
list_1.append(0)
list_2 += [0]
print('list_1 = {}'.format(list_1))
print('list_2 = {}'.format(list_2))

print('按值删除、按下标删除')
list_1.remove(999)
del list_2[len(list_2)-1]
print('list_1 = {}'.format(list_1))
print('list_2 = {}'.format(list_2))

print('遍历并按条件删除')
for item in list_1[:]:
if item == 4:
del item
for i in range(len(list_1)-1, -1, -1):
if list_2[i] == 4:
del list_1[i]
print('list_1 = {}'.format(list_1))
print('list_2 = {}'.format(list_2))

print('排序')
new_list = sorted(list_1, key=lambda x: -x)
print(new_list)


def dict_demo():
print('创建')
dict_0 = {}
dict_1 = {0: 'zero', 1: 'one', 2: 'two', 3: 'three', 4: 'four'}
dict_2 = {x: x**2 for x in range(10)}
print('list_0 = {}'.format(dict_0))
print('list_1 = {}'.format(dict_1))
print('list_2 = {}'.format(dict_2))

print('按下标索引')
print('dict_1[0] = {}'.format(dict_1[0]))
print('dict_1[999] = {}'.format(dict_1.get(999, 'default')))

print('按下标、切片修改')
dict_1[0] = 999
print('dict_1 = {}'.format(dict_1))

print('增加元素、拓展字典')
dict_1[-1] = 'minus one'
dict_2.update(dict_1)
print('dict_1 = {}'.format(dict_1))
print('dict_2 = {}'.format(dict_2))

print('按下标删除')
del dict_2[-1]
print('dict_2 = {}'.format(dict_2))

print('遍历并按条件删除')
for k in list(dict_2.keys()):
if k == 2 or dict_2[k] == 'three':
del dict_2[k]
print('dict_2 = {}'.format(dict_2))

print('按value排序')
dict_2[-1] = 999
new_list = sorted(dict_2.items(), key=lambda item: (str(item[1]), -item[0]))
print(new_list)


def set_demo():
print('创建')
set_0 = set()
set_1 = {0, 1, 2, 3, 4, 4, 4}
set_2 = {x for x in range(10)}
print('list_0 = {}'.format(set_0))
print('list_1 = {}'.format(set_1))
print('list_2 = {}'.format(set_2))

print('增加元素、拓展字典')
set_1.add(999)
set_1.update(set_2)
print('set_1 = {}'.format(set_1))

print('按元素删除')
set_1.remove(999)
print('set_1 = {}'.format(set_1))


if __name__ == '__main__':
list_demo()
print(''.center(100, '-'))
dict_demo()
print(''.center(100, '-'))
set_demo()

Shell后台运行

  • 使用nohup&来执行some_cmd
1
2
3
nohup some_cmd >out 2>&1 &

ps -ef | grep some_cmd

原理:
后台执行的进程,其父进程还是当前终端shell的进程,而一旦父进程退出,则会发送hangup信号给所有子进程,子进程收到hangup以后也会退出。
如果我们要在退出shell的时候继续运行进程,则需要使用nohup忽略hangup信号(或者setsid将将父进程设为init进程,进程号为1)。

脚本中示例:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# run two processes in the background and wait for them to finish

nohup sleep 3 &
nohup sleep 10 &

echo "This will wait until both are done"
date
wait
date
echo "Done"

注意,可以使用wait等待脚本当前产生所有子进程结束。

  • 对已经跑起来的some_cmd
1
2
3
4
5
6
some_cmd
Ctrl + Z
bg 1

jobs
fg 1

但这样问题是,终端(意外)退出,该进程会被关闭。

  • 可以使用disown
1
2
3
4
5
6
some_cmd
Ctrl + Z
bg 1
disown -h %1

ps -ef | grep some_cmd
  • 还有一个关于subshell的小技巧

我们知道,将一个或多个命名包含在()中就能让这些命令在子shell中运行。
当我们将&也放入()内之后,新提交的进程的父ID(PPID)为1(init 进程的 PID),并不是当前终端的进程 ID。
因此并不属于当前终端的子进程,从而也就不会受到当前终端的hangup信号的影响了。

PyTorch Cheatsheet

Tensor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
x = torch.Tensor(5, 3) # 构造一个未初始化的5*3的矩阵
x = torch.rand(5, 3) # 构造一个随机初始化的矩阵
x # 此处在notebook中输出x的值来查看具体的x内容
x.size()

#NOTE: torch.Size 事实上是一个tuple, 所以其支持相关的操作*
y = torch.rand(5, 3)

#此处 将两个同形矩阵相加有两种语法结构
x + y # 语法一
torch.add(x, y) # 语法二

# 另外输出tensor也有两种写法
result = torch.Tensor(5, 3) # 语法一
torch.add(x, y, out=result) # 语法二
y.add_(x) # 将y与x相加,inplace

# 特别注明:任何可以改变tensor内容的操作都会在方法名后加一个下划线'_'
# 例如:x.copy_(y), x.t_(), 这俩都会改变x的值。

#另外python中的切片操作也是资次的。
x[:,1] #这一操作会输出x矩阵的第二列的所有值

Tensor与numpy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 此处演示tensor和numpy数据结构的相互转换
a = torch.ones(5)
b = a.numpy()

# 此处演示当修改numpy数组之后,与之相关联的tensor也会相应的被修改
a.add_(1)
print(a)
print(b)

# 将numpy的Array转换为torch的Tensor
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)

# 另外除了CharTensor之外,所有的tensor都可以在CPU运算和GPU预算之间相互转换
# 使用CUDA函数来将Tensor移动到GPU上
# 当CUDA可用时会进行GPU的运算
if torch.cuda.is_available():
x = x.cuda()
y = y.cuda()
x + y

Autograd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from torch.autograd import Variable
x = Variable(torch.ones(2, 2), requires_grad = True)
y = x + 2
y.creator

# y 是作为一个操作的结果创建的因此y有一个creator
z = y * y * 3
out = z.mean()

# 现在我们来使用反向传播
out.backward()

# out.backward()和操作out.backward(torch.Tensor([1.0]))是等价的
# 在此处输出 d(out)/dx
x.grad

Neural Network

  • 定义一个有着可学习的参数(或者权重)的神经网络
  • 对着一个输入的数据集进行迭代:
    • 用神经网络对输入进行处理
    • 计算代价值 (对输出值的修正到底有多少)
    • 将梯度传播回神经网络的参数中
    • 更新网络中的权重
      • 通常使用简单的更新规则: weight = weight + learning_rate * gradient

定义网络

1
2
3
4
5
6
7
8
9
10
11
12
import torch.nn as nn
import torch.nn.functional as F

class LRModel(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
# linear内置了参数初始化方法,此处无需显式指定
self.linear = torch.nn.Linear(2, 1) # 2 in, 1 out

def forward(self, x):
y_pred = F.sigmoid(self.linear(x))
return y_pred

仅仅需要定义一个forward函数就可以了,backward会自动地生成。
你可以在forward函数中使用所有的Tensor中的操作。
模型中可学习的参数会由net.parameters()返回。

定义loss与optimizer

1
2
criterion = torch.nn.BCELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

训练迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
x_data = Variable(torch.Tensor(X))
y_data = Variable(torch.Tensor(T))

model = LRModel()

for epoch in range(1000):
# Forward pass: Compute predicted y by passing x to the model
y_pred = model(x_data)

# Compute and print loss
loss = criterion(y_pred, y_data)
print(epoch, loss.data[0])

# Zero gradients, perform a backward pass, and update the weights.
optimizer.zero_grad()
loss.backward()
optimizer.step()

Deep CXR

0. Naive Method

  • categorical fields -> one-hot encoding
  • 上面直接堆叠deep,deep一般是MLP,multi-layer perceptron

1. 对高维稀疏特征进行embedding

one-hotted features高维稀疏,这对LR可以勉强忍受,但对DNN计算开销太大,拖低了模型性能effectiveness & efficiency。
对此,将各个field进行embedding,即将一个高维稀疏向量(one-hot),映射为一个低维稠密向量(real values)。

Deep Crossing

  • Microsoft 2016
  • embedding:通过dense,将各个稀疏域转化为低维稠密特征向量并concat在一起
  • deep:5层残差网络(何恺明ResNet),避免梯度爆炸、消失

2. 交叉特征与低维特征

DNN的优势在于能够自动学得高抽象层次(high-level)的特征,这些特征在而CV、NLP中往往是非常有用的,因为CV、NLP往往解决的是human替代任务,即模型代替人工进行工作。
但CXR预估并不是一个human替代任务,因而除了DNN能够隐式学得的高抽象层次特征,还应该尝试向模型中引入一些其他的特征来提升模型性能。
引入点包括:1)交叉特征;2)低抽象层次特征。

对于交叉特征,由于DNN的节点计算方式(对输入加权求和,再套一个非线性,不存在特征间相乘),对feature interactions并不能很好的描述。
为此,可以主动做特征交叉,补充到模型中,以增加模型对于特征间interactions的表达能力。
模型结构调整方式:生成的交叉特征,可以作为deep的input的补充,输入到deep中(串行结构);也可以concat在deep旁边,与deep结果一起进入最终预测节点(并行结构)。

对于低抽象层次特征,由于我们的CXR预估任务并不是只关注于高抽象层次,故模型不仅使用DNN的高抽象层次特征对应结果,还应该考虑引入低抽象层次特征,综合两种层次的特征来生成结果。
模型结构调整方式:低抽象层次特征,concat在deep的低抽象层次特征旁边,与一起进入最终预测节点(并行结构)。

2.1. 引入FM对特征做embedding [描述feature interactions]

  • FM的特异之处:
    • FM基于隐向量,可以学得数据集中出现次数较少、或根本未出现的特征组合
    • 使用FM进行embedding,尤其对于高维稀疏的one-hotted features,能通过2阶交叉特征的引入,增强模型的预测能力
    • 当然,FM的shortcomings也很明显:1)linear model;2)at most 2-order features

FNN

  • 2016
  • pretrain一个基于Dense的FM,做embedding层,上面叠加deep
    • embedding层包含FM一阶、二阶部分(将u_units看做,即1个一阶参数,u_units - 1个二阶参数)

2.2. 在embedding和deep之间插入“特征交互描述”层 [描述feature interactions]

我们知道基于dense做embedding、然后进行concat、然后上面堆叠deep,并不能描述feature interactions。
对此,在embedding和deep之间插入“特征交互描述”层,来专门描述feature interactions。

PNN

  • 2016
  • 在embedding和deep之间加入了product layer进行不同field之间特征交叉
  • 使用inner product、outer product
    • inner product相当于引入了FM的内积部分
    • outer product会得到一个矩阵,而非像inner product得到一个数。向后进入deep在一个unit中加权求和时,与一个数操作类似,只不过是对矩阵各个元素加权求和
    • product得到的结果(一系列数值),同时concat一阶特征,作为deep输入
  • 不是被动通过deep学习特征之间关系,而是强制通过product层引入field之间特征交叉

NFM

  • 2017
  • NFM中使用:
  • ,即文中所谓Bi-Interaction,本质上是一个pooling,对得到的embeddings进行了encode而非concat:
    • 1)embedding层,隐向量feat_val(因为不只one-hot输入,还有real-value输入),获取embeddings
    • 2),即“各种组合的元素乘”的向量和,此结果shape与的shape相同,都是embedding_size
    • 这个结果encode了embedding space的2-order feature interactions,作为“将feature embeddings进行concat”的替代
    • 3)上面再堆叠deep
  • 相较于FNN,NFM的网络结构中多了一个Bi-Interaction - sum pooling,形式上更像FM(但element-wise product后未将向量各元素累加,仍保留向量形式,所以还不是内积)
  • 相较于PNN,PNN也是不想直接将embeddings concat后输入到deep中,觉得会欠缺feature interactions,对此PNN的做法是各两个embeddings内积得到一个数之后,再将各个数concat在一起,再concat上一阶特征,输入到DNN中;而NFM则不是对各两个embeddings做内积,而是做元素乘,然后sum pooling
  • 相较于Wide & Deep(或DeepFM),NFM不通过joint一个Wide来引入cross features,而是在embedding与deep之间加一个Bi-Interaction pooling来描述特征交叉(in the low level)
  • NFM更像FNN、PNN,但也可看成是下文Wide & Deep的一种改进形式(因为一阶特征未进入deep,而是进入了最终结果层)

AFM

  • 2017
  • AFM是在NFM基础上改的,作者中包含NFM的作者
  • 加入了attention机制,即,Pire-wise Interaction之后,根据组合不同,后续操作时分配不同的权重
  • 即在Interaction Layer中,sum pooling时,为每个“组合的元素乘”分配一个权重,再做向量和。(此结果shape与的shape相同)
  • 这个attention weight是不好学习的:数量级、有可能组合未出现过。对此参数的学习,单独使用一个所谓的attention network
    • attention network输入为各种“组合的元素乘,输出维度为各组合数量的softmax。这样对于某个,我们将该网络的结果项作为attention weight
  • Interaction Layer之后,原文的AFM没有再堆叠deep了

2.3. 为deep joint一个wide结构 [同时考虑低抽象层次特征]

Wide & Deep

  • Google 2016
  • Wide部分:直接对原始特征、交叉特征(人工选择)做linear
  • Deep部分:先用Dense做embedding,然后上面叠加deep
    • Deep部分的参数使用随机初始化,未pretrain
  • joint train Wide部分和Deep部分,即,两部分做linear后,套一个sigmoid,作为结果,使用一个loss做优化
    • Wide部分本身就是linear,所以两部分做linear时不需要再乘一个权重,只给Deep部分结果乘一个权重即可

2.3.1. 在wide中考虑特征交叉 [同时考虑低抽象层次特征、描述feature interactions]

DeepFM

  • Huawei 2017
  • 对Wide & Deep引入FM进行优化
  • 最底层使用Dense
    • Wide部分:使用FM(包含一阶、二阶、常数项,Dense作为FM的二阶部分)的结果,作为低抽象层次特征
    • Deep部分:使用Dense(FM的二阶部分)作为embedding层
    • Dense、FM的参数使用随机初始化,未pretrain(若做pretrain,label其实与overall network的label相同,所以无需做pretrain,直接end-to-end训练)

DCN

  • Google 2017
  • Wide部分 -> Cross部分:N层可显式的表示到N阶的特征交叉
    • 每层为一个变换节点,为
    • 是个矩阵,使得交叉特征阶数+1,则保存了之前所有低阶交叉特征
    • 最后层的输出,相当于各阶交叉特征加权求和

xDeepFM

  • Microsoft 2018
  • 对比其他
    • Wide & Deep的deep结构中,将隐向量的各个bit进行交互。这是有一定问题的:这种交互是bit-wise描述的,它意识不到field隐向量的概念。并且在multi-hot的field中,同一隐向量内各个bit也会各自影响。这些与FM的初衷是不一致的。
    • DeepFM的wide结构中,倒是引入了vector-wise的交互描述。但它只有2阶。
    • DCN在wide结构中,同样引入了vector-wise的交互描述,并且阶数可由网络层数指定。但其只能学习某特定形式的高阶交互,并且也是bit-wise描述的。
  • xDeepFM的wide结构中,cross方式(Compressed Interaction Network,CIN):
    • 表示第层的输出,其中表示第层的vector个数(n_units),vecor维度始终为,保持和输入层一致(vector-wise操作)。具体地,第层每个vector的计算方式为:
    • 其中表示第层的第个vector(或者理解成DNN一层中的一个unit)的权重矩阵,表示元素乘
      • 其实就是:取前一层中的个vector,与输入层中的个vector,进行两两元素乘运算,得到个vector。然后个unit有个权重矩阵,每个unit中的个vector,乘以相应的权重矩阵后相加(加权求和)。这样,每个unit输出1个维度为的vector,本层输出为
    • 这样,在第层,CIN只包含阶的组合特征。最终,需要CIN将每层的中间结果都输出出来
      • sum pooling:,即将第层中,个units中,各vector的bit进行累加。注意,这隐含着内积的思想。最终,对于该层,会得到个数,作为一个vector
      • 最终,将各层concat在一起,作为CIN输出,(并且concat上raw,作为wide输出)
  • concat上deep(MLP)的结果,作为最终预测层输入

3. 加入attention机制

DIN

  • Ali 2018
  • 对于“用户的历史商品列表”这个multi-hot特征,在embedding后得到不定个数的embeddings向量。通常做法是将这项向量sum pooling或avg pooling,然后concat到其他特征embeddings,输入给deep。进一步,在做sum pooling时引入weights,表现不同的历史兴趣item,重要性不同
    • 在遇见sum pooling、avg pooling这种东西时,都可以考虑进一步引入weights
    • 如果只是加一个general weights,其描述的“不同item的重要程度”是固定的,不会区分场景。再进一步,对于不同candidate ad,可以让weights不同,即,对于不同的ad,商品embeddings的weights是不同的,然后再weighted sum pooling,获取基于历史信息的、用户对该商品的兴趣表达,即引入了attention机制
    • 理解一下,attention不仅仅是加weights,还需要对于不同的query,使用的是一组不同的weights(一个query对应一组weights)
    • recall一下AFM,其实是在其设计的NFM的interaction layer中出现了sum pooling,就向其中进一步引入了weights,从而声称使用了attention机制(但其实并没有query的概念,也就没有为每个query生成不同的weights组的概念,仅是一个general的weightes进行sum pooling)
  • attention weights的计算:local activation unit
    • 某item embedding(待weighted项)与ad embedding(query)进行element-wise product,然后concat两个embeddings,输入一层deep(输出未进行softmax,想描述weights的绝对差异)(u_units对齐embedding_size,保证weight能element-wise product上去)(end-to-end training)

DIEN

  • Ali 2018
  • 本深度模型关注点,不同域之间的特征交互描述 -> 用户兴趣表示
  • 对于“行为列表”这类特征,考虑时序,使用seq模型
    • 不同于DIN中,用户行为列表是没有顺序的,故使用multi-hot
    • 对用户interest的representation,往往直接使用用户行为(embedding & pooling);DIEN尝试通过显示的用户行为,提取隐含的用户当前兴趣表示、及兴趣发展趋势
  • 时序兴趣特征提取:
    • GRU,使用下一动作作为监督信号
    • 描述特征之间的影响
  • 兴趣发展进程:
    • Attention Update GRU
    • 描述与target AD相关的interest

[DSIN]

  • Ali 2019
  • 以session为粒度

TODO

xDeepFM
FiBiNet

Behavior Sequence Transformer
Deep Spatio-Temporal Neural Networks for Click-Through Rate Prediction
ATRank: An Attention-Based User Behavior Modeling Framework for Recommendation

AutoInt

CNN系CCPM、FGCNN


4. 多任务学习

通常来说,如果你发现你需要的不止一个损失函数,你就是在做多任务学习。

  • MTL可以有效增加用于模型训练的样本量
  • 学到更好的特征表示、关系
  • 约束参数适应于各个子任务,降低过拟合风险
  • 其实感觉实际上更多是:1)为了获取更通用的特征表达,对各个任务都有利;2)单独某个任务比较困难,可能是样本不够,需要其他的任务带一带。

4.1. 解决“训练数据空间”与“预测数据空间”不一致的问题

ESMM

  • Ali 2018
  • pCVR的问题:Sample Selection Bias,即pCVR训练基于点击数据,推理却基于曝光数据
    • 现在想对pCVR使用全特征域(曝光数据)学习
  • 分析:
    • 其中,是可以通过全特征域学习的
    • 故,可以引入多任务学习MTL,使用全域特征显示学习,同时隐式学习
  • 模型关键:
    • 一个网络、一个网络(网络结构为简单的embedding+MLP),结果相乘得到
    • loss精巧设计,显示的、joint训练,这两部分都可以通过全域特征进行训练,一定程度的消除了Sample Selection Bias
    • loss由两部分组成,即显示学习、joint训练的pCTR部分和pCTCVR部分。而pCVR只能隐式学习,因为1)loss中各任务应该是基于全域特征学习的;2)其负样本标注是有些问题的。在loss中,要保证label都是准的。所以,pCVR不适合基于全域特征,在loss中直接进行监督
      • pCVR使用全域特征,隐含了一个负样本标注的问题:“CVR预估到底要预估什么”,论文虽未明确提及,但理解这个问题才能真正理解CVR预估困境的本质。想象一个场景,一个item,由于某些原因,例如在feeds中的展示头图很丑,它被某个user点击的概率很低,但这个item内容本身完美符合这个user的偏好,若user点击进去,那么此item被user转化的概率极高。CVR预估模型,预估的正是这个转化概率,它与CTR没有绝对的关系,很多人有一个先入为主的认知,即若user对某item的点击概率很低,则user对这个item的转化概率也肯定低,这是不成立的。更准确的说,CVR预估模型的本质,不是预测“item被点击,然后被转化”的概率(CTCVR),而是“假设item被点击,那么它被转化”的概率(CVR)。这就是不能直接使用全部样本训练CVR模型的原因,因为咱们压根不知道这个信息:那些unclicked的item,假设他们被user点击了,它们是否会被转化。如果直接使用0作为它们的label,会很大程度上误导CVR模型的学习。
    • 注意,loss虽然是对pCTR部分和pCTCVR部分的描述,但其参数还是来自于pCTR网络和pCVR网络的,即
    • 是点击label,是转化label,是单独一个网络的loss函数(交叉熵),是模型函数
    • 此外,两个网络共享embedding,joint学习,这也解决了转化正样本稀疏的问题,Data Sparsity
      • 当然,pCVR网络的训练、推理,都使用全域特征了哦。即,隐式学得了一个输入全域特征,预测的子网络
  • 为什么使用乘法形式,而不使用除法形式,即
    • 因为数值往往较小,容易造成数值溢出

ESM2

DUPN

Modeling Task Relationships in Multi-task Learning with Multi-gate Mixture-of-Experts

CTR预估中负采样修正

负采样

CTR预估中,负类(label==0)的样本数往往远大于正类。
为了解决样本不均匀的问题,往往对负类进行采样,就是所谓的负采样。

但需要注意的是,负采样后的结果,会使得测试集中CTR被高估:

  • 相当于将训练集中一部分真实的负例排除掉了,使得模型获取的信息不足,并不能准确地识别出(这些)负例,从而在测试集中将一部分负类向正类预测,即CTR被(整体)高估。

这在CTR预估中是有问题的,涉及到“保序”与“保距”的概念:

  • 假设我们有这么一个序列“牛 500KG,羊100KG,兔子 5kg”,我们有一个模型,输入这些动物之后,根据体重排序,并且出一个体重的预估值。如果模型只是采用AUC这个指标的话,那么我们模型输出“牛 100kg,羊 20 kg,兔子1kg”,这样的结果是没问题的,但是这只是做到了保序,但是他们之间的差值变小了,没有做到保距。
  • 在CTR预估中,比如我们有这么一些广告,A的实际点击率是10%,B的实际点击率是5%,C的实际点击率是1%,但是A B C的点击收益分别是2,5,10,如果我们的模型没有做到保距,那么输出的预估值是5%,1%,0.5%,这样的话AUC的排序指标是满足了,但是实际收益并不是最优的。

因此需要对负采样后的CTR预估值进行校准,使得整体CTR距离真实值越近越好。

修正方法

$p$为负采样后预测值,$w$为采样率$[0,1]$,$q$为修正后的预测概率。

推导

假设输入向量$x$,预测标签$C_k$,那么可以用条件概率表示,即计算$p(C_k|x)$的概率。根据贝叶斯公式,条件概率:

上面是没有做重采样时,得到概率。
当做重采样时,只是改变了标签$C_k$的先验概率$p(C_k)$,即将$p(C_k)$变为$p′(C_k)$($C_k$的先验分布改变)。而$p(x)$是条件$x$发生的概率,不会变化。$p(x|C_k)$是后验概率,也不会变化。(假设均匀采样,不改变样本中正样本分布)

对负样本进行抽样,比例为$w$,则$n’(0)=n(0)\times w$,$n’(1)=n(1)$,则先验概率$p(1)$与$p′(1)$的关系为:

由此,若想修正$p(C_k|x)$,则:

由于预测时并不能获知正样本比例$p’(1)$,则令$p’(1) \approx p’(1|x)$
群体比例由单体概率估算