File size: 13,059 Bytes
c3d6fc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# -*- coding: utf-8 -*-
"""
把训练好的智能体 + 代码 + 视频 + 评估分数打包成"可以直接上传 Hub"的文件夹。
================================================================
对应 PPT 第 28 页学习目标第 4 条:
  "能够将训练好的智能体和代码上传到 Hub,并附上一个漂亮的视频回放和评估分数"

产物 hub_package/ 结构:
  README.md                    自动填好分数 + 内嵌视频的模型卡
  metrics.json                 三/四个智能体的评估分数汇总
  requirements.txt
  models/qtable_*.pkl          训练出的模型文件(Q 表 + 超参 + 指标)
  videos/replay_*.mp4          回放视频(含总回放 replay_value_based_all.mp4)
  eval_results/eval_*.json     评估分数(逐局明细)
  logs/<run>/train_console.log 训练日志
  logs/<run>/train_curve.png   训练曲线
  assets/qtable_policy_*.png   学到的策略可视化
  code/                        训练 + 评估 + 可视化 + 上传脚本(可复现)
  run_all.ps1                  一键复现脚本

用法:
  D:\\study\\robot\\pyenv\\DRL\\python.exe build_hub_package.py
  D:\\study\\robot\\pyenv\\DRL\\python.exe build_hub_package.py --repo-id 你的用户名/value-based-qlearning
"""

import argparse
import json
import os
import shutil
from datetime import datetime

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 要打包的智能体(按 PPT 顺序)
AGENTS = [
    dict(run="frozenlake4x4", title="FrozenLake-v1 · 4x4 不滑",
         desc="PPT 第一部分主实验:确定性冰面,从零手写 Q 表,10,000 回合"),
    dict(run="frozenlake4x4_slippery", title="FrozenLake-v1 · 4x4 滑冰版",
         desc="对照实验:只改 is_slippery=True,冰面会打滑,20,000 回合"),
    dict(run="frozenlake8x8", title="FrozenLake-v1 · 8x8",
         desc="进阶实验:地图 8x8 + 拉长探索期,60,000 回合"),
    dict(run="taxi", title="Taxi-v3",
         desc="PPT 第二部分主实验:500 状态 × 6 动作,25,000 回合,官方 100 个评估种子"),
]

CODE_FILES = [
    "train_qlearning.py",
    "evaluate_and_record.py",
    "visualize_qtable.py",
    "make_combined_video.py",
    "build_hub_package.py",
    "upload_to_hub.py",
    "run_all.ps1",
    "01_实践3说明.md",
]

REQUIREMENTS = """gymnasium>=1.0
numpy>=1.24
imageio>=2.31
imageio-ffmpeg>=0.4
Pillow>=9.0
matplotlib>=3.7
tqdm>=4.65
"""


