引言:一个看似简单的想法

“红色,深一点,再偏蓝一些。”

当我试图让计算机理解这句话时,以为这会是件简单的事情——就算不简单,也不会太难。

我的想法很理所当然:颜色词是有限的,修饰词也是有限的。把它们的关系建立起来,训练一个模型,应该就可以了——最多,再加上一些修饰组合的不同方式。这和让AI写文章、理解代码什么的相比,要简单得多。颜色是可以精确量化的,还有那么多量化空间可以选择,不像语言和代码那么模糊。

但当我真正开始做的时候,才发现事情远远没有这么简单。

"朱红"应该是什么?一个颜色空间的三维坐标。
"红"和"深红"的关系呢?两个独立的颜色?还是一个基准颜色加上修饰?
"比刚刚柔和一点"时,怎么让模型知道什么是"刚刚"?

更让我没想到的是,这些看似简单的问题,最终会让我面临一些更根本的选择:哪个颜色空间?什么模型架构?让模型学什么,怎么学?同时,有什么又是不需要模型学习的?当“符合人类直觉”和“数学上正确”产生冲突时,应该如何选择?

这篇文章记录了这个过程——不是成功的经验总结,而是充满怀疑、反复推倒重来的探索轨迹。

这是《溯源 • 求索 • 笃行》系列的第二篇。
心潮涌链》让我理解了语言模型推理的本质,现在...该把它用起来了。

初心与直觉:RYB的美好愿景

从最朴素的想法开始

项目的起点来自一个朴素而强烈的直觉:既然“蓝+黄=绿”是如此符合人类经验,为什么不基于RYB色轮构建一个颜色语义空间呢?

RYB(红黄蓝)色轮是传统艺术教学中的经典模型,它将复杂的色彩世界简化为三个"原色"的组合关系。在这个模型中:

  • 红+黄=橙
  • 黄+蓝=绿
  • 蓝+红=紫

这些规律深深印在每个人的认知中,从幼儿园的美术课到专业的色彩理论,RYB都被当作理解颜色的入门钥匙。

初始设想的优雅之处

  1. 符合人类经验:任何人都能理解"偏蓝的红"意味着什么
  2. 数学结构简洁:三维空间,线性组合,几何直观
  3. 文化语义丰富:中文颜色词汇与RYB概念天然对应

我设想中的系统是这样的:

中文颜色词 → RYB坐标 → 语义理解 → 调整操作 → 新的RYB坐标 → 输出颜色

在这个框架中,“深蓝”可以表示为RYB(0, 0, 0.8),“偏红的蓝”可以表示为RYB(0.2, 0, 0.8),而“浅一点偏红的蓝”则是RYB(0.2, 0, 0.6)。一切都是如此自然和谐。

三阶段递进的系统设想

基于RYB的核心思想,我设计了一个三阶段递进的训练体系:

第一阶段:静态锚点建立

  • 输入:红色
  • 输出:RYB(1.0, 0, 0)
  • 任务:建立基础颜色词到RYB坐标的映射

第二阶段:修饰词理解

  • 输入:深红
  • 输出:RYB(0.8, 0, 0) # "深"意味着明度下降
  • 任务:理解修饰词对基础坐标的变换规则

第三阶段:自然语言调整

  • 输入对话:
    • 用户:红色
    • 系统:RYB(1.0, 0, 0)
    • 用户:深一点
    • 系统:RYB(0.8, 0, 0)
  • 任务:基于上下文进行相对调整

这个设想具有完整的逻辑闭环:从离散的词汇开始,逐步扩展到修饰组合,最终支持自然语言的动态交互。

而其底层的技术设想,是在一阶段让模型学习颜色的语义映射,形成一个合理的高维空间,能满足『黄 + 蓝 = 绿』这样的运算;二阶段时,模型学会基于某个基准色,应用某种偏移,得出新的颜色;三阶段则为更自然的语义学习,得出基准颜色和修饰列表,并把修饰应用于基准之上,得到最终颜色。

理想主义的技术路线

围绕RYB核心,我制定了一条看似完美的技术路线:

数据构建策略

  1. 收集中文传统色名(朱红、靛蓝、柠檬黄等)
  2. 基于RYB色轮为每个色名分配“直觉坐标”
  3. 添加修饰词规则(深=明度-0.2,亮=明度+0.2)
  4. 生成大量组合样本训练模型

模型架构设想

输入文本 → 编码器 → RYB预测器 → 后处理 → 显示颜色

预期效果

  • 用户说“蓝色”,系统理解为RYB(0, 0, 1)
  • 用户说“偏黄的蓝”,系统理解为RYB(0, 0.3, 1)
  • 用户说“再深一点”,系统调整为RYB(0, 0.3, 0.8)

一切都显得如此合理。RYB不仅符合人类直觉,还能提供简洁的数学结构和丰富的语义表达能力。

但现实很快让我意识到,事情没这么简单。

第一次认知冲击:荧光绿的挑战

当我开始具体构建RYB语料时,遇到了第一个严重问题:许多现代颜色在RYB系统中根本无法表达

