AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Category Theory Foundations: Categorical Perspective on Machine Learning and Data Flow

🔬
Beyond Syllabus — Enrichment Content

This chapter covers advanced research topics beyond standard CBSE/NCERT scope. It's designed for curious minds preparing for IIT-JEE Advanced, KVPY, or research-track studies. Core exam preparation does not require this material.

📚 Programming & Coding⏱️ 25 min read🎓 Grade 10🔬 Beyond Syllabus
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 25 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any machine learning pipeline you have ever written — even a small one, like a script that reads a CSV of cricket scores and predicts a player's next-match performance — and you will find the same shape hiding underneath the code. Raw data goes through a cleaning step. The cleaned data goes through a feature-extraction step. The features go through a model. The model's output goes through a decoding step that turns numbers back into a human-readable label. Four separate functions, chained end to end. You never think about them as one function, but the computer does: it composes them.

Here is the question this chapter answers: what mathematical object is a "pipeline that composes functions and never cares what's inside the boxes, only how the boxes connect"? The answer, discovered not by computer scientists but by two mathematicians (Samuel Eilenberg and Saunders Mac Lane) in 1945 while working on an entirely different problem in topology, is called a category. You already know a huge amount of category theory without the name — you have been composing functions since Class 10's Relations and Functions chapter. This chapter gives that intuition a formal skeleton, and then shows you why that skeleton is exactly the language modern deep-learning architecture papers use to describe neural network layers, data batching, and even backpropagation itself.

The Pipeline as a Chain of Functions

Consider a tiny, concrete ML-style pipeline. A student's raw exam score (out of 100) needs to become a one-hot encoded vector suitable for feeding into a downstream classifier. Three functions do the work:

def normalize(score):
    return score / 100          # f: raw score -> fraction in [0, 1]

def bucket(x):
    if x < 0.4:
        return 'low'
    elif x < 0.8:
        return 'mid'
    else:
        return 'high'           # g: fraction -> category label

def encode(label):
    order = ['low', 'mid', 'high']
    idx = order.index(label)
    return tuple(1 if i == idx else 0 for i in range(3))  # h: label -> one-hot tuple

Trace a score of 72 through the chain by hand, because tracing by hand is the only way to be sure you understand composition rather than just believing it:

normalize(72)        -> 0.72
bucket(0.72)          -> 'mid'      (since 0.4 <= 0.72 < 0.8)
encode('mid')         -> (0, 1, 0)  (index of 'mid' in order is 1)

In ordinary function notation, if f = normalize, g = bucket, h = encode, this pipeline computes h(g(f(72))), which mathematicians write compactly as (h ∘ g ∘ f)(72). The small circle ∘ is read "after" — h ∘ g means "do g, then do h". You met this exact notation in the Class 12 CBSE syllabus under "composition of functions"; a category is what you get when you stop asking what f, g, and h actually compute and start asking only: can they be chained, and does the chaining obey certain rules?

Formal Definition: What a Category Actually Is

A category C consists of exactly four ingredients, and nothing else is allowed to sneak in:

  • Objects. A collection of "things" — write it Ob(C). In our pipeline, reasonable objects are the sets RawScore, Fraction, Label, OneHot — the types the data lives in at each stage.
  • Morphisms (arrows). For every ordered pair of objects A, B, a collection of arrows f: A → B. Our functions normalize, bucket, encode are morphisms: normalize: RawScore → Fraction, and so on.
  • Composition. For any two morphisms f: A → B and g: B → C, there must exist a composite morphism g ∘ f: A → C. Crucially, the target of f must equal the source of g — you cannot compose two arrows unless the output of the first is a valid input to the second. This is the whole reason composition "type-checks" in a real pipeline: you cannot feed a one-hot tuple into bucket, because bucket expects a fraction.
  • Identity morphisms. For every object A, there is an identity arrow idA: A → A that does nothing.

And these ingredients must obey exactly two laws:

  1. Identity law: for any f: A → B, composing with identity changes nothing: f ∘ idA = f and idB ∘ f = f.
  2. Associativity law: for any composable chain f: A → B, g: B → C, h: C → D, it does not matter how you group the composition: (h ∘ g) ∘ f = h ∘ (g ∘ f).

That is the entire definition. No numbers, no geometry, no notion of "size" — just objects, arrows between them, a way to chain arrows, and two rules that keep the chaining sane. The famous slogan among category theorists is that a category is "objects and arrows, where all that matters is how the arrows compose" — the internal structure of the objects is deliberately thrown away. This is precisely why category theory turns out to be the right language for describing a data pipeline: a pipeline architect genuinely does not care whether Fraction is implemented as a Python float or a NumPy float32 — only that normalize produces something bucket can consume.

Verifying the Identity Law in Code

Let's not take the identity law on faith. Define a generic composer and an identity function, and check the law against real data:

