如果把一个离散的符号判断直接塞进神经网络,前向计算能给出确定结果,反向传播却常常寸步难行。原因不在于规则本身写不出来,而在于逻辑连接词和比较操作产生的是布尔值或硬阈值输出,这些输出对输入的偏导几乎处处为零。优化器看不到任何梯度信号,只能靠随机扰动或外部奖励来学习,效率很低。为了把符号推理接入梯度下降,松弛化和可微逻辑给出了两条互补路线:前者把逻辑门软化,后者把更复杂的规则系统改造成可微计算图。理解它们的关键是搞清楚连续化在哪里引入、引入了多少偏差,以及训练结束后如何恢复离散决策。

符号推理为什么会让梯度消失
符号推理的典型操作可以拆成三类:逻辑连接词如合取、析取、蕴含,比较操作如大于、小于、等于,以及选择操作如根据条件返回不同分支。它们的共同点是输出通常落在有限集合中,例如真值只能取0或1,比较结果只能取True或False。对反向传播来说,这类操作等同于阶跃函数或分段常数函数。阶跃函数在跳变点不可导,在平坦区域导数为0,所以无论输入如何变化,梯度都无法穿过。
以规则 r = A && B 为例,如果A和B都是布尔变量,r对A的偏导在A从0到1的临界处没有定义;如果强行按数值计算,把A表示成0.4、B表示成0.7,布尔结果仍然是False,损失对A的梯度是0。模型完全不知道A从0.4增加到0.6会让规则成立。同样的现象也出现在 if x > 0.5 这样的比较里。硬判决把连续信号压成离散事件,丢掉的不只是信息,还有可供优化的梯度。
这个问题的本质是符号推理的语义建立在离散集合上,而深度学习优化建立在连续流形上。要让两者协同工作,就必须在训练阶段给逻辑运算一个连续的影子语义。这个影子的目标不是替代最终的离散推理,而是在参数空间中提供方向。接下来介绍的松弛化,就是最直接的连续化手段。
import torch
def hard_and(a, b):
return (a > 0.5) & (b > 0.5)
a = torch.tensor(0.4, requires_grad=True)
b = torch.tensor(0.7, requires_grad=True)
# 返回布尔值,无法参与反向传播
y = hard_and(a, b)
print(y)
从硬逻辑到软逻辑:连接词松弛化与温度控制
松弛化的第一步,是把逻辑真值的取值范围从 {0, 1} 扩展到连续区间 [0, 1]。这样,A与B不再必须是非真即假,而可以表示A有0.6的置信度。逻辑连接词也随之从真值表变成连续函数。最常见的两组方案来自三角范数,也就是t-norm。乘积型定义合取为 a * b,析取为 a + b - a * b,否定为 1 - a。它的优点是处处光滑,梯度分布均匀;缺点是多个合取项连乘后真值会迅速趋近0,深层规则容易出现梯度消失。
卢卡西维茨逻辑采用另一种形式:合取为 max(0, a + b - 1),析取为 min(1, a + b),蕴含为 min(1, 1 - a + b)。这种形式在满足条件时梯度为1,在远不满足时梯度为0,行为更接近线性约束。它的数值稳定性更好,适合规则较长、约束较强的场景。不同t-norm不是单纯换公式,它们决定了规则满足度如何传播到参数,也会影响模型倾向于输出中间值还是极端值。
另一个重要工具是温度参数。逻辑比较 x > 0.5 可以用带温度的逻辑斯蒂函数近似为 sigmoid((x - 0.5) / tau)。当温度 tau 较大时,过渡区宽,梯度信号容易穿过;当 tau 趋向0时,函数逐渐逼近硬阶跃。训练早期使用较高温度,让模型先解决大致方向,再逐步降低温度恢复硬逻辑,这是常用的退火策略。
import torch
def product_and(a, b):
return a * b
def product_or(a, b):
return a + b - a * b
def lukasiewicz_and(a, b):
return torch.clamp(a + b - 1, min=0.0)
def lukasiewicz_imp(a, b):
return torch.clamp(1 - a + b, min=0.0, max=1.0)
a = torch.tensor(0.6, requires_grad=True)
b = torch.tensor(0.8, requires_grad=True)
y = lukasiewicz_and(a, b)
y.backward()
print(a.grad) # 当 a+b>1 时梯度为1
可微逻辑编程:让规则系统整体参与训练
门级松弛化解决单个逻辑运算的可导问题,但当规则数量多、变量关系复杂时,手动逐条软化会非常繁琐。可微逻辑编程把一阶逻辑或逻辑程序整体视为可微计算图,让谓词、量词和规则都拥有连续语义。代表性框架包括Logic Tensor Network、DeepProbLog和Scallop。它们的共同思路是:将对象编码成向量,用神经网络表示谓词,逻辑公式则通过模糊语义或概率语义聚合为标量满足度,最后把它放进损失函数。
以Logic Tensor Network为例,谓词 cat(x) 和 animal(x) 不再返回布尔值,而是返回0到1之间的分数。规则“所有猫都是动物”可以表示为蕴含式 imp(cat(x), animal(x)),实际训练时对一批样本计算 min(1, 1 - cat_score + animal_score) 的平均值。这个平均值越高,规则越满足。如果某个样本被判断为猫的概率高、被判断为动物的概率低,损失项就会变大,梯度会同时流向两个谓词网络。相比把规则硬编码成if-else,这种设计允许外部监督信号和逻辑约束共同塑造表示。
DeepProbLog走的是概率路线。它把神经网络的输出当作原子事实的概率,再交给ProbLog程序做概率推理,最终通过概率图的边际分布计算损失。这种方式保留了概率语义的可解释性,代价是推理复杂度更高。选择Logic Tensor Network还是DeepProbLog,通常取决于任务是偏向规则约束,还是偏向不确定性推理。前者适合在视觉等连续输入上施加逻辑先验,后者适合知识图谱补全、概率规划等需要显式概率的问题。
import torch import torch.nn as nn torch.manual_seed(0) x = torch.randn(4, 6) cat_layer = nn.Linear(6, 1) animal_layer = nn.Linear(6, 1) cat_score = torch.sigmoid(cat_layer(x)).squeeze() animal_score = torch.sigmoid(animal_layer(x)).squeeze() # 卢卡西维茨蕴含:min(1, 1 - cat + animal) imp = torch.clamp(1 - cat_score + animal_score, min=0.0, max=1.0) rule_loss = 1 - imp.mean() rule_loss.backward() print(cat_layer.weight.grad)
实际调参中容易忽视的问题
松弛化不是万能的。一个常见问题是模型学会输出中庸真值来糊弄逻辑损失。例如所有谓词都输出0.5,某些对称规则的平均满足度可能并不低,但这样的模型没有决策能力。解决方案通常是为关键谓词保留监督损失,或者对真值施加熵正则,推动输出靠近0或1。温度退火也能在后期把中间值压向两端,但要注意退火过快会导致梯度重新消失,退火过慢则模型长期停留在模糊状态。
另一个问题是松弛化带来的语义偏差。乘积型合取假设两个子句在概率上相互独立,这在实际规则中不一定成立;卢卡西维茨逻辑偏向线性可加,容易忽略证据重复带来的饱和效应。因此,不要盲目在所有规则上使用同一种t-norm。对于短规则、独立证据,乘积型足够;对于长规则、约束硬性较大的场景,卢卡西维茨更好。更稳妥的做法是先在少量数据上观察满足度曲线和梯度幅度,再决定算子组合。
还应当考虑是否需要可微。某些离散搜索、定理证明或程序合成任务中,规则空间本身是离散的,强行松弛化可能引入大量局部极小。此时REINFORCE、Gumbel-softmax或分支限界可能是更合适的选择。可微逻辑的收益最大之处在于:输入本身连续、规则作为约束存在、希望端到端训练。比如让视觉模型输出物体类别概率,同时要求类别关系满足知识图谱约束,松弛化逻辑就能在反向传播中提供连续监督。
import math
import torch
def soft_compare(x, threshold, tau):
return torch.sigmoid((x - threshold) / tau)
tau = 1.0
for epoch in range(100):
tau = max(0.05, 1.0 * math.exp(-0.03 * epoch))
# 将 tau 传入训练计算图
总体来看,解决符号推理不可微的关键不是把离散逻辑删除,而是在训练阶段构造一个连续的代理目标。门级松弛化适合快速改造已有规则,可微逻辑编程适合把复杂规则系统纳入端到端学习。无论哪种路线,都要关注算子的梯度行为、温度调度和最终离散化策略。理解了这些设计选择之后,再遇到逻辑规则与神经网络结合的工程问题,就能更清楚地判断该在哪里软化、该在哪里保留硬约束。