Weight Initialization

权重初始化方法

策略:

Activation Function Uniform Distribution $[-r, +r]$ Normal Distribution
sigmoid $r = 4\sqrt{\frac{6}{n_{in} + n_{out}}}$ $\sigma = 4\sqrt{\frac{2}{n_{in} + n_{out}}}$ Glorot
tanh $r = \sqrt{\frac{6}{n_{in} + n_{out}}}$ $\sigma = \sqrt{\frac{2}{n_{in} + n_{out}}} = \sqrt{\frac{1}{n_{in}}}$ Glorot
relu $r = \sqrt{2}\sqrt{\frac{6}{n_{in} + n_{out}}}$ $\sigma = \sqrt{\frac{4}{n_{in} + n_{out}}} = \sqrt{\frac{2}{n_{in}}}$ He

示例:

1
2
3
4
glorot = np.sqrt(2.0 / (self.deep_layers[i - 1] + self.deep_layers[i]))
weights["layer_%d" % i] = tf.Variable(
np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i - 1], self.deep_layers[i])),
dtype=np.float32)

FM embedding

FM模型

Factorization Machines

一般的线性模型

线性模型压根没有考虑特征间的关联。

  • 为了表述特征间的相关性,我们采用多项式模型。
  • $x_i$为第$i$个特征,$x_ix_j$(相乘,得到一个数)表示特征$x_i$和$x_j$的特征组合,其系数$w_{ij}$即为我们学习的参数

Poly2模型

但这个模型有些问题:

  • 参数的个数是非常的多。$d$为特征数量(注意,one-hot后每个为单独一类,因此特征数量大大增加),对于参数数量:一次项有$d+1$个,二次项(即组合特征的参数)共有$\frac{d(d−1)}{2}$个。
  • 参数与参数之间被认为彼此独立,在稀疏场景下(one-hot)二次项的训练是很困难的。因为要训练$w_{ij}$,需要有大量的$x_i$和$x_j$都非零的样本(只有非零组合才有意义)。而样本本身是稀疏的,满足$x_i x_j \ne 0$的样本会非常少,样本少则难以估计参数$w_{ij}$,训练出来容易导致模型的过拟合。

为解决这些问题,提出了FM模型。
上文Poly2模型,对于不同的特征对$x_i,x_j$和$x_i,x_k$认为是完全独立的,对参数$w_{ij}$和$w_{ik}$分别进行训练。
而实际上并非如此,不同的特征之间进行组合并非完全独立。

  • 因此,对所有的$i$、$j$求$w_{ij}$,也就是一个矩阵$W_{ij}$时($shape=(d,d)$),可以分解成两个矩阵相乘$\mathbf{V}^T\mathbf{V}$($shape=(d,k)*(k,d)$)。
  • 这样,在求某个$w_{ij}$时,可以分解成两个向量内积$(\mathbf{v}_i \cdot \mathbf{v}_j)$。其中,$\mathbf{v}_i$作为特征$i$的隐向量,就是$\mathbf{V}$的第$i$列,是对特征$i$的描述(可理解成类似专用于求二次项时$x_i$的权重),长度为$k$。
  • 换句话说,一个(one-hot后布尔型)特征$i$,对应一个隐向量$\mathbf{v}_i$。

怎么解决上述问题的?
1)只学隐向量,参数少;2)对于几乎不出现或者很少出现的$i,j$组合,可以通过包含$i$和包含$j$的其他组合的数据,学到隐向量$\mathbf{v}_i$、$\mathbf{v}_j$就够了,反正计算时直接使用隐向量内积

FM模型

隐向量长度为$k\ll d$。$(\mathbf{v}_i \cdot \mathbf{v}_j)$为内积,其乘积为原来的$w_{ij}$,即$w_{ij} = ( \mathbf{v}_i \cdot \mathbf{v}_j ) = \sum_{t=1}^kv_{i,t} \cdot v_{j,t}$
二次项参数个数减少为$kd$个,$d$为特征数量(one-hot每个为一类),$k$为隐向量长度。

FFM模型

