注意:这篇文章没有解释原因,只是探索了一个相似方法
一般认为是可以用来简化计算的,log可以将原本的累乘转换为累加,例如 a*b*c 总体加上一个log就可以转换为 loga+logb+logc,当然,能总体加log是因为log保留的原来函数的单调方向,转成加法还有个好处就是一定程度上可以防止梯度消失或爆炸,因为连乘很容易为0或无穷
使用最朴素的policy gradient写了一个玩平衡杆的策略模型,发现如果使用SGD会出现一些问题
# 以下代码是有问题的
...
action_probabilities = model(current_env)
...
choose_action_prob = action_probabilities[choose_action]
...
loss = -reward * choose_action_prob
loss.backward()
...
问题出现在直接使用 choose_action_prob
用作梯度上升。
其实直观上感觉应该就是这样的:一局游戏的得分的期望等于这局的total reward乘以这局游戏中各个过程的出现概率。目标就是最大化这个期望,如果使用现有的梯度下降框架,则直接在前面乘以一个负号即可。感觉应该就是这样的,实验结果表明这样模型很有可能不会收敛,模型不会选择最优策略。
我猜测问题可能出现在负号上,理论上这个loss可以无限小,也就是它可能是无解的,但可以通过修改代码使得模型可以趋近于一个值(注:问题原因并不一定是这个,我猜的,但这个解决方法确实可以解决这个问题)
解决方法是让 choose_action_prob = -1/choose_action_prob
,因为我们的目的是使得上面的loss更小,也就是使得这里的choose_action_prob更大,前面加上一个负号就可以让它和loss前面的负号抵消,然后让它做除数,即不影响它原本的单调性。
我实验了一下,这种方法确实可行,但更常用的方法是取log
# 方法一
choose_action_prob = 1/choose_action_prob
loss = reward * choose_action_prob
# 方法二,注意后面加一个很小的数防止为0
choose_action_prob = torch.log(choose_action_prob + 1e-8)
loss = -reward * choose_action_prob
这样一来,loss应该是越趋近于0越好。不过实验结果表明,方法二的效果远远好于方法一,甚至于 -1/x+1 也比上面的1/x要好得多,why?
这是 log_e(x) 和 -1/x+1 的曲线

以下为完整代码:
import itertools
import threading
import gymnasium
import numpy as np
import torch
from loguru import logger
from torch.utils.tensorboard import SummaryWriter
board = SummaryWriter('./logs/division_factor_random')
torch.random.manual_seed(1)
np.random.seed(0)
def visualize_train(policy: torch.nn.Module):
with torch.no_grad():
test_env = gymnasium.make("CartPole-v1", render_mode='human')
for i in itertools.count():
total_reward = 0
obs, _ = test_env.reset(seed=0)
while True:
action = torch.argmax(policy(torch.FloatTensor(obs)))
obs, reward, terminated, _, info = test_env.step(action.item())
total_reward += reward
if terminated:
board.add_scalar("test/reward", total_reward, i)
logger.info(f"total reward: {total_reward}")
break
test_env.close()
class Actor(torch.nn.Module):
def __init__(self):
super().__init__()
self.dnn = torch.nn.Sequential(
torch.nn.Linear(in_features=4, out_features=20),
torch.nn.ReLU(),
torch.nn.Linear(in_features=20, out_features=2),
torch.nn.Softmax(dim=0)
)
def forward(self, x):
return self.dnn(x)
actor = Actor()
threading.Thread(target=visualize_train, args=(actor,)).start()
optimizer = torch.optim.Adam(params=actor.parameters())
env = gymnasium.make("CartPole-v1", render_mode=None)
for game_count in itertools.count():
obs, _ = env.reset(seed=0)
acts = []
rewards = []
for s in itertools.count():
action_probs = actor(torch.tensor(obs, dtype=torch.float))
action = torch.distributions.Categorical(action_probs).sample().item()
# if np.random.random() < 0.1: action = np.random.randint(0, 2)
obs, reward, terminated, _, info = env.step(action)
acts.append(1 / action_probs[action])
rewards.append(reward)
if terminated:
# loss with discount factor=0.99
loss = sum([a * weighted_r for a, weighted_r in zip(acts, [sum([r * 0.99 ** j for j, r in enumerate(rewards[i:])]) for i in range(len(rewards))])])
# loss = (sum(rewards) * sum(acts))
optimizer.zero_grad()
loss.backward()
optimizer.step()
break
env.close()