返回 文章 build CMS 文章

用 TRL 和 OpenEnv 训练编码模型画水彩画:一次开放复现

用 TRL 与 OpenEnv 复现「让模型写代码画水彩」的完整开放配方,并比较三种审美奖励混合。

强化学习TRLOpenEnv水彩画
成长分 / 100 84 综合收获、行动、留存与影响

用 TRL 和 OpenEnv 训练编码模型画水彩画:一次开放复现
为什么值得读完整公开了从参考池、RL 环境到训练脚本和模型的端到端配方,可直接复现。

展示了在不可验证的审美偏好上做 RL 的奖励设计、训练细节与失败排查。

关键洞察
  1. 奖励由门控、长度软推动、成对评判器和 HPSv3 组成,其中成对评判器权重最高,池子定义了品味。
  2. 模型只被允许使用 p5.brush 的十个方法,只能绘制填充形状,风格来自这一限制。
  3. 训练中模型首先学会停止产出糟糕画作,成对评判改变的是顶部质量,学习体现在分布中段。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:27497

强化学习 • 已更新 • 22 • 1

8月23日,[Surya

Narreddi](https://x.com/kickingkeys/status/2091570990048276897) 发布了一段精美的视频,展示了一个语言模型绘制的水彩画。该模型通过

p5.brush 编写 JavaScript,这个库“为 p5.js

添加了自然绘图工具”。视频迅速走红,截至撰写本文时已获得超过 150 万次观看。

该视频附带了一篇[博客

文章](https://surya.website/rling-qwen-to-paint-with-code),解释了项目早期较窄阶段(特写花朵,而非视频中的完整构图)背后的训练过程,遗憾的是尚未公开相关制品。他的网站表示完整的技术报告即将发布,所以请务必关注他。最初的想法来自他,源于艺术与设计领域,[他的技能远超于我](https://x.com/kickingkeys/status/2094901433149612118)。我的尝试则是在工程方面,公开复现这一配方,并发布每一个部分。

注意:关于项目背后的背景,由 Surya 本人讲述,请观看[他的论文视频]。

在本文中,我尝试用 TRL

OpenEnv 复现他的想法。参考池数据集、RL

环境、训练脚本以及训练好的模型,全部开源。

整个流程在 Hugging Face 上端到端运行:

一旦两个 Space 启动,配方只需一条命令。复制

环境

评分模型,设置两个

环境变量用于奖励混合,然后启动:

hf jobs uv run train/watercolour_grpo.py --flavor h200 --timeout 48h --secrets HF_TOKEN -- \
--env-url https://<you>-watercolour-env.hf.space \
--model Qwen/Qwen3.5-35B-A3B --lora --all-linear --bf16 --gradient-checkpointing \
--subject 'a peach hibiscus' --references 4 \
--top-p 0.95 --top-k 20 \
--lr 5e-5 --lr-scheduler constant_with_warmup --warmup-steps 5 \
--scale-rewards none \
--steps 110 --n-episodes 240 --num-generations 8 \
--per-device-batch-size 1 --gradient-accumulation-steps 8 \
--max-completion-length 8192 \
--run-tag my-run --out <you>/watercolour-grpo --push-to-hub

本文的其余部分讲述的是如何实现这一目标,[每一部分都在

仓库中](https://github.com/adithya-s-k/HuggingEnvs/tree/main/02-watercolour)。

我一步步遵循了原博客,只在确实需要时才做改动。我自己的每一个想法都放进了清单,而不是放进实验里,这份清单最终成了结尾处的“我接下来会尝试什么”,旁边是已发布产物的完整列表。如果你已经读过他的文章,那么整体框架和

奖励设计对你来说会很熟悉。新的内容是开放实现、人工评分的池子,以及三种奖励混合方式的训练与比较,它从[你需要构建的 RL

环境](https://huggingface.co#the-rl-environment-you-need-to-build)开始。

这些画看起来松散、不完美、手工感强,而当下图像模型产出的却是完美(统计上平均)的图片。我猜这种反差正是视频

走红的重要原因。它让我想起生成式 AI 艺术的早期,那时重点

是探索媒介。DeepDream(2015)本是一个调试工具,人们把它变成了艺术,像 [Edmond de

Belamy](https://en.wikipedia.org/wiki/Edmond_de_Belamy)(2018)这样的作品来自艺术家们探索

GAN 能做什么,而像 Mario Klingemann 这样的艺术家在那几年用神经网络制作了梦幻般的肖像

这个项目感觉更接近那些早期日子。在他的论文中,Surya 描述了通向这里的路径。他从提示文本到图像模型开始,在那里提示词是

你唯一能拉动的杠杆,更多细节只能在一定程度上换来更多控制。训练

模型本身则走得更远。这个想法的

另一半是媒介。模型编写一段约 150

行的 JavaScript 程序来绘制图像。那个模型输出就是代码。你可以阅读、编辑并再次运行它,每一笔背后的决策都可见。而风格来自一个限制:模型只被允许使用库中十个方法。更多内容见下文。

在同一时期,Anna Ridler

拍摄了数千朵郁金香,手工标注每一朵,把数据集本身作为艺术品展出,后来又在上面训练了一个模型。我通过构建这个项目时

AI 代理带回的参考文献发现了她的作品,并且很喜欢它,因为这个

项目做了非常相似的事情:手工策划一组图像,然后针对它们进行训练。

最近大多数关于语言模型的 RL 工作都使用可验证的奖励。例如,有已知答案的数学题、能通过测试的代码,或者对错分明且运行成本低的评分器。这个项目更接近更早的例外——RLHF,即模型从人类偏好中学习一个奖励模型。

这里的奖励是审美偏好。没有正确答案。这个项目真正的问题

是:你能否在品味之上做 RL。

奖励,正如他的博客所定义、也正如我构建的 RL 环境所实现的那样:

术语 权重 衡量内容
gate
0.05 草图能编译、能画出东西、没有作弊
length
0.05 对更长代码片段的软性推动
成对评判器 0.60 风格,与从池中抽取的参考进行比较

HPSv3 是一个开放的 7B 偏好模型。给它一张图像和一段文字描述,它会返回

一个分数,表示一个人对该图像的偏好程度。它是在大量

人类对成对图像的选择数据上训练的,因此它的分数是许多人品味的平均值。成对评判模型是

Qwen3-VL-30B-A3B-Instruct,一个

通过 HF Inference Providers 调用的通用视觉模型。成对评判模型会看到候选

画作

与从池中随机选取的四张参考图并排放置,并由一段关于权衡标准的文字描述(晕染、半透明淡彩、柔和边缘)引导,每次比较都以两种呈现顺序进行,其分数是候选画作

赢得的比较所占的比例。它唯一的标准就是那个池,因此它的分数就是我的品味,正如

那些评分所编码的那样。

那些就是 Narreddi 收敛到的权重。在这里,池定义了品味。这把工作从调超参数转变为构建决定何为美的集合。

我用这个奖励训练了三次运行。它们唯一的区别在于两个模型评判之间的权重分配:

运行 成对评判 HPSv3 角色
judge-led
0.60 0.30 原始混合,在第 110 步停止
hps-led
0.30 0.60 中间点,在第 110 步停止
hps-only
0.00 0.90 验证运行,在第 60 步停止

我从 hps-only 开始,以验证该流程是否能够学习。一旦奖励开始上升且指标健康,就没有理由再运行更久,所以我转而启动了两个更长的运行。更长的运行所提出的问题是:你能把 HPSv3 的多少能力交给成对评判?评判承载的权重越多,奖励就越意味着我的品味而非所有人的品味,也就应该越难攀升。顺带一提,如果你把它推得足够远,或者你的风格与平均值相差太远,模型可能会完全停止。

幸运的是,它并没有停止,两个开启成对评判的运行也都学会了。人工评分的池可以引导策略,至少从指标和最终画作来看是这样。数字如下。

免责声明。如果我们使用前沿模型,它已经可以根据提示生成绘制水彩画的 JavaScript 代码。这就是起点。这里的工作是关于教一个更小的模型去做这件事,并结合一个人自己的艺术偏好。

环境包裹了模型与奖励之间的一切,包括模型用于绘画的 JavaScript 库、限制它的系统提示、渲染每张草图的无头 Chromium,以及拒绝作弊的门控。

这个库所做的工作比看起来要多。

p5.brush,由

@acamposuribe 开发,模拟的是一种媒介,而不是绘制

形状:颜料会渗出填充的边缘,纸张有纹理,笔触有分量,流场会拖动笔触四处移动。当模型调用 brush.fillBleed(0.25)

时,它是在决定墨水

渗出多远。

注意。p5.brush 的作者早在这一切之前就一直在尝试教机器画画。2022 年,他创作了一个生成艺术系列,其中隐藏着一本关于教 p5.js 像孩子一样画画的日记:“它几乎不会使用蜡笔 [...] 它无法遵循简单的指令。我今天就到此为止,非常令人恼火。”这个系列原本计划有三件作品,他完成了两件。当 Surya 的视频走红时,[他引用了它],分享了那本日记,并说这件作品是第三件自行到来的作品。

p5.brush 暴露了 47 个方法。提示词允许 10 个:scaleBrushes

noStroke

fill

noFill

fillBleed

fillTexture

beginShape

vertex

endShape

circle

。其他三十七个方法所添加的功能,如线条、排线和自定义画笔,会破坏水彩效果。有了这十个方法,模型只能绘制填充形状,而库会为每一个形状添加晕染。

draw()

以及它渲染到的内容。注释是模型自己的。奖励 0.864,129 行,第 22 步。每幅画的完整源代码都在 rollouts 数据集中。他的博客文章为我节省了大量可能浪费在迭代提示词上的时间。冗长的 API 参考会让模型发明不存在的方法,而他 200 次 GEPA 迭代收敛到了一个没有文档的严格允许列表。我看到了同样的失败,并手动编写了允许列表。我对该配方唯一的补充是一句话:将每片花瓣涂两到三次,先涂一大笔,然后在里面涂一笔更小、更不透明的。这个小改动让我的输出色彩丰富了很多。

注意。如果你是第一次听说 [GEPA],它是一个自动提示词优化器。一个语言模型用平实的语言反思当前提示词在哪里失败,并提出一个更好的提示词,然后循环重复。

门控是最后一块。草图必须能编译,使用库而不是直接调用 p5,在画布上放置真正的颜料,并且不能试图欺骗评分器,例如在画布上写文字。

池子由 178 幅画 组成,根据我的个人偏好分为

两个层级,love

okay

。所有这些实际上都是由模型生成的。四个开放权重模型,通过 Inference Providers 调用,编写了 p5.brush 草图,每个模型都基于一张

来自 iNaturalist 的真实、开放许可的芙蓉照片。一个视觉模型对每张草图给出了书面

反馈,经过三次细化迭代。然后每一张最终渲染图都由我逐一评分,178 张入选。

生成器 画作数量

在这里,我选择了四个不同的模型家族来测试它们不同的风格。这四个是在快速可靠性检查中每次都能生成有效草图的开放模型,另外两个候选模型因未通过而被放弃。如果你想生成自己的池子,你可能会选择其他模型。

这些层级在奖励中起着实际作用。当成对评判器抽取四个参考时,一半来自 love

,一半来自 okay

,因此策略总是面对一些它

有时能击败的对手,而且无论对哪个层级,获胜的报酬相同。这是我少数几个

有意的改动之一:原始版本只与最高层级比较,而我在抽取中保留了

较容易的层级,这样弱小的早期策略仍然能获得信号。

那里没有人类创作的画作,这是一个真正的局限。p5.brush 是一个小众库,其中存在的、带有可访问代码的人类作品只有寥寥几件,远达不到训练语料库所需的数量,正如他的博客也指出的那样。

正如我之前已经讨论过的,这里有趣的想法是,模型将学会模仿池中包含的内容。如果我们把环境指向不同的数据集,奖励就会自动改变,而无需改动一行代码。对于我生成并公开分享的数据集,我也附上了源草图。

如果我们更仔细地观察评判器,它们回答的是不同的问题。HPSv3 判断它是否是一朵花,而成对评判器则判断它是否以我选择的风格画得好。

在任何东西奏效之前,有一段很长的奖励曲线平坦期。如果你曾尝试在没有开放工件的情况下复现一篇研究论文/博客,你大概能感同身受。每一次运行都在检验我认为关于问题所在的一个合理理论。一次运行需要很多时间,所以我在还在查看上一次运行结果的同时,就排队了下一次运行。一如既往,从更简单且能奏效的东西开始,然后在此基础上构建,才是答案。一个简单的控制任务,没有浏览器也没有评判器,是第一次成功学习的尝试,原因是我的学习率太低了。

另一个让我花时间才发现的变化是正确调整 LoRA 参数。通常的 target_modules

列表假设的是稠密模型,而 Qwen/Qwen3.5-35B-A3B 是一个专家混合模型,其大多数投影的命名方式不同,所以适配器只训练了四十层中的十层。我通过将其改为

all-linear

解决了这个问题,这会触及每一个线性层。此架构中的路由专家是融合张量,即使 all-linear

也会使其保持冻结,但其他所有部分都会获得适配器,而这已足够学习。修复方法是在 TRL 的 GRPOTrainer 中做了四处更改:

设置 原因
学习率 2e-5 5e-5
《LoRA Without Regret》用于 GRPO 的上限
调度器 linear
constant_with_warmup
线性衰减在运行中途就消耗了大部分学习率,所以奖励从未起飞
scale_rewards
group
none
一个门控拒绝正在缩小组内其他所有优势
target_modules
手动列表 all-linear
触及每一个线性层

这四处更改解锁了第一次成功的运行(hps-only

),奖励明显改善。

有了这个配置,三次运行都能学习。两次评判器运行都启动了 200 步,并在 110 步时停止,奖励仍在缓慢攀升。一步需要十五到十八分钟,而混合之间的比较已经稳定,所以我停止了这两次以节省算力。每次运行前三分之一和最后三分之一的平均组奖励:

运行 步数 前三分之一 最后三分之一 Δ
hps-only
60 0.58 0.71 +0.13
judge-led
110 0.45 0.72 +0.27
hps-led
110 0.57 0.82 +0.24

这三条曲线与我的品味所占的权重相符。评判器

权重越大,起点越低,攀升越嘈杂。judge-led

在最初三十步几乎平坦

才开始移动。这与解决调试的是同一个动作。把问题缩小到

某个东西能学习,然后一次一个地把困难的部分加回来。

成对评判项本身在使用它的两次运行中都有所上升。随着训练推进,模型在与池子的比较中赢得更多

比较,这是 hps-only

无法做到的。任何一次运行中都没有出现某一组坍缩到相同奖励的情况,而那种情况正是会

杀死梯度的 GRPO 失败模式。给好奇的读者:各指标曲线(HPSv3、颜料覆盖率、熵)在仓库中以 CSV 形式提供

完整的启动命令、硬件以及将其变成另外两次运行的两个环境变量都在[这份

配方](https://github.com/adithya-s-k/HuggingEnvs/tree/main/02-watercolour)中。

**在每一次运行中,模型学到的第一件事就是停止产出糟糕的

画作**,即那些总奖励低于 0.3 的近空白画布和无定形涂抹。在

hps-only

中,组均值上升的四分之三来自糟糕画作变得罕见。在评判运行中这种坍缩更加陡峭:低于 0.3 的 rollout 在 judge-led

的三段中从 99 降到 16,在 hps-led

中从 37 降到 4。

这就是为什么最直观的视觉呈现——每一步的最佳画作——在

hps-only

中几乎看不出差别。它在整个运行中变化 +0.034,而中位数变化 +0.155

学习体现在分布的中段。

成对评判改变的是顶部。hps-only

中,画作变得更可靠,但没有变得更好。好画作的质量对

组均值只贡献了 +0.03,而且一旦 HPSv3 看到中心周围有花瓣和一根茎,它就不再要求

更多颜料。开启评判后,故事的另一半出现了。这里的更好意味着更接近

池子,也就是更接近我评为“好”或我更喜欢的作品。这在 judge-led

中贡献了 +0.12,在 hps-led

中贡献了 +0.16

,每一步的最佳画作也上升了,并且

两次运行中颜料覆盖率都翻倍(0.11 到 0.23,以及 0.13 到 0.30),而 hps-only

几乎没让它动。有了一个可供追赶的参照,一幅好画作仍然可以变得更好,模型也开始因使用更多颜料而获得奖励。

还有一个发现。模型忽略了一条明确的指令,而且它这样做是对的。

系统提示要求十五到三十个填充形状。如果我们看真实的均值,它在 7 到 9 之间,而且

n_shapes

在任何一次运行中都几乎与奖励不相关(+0.000、−0.14、+0.07)。策略

并没有因为遵守那句话而获得奖励,所以它不遵守。

hps-only

这条路线也有一个上限。如果每个 rollout 都匹配它的好

画作,那次运行的均值会停在 0.771。更多步数是否会打破它,是一个悬而未决的问题。

这些画作还显示出表格所遗漏的东西。在每一次运行内部,它们看起来都

很相似。随着训练推进,每一组内部的奖励变得更接近,开场视频中的中位数画作看起来像是同一朵花的不同演绎。这就是 GRPO

在用由单一主题构建的池子做它被设计来做的事。池子决定了

什么算作多样性,就像它决定了什么算作质量一样。如果奖励

只为匹配某一朵花而支付,模型就学会画那一朵花。更多样的输出需要

更多样的池子,而构建一个池子是更多的策展工作。Surya 较新的作品就是一个例子。[Alex Yango 的动物

画作](https://x.com/alexyango/status/2091696296931574217)是同样的配方,只是

池子里的不同选择。这是审美奖励与数学评分器之间最大的区别。数字背后是一项非常人性化的工作,即决定什么该被纳入奖励集合。Jason Liu 关于品味的文章用一句话概括了通用版本。AI 把瓶颈从制作转移到了察觉。

Surya 在博客结尾列出了一些他最喜欢的作品。我不挑选自己的,而是下面放一面墙,展示在两次评判运行中奖励得分最高的 178 幅画作,与参考池所包含的数量相同,顺序不分先后。打开它,挑选你自己的。

要做出选择,你看了很多,留下了少数几幅,而这正是构建这个项目奖励的工作。每一次运行的每一幅画,连同它的草图和奖励,都在 rollouts 数据集中,并可在这个画廊中浏览。

由于奖励部分基于我的品味,以我作为观看者的评判来收尾是公平的。在我看来,judge-led

是最终最多样、在艺术上最有趣的运行。hps-led

画出了令人信服的水彩画,但它最好的作品共享一种柔和、湿画法的外观,几乎自成一种风格。hps-only

收敛得最厉害,它的大多数画作都落在相同的颜色上。你可以在画廊中自行判断,那里有每一次运行的每一幅画,可按步骤和奖励排序。

这个项目主要是基础设施。一次运行需要一个训练器、两个 Space、一个推理路由器和 websocket,才能连续数小时保持健康,而每一个悄悄失败的部件都会在别处变成一个错误的数字。一半的工作是检查你读到的数字是否与实际发生的情况相符。

基础设施的故障正以零的形式进入奖励。 一次超时的渲染或一个没有应答的评分器,与一幅糟糕的画作得分相同,在组内为 0.0。在我所有的运行中,这大约占 rollouts 的 1.5%,而在最差的一次运行中,达到了 5.2%。这会让模型在噪声上训练,所以这些路径现在

返回 None

,该 rollout 被排除在组外。

我还在 OpenEnv 中发现了一个 bug,并将修复提交到了上游。 客户端保持一个

持久 websocket,而被远端关闭的 socket 仍被缓存,因此之后每次

调用都失败,即使环境是健康的。我花了两次半途而废的运行

才发现它。修复已[提交

上游](https://github.com/huggingface/OpenEnv/pull/1103),用它启动的运行

此后一直运行干净。

judge-led

运行,各取最好的四幅。第 12 步的画作是否看起来差了半分,由你决定。一步的奖励取决于它抽到了哪些参考。 成对评判器

每步采样四个参考,所以每一步面对一组不同的对手,而有些抽取就是更难。GRPO 本身大多是安全的,因为优势是在组内

计算的,一次困难的抽取会让整个组一起移动。我

当时读的曲线并不安全,一些看起来像糟糕步骤的东西只是困难的抽取。

上图就是一个例子。第 12 步比第 11 步低了半分,主要是因为

它抽到了这次运行中最难的参考,而画作本身看起来差不多。

四舍五入的数字,且仅针对已完成的运行。

组件 所需资源
训练器 1 块 H200。60 步需 18 小时,110 步约需 34 小时
HPSv3 一个 a100-large Space,在整个运行期间保持在线
环境 一个 cpu-upgrade Space,可以及时完成渲染
成对评判器 Qwen/Qwen3-VL-30B-A3B-Instruct 的 Inference Providers 配额
素材池,一次性 来自 iNaturalist 的开放许可照片、四个生成器的 Inference Providers 配额,评分则需你亲自投入时间

一步是八次 rollout,耗时十五到十八分钟,其中 **70% 到 80% 是

渲染**。单次渲染耗时 69 到 96 秒,而截止时间是 90 秒。部分原因是

意料之中的:这个 Space 没有 GPU,所以 Chromium 用软件方式渲染 WEBGL 画布,而

p5.brush 的晕染和纹理是繁重的像素运算。即便如此,我原本以为它会

更快,而我至今没有找到全部原因。

评分器的开销可能超过使用它的训练:HPSv3 必须在整个运行期间保持在线,所以运行结束时请暂停该 Space,或设置它的休眠计时器。

一切都运行在 [HF

Jobs](https://huggingface.co/docs/huggingface_hub/guides/jobs) 上,环境是一个

Docker Space,指标记录在

trackio 中。

这个项目的原则是在所有资源开放的前提下复现该配方,而不是改进它,所以一路上积累了一堆未尝试的想法。以下是我真正会去尝试的那些,按证据多少排序。

多步,并让模型看到自己画的东西。 这是我会尝试的第一件事。原博客

训练的是单轮,所以我也训练单轮,而在这种

设置下,模型是闭着眼睛作画的。没有任何图像输入,它得到的唯一

反馈就是一个数字。反馈循环有效的证据就是素材池

本身。参考画作来自模型在视觉评判器下迭代三轮的产物,后面的轮次

更好。定义奖励的素材,是用策略从未获得过的循环制作出来的。

更小的模型。 有证据表明 35B 超出了所需。在我的侧

实验中,一个 4B 模型已经能写出通过门槛的有效草图。如果一个 4B 能学会

这件事,实验成本就会下降一个数量级。

清单上的其他想法还有:在开始 RL 之前对素材池来源做 SFT、显式地奖励颜料

使用、随着运行推进把评判器的参考混合从简单逐步移到困难,直到

只剩下 love

、放宽十种方法的允许列表以获得更大的视觉范围(我在这方面的

尝试让更多草图崩溃,并破坏了水彩观感),以及通过对同一张图像评分两次来检查成对评判器到底有多一致。

而且这个方法并不专属于花卉。[Alex Yango 用同样的

机制画了动物](https://x.com/alexyango/status/2091696296931574217),[Brendan Hogan

训练了画布动画](https://x.com/brendanh0gan/status/2092650655789855222)

,对照的是一批人工评分的片段。我之前还用 [Simon

Willison 的鹈鹕

基准](https://huggingface.co/blog/sergiopaniego/pelican-env-openenv) 玩过类似的东西,那里代码会被渲染成图像并评分。

而在这一切之下,是这个项目无法解答的问题。**由模型创作的 178 幅画作

定义了这个训练后的模型认为什么是美的。** 素材池是瓶颈,而它是整条流水线中

没有原则性答案的那一部分。

对于任何复现此工作的人,有两处与 Narreddi 的配方有意偏离,这两点已在文章前面讨论过:

love

以及一半来自 okay

,而不是

仅与顶级层级比较,因此早期较弱的策略仍能获得信号。其余部分,LoRA 使用 all-linear

,基础设施故障返回 None

而不是 0.0,在第 110 步停止评判运行,这些是我在此过程中不得不做出的决定

因为他的博客没有具体说明。他的实现未公开,所以

我无法判断它们是与他的选择一致还是有所偏离。

artifact where
配方,以及如何复现
02-watercolour/

watercolour-reference-pool

watercolour-env

watercolour-hpsv3

watercolour-grpo-hps-only

watercolour-rollouts-hps-only

watercolour-grpo-judge-led

watercolour-rollouts-judge-led

watercolour-grpo-hps-led

watercolour-rollouts-hps-led

watercolour-gallery

judge-led

hps-led

hps-only

results/

本文中每次 rollout 的数字可以从已发布的数据集重新计算。这里没有任何内容依赖于任何 Space 保持开启。

方法和原始想法属于 Surya Narreddi。库属于 Alejandro Campos Uribe。

每次运行的每幅画,以及创作它的草图

显示轨迹信息和可视化

显示交互式轨迹可视化

显示交互式轨迹可视化