FM模型不区分$x_i,x_j$组合和$x_k,x_j$组合中$x_j$的区别,集,使用同一个隐向量$\mathbf{v}_j$来对应特征$x_j$,与不同的$x_i$、$x_k$相乘。
而FFM则认为,对于特征$x_j$的隐向量,计算二次项系数时,对于不同的两种field组合,应该使用(各自)不同field的隐向量。
这样一来,若field有$f$个,则每个特征提供的隐向量就应该有$f$个(对应相乘特征所属field有$f$个)(而FM中只有1个哦,不区分field)。
而对于二次项参数数量,FFM模型相当于比FM模型,多了$f$倍,$f \times kd$。

FM embedding
对FM模型,如果不进行最后一步的向量内积、累加,则可以认为是对$x_i$的embedding。(常数项各记录都一样,有没有无所谓)

如果$x_i$都来自于one-hot,取值为1或0,那就是

为什么说FM embedding能降维
one-hot稀疏,但可能很长。
对于某个one-hot的数据:

  • 直接做输入时,对于一个one-hot会有多个类别值,长度为其“类别数量”。
  • 而进行FM embedding后,由于其只有一个“1”,输出为一个隐向量,长度为$k$。(或者算上一次项系数,那就是$k+1$)
    • 其实是one-hot特征组输入进来后,会对应“类别数量-1”个0,以及1个由“1”的位置$i$决定取值的隐向量$\mathbf{v}_i$。
    • 则相当于给定一组one-hot特征,只会对应一个特定的隐向量。

所以,对于一个one-hot,或者说对于一个field,特征维度从“类别数量”降至$k$。

即,一组同一个field的特征,对于该field的参数矩阵$\mathbf{V_{this_field}}$只取一列。$\mathbf{V} = [\mathbf{V_{field1}}, \mathbf{V_{field2}}, \mathbf{V_{field3}}, …]$
我们看到,降维是以field为粒度的,所以这也是后文NN结构中,对同一个field专用一个FC连接的原因。

FM中二次项求算方法

对$d^2$种组合,求算$k$长度的内积,复杂度为$O(kd^2)$
下面方法,可以将复杂度降低到$O(kd)$

定义前向网络计算方式时,使用这种方法

利用NN的FM embedding

对每个field,连接一个(只对本field的)FC层,神经元数量即为$k$隐向量长度。
一个field,对应其下one-hot的多个特征,即,这些特征使用了一个FC层。
值得注意的是,即使各个field的维度(one-hot转化出的特征的数量)是不一样的,但是它们embedding后长度均为$k$。

  • 这样做是因为,本层的参数$W$($shape=(k,\text{n_one-hot_features}))$)相当于上文的$\mathbf{V_{this_field}}$
  • 区分各个field,是为了利用one-hot的特性,由仅有1个“1”从$\mathbf{V_{this_field}}$只取出一个隐向量(“1”所在的位置,指定选哪列)(否则所有$d$个特征连接同一个FC层,会有多个“1”,会从$\mathbf{V} = [\mathbf{V_{field1}}, \mathbf{V_{field2}}, \mathbf{V_{field3}}, …]$中取出多个隐向量,并各元素加在一起)(但如果同field中就是有多个“1”,不是one-hot,那就确实需要同field中取多个隐向量,按各元素加在一起)
  • 此外,也是避免全局全连接,造成参数数量太多的问题。

怎么训练
按FM公式,求预测值;数据中有真实值;梯度下降求模型参数
EM embedding时,可以锁或不锁embedding层的参数

强化学习 DQN

DQN(Deep Q-Network),实际上就是Q Learning和Deep Learning的结合,使用DNN代替Q表给出$Q$值,以解决某些场景中,状态空间或者动作空间过大(状态数、动作数过多)的问题。

价值函数近似(Value Function Approximation)的方法,通过用函数(而不是Q表)来表示$Q$值。

为什么叫近似,因为我们并不知道$Q$值的实际分布情况,本质上就是基于训练,用一个函数来近似$Q$值的分布:

这个函数可以是线性的也可以使非线性的,例如:

当$f$为DNN时,则构成了DQN。

