强化学习在广告点击业务中的应用
业务场景
广告投放是为了最大程度增加顾客点击的可能性,尽可能给顾客展示他们可能感兴趣的东西.假设一个小型的电子商务网站,有十个不同的版块,分别对应这不同类型的商品,网站为了增加销量,每次有客户在完成支付后,会向顾客展示广告链接,可以跳转到网站的不同版块,希望顾客也会购买其他的商品,另外,也会展示跟顾客购买类型相同的版块.但是该如何选择,可以选择随机投放,或者采用更加有效的方法.对于这个问题,也可以把它看作一个游戏,一个比多臂强盗更复杂的游戏,每次游戏中,当有顾客完成一次支付的动作,我们可以采取 N = 10 N = 10 N=10个可能的动作(10个不同的版块的广告链接),我们也可以通过历史的记录,观察这10个动作的历史点击情况,来进行投放.但是有些时候,这个结果并不合理,例如:一个顾客刚刚购买了一台笔记本电脑,但是我们根据历史点击最多的广告,推荐了文具版块,这显然不合理,此时顾客可能会更需要鼠标,键盘之类的商品.
广告投放vs多臂强盗
在广告问题中,知道顾客在购买的特定类型的商品,会给我们提供一些用户的偏好信息/上下文信息,这些信息可以称之为状态,我们可以根据状态进行更好的决策.在多臂强盗中,没有引入状态这个东西,只有动作空间(所有可能的动作集合),我们通过多次反复探索哪个动作可以获得更高的回报.我们只需要学习行动与奖励之间的关系,动作和奖励之间的关系可用一个二元组来表示: ( a , a v g ( r a ) ) (a, avg(r_a)) (a,avg(ra)),而且多臂强盗问题中,的动作空间只有10,可以很轻松通过查表进行决策.但是在广告问题中,引入了包含上下文信息的状态空间,同时对于每个状态,都有10个可以操作的动作空间,所以在这个问题中,我们要学习的是一个 ( s , a , r ) (s, a, r) (s,a,r)的三元组.如果,状太空间有1000个,动作空间有100个,那么组合后的元组就有10万个.而且随着问题的复杂度,动作和状态空间会更大,通过查询状态-动作-奖励三元表,这个显然是不合理的.
为什么要在强化学习中引入深度学习?
其实在原则上强化学习算法采用任何统计学习模型都是可以的,除了神经网络模型比较流行以外,更重要的是它可以从学习数据中对状态(state)信息进行抽象压缩,只保留重要的信息,因此,神经网络可用于学习(状态-行为)对与奖励之间的复杂关系,不需要我们像都臂强盗那样将经验( ( a , r ) (a, r) (a,r)统计信息存储起来.神经网络模型在强化学习中的角色就是Agent,接受环境中的信息(state),做出决策.
广告优化问题
1.问题的抽象
构建一个简单的模拟环境,首先确定环境中包含10个不同的状态,每个状态包含10个不同的动作,每个动作在不同的状态奖励有不同的奖励分布,所以我们学习的目标就是状体-动作和奖励(点击)之间的关系.通过Pytorch构建神经网络来对状态-动作,奖励之间的关系进行学习.
2.学习过程
- 首先初始化环境后,获得环境的初始状态 s s s
- 对状态 s s s 进行one-hot编码,输入神经网络(agent)
- 神经网络输出每个动作的预测奖励
- 经过softmax()转换后,输出每个动作的概率分布
- 从动作的概率分布中选择一个动作后,更新状态,返回该动作的奖励
- 接下来是网络反向传播的过程:根据在(s, a)获得的真实回报和模型预测出的回报,通过方向传播算法,更新网络模型
- 循环执行1~6,通过大量的迭代,神经网络模型可以准确的预测在给定状态后 s s s 后,采取每个动作的预期回报.然后选择最优的动作
3. 模拟
import random
import numpy as np
import torch as th
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
N_states = 10 # 状态数
N_actions = 10 # 动作数
SA_matrix = np.random.rand(N_states, N_actions) # 状态-动作矩阵
SA_matrix
array([[0.11422515, 0.09092939, 0.00735087, 0.70990059, 0.47693739,
0.93050111, 0.37159464, 0.52027904, 0.97294165, 0.83063053],
[0.65204554, 0.24394818, 0.34084444, 0.13033096, 0.92151777,
0.54476444, 0.48967794, 0.25235549, 0.16122367, 0.61106598],
[0.33870062, 0.34749543, 0.57444507, 0.11968757, 0.29020333,
0.92332385, 0.06073439, 0.77042511, 0.9905307 , 0.8846662 ],
[0.06776982, 0.62383732, 0.95223723, 0.30560221, 0.99981735,
0.17590289, 0.12124022, 0.6117988 , 0.11621273, 0.96640077],
[0.99773985, 0.03933903, 0.49604137, 0.36435501, 0.51852117,
0.69071493, 0.02254719, 0.60368221, 0.05470674, 0.07977912],
[0.49344154, 0.51994844, 0.39750256, 0.64393745, 0.89769783,
0.71788936, 0.66673465, 0.43628178, 0.04833739, 0.362473 ],
[0.09912828, 0.07428151, 0.6670061 , 0.58874647, 0.766104 ,
0.345672 , 0.5025386 , 0.85374251, 0.67627606, 0.16335068],
[0.09962316, 0.07237915, 0.1765769 , 0.48017888, 0.02377506,
0.07667321, 0.39435367, 0.6800073 , 0.51016068, 0.16750914],
[0.2125746 , 0.93270855, 0.26005985, 0.71137915, 0.1754577 ,
0.96373007, 0.51086934, 0.19228211, 0.02075661, 0.6396285 ],
[0.31808042, 0.69882358, 0.36034518, 0.56990081, 0.23085684,
0.47232618, 0.3692941 , 0.23126537, 0.03338656, 0.30866978]])
State-Action 矩阵可视化
不同状态下,不同动作的概率分布
辅助函数
- get_reward() : 计算真实的奖励,给网络的训练提供监督信号
- get_state() : 获取随机状态
- softmax() : 把模型的预测奖励转换为概率分布
- one_hot() : 把状态信息进行one-hot编码
- softmax() / argmax(), 提供两种action选择方法
- train() : 模型训练
# 奖励函数,输入P(s,a),输入真实的奖励
def get_reward(prob):
reward = 0
for i in range(N_actions):
if random.random() < prob:
reward += 1
return reward
def get_state():
return np.random.randint(0, N_states)
def softmax(av, tau=2.):
softm = ( np.exp(av / tau) / np.sum( np.exp(av / tau) ) )
return softm
def one_hot(N_states, state, val=1):
one_hot_vec = np.zeros(N_states)
one_hot_vec[state] = val
return one_hot_vec
Action选择函数
- softmax_choice() : 根据动作的概率分布进行选择
- argmax_choice() : 一定的概率随机选择,同时也直接基于模型的预测直接选择回报最高的动作
# 两种动作选择方法
def softmax_choice(y_pred, state, tau=0.6):
av_softmax = softmax(y_pred.data.numpy(), tau)
av_softmax /= av_softmax.sum()
action = np.random.choice(np.arange(N_actions), p=av_softmax)
reward = get_reward(SA_matrix[state, action])
new_state = get_state()
return action, reward, new_state
def argmax_choice(y_pred, state, p=0.1):
if random.random() > p:
action = np.argmax(y_pred.data.numpy())
else:
action = np.random.randint(N_actions)
reward = get_reward(SA_matrix[state, action])
new_state = get_state()
return action, reward, new_state
Training model
- rewards :记录整个训练过程中模型的平均累计奖励
- record : 记录训练过程中每个状态在不同动作下的真实奖励和预测奖励
def train(epochs=3000, lr=0.099, choice_method = 'softmax'):
if choice_method == 'softmax':
decision_func = softmax_choice
else:
decision_func = argmax_choice
cur_state = get_state() # init state
rewards = []
# 记录state, action, reward
record = {0:{'a':[],'r':[],'pr':[]},
1:{'a':[],'r':[],'pr':[]},
2:{'a':[],'r':[],'pr':[]},
3:{'a':[],'r':[],'pr':[]},
4:{'a':[],'r':[],'pr':[]},
5:{'a':[],'r':[],'pr':[]},
6:{'a':[],'r':[],'pr':[]},
7:{'a':[],'r':[],'pr':[]},
8:{'a':[],'r':[],'pr':[]},
9:{'a':[],'r':[],'pr':[]},
}
model = th.nn.Sequential(
th.nn.Linear(10, 64),
th.nn.ReLU(),
th.nn.Linear(64, 10),
th.nn.ReLU())
optimizer = th.optim.Adam(model.parameters(), lr)
loss_fn = th.nn.MSELoss() # MSE loss function
for i in range(epochs):
y_pred = model(th.Tensor(one_hot(N_states, cur_state)))
action, reward, state = decision_func(y_pred, cur_state)
#action, reward, state = argmax_choice(y_pred, cur_state, 0.1)
one_hot_reward = y_pred.data.numpy().copy() #G
record[cur_state]['pr'].append(one_hot_reward[action])
one_hot_reward[action] = reward
record[cur_state]['a'].append(action)
record[cur_state]['r'].append(reward)
reward_tensor = th.Tensor(one_hot_reward)
rewards.append(reward)
loss = loss_fn(y_pred, reward_tensor)
optimizer.zero_grad()
loss.backward()
optimizer.step()
cur_state = state
cum_mean_rewards = np.cumsum(rewards)/np.arange(1, len(rewards)+1)
return cum_mean_rewards, record
argmax_choice()训练结果:
- 平均累计奖励
- 不同状态下的累计奖励
state : 0
state : 1
state : 2
state : 3
state : 4
state : 5
state : 6
state : 7
state : 8
state : 9
argmax_choice()
- 有些状态下对 ( s , a ) − r (s, a)-r (s,a)−r的关系已经学习的很好了
- 有些状态下,还没有找到最佳action
- state : 7,9没有找到最佳的action累计误差在增加
softmax_choice()训练结果
- 累计平均奖励
- 不同状态的累计奖励
state : 0
state : 1
state : 2
state : 3
state : 4
state : 5
state : 6
state : 7
state : 8
state : 9
softmax_choice()
- softmax_choice() 在每个状态都能进行很好的学习,对奖励信号拟合的较好
softmax_choice() vs argmax_choice()
softmax_choice()完胜
接下来观察argmax_choice()的两个极端情况
- P = 0 P = 0 P=0 : 0随机探索
- P = 1 P = 1 P=1 : 完全随机
小结:
- 完全的贪心要比完全随机猜测好
- 适当的随机探索+贪心策略累
- 相比之下,基于动作概率分布来决策效果更好