一个隐态向量里究竟保存了什么?我们经常用输出层回答这个问题:把向量送进读取器,看看哪些 token 得到较高分数。但当两个隐态先经过一次联合变换,再分别读出时,事情就变得有趣了。我们能否用一组 Key,控制这次变换,使左右输出呈现不同的信息,同时又保留恢复原始输入的可能?
本文讨论的不是已经完成的模型,而是一个准备验证的设计。下面的 Python 风格代码用于说明数据流,并非声称可以直接运行。我们的目标不是先宣称一种新的语言理论,而是把一个计算单元写清楚,然后通过 echo 观察它。
从一个 token 的回声开始
词表中的 token ID 先通过 embedding 表得到一个向量。一个最简单的 echo 问题是:给定这个向量,能否找回原来的 token?
token_embedding = embedding_table[token_id]
token_scores = echo_reader(token_embedding)
predicted_token_id = argmax(token_scores)
如果向量没有被改变、表中的编码彼此不同,而且我们可以访问整张表,也可以用近邻回查;不一定需要训练一个 reader。训练读取器的意义,是研究经过投影、混合或其他运算后的状态,是否仍能在指定读取条件下找回信息。
接下来我们将两个 token,或者两段由多个 token 形成的状态,送入同一个计算单元。比如,希望最后能分别读出:
左侧:西西弗斯、推着
右侧:石头、上山
这里词只是说明;实际 tokenizer 如何切分,必须以所选 tokenizer 为准。词袋 echo 也不自动验证每侧内部顺序。若要观察顺序,应另设位置或序列读出目标。
Embedding 之后还有什么?
假设采用一个预训练模型的输入 embedding。tokenizer 与 embedding 表必须来自匹配的版本;否则同一个 ID 可能指向错误的表项。
left_ids = tokenizer(left_text)
right_ids = tokenizer(right_text)
left_embeddings = embedding_table[left_ids]
right_embeddings = embedding_table[right_ids]
如果每个 token 是 512 维,而左侧包含多个 token,那么左侧 embedding 是一个矩阵,不是一个 512 维向量:
left_embeddings.shape # [left_length, 512]
我们的单层联合变换需要明确输入形状。因此还要有一个 side_encoder:
left_hidden = side_encoder(left_embeddings) # [d]
right_hidden = side_encoder(right_embeddings) # [d]
这个组件不能藏在一个没有说明的 encode() 里。它可以先选择简单聚合,也可以使用可训练编码器;不同选择可能保留或丢失不同信息。实验要先检查两侧编码后的输入是否可以 echo,否则后续读出失败不能全归咎于分裂单元。
我们准备把 512 维直入作为重要候选,不再默认添加 512 → 64 的投影。投影不是绝对不能用,但它应是明确配置:不同 token 经过降维后是否仍可区分,必须测量。
一次联合计算,再截出左右
我希望左右同时参与一次计算,而不是先更新左边,再用更新后的左边更新右边。设左右各为 d 维:
combined_hidden = concat(left_hidden, right_hidden) # [2*d]
combined_output = expanded_key @ combined_hidden # [2*d]
next_left = combined_output[:d]
next_right = combined_output[d:]
这里的截取只是数组切片,不需要额外的分裂网络。将矩阵想成四块,便能看到左右如何交叉:
next_left = key_LL @ left_hidden + key_LR @ right_hidden
next_right = key_RL @ left_hidden + key_RR @ right_hidden
这两行只是一次大矩阵乘法的展开。每个输出都可以同时受两个输入影响;但形状仍然是 [d],并不意味着输出已保持了原来的词信息。形状正确、数值可恢复、词袋可读,是三个不同的检查。
让多个 Key 参与注意力
如果我们有多组 Key,就可以根据当前隐态计算它们的作用程度。下面是一个待验证的候选:
combined_hidden = concat(left_hidden, right_hidden) # [2*d]
keys.shape # [key_count, 2*d, 2*d]
def match(hidden, key_matrix):
transformed = key_matrix @ hidden
return dot(hidden, transformed) / sqrt(2 * d)
scores = [match(combined_hidden, key) for key in keys]
weights = softmax(scores) # [key_count]
expanded_key = weighted_sum(keys, weights) # [2*d, 2*d]
combined_output = expanded_key @ combined_hidden
这里 Key 矩阵不仅作为候选条件,还参与构造联合变换。match 是为了使实验有一个具体起点,不是已经找到的最佳注意力公式。多个 Key 也不自动等于多个 attention head。
需要特别区分:上述流程里左右状态作为输入已经给出。因此第一轮测试的是“已有左右内容经过联合变换后怎样变化、能否恢复”,还不是从一个混合父状态自动发现语序边界。
READ:两种信息合同
要检验逆向过程,我会同时保留两种明确条件。第一种,允许 READ 使用正向计算的 weights:
# 条件 A:weights 随输出一起保存
expanded_key = weighted_sum(keys, saved_weights)
recovered = solve(expanded_key, combined_output)
这时要报告 weights 也是恢复所使用的信息。还要记录方程能否求解、数值条件和恢复误差:多个可逆矩阵的加权和不一定可逆。
第二种,READ 只拿输出与 keys:
# 条件 B:不额外提供正向 weights
recovered = learned_reader(combined_output, keys)
这不是预先宣布它会成功或失败,而是让实验给出结果。我们会在未见样本上测误差;一个 reader 没成功,只能说明这个实现和训练条件没有成功,不能证明不存在其他恢复方法。
已有 TreeHeap 的 lifting 是重要参照:它把一对状态组织成 parent 和保留的 detail,再进行对应的代数 UNFOLD。历史的可逆双节点通信也表明可训练交叉变换可以有明确逆式。但它们并不等同于本文的输入相关多 Key 联合矩阵;历史结果不能直接替本候选证明成功。
Echo 观察什么
一次往返实验至少要观察三个位置:
before_left = echo_reader(left_hidden)
before_right = echo_reader(right_hidden)
after_left = echo_reader(combined_output[:d])
after_right = echo_reader(combined_output[d:])
recovered_left, recovered_right = recover(...)
recovered_left_echo = echo_reader(recovered_left)
recovered_right_echo = echo_reader(recovered_right)
我们分别记录左右命中、漏词、跨侧读出,以及原状态和恢复状态的误差。只看 argmax 会丢掉大量信息;如果要判断多个词是否同时可读,需要明确多标签读取方式。词表 softmax 是相对分布,不应直接解释成每个词独立存在的概率。重复词和词序也需要单独定义目标。
即使左右合起来可以重建原向量,也不自动证明每一侧保留了正确的词袋。反过来,reader 读不出某个词,也不能立刻证明信息彻底消失:读取器自身可能不适配。我们需要同一读取合同、独立数据和对照来解释结果。
配置管理:明确选择,而不是自动猜答案
一个完整实验包含 tokenizer、embedding、side encoder、keys、attention、联合变换、READ、echo reader 和训练目标。配置管理器的职责,是把这些选择写出来,并在开始前检查它们是否能对齐:
config = ExperimentConfig(
tokenizer="matched_to_embedding",
embedding="pretrained_frozen",
embedding_dim=512,
hidden_dim=512,
side_encoder="explicit_choice",
attention="input_conditioned",
transform="weighted_keys",
read_access="output_and_keys",
)
validate_shapes(config)
validate_tokenizer_and_embedding(config)
validate_read_access(config)
record_config_and_hashes(config)
如果维度不匹配,管理器应报错,要求明确选择投影,而不是自动插入一个未说明的层。它还应记录数据划分、随机种子、参数量、显存、计算预算和结果。
配置管理器可以根据少量真实实验结果推荐下一批候选,但不能声称找到了全局最优。验证集用来选择配置,测试集留到配置确定后评估。512 维输入意味着拼接后为 1024 维,一个完整 1024 × 1024 矩阵约有 105 万个元素;多个矩阵 Key 的代价必须实测,不能先假定它便宜。
从基线开始
我准备比较四种单层变换:单位矩阵、固定可逆矩阵、多个 Key 等权组合、多个 Key 的输入相关 attention 组合。它们使用相同数据与读取合同,同时记录正向输出、恢复误差、echo、矩阵数值条件和资源开销。
这套设计的目的不是证明“语言就是排序”,也不是证明 attention 一定能把词袋准确分开。它是把问题变成可观察的计算:当 Key 改变隐态时,什么仍能被读出?什么能被恢复?哪些变化依赖输入,哪些只是固定混合?
如果最简单的单层合同都不能说明这些结果,我们就继续修正单元;如果它能给出稳定、可复现的观察,再考虑更复杂的递归结构。