值得注意,往往求算$s$的所有$a$,故$f$的输入输出形式,除了“$f(s, a) = Q(s, a)$”,输入状态+动作,输出该状态+动作对应的$Q值$;还可以“$f(s) = [Q(s, a_1), Q(s, a_2), Q(s, a_3), …, Q(s, a_n)]$”,输入状态,输出该状态下所有动作的$Q$值。

那么如何训练NN呢?

我们知道,神经网络的训练是一个最优化问题,最优化一个损失函数loss function,也就是标签和网络输出的偏差,目标是让损失函数最小化。
为此,我们需要有样本,巨量的有标签数据,然后通过反向传播使用梯度下降的方法来更新神经网络的参数。
使用什么作为标签呢?
DQN的输出是$Q$值,这个是预测结果。
我们回看Q Learning里的更新公式

新学习时,更新的差距,在于(与$\alpha$因子相乘的)$R(s,a) + \gamma \max_{a_i}Q(s’, a_i)$和$Q(s,a)_{old}$的差距。

  • 将$Q(s,a)_{old}$看做当前的现状;(或者此处理解为,DNN给出的预测值)
  • 将$R(s,a) + \gamma \max_{a_i}Q(s’, a_i)$看做利用Reward和Q计算出来的目标Q值。把这个目标Q值作为标签,让NN给出的Q值趋近于目标Q值。(或者此处理解为,将对未来的估计当做此处的真实$Q$值,要知道真正的真实$Q$值我们是无法知道的)
  • loss使用这两者mse即可

经验回放 Experience Replay
为什么要采用经验回放的方法?
神经网络进行训练时,假设样本是独立同分布的。而通过强化学习采集到的数据之间存在着关联性,利用这些数据进行顺序训练,神经网络当然不稳定。
经验回放通过随机抽取,可以打破数据间的关联。

具体做法是把每个时间步agent与环境交互得到的转移样本$(s_t, a_t, r_t, s_{t+1})$储存到回放记忆单元,要训练时就随机拿出一些minibatch来训练。

目标网络 Target Net

使用另一个网络(这里称为TargetNet)产生Target Q值。具体地,

  • 当前网络MainNet的输出$Q(s,a)_{old}$或者理解为$Q(s,a)_{predict}$(计算预测值);
  • 目标网络TargetNet的输出,代入上面求$R(s,a) + \gamma \max_{a_i}Q(s’, a_i)$公式中得到Target Q值(计算标签值)。
  • 选择$a’$的时候,还是使用MainNet的$Q$计算并选择的)

根据loss更新MainNet的参数,每经过N轮迭代,将MainNet的参数复制给TargetNet。

引入TargetNet后,再一段时间里Target Q值是保持不变的,一定程度降低了当前Q值和目标Q值的相关性,提高了算法稳定性。

demo

主逻辑流程,还是Q Learning的框架

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
def run_maze():
step = 0
# 迭代300回合
for episode in range(300):
# 初始状态
observation = env.reset()

while True:
# fresh env
env.render()

# 根据策略,选择本次动作
action = RL.choose_action(observation)

# 执行动作,获取奖励、下一状态
observation_, reward, done = env.step(action)

# 为DQN全局存储(s_t, a_t, r_t, s_{t+1}),对应一次交互
# 注意这些都是数值(list或单值),保存时直接flat
RL.store_transition(observation, action, reward, observation_)

# 更新计算Q值的DNN,200步后每隔5步训练一次
# 训练时会抽取minibatch,梯度下降优化loss,以获取DNN的参数
if (step > 200) and (step % 5 == 0):
RL.learn()

# 更新状态
observation = observation_

# 当前步骤为最后的结束步骤时,本回合结束,break
if done:
break
step += 1

# end of game
print('game over')
env.destroy()

DNN主要负责对MainNet和TargetNet的训练,在进入“更新”步骤时,抽取minibatch,训练两个DNN
此外还有记忆单元,全局缓存$(s_t, a_t, r_t, s_{t+1})$

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import numpy as np
import tensorflow as tf

np.random.seed(1)
tf.set_random_seed(1)


