用 TRL 和 OpenEnv 训练编码模型画水彩
Training a coding model to paint watercolours with TRL and OpenEnv
Hugging Face 博客介绍了一个用 TRL 和 OpenEnv 训练编码模型绘制水彩画的项目:模型输出约 150 行 JavaScript 代码,通过 p5.brush 库作画,提示词只允许使用该库 47 个方法中的 10 个。奖励由 HPSv3 开放 7B 偏好模型与 Qwen3-VL-30B-A3B-Instruct 成对评判器共同给出,后者将候选画作与随机抽取的 4 张参考图比较,共训练了三次不同权重配比的运行。
正文 · AI 翻译
为什么人们喜欢它
这些画看起来松散、不完美、带有手工感,而当下图像模型产出的却是完美(统计意义上平均)的图片。我猜这种反差正是这段视频走红的重要原因。它让我想起生成式 AI 艺术的早期,那时重点在于探索这种媒介。DeepDream(2015)本是一个调试工具,人们把它变成了艺术;像 Edmond de Belamy(2018)这样的作品,源自艺术家对 GAN 能力的探索;而 Mario Klingemann 等艺术家则在那几年里用神经网络创作梦幻般的肖像。
这个项目感觉更接近那些早期岁月。Surya 在论文中描述了通向这里的路径。他从给文本到图像模型写提示词开始,在那里提示词是你唯一能拉动的杠杆,增加细节只能在一定程度上换来更多控制。训练模型本身则更进一步。这个想法的另一半是媒介。模型会写出一段约 150 行的 JavaScript 程序来绘制图像。模型的输出就是代码。你可以阅读它、编辑它并再次运行它,每一笔背后的决策都清晰可见。而风格则来自一项限制:模型只被允许使用该库十个方法。详见下文。
在同一时期,Anna Ridler 拍摄了数千朵郁金香,为每一朵手工打标签,把数据集本身作为艺术品展出,后来又在其上训练了一个模型。我是在构建这个项目时通过 AI 智能体带回的参考文献发现她的作品的,我很喜欢它,因为这个项目做的事情非常相似:手工策划一组图像,然后针对它们进行训练。
基于品味的强化学习
近期大多数针对语言模型的强化学习工作都使用可验证的奖励。例如答案已知的数学题、能通过测试的代码,或者判断对错且运行成本低廉的评分器。这个项目更接近较早的例外——RLHF,即模型从人类偏好中学习一个奖励模型。
在这里,奖励是审美偏好。没有正确答案。这个项目真正要问的是:你能否针对品味做强化学习。
奖励的定义,正如他的博客所定义、也正如我构建的强化学习环境所实现的那样:
HPSv3 是一个开放的 7B 偏好模型。给它一张图像和一段文本描述,它会返回一个分数,表示人们会有多偏好这张图像。它是在大量人类对图像对的成对选择上训练的,因此它的分数是许多人品味的平均值。成对评判器是 Qwen3-VL-30B-A3B-Instruct,一个通过 HF Inference Providers 调用的通用视觉模型。成对评判器会看到候选画作,以及从池中随机抽取的四张参考图,并以一段书面描述来指导它权衡哪些方面(晕染、半透明淡彩、柔和边缘),每次比较都以两种呈现顺序各进行一次,其分数是候选画作在比较中获胜的比例。它唯一的标准就是那个池子,所以它的分数就是我的品味,以那些评分为载体编码其中。
这些就是 Narreddi 最终收敛到的权重。在这里,池子定义了品味。这就把工作从调超参数转变为构建那个决定何为美的集合。
我用这个奖励训练了三次运行。它们唯一的区别在于两个模型评判器之间的权重分配:
我先用仅 HPS 的设置来验证整个流程是否至少能学习。一旦奖励开始上升、各项指标健康,就没有理由再让它跑更久,于是我转而启动了两个更长的运行。更长的运行要问的问题是:你能把 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。其余三十七个方法所添加的功能——线条、排线、自定义笔刷——会破坏水彩效果。有了这十个方法,模型只能绘制填充形状,而库会为每一个形状添加渗色效果。
他的博客文章为我节省了大量时间,否则我可能会浪费在反复迭代提示词上。冗长的 API 参考会让模型发明不存在的方法,而他 200 次 GEPA 迭代收敛到了一个没有文档的严格允许列表。我看到了同样的失败,于是手动编写了允许列表。我对那个配方唯一的补充是一句话:把每片花瓣画两到三次,先画一大笔,然后在里面画一笔更小、更不透明的。这个小小的改动让我的输出色彩丰富了很多。
注:如果你是第一次听说 GEPA,它是一个自动提示词优化器。一个语言模型用平实的语言反思当前提示词在哪里失败了,并提出一个更好的提示词,然后循环重复。
关卡是最后一块拼图。草图必须能够编译,使用该库而不是直接调用 p5,在画布上留下真正的颜料,并且不能试图欺骗评分器,例如在画布上写文字。
池子就是奖励函数
池子由 178 幅画组成,根据我的个人偏好分为两个层级:喜欢和还行。它们实际上都是由模型生成的。四个开放权重模型通过 Inference Providers 调用,编写 p5.brush 草图,每个模型都基于一张来自 iNaturalist 的真实、开放许可的朱槿照片进行创作。一个视觉模型对每张草图给出了书面反馈,经过三轮细化迭代。然后每一张最终渲染图都由我逐一评分,178 幅入选。
在这里,我选择了四个不同的模型家族来测试它们不同的风格。这四个是在快速可靠性检查中每次都能生成有效草图的开放模型,另外两个候选模型因未通过而被淘汰。如果你想创建自己的池子,你可能会选择其他模型。
这些层级在奖励中发挥着实际作用。当成对评判器抽取四张参考图时,一半来自喜欢,一半来自还行,因此策略总是面对一些它有时能击败的对手,而且无论对哪个层级,获胜的报酬都是一样的。这是我有意做出的少数改动之一:原始版本只与它的最高层级比较,而我保留了较容易的层级参与抽取,这样弱小的早期策略仍然能获得信号。
其中没有人类创作的画作,这是一个真正的局限。p5.brush 是一个小众库,其中存在且代码可获取的人类作品只有寥寥几件,远远达不到训练语料库所需的规模,他的博客也指出了这一点。
正如我之前已经讨论过的,这里有趣的想法是模型会学会模仿池子中包含的内容。如果我们把环境指向不同的数据集,奖励就会自动改变,而无需改动一行代码。对于我生成并公开分享的数据集,我也包含了源草图。
如果我们更仔细地观察评判器,这两个评判器回答的是不同的问题。HPSv3 判断它是不是一朵花,而成对评判器判断它是否以我选择的风格画得好。
再来一次 yolo 运行
在任何东西奏效之前,有一段很长的奖励曲线平坦期。如果你曾尝试在没有开放产物的情况下复现一篇研究论文/博客,你大概能感同身受。每次运行都检验了我认为关于问题所在的一个合理理论。一次运行需要大量时间,所以我还在查看上一次运行结果时,就已经排好了下一次运行。一如既往,从更简单且有效的东西开始,然后在此基础上构建,才是答案。一个简单的控制任务,没有浏览器也没有评判器,是第一个学会的尝试,原因是我的学习率太低了。
另一个耗费我时间才发现的改动是正确调整 LoRA 参数。通常的 target_modules 列表假设的是稠密模型,而 Qwen/Qwen3.5-35B-A3B 是一个专家混合模型,它大多数投影层的命名都不同,所以适配器只训练了四十层中的十层。我通过将其改为 all-linear 解决了这个问题,这会触及每一个线性层。这个架构中的路由专家是融合张量,即使 all-linear 也会让它们保持冻结,但其他所有部分都获得了适配器,这就足以学习了。
修复方案是对 TRL 的 GRPOTrainer 做了四处改动:
这四处改动解锁了第一次成功的运行(hps-only),奖励明显提升。
在这种配置下,三次运行都学到了东西。两次评判器运行都启动了 200 步,并在 110 步时停止,此时奖励仍在缓慢攀升。一步需要十五到十八分钟,而不同混合配比之间的比较已经稳定,所以我停掉了这两次运行以节省算力。每次运行前三分之一和最后三分之一的平均组奖励:
三条曲线与我的品味所占权重的多少相吻合。评判器权重越大,起点越低,攀升也越嘈杂。judge-led 的前三十步几乎持平,之后才开始移动。这与解决调试问题用的是同一招。把问题缩小到某个东西能学会为止,然后一次一个地把困难的部分加回来。
成对评判器项本身在两次使用它的运行中都上升了。随着训练推进,模型在对阵池子时赢下更多比较,这正是 hps-only 无法做出的断言。任何运行中都没有组坍缩到完全相同的奖励,那是会杀死梯度的 GRPO 失败模式。给好奇的人:各指标曲线(HPSv3、颜料覆盖率、熵)以 CSV 形式放在仓库里。
完整的启动命令、硬件,以及把这次运行变成另外两次运行的两个环境变量,都在配方里。
它实际学到了什么
在每次运行中,模型学到的第一件事就是停止产出糟糕的画作,即那些总奖励低于 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 的动物画作是同一配方,只是在池子里做了不同选择。这是审美奖励与数学评分器之间最大的区别。数字背后有一项非常人性化的工作:决定什么该属于奖励集。Jason Liu 关于品味的那篇文章用一句话概括了普遍版本。AI 把瓶颈从制作转移到了注意。
Surya 用他的一些最爱来结束他的博客。我没有挑选自己的,而是下面放了一面墙,展示两次评判器运行中奖励评分最高的 178 幅画作,与参考池所容纳的数量相同,不分先后顺序。打开它,挑选你自己的。
为了选择,你看了很多,留下了少数,而这正是构建这个项目奖励的那项工作。每次运行的每幅画作,连同其草图和奖励,都在 rollouts 数据集中,并可在这个画廊中浏览。
由于奖励部分基于我的品味,作为观众以我的评判来收尾是合理的。在我看来,由评判器主导的那次运行最终呈现出最多样、也最具艺术趣味的作品。hps-led 画出了令人信服的水彩,但它最好的作品都带有一种柔和、湿画法(wet-on-wet)的观感,几乎自成一派。hps-only 收敛得最厉害,它的大多数画作都落在相同的颜色上。你可以在画廊里自行判断,那里有每次运行的每一幅画,可按步数和奖励排序。
基础设施很难
这个项目主要是基础设施。一次运行需要一个训练器、两个 Space、一个推理路由器和一条 websocket,才能连续数小时保持健康,而每一个悄悄失败的环节都会在别处变成一个错误的数字。一半的工作是检查你读到的数字是否与实际发生的情况相符。
基础设施的故障曾以零的形式进入奖励。一次超时的渲染或一个没有应答的评分器,与一幅糟糕的画作得分相同,在组内都是 0.0。在我所有的运行中,这大约占 rollout 的 1.5%,而在最差的一次运行中达到了 5.2%。这等于用噪声训练模型,所以这些路径现在返回 None,该 rollout 被排除在组外。
我还在 OpenEnv 中发现了一个 bug,并把修复提交到了上游。客户端保持一条持久的 websocket,而被远端关闭的 socket 仍被缓存,导致之后每次调用都失败,尽管环境是健康的。我花了两次半途而废的运行才发现它。修复已提交到上游,用该修复启动的运行此后一直运行正常。
一步的奖励取决于它抽到了哪些参考图。成对评判器每步采样四张参考图,所以每一步面对的对手组合都不同,有些抽样本身就更难。GRPO 本身基本是安全的,因为优势是在组内计算的,一次困难的抽样会让整个组一起移动。我当时看的曲线并不安全,一些看起来像糟糕一步的东西其实只是困难的抽样。上图就是一个例子。第 12 步比第 11 步低了半分,主要是因为它抽到了这次运行中最难的参考图,而画作本身看起来差不多。
成本是多少
四舍五入的数字,且仅针对已完成的运行。
一步是八个 rollout,耗时十五到十八分钟,其中 70% 到 80% 是渲染。单次渲染耗时 69 到 96 秒,而截止期限是 90 秒。其中一部分是意料之中的,Space 没有 GPU,所以 Chromium 用软件渲染 WEBGL 画布,而 p5.brush 的晕染和纹理是繁重的像素工作。即便如此,我本以为它会更快,而我还没有找到全部原因。
评分器的成本可能超过使用它的训练:HPSv3 必须在整个运行期间保持在线,所以运行结束时暂停该 Space,或设置它的休眠计时器。
一切都在 HF Jobs 上运行,环境是一个 Docker Space,指标在 trackio 中。
我接下来会尝试什么
这个项目的规则是在所有资源开放的情况下复现该配方,而不是改进它,所以一路上积累了一堆未尝试的想法。以下是我真正会尝试的,按证据多少排序。
多步,并让模型看到它画了什么。这是我会尝试的第一件事。原博客训练的是单轮,所以我也训练单轮,而在这种设置下,模型是闭着眼睛作画的。没有任何图像输入,它得到的唯一反馈是一个数字。反馈循环有效的证据就是池子本身。参考画作来自模型在视觉评判器下迭代三轮的产物,而后几轮更好。定义奖励的材料是用一个策略从未获得的循环制作出来的。
更小的模型。有证据表明 35B 超出了所需。在我的侧实验中,一个 4B 模型已经能写出通过门槛的有效草图。如果 4B 能学会这个,实验成本就会下降一个数量级。
清单上的其他想法包括:在开始 RL 之前对池子来源做 SFT、显式奖励颜料、随着运行推进把评判器的参考组合从易到难移动,直到只剩下 love、放宽十种方法的允许列表以获得更大的视觉范围(我在这方面的尝试让更多草图崩溃,并破坏了水彩观感),以及通过给同一张图像打分两次来检查成对评判器到底有多一致。
而且这个方法并不专属于花。Alex Yango 用同样的机制画了动物,Brendan Hogan 针对一批人工评分的片段训练了画布动画。我之前还用 Simon Willison 的 pelican 基准玩过类似的东西,那里代码被渲染成图像并打分。
而在这一切之下,是这个项目无法回答的问题。178 幅由模型创作的画作定义了训练后的模型认为什么是美的。池子是瓶颈,而它是流水线中没有原则性答案的那一部分。
我相对于原版改了什么
对于任何要复现这项工作的人,这里有两处与 Narreddi 的配方有意不同,这两点本文前面都已论证过:
成对评判器的参考图一半取自 love、一半取自 okay,而不是只与最高档比较,这样即使早期策略较弱也仍能获得信号。
提示词中有一句小小的技巧性描述:把每片花瓣画两到三次,先画一大笔,再在内部画一笔更小、更不透明的。
其余部分——LoRA 采用全线性、基础设施故障返回 None 而不是 0.0、在第 110 步停止评判器运行——都是我在过程中不得不做出的决定,因为他的博客没有说明这些。他的实现没有公开,所以我无法判断它们与他的选择一致还是不同。
一切都已公开
本文中每次 rollout 的数字都可以从已发布的数据集重新计算。这里没有任何内容依赖于某个 Space 保持开启。
方法与原始想法属于 Surya Narreddi。该库属于 Alejandro Campos Uribe。
本文提到的模型 7
MizzenAI/HPSv3
图像-文本到文本 • 更新于 2025年8月13日 • 300 • 38 Qwen/Qwen3-Coder-Next 文本生成 • 80B • 更新于 2月3日 • 599k • 1.67k Qwen/Qwen3-VL-30B-A3B-Instruct 图像-文本到文本 • 31B • 更新于 2025年11月26日 • 426k • 609 Qwen/Qwen3.5-122B-A10B 图像-文本到文本 • 125B • 更新于 4月24日 • 446k • 624 Qwen/Qwen3.5-35B-A3B 图像-文本到文本 • 36B • 更新于 4月24日 • 1.6M • 1.52k moonshotai/Kimi-K3 图像-文本到文本 • 2.8T • 更新于 30 天前 • 1.27M • 11.6k zai-org/GLM-5.2 文本生成 • 753B • 更新于约 1 个月前 • 731k • 5.14k 本文提到的论文 1 GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning 论文 • 2507.19457 • 发布于 2025年7月25日 • 36 更多来自我们博客的文章
用 100 步 GRPO 微调 350M 模型以获得更好的结构化输出
143 2026年9月3日 llm rl trl 跨 HF Jobs 使用 LoRA 的异步 GRPO:一个桶、一个代理,没有 NCCL 53 2026年9月10日 社区 干得好 \n","updatedAt":"2026-09-14T03:27:18.564Z","author":{"_id":"61b71cca7b0e8fac33cb0105","avatarUrl":"/avatars/97c3c64f0d266a521a2605178cfbe66d.svg","fullname":"blake martin","name":"blakegger","type":"user","isPro":false,"isHf":false,"isHfAdmin":false,"isMod":false,"followerCount":1,"isUserFollowing":false}},"numEdits":0,"identifiedLanguage":{"language":"en","probability":0.9447146058082581},"editors":["blakegger"],"editorAvatarUrls":["/avatars/97c3c64f0d266a521a2605178cfbe66d.svg"],"reactions":[],"isReport":false}},{"id":"6aba5ac0097303f1f02d8ade","author":{"_id":"67c0a75e8a31e5840dc10ac4","avatarUrl":"https://cdn-avatars.huggingface.co/v1/production/uploads/noauth/4lXIBgkbcKDx83-H7sjiA.jpeg","fullname":"shubham giri","name":"xshubhamg","type":"user","isPro":false,"isHf":false,"isHfAdmin":false,"isMod":false,"isUserFollowing":false},"createdAt":"2026-09-28T12:17:04.000Z","type":"comment","data":{"edited":false,"hidden":false,"latest":{"raw":"Awesome this is so COOL","html":" Awesome this is so COOL \n","updatedAt":"2026-09-28T12:17:04.422Z","author":{"_id":"67c0a75e8a31e5840dc10ac4","avatarUrl":"https://cdn-avatars.huggingface.co/v1/production/uploads/noauth/4lXIBgkbcKDx83-H7sjiA.jpeg","fullname":"shubham giri","name":"xshubhamg","type":"user","isPro":false,"isHf":false,"isHfAdmin":false,"isMod":false,"isUserFollowing":false}},"numEdits":0,"identifiedLanguage":{"language":"en","probability":0.875470757484436},"editors":["xshubhamg"],"editorAvatarUrls":["https://cdn-avatars.huggingface.co/v1/production/uploads/noauth/4lXIBgkbcKDx83-H7sjiA.jpeg"],"reactions":[],"isReport":false}}],"status":"open","isReport":false,"pinned":false,"locked":false,"collection":"community_blogs"},"contextAuthors":["sergiopaniego"],"primaryEmailConfirmed":false,"discussionRole":0,"acceptLanguages":["en","zh"],"withThread":true,"cardDisplay":false,"repoDiscussionsLocked":false,"hideComments":true}"> blakegger 17 天前 干得好
回复 xshubhamg 3 天前 太棒了,这太酷了
回复 编辑 预览 通过将文本拖入输入框、粘贴或点击此处来上传图像、音频和视频。点击或粘贴到此处上传图像 评论 · 注册或登录以发表评论
+64
本文提到的模型 7
MizzenAI/HPSv3
Qwen/Qwen3-Coder-Next
本文提到的论文 1