最典型的例子是荧光绿——那种在电脑屏幕、霓虹灯、运动服装中随处可见的鲜艳绿色。在RGB空间中,它可能是(0, 255, 50)这样的极端值,但在RYB系统中,绿色只能通过蓝+黄混合得到,这种混合的结果永远不可能达到纯RGB绿色的饱和度。

类似的问题还有:

  • 电光蓝:RGB显示器特有的饱和蓝色
  • 洋红色:印刷中的重要颜色,RYB无法精确表达
  • 各种“网络流行色”:奶茶色、雾霾蓝、抹茶绿……

这些颜色在现代生活中极其常见,但RYB的"传统框架"无法容纳它们。当我试图为"荧光绿"分配一个RYB坐标时,我发现无论如何调整黄蓝比例,都无法得到令人满意的结果。

RYB本身就不是一个科学的颜色空间。它是基于颜料混合经验总结的近似模型,在物理上并不准确。现代色彩科学早已证明,真正的原色是RGB(光学)或CMY(印刷),而RYB只是一个教学简化。

第一次现实冲击:从RYB直接转向RGB

当“荧光绿”这样的现代颜色无法在RYB系统中准确表达时,我意识到继续在一个过时的颜色模型上花费精力是没有意义的。既然RGB是现代数字世界的标准,那就直接用RGB构建语料。

新的技术路线

中文颜色词 → 直接映射到RGB坐标 → 模型训练 → RGB输出

我开始重新构建语料:

朱红,220,20,60
天青,80,200,220  
象牙白,255,255,240
荧光绿,57,255,20    # 这在RYB中不可能表达

这种直接的方法看起来简洁明了,也能处理所有现代颜色。但很快我就发现了RGB的新问题……

摸索与调整:五次空间演化的认知螺旋

RGB:第一次现实碰撞

从RYB妥协到RGB后,我开始了第一次具体的语料构建。但很快发现RGB空间对于语义建模极不友好:

RGB的问题

  • 感知非均匀:RGB(100,0,0)和RGB(200,0,0)在数值上相差100,但视觉差异不等同于RGB(0,0,100)和RGB(0,0,200)
  • 语义不直观:很难直接从RGB值理解颜色的“亮度”或“饱和度”
  • 修饰词难以建模:什么叫“更亮的红”?R增加?G和B如何变化?

在RGB空间中建立语义规则变成了一件极其困难的事情。“深红”应该对应RGB的哪种变化?是所有分量等比缩放?还是只降低R分量?不同的策略会产生完全不同的视觉效果。

认知转变一:我开始意识到,颜色空间的选择不仅影响计算精度,更影响语义建模的可能性。技术选择需要同时考虑数学性质和认知结构。

HSV:艺术直觉的初步满足

RGB的困境推动我转向HSV空间。HSV(色相Hue、饱和度Saturation、明度Value)的三个维度直接对应了人类描述颜色的三个基本角度:

深蓝   → H不变,S不变,V降低
浅蓝   → H不变,S降低,V提高  
艳蓝   → H不变,S提高,V不变

这种对应关系让修饰词的建模变得直观许多。我开始构建基于HSV的语料:

颜色词,H,S,V,修饰词规则
红,0,100,100,深=V-20,浅=S-30&V+20
蓝,240,100,100,深=V-20,浅=S-30&V+20

HSV确实改善了语义建模的体验,但新的问题很快出现:

HSV的局限性

  • 感知不够均匀:HSV空间中的欧氏距离与人眼感知差异不够对应
  • 极值区域问题:当S=0时,H变得无意义;当V=0时,整个颜色变成黑色
  • 工业应用受限:设计和印刷行业更多使用基于CIE标准的色彩空间

当我开始查阅色彩科学文献时,发现HSV虽然直观,但在严肃的颜色管理应用中并不是首选。如果要构建一个真正实用的颜色语义系统,我需要更科学的基础。

认知转变二:我开始怀疑,是否存在一个既符合感知特性,又适合语义建模的统一空间

CIELab:科学精度的诱惑与语料断裂的发现

色彩科学的研究指向了CIELab空间——这是目前最接近人类视觉感知的标准化颜色空间:

CIELab的优势

  • 感知均匀性:空间中的欧氏距离直接对应视觉差异
  • 科学严谨性:基于严格的心理物理学实验制定
  • 工业标准:印刷、显示、纺织等行业的通用参考

我兴奋地开始了基于CIELab的语料构建,并成功完成了第一阶段的模型训练。结果令人鼓舞——模型能够准确地将“朱红”“天青”“象牙白”等颜色词映射到CIELab坐标,损失函数稳定收敛,可视化效果良好。

一阶段模型仓库:GitHub - color-model

第一阶段的成功让我充满信心地开始准备二阶段的工作。但当我开始构建修饰词语料时,遇到了一个意想不到的问题。

语料断裂

  • 一阶段语料:基础颜色词 → CIELab坐标的直接映射
  • 二阶段语料:修饰词+颜色词 → CIELab坐标的组合映射
  • 三阶段语料:完整自然语言描述 → CIELab坐标的语义理解映射