# Deep Q Network off-policy
class DeepQNetwork:
def __init__(
self,
n_actions,
n_features,
learning_rate=0.01,
reward_decay=0.9,
e_greedy=0.9,
replace_target_iter=300,
memory_size=500,
batch_size=32,
e_greedy_increment=None,
output_graph=False,
):
# 动作个数
self.n_actions = n_actions
# (一个)状态对应的特征个数,即状态的特征维度
self.n_features = n_features
# DNN学习率
self.lr = learning_rate
# gamma,计算label用
self.gamma = reward_decay
# 把MainNet参数拷贝至TargetNet的轮数
self.replace_target_iter = replace_target_iter
# 经验回放中保存的记录数
self.memory_size = memory_size
# DNN minibatch大小
self.batch_size = batch_size
# epsilon如果增加的话,最大增至多少
self.epsilon_max = e_greedy
# epsilon随时间增加(0到epsilon_max)
self.epsilon_increment = e_greedy_increment
# epsilon,策略选择的e_greedy方法用
self.epsilon = 0 if e_greedy_increment is not None else self.epsilon_max

# 更新DNN的当前轮数(决定是否把MainNet参数拷贝至TargetNet)
self.learn_step_counter = 0

# 经验回放,注意[s, a, r, s_]被flat了
self.memory = np.zeros((self.memory_size, n_features * 2 + 2))

# 构建前向计算OP、loss OP、train(optimizer) OP
self._build_net()

# 取出两个Net的参数
t_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='target_net')
e_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='eval_net')

# 构建Net参数拷贝OP
with tf.variable_scope('hard_replacement'):
self.target_replace_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]

# 启动默认图
self.sess = tf.Session()

# for tensorboard
if output_graph:
# $ tensorboard --logdir=logs
tf.summary.FileWriter("logs/", self.sess.graph)

# run tf变量初始化
self.sess.run(tf.global_variables_initializer())

# 每轮学习的cost值保存,用于画曲线
self.cost_his = []

def _build_net(self):
# ------------------ all inputs ------------------------
# 不限行,列数为特征维度
self.s = tf.placeholder(tf.float32, [None, self.n_features], name='s') # input State
self.s_ = tf.placeholder(tf.float32, [None, self.n_features], name='s_') # input Next State
# 不限行,列数为1
self.r = tf.placeholder(tf.float32, [None, ], name='r') # input Reward
self.a = tf.placeholder(tf.int32, [None, ], name='a') # input Action

w_initializer, b_initializer = tf.random_normal_initializer(0., 0.3), tf.constant_initializer(0.1)

# ------------------ build evaluate_net ------------------
# 定义MainNet前向计算输出OP
with tf.variable_scope('eval_net'):
e1 = tf.layers.dense(self.s, 20, tf.nn.relu, kernel_initializer=w_initializer,
bias_initializer=b_initializer, name='e1')
self.q_eval = tf.layers.dense(e1, self.n_actions, kernel_initializer=w_initializer,
bias_initializer=b_initializer, name='q')

# 定义TargetNet前向计算输出OP
# ------------------ build target_net ------------------
with tf.variable_scope('target_net'):
t1 = tf.layers.dense(self.s_, 20, tf.nn.relu, kernel_initializer=w_initializer,
bias_initializer=b_initializer, name='t1')
self.q_next = tf.layers.dense(t1, self.n_actions, kernel_initializer=w_initializer,
bias_initializer=b_initializer, name='t2')

# 定义q_target OP
with tf.variable_scope('q_target'):
q_target = self.r + self.gamma * tf.reduce_max(self.q_next, axis=1, name='Qmax_s_') # shape=(None, )
# 反传截断,避免对loss进行反传时影响到TargetNet的参数
self.q_target = tf.stop_gradient(q_target)

# 定义q_eval OP
with tf.variable_scope('q_eval'):
# 在self.a前面增加indices,生成的indices指定了哪一行取哪个动作(因为动作是0, 1, 2, ...编码的)
a_indices = tf.stack([tf.range(tf.shape(self.a)[0], dtype=tf.int32), self.a], axis=1)
# 收集params[indices],即从q_eval=[Q(s, a0), Q(s, a1), Q(s, a2), ...]中取本次a对应的q
self.q_eval_wrt_a = tf.gather_nd(params=self.q_eval, indices=a_indices) # shape=(None, )

