梯度如何通过随机样本反向传播?

How does a gradient backpropagates through random samples?

我正在学习策略梯度,但我很难理解梯度是如何通过随机操作的。来自 hereIt is not possible to directly backpropagate through random samples. However, there are two main methods for creating surrogate functions that can be backpropagated through.

他们有一个例子 score function:

probs = policy_network(state)
# Note that this is equivalent to what used to be called multinomial
m = Categorical(probs)
action = m.sample()
next_state, reward = env.step(action)
loss = -m.log_prob(action) * reward
loss.backward()

我试图创建一个示例:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Normal
import matplotlib.pyplot as plt
from tqdm import tqdm

softplus = torch.nn.Softplus()

class Model_RL(nn.Module):
    def __init__(self):
        super(Model_RL, self).__init__()
        self.fc1 = nn.Linear(1, 20)
        self.fc2 = nn.Linear(20, 30)
        self.fc3 = nn.Linear(30, 2)

    def forward(self, x):
        x1 = self.fc1(x)
        x = torch.relu(x1)
        x2 = self.fc2(x)
        x = torch.relu(x2)
        x3 = softplus(self.fc3(x))
        return x3, x2, x1

# basic 

net_RL = Model_RL()

features = torch.tensor([1.0]) 
x = torch.tensor([1.0]) 
y = torch.tensor(3.0)

baseline = 0
baseline_lr = 0.1

epochs = 3

opt_RL = optim.Adam(net_RL.parameters(), lr=1e-3)
losses = []
xs = []
for _ in tqdm(range(epochs)):
    out_RL = net_RL(x)
    mu, std = out_RL[0]
    dist = Normal(mu, std)
    print(dist)
    a = dist.sample()
    log_p = dist.log_prob(a)
    
    out = features * a
    reward = -torch.square((y - out))
    baseline = (1-baseline_lr)*baseline + baseline_lr*reward
    
    loss = -(reward-baseline)*log_p

    opt_RL.zero_grad()
    loss.backward()
    opt_RL.step()
    losses.append(loss.item())

这似乎神奇地工作得很好,我不明白梯度是如何通过的,因为他们提到它不能通过随机操作(但不知何故它确实如此)。

现在由于梯度无法通过随机操作流动我尝试替换 mu, std = out_RL[0]mu, std = out_RL[0].detach() 并导致错误: RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn。如果梯度不通过随机操作,我不明白为什么在操作之前分离张量很重要。

采样确实不是可微分的操作本身。但是,有两种(广泛的)方法可以缓解这种情况 - [1] REINFORCE 方法和 [2] reparameterization 方法。由于您的示例与 [1] 相关,因此我将回答 REINFORCE。

REINFORCE 所做的是完全摆脱计算图中的采样操作。但是,采样操作仍然在图形之外。所以,你的声明

.. how does the gradient passes through a random operation ..

不正确。它通过任何随机操作。让我们看看你的例子

mu, std = out_RL[0]
dist = Normal(mu, std)
a = dist.sample()
log_p = dist.log_prob(a)

a的计算不涉及创建计算图。它在技术上等同于从数据集中插入一些离线数据(如在监督学习中)

mu, std = out_RL[0]
dist = Normal(mu, std)
# a = dist.sample()
a = torch.tensor([1.23, 4.01, -1.2, ...], device='cuda')
log_p = dist.log_prob(a)

因为我们事先没有离线数据,所以我们即时创建它们,而.sample()方法只做这些。

所以,图上没有随机操作。 log_p 确定性地依赖于 mustd,就像任何标准计算图一样。如果你这样切断连接

mu, std = out_RL[0].detach()

..当然要投诉了

还有,不要被这个操作搞糊涂了

dist = Normal(mu, std)
log_p = dist.log_prob(a)

因为它本身不包含任何随机性。这只是为 Normal 分发编写乏味的 log-likelihood formula 的捷径。

@ayandas 很好地解释了第一种方式。

第二种方法,重新参数化方法,完全不同。

sample() 相比,使用 rsample() returns 样本的重新参数化 维持 计算图。

这是通过添加一个随机值(但保持模型的参数)来完成的。

用简单的代码检查this explanation