这两套语料在格式、复杂度、语义结构上完全不同,更关键的是会产生两个不同的词表文件。一阶段的词表只包含基础颜色词,二阶段在此基础上增加了修饰词,三阶段则需要容纳完整的自然语言表达。虽然扩展词表在技术上是可行的,但当时的我并不清楚如何操作——如何在保持一阶段能力的同时扩充模型以容纳新词汇,如何处理嵌入层的动态增长,如何避免灾难性遗忘。在我当时的认知框架中,这意味着二阶段开始必须重新构建词表,重新训练模型。

如果分别训练多个模型:

  • 二阶段模型处理“深红”时,无法使用一阶段模型已经学会的“红色”知识
  • 每个阶段都要重新学习基础颜色映射,造成巨大的能力浪费
  • 三个阶段就需要三个独立模型,无法形成能力的积累

面对这些问题,我逐渐意识到,这不只是词表扩展或训练策略的技术细节,还暴露了一个更根本的认知盲点:我完全不知道如何设计一个能够在不同复杂度任务间共享和积累能力的系统。每个新阶段都要推倒重来,这本身就说明我缺少某个关键的架构思维框架。

同时,CIELab的建模困难也开始暴露:

  • 维度语义模糊:L是明度,但a和b是什么?红绿对抗?黄蓝对抗?
  • 修饰词映射复杂:“偏红”意味着a增加,但增加多少?与L和b的关系如何?
  • 变化量缺乏直觉:想要“更亮一点”或“偏蓝一些”时,无法直观地确定应该如何调整a、b值

面对这些问题,我意识到自己缺少整个工程框架——如何让不同复杂度的任务共享能力?如何设计训练节奏?如何避免灾难性遗忘?

我暂停了实践,回头补了一轮课:多任务学习、课程式训练、参数调度...这些工程范式本身不复杂,但它们提供了一套思考框架——我知道该怎么组织这个系统了。

CIELCh:极坐标的启发

CIELab的建模困难驱使我开始寻找替代方案,在查阅资料的过程中,我发现了CIELCh空间——CIELab的极坐标表示:

  • L:明度(与CIELab相同)
  • C:彩度(saturation,a²+b²的平方根)
  • h:色相角(arctan(b/a),以度为单位)

CIELCh在保持CIELab感知均匀性的同时,提供了更直观的语义维度:

深蓝   → L减少,C和h基本不变
鲜艳蓝 → C增加,L和h基本不变

这种单维度调整的清晰对应让修饰词的建模变得可行。

但随之而来的是色相角的周期性问题:0°和360°表示同一颜色(红色),但在数值上相差360。这意味着当模型试图从359°学习到1°时,梯度会认为这是一个巨大的跳跃,而非微小的调整——周期边界会让训练陷入混乱。

认知转变三:我开始思考,能否用数学方法消除角度的周期边界

CIELCh+cos/sin:角度周期的优雅解决

色相角的周期性问题让我陷入思考:如何让模型理解359°到1°是微小调整而非巨大跳跃?

在查阅资料后,我发现了一个经典的数学技巧:将角度投影到单位圆上

h_degree → h_radian = h_degree × π/180
cos_h = cos(h_radian)
sin_h = sin(h_radian)

这样,任何角度都可以表示为(cos_h, sin_h)的坐标对,且:

  • 连续性:相近的角度对应相近的坐标
  • 周期性:0°和360°的坐标完全相同
  • 可微性:支持梯度下降等优化算法

最终的四维空间变成:[L, C, cos_h, sin_h]

这个空间具备了我需要的所有特性:

  • 感知均匀:继承自CIELCh空间
  • 语义直观:L、C、h的含义清晰
  • 数学友好:cos/sin展开解决了周期边界
  • 计算高效:四维向量的线性运算

但就在我为找到"完美空间"而兴奋时,一个更深层的问题浮现了……

根本性问题:为什么还要从RYB出发?

在完成CIELCh+cos/sin空间的技术验证后,我准备开始大规模语料构建。但当我坐在电脑前,准备按照老套路——“基于RYB语义确定颜色词关系,转换为CIELCh+cos/sin数值”——时,一个简单而致命的问题突然击中了我:

既然模型最终在CIELCh+cos/sin空间中运行,我为什么不直接在这个空间中构建语料?

这个问题让我重新审视了整个技术架构:

旧思路:RYB语义 → RGB转换 → HSV转换 → CIELab转换 → CIELCh转换 → cos/sin展开
新思路:直接在LCh+cos/sin空间中建立语义关系

旧思路的问题

  1. 语义空间和计算空间的分裂:在RYB中定义概念关系,在LCh中执行计算,两者的逻辑结构根本不匹配
  2. 概念覆盖不完整:RYB框架本身就遗漏了洋红、青色等颜色
  3. 多重转换累积误差:每次空间转换都损失精度
  4. 概念混乱:RYB的"蓝+黄=绿"与LCh的矢量加法不是一回事

新思路的优势

  1. 架构一致性:语义建模与计算执行在同一空间
  2. 数学纯净性:无转换误差,几何关系清晰
  3. 工程简洁性:单一颜色空间,单一数据格式
  4. 扩展性:后续功能都在统一框架内发展