# 定义loss OP
with tf.variable_scope('loss'):
self.loss = tf.reduce_mean(tf.squared_difference(self.q_target, self.q_eval_wrt_a, name='TD_error'))

# 定义train (optimizer) OP
with tf.variable_scope('train'):
self._train_op = tf.train.RMSPropOptimizer(self.lr).minimize(self.loss)

def store_transition(self, s, a, r, s_):
if not hasattr(self, 'memory_counter'):
self.memory_counter = 0
# flat
transition = np.hstack((s, [a, r], s_))
# replace the old memory with new memory
index = self.memory_counter % self.memory_size
self.memory[index, :] = transition
self.memory_counter += 1

def choose_action(self, observation):
# 在np.newaxis位置增加一个轴(因为只有一行记录)
observation = observation[np.newaxis, :]

if np.random.uniform() < self.epsilon:
# MainNet执行前向计算
actions_value = self.sess.run(self.q_eval, feed_dict={self.s: observation})
action = np.argmax(actions_value)
else:
action = np.random.randint(0, self.n_actions)
return action

def learn(self):
# 是否拷贝MainNet参数至TargetNet
if self.learn_step_counter % self.replace_target_iter == 0:
self.sess.run(self.target_replace_op)
print('\ntarget_params_replaced\n')

# sample batch memory from all memory
if self.memory_counter > self.memory_size:
# 缓存不够,有放回抽取
sample_index = np.random.choice(self.memory_size, size=self.batch_size)
else:
sample_index = np.random.choice(self.memory_counter, size=self.batch_size)
batch_memory = self.memory[sample_index, :]

_, cost = self.sess.run(
[self._train_op, self.loss],
feed_dict={
self.s: batch_memory[:, :self.n_features], // 前特征维度个数值,为s
self.a: batch_memory[:, self.n_features], // 1个,为a
self.r: batch_memory[:, self.n_features + 1], // 1个,为r
self.s_: batch_memory[:, -self.n_features:], // 后特征维度个数值,为s_next
})

# 记录cost
self.cost_his.append(cost)

# increasing epsilon
self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
self.learn_step_counter += 1

def plot_cost(self):
import matplotlib.pyplot as plt
plt.plot(np.arange(len(self.cost_his)), self.cost_his)
plt.ylabel('Cost')
plt.xlabel('training steps')
plt.show()

if __name__ == '__main__':
DQN = DeepQNetwork(3,4, output_graph=True)

强化学习 SARSA

SARSA与Q Learning类似,都是最终训练生成一个Q表。

状态S 动作A-飞 动作A-不飞
$S_1$ 1 20
$S_2$ 20 -100
$S_{n-1}$ -100 30
$S_n$ 50 -20

[推理]

SARSA的推理与Q Learning一样

[训练]

与Q Learning一样,我们会经历状态$S_1$,然后挑选一个带来最大潜在奖励的动作比如$a_2$,这样我们就到达了状态$S_2$。
在这一步,如果使用的是Q Learning,选取一个会在$S_2$上会带来最大的潜力动作,来用于计算更新Q值;但是在真正处于$S_2$上要做决定时,却不一定会选取到那个带来最大奖励的动作($\varepsilon$-greedy)。
如果使用的是SARSA,则说到做到,在$S_2$上用于计算更新Q值的动作,也是接下来要做的动作。即,对于$Q(S_1, a_2)$的计算,不再需要使用$\max Q$,而是使用策略确定在$S_2$上选取哪个动作,比如$a_1$,然后使用$Q(S_2, a_1)$计算更新$Q(S_1, a_2)$,并最后进入$S_2$并执行$a_2$。
注意,SARSA的动作选择,只在初始化时由策略动态选择,后续各步骤迭代时,动作其实已经是确定的了。确定的时机是在上一步中,确定的方法仍是由策略给出的。

[Q值的更新(对比)]

SARSA算法(on-policy)

