diff --git a/404.html b/404.html index 1d85e8b..37684e1 100644 --- a/404.html +++ b/404.html @@ -21,7 +21,13 @@
Musicaldg - +

404

页面未找到

回到首页 ↗
diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000..54ce126 --- /dev/null +++ b/about/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + About me · Musicaldg + + + + + + + + +
+
+ Musicaldg + +
+

About me

关于 Musicaldg。

机器学习笔记与个人主页。更多内容稍后补充。

+

目前主要整理模仿学习、主动学习等相关笔记,源码与页面托管在 GitHub。

+
© 2026 MusicaldgRSS
+
+ + diff --git a/assets/geometry.svg b/assets/geometry.svg index 70d95bd..d9d6ba0 100644 --- a/assets/geometry.svg +++ b/assets/geometry.svg @@ -1,9 +1,5 @@ - diff --git a/assets/style.css b/assets/style.css index a89cce4..e99daa8 100644 --- a/assets/style.css +++ b/assets/style.css @@ -44,11 +44,11 @@ a { color: inherit; text-decoration: none; } a:hover { color: var(--accent-200); } a:focus-visible { outline: 2px solid var(--accent-100); outline-offset: 4px; } ::selection { background: var(--primary-100); color: var(--text-100); } -.wrap { max-width: 860px; min-height: 100svh; margin: auto; padding: 0 48px; display: flex; flex-direction: column; } +.wrap { max-width: 860px; min-height: 100svh; margin: auto; padding: 0 48px; display: flex; flex-direction: column; overflow: visible; } .header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 36px 0 22px; } .brand { font: 22px/1 var(--serif); letter-spacing: -.03em; color: var(--text-100); } .brand:hover { color: var(--accent-200); } -nav { display: flex; align-items: center; gap: 8px; font-size: 13px; } +nav { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 4px; font-size: 13px; } nav a { color: var(--text-200); padding: 6px 12px; border-radius: 999px; } nav a:hover { color: var(--accent-200); background: color-mix(in srgb, var(--primary-100) 70%, transparent); } nav a[aria-current=page] { color: var(--accent-200); background: var(--primary-100); } @@ -57,35 +57,130 @@ main { flex: 1; } display: flex; justify-content: center; align-items: center; - padding: 18px 0 28px; + padding: 36px 0 24px; + overflow: visible; } .geometry-glow { display: grid; place-items: center; - width: min(250px, 70vw); + width: min(340px, 84vw); aspect-ratio: 1; border-radius: 50%; - background: radial-gradient(circle, color-mix(in srgb, var(--accent-100) 42%, var(--primary-100)) 0%, color-mix(in srgb, var(--primary-100) 45%, transparent) 52%, transparent 72%); + overflow: visible; + perspective: 760px; + perspective-origin: 50% 50%; + transform-style: preserve-3d; + background: radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--accent-100) 42%, var(--primary-100)) 0%, color-mix(in srgb, var(--primary-100) 45%, transparent) 52%, transparent 72%); animation: geometry-glow 6s ease-in-out infinite; } +.geometry-stage { + width: 100%; + height: 100%; + display: grid; + place-items: center; + transform-style: preserve-3d; + transform-origin: 50% 52%; + animation: geometry-orbit 32s linear infinite; +} .geometry-mark { - width: 86%; + width: 58%; height: auto; display: block; + transform-style: preserve-3d; + transform: rotateX(28deg) translateZ(8px); + filter: drop-shadow(0 18px 20px color-mix(in srgb, var(--accent-200) 14%, transparent)); } @keyframes geometry-glow { - 0%, 100% { transform: scale(1); filter: saturate(1); } - 50% { transform: scale(1.035); filter: saturate(1.12); } + 0%, 100% { filter: saturate(1) brightness(1); } + 50% { filter: saturate(1.14) brightness(1.03); } +} +@keyframes geometry-orbit { + from { transform: rotate3d(0.55, 1, 0.12, 0deg); } + to { transform: rotate3d(0.55, 1, 0.12, 360deg); } } .post-list { display: grid; gap: 18px; padding-bottom: 64px; } .post-list:empty { padding: 0; } +.dash-panel { + max-width: 640px; + margin: 0 auto 28px; +} +.dash-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin-bottom: 12px; +} +.dash-head h2 { + margin: 0; + font: normal 18px/1.4 var(--serif); + letter-spacing: -.01em; + color: var(--text-100); +} +.dash-more { + font-size: 13px; + color: var(--accent-200); + white-space: nowrap; +} +.dash-empty { + margin: 0; + color: var(--text-200); + font-size: 14px; +} +.news-list { + list-style: none; + margin: 0; + padding: 14px 16px; + display: grid; + gap: 10px; + border: 1px solid var(--bg-300); + border-radius: 16px; + background: color-mix(in srgb, var(--bg-100) 88%, var(--primary-100)); + box-shadow: 0 8px 22px color-mix(in srgb, var(--primary-300) 4%, transparent); +} +.news-item { + display: grid; + grid-template-columns: 7.2rem 1fr; + gap: 12px; + align-items: start; +} +.news-item time { + font-size: 12px; + color: var(--text-200); + font-variant-numeric: tabular-nums; + padding-top: 2px; +} +.news-text { + color: var(--text-100); + font-size: 14px; + line-height: 1.65; +} +.news-text a { color: var(--accent-200); } .home-feed { display: grid; gap: 12px; max-width: 560px; - margin: 0 auto; - padding-bottom: 56px; + margin: 0; + padding-bottom: 40px; +} +.blog-feed { + display: grid; + gap: 18px; + padding-bottom: 64px; +} +.blog-section-title { + margin: 10px 0 0; + font: normal 18px/1.4 var(--serif); + color: var(--text-100); } +.page-shell { max-width: 680px; padding-bottom: 64px; } +.page-lede { + margin: 12px 0 0; + color: var(--text-200); + font-size: 15px; + line-height: 1.7; +} +.page-prose { margin-top: 8px; } .series-folder { border: 1px solid var(--bg-300); border-radius: 16px; @@ -230,7 +325,8 @@ main { flex: 1; } border-radius: 2px; background: color-mix(in srgb, var(--accent-100) 70%, var(--primary-200)); } -.series-folder-posts .post-card-compact { +.series-folder-posts .post-card-compact, +.series-folder-posts .post-card-roomy { margin-left: 8px; display: grid; grid-template-columns: auto 1fr; @@ -239,14 +335,72 @@ main { flex: 1; } background: color-mix(in srgb, var(--bg-100) 92%, var(--primary-100)); animation: series-card-in .34s ease both; } -.series-folder[open] .series-folder-posts .post-card-compact:nth-child(1) { animation-delay: .04s; } -.series-folder[open] .series-folder-posts .post-card-compact:nth-child(2) { animation-delay: .1s; } -.series-folder[open] .series-folder-posts .post-card-compact:nth-child(3) { animation-delay: .16s; } -.series-folder[open] .series-folder-posts .post-card-compact:nth-child(4) { animation-delay: .22s; } +.series-folder[open] .series-folder-posts .post-card-compact:nth-child(1), +.series-folder[open] .series-folder-posts .post-card-roomy:nth-child(1) { animation-delay: .04s; } +.series-folder[open] .series-folder-posts .post-card-compact:nth-child(2), +.series-folder[open] .series-folder-posts .post-card-roomy:nth-child(2) { animation-delay: .1s; } +.series-folder[open] .series-folder-posts .post-card-compact:nth-child(3), +.series-folder[open] .series-folder-posts .post-card-roomy:nth-child(3) { animation-delay: .16s; } +.series-folder[open] .series-folder-posts .post-card-compact:nth-child(4), +.series-folder[open] .series-folder-posts .post-card-roomy:nth-child(4) { animation-delay: .22s; } @keyframes series-card-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +.series-folder-roomy { + max-width: none; +} +.series-folder-roomy .series-folder-summary { + padding: 20px 22px; +} +.series-folder-roomy .series-folder-title { + font-size: 26px; +} +.series-folder-roomy .series-folder-blurb { + font-size: 14px; + padding: 0 22px 14px; +} +.series-folder-roomy .series-folder-posts { + gap: 12px; + padding: 0 16px 16px; +} +.series-folder-roomy .series-folder-posts::before { + left: 34px; + top: 10px; + bottom: 28px; +} +.series-folder-roomy .post-card-roomy { + margin-left: 10px; + padding: 20px 22px 18px; + border-radius: 14px; + border-top-width: 3px; +} +.series-folder-roomy .series-index { + width: 32px; + height: 32px; + margin-top: 4px; + font-size: 13px; +} +.series-folder-roomy .series-folder-link { + margin: 0 22px 20px; + font-size: 14px; +} +.post-card-roomy h3 { + font: normal 24px/1.4 var(--serif); + margin: 10px 0 8px; + letter-spacing: -.02em; + color: var(--text-100); +} +.post-card-roomy p { + margin: 0; + color: var(--text-200); + font-size: 14px; + line-height: 1.7; +} +.post-card-roomy .card-open { + margin-top: 16px; +} +.roomy-copy { min-width: 0; } .series-index { display: grid; place-items: center; @@ -283,12 +437,16 @@ main { flex: 1; } max-width: 640px; padding-bottom: 64px; } -.series-page-list .post-card-compact { +.series-page-list .post-card-compact, +.series-page-list .post-card-roomy { display: grid; grid-template-columns: auto 1fr; gap: 14px; align-items: start; } +.series-page-list .post-card-roomy { + padding: 22px 24px 20px; +} .post-card { display: block; padding: 28px 32px 26px; @@ -426,7 +584,13 @@ main { flex: 1; } .post-card h3 { font-size: 22px; } .post-card-compact { padding: 12px 16px; } .post-card-compact h3 { font-size: 18px; } - .geometry { padding: 10px 0 20px; } + .geometry { padding: 18px 0 12px; } + .geometry-mark { width: 72%; } + .news-item { grid-template-columns: 1fr; gap: 2px; } + .news-item time { padding-top: 0; } + .series-folder-roomy .series-folder-title { font-size: 22px; } + .post-card-roomy h3 { font-size: 20px; } + .series-folder-roomy .post-card-roomy { padding: 16px 16px 14px; } .article-page .wrap { padding: 0 16px; } .article-layout { padding-top: 16px; } .article { padding: 26px 18px 32px; border-radius: 14px; } @@ -444,7 +608,8 @@ main { flex: 1; } .post-card { transition: none; } .post-card:hover { transform: none; } .post-card-compact .card-summary { transition: none; } - .geometry-glow { animation: none; } + .geometry-glow, + .geometry-stage { animation: none; } .series-folder, .series-folder-body, .series-folder-stack, diff --git a/blog/active-learning/index.html b/blog/active-learning/index.html index ec87d4b..2570692 100644 --- a/blog/active-learning/index.html +++ b/blog/active-learning/index.html @@ -21,7 +21,13 @@
Musicaldg - +
← 全部文章

Active Learning

目录

人工标注的成本,来自标注者投入的时间、专业知识,以及组织和审核标注的过程。面对大量未标注数据,一个自然的问题是:如果只能获得少量标签,应该先标注哪些样本?

Active Learning(主动学习)让模型参与这个选择过程。模型先评估未标注样本,再把其中一些交给人类标注,用新增标签更新自己。

diff --git a/blog/behavior-cloning-plid/index.html b/blog/behavior-cloning-plid/index.html index 884ac81..bd9de97 100644 --- a/blog/behavior-cloning-plid/index.html +++ b/blog/behavior-cloning-plid/index.html @@ -21,7 +21,13 @@
Musicaldg - +
← 全部文章

Behavior Cloning 与 PLID

目录

Behavior Cloning 把专家示范当作监督学习数据,但模型部署后会访问训练时没有覆盖的状态。本文从模仿学习的基本形式化出发,说明 covariate shift 与 compounding errors,并推导 PLID / DAgger 如何通过当前策略 rollout、再由专家补充标签,逐步修正状态分布偏移。

前置知识:概率、条件概率、监督学习损失,以及基本的 Markov Decision Process(MDP)概念。KL 散度的定义见 Active Learning。

diff --git a/blog/distribution-matching-gail/index.html b/blog/distribution-matching-gail/index.html index aeea5c9..9b34dd3 100644 --- a/blog/distribution-matching-gail/index.html +++ b/blog/distribution-matching-gail/index.html @@ -21,7 +21,13 @@
Musicaldg - +
← 全部文章

Distribution Matching 与 GAIL

目录

Behavior Cloning 直接拟合专家动作,PLID 则让专家在模型实际访问的状态上补充标签。若不能持续询问专家,能否仍让模型产生与专家相似的轨迹或状态—动作分布?本文从轨迹概率、Forward KL 与状态—动作分布出发,说明 GAIL 如何用判别器间接完成分布匹配。

系列上一篇:Behavior Cloning 与 PLID。前置知识还包括 MDP、条件概率,以及 Active Learning 中的 KL divergence。

diff --git a/blog/index.html b/blog/index.html index e70d792..6d2fd82 100644 --- a/blog/index.html +++ b/blog/index.html @@ -5,25 +5,31 @@ - + - 文章 · Musicaldg + Blog · Musicaldg - +
Musicaldg - +
-

文章

约 13 分钟

IRL、RLHF 与 DPO

从逆强化学习出发,说明如何从专家行为与人类偏好学习目标,并连接到 RLHF 与 DPO。

阅读全文
约 10 分钟

Distribution Matching 与 GAIL

从轨迹与状态—动作分布匹配出发,说明 Forward KL 为何可能退化为 BC,以及 GAIL 如何用判别器完成分布匹配。

阅读全文
约 10 分钟

Behavior Cloning 与 PLID

说明 Behavior Cloning 的状态分布偏移,并推导 PLID / DAgger 如何用交互式专家标签修正策略。

阅读全文
约 12 分钟

Active Learning

介绍主动学习的基本流程,比较不确定性采样、QBC 和 BALD 三种选样方法。

阅读全文
+

Blog

系列Imitation Learning3 篇 · 2026-10-20 — 2026-11-03

其他文章

约 12 分钟

Active Learning

介绍主动学习的基本流程,比较不确定性采样、QBC 和 BALD 三种选样方法。

阅读全文
© 2026 MusicaldgRSS
diff --git a/blog/irl-rlhf-dpo/index.html b/blog/irl-rlhf-dpo/index.html index ff57a71..6876da1 100644 --- a/blog/irl-rlhf-dpo/index.html +++ b/blog/irl-rlhf-dpo/index.html @@ -21,7 +21,13 @@
Musicaldg - +
← 全部文章

IRL、RLHF 与 DPO

目录

Behavior Cloning、PLID 和 GAIL 主要学习专家如何行动。若希望模型在新环境中仍能做出合理决策,就需要进一步理解专家行为背后的目标。本文从 Inverse Reinforcement Learning 出发,说明如何从专家行为推断奖励,并连接到 RLHF 与 DPO。

系列前文:Behavior Cloning 与 PLID、Distribution Matching 与 GAIL。

diff --git a/content/news.json b/content/news.json new file mode 100644 index 0000000..941b9f6 --- /dev/null +++ b/content/news.json @@ -0,0 +1,17 @@ +[ + { + "date": "2026-11-03", + "text": "发布 Imitation Learning 系列第三篇:IRL、RLHF 与 DPO。", + "href": "/blog/irl-rlhf-dpo/" + }, + { + "date": "2026-11-02", + "text": "更新 Distribution Matching 与 GAIL,并整理仿作学习系列。", + "href": "/blog/distribution-matching-gail/" + }, + { + "date": "2026-10-20", + "text": "发布 Behavior Cloning 与 PLID。", + "href": "/blog/behavior-cloning-plid/" + } +] diff --git a/content/pages/about.md b/content/pages/about.md new file mode 100644 index 0000000..2e461ca --- /dev/null +++ b/content/pages/about.md @@ -0,0 +1,8 @@ +--- +title: About me +summary: 关于 Musicaldg。 +--- + +机器学习笔记与个人主页。更多内容稍后补充。 + +目前主要整理模仿学习、主动学习等相关笔记,源码与页面托管在 [GitHub](https://github.com/Musicaldg)。 diff --git a/content/pages/cv.md b/content/pages/cv.md new file mode 100644 index 0000000..7b524e4 --- /dev/null +++ b/content/pages/cv.md @@ -0,0 +1,8 @@ +--- +title: CV +summary: Curriculum Vitae。 +--- + +CV 页面已预留,正文与可下载版本稍后补充。 + +也可先通过 [GitHub](https://github.com/Musicaldg) 查看公开项目与提交记录。 diff --git a/cv/index.html b/cv/index.html new file mode 100644 index 0000000..012c7a1 --- /dev/null +++ b/cv/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + CV · Musicaldg + + + + + + + + +
+
+ Musicaldg + +
+

CV

Curriculum Vitae。

CV 页面已预留,正文与可下载版本稍后补充。

+

也可先通过 GitHub 查看公开项目与提交记录。

+
© 2026 MusicaldgRSS
+
+ + diff --git a/generated-files.json b/generated-files.json index 8aaa90d..8080918 100644 --- a/generated-files.json +++ b/generated-files.json @@ -2,6 +2,8 @@ "index.html", "blog/index.html", "404.html", + "about/index.html", + "cv/index.html", "series/imitation-learning/index.html", "blog/irl-rlhf-dpo/index.html", "blog/distribution-matching-gail/index.html", diff --git a/index.html b/index.html index baed61e..dc784b3 100644 --- a/index.html +++ b/index.html @@ -21,15 +21,35 @@
Musicaldg - +
-
+
+
+

News

+
+ +
+
+
+

Latest Writing

+ All posts +
+
© 2026 MusicaldgRSS
diff --git a/scripts/build.py b/scripts/build.py index b744ab8..1c04c9c 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -144,7 +144,10 @@ def read_posts(): continue source = parts[2].strip() headings = re.findall(r'(.*?)', rendered) - toc = ''.join(f'{esc(html.unescape(re.sub(r"<[^>]+>", "", text)))}' for level, anchor, text in headings) + toc = ''.join( + f'{esc(html.unescape(re.sub(r"<[^>]+>", "", text)))}' + for level, anchor, text in headings + ) plain = re.sub(r'\$\$[\s\S]*?\$\$|\$[^$\n]+\$', '', source) chinese = len(re.findall(r'[\u4e00-\u9fff]', plain)) words = len(re.findall(r'[A-Za-z]+', plain)) @@ -165,7 +168,7 @@ def read_series(posts_by_slug): if not entry.get(key): raise ValueError(f'series.json: 缺少 {key}') if not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', entry['id']): - raise ValueError(f'series.json: id 请使用小写英文、数字和连字符') + raise ValueError('series.json: id 请使用小写英文、数字和连字符') members = [] for slug in entry['posts']: post = posts_by_slug.get(slug) @@ -182,8 +185,57 @@ def read_series(posts_by_slug): 'count': len(members), 'start': min(p['date'] for p in members), 'end': max(p['date'] for p in members), + 'updated': max(p['date'] for p in members), + }) + return sorted(series_list, key=lambda x: x['updated'], reverse=True) + + +def read_news(): + path = ROOT / 'content/news.json' + if not path.exists(): + return [] + items = json.loads(path.read_text(encoding='utf-8')) + news = [] + for entry in items: + for key in ('date', 'text'): + if not entry.get(key): + raise ValueError('news.json: 缺少 date 或 text') + dt.date.fromisoformat(entry['date']) + href = entry.get('href') or '' + if href and not href.startswith('/'): + raise ValueError(f'news.json: href 仅支持站内路径,收到 {href}') + news.append({'date': entry['date'], 'text': entry['text'], 'href': href}) + return sorted(news, key=lambda x: x['date'], reverse=True) + + +def read_pages(): + pages = [] + root = ROOT / 'content/pages' + if not root.exists(): + return pages + for path in sorted(root.glob('*.md')): + raw = path.read_text(encoding='utf-8') + if not raw.startswith('---\n'): + raise ValueError(f'pages/{path.name}: 缺少元信息') + parts = raw.split('---', 2) + if len(parts) != 3: + raise ValueError(f'pages/{path.name}: 元信息未闭合') + meta = {} + for line in parts[1].strip().splitlines(): + key, value = line.split(':', 1) + meta[key.strip()] = value.strip() + for key in ('title', 'summary'): + if not meta.get(key): + raise ValueError(f'pages/{path.name}: 缺少 {key}') + if not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', path.stem): + raise ValueError(f'pages/{path.name}: 文件名请使用小写英文、数字和连字符') + pages.append({ + 'slug': path.stem, + 'title': meta['title'], + 'summary': meta['summary'], + 'body': markdown(parts[2].strip()), }) - return series_list + return pages def build(): @@ -194,6 +246,8 @@ def build(): series_list = read_series(posts_by_slug) series_slugs = {slug for series in series_list for slug in (p['slug'] for p in series['posts'])} standalone = [p for p in posts if p['slug'] not in series_slugs] + news_items = read_news() + pages = read_pages() previous = json.loads((ROOT / 'generated-files.json').read_text()) if (ROOT / 'generated-files.json').exists() else [] generated = [] @@ -203,18 +257,47 @@ def write(path, content): target.write_text(content.rstrip() + '\n', encoding='utf-8') generated.append(path) - def page(path, title, description, body, current='', og_type='website', extra_assets=''): + def page(path, title, description, body, current='', og_type='website', extra_assets='', page_class=''): url_path = '/' if path == 'index.html' else '/' + path.removesuffix('index.html') - article_assets = '' if og_type == 'article' else '' - write(path, layout.substitute(name=esc(config['name']), github=esc(config['github']), year=dt.date.today().year, title=esc(title), description=esc(description), canonical=esc(config['url'] + url_path), body=body, home_current='aria-current="page"' if current == 'home' else '', blog_current='aria-current="page"' if current == 'blog' else '', og_type=og_type, page_class='article-page' if og_type == 'article' else '', article_assets=article_assets + extra_assets)) + article_assets = ( + '' + '' + '' + if og_type == 'article' else '' + ) + classes = ' '.join(x for x in [page_class, 'article-page' if og_type == 'article' else ''] if x) + write(path, layout.substitute( + name=esc(config['name']), + github=esc(config['github']), + year=dt.date.today().year, + title=esc(title), + description=esc(description), + canonical=esc(config['url'] + url_path), + body=body, + home_current='aria-current="page"' if current == 'home' else '', + about_current='aria-current="page"' if current == 'about' else '', + cv_current='aria-current="page"' if current == 'cv' else '', + blog_current='aria-current="page"' if current == 'blog' else '', + og_type=og_type, + page_class=classes, + article_assets=article_assets + extra_assets, + )) def blog_rows(selected): - return ''.join(f'
约 {p["minutes"]} 分钟

{esc(p["title"])}

{esc(p["summary"])}

阅读全文
' for p in selected) or EMPTY + return ''.join( + f'' + f'
' + f'约 {p["minutes"]} 分钟
' + f'

{esc(p["title"])}

{esc(p["summary"])}

' + f'阅读全文
' + for p in selected + ) or EMPTY def compact_card(post, index=None): index_html = f'{index:02d}' if index is not None else '' return ( - f'' + f'' f'{index_html}' f'
' f'

{esc(post["title"])}

' @@ -222,14 +305,32 @@ def compact_card(post, index=None): f'
' ) - def series_folder(series, open_by_default=False): + def roomy_card(post, index=None): + index_html = f'{index:02d}' if index is not None else '' + return ( + f'' + f'{index_html}' + f'
' + f'
' + f'约 {post["minutes"]} 分钟
' + f'

{esc(post["title"])}

' + f'

{esc(post["summary"])}

' + f'阅读全文 ' + f'
' + ) + + def series_folder(series, *, card='compact', open_by_default=False, variant=''): span = series['start'] if series['start'] == series['end'] else f'{series["start"]} — {series["end"]}' - cards = ''.join(compact_card(post, index=i) for i, post in enumerate(series['posts'], 1)) + maker = roomy_card if card == 'roomy' else compact_card + cards = ''.join(maker(post, index=i) for i, post in enumerate(series['posts'], 1)) open_attr = ' open' if open_by_default else '' + class_name = 'series-folder' + (f' {variant}' if variant else '') return ( - f'
' + f'
' f'' - f'' + f'' f'系列' f'{esc(series["title"])}' f'{series["count"]} 篇 · {span}' @@ -238,55 +339,155 @@ def series_folder(series, open_by_default=False): f'
' f'

{esc(series["summary"])}

' f'
{cards}
' - f'打开系列页 ' + f'打开系列页 ' + f'' f'
' ) + def news_list(items, limit=3): + selected = items[:limit] + if not selected: + return '

No news yet.

' + rows = [] + for item in selected: + text = esc(item['text']) + if item['href']: + text = f'{text}' + rows.append( + f'
  • ' + f'{text}
  • ' + ) + return '
      ' + ''.join(rows) + '
    ' + def home_feed(): - parts = [series_folder(series) for series in series_list] - parts.extend(compact_card(post) for post in standalone[:4]) - return ''.join(parts) or EMPTY + latest_series = series_list[:1] + parts = [series_folder(series) for series in latest_series] + parts.extend(compact_card(post) for post in standalone[:2]) + return ''.join(parts) or '

    No posts yet.

    ' - home = Template((ROOT / 'templates/home.html').read_text(encoding='utf-8')).substitute(feed=home_feed()) + def blog_feed(): + parts = [series_folder(series, card='roomy', variant='series-folder-roomy') for series in series_list] + if standalone: + if series_list: + parts.append('

    其他文章

    ') + parts.append('
    ' + blog_rows(standalone) + '
    ') + elif not series_list: + parts.append('
    ' + EMPTY + '
    ') + return ''.join(parts) + + home = Template((ROOT / 'templates/home.html').read_text(encoding='utf-8')).substitute( + news=news_list(news_items, 3), + feed=home_feed(), + ) page('index.html', config['name'], config['description'], home, 'home', extra_assets='') - page('blog/index.html', '文章 · ' + config['name'], '机器学习笔记。', '

    文章

    ' + blog_rows(posts) + '
    ', 'blog') - page('404.html', '页面未找到 · ' + config['name'], '页面未找到', '

    404

    页面未找到

    回到首页 ↗
    ') + page( + 'blog/index.html', + 'Blog · ' + config['name'], + '机器学习笔记。', + '

    Blog

    ' + '
    ' + blog_feed() + '
    ', + 'blog', + extra_assets='', + ) + page( + '404.html', + '页面未找到 · ' + config['name'], + '页面未找到', + '

    404

    页面未找到

    回到首页 ↗
    ', + ) + for item in pages: + body = ( + f'

    {esc(item["title"])}

    ' + f'

    {esc(item["summary"])}

    ' + f'
    {item["body"]}
    ' + ) + page( + f'{item["slug"]}/index.html', + item['title'] + ' · ' + config['name'], + item['summary'], + body, + item['slug'], + page_class='site-page', + ) for series in series_list: - cards = ''.join(compact_card(post, index=i) for i, post in enumerate(series['posts'], 1)) + cards = ''.join(roomy_card(post, index=i) for i, post in enumerate(series['posts'], 1)) body = ( - f'
    ← 首页' + f'
    ← Blog' f'

    系列

    {esc(series["title"])}

    ' f'

    {esc(series["summary"])}

    ' f'
    {cards}
    ' ) - page(f'series/{series["id"]}/index.html', series['title'] + ' · ' + config['name'], series['summary'], body, 'home') + page( + f'series/{series["id"]}/index.html', + series['title'] + ' · ' + config['name'], + series['summary'], + body, + 'blog', + ) for post in posts: toc = '' - body = f'
    ← 全部文章

    {esc(post["title"])}

    目录{toc}
    {post["body"]}
    ' - page(f'blog/{post["slug"]}/index.html', post['title'] + ' · ' + config['name'], post['summary'], body, 'blog', 'article') + body = ( + f'
    ' + f'← 全部文章' + f'

    {esc(post["title"])}

    ' + f'' + f'
    目录{toc}
    ' + f'
    {post["body"]}
    ' + f'
    ' + ) + page( + f'blog/{post["slug"]}/index.html', + post['title'] + ' · ' + config['name'], + post['summary'], + body, + 'blog', + 'article', + ) feed = ET.Element('rss', version='2.0') channel = ET.SubElement(feed, 'channel') - for key, value in [('title', config['name']), ('link', config['url']), ('description', config['description']), ('language', 'zh-CN')]: + for key, value in [ + ('title', config['name']), + ('link', config['url']), + ('description', config['description']), + ('language', 'zh-CN'), + ]: ET.SubElement(channel, key).text = value for post in posts: item = ET.SubElement(channel, 'item') - for key, value in [('title', post['title']), ('link', config['url'] + '/blog/' + post['slug'] + '/'), ('guid', config['url'] + '/blog/' + post['slug'] + '/'), ('description', post['summary']), ('pubDate', dt.datetime.strptime(post['date'], '%Y-%m-%d').strftime('%a, %d %b %Y 00:00:00 GMT'))]: + for key, value in [ + ('title', post['title']), + ('link', config['url'] + '/blog/' + post['slug'] + '/'), + ('guid', config['url'] + '/blog/' + post['slug'] + '/'), + ('description', post['summary']), + ('pubDate', dt.datetime.strptime(post['date'], '%Y-%m-%d').strftime('%a, %d %b %Y 00:00:00 GMT')), + ]: ET.SubElement(item, key).text = value write('feed.xml', ET.tostring(feed, encoding='utf-8', xml_declaration=True).decode('utf-8')) sitemap = ET.Element('urlset', xmlns='http://www.sitemaps.org/schemas/sitemap/0.9') for path in generated: if path.endswith('index.html'): entry = ET.SubElement(sitemap, 'url') - ET.SubElement(entry, 'loc').text = config['url'] + ('/' if path == 'index.html' else '/' + path.removesuffix('index.html')) + ET.SubElement(entry, 'loc').text = config['url'] + ( + '/' if path == 'index.html' else '/' + path.removesuffix('index.html') + ) write('sitemap.xml', ET.tostring(sitemap, encoding='utf-8', xml_declaration=True).decode('utf-8')) write('robots.txt', 'User-agent: *\nAllow: /\nSitemap: ' + config['url'] + '/sitemap.xml') + page_slugs = {p['slug'] for p in pages} for obsolete in set(previous) - set(generated): - if obsolete.endswith('/index.html') and (obsolete.startswith('blog/') or obsolete.startswith('series/')): + if not obsolete.endswith('/index.html') or obsolete == 'index.html': + continue + top = obsolete.split('/', 1)[0] + if ( + obsolete.startswith('blog/') + or obsolete.startswith('series/') + or top in page_slugs + ): target = ROOT / obsolete if target.exists(): target.unlink() write('generated-files.json', json.dumps(generated, indent=2)) - print(f'构建完成:{len(posts)} 篇已发布文章。') + print(f'构建完成:{len(posts)} 篇已发布文章,{len(pages)} 个页面。') if __name__ == '__main__': diff --git a/series/imitation-learning/index.html b/series/imitation-learning/index.html index 9e4c101..530621a 100644 --- a/series/imitation-learning/index.html +++ b/series/imitation-learning/index.html @@ -21,9 +21,15 @@ diff --git a/sitemap.xml b/sitemap.xml index 053215e..ffb3a45 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,2 +1,2 @@ -https://musicaldg.github.io/https://musicaldg.github.io/blog/https://musicaldg.github.io/series/imitation-learning/https://musicaldg.github.io/blog/irl-rlhf-dpo/https://musicaldg.github.io/blog/distribution-matching-gail/https://musicaldg.github.io/blog/behavior-cloning-plid/https://musicaldg.github.io/blog/active-learning/ +https://musicaldg.github.io/https://musicaldg.github.io/blog/https://musicaldg.github.io/about/https://musicaldg.github.io/cv/https://musicaldg.github.io/series/imitation-learning/https://musicaldg.github.io/blog/irl-rlhf-dpo/https://musicaldg.github.io/blog/distribution-matching-gail/https://musicaldg.github.io/blog/behavior-cloning-plid/https://musicaldg.github.io/blog/active-learning/ diff --git a/templates/home.html b/templates/home.html index 47c326f..a25466b 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,8 +1,22 @@ -
    +
    +
    +

    News

    +
    + $news +
    +
    +
    +

    Latest Writing

    + All posts +
    +
    $feed +
    diff --git a/templates/layout.html b/templates/layout.html index 51a0547..5258685 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -21,7 +21,13 @@
    $name - +
    $body