Learn AI Series (#145) - Neuro-Symbolic AI
Learn AI Series (#145) - Neuro-Symbolic AI

What will I learn
- Why the two oldest tribes in AI keep failing at each other's job: neural nets that perceive brilliantly but reason fuzzily, symbolic systems that reason flawlessly but cannot perceive;
- knowledge graphs and TransE: representing facts as (subject, relation, object) triples and learning embeddings where relations act as translations, so you can predict missing facts;
- neural theorem provers: differentiable, learned rules you can chain to prove statements -- symbolic-style logic trained end-to-end with gradient descent;
- concept bottleneck models: forcing a network to reason through human-readable concepts, so you can inspect, correct, and debug its reasoning;
- the core integration challenge: bridging continuous (differentiable) neural computation with discrete symbolic logic, and the fuzzy-logic trick that makes AND and OR differentiable;
- an honest read on whether we actually NEED deep neuro-symbolic architectures or whether scaling LLMs quietly ate the whole problem.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Python 3.10+ with PyTorch installed (
pip install torch) -- everything here runs on a CPU in seconds, nothing needs a GPU; - You remember embeddings and vector search (#63), you've seen graph neural networks (#131), and it helps a LOT to have read the reasoning-and-planning episode (#142) and last episode on few-shot and zero-shot learning (#144), because we walk straight through the door I left open at the end of it.
Difficulty
- Beginner
Curriculum (of the Learn AI Series):
- Learn AI Series (#1) - What Machine Learning Actually Is
- Learn AI Series (#2) - Setting Up Your AI Workbench - Python and NumPy
- Learn AI Series (#3) - Your Data Is Just Numbers - How Machines See the World
- Learn AI Series (#4) - Your First Prediction - No Math, Just Intuition
- Learn AI Series (#5) - Patterns in Data - What "Learning" Actually Looks Like
- Learn AI Series (#6) - From Intuition to Math - Why We Need Formulas
- Learn AI Series (#7) - The Training Loop - See It Work Step by Step
- Learn AI Series (#8) - The Math You Actually Need (Part 1) - Linear Algebra
- Learn AI Series (#9) - The Math You Actually Need (Part 2) - Calculus and Probability
- Learn AI Series (#10) - Your First ML Model - Linear Regression From Scratch
- Learn AI Series (#11) - Making Linear Regression Real
- Learn AI Series (#12) - Classification - Logistic Regression From Scratch
- Learn AI Series (#13) - Evaluation - How to Know If Your Model Actually Works
- Learn AI Series (#14) - Data Preparation - The 80% Nobody Talks About
- Learn AI Series (#15) - Feature Engineering and Selection
- Learn AI Series (#16) - Scikit-Learn - The Standard Library of ML
- Learn AI Series (#17) - Decision Trees - How Machines Make Decisions
- Learn AI Series (#18) - Random Forests - Wisdom of Crowds
- Learn AI Series (#19) - Gradient Boosting - The Kaggle Champion
- Learn AI Series (#20) - Support Vector Machines - Drawing the Perfect Boundary
- Learn AI Series (#21) - Mini Project - Predicting Crypto Market Regimes
- Learn AI Series (#22) - K-Means Clustering - Finding Groups
- Learn AI Series (#23) - Advanced Clustering - Beyond K-Means
- Learn AI Series (#24) - Dimensionality Reduction - PCA
- Learn AI Series (#25) - Advanced Dimensionality Reduction - t-SNE and UMAP
- Learn AI Series (#26) - Anomaly Detection - Finding What Doesn't Belong
- Learn AI Series (#27) - Recommendation Systems - "Users Like You Also Liked..."
- Learn AI Series (#28) - Time Series Fundamentals - When Order Matters
- Learn AI Series (#29) - Time Series Forecasting - Predicting What Comes Next
- Learn AI Series (#30) - Natural Language Processing - Text as Data
- Learn AI Series (#31) - Word Embeddings - Meaning in Numbers
- Learn AI Series (#32) - Bayesian Methods - Thinking in Probabilities
- Learn AI Series (#33) - Ensemble Methods Deep Dive - Stacking and Blending
- Learn AI Series (#34) - ML Engineering - From Notebook to Production
- Learn AI Series (#35) - Data Ethics and Bias in ML
- Learn AI Series (#36) - Mini Project - Complete ML Pipeline
- Learn AI Series (#37) - The Perceptron - Where It All Started
- Learn AI Series (#38) - Neural Networks From Scratch - Forward Pass
- Learn AI Series (#39) - Neural Networks From Scratch - Backpropagation
- Learn AI Series (#40) - Training Neural Networks - Practical Challenges
- Learn AI Series (#41) - Optimization Algorithms - SGD, Momentum, Adam
- Learn AI Series (#42) - PyTorch Fundamentals - Tensors and Autograd
- Learn AI Series (#43) - PyTorch Data and Training
- Learn AI Series (#44) - PyTorch nn.Module - Building Real Networks
- Learn AI Series (#45) - Convolutional Neural Networks - Theory
- Learn AI Series (#46) - CNNs in Practice - Classic to Modern Architectures
- Learn AI Series (#47) - CNN Applications - Detection, Segmentation, Style Transfer
- Learn AI Series (#48) - Recurrent Neural Networks - Sequences
- Learn AI Series (#49) - LSTM and GRU - Solving the Memory Problem
- Learn AI Series (#50) - Sequence-to-Sequence Models
- Learn AI Series (#51) - Attention Mechanisms
- Learn AI Series (#52) - The Transformer Architecture (Part 1)
- Learn AI Series (#53) - The Transformer Architecture (Part 2)
- Learn AI Series (#54) - Vision Transformers
- Learn AI Series (#55) - Generative Adversarial Networks
- Learn AI Series (#56) - Mini Project - Building a Transformer From Scratch
- Learn AI Series (#57) - Language Modeling - Predicting the Next Word
- Learn AI Series (#58) - GPT Architecture - Decoder-Only Transformers
- Learn AI Series (#59) - BERT and Encoder Models
- Learn AI Series (#60) - Training Large Language Models
- Learn AI Series (#61) - Instruction Tuning and Alignment
- Learn AI Series (#62) - Prompt Engineering - Getting the Most from LLMs
- Learn AI Series (#63) - Embeddings and Vector Search
- Learn AI Series (#64) - Retrieval-Augmented Generation (RAG) - Basics
- Learn AI Series (#65) - RAG - Advanced Techniques
- Learn AI Series (#66) - Working with LLM APIs
- Learn AI Series (#67) - Building AI Agents (Part 1) - Foundations
- Learn AI Series (#68) - Building AI Agents (Part 2) - Advanced Patterns
- Learn AI Series (#69) - Fine-Tuning Language Models
- Learn AI Series (#70) - Running Local Models
- Learn AI Series (#71) - Text Generation Techniques
- Learn AI Series (#72) - Tokenization Deep Dive
- Learn AI Series (#73) - LLM Evaluation
- Learn AI Series (#74) - The Hugging Face Ecosystem
- Learn AI Series (#75) - Multimodal Models - Text Meets Vision
- Learn AI Series (#76) - Mini Project - Your Own AI Assistant
- Learn AI Series (#77) - Image Processing Fundamentals
- Learn AI Series (#78) - Object Detection (Part 1) - Foundations
- Learn AI Series (#79) - Object Detection (Part 2) - Modern Approaches
- Learn AI Series (#80) - Image Segmentation
- Learn AI Series (#81) - Pose Estimation and Tracking
- Learn AI Series (#82) - Optical Character Recognition
- Learn AI Series (#83) - Video Understanding
- Learn AI Series (#84) - Generative Images - Diffusion Models (Part 1)
- Learn AI Series (#85) - Generative Images - Diffusion Models (Part 2)
- Learn AI Series (#86) - Image-to-Image and Editing
- Learn AI Series (#87) - 3D Vision
- Learn AI Series (#88) - Face Analysis
- Learn AI Series (#89) - Medical and Scientific Imaging
- Learn AI Series (#90) - Self-Supervised Learning for Vision
- Learn AI Series (#91) - Mini Project - Building a Visual AI System
- Learn AI Series (#92) - Audio Fundamentals for AI
- Learn AI Series (#93) - Speech Recognition
- Learn AI Series (#94) - Text-to-Speech (TTS)
- Learn AI Series (#95) - Audio Classification
- Learn AI Series (#96) - Music Generation
- Learn AI Series (#97) - Speaker Recognition and Diarization
- Learn AI Series (#98) - Natural Language Understanding for Voice
- Learn AI Series (#99) - Audio Enhancement
- Learn AI Series (#100) - Multimodal Audio-Visual Models
- Learn AI Series (#101) - Mini Project: Voice-Controlled AI Assistant
- Learn AI Series (#102) - What Is Reinforcement Learning?
- Learn AI Series (#103) - Multi-Armed Bandits
- Learn AI Series (#104) - Dynamic Programming
- Learn AI Series (#105) - Monte Carlo Methods
- Learn AI Series (#106) - Temporal Difference Learning
- Learn AI Series (#107) - Deep Q-Networks (DQN)
- Learn AI Series (#108) - Policy Gradient Methods
- Learn AI Series (#109) - Advanced Policy Optimization
- Learn AI Series (#110) - Model-Based Reinforcement Learning
- Learn AI Series (#111) - Multi-Agent Reinforcement Learning
- Learn AI Series (#112) - RL for Games
- Learn AI Series (#113) - RL for Real-World Applications
- Learn AI Series (#114) - Inverse Reinforcement Learning
- Learn AI Series (#115) - Offline Reinforcement Learning
- Learn AI Series (#116) - Mini Project: Training a Game-Playing AI
- Learn AI Series (#117) - ML System Design
- Learn AI Series (#118) - Data Engineering for AI
- Learn AI Series (#119) - Experiment Tracking and Reproducibility
- Learn AI Series (#120) - Model Optimization: Making Models Fast
- Learn AI Series (#121) - Model Serving Architecture
- Learn AI Series (#122) - Edge AI: Running Models on Devices
- Learn AI Series (#123) - Monitoring ML in Production
- Learn AI Series (#124) - CI/CD for Machine Learning
- Learn AI Series (#125) - GPU Programming Basics
- Learn AI Series (#126) - Distributed Training
- Learn AI Series (#127) - AI Security
- Learn AI Series (#128) - Privacy-Preserving AI
- Learn AI Series (#129) - AutoML and Neural Architecture Search
- Learn AI Series (#130) - Causal Inference and ML
- Learn AI Series (#131) - Graph Neural Networks
- Learn AI Series (#132) - AI for Structured Data
- Learn AI Series (#133) - Synthetic Data Generation
- Learn AI Series (#134) - AI Infrastructure Economics
- Learn AI Series (#135) - Building AI Teams and Processes
- Learn AI Series (#136) - Mini Project: Production AI Platform
- Learn AI Series (#137) - Foundation Models
- Learn AI Series (#138) - Multimodal AI
- Learn AI Series (#139) - AI for Code
- Learn AI Series (#140) - Scientific AI
- Learn AI Series (#141) - Robotics and Embodied AI
- Learn AI Series (#142) - AI Reasoning and Planning
- Learn AI Series (#143) - Continual Learning
- Learn AI Series (#144) - Few-Shot and Zero-Shot Learning
- Learn AI Series (#145) - Neuro-Symbolic AI (this post)
Learn AI Series (#145) - Neuro-Symbolic AI
I closed last episode with a deliberate cliffhanger, so let me pay it off before we do anything else. In #144 we spent the whole hour on few-shot and zero-shot learning -- prototypes, MAML, in-context prompts -- and I ended by pointing out that every one of those tricks, however clever, is still pure pattern recognition. Powerful pattern recognition, but pattern recognition. None of it has an explicit notion of a RULE. It cannot say "all A are B, this is an A, therefore it is a B" and be CERTAIN, the way a logic engine can. And I left the thread dangling with a promise: what happens if you bolt neural perception onto hard symbolic logic? That marriage has a name, and it's where we go today.
Because here's the thing that has been true since the 1950s -- there are two tribes in AI, and they have been arguing my entire lifetime. The symbolic tribe says intelligence is rules, logic, and structured knowledge. "A cat is a mammal. All mammals are warm-blooded. Therefore a cat is warm-blooded." Clean, interpretable, provably correct. That view powered expert systems, databases, and the first forty years of the field. The neural tribe says intelligence is learning patterns from data. Don't program the rules, LEARN them. That view gave us everything we've built across 144 episodes: CNNs, transformers, LLMs.
Both tribes have devastating weaknesses, and they are almost perfect mirror images of each other. Symbolic systems reason flawlessly but cannot perceive -- you cannot write if-then rules that robustly spot a cat in a photograph. Neural systems perceive brilliantly but reason unreliably -- ask an LLM to check whether a logical argument is valid and it will sometimes hand you a confident, fluent, completely wrong answer. Neuro-symbolic AI is the decades-long attempt to get both at once: neural perception feeding symbolic reasoning, or symbolic structure guiding neural learning. Whether it truly works is still an open question. Let's dive right in and look at how people are trying ;-)
Solutions to episode #144's exercises
House rules first, same as always -- we settle last time's homework before we open anything new. Episode #144 was few-shot and zero-shot, and all three tasks were about feeling the mechanism in your own hands rather than nodding along.
Exercise 1 -- 1-shot vs 5-shot. Take the synthetic-blob smoke test for PrototypicalNetwork and train two models, one with k_shot=1 and one with k_shot=5, everything else identical. Report the final query accuracy of each, and explain why more shots give a more reliable prototype.
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
class PrototypicalNetwork(nn.Module):
def __init__(self, input_dim=784, embed_dim=64):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, 256), nn.ReLU(),
nn.Linear(256, 128), nn.ReLU(),
nn.Linear(128, embed_dim),
)
def forward(self, support_x, support_y, query_x):
se = self.encoder(support_x); qe = self.encoder(query_x)
protos = torch.stack([se[support_y == c].mean(0)
for c in torch.unique(support_y)])
return -torch.cdist(qe, protos) # closer prototype = higher score
def make_dataset():
ds = {}
for c in range(10):
centre = torch.randn(784) * 3.0
ds[c] = centre + torch.randn(50, 784) * 0.5
return ds
def sample_episode(ds, n_way, k_shot, n_query):
classes = list(ds.keys())
sel = torch.randperm(len(classes))[:n_way]
sx, sy, qx, qy = [], [], [], []
for new, orig in enumerate(sel):
ex = ds[classes[orig]]; perm = torch.randperm(len(ex))
s, q = perm[:k_shot], perm[k_shot:k_shot + n_query]
sx.append(ex[s]); sy += [new] * k_shot
qx.append(ex[q]); qy += [new] * len(q)
return (torch.cat(sx), torch.tensor(sy), torch.cat(qx), torch.tensor(qy))
def run(k_shot, episodes=300):
torch.manual_seed(0)
ds, model = make_dataset(), PrototypicalNetwork()
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for _ in range(episodes):
sx, sy, qx, qy = sample_episode(ds, 5, k_shot, 15)
loss = F.cross_entropy(model(sx, sy, qx), qy)
opt.zero_grad(); loss.backward(); opt.step()
sx, sy, qx, qy = sample_episode(ds, 5, k_shot, 15)
return (model(sx, sy, qx).argmax(1) == qy).float().mean().item()
print(f"1-shot query accuracy: {run(1):.1%}")
print(f"5-shot query accuracy: {run(5):.1%}")
Both land high on these easy blobs, but the 5-shot run is steadier and edges ahead, and the reason is one sentence: a prototype is a MEAN, and the mean of five noisy samples sits much closer to the true class centre than the mean of one, because averaging K independent samples shrinks the noise by a factor of the square root of K. One shot IS the prototype, warts and all. Five shots average the warts away.
Exercise 2 -- Break the prototype with an outlier. Build a 3-way 5-shot support set by hand, compute the three prototypes, then corrupt ONE support example of class 0 with a huge vector. Recompute class 0's prototype and measure how far it moved.
import torch
torch.manual_seed(1)
centres = [torch.zeros(8), torch.ones(8) * 5, torch.ones(8) * -5]
support = {c: centres[c] + torch.randn(5, 8) * 0.3 for c in range(3)}
proto0_clean = support[0].mean(0)
corrupted = support[0].clone()
corrupted[0] = corrupted[0] + torch.ones(8) * 50.0 # one wild outlier
proto0_dirty = corrupted.mean(0)
shift = torch.norm(proto0_dirty - proto0_clean).item()
print(f"prototype moved by: {shift:.2f}")
print(f"clean proto[:3]: {proto0_clean[:3]}")
print(f"dirty proto[:3]: {proto0_dirty[:3]}")
print(f"median proto[:3]: {corrupted.median(0).values[:3]}") # robust alternative
One bad example drags the whole prototype roughly ten units off its true position -- catastrophic for a method that classifies by distance. The one-sentence why: the mean has a breakdown point of zero, meaning a SINGLE arbitrarily large outlier can move it arbitrarily far, so five clean points and one lunatic average to nonsense. The fix the exercise fished for: use a robust estimator instead -- the coordinate-wise median (printed on the last line) barely budges, and a trimmed mean that throws away the extremes before averaging is the pragmatic middle ground.
Exercise 3 -- Zero-shot vs few-shot prompt, by hand. Using build_prompt, construct a zero-shot and a 3-shot prompt for a new task of your choice, print both, and argue which you'd trust more on a genuinely ambiguous input.
def build_prompt(task_description, examples, query):
prompt = f"Task: {task_description}\n\n"
for i, (text, label) in enumerate(examples):
prompt += f"Example {i+1}:\nText: {text}\nLabel: {label}\n\n"
prompt += f"Now classify:\nText: {query}\nLabel:"
return prompt
task = "Classify the message as SPAM or HAM (not spam)."
ambiguous = "Congrats, your account review is complete - reply YES to continue."
zero = build_prompt(task, [], ambiguous)
few = build_prompt(task, [
("Win a FREE prize now, click here!!!", "SPAM"),
("Hey, are we still on for lunch tomorrow?", "HAM"),
("URGENT: verify your bank details immediately", "SPAM"),
], ambiguous)
print(zero); print("=" * 40); print(few)
Print both and the difference is obvious on the page: the zero-shot prompt only NAMES the task, while the few-shot prompt SHOWS the model where the decision boundary sits. On a borderline message like that fake "account review", I trust the few-shot version more -- and the two or three sentences the exercise wanted: the in-context examples pin down both the exact output vocabulary (SPAM/HAM, not a paragraph of waffle) and the STYLE of thing that counts as spam (urgency, reply-to-continue bait), so the model anchors the ambiguous case against concrete neighbours instead of a bare label name. And remember from #144 -- not a single weight changed to get that improvement. Right, homework settled. Now let's teach a network some actual facts ;-)
What neural networks can't do (and why)
Neural networks are function approximators -- we built that intuition all the way back in the forward-pass episode (#38). They learn continuous mappings from inputs to outputs, and they are genuinely incredible at it for perception: vision, language, audio. But they stumble on things that feel trivially easy to a symbolic system, and it is worth being precise about WHICH things, because that list is exactly what neuro-symbolic methods are trying to buy back.
Compositionality. "John is taller than Mary. Mary is taller than Sue. Is John taller than Sue?" Answering needs you to chain two facts through a rule. A neural network can memorise this specific pattern, but it does not reliably learn the transitive property as a general principle. Swap "taller than" for "older than" and it may fail, even though the logic is character-for-character identical. The rule lives in the data, not in the model.
Systematic generalization. Teach a model "red square" and "blue circle" and it ought to handle "red circle" and "blue square" for free -- it is just recombining known parts. Humans do this without blinking. Neural networks are famously shaky at this kind of combinatorial recombination, a complaint Fodor and Pylyshyn lodged against connectionism back in 1988 and which has never fully gone away.
Guaranteed correctness. A neural network's output is always a probability. It can be 99.9% confident and still be dead wrong. For domains where correctness is not negotiable -- medical dosing, legal reasoning, financial compliance -- "probably right" is a liability, not a feature.
What symbolic systems can't do (and why)
Flip the coin and the symbolic tribe has its own trio of miseries.
Perception. No hand-crafted rule base robustly classifies images, transcribes speech, or parses natural language in the wild. The real world is too messy, too continuous, too full of edge cases for brittle symbol-pushing. This is the wall that killed the expert-system boom of the 1980s.
Handling uncertainty. Symbols are true or false. Reality is probabilistic, noisy, ambiguous, and graded. A logic engine has no natural way to say "this is 0.7 of a cat".
Scalability. Rules are written by hand. To cover a new domain, some expert has to sit down and encode knowledge, painstakingly, forever. Neural networks learn from data -- scale the data, scale the knowledge, no human bottleneck. That single asymmetry is most of why the neural tribe won the last decade.
So the dream is obvious: keep neural perception, keep symbolic reasoning, and glue them together so each covers the other's blind spot. The rest of this episode is three honest attempts at that glue.
Knowledge graphs: structured neural reasoning
Start with the simplest bridge -- give the network some structured facts to reason over. A knowledge graph stores facts as (subject, relation, object) triples: (Paris, capital_of, France), (France, is_a, Country), (Paris, located_in, Europe). It is the same idea as a graph neural network's substrate (#131), but here the edges carry explicit, human-meaningful relation types.
The trick that makes it neural is knowledge graph embeddings: map every entity and every relation into a vector space (embeddings again -- #63), such that the geometry of that space encodes the logic of the graph. The classic method is TransE (Bordes et al., 2013), and its central idea is almost cheeky in its simplicity -- represent each relation as a TRANSLATION in embedding space.
import torch
import torch.nn as nn
class TransE(nn.Module):
"""TransE: knowledge graph embedding via translation."""
def __init__(self, n_entities, n_relations, embed_dim=100):
super().__init__()
self.entity_emb = nn.Embedding(n_entities, embed_dim)
self.relation_emb = nn.Embedding(n_relations, embed_dim)
nn.init.xavier_uniform_(self.entity_emb.weight)
nn.init.xavier_uniform_(self.relation_emb.weight)
def score(self, head, relation, tail):
"""Score a triple (h, r, t). TransE principle: h + r is close to t
for true triples, so a smaller distance means more likely true."""
h = self.entity_emb(head)
r = self.relation_emb(relation)
t = self.entity_emb(tail)
return -torch.norm(h + r - t, p=2, dim=-1) # higher = more plausible
def training_loss(self, pos_triples, neg_triples, margin=1.0):
"""Margin ranking loss: true triples must outscore corrupted ones."""
pos = self.score(*pos_triples)
neg = self.score(*neg_triples)
return torch.relu(margin - pos + neg).mean()
Read the score method slowly, because the whole method is in there. If (Paris, capital_of, France) is true, then the vector for "Paris" PLUS the vector for "capital_of" should land right on top of the vector for "France". Relations are little arrows you add. And once that geometry holds, you get reasoning for free: to answer "what is the capital of Germany?" you compute (Germany embedding) plus (capital_of embedding) and find the nearest entity. Here's that query in a few lines on a trained model:
def predict_tail(model, head_id, relation_id, top_k=3):
"""Rank all entities as candidate tails for (head, relation, ?)."""
h = model.entity_emb.weight[head_id]
r = model.relation_emb.weight[relation_id]
target = h + r # where the answer SHOULD sit
dists = torch.norm(model.entity_emb.weight - target, dim=1)
best = torch.topk(-dists, top_k).indices # nearest entities = answers
return best.tolist()
# after training: predict_tail(model, GERMANY, CAPITAL_OF) -> [BERLIN, ...]
Beautiful, but be honest about what it is: TransE learns statistical regularities in the graph, not logical rules. It will happily predict a plausible-looking false fact if the geometry drifts. It has no concept of PROOF. Which is exactly the gap the next method attacks.
Neural theorem provers
A neural theorem prover learns to chain logical rules to prove a statement -- but instead of hard-coded if-then rules, the rules are parameterized and learned from data. This is the line of work behind NeuralLP (Yang et al., 2017) and DRUM, and it is where "differentiable logic" stops being a slogan and becomes running code.
import torch
import torch.nn as nn
class NeuralRule(nn.Module):
"""A differentiable rule: prove r(X,Y) via some intermediate Z where
body1(X,Z) followed by body2(Z,Y) composes into r."""
def __init__(self, embed_dim=64, n_relations=20):
super().__init__()
self.body_relation_1 = nn.Linear(embed_dim, n_relations)
self.body_relation_2 = nn.Linear(embed_dim, n_relations)
self.confidence = nn.Parameter(torch.tensor(0.5))
def forward(self, entity_embs, relation_embs, query_relation):
# soft attention over which two relations form this rule's body
r1_w = torch.softmax(self.body_relation_1(query_relation), dim=-1)
r2_w = torch.softmax(self.body_relation_2(query_relation), dim=-1)
r1 = (r1_w.unsqueeze(-1) * relation_embs).sum(dim=-2)
r2 = (r2_w.unsqueeze(-1) * relation_embs).sum(dim=-2)
composed = r1 + r2 # TransE-style composition
score = -torch.norm(composed - query_relation, dim=-1)
return torch.sigmoid(self.confidence) * torch.sigmoid(score)
class NeuralTheoremProver(nn.Module):
"""Prove queries by learning and chaining soft rules."""
def __init__(self, n_entities, n_relations, embed_dim=64, n_rules=10):
super().__init__()
self.entity_emb = nn.Embedding(n_entities, embed_dim)
self.relation_emb = nn.Embedding(n_relations, embed_dim)
self.rules = nn.ModuleList(
[NeuralRule(embed_dim, n_relations) for _ in range(n_rules)]
)
def prove(self, head, relation, tail):
h = self.entity_emb(head)
r = self.relation_emb(relation)
t = self.entity_emb(tail)
direct = torch.sigmoid(-torch.norm(h + r - t, dim=-1)) # direct evidence
rule_scores = [rule(self.entity_emb.weight,
self.relation_emb.weight, r) for rule in self.rules]
all_scores = torch.stack([direct] + rule_scores)
# fuzzy OR: any proof path suffices -> 1 - product of (1 - each score)
return 1.0 - torch.prod(1.0 - all_scores, dim=0)
The system learns soft rules like "uncle(X,Y) if father(X,Z) and brother(Z,Y)", but represented as differentiable attention over relation types rather than as hand-written clauses. Because every operation is differentiable, you train the whole prover end-to-end with ordinary gradient descent. That last line is the quietly clever bit: it combines all the possible proof paths with a fuzzy OR -- one minus the product of the failure probabilities -- so the model rewards ANY working chain, exactly the way disjunction works in real logic. The appeal is real: you get symbolic-style compositional rules, but LEARNED from data instead of encoded by an expert. The catch is equally real -- these systems are finicky to train and struggle to scale past small, clean graphs. But as a proof of concept that logic can live inside a differentiable model, it is genuinely lovely.
Concept bottleneck models
The first two methods build reasoning engines. Concept bottleneck models (CBMs, Koh et al., 2020) take a far more pragmatic angle, and they are the one I would actually reach for in a production system today. The idea: force the network to first predict human-readable CONCEPTS, and only then predict the final answer from those concepts. You bolt an interpretable waist into the middle of the model.
import torch
import torch.nn as nn
class ConceptBottleneckModel(nn.Module):
"""Predict via interpretable concepts: input -> concepts -> class."""
def __init__(self, input_dim=2048, n_concepts=15, n_classes=200):
super().__init__()
self.concept_predictor = nn.Sequential(
nn.Linear(input_dim, 512), nn.ReLU(),
nn.Linear(512, n_concepts), nn.Sigmoid(), # each concept a probability
)
self.label_predictor = nn.Sequential(
nn.Linear(n_concepts, 128), nn.ReLU(),
nn.Linear(128, n_classes),
)
def forward(self, x, intervene=None):
concepts = self.concept_predictor(x)
if intervene is not None:
mask = (intervene >= 0) # -1 entries = no override
concepts = torch.where(mask, intervene, concepts)
return self.label_predictor(concepts), concepts
def training_loss(self, x, concept_labels, class_labels):
logits, pred_concepts = self.forward(x)
c_loss = nn.functional.binary_cross_entropy(pred_concepts, concept_labels)
y_loss = nn.functional.cross_entropy(logits, class_labels)
return c_loss + y_loss # learn BOTH jointly
Picture a bird classifier. The concepts might be "has red breast", "has long tail", "has webbed feet", "has hooked beak". The model predicts those attributes first, then classifies the species FROM them. The bottleneck buys you three things a black box cannot: you can INSPECT which concepts drove a decision ("robin, because red-breast=0.95, small-size=0.88"), a human expert can INTERVENE and correct a concept mid-inference, and you can DEBUG exactly where the reasoning went wrong -- at the concept step or the concept-to-class step. That intervention path is worth a tiny demo, because it is the whole selling point:
model = ConceptBottleneckModel(input_dim=2048, n_concepts=15, n_classes=200)
x = torch.randn(1, 2048)
logits_auto, concepts = model(x) # model decides everything
# expert overrides concept 0 ("red breast") to certainly-true, leaves rest alone
override = torch.full((1, 15), -1.0) # -1 = "don't touch this one"
override[0, 0] = 1.0
logits_fixed, _ = model(x, intervene=override)
print("changed prediction:",
logits_auto.argmax(1).item() != logits_fixed.argmax(1).item())
The cost is honest and worth stating: concept labels are expensive to collect (someone has to annotate "has red breast" on thousands of images), and the bottleneck can throttle expressiveness -- if a concept the model needs is not in your list, it literally cannot use a feature it can see but cannot name. A part from that, CBMs are the most deployable neuro-symbolic idea on this page, precisely because they don't try to reinvent logic; they just make the neural network show its work.
The integration challenge
Step back and you see the one deep problem sitting under all three methods. Neural networks are differentiable but approximate. Symbolic systems are exact but discrete. Gradient descent needs continuous, smooth surfaces to slide down; logic lives on hard true/false symbols with no gradient anywhere. Bridging them means either making logic differentiable, or making neural nets more discrete -- and both directions are active research.
The "make logic differentiable" route is the one you can actually feel in code. Replace hard boolean AND and OR with fuzzy versions that pass gradients: AND becomes multiplication, OR becomes that one-minus-product trick we already used in the theorem prover, NOT becomes one-minus-x.
import torch
def fuzzy_and(a, b): return a * b # both must be true
def fuzzy_or(a, b): return a + b - a * b # either suffices
def fuzzy_not(a): return 1.0 - a
# "(warm-blooded AND has-fur) OR lays-eggs-but-is-monotreme"
warm = torch.tensor(0.9, requires_grad=True)
fur = torch.tensor(0.8, requires_grad=True)
mono = torch.tensor(0.2, requires_grad=True)
is_mammal = fuzzy_or(fuzzy_and(warm, fur), mono)
is_mammal.backward() # gradients flow through logic
print(f"is_mammal = {is_mammal.item():.3f}, d/dwarm = {warm.grad.item():.3f}")
Run it and you get a graded truth value AND a gradient telling you how much each premise mattered -- logic you can train. This is the machinery behind probabilistic logic programming and systems like DeepProbLog, and it is genuinely the cleanest answer to "how do you backprop through a rule".
Now, the plot twist of 2026: current LLMs sidestep the whole discrete-versus-continuous headache by using TEXT as the shared representation. The model "reasons" in natural language, which is itself a symbolic system humans can read. Seen this way, RAG (episodes #64-65) is already a neuro-symbolic system -- neural retrieval bolted onto structured knowledge. An agent (episodes #67-68) that writes and executes code, or fires off a database query, is mixing neural intuition with exact symbolic computation through a clean interface. Whether we truly need deeply integrated neuro-symbolic architectures, or whether scaling LLMs plus tool-use quietly subsumes the whole symbolic side, is one of the genuinely open debates in the field right now. I do not think anyone honestly knows yet.
The pragmatic answer for building things TODAY, though, is unglamorous and reliable: use neural networks for perception and pattern-matching, use symbolic systems (a calculator, a database, a type checker, a logic solver) for the parts that must be exactly correct, and connect them with well-defined interfaces. You don't have to pick a tribe. You have to know which tool is load-bearing for which part of the job.
Exercises
Get your hands dirty before the next episode. Three tasks, climbing in difficulty:
Verify the TransE geometry by hand. Skip training entirely. Create a tiny
TransEwith 4 entities and 2 relations, then manually SET the embeddings so thatentity[PARIS] + relation[CAPITAL_OF]equalsentity[FRANCE]exactly (usetorch.no_grad()to assign.weightvalues). Callscoreon the true triple and on a false one, and confirm the true triple scores higher. In one sentence, explain what the margin loss would be pushing on if the false triple scored higher.Break the fuzzy OR. Using
fuzzy_and,fuzzy_or,fuzzy_not, build the truth value for "penguin is a bird" from premises "has-feathers=0.99" and "can-fly=0.05". Then show that fuzzy logic is NOT the same as boolean logic by evaluatingfuzzy_and(x, fuzzy_not(x))forx = 0.5-- in boolean logic that is always false (zero), so report what fuzzy logic gives instead and explain in one sentence why that non-zero value is both a feature and a bug.Intervene on a concept bottleneck. Take the
ConceptBottleneckModel, run a random input, then write a loop that overrides EACH concept to 1.0 one at a time (leaving the others untouched) and records how often the final predicted class changes. Report which single concept flips the prediction most easily, and explain in two sentences why that "concept sensitivity" is exactly the kind of debugging a plain black-box classifier cannot give you.
We'll open next episode with full solutions, as always.
Quick recap
- AI has two ancient tribes -- symbolic (rules, logic, provably correct, but blind) and neural (learns patterns, perceives brilliantly, but reasons fuzzily) -- and neuro-symbolic AI is the long attempt to weld their strengths together;
- knowledge graphs store facts as (subject, relation, object) triples, and TransE embeds them so relations act as translations (h + r lands near t), giving you missing-fact prediction for free -- but it learns statistics, not proofs;
- neural theorem provers learn differentiable, soft rules and chain them with a fuzzy OR, delivering symbolic-style compositional reasoning trained end-to-end by gradient descent -- elegant, but hard to scale;
- concept bottleneck models force prediction through human-readable concepts, buying inspection, expert intervention, and step-level debugging -- the most deployable idea here, at the cost of expensive concept labels and a capped expressiveness;
- the deep obstacle is bridging continuous/differentiable neural math with discrete/exact symbolic logic, and the practical trick is fuzzy logic (AND as multiply, OR as one-minus-product) so gradients flow through the rules;
- modern LLMs partly dodge the whole thing by reasoning in natural language and calling exact tools (RAG, code execution, database queries) -- and whether we still NEED deep neuro-symbolic architectures is genuinely unsettled.
And here's the thread I'll leave dangling for next time. Concept bottleneck models gave us a taste of something we have mostly ignored across 145 episodes: the ability to look INSIDE a model and understand WHY it decided what it decided. But a CBM only works because we DESIGNED it to be transparent -- we built the bottleneck on purpose. What about the millions of models that were NOT built that way? The transformer you fine-tuned, the CNN you downloaded, the giant LLM behind an API -- how do you pry open a black box that nobody designed to be opened, and actually trust what you find? That question has a whole field behind it, and it's where we head next ;-)