认知转变四:我意识到,真正的架构统一不是让分裂的概念体系勉强对接,而是从一开始就在同一个空间中建立语义和计算

从这一刻起,我放弃了基于传统色彩理论的语料构建路径,转而直接在CIELCh+cos/sin空间中定义颜色关系。

统一与简化:架构一致性的胜利

直接构建:摆脱转换链路的束缚

补完工程范式的课后,我掌握了多任务架构的基本概念,也已经意识到了"直接在LCh+cos/sin空间构建"的必要性。

CIELab一阶段的成功给了我信心,但也暴露了我的思维惯性。即使当时已经决定抛弃RYB,我在实际构建语料时依然习惯性地这样工作:

  1. 基于RYB语义理解“朱红”:偏黄的红色,所以应该是红+少量黄
  2. 查找“朱红”在RGB中的标准值:RGB(220, 20, 60)
  3. 使用颜色转换算法将RGB转换为CIELab:CIELab(48.2, 73.5, 38.1)
  4. 把这个CIELab坐标作为“朱红”的训练目标

那个时候,虽然模型在CIELab空间中训练,但我构建语料时的思维链条仍然是:RYB语义理解→查找RGB标准值→转换为CIELab坐标。表面上抛弃了RYB,实际上RGB仍然是我确定颜色的唯一中介

做出“直接基于CIELCh+cos/sin构建”的决定后,我开始了全新的语料构建过程。这一次,我不再从任何传统颜色理论出发,而是直接在四维空间中建立颜色词的坐标。

新的构建策略

  1. 权威色彩标准为基准
    • Pantone色卡的CIELCh坐标
    • 国际照明委员会(CIE)标准
    • Adobe RGB等专业色域的定义
  2. 中文颜色词的直接映射
    朱红 → CIELCh(48.2, 73.5, 38.1) → [48.2, 73.5, 0.785, 0.619]
    天青 → CIELCh(70.1, 25.8, 196.4) → [70.1, 25.8, -0.914, -0.406]  
    象牙白 → CIELCh(95.3, 2.1, 102.5) → [95.3, 2.1, -0.208, 0.978]
    

语料构建的效率提升了一个数量级。我不再需要维护复杂的转换逻辑,不再需要担心精度损失,不再需要在不同颜色空间之间来回翻译。每个颜色词都有确定的四维坐标,每个修饰词都有明确的向量偏移。

更重要的是,我可以直接验证语义关系的数学合理性:

# 验证相似色的空间距离
distance_红_朱红 = euclidean_distance([红的坐标], [朱红的坐标])
distance_红_绿 = euclidean_distance([红的坐标], [绿的坐标])

assert distance_红_朱红 < distance_红_绿  # 语义相近的颜色空间距离更近

多任务架构的编程直觉与实现迷茫

统一的颜色空间解决了数据问题,补完工程范式的课也让我理解了多任务学习的基本概念,我确定了基本的模型架构——小型路由+模型主体。

基于编程经验,我对路由的设计有一个朴素的想法:

编程直觉驱动的设想: 就像写程序时用switch语句根据不同条件分流到不同逻辑分支一样,是否可以让模型根据输入复杂度自动选择处理方式?

if (输入是基础颜色词):
    用一阶段方法处理
elif (输入是自然语言):
    用二阶段方法处理  
elif (输入是对话上下文):
    用三阶段方法处理

基本框架设想

  • 三个阶段的语料一起收集,形成统一的词表
  • 模型通过某种方式学会判断输入属于哪种类型
  • 根据判断结果选择对应的处理方式
  • 所有输出都是统一的四维坐标[L, C, cos_h, sin_h]

这个"某种方式"让我完全卡住了。我不知道这个路由在代码层面应该是什么——一个简单的if-else?一个需要训练的分类器?如果是分类器,用什么标签训练?基于什么特征判断?这些具体的技术细节在我脑中完全是空白。

这个技术困惑让我暂时搁置了多任务架构,决定先在统一颜色空间中把单一任务做扎实。

最新架构:OKLCh + 外置程序的理念突破

经过一段时间对路由问题的思考和其他的学习积累,我意识到编码器、解码器、注意力机制等不只是抽象概念,而是可以具体设计和组合的模块。

这个认知让我重新理解了多任务设计的可能性——不同的任务可以用不同的架构组件来处理,通过路由进行协调,一个初步的多任务架构框架开始成型。

就在这个框架逐渐清晰时,我遇到了新的问题:CIELCh的感知亮度分布仍然不够均匀

在实际测试中,我发现CIELCh在暗部区域的亮度变化过于敏感,而在亮部区域又显得迟钝。这导致从“浅蓝”到“深蓝”的调整效果不及预期。

OKLCh的优势

  • 改进的感知均匀性:基于最新的视觉感知研究
  • 更好的暗部表现:在暗色区域提供更自然的过渡
  • 与现代显示技术适配:针对当前的显示器特性优化

