主题
离线强化学习
离线强化学习(Offline RL,也称 Batch RL)只利用事先收集好的固定数据集学习策略,训练过程中不再与环境交互。它适用于医疗、自动驾驶、推荐系统、工业控制等在线试错成本高或有风险、但已经积累了大量历史数据的场景。核心挑战是分布偏移:学到的策略可能选择数据集中很少出现的动作,而 Q 函数对这些动作的估计往往严重偏高,又无法通过交互来纠正。代表方法包括 BCQ、TD3+BC、CQL、IQL 和 Decision Transformer。
问题设定
给定数据集
与在线 RL、模仿学习的区别
| 在线 RL | 离线 RL | 模仿学习(BC) | |
|---|---|---|---|
| 与环境交互 | 可以 | 不可以 | 不可以 |
| 需要奖励 | 是 | 是 | 否 |
| 数据质量要求 | — | 任意(可以是次优数据) | 需要专家数据 |
| 能否超过数据中的策略 | — | 有可能 | 一般不能 |
离线 RL 的吸引力在于:它有可能从次优数据中“拼接”出比任何一条数据轨迹都好的策略,而 行为克隆 最多只能模仿数据中的行为。
分布偏移与外推误差
为什么不直接在固定数据上跑 DQN 或 SAC?考虑 Q-learning 的目标:
离线 RL 的各种方法本质上都在回答:如何让策略或 Q 函数待在数据分布附近?
策略约束方法:BCQ、TD3+BC
BCQ(Batch-Constrained Q-learning,Fujimoto et al., 2019):用生成模型(VAE)拟合行为策略,只在生成模型认为“数据中可能出现”的动作中选取 Q 值最高的,从而避免 OOD 动作。
TD3+BC(Fujimoto & Gu, 2021):极简的做法——在 TD3 的 Actor 损失上加一个行为克隆项:
其中
保守价值估计:CQL
CQL(Conservative Q-Learning,Kumar et al., 2020)不约束策略,而是直接让 Q 函数变得保守:在标准 TD 损失之外,压低所有动作(特别是 OOD 动作)的 Q 值,同时抬高数据中动作的 Q 值。对离散动作,常用的 CQL(H) 正则项为:
IQL
IQL(Implicit Q-Learning,Kostrikov et al., 2022)更进一步:训练过程中完全不查询数据集外的动作。它用期望分位数回归(expectile regression)从数据中的
Q 的目标改为
序列建模视角:Decision Transformer
Decision Transformer(Chen et al., 2021)完全换了一个视角:把强化学习看成条件序列建模。把轨迹表示为
代码:CartPole 上的离线 RL 玩具实验
为了直观看到分布偏移的影响,我们在 CartPole 上做一个小实验。用一个手写的“专家”规则
- 纯专家数据:5000 条转移,没有任何随机动作——动作覆盖极窄;
- 高噪声数据:20000 条转移,80% 的概率执行随机动作。
在每个数据集上分别训练:行为克隆(BC)、朴素离线 DQN、离散动作 CQL(
py
"""离线强化学习玩具实验(CartPole-v1):行为克隆(BC) vs 朴素离线 DQN vs 离散动作 CQL。
专家规则 a = 1[θ + 0.5·θ̇ > 0]。对比两个数据集:
(1) 纯专家数据(5000 条,无噪声,动作覆盖极窄)
(2) 高噪声数据(20000 条,80% 概率随机动作)"""
import random
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
random.seed(0); np.random.seed(0); torch.manual_seed(0)
env = gym.make("CartPole-v1")
env.action_space.seed(0) # action_space.sample() 使用独立的随机数生成器
GAMMA = 0.99
def expert(o):
return int(o[2] + 0.5 * o[3] > 0)
# ---------- 1) 收集固定数据集,之后不再与环境交互 ----------
def collect(eps_data, n_transitions):
data, ep_returns = [], []
o, _ = env.reset(seed=0); G = 0.0
while len(data) < n_transitions:
a = env.action_space.sample() if random.random() < eps_data else expert(o)
o2, r, te, tr, _ = env.step(a)
data.append((o, a, r, o2, float(te))); G += r; o = o2
if te or tr:
ep_returns.append(G); G = 0.0; o, _ = env.reset()
S, A, R, S2, D = (torch.as_tensor(np.array(x), dtype=torch.float32) for x in zip(*data))
return S, A.long(), R, S2, D, np.mean(ep_returns)
def net():
return nn.Sequential(nn.Linear(4, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 2))
def evaluate(model, n=20):
e = gym.make("CartPole-v1"); tot = []
for i in range(n):
o, _ = e.reset(seed=10_000 + i); done, g = False, 0.0
while not done:
with torch.no_grad():
o, r, te, tr, _ = e.step(int(model(torch.as_tensor(o, dtype=torch.float32)).argmax()))
g += r; done = te or tr
tot.append(g)
return np.mean(tot)
def train_bc(steps=5_000):
pi = net(); opt = torch.optim.Adam(pi.parameters(), lr=1e-3)
for _ in range(steps):
i = torch.randint(0, len(S), (256,))
loss = F.cross_entropy(pi(S[i]), A[i])
opt.zero_grad(); loss.backward(); opt.step()
return pi
def train_q(cql_alpha=0.0, steps=20_000):
q, q_t = net(), net(); q_t.load_state_dict(q.state_dict())
opt = torch.optim.Adam(q.parameters(), lr=3e-4)
for t in range(steps):
i = torch.randint(0, len(S), (256,))
qs = q(S[i])
q_sa = qs.gather(1, A[i, None]).squeeze(1)
with torch.no_grad():
y = R[i] + GAMMA * (1 - D[i]) * q_t(S2[i]).max(1).values
loss = F.mse_loss(q_sa, y)
if cql_alpha > 0: # CQL 正则:压低所有动作的 Q(logsumexp),抬高数据中动作的 Q
loss = loss + cql_alpha * (torch.logsumexp(qs, dim=1) - q_sa).mean()
opt.zero_grad(); loss.backward(); opt.step()
if t % 500 == 0:
q_t.load_state_dict(q.state_dict())
return q
for name, eps_data, n in [("纯专家数据", 0.0, 5_000), ("高噪声数据", 0.8, 20_000)]:
S, A, R, S2, D, behavior_return = collect(eps_data, n)
print(f"== {name}:{n} 条转移,行为策略平均回报 {behavior_return:.1f}")
print(f" 行为克隆 BC : 评估回报 {evaluate(train_bc()):6.1f}")
print(f" 朴素离线 DQN : 评估回报 {evaluate(train_q(0.0)):6.1f}")
print(f" CQL (α=1.0) : 评估回报 {evaluate(train_q(1.0)):6.1f}")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
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
实际运行输出:
text
== 纯专家数据:5000 条转移,行为策略平均回报 500.0
行为克隆 BC : 评估回报 500.0
朴素离线 DQN : 评估回报 9.4
CQL (α=1.0) : 评估回报 500.0
== 高噪声数据:20000 条转移,行为策略平均回报 39.3
行为克隆 BC : 评估回报 498.7
朴素离线 DQN : 评估回报 189.9
CQL (α=1.0) : 评估回报 267.91
2
3
4
5
6
7
8
2
3
4
5
6
7
8
结果解读(单次运行,玩具规模,请勿过度推广):
- 纯专家数据上,朴素离线 DQN 彻底失败(9.4 分,比随机策略还差)。数据中每个状态只出现专家选择的那一个动作,另一个动作的 Q 值完全没有数据约束,
操作恰恰会选中这些被高估的分布外动作——这正是外推误差的典型表现。加上 CQL 正则后,分布外动作的 Q 值被压低,策略回到数据分布内,拿到满分。 - 高噪声数据上,CQL 比朴素 DQN 好一些(267.9 对 189.9),但两者都远不如行为克隆(498.7)。原因在于这个数据集的特殊性:80% 随机 + 20% 专家,意味着在每个状态下专家动作出现的概率约为
,高于另一个动作,所以 BC 学到的“最常见动作”恰好就是专家动作。而 Q-learning 类方法需要从行为策略平均只能坚持约 39 步的短轨迹中正确估计长期价值,在 2 万步的训练预算内没有学好。 - 高噪声数据上 CQL 与朴素 DQN 的相对优劣并不稳定。 在给
env.action_space设置种子之前,我们的两次运行(数据集中的随机动作不同)分别得到 DQN 118.8 / CQL 138.0 和 DQN 397.1 / CQL 134.8,而 BC 三次都在 470 分以上。也就是说,在这个玩具设置下“BC 远好于两种 Q-learning 方法”是稳定的结论,“CQL 优于朴素 DQN”则不是——要得出可靠结论需要多个种子、多份数据集。 - 这提醒我们:离线 RL 方法并不总是优于简单的 BC,结果高度依赖数据集的构成。Kumar 等人(2022, When Should We Prefer Offline Reinforcement Learning Over Behavioral Cloning?)对此有系统的讨论。
代码:Pendulum 上的连续动作 CQL
离散动作的 CQL 可以直接对所有动作求
再把
实验设置:
- 数据集:在线 SAC 训练 6000 步得到的完整回放缓冲区(前 1000 步为均匀随机动作),共 30 个回合,回合回报从约 −1800 到接近 0,平均约 −820。这类“从差到好混在一起”的数据类似 D4RL 的 medium-replay。
- 三种方法,各用这份固定数据训练 2 万步、不与环境交互:行为克隆 BC、朴素离线 SAC(不加任何约束)、CQL(
, )。 - 在真实环境中用确定性策略评估 10 个回合;另外记录训练结束时 Q 网络对数据集中
的平均估计。
py
"""连续动作离线强化学习:Pendulum-v1 上的 BC、朴素离线 SAC 与 CQL(H)(基于 SAC)。
数据集:在线 SAC 训练前 DATA_STEPS 步的完整回放缓冲区(前 1000 步为随机动作),质量从随机到较好混杂,
类似 D4RL 的 "medium-replay"。之后三种方法只用这份固定数据训练,不再与环境交互。
用法:python cql_pendulum.py [种子];DQN_QUICK=1 只跑很短的版本。"""
import copy
import os
import sys
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
QUICK = os.environ.get("DQN_QUICK") == "1"
torch.set_num_threads(1)
DATA_STEPS = 2_000 if QUICK else 6_000
OFFLINE_STEPS = 2_000 if QUICK else 20_000
GAMMA, TAU, LR, BATCH, A_MAX = 0.99, 0.005, 3e-4, 256, 2.0
CQL_ALPHA, CQL_N = 5.0, 10 # CQL 正则系数、每个状态采样的动作数
def mlp(i, o, h=256):
return nn.Sequential(nn.Linear(i, h), nn.ReLU(), nn.Linear(h, h), nn.ReLU(), nn.Linear(h, o))
class Actor(nn.Module):
def __init__(self):
super().__init__(); self.net = mlp(3, 2)
def forward(self, s):
mu, log_std = self.net(s).chunk(2, -1)
d = Normal(mu, log_std.clamp(-20, 2).exp()); u = d.rsample(); a = torch.tanh(u)
logp = d.log_prob(u).sum(-1) - torch.log(1 - a.pow(2) + 1e-6).sum(-1)
return a, logp, torch.tanh(mu)
class SAC:
def __init__(self):
self.actor, self.q1, self.q2 = Actor(), mlp(4, 1), mlp(4, 1)
self.q1_t, self.q2_t = copy.deepcopy(self.q1), copy.deepcopy(self.q2)
self.opt_a = torch.optim.Adam(self.actor.parameters(), lr=LR)
self.opt_q = torch.optim.Adam(list(self.q1.parameters()) + list(self.q2.parameters()), lr=LR)
self.log_alpha = torch.zeros(1, requires_grad=True); self.opt_al = torch.optim.Adam([self.log_alpha], lr=LR)
def update(self, s, a, r, s2, cql=0.0):
alpha = self.log_alpha.exp().detach()
with torch.no_grad():
a2, lp2, _ = self.actor(s2); sa2 = torch.cat([s2, a2], 1)
y = r + GAMMA * (torch.min(self.q1_t(sa2), self.q2_t(sa2)) - alpha * lp2[:, None])
sa = torch.cat([s, a], 1); q1, q2 = self.q1(sa), self.q2(sa)
lq = F.mse_loss(q1, y) + F.mse_loss(q2, y)
if cql > 0:
# CQL(H):压低 logsumexp_a Q(s,a)(用均匀动作 + 当前策略动作做重要性采样估计),抬高数据中的 Q(s,a)
B = len(s); s_rep = s.repeat_interleave(CQL_N, 0)
a_rand = torch.rand(B * CQL_N, 1) * 2 - 1
with torch.no_grad():
a_pi, lp_pi, _ = self.actor(s_rep)
for q_net, q_data in ((self.q1, q1), (self.q2, q2)):
q_rand = q_net(torch.cat([s_rep, a_rand], 1)).view(B, CQL_N) - np.log(0.5) # 均匀分布密度 1/2
q_pi = q_net(torch.cat([s_rep, a_pi], 1)).view(B, CQL_N) - lp_pi.view(B, CQL_N)
lse = torch.logsumexp(torch.cat([q_rand, q_pi], 1), 1) - np.log(2 * CQL_N)
lq = lq + cql * (lse.mean() - q_data.mean())
self.opt_q.zero_grad(); lq.backward(); self.opt_q.step()
an, lp, _ = self.actor(s); san = torch.cat([s, an], 1)
la = (alpha * lp - torch.min(self.q1(san), self.q2(san)).squeeze(1)).mean()
self.opt_a.zero_grad(); la.backward(); self.opt_a.step()
lal = -(self.log_alpha * (lp.detach() - 1.0)).mean() # 目标熵 = -|A| = -1
self.opt_al.zero_grad(); lal.backward(); self.opt_al.step()
with torch.no_grad():
for n, t in ((self.q1, self.q1_t), (self.q2, self.q2_t)):
for p, tp in zip(n.parameters(), t.parameters()):
tp.mul_(1 - TAU).add_(TAU * p)
def evaluate(act, n=10):
env, tot = gym.make("Pendulum-v1"), []
for k in range(n):
o, _ = env.reset(seed=10_000 + k); done, G = False, 0.0
while not done:
with torch.no_grad():
a = act(torch.as_tensor(o, dtype=torch.float32))
o, r, te, tr, _ = env.step(a.numpy() * A_MAX); G += r; done = te or tr
tot.append(G)
return float(np.mean(tot))
def collect(seed):
"""在线 SAC 训练 DATA_STEPS 步,把整个回放缓冲区作为离线数据集。"""
rng = np.random.default_rng(seed); env = gym.make("Pendulum-v1"); agent = SAC()
D = {k: torch.zeros(DATA_STEPS, d) for k, d in (("s", 3), ("a", 1), ("r", 1), ("s2", 3))}
o, _ = env.reset(seed=seed); rets, G = [], 0.0
for t in range(DATA_STEPS):
if t < 1000:
a = rng.uniform(-1, 1, 1).astype(np.float32)
else:
with torch.no_grad():
a = agent.actor(torch.as_tensor(o, dtype=torch.float32))[0].numpy()
o2, r, te, tr, _ = env.step(a * A_MAX); G += r
for k, v in (("s", o), ("a", a), ("r", [r]), ("s2", o2)):
D[k][t] = torch.as_tensor(np.asarray(v, np.float32))
o = o2
if te or tr:
rets.append(G); G = 0.0; o, _ = env.reset()
if t >= 1000:
j = torch.as_tensor(rng.integers(0, t + 1, BATCH))
agent.update(*(D[k][j] for k in ("s", "a", "r", "s2")))
return D, rets
def train_offline(D, method, seed):
torch.manual_seed(seed); rng = np.random.default_rng(seed); N = len(D["s"])
if method == "bc":
pi = mlp(3, 1); opt = torch.optim.Adam(pi.parameters(), lr=1e-3)
for _ in range(OFFLINE_STEPS):
j = torch.as_tensor(rng.integers(0, N, BATCH))
loss = F.mse_loss(torch.tanh(pi(D["s"][j])), D["a"][j]); opt.zero_grad(); loss.backward(); opt.step()
return evaluate(lambda o: torch.tanh(pi(o)))
agent = SAC()
for _ in range(OFFLINE_STEPS):
j = torch.as_tensor(rng.integers(0, N, BATCH))
agent.update(*(D[k][j] for k in ("s", "a", "r", "s2")), cql=CQL_ALPHA if method == "cql" else 0.0)
with torch.no_grad():
q_data = torch.min(agent.q1(torch.cat([D["s"], D["a"]], 1)), agent.q2(torch.cat([D["s"], D["a"]], 1))).mean().item()
return evaluate(lambda o: agent.actor(o)[2]), q_data
if __name__ == "__main__":
seeds = [int(sys.argv[1])] if len(sys.argv) > 1 else ([0] if QUICK else [0, 1])
for sd in seeds:
torch.manual_seed(sd)
D, rets = collect(sd)
print(f"种子{sd}: 数据集 {len(D['s'])} 条转移,{len(rets)} 个回合,回合回报 最差 {min(rets):.0f} / 平均 {np.mean(rets):.0f} / 最好 {max(rets):.0f}", flush=True)
print(f" BC 评估回报 {train_offline(D, 'bc', sd):8.1f}", flush=True)
for m in ("sac", "cql"):
score, q = train_offline(D, m, sd)
name = "朴素离线 SAC" if m == "sac" else f"CQL(α={CQL_ALPHA:g})"
print(f" {name:11s} 评估回报 {score:8.1f} 数据上的平均 Q 估计 {q:9.1f}", flush=True)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
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
实际运行输出(两个种子,每个种子的数据集也不同):
text
种子0: 数据集 6000 条转移,30 个回合,回合回报 最差 -1801 / 平均 -823 / 最好 -2
BC 评估回报 -892.9
朴素离线 SAC 评估回报 -108.5 数据上的平均 Q 估计 -85.1
CQL(α=5) 评估回报 -125.5 数据上的平均 Q 估计 -145.0
种子1: 数据集 6000 条转移,30 个回合,回合回报 最差 -1721 / 平均 -812 / 最好 -1
BC 评估回报 -821.9
朴素离线 SAC 评估回报 -108.9 数据上的平均 Q 估计 -84.9
CQL(α=5) 评估回报 -124.8 数据上的平均 Q 估计 -142.81
2
3
4
5
6
7
8
2
3
4
5
6
7
8
结果解读(两个种子,结论只针对这个设置):
- BC 只学到了数据的“平均水平”(−893 / −822,和数据集平均回报 −820 左右相当)。数据里大量是随机动作和早期的差策略,模仿它们自然得不到好策略——这正是离线 RL 想要超越 BC 的场景。
- 在这份数据上,朴素离线 SAC 反而最好(约 −109),CQL 稍差(约 −125),两者都远好于数据平均。原因是这份数据的覆盖很广:前 1000 步的均匀随机动作让每个状态附近的各种动作都有样本,外推误差没有严重到毁掉朴素方法。这与上面 CartPole 纯专家数据(覆盖极窄)上朴素 DQN 崩溃形成对照:外推误差有多严重,主要取决于数据覆盖。
- CQL 确实更保守:它对数据中
的平均 Q 估计约为 −144,明显低于朴素 SAC 的约 −85。在覆盖充分的数据上,这种额外的保守性没有带来收益,反而让策略略差; 可能偏大,我们没有在这里搜索 (见练习 6)。 - 我们没有测量真实的
,所以不能断言哪一个 Q 估计“更准”,只能说 CQL 的估计更低。
数据集与基准:D4RL / Minari
- D4RL(Fu et al., 2020):离线 RL 最常用的基准,包含 MuJoCo 运动(random / medium / medium-replay / medium-expert / expert 等不同质量的数据)、AntMaze 迷宫、Adroit 灵巧手、厨房任务等。
- Minari:Farama 基金会维护的离线 RL 数据集标准库,与 Gymnasium 配套,D4RL 的数据集已迁移到 Minari。
- RL Unplugged(Gulcehre et al., 2020):DeepMind 发布的离线 RL 基准,包括 Atari 和控制任务。
实践建议与常见误区
- 先跑 BC 基线:在高质量数据上,BC 往往已经很强。
- 超参数选择是离线 RL 的隐患:真实场景中不能用在线评估来调参,需要离线策略评估(OPE)方法。
- CQL 的
很关键:太大会过于保守(退化为 BC),太小会高估。 - 误区:把离线 RL 当成“更好的 BC”:两者解决的问题不同,离线 RL 的优势体现在次优数据和需要拼接的任务上。
- 误区:忽略数据覆盖:如果数据从未覆盖好的区域,任何离线算法都无法学到好的策略。
小结
- 离线 RL 只用固定数据集学习,核心挑战是分布外动作的外推误差。
- 策略约束(BCQ、TD3+BC)让策略靠近数据;保守估计(CQL)压低分布外动作的 Q 值;IQL 完全避免查询分布外动作。
- Decision Transformer 把 RL 看作条件序列建模。
- 我们的玩具实验清楚地展示了朴素离线 DQN 在窄分布数据上的失败,也展示了 BC 在某些数据上可以胜过离线 RL。
- 连续动作的 Pendulum 实验则显示了另一面:数据覆盖充分时,朴素离线 SAC 已经很好(约 −109),CQL 更保守但没有更好(约 −125),而 BC 只能达到数据平均水平。
练习题
- 在纯专家数据上,打印朴素离线 DQN 对数据中动作和另一个动作的 Q 值,验证分布外动作被高估。
- 把 CQL 的
改为 0.1、5、20,在两个数据集上比较结果。 - 增加高噪声数据集上的训练步数(例如 10 万步),CQL 和 DQN 的表现会改善吗?
- 实现离散动作版本的 IQL,与 CQL 比较。
- 构造一个 BC 会失败而离线 RL 能成功的数据集(提示:混合一个好策略和一个差策略的数据,且差策略的数据更多)。
- 在
cql_pendulum.py中把 改为 0.5、1、2,观察评估回报和“数据上的平均 Q 估计”如何变化。 - 去掉
cql_pendulum.py数据集中前 1000 步的随机动作(只保留 SAC 策略产生的数据),朴素离线 SAC 和 CQL 的差距会怎样变化?
参考文献
- Lange, S., Gabel, T., & Riedmiller, M. (2012). Batch Reinforcement Learning. In Reinforcement Learning: State-of-the-Art, Springer.
- Levine, S., Kumar, A., Tucker, G., & Fu, J. (2020). Offline Reinforcement Learning: Tutorial, Review, and Perspectives on Open Problems. arXiv:2005.01643
- Fujimoto, S., Meger, D., & Precup, D. (2019). Off-Policy Deep Reinforcement Learning without Exploration. ICML. arXiv:1812.02900(BCQ)
- Kumar, A., Zhou, A., Tucker, G., & Levine, S. (2020). Conservative Q-Learning for Offline Reinforcement Learning. NeurIPS. arXiv:2006.04779
- Fu, J., Kumar, A., Nachum, O., Tucker, G., & Levine, S. (2020). D4RL: Datasets for Deep Data-Driven Reinforcement Learning. arXiv:2004.07219
- Fujimoto, S., & Gu, S. S. (2021). A Minimalist Approach to Offline Reinforcement Learning. NeurIPS. arXiv:2106.06860(TD3+BC)
- Chen, L. et al. (2021). Decision Transformer: Reinforcement Learning via Sequence Modeling. NeurIPS. arXiv:2106.01345
- Kostrikov, I., Nair, A., & Levine, S. (2022). Offline Reinforcement Learning with Implicit Q-Learning. ICLR. arXiv:2110.06169
- Kumar, A., Hong, J., Singh, A., & Levine, S. (2022). When Should We Prefer Offline Reinforcement Learning Over Behavioral Cloning? ICLR. arXiv:2204.05618