1
2
3
4
5
6
7
8
9
10
Initialize Q arbitrarily // 随机初始化Q表
Repeat (for each episode): // 每一次从开始到结束是一个episode
Initialize S // S为初始位置的状态
Choose a from s using policy derived from Q (epsilon-greedy) // 仅第一步需使用策略选择下一步,后续步骤则在前一步中已经确定了下一步
Repeat (for each step of episode):
Take action a, observe r, s_next
Choose a_next from s_next using policy derived from Q (epsilon-greedy) // 根据策略,定死了下一步选哪个
Q(s, a) := (1 - ALPHA) * Q(s, a) + ALPHA * (r + GAMMA * Q(s_next, a_next)) // 更新Q(s, a)
s := s_next; a := a_next // 更新s、a
until s is terminal // 到游戏结束为止,即到达最后一步

处于状态s时,1)若是第一步则根据策略来选取动作a;2)若是中间步骤则使用上一步中指定的a。进而观测到下一步状态s_next,并再次根据策略选择动作a_next,这样就有了一个[s, a, r, s_next, a_next]序列(SARSA)。
处于状态s_next时,在上一步就知道了要采取哪个a_next,此时真的采取a_next这个动作…
动作的选取遵循相同的策略(比如epsilon-greedy)。

Q-learning算法(off-policy)

1
2
3
4
5
6
7
8
9
Initialize Q arbitrarily // 随机初始化Q表
Repeat (for each episode): // 每一次从开始到结束是一个episode
Initialize S // S为初始位置的状态
Repeat (for each step of episode):
Choose a from s using policy derived from Q (epsilon-greedy) // 每次都是根据当前S和Q,使用一种策略,动态得到(下一步)
Take action a, observe r, s_next
Q(s, a) := (1 - ALPHA) * Q(s, a) + ALPHA * (r + GAMMA * Q(s_next, a_maxQ_at_snext)) // 更新Q(s, a)
s := s_next // 只更新s,a由下一步动态确定
until s is terminal // 到游戏结束为止,即到达最后一步

处于状态s时,根据策略来选取动作a。进而观测到下一步状态s_next,这样就有了一个[s, a, r, s_next]序列。
处于状态s_next时,再次根据策略来选取动作a_next

[SARSA lambda]

对$Q$值更新,不只更新本次获取$r$的步骤,而是更新之前各个步骤,不过使用$\lambda$来衰减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Initialize Q arbitrarily // 随机初始化Q表
Repeat (for each episode): // 每一次从开始到结束是一个episode
for all s in S, a in A: E(s, a) = 0 // 初始化E表
Initialize S // S为初始位置的状态
Choose a from s using policy derived from Q (epsilon-greedy) // 仅第一步需使用策略选择下一步,后续步骤则在前一步中已经确定了下一步
Repeat (for each step of episode):
Take action a, observe r, s_next
Choose a_next from s_next using policy derived from Q (epsilon-greedy) // 根据策略,定死了下一步选哪个
delta = r + GAMMA * Q(s_next, a_next) - Q(s, a)
E(s, a) := E(s, a) + 1 // 更新E表
for all s in S, a in A: // 对E表中所有点
Q(s, a) += E(s, a) * ALPHA * delta // 利用更新后的E表,计算更新Q(s, a)
E(s, a) := LAMBDA * E(s, a) + 1 // 衰减E表
s := s_next; a := a_next // 更新s、a
until s is terminal // 到游戏结束为止,即到达最后一步

先定义一个$E$表,用来记录经过的各个位置($S$)。
每走一步,如果这个点不在$E$表中则添加这个点到$E$表中,更新$E(s,a)$的值改为$E(s,a)+1$(还可以优化,下面说)。
而在走下一步之前,会对$E$表进行整体衰减。
也就是说每走一步,就要对$E$表的当前位置的值进行刷新,然后计算$Q$表,然后衰减$E$表。
衰减的意义就在于如果一旦到达终点,就可以体现出来$E$表中各个位置对到达终点的不可或缺性。

  • 如果衰减比例为0,也就是每次都给$E$表里的值乘0,就意味着表里只会剩下最后一个位置,这就是上文的SARSA;
  • 如果衰减比例为1,则不会出现衰减,完全保留并更新之前经过的所有$S$。但有一个问题:$E$表里的重复的越多的位置收益越大。前面我们说每次经过这个某个位置,都把$E$表里对应值+1,这样对有些位置会很不公平,可能会出现离终点最近的那个位置的$E$值反而比中间的某个点的$E$值要低,这很不科学。优化办法就是给$E$里的值定个上限,比如就是1,每次走到这个位置,就把他重新定为1,然后从1开始衰减,这样就不会出现上述的问题了。(在$E(s, a) = 1$的同时,可以令$E(s, others) = 0$,减少环的影响)