但更重要的突破来自一次意外的学习经历。在做其他事情时,我接触到了xAI的API文档,其中的function_calling概念让我眼前一亮。进一步学习function_calling时,我了解到思维链(Chain of Thought)的理念——让AI不直接输出最终答案,而是输出推理过程,再由外部程序执行具体操作。

这个发现引发了架构理念的根本转变:为什么要让神经网络做所有事情?如果让外置程序处理数值,模型就不再需要学习数值变换,是否...不再需要把色相角度用cos和sin拆分?

外置程序的设计

旧有设计:文本 → 神经网络 → 最终颜色
新设计:  文本 → 神经网络 → 操作序列 → 外置程序 → 最终颜色

具体实现

输入:红色
神经网络输出:color 628 258 29227

输入:深一点的红色
神经网络输出:color 628 258 29227
             lightness $p -0.1

外置程序执行:接收颜色坐标 → 应用明度调整 → 返回结果

基于这个理念,我重新设计了语料结构:

  • 一阶段:基本颜色词 → 基础颜色查表
  • 二阶段:非完全泛化的自然语言 → 语义解析+颜色查表+函数查表+操作序列
  • 三阶段:对话上下文 → 上下文+增量操作序列

整体架构构想

外部输入 → 路由决断 → 三个专用任务头

每个阶段都输出操作序列而非直接的坐标值,让神经网络专注于语义理解,将精确的数值计算交给外置程序。

但我依然面临路由设计的问题。虽然现在有了三个明确的任务分支,但如何让模型自动选择合适的分支仍然困惑。我模糊地知道可能需要训练某种类似“文本分类”的东西,也许是个简单的MLP,但具体怎么实现还不清楚。

这个架构革新解决了很多问题,但也带来了新的技术挑战。

这种分工的优势

  1. 各司其职:神经网络专注语义理解,外置程序专注数值计算
  2. 精度保证:颜色操作使用专业算法,不受神经网络近似的影响
  3. 可扩展性:新的颜色操作可以通过添加外置函数实现
  4. 可调试性:操作序列可以检查和修正

认知转变五:神经网络不需要包办一切。让它专注于语义理解,把精确的数值计算交给确定性的程序,这样的分工比让单一模型学会所有事情更清晰、更可靠。

回顾:从空间分裂到架构统一

这个颜色空间的演进过程,本质上是一个逐步消除概念分裂的过程:

阶段一:RYB语义 + RGB数值 + CIELab计算
→ 三个空间各有语义,互相转换

阶段二:直接在CIELCh+cos/sin构建语料
→ 语义和计算统一在同一空间

阶段三:神经网络输出操作序列 + 外置程序执行
→ 语义理解和数值计算进一步分离,各司其职

每一步转变的核心都是:识别出哪些是必须耦合的,哪些应该解耦。RYB和LCh不该耦合,所以放弃RYB;语义理解和精确计算应该解耦,所以引入外置程序。整个过程中,我对系统边界的认知逐步清晰

构筑与磨合:理念落地的工程实践

在确定了OKLCh+外置程序的技术路线后,下一个关键步骤是将三阶段的理论架构转化为具体的训练语料和网络结构。这个过程中,我发现语料格式的设计不仅影响模型的学习效率,也加深了我对"智能协作"的深层理解。

语料结构:从理论架构到可执行格式

CoT式操作序列的设计理念

基于思维链(Chain of Thought)的启发,我设计了一种特殊的输出格式:让模型生成操作序列,而不是直接输出最终结果

这种设计的核心思想是:让神经网络专注于语义理解,将精确的数值计算交给外置程序

三阶段语料的具体格式

一阶段:基本空间构建

红
color 628 258 29227

模型内部机制:颜色名称 → 坐标查表 + 格式封装

任务本质:纯映射,建立基础词汇到坐标的稳定关联,形成合理的高维空间

二阶段:自然语言解析

泛粉的暗纯热情红
color 774 165 12056          # vars.append
chroma $p 0.1                # chroma(vars.popleft, 0.1); vars.append  
lightness $p -0.1            # lightness(vars.popleft, -0.1); vars.append
color 774 165 12056          # vars.append
mix $p $p 0.1                # mix(vars.popleft, vars.popleft, 0.1)

模型内部机制:复杂语义 → 操作序列规划

任务本质:语序解析,将自然语言转化为基准颜色和逐步执行的颜色变换,模型从空间中提取基准坐标,为其应用修饰调整

三阶段:对话上下文

红色
###
color 628 258 29227
---
红色
color 628 258 29227
深一点
###
color 628 258 29227
lightness $p -0.1
---
红色  
color 628 258 29227
深一点
color 628 258 29227
lightness $p -0.1
偏蓝绿
###
color 628 258 29227
lightness $p -0.1
color 915 130 168990
mix $p $p 0.1

语料会经过处理,以这个语料而言,实际送入模型的是以下三个语料,一个一阶段,两个三阶段:

一阶段:

红色
color 628 258 29227

三阶段:

红色<sep>color 628 258 29227<sep>深一点
color 628 258 29227<sep>lightness $p -0.1