def compose(f, g):
    """Returns f after g, i.e. the function x -> f(g(x))."""
    def h(x):
        return f(g(x))
    return h

def identity(x):
    return x

for score in [0, 50, 100]:
    left  = compose(normalize, identity)(score)   # normalize ∘ id
    right = compose(identity, normalize)(score)    # id ∘ normalize
    plain = normalize(score)
    print(score, left, right, plain, left == right == plain)

Trace it: for score = 0, identity(0) = 0, so compose(normalize, identity)(0) = normalize(0) = 0.0; and compose(identity, normalize)(0) = identity(normalize(0)) = identity(0.0) = 0.0. Both equal normalize(0) = 0.0, so the printed boolean is True. The same holds at 50 (all three equal 0.5) and 100 (all three equal 1.0). The identity law isn't a mysterious axiom — it is the precise mathematical statement of "a no-op step doesn't change your pipeline's output," which is exactly the property you rely on when you insert a debug pass-through stage into a real pipeline and expect nothing downstream to break.

Verifying Associativity — and Why IIT-JEE Cares

Run the three-stage pipeline both ways — group the first two steps first, or the last two steps first — and confirm they agree:

pipeline_a = compose(encode, compose(bucket, normalize))   # h ∘ (g ∘ f)
pipeline_b = compose(compose(encode, bucket), normalize)   # (h ∘ g) ∘ f

print(pipeline_a(72))   # (0, 1, 0)
print(pipeline_b(72))   # (0, 1, 0)
print(pipeline_a(72) == pipeline_b(72))   # True

Both grouped versions compute normalize(72) = 0.72, then bucket(0.72) = 'mid', then encode('mid') = (0, 1, 0) — the grouping of the parentheses never changed which computation actually happens, only the order in which you write it down. This is worth dwelling on because IIT-JEE and BITSAT function-composition problems routinely test exactly this fact in disguise: when a question asks you to simplify (f ∘ g) ∘ h (x) versus f ∘ (g ∘ h) (x), the "trick" is that there is no trick — associativity guarantees they are identical, so you are free to compute whichever grouping is arithmetically more convenient. Category theory is the reason that freedom is always safe, not just a coincidence of the specific functions in a given JEE question.

Misconception 1: "Category Theory Is About Classifying Objects"