强化学习 Q-Learning

强化学习中有状态(State)、动作(Action)、奖赏(Reward)这三个要素。智能体(Agent)会根据当前状态来采取动作,并记录被反馈的奖赏,以便下次再到相同状态时能采取更优的动作。

提到Q-learning,我们需要先了解Q的含义。
Q为动作效用函数(action-utility function),用于评价在特定状态下采取某个动作的优劣。
它是智能体的记忆。训练时学的就是这个东西。

在Flappy Bird这个问题中,状态和动作的组合是有限的。所以我们可以把Q当做是一张表格(Q Table)。
表中的每一行记录了状态$S_i = (x_i, y_i)$,选择不同动作(飞或不飞)时的奖赏:

状态S 动作A-飞 动作A-不飞
$S_1$ 1 20
$S_2$ 20 -100
$S_{n-1}$ -100 30
$S_n$ 50 -20

这张表一共$n$行,即$n$个状态,每个状态所对应的动作都有一个效用值

[推理]
理想状态下,在完成训练后,我们会获得一张完美的Q表格。
我们希望只要小鸟根据当前位置查找到对应的行,选择效用值较大的动作作为当前帧的动作,就可以无限地存活。

[训练]

  • 初始化 $Q$ = {};
  • while $Q$未收敛(状态有限,一定会收敛)
    • 初始化小鸟的位置为$S$,开始新一轮游戏
    • while $S$ != 死亡状态
      • 使用策略$\pi$,获得动作$a=\pi(S)$
      • 使用动作$a$进行游戏,获得小鸟的新位置$S’$,与奖励$R(S,a)$
      • 更新$Q(S,a) := (1-\alpha) \times Q(S,a) + \alpha \times [R(S,a) + \gamma \max_{a_i}Q(S’, a_i)]$
      • 更新$S := S’$

其中,使用策$\pi$,获得动作$a=\pi(S)$,最直观易懂的策略是根据Q表格来选择效用最大的动作(若两个动作效用值一样,如初始时某位置处效用值都为0,那就选第一个动作)。但这样的选择可能会使Q陷入局部最优。改进的策略为$\varepsilon$-greedy方法:每个状态以$\varepsilon$的概率选取当前状态下效用值最大的动作,而剩下的$1-\varepsilon$的概率则进行随机选取。

Q值的更新

其中等号$Q(S,a)$为旧值,$\alpha$为学习速率(learning rate),$\gamma$为折扣因子(discount factor)。根据公式可以看出,
学习速率$\alpha$越大,保留之前训练的效果就越少。
折扣因子$\gamma$越大,$\max_{a_i}Q(S’, a_i)$所起到的作用就越大。

但$\max_{a_i}Q(S’, a_i)$指什么呢?
小鸟在对状态进行更新时,会关心到眼前利益$R(S, a)$,和记忆中的利益$\max_{a_i}Q(S’, a_i)$。
$\max_{a_i}Q(S’, a_i)$是记忆中的利益,即小鸟记忆中新位置$S’$能给出的最大效用值。
如果小鸟在过去的游戏中于位置$S’$的某个动作上吃过甜头(例如选择了某个动作之后获得了50的奖赏),这个公式就可以让它提早地得知这个消息,以便使下回再通过位置$S$时选择正确的动作继续进入这个后续吃甜头的位置$S’$。
可以看出,$\gamma$越大,小鸟就会越重视“后续利益的以往经验”$\max_{a_i}Q(S’, a_i)$,$\gamma$越小,小鸟只重视“眼前利益”$R(S, a)$。

$R(S, a)$由训练时交互获取,$\alpha$、$\gamma$为超参,$Q(S,a)$为学到的模型参数。