红色<sep>color 628 258 29227<sep>深一点<sep>color 628 258 29227<sep>lightness $p -0.1<sep>偏蓝绿
color 628 258 29227<sep>lightness $p -0.1<sep>color 915 130 168990<sep>mix $p $p 0.1

模型内部机制:上下文推理 → 增量操作生成

任务本质:对话记忆,基于历史状态进行相对调整

路由设计:从编程思维到神经网络的认知碰撞

确定了三个任务头的架构后,下一个问题是:如何让模型自动选择合适的任务头?

编程本能的设计方案

基于多年的编程经验,我的第一反应是设计一个层次化的条件判断:

第一层:判断输入中有没有"---"
→ 有就是三阶段,直接路由

第二层:统计修饰词数量
→ 多于一个就是二阶段
→ 否则就是一阶段

这个逻辑清晰、简洁、确定。第一层只需要识别一个简单符号,用单层MLP(512 → 1 + sigmoid)就够了;第二层稍复杂,需要提取修饰词并计数,用两层MLP应该足够。

但随即我遇到一个根本性的问题:我要怎么告诉模型,让它在特定的层数这么处理?

认知碰撞:我无法"告诉"模型怎么做

带着这个疑问,我去查阅资料、学习案例。但所有的信息都指向同一个结论:

我无法直接告诉模型,它的内部要怎么进行结构整合、数据处理、逻辑判断。

我能做的只是:

  1. 设计网络结构(比如两层MLP)
  2. 提供训练数据(输入 → 正确的阶段标签)
  3. 让模型自己学习如何从输入映射到输出

模型会找到它自己的方式来利用这两层:

  • 也许第一层学会提取修饰特征,第二层判断数量
  • 也许第一层学会某种完全不同的抽象
  • 我无法控制它具体怎么学

这个认知让我有些震惊。在传统编程中,我写下 if count_modifiers() > 1 就意味着程序会精确执行这个逻辑。但在神经网络中,我只能设计"可能性空间"——提供足够的表达能力,然后让模型自己找到解决方案。

最终选择:[CLS] Token分类

既然无法按照编程思维直接设计逻辑,我转向了业界成熟的方案:基于[CLS] Token的序列分类

核心机制:在输入序列开头添加一个特殊的[CLS] token,通过Transformer编码器的自注意力机制,让这个token自动学会如何聚合整个序列的信息,最终只用[CLS]位置的输出进行分类。

架构配置

  • 4层Transformer编码器(8头注意力,前馈维度1024)
  • 基于[CLS]输出的分类头(256维 → 128维 → 3类)

这个架构的本质是:不预设特征提取规则,让模型通过注意力机制自主学习"什么是重要的"

[CLS] token在训练中会自己学会:

  • 对于"红",关注词根特征
  • 对于"深红",重点关注修饰词
  • 对于含"---"的序列,强烈关注结构标记

这次设计经历让我第一次清晰地感受到:神经网络和传统编程的根本差异不在于技术细节,而在于控制范式——我不再是精确指挥每一步操作的程序员,而是设计学习环境的架构师。

任务头设计:基础概念的认知补全

从设想到困惑:我不知道怎么输出文本

三阶段语料格式确定后,我开始设计具体的任务头。在我的设想中,这应该是个直接的过程:既然明确了每个阶段要做什么,设计对应的网络结构应该水到渠成。

但当我开始为一阶段设计任务头时,立即遇到了障碍。

我的困惑:一阶段需要输出 color 628 258 29227,这看起来是个四维输出问题——"color"、三个数字。我记得Transformer内部有MLP组件,用来变换特征,那我能用MLP做任务头,让它输出这个格式化字符串吗?

但当我试图实现时,发现自己对这些基础概念的理解存在巨大盲区:

  • Transformer内部的MLP和任务头MLP是一回事吗?
  • MLP到底能输出什么样的结果?
  • 我总看到"Encoder"和"Decoder",它们和MLP是什么关系?

我不得不暂停设计,开始系统学习这些基础组件。

概念厘清:MLP不能生成文本

通过查阅文档和代码,我逐渐理解了几个容易混淆的概念:

Transformer内部的MLP:层内的特征变换器,输入输出都是固定维度向量

任务头MLP:将编码器输出映射到任务结果,但它只能做固定维度的数值映射——输入512维向量,输出4维向量或分类概率。它无法生成文本,无法处理变长序列。

Encoder:序列到向量的压缩器
Decoder:向量到序列的展开器,逐个生成token

认知冲击:所有任务都需要Decoder

当我理解这些概念后,重新审视我的任务需求:

  • color 628 258 29227 本质是7个token的序列(包括空格)
  • mix $p $p 0.1 也是token序列
  • 所有任务头都需要输出文本,都需要Decoder

这完全推翻了我的原始设想。我本以为"简单的查表"可以用MLP,"函数调用"只是分类,但它们本质上都是序列生成任务。

重新设计:基于直觉的层数配置

既然所有任务都需要Decoder,那区别只在于复杂度。我重新设计了三个任务头:

一阶段:2层Decoder - 最简单的查表任务
二阶段:4层Decoder - 需要理解自然语言和语序
三阶段:8层Decoder - 需要处理对话上下文