def read_json(path):
    if not os.path.exists(path):
        return None
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def build_readme(repo_id=None, scores=None, now=None):
    raw = f"https://huggingface.co/{repo_id}/resolve/main/" if repo_id else ""
    link = (lambda p: raw + p) if repo_id else (lambda p: p)

    lines = []
    lines.append("# Value-based 实践 · Q-Learning(FrozenLake-v1 + Taxi-v3)")
    lines.append("")
    lines.append("> 《第3周-深度强化学习原理实践》PPT 第 27–40 页 value-based 实践产物:"
                 "**从零手写 Q-Learning**(Q 表 + epsilon-greedy + Bellman 更新),"
                 "训练好的智能体 + 训练代码 + 回放视频 + 评估分数。")
    lines.append("")
    lines.append("## 1. 评估分数")
    lines.append("")
    lines.append("| 智能体 | 环境 | 平均回报(100 局) | 成功率 | 平均步数 | 训练回合 |")
    lines.append("|---|---|---|---|---|---|")
    for agent in AGENTS:
        s = (scores or {}).get(agent["run"])
        if s:
            lines.append(
                f"| {agent['title']} | `{s['env_id']}` | **{s['mean_reward']:.2f} ± "
                f"{s['std_reward']:.2f}** | {s['success_rate'] * 100:.0f}% | "
                f"{s['mean_steps']:.1f} | {s.get('n_training_episodes', '—')} |"
            )
        else:
            lines.append(f"| {agent['title']} | — | 待运行 | — | — | — |")
    lines.append("")
    lines.append("> 评估方式:独立环境、贪心策略 `argmax_a Q(s,a)`、每局最多 99 步,跑 100 局;"
                 "FrozenLake 的回报是「是否到达目标」(0/1),Taxi 的回报是"
                 "「每步 −1,成功送达 +20」。")
    lines.append("")

    lines.append("## 2. 回放视频")
    lines.append("")
    if os.path.exists(os.path.join(BASE_DIR, "videos", "replay_value_based_all.mp4")):
        lines.append("**总回放(三个智能体合集)**")
        lines.append("")
        lines.append(f'<video controls width="100%" src="{link("videos/replay_value_based_all.mp4")}"></video>')
        lines.append("")
    for agent in AGENTS:
        video = f"videos/replay_{agent['run']}.mp4"
        if os.path.exists(os.path.join(BASE_DIR, video)):
            lines.append(f"**{agent['title']}**")
            lines.append("")
            lines.append(f'<video controls width="100%" src="{link(video)}"></video>')
            lines.append("")

    lines.append("## 3. 学到的策略(Q 表可视化)")
    lines.append("")
    for agent in AGENTS:
        fig = f"assets/qtable_policy_{agent['run']}.png"
        if os.path.exists(os.path.join(BASE_DIR, fig)):
            lines.append(f"![{agent['title']}]({link(fig)})")
            lines.append("")

    lines.append("## 4. 训练曲线")
    lines.append("")
    for agent in AGENTS:
        fig = f"logs/{agent['run']}/train_curve.png"
        if os.path.exists(os.path.join(BASE_DIR, fig)):
            lines.append(f"![{agent['title']} 训练曲线]({link(fig)})")
            lines.append("")

    lines.append("## 5. 怎么复现")
    lines.append("")
    lines.append("```powershell")
    lines.append("# 环境(本机已有):conda activate DRL")
    lines.append("conda activate DRL")
    lines.append("")
    lines.append("# 训练(每个实验 1~30 秒)")
    lines.append("python train_qlearning.py --env frozenlake4x4")
    lines.append("python train_qlearning.py --env frozenlake4x4_slippery")
    lines.append("python train_qlearning.py --env frozenlake8x8")
    lines.append("python train_qlearning.py --env taxi")
    lines.append("")
    lines.append("# 评估 + 录视频(生成 eval_results/*.json 和 videos/*.mp4)")
    lines.append("python evaluate_and_record.py --model models/qtable_frozenlake4x4.pkl")
    lines.append("python evaluate_and_record.py --model models/qtable_frozenlake4x4_slippery.pkl --video-episodes 5 --slowmo 4")
    lines.append("python evaluate_and_record.py --model models/qtable_frozenlake8x8.pkl")
    lines.append("python evaluate_and_record.py --model models/qtable_taxi.pkl")
    lines.append("")
    lines.append("# Q 表可视化 + 总回放 + 打包")
    lines.append("python visualize_qtable.py --all")
    lines.append("python make_combined_video.py")
    lines.append("python build_hub_package.py")
    lines.append("```")
    lines.append("")
    lines.append("## 6. 文件说明")
    lines.append("")
    lines.append("- `models/qtable_*.pkl`:模型文件。pickle 格式,内含 Q 表、超参数、训练种子和评估指标;")
    lines.append("  `evaluate_and_record.py` 可以直接加载(不需要重新训练)。")
    lines.append("- `videos/*.mp4`:回放,每帧标注步数/动作/奖励/Q 值条形图,片头片尾是标题卡和分数卡。")
    lines.append("- `eval_results/*.json`:评估分数 + 逐局明细(起点种子、回报、步数、是否成功)。")
    lines.append("- `logs/<run>/train_console.log`:完整训练日志(环境自检、超参数、每 N 回合进度、每次评估)。")
    lines.append("- `code/`:全部代码(训练 / 评估录像 / 可视化 / 打包 / 上传 / 一键脚本)。")
    lines.append("")
    lines.append("## 7. 对应 PPT 页码")
    lines.append("")
    lines.append("| PPT 页 | 内容 | 对应实现 |")
    lines.append("|---|---|---|")
    lines.append("| 29 | Q 表 / Q 函数 / 最优策略 | `train_qlearning.py` 的 `initialize_q_table` / `greedy_policy` |")
    lines.append("| 31–32 | FrozenLake 观测/动作/奖励 | 训练日志里的「环境自检」 |")
    lines.append("| 33 | Q 表初始化 + 贪心策略 | `initialize_q_table` / `greedy_policy` |")
    lines.append("| 34 | epsilon-greedy + 超参数 | `epsilon_greedy_policy` + `EXPERIMENTS` 配置 |")
    lines.append("| 35 | 训练循环(Bellman 更新) | `train_qlearning.py::train` 里的 `Q[s][a] += lr*(...)` |")
    lines.append("| 36–37 | 训练、Q 表可视化、评估、保存模型 | 训练日志 / `visualize_qtable.py` / `evaluate_and_record.py` / `models/*.pkl` |")
    lines.append("| 38–40 | Taxi-v3(500 状态 × 6 动作) | `--env taxi`(含 PPT 的 100 个官方评估种子) |")
    lines.append("| 28 | 上传 Hub + 视频 + 分数 | `build_hub_package.py` + `upload_to_hub.py` + README |")
    lines.append("")
    lines.append(f"- 生成时间:{now:%Y-%m-%d %H:%M}")
    lines.append("")
    return "\n".join(lines)


