用 TRL 和 OpenEnv 训练一个会画水彩画的编码模型
英文标题:Training a coding model to paint watercolours with TRL and OpenEnv
Hugging Face 工程师 Adithya S K 开源了一个完整的强化学习配方,用 TRL 和 OpenEnv 训练语言模型编写 JavaScript 代码绘制水彩画,并公开了所有代码、数据和模型。
为什么值得关注
这项工作的意义在于,它展示了如何用强化学习训练模型生成具有审美价值的艺术作品,而不是仅仅追求像素级准确。它把“品味”编码进奖励函数,让模型学会模仿人类偏好,而不是平均化的完美。这为生成式 AI 艺术开辟了新方向,也让普通人能通过调整参考图池来定制模型的风格。
核心要点
- 开源了完整的强化学习配方,包括训练脚本、环境、参考图池和模型。
- 奖励由 HPSv3 偏好模型和成对评判模型混合构成,参考图池定义了“美”的标准。
- 比较了三种奖励混合方案,发现成对评判模型能提升画作质量。
- 模型学会了绘制水彩画,但忽略了提示词中关于形状数量的指令。
- 基础设施故障会导致奖励为 0,作者修复了 OpenEnv 的一个 bug 并提交上游。
- 所有产物均公开,读者可一键复现。
8月23日,Surya Narreddi 发布了一段漂亮的视频,展示了一个语言模型画的水彩画。这个模型通过 p5.brush 编写 JavaScript——这是一个“为 p5.js 添加自然绘画工具”的库。这段视频迅速走红,截至本文写作时已有超过150万次观看。
视频附带了一篇博客文章,解释了该项目早期、范围更窄阶段的训练过程——画的是近距离的花朵特写,而不是视频中的完整构图,遗憾的是还没有公开的成品。他的网站说完整的技术报告即将发布,所以记得关注他。最初的创意来自他,源于艺术和设计领域,他的技能远超于我。我的尝试则是在工程方面,公开复现这个配方,并发布每一部分。
注意:想了解项目背景,由 Surya 本人讲述,请看他的论文视频。
在本文中,我尝试用 TRL 和 OpenEnv 复现他的想法。参考图池数据集、强化学习环境、训练脚本和训练好的模型,全部开源。
整个流程端到端运行在 Hugging Face 上:
- 在 Jobs 上训练
- 强化学习环境和评分模型作为 Spaces 部署
- 通过 Inference Providers 调用成对评判模型
- 所有产物都放在 Hub 上,汇总在一个集合里
一旦两个 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
本文的其余部分讲述的是如何走到这一步的故事,每一部分都在代码仓库里。
我一步一步地跟随了原始博客,只有在绝对必要时才做改动。我自己的每个想法都记在清单里,而不是直接加进实验,这份清单变成了文末的“我接下来想尝试什么”,旁边是已发布产物的完整列表。如果你已经读过他的文章,框架和奖励设计会感觉很熟悉。新的内容在于开源实现、人工评分的参考图池,以及三种训练并比较的奖励混合方案,从你需要构建的强化学习环境开始。
为什么大家喜欢它
这些画看起来松散、不完美、有手工感,而当下图像模型产出的是完美(统计上平均)的图片。我猜这种对比是视频走红的重要原因。这让我想起生成式 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),奖励明显在提升。
用这个配置,三次运行都能学习。两次带评判模型的运行都启动了200步,在110步时停止,奖励仍在缓慢上升。一步需要十五到十八分钟,混合方案之间的比较已经稳定,所以我停止了它们以节省算力。每次运行前三分之一和后三分之一的组平均奖励:
三条曲线与我的品味所占权重一致。评判模型权重越大,起点越低,爬升越嘈杂。judge-led 在前三十步几乎持平才动起来。这和解决调试问题用的是同一个动作:把问题缩小到能学习为止,然后一次加回一个难点。
成对评判模型这一项在两次使用它的运行中确实在上升。随着训练推进,模型在图池对比中赢得的比较更多,这是仅 HPS 无法声称的。没有任何运行中的组崩溃到相同奖励——这是会杀死梯度的 GRPO 失败模式。好奇的话,每个指标(HPSv3、颜料覆盖率、熵)的曲线都在仓库的 CSV 文件里。
完整的启动命令、硬件配置,以及把这次运行变成另外两次运行的两个环境变量,都在配方里。
它实际学到了什么
在每次运行中,模型学到的第一件事是停止产出糟糕的画——那些近乎空白的画布和无形状的水洗,总分低于0.3。在仅 HPS 中,组均值上升的四分之三来自坏画变得稀少。在带评判模型的运行中,下降更陡峭:低于0.3的 rollout 在 judge-led 的三分之一段从99降到16,在 hps-led 中从37降到4。
这就是为什么最明显的视觉——每一步最好的画——在仅 HPS 中几乎看不出差别。它在整个运行中只移动了 +0.034,而中位数移动了 +0.155。学习发生在分布的中段。
成对评判模型改变的是顶端。在仅 HPS 中,画变得更可靠但没有变得更好。好画的质量只给组均值加了 +0.03,一旦 HPSv3 看到中心有花瓣和一根茎,它就不再要求更多颜料了。有了评判模型,故事的另一半出现了。这里的“更好”意味着更接近图池,也就是更接近我评为“好”或我更喜欢的东西。这在 judge-led 中加了 +0.12,在 hps-led 中加了 +0.16,每一步的最佳画作也在上升,颜料覆盖率在两次运行中都翻倍了(0.11到0.23,以及0.13到0.30),而仅 HPS 几乎没动它。只要还有一个参考可以超越,好画还能变得更好,模型开始因为使用更多颜料而获得奖励。
还有一个发现。模型忽略了一条明确的指令,而且它是对的。系统提示词要求十五到三十个填充形状。如果我们看实际均值,它在7到9之间,而且 n_shapes 在任意运行中与奖励几乎没有相关性(+0.000、−0.14、+0.07)。策略不会因为遵守那句话而获得奖励,所以它不遵守。
仅 HPS 路线也有一个上限。如果每次 rollout 都达到它好画的水平,那次运行的均值会停在0.771。更多步骤是否能突破它,这是一个开放问题。
画作还展示了表格遗漏的东西。在每次运行内部,它们看起来都很相似。随着训练推进,每组内部的奖励越来越接近,开场视频中的中位数画作看起来像同一朵花的多个版本。这是 GRPO 在做它设计要做的事——用一个基于单一主题构建的图池。图池决定什么算多样性,就像它决定什么算质量一样。如果奖励只奖励匹配一朵花,模型就学会画那一朵花。更多样的输出需要更多样的图池,而构建一个这样的图池需要更多策展工作。Surya 更新的构图就是一个例子。Alex Yango 的动物画是同一个配方,只是图池中的选择不同。这是审美奖励和数学评分器之间最大的区别。在数字背后有一项非常人性化的工作:决定什么该放进奖励集。Jason Liu 关于品味的文章用一句话概括了通用版本。AI 把瓶颈从“制作”转移到了“发现”。
Surya 在博客结尾分享了他的一些最爱。我不选我的,下面是一面墙,展示两次带评判模型运行中奖励最高的178幅画,和参考图池的数量一样,没有特定顺序。打开它,选你自己的。
要选择,你看了很多然后保留了几幅,这正是构建这个项目奖励的工作。每次运行的每一幅画,连同它的草图和奖励,都在 rollout 数据集中,可以在这个画廊里浏览。
因为奖励部分基于我的品味,以我作为观众的评价来结尾是公平的。在我看来,judge-led 是最终最多样、艺术上最有趣的运行。hps-led 画出了令人信服的水彩画,但它最好的作品共享一种柔和的湿画法外观,几乎自成一种风格。仅 HPS 收敛得最厉害,它的大多数画都落在相同的颜色上。你可以在画廊里自己判断,那里有每次运行的每一幅画,可以按步骤和奖励排序。
基础设施很难
这个项目主要是基础设施。一次运行需要一个训练器、两个 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能学会这个,实验成本会下降一个数量级。
清单上的其他想法包括:在开始强化学习之前对图池来源做 SFT、显式奖励颜料使用、随着运行推进把评判模型的参考混合从容易到难直到只剩下“喜欢”、扩大十个方法的白名单以获得更多视觉范围(我在这方面的尝试让更多草图崩溃并破坏了水彩效果),以及通过给同一张图打两次分来检查成对评判模型到底有多一致。
而且这个方法并不只适用于花朵。Alex Yango 用同样的机制画了动物,Brendan Hogan 则用一批人工评分的片段训练了画布动画。我之前也玩过类似的东西,用的是 Simon Willison 的 pelican 基准测试,就是把代码渲染成图片再打分。
而在这一切背后,有一个这个项目无法解答的问题。模型创作的 178 幅画定义了它认为的美。这个样本池是瓶颈,也是整个流程中最没有原则性答案的部分。
我改动的地方
对于想复现的人来说,我对 Narreddi 的方法做了两处刻意的偏离,前面文章里都讨论过:
-
成对评判器(pairwise judge,即比较两幅画选更好的工具)的参考样本一半来自“喜欢”类,一半来自“还行”类,而不是只跟最高档比,这样早期较弱的策略也能得到反馈信号。
-
提示词里加了一句小技巧:每片花瓣画两三次,先画一大笔,再在里面画一笔更小、更不透明的。
其余部分,比如 LoRA 的全线性设置、基础设施故障时返回 None 而不是 0.0、在第 110 步停止评判器运行,都是我在过程中不得不做的决定,因为他的博客没写清楚。他的实现没有公开,所以我无法判断这些是跟他的选择一致,还是有所偏离。
一切都已公开
这篇文章里的每次运行数据都可以从公开的数据集重新计算。这里没有任何东西依赖某个 Space 保持在线。
方法和原始想法归功于 Surya Narreddi。库是 Alejandro Campos Uribe 写的。
本文提到的模型 10 个
本文提到的数据集 4 个
本文提到的 Spaces 6 个
本文提到的论文 1 篇
本文提到的收藏集 1 个
更多博客文章
社区
本文提到的模型 10 个
本文提到的数据集 4 个
本文提到的 Spaces 6 个
本文提到的论文 1 篇
本文提到的收藏集 1 个
术语表
- TRL
- Hugging Face 的强化学习库,用于训练语言模型,支持 GRPO 等算法。
- OpenEnv
- Hugging Face 的强化学习环境库,用于定义环境与模型的交互。
- GRPO
- 一种强化学习算法,通过组内相对奖励计算优势,避免梯度崩溃。
- LoRA
- 低秩适配,一种参数高效微调方法,只训练少量附加参数。
- HPSv3
- 一个开源的 7B 偏好模型,根据人类偏好对图像打分。
- 成对评判模型
- 一个视觉语言模型,通过比较候选画作与参考图池来打分。
- p5.brush
- 一个 p5.js 库,模拟自然绘画工具,如晕染、纹理等。
- 参考图池
- 一组人工评分的画作,用于定义奖励标准。
- GEPA
- 自动提示词优化器,用语言模型反思并改进提示词。
- 混合专家模型
- 一种模型架构,包含多个专家网络,每次只激活部分专家。
生产区
使用这篇文章
复制包含 frontmatter、中文正文和英文来源的 Markdown。
我的研究区
记录自己的判断,不会写回原文或公开页面。
一句话说清这篇材料能支撑什么角度。
每行一个,最多 20 个。
支持 Markdown。要核对的数据、可复用的方法、反方观点或补充来源。