说实话,这个2、4、8的配置完全是靠直觉。 我感觉一阶段最简单,2层够了;二阶段复杂些,4层;三阶段更复杂,8层。为什么不是2、3、4或2、4、6?没有理论依据,就是一种"任务复杂度应该匹配架构复杂度"的工程直觉。

但我心里其实没底。 这种直觉式的设计真的合理吗?模型真的会按照我设想的"阶段递进"来学习吗?那些"阶段边界"真的存在吗?

这些疑问在设计时隐约闪过,但我当时告诉自己:先实现出来,训练会给出答案。

实践的冲击:理论遭遇现实

重新设计完成后,我开始实现训练代码。第一步是路由——让它学会判断输入属于哪个阶段。

按照设计,判断逻辑很简单:包含"---"就是三阶段,有修饰词就是二阶段,其余是一阶段。我用标注好的语料训练路由分类器,理论上这应该是个简单的分类任务。

路由的失败:只会排除法

但实际训练中的现象让我困惑。小型路由(几百万参数)只学会了排除法:

"蓝珀" (未见词汇) → 不确定是否为一阶段 → 70%二阶段
"淡一点偏红的蓝珀" → 明显不是一阶段 → 100%二阶段  
"素一些淡一点偏红的蓝珀" → 更明显不是一阶段 → 100%二阶段

它根本没有做特征识别:"淡"、"偏"、"素"是修饰词 → 二阶段。而是:

强排除:包含"---" → 三阶段
弱排除:不像一阶段样本 → 二阶段
剩余归类:其他 → 一阶段

我试着提升参数量到70M,它才勉强学会识别修饰词:

蓝珀 → 阶段0 (置信度: 100.0%)
深蓝珀 → 阶段0 (置信度: 99.8%)
艳蓝珀 → 阶段1 (置信度: 100.0%)
艳浅蓝珀 → 阶段1 (置信度: 100.0%)

但依然不准确。"深蓝珀"明明有修饰词"深",为什么是阶段0?

根本性质疑:阶段边界真的存在吗?

盯着这些测试结果,一个简单而致命的问题突然击中我:

一阶段语料中有"红"也有"深红"。如果"深红"既可以是一阶段的基础词,也可以是二阶段的修饰结果,那"阶段"的边界到底在哪里?

我一直以来的想法是:模型先建立基础颜色空间,然后学习修饰(基于坐标+向量偏移),再学习对话(基于上下文链路)。这个思路看起来很合理——符合人类的学习顺序。

但现在我开始反思:真的存在合理的阶段区分吗?真的有必要让我来考虑这个"基础+操作"的问题吗?

我想起了Transformer那篇论文的标题:"Attention is All You Need"

也许,我应该放弃自己在编程领域和人类认知中的经验,让模型自己找到表达方式?

推翻重来:去掉路由

我决定去掉路由,把所有语料混在一起训练——我来设计问题和答案,剩下的让模型自己来

原本设计的500M模型,三个任务头,复杂的路由机制,全部推翻。新的架构极其简单:

文本输入 → 编码器 → 解码器 → 操作序列

没有路由,没有阶段划分,就是一个标准的Seq2Seq模型。

意外的结果

开始训练时,我用了157M参数,结果频频过拟合。我不断降低参数量,直到75M时,才达到预期效果:

[测试]> 蓝
→ 'color 452 313 264059'

[测试]> 蓝<sep>color 452 313 264059<sep>浅一些
→ 'color 452 313 264059<sep>lightness $p 0.01'

[测试]> 艳一些的蓝
→ 'color 856 49 246428<sep>chroma $p 0.107'

[测试]> 深蓝
→ 'color 288 199 264059'

[测试]> 艳一些的深蓝
→ 'color 452 313 264059<sep>chroma $p 0.116<sep>lightness $p 0.054'

模型确实做到了"自己找到合理的表达"。特别是最后一个情景,按常理"艳一些的深蓝"应该和"艳一些的蓝"用同样模式——都是在某个蓝的基础上调整彩度。但模型选择了两步操作,而且做对了。

它找到了自己的方式,而不是我设计的方式。

最终的模型配置:

词汇表大小: 15000
数据集大小: 89794 样本
模型参数量: 75.3M
编码器: 8层,512维,8头注意力
解码器: 8层,512维,8头注意力
训练样本: 5050
验证样本: 561

Epoch 16 | 验证损失: 0.18491 | Token准确率: 92.97%

更深的反思:那些"必要性"真的必要吗?

这次推翻让我开始质疑最初的那些假设:

为什么我觉得需要500M模型?

  • 数值精度问题 → 需要外置程序
  • 语义歧义爆炸 → 需要阶段划分
  • 训练数据需求 → 需要路由分流

但60.6M就够了。那么这些"问题"真的存在吗?

"为什么要让模型做所有事情"这个质问,本身就预设了一个前提:神经网络不擅长精确数值计算。这个前提让我设计了DSL + 外置程序,让模型"专注于语义理解"。

但也许,这个前提本身就值得怀疑?

我现在无法确定答案。外置程序的架构已经实现了,确实能工作。但如果重新开始,我会不会选择另一条路?

这个问题,我暂时留给未来。

最后的优化:单字词表的意外收获

就在模型训练完成、准备收尾时,我在测试中发现了一个问题:

"深一点的森林绿" → 正常工作
"深一点的森林绿色" → 输出混乱

"森林绿"在训练语料中存在,所以模型能正确处理。但"森林绿色"不存在,模型就失效了。

我检查了分词结果,发现了问题所在:

克莱因蓝 → ["克莱因蓝"]  # 整个词被当作一个token
亮一些泛蓝的浅红 → ["亮一些", "泛", "蓝的浅", "红"]  # "蓝的浅"这种莫名其妙的切分

我尝试了一个极端的方案:单字词表

这是个不得已的选择。unigram分词算法基于统计频率,会把颜色词随机切断——"克莱因蓝"可能被切成["克莱因蓝"],也可能被切成["克", "莱因蓝"],这种不确定性干扰了模型学习。

既然概率切分靠不住,那就干脆不让它切,设置 max_sentencepiece_length=1,让模型自己从单字组合中学习语义。

克莱因蓝 → ["克", "莱", "因", "蓝"]
亮一些泛蓝的浅红 → ["亮", "一", "些", "泛", "蓝", "的", "浅", "红"]

理论上,这会让序列变长,增加学习难度。但训练结果再次出乎意料:

参数量降低:从75.3M降到60.6M
泛化能力提升:所有"XX色"类问题消失

测试时还发现了有趣的现象:

金黄 → color 863 176 89840
金黄色 → color 887 182 95337

绿 → color 520 177 142511
绿色 → color 520 177 142511

深绿 → color 436 148 142511
深绿色 → color 520 177 142511<sep>lightness $p -0.101

模型对同义表达找到了不同的处理方式:

  • "金黄"和"金黄色":给出略微不同但视觉等价的色值
  • "绿"和"绿色":完全相同的输出
  • "深绿"和"深绿色":前者在语料中存在,直接输出颜色;后者在语料中没有,它理解为"绿色"的基准 + "深"的修饰操作

这些都是合理的结果。模型没有机械地记忆词形,而是真正理解了颜色语义——对于同义表达,它可以给出视觉等价的结果;对于修饰组合,它会根据自己学到的模式选择直接输出或基准+操作的方式。

模型又一次找到了自己的方案。

我原本以为需要精巧的分词策略来帮助模型理解语义,但单字词表反而让它有了更强的泛化能力——不再依赖预设的词边界,而是自己学会了从字的组合中提取含义。

当然,这个方案也有局限性。 单字词表适合中文颜色语义这种特定场景,但不是普适的分词策略。它解决了当前的问题,也提醒我:有时候,针对具体问题的简单方案,比追求通用性的复杂设计更有效。

尾声:一个段落,而非终点

这个项目从"能不能让AI理解颜色"这个简单念头开始,走过了三次彻底的架构推翻,和无数次的调整优化:

从RYB的人文直觉到OKLCh的科学空间,我放弃了"符合人类习惯"的执念,选择了数学上正确的方案。

从精心设计的多任务路由到统一生成模型,我发现那些"阶段划分"可能从一开始就是个伪命题。

从500M到75M再到60.6M,从复杂的协调机制到简单的Seq2Seq,从精巧的分词策略到单字词表——每一次简化,模型都变得更好。

但最大的转变不是技术架构,而是控制范式。

在路由设计时,我试图让模型按照"我的逻辑"工作——第一层检测符号,第二层统计修饰词,但模型不听我的。那种失控感让我一度觉得:训练就是个不可控的黑箱,推理就是在概率猜谜。

但冷静下来后我意识到,问题不在于"不可控",而在于我试图控制不该控制的东西。模型内部如何组织特征、如何形成表示——这本来就不是我该精确指定的。我的工作是设计问题和答案,提供合适的架构,剩下的应该交给模型自己。

从"我告诉模型怎么做"到"我设计问题让模型自己找答案"——这个转变贯穿了整个项目。


现在回头看,那些被推翻的设计并非无意义。每一次推翻都是对问题本质的更深理解:

如果没有经历多任务路由的失败,我不会意识到"阶段"是我强加的概念。

如果没有设计DSL + 外置程序,我不会开始质疑"神经网络不擅长数值计算"这个前提——但这个质疑,我暂时留给未来。

外置程序的架构已经实现了,确实能工作。DSL让整个系统更可控、可调试,也许这就是当前最合适的方案。也许未来会有新的认知冲击,让我再一次推翻现在的设计。

"其一"到此为止。它解决了"让AI理解基础颜色和简单修饰"的问题,但留下了新的疑问:

  • 外置程序真的必要吗?模型能否直接学会精确的数值调整?
  • DSL是约束还是枷锁?它提供了可控性,但也限制了表达的自由度?
  • 如何让它理解更柔和的表达——"比刚刚亮一些"?"柔和一点"?

这些问题,会在"其二"和后续中继续探索。

路还很长。但至少现在,它已经能理解"亮一些的蓝"了。

💾 项目资源