def copy_file(src, dst):
    os.makedirs(os.path.dirname(dst), exist_ok=True)
    shutil.copy2(src, dst)


def main():
    parser = argparse.ArgumentParser(description="打包成可上传 Hub 的文件夹")
    parser.add_argument("--repo-id", type=str, default=None,
                        help="你的 HF 用户名/仓库名,例如 zhangsan/value-based-qlearning")
    parser.add_argument("--out", type=str, default=os.path.join(BASE_DIR, "hub_package"))
    args = parser.parse_args()

    out = args.out
    if os.path.exists(out):
        shutil.rmtree(out)
    os.makedirs(out)
    now = datetime.now()

    # 1) 读取所有评估分数
    scores = {}
    for agent in AGENTS:
        run = agent["run"]
        s = read_json(os.path.join(BASE_DIR, "eval_results", f"eval_{run}.json"))
        if s is None:
            print(f"[警告] 缺少 eval_results/eval_{run}.json,"
                  f"请先跑 evaluate_and_record.py --model models/qtable_{run}.pkl")
            continue
        scores[run] = s
    metrics = {}
    for run, s in scores.items():
        metrics[run] = {k: s[k] for k in
                        ("env_id", "n_episodes", "mean_reward", "std_reward",
                         "success_rate", "mean_steps", "qtable_shape",
                         "qtable_nonzero", "evaluated_at")}

    # 2) 拷模型
    for agent in AGENTS:
        src = os.path.join(BASE_DIR, "models", f"qtable_{agent['run']}.pkl")
        if os.path.exists(src):
            copy_file(src, os.path.join(out, "models", os.path.basename(src)))
        else:
            print(f"[警告] 缺少模型 {src}")

    # 3) 拷视频(含总回放)
    video_dir = os.path.join(BASE_DIR, "videos")
    if os.path.isdir(video_dir):
        for name in sorted(os.listdir(video_dir)):
            if name.endswith(".mp4"):
                copy_file(os.path.join(video_dir, name),
                          os.path.join(out, "videos", name))

    # 4) 拷评估 json
    for agent in AGENTS:
        src = os.path.join(BASE_DIR, "eval_results", f"eval_{agent['run']}.json")
        if os.path.exists(src):
            copy_file(src, os.path.join(out, "eval_results", os.path.basename(src)))

    # 5) 拷日志 + 训练曲线
    for agent in AGENTS:
        run_dir = os.path.join(BASE_DIR, "logs", agent["run"])
        for name in ("train_console.log", "train_curve.png", "progress.csv"):
            src = os.path.join(run_dir, name)
            if os.path.exists(src):
                copy_file(src, os.path.join(out, "logs", agent["run"], name))

    # 6) 拷 Q 表可视化
    for agent in AGENTS:
        src = os.path.join(BASE_DIR, "assets", f"qtable_policy_{agent['run']}.png")
        if os.path.exists(src):
            copy_file(src, os.path.join(out, "assets", os.path.basename(src)))

    # 7) 拷代码
    for name in CODE_FILES:
        src = os.path.join(BASE_DIR, name)
        if os.path.exists(src):
            copy_file(src, os.path.join(out, "code", name))

    # 8) 写 README / metrics / requirements
    with open(os.path.join(out, "README.md"), "w", encoding="utf-8") as f:
        f.write(build_readme(repo_id=args.repo_id, scores=scores, now=now))
    with open(os.path.join(out, "metrics.json"), "w", encoding="utf-8") as f:
        json.dump(metrics, f, ensure_ascii=False, indent=2)
    with open(os.path.join(out, "requirements.txt"), "w", encoding="utf-8") as f:
        f.write(REQUIREMENTS)

    n_files = sum(len(files) for _, _, files in os.walk(out))
    print(f"[完成] hub_package/ 共 {n_files} 个文件")
    for root, dirs, files in os.walk(out):
        rel = os.path.relpath(root, out)
        for name in sorted(files):
            path = os.path.join(root, name)
            print(f"  {os.path.join(rel, name) if rel != '.' else name}"
                  f"  ({os.path.getsize(path) / 1024:.0f} KB)")
    print("\n下一步上传:")
    print("  hf auth login                       # 粘贴你的 write token")
    print("  python upload_to_hub.py --repo-id 你的用户名/value-based-qlearning")


if __name__ == "__main__":
    main()