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

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

variant-c-11-teal.png

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 (#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:

  1. Verify the TransE geometry by hand. Skip training entirely. Create a tiny TransE with 4 entities and 2 relations, then manually SET the embeddings so that entity[PARIS] + relation[CAPITAL_OF] equals entity[FRANCE] exactly (use torch.no_grad() to assign .weight values). Call score on 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.

  2. 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 evaluating fuzzy_and(x, fuzzy_not(x)) for x = 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.

  3. 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 ;-)

Bedankt voor het lezen! Go wire up a five-fact knowledge graph and watch a network answer a question you never explicitly taught it -- that first correct multi-hop answer is a proper little kick. Tot de volgende keer! ;-)

@scipio



0
0
0.000
0 comments