The name "category theory" misleads almost every beginner the same way: it sounds like it should be about sorting objects into buckets — like Linnaean taxonomy for mathematical structures. It is not. Look back at the formal definition: the objects RawScore, Fraction, Label, OneHot are never inspected, opened up, or compared to each other for "similarity." Everything a category theorist can ever say about an object is said entirely in terms of the arrows going in and out of it. Two objects that have exactly the same arrows to and from every other object are, for the purposes of category theory, indistinguishable — this idea is formalized later (beyond this chapter's scope) as "isomorphism," but the intuition matters now: category theory is a theory of relationships between things, not a theory of the things themselves. A pipeline architect using this mindset asks "what can flow into this stage and what can flow out of it?" — never "what is this stage, deep down?" That relational, interface-first way of thinking is precisely the discipline that lets you swap out a neural network layer's internal implementation (say, replace a hand-written linear layer with a highly optimized cuBLAS call) without touching any other stage of the pipeline, as long as the input and output shapes — the arrows — stay the same.

Composition of Linear Maps: Where the Dimension Rule Actually Comes From

Function composition in a neural network is not composition of arbitrary Python functions — it is, at each linear layer, composition of linear maps represented by matrices. This is where category theory connects directly to the linear algebra you already study for Class 11-12 board exams, and where a dimension-matching rule needs to be derived carefully rather than memorized.

Let A be an m×n matrix. As a function, A represents a linear map TA: ℝⁿ → ℝᵐ — it eats a vector of length n (this is why it has n columns) and produces a vector of length m (this is why it has m rows). Let B be a p×q matrix, representing TB: ℝq → ℝᵖ.

Now ask: when can we form the composite "apply B, then apply A", written as the matrix product AB? A vector x ∈ ℝq goes in. B produces Bx ∈ ℝᵖ. For A to accept this output as its input, A's input dimension must match — and A's input dimension is n (the number of columns of A). So the composability condition is:

p = n

— the output dimension of B (which is p) must equal the input dimension of A (which is n). When this holds, the product AB is an m×q matrix representing the composite map q → ℝᵐ, exactly mirroring the categorical rule that f: A → B composed with g: B → C only exists when the middle objects match. This is precisely why, in a real neural network, the number of output units of one dense layer must equal the number of input units of the next layer — a mismatch there is not a stylistic error, it is the categorical composability condition failing, and every deep learning framework raises a shape-mismatch exception for exactly this reason. In JEE-style matrix algebra questions that ask you to re-parenthesize a triple product (AB)C versus A(BC), the reason both are always defined and always equal is the same associativity law proved above for the pipeline — matrices under multiplication form a category (objects = dimensions, morphisms = matrices) and inherit its laws for free.

Functors: Structure-Preserving Maps Between Categories

Once you have categories, the natural next question is: how do you compare two categories, or move information from one category to another, without destroying the compositional structure that makes a category a category? The answer is a functor. A functor F: C → D does two things at once:

  • sends every object A in C to an object F(A) in D;
  • sends every morphism f: A → B in C to a morphism F(f): F(A) → F(B) in D;

subject to two laws that should look very familiar, because they are the identity and associativity laws lifted one level up:

  1. F preserves identity: F(idA) = idF(A)
  2. F preserves composition: F(g ∘ f) = F(g) ∘ F(f)

Here is a functor you already use every time you write vectorized code: batching. Given any single-item function fn, define batch(fn) to be the function that applies fn to every element of a list. This is a functor from the category of single data points (Set) to the category of lists-of-data-points (also built from Set), because it maps every object X to the object "list of X" and every function to its element-wise counterpart.

def batch(fn):
    return lambda xs: [fn(x) for x in xs]

def classify(score):
    if score < 40:
        return 'low'
    elif score < 80:
        return 'mid'
    else:
        return 'high'

scores = [10, 72, 95]

Bid   = batch(identity)              # F(id) applied to the batch
left  = batch(compose(classify, identity))(scores)   # F(g ∘ f)
right = compose(batch(classify), batch(identity))(scores)  # F(g) ∘ F(f)

print(Bid(scores))   # [10, 72, 95]  -- Bid preserves the list exactly
print(left)          # ['low', 'mid', 'high']
print(right)         # ['low', 'mid', 'high']
print(left == right) # True

Trace it: Bid(scores) = [identity(10), identity(72), identity(95)] = [10, 72, 95], unchanged — this is functor law 1, F(id) = id, verified concretely: batching the identity does nothing to the list, exactly as batching nothing should. For left: compose(classify, identity) is the function x -> classify(identity(x)) = classify(x), so batching it over [10, 72, 95] gives [classify(10), classify(72), classify(95)] = ['low', 'mid', 'high'] (10 < 40 so 'low'; 40 ≤ 72 < 80 so 'mid'; 95 ≥ 80 so 'high'). For right: batch(identity)(scores) = [10, 72, 95] first (no change), then batch(classify) applied to that gives the same ['low', 'mid', 'high']. Both routes agree — this is functor law 2, verified concretely. In practice, this law is the mathematical guarantee behind an assumption every ML engineer makes without proof: that classifying-then-batching and batching-then-classifying a dataset produce identical results, which is what lets a deep learning framework freely choose to vectorize operations for speed without changing what your model computes.

Misconception 2: "Any Function Between Two Categories Is a Functor"

It is tempting to think a functor is just "some rule that turns each object and arrow of C into an object and arrow of D," full stop. It is not — the two functor laws are real constraints that a naively-written "structure map" can easily violate. Here is a deliberately broken candidate, and a check that catches it:

def broken_batch(fn):
    """Looks like a batching functor, but only evaluates fn on the FIRST
    element and repeats that result across the whole output list."""
    return lambda xs: [fn(xs[0])] * len(xs)

def check_identity_law(F, xs):
    return F(identity)(xs) == xs

print(check_identity_law(batch, scores))          # True
print(check_identity_law(broken_batch, scores))    # False

Trace the failing case: broken_batch(identity)(scores) computes identity(scores[0]) once — that's identity(10) = 10 — and repeats it three times, giving [10, 10, 10]. Compare against scores = [10, 72, 95]: they are not equal, so check_identity_law returns False. broken_batch is a perfectly well-typed Python function — it takes a function and returns a function on lists, exactly like batch does syntactically — but it is not a functor, because it fails to preserve the identity morphism: applying it to "do nothing" does not give you back "do nothing." This is worth internalizing precisely because bugs of this shape are common in real vectorized ML code: an operation that silently assumes all elements of a batch behave like the first one (a subtle indexing bug, or an accidental broadcast) breaks the functor law invisibly, and the code will run without crashing while quietly producing wrong batched results. Category theory turns "this feels wrong" into a checkable law.

A Genuine Diagram: The Commuting Triangle

The single most important picture in category theory is the commuting triangle, which is just associativity drawn as a shape instead of written as an equation. "Commutes" means: every path from one corner to another, no matter which arrows you follow, computes the same thing.

A C B f g g ∘ f Both paths A → C agree: going through B via f then g equals the direct arrow g ∘ f

Categorical Deep Learning: This Language Is Not Just an Analogy

It would be a stretch to claim that ML engineers sit down and write out category axioms before training a model — most don't, and you don't need to either. But the connection is not decorative. "Categorical deep learning" is an active area of machine learning research (associated with work by researchers including Bruno Gavranović, Petar Veličković, and collaborators around 2022–2024) that studies neural network architectures — convolutional layers, graph neural networks, transformers — as functors and natural transformations between categories built from the symmetries a problem needs to respect. A residual connection (the "skip connection" popularized by ResNet, where a layer computes x + F(x) instead of just F(x)) has a precise categorical description: it lives in an additive category, a category where morphisms between two objects can themselves be added together and there is a "zero morphism" that acts like doing nothing. The addition operation inside x + F(x) is exactly the additive structure an additive category equips its arrows with — it is not a coincidence of notation that residual connections are described this way in recent architecture papers; it is the same abstract pattern recurring at a higher level of the exact hierarchy this chapter has built: category → functor → additive category. You do not need to read that research to pass your board exam, but it is worth knowing this chapter's abstractions are not classroom fiction — they are the working vocabulary of a live research frontier.

Where This Maps to Your Exams

For CBSE Class 12 Mathematics, the Relations and Functions unit's treatment of composition of functions, associativity of composition, and identity functions is literally the categorical identity and associativity laws for the single category "Set" — you can now recognize board-exam proofs asking you to show (f ∘ g) ∘ h = f ∘ (g ∘ h) as a special case of a law that holds in every category, not a one-off algebraic curiosity. For IIT-JEE Main and Advanced, questions on composite and inverse functions, and matrix multiplication associativity, draw on exactly the two derivations worked through above — the dimension-compatibility rule for matrix products and the associativity proof for triple compositions. For BITSAT's algebra-of-functions questions, the same reasoning about domain and codomain matching (an arrow f: A → B composed with g: B → C requires the middle object to match) is the formal justification for "domain of g ∘ f is contained in the domain of f" rules taught by rote in coaching classes. And for students aiming at the Indian Statistical Institute's B.Stat/B.Math entrance exam or the Chennai Mathematical Institute entrance — both of which are known for testing abstract structural reasoning about sets, functions, and algebraic systems rather than computational speed — the habit this chapter builds, of stripping a problem down to "what are the objects, what are the arrows, do they compose," is directly the kind of thinking those exams reward.

Active Recall

  1. (Worked) A category has morphisms f: X → Y and g: Y → Z. Is f ∘ g defined? Why or why not, and what would need to be true for it to be defined?
    Answer: As written, no — f ∘ g means "do g first, then f," so its middle object requirement is: the target of g (which is Z) must equal the source of f (which is X). Unless X = Z, f ∘ g is not defined, even though g ∘ f: X → Z is perfectly well-defined. This is exactly why matrix multiplication is not commutative in general: AB and BA are different composability questions.
  2. (Worked) A 3×5 matrix A and a 5×2 matrix B. Is the product AB defined? What are its dimensions, and what map does it represent?
    Answer: A represents ℝ⁵ → ℝ³ (m=3, n=5); B represents ℝ² → ℝ⁵ (p=5, q=2). The composability condition derived above is p = n: here p = 5 and n = 5, so yes, AB is defined. It represents the composite map ℝ² → ℝ³ and is a 3×2 matrix.
  3. Write the two functor laws in your own words, using the batching functor as your example for each.
  4. Construct your own two-line counterexample function that fails the composition-preservation functor law (F(g ∘ f) ≠ F(g) ∘ F(f)) rather than the identity law, and show the failing trace by hand.
  5. In the pipeline category from this chapter, what is idFraction, concretely, as a one-line Python function? Verify that composing it with bucket on both sides gives back bucket unchanged, for the input 0.72.

Summary

A category packages objects, arrows between them, a rule for composing arrows, and identity arrows, under exactly two laws: identity does nothing, and composition is associative regardless of grouping. An ML pipeline is a chain of composable arrows in the category Set, and the composability condition — target of one arrow equals source of the next — is exactly the reason shape-mismatch errors happen in real code, and exactly the reason a matrix product AB requires the output dimension of B to equal the input dimension of A. Functors are structure-preserving maps between categories that must satisfy the identity and composition laws one level up; batching is a working, checkable example, and it is possible to write a function that looks like a functor but provably is not one, as the broken-batch counterexample showed. This categorical vocabulary — objects, morphisms, composition, functors — is not a classroom-only abstraction: it is the actively used language of categorical deep learning research describing why architectural choices like residual connections work the way they do, and the same reasoning about domains, composition, and associativity is precisely what CBSE, IIT-JEE, BITSAT, and India's top math-focused entrance exams test under the heading "composition of functions."

← Lie Groups and Symmetries: Continuous Groups in Deep Learning and Geometric ComputingAlgebraic Topology in Data: Homology, Cohomology, and Topological Data Analysis →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn