Skip to content

Chapter 11: System.Numerics.Tensors - The Foundation of AI

Theoretical Foundations

In the realm of AI development, data is rarely static. It flows through pipelines: raw text is tokenized, numerical values are normalized, and noisy records are filtered out. To handle these transformations efficiently, especially with massive datasets, we must move beyond standard loops and embrace a functional, declarative mindset. This is where LINQ (Language Integrated Query) becomes the backbone of data preprocessing.

The Assembly Line Analogy

Imagine a factory assembly line producing cars. At the start, raw materials (steel sheets, engines, tires) are dumped onto a conveyor belt. As they move down the line, stations perform specific tasks: one station stamps the steel, another installs the engine, and a third paints the body.

Crucially, the car is not fully built until it reaches the end of the line. The stations do not work on every item simultaneously; they only process items as they arrive. This is Deferred Execution. The moment the car is complete and drives off the line, that is Immediate Execution.

In C#, LINQ queries behave exactly like this assembly line. When you define a query using Where or Select, you are not processing data yet. You are simply designing the stations and arranging the conveyor belt. The data only flows through these stations when you explicitly ask for the final product (e.g., by calling .ToList() or iterating over the query).

The Core Concept: Deferred vs. Immediate Execution

In the previous book, we explored System.Collections and IEnumerable<T>. We learned that iterating over a collection triggers the computation. LINQ extends this by introducing a chain of operations that are not executed until the sequence is enumerated.

Deferred Execution is the property where the execution of a query is postponed until the result is actually enumerated. This is incredibly powerful for performance. It allows you to build complex, multi-step queries without incurring the cost of intermediate collections or unnecessary calculations.

Immediate Execution forces the query to run right now and return a concrete result (like a List<T> or an Array). This is necessary when you need to materialize the data, perhaps to store it or to ensure the underlying data source (like a database connection) doesn't close before you're done.

Let's look at the difference in code.

using System;
using System.Collections.Generic;
using System.Linq;

public class ExecutionDemo
{
    public static void ShowDifference()
    {
        IEnumerable<int> numbers = new[] { 1, 2, 3, 4, 5 };

        // STATION 1: The Assembly Line (Deferred)
        // No code runs here. We are just defining the logic.
        var query = numbers
            .Where(n => n % 2 == 0) // Filter even numbers
            .Select(n => n * 10);   // Multiply by 10

        // STATION 2: Triggering the Assembly (Immediate)
        // The query executes now. The result is stored in a list.
        List<int> result = query.ToList(); 
        // Output: [20, 40, 60, 80, 100]

        // If we iterate over 'query' again, it re-runs the logic.
        foreach(var item in query) 
        {
            // This works, but if the source was a database query, 
            // it would hit the database again.
        }
    }
}

Pure Functional Pipelines in AI

In AI, specifically when preparing data for embeddings or neural networks, we must adhere to Pure Functional Programming principles. A "pure" function produces no side effects; given the same input, it always returns the same output and modifies nothing outside its scope.

Why is this critical? Imagine a .Select statement that normalizes a vector but also increments a global counter variable. If you run this query in parallel (using PLINQ), the counter becomes a race condition, leading to corrupted data and unpredictable bugs. Furthermore, side effects make code hard to test and debug.

The Rule: Inside a LINQ lambda (e.g., inside .Select(x => ...)), do not modify external variables. Treat the input data as immutable.

Example: Cleaning a Dataset for Embeddings

Let's say we have a raw dataset of user reviews. We need to filter out empty reviews, normalize the text, and convert them into numerical vectors (embeddings).

using System;
using System.Collections.Generic;
using System.Linq;

public class Review
{
    public string Text { get; set; }
    public bool IsSpam { get; set; }
}

public class DataPreprocessor
{
    // A mock embedding function (in reality, this would call an AI model)
    private static float[] GetEmbedding(string text)
    {
        // Simulates converting text to a vector of floats
        return new float[] { text.Length, text.Average(c => c), 0.0f };
    }

    public static List<float[]> ProcessReviews(IEnumerable<Review> rawReviews)
    {
        // THE PIPELINE
        // 1. Filter (Where): Remove spam and empty text
        // 2. Transform (Select): Convert text to embeddings
        // 3. Materialize (ToList): Execute the pipeline
        return rawReviews
            .Where(r => !r.IsSpam)                 // Filter 1
            .Where(r => !string.IsNullOrWhiteSpace(r.Text)) // Filter 2
            .Select(r => r.Text.Trim().ToLower())  // Normalize text
            .Select(t => GetEmbedding(t))          // Transform to Vector
            .ToList();                             // Immediate Execution
    }
}

Notice how the logic is declarative. We describe what we want (clean reviews as embeddings), not how to loop through them. This makes the code readable and less prone to off-by-one errors common in for loops.

Parallelism: The PLINQ Advantage

When processing massive datasets (millions of records), a single-threaded assembly line is too slow. We need multiple lines working simultaneously. PLINQ (Parallel LINQ) does exactly this by using the .AsParallel() extension method.

PLINQ automatically partitions the data and processes chunks on different CPU cores. However, this introduces complexity: the order of results is not guaranteed unless you explicitly sort them.

AI Context: In AI, we often shuffle datasets to prevent the model from learning patterns based on data order. PLINQ's non-deterministic ordering can act as a pseudo-shuffle, or we can use AsOrdered() to preserve sequence.

using System.Collections.Generic;
using System.Linq;

public class ParallelPreprocessor
{
    public static List<float[]> ProcessInParallel(List<Review> rawReviews)
    {
        // Using PLINQ to utilize multiple CPU cores
        return rawReviews
            .AsParallel() // Enables parallel execution
            .Where(r => !r.IsSpam)
            .Select(r => r.Text.Trim().ToLower())
            .Select(t => GetEmbedding(t))
            .ToList(); // Forces execution across threads and aggregates results
    }
}

The Danger of Side Effects in Parallel Pipelines

Let's look at what happens when we break the "Pure Functional" rule. This is a common anti-pattern.

// ANTI-PATTERN: DO NOT DO THIS
public static void BrokenParallelProcessing(List<Review> reviews)
{
    int processedCount = 0; // External state

    reviews
        .AsParallel()
        .ForAll(r => 
        {
            // RACE CONDITION!
            // Multiple threads try to write to 'processedCount' simultaneously.
            // The final value will be incorrect and unpredictable.
            processedCount++; 
            Console.WriteLine($"Processed: {r.Text}");
        });
}

Instead, we rely on the functional nature of LINQ. The result of the query is the data we care about. We don't need to manually count or track state; the Count() or ToList() methods provide that information safely.

Deferred Execution in Large-Scale Data

In AI, we often deal with datasets larger than available RAM. If we load a 50GB dataset into a List<T>, the application will crash. Deferred execution solves this via streaming.

Consider a scenario where we read a massive CSV file line by line.

using System.IO;
using System.Linq;

public class StreamingData
{
    public IEnumerable<string> ReadLines(string path)
    {
        // File.ReadLines uses deferred execution.
        // It reads one line at a time, not the whole file into memory.
        return File.ReadLines(path);
    }

    public void ProcessBigData(string path)
    {
        var lines = ReadLines(path);

        // The pipeline processes one line at a time.
        // Memory usage remains constant regardless of file size.
        var cleanData = lines
            .Where(l => !string.IsNullOrEmpty(l))
            .Select(l => l.Split(','))
            .Select(parts => new { Id = parts[0], Value = float.Parse(parts[1]) })
            .Take(1000); // Stop after 1000 records

        // Execution happens here, streaming through the file
        foreach(var item in cleanData)
        {
            // Process item
        }
    }
}

Visualizing the Pipeline

The following diagram illustrates how data flows through a LINQ pipeline, highlighting the separation between the query definition (Deferred) and the materialization (Immediate).

A diagram illustrating the LINQ pipeline shows data streaming through deferred query operations before being immediately materialized by the foreach loop.
Hold "Ctrl" to enable pan & zoom

A diagram illustrating the LINQ pipeline shows data streaming through deferred query operations before being immediately materialized by the `foreach` loop.

Summary of Architectural Implications

  1. Memory Efficiency: By using deferred execution with streaming sources (like File.ReadLines or database IQueryable), AI applications can process terabytes of data with minimal RAM footprint.
  2. Composability: Pipelines are highly composable. You can write a base query for filtering noise and extend it with specific transformations for different models without duplicating code.
  3. Testability: Pure functional pipelines are easy to unit test. You can feed a known input list and assert the output list matches expectations without mocking complex database states or global variables.

By mastering LINQ and PLINQ, you move from writing procedural scripts to engineering robust, scalable data pipelines—the essential foundation for any serious AI application.

Basic Code Example

using System;
using System.Collections.Generic;
using System.Linq;

public class DataPreprocessingPipeline
{
    public static void Main()
    {
        // 1. IMMEREDIATE EXECUTION: 
        // The .ToList() call forces the query to execute immediately.
        // This creates a concrete List<T> in memory, capturing the state at this moment.
        // In a real scenario, this might be a database call or a file read.
        List<UserData> rawData = GenerateRawData().ToList();
        Console.WriteLine("--- Raw Data (Snapshot) ---");
        rawData.ForEach(d => Console.WriteLine(d));

        // 2. DEFERRED EXECUTION:
        // This query is NOT executed yet. It is merely a definition, a blueprint of operations.
        // No filtering or calculation happens until the result is enumerated.
        // This is the core of the "Functional Pipeline".
        var preprocessingPipeline = rawData
            .Where(d => d.IsValid()) // Step A: Cleanse
            .Select(d => d.Normalize()) // Step B: Normalize
            .Select(d => d.NoiseReduction()); // Step C: Filter Noise

        // 3. TRIGGERING EXECUTION:
        // We iterate over the pipeline. Only NOW do the lambda expressions fire.
        // The pipeline executes in a single pass (streaming), optimizing memory usage.
        Console.WriteLine("\n--- Processed Data (Functional Pipeline) ---");
        foreach (var processedItem in preprocessingPipeline)
        {
            Console.WriteLine(processedItem);
        }
    }

    // Simulating a raw data source (e.g., CSV, API)
    static IEnumerable<UserData> GenerateRawData()
    {
        yield return new UserData { Id = 1, Value = 10.5f, IsValidFlag = true };
        yield return new UserData { Id = 2, Value = -5.2f, IsValidFlag = false }; // Invalid
        yield return new UserData { Id = 3, Value = 0.0f, IsValidFlag = true };   // Noise
        yield return new UserData { Id = 4, Value = 25.0f, IsValidFlag = true };
    }
}

// Simple POCO (Plain Old CLR Object) to represent a data point
public record UserData
{
    public int Id { get; set; }
    public float Value { get; set; }
    public bool IsValidFlag { get; set; }

    // Pure function: No side effects
    public bool IsValid() => IsValidFlag;

    // Pure function: Normalizes the value (e.g., Min-Max scaling logic)
    public UserData Normalize() => this with { Value = Value / 100.0f };

    // Pure function: Removes low-value noise
    public UserData NoiseReduction() => this with { Value = Math.Abs(Value) > 0.01f ? Value : 0.0f };

    public override string ToString() => $"[ID: {Id}, Val: {Value:F4}, Valid: {IsValidFlag}]";
}

Explanation of the Functional Pipeline

This example demonstrates the "Hello World" of AI data preprocessing using pure LINQ. In a real-world AI context, raw data is rarely ready for a neural network. It must be cleaned, normalized, and transformed. This code models that workflow.

  1. The Data Source (GenerateRawData): We simulate a stream of raw data using yield return. This creates an IEnumerable<UserData>. Crucially, this is "lazy." Nothing is loaded into memory until requested.

  2. The Pipeline Definition (preprocessingPipeline): This is the core logic. We chain three operations:

    • .Where(d => d.IsValid()): Filters out bad data. If IsValidFlag is false, the item is dropped.
    • .Select(d => d.Normalize()): Transforms the data. Here, we scale the Value property. In AI, this ensures all inputs have a similar range (e.g., 0 to 1).
    • .Select(d => d.NoiseReduction()): A second pass to clean data that might have passed the first filter (e.g., near-zero values that could skew training).
  3. Deferred Execution: Notice that preprocessingPipeline is defined before the foreach loop. At the moment of definition, no code runs. The Where and Select lambdas are not executed. This allows you to build complex queries dynamically based on runtime conditions without paying a performance cost until you actually need the data.

  4. Immediate Execution (Enumeration): The foreach loop (or a call to .ToList()) triggers the pipeline.

    • The loop asks for the first item.
    • The request flows backward through the pipeline.
    • GenerateRawData yields item 1.
    • Where checks it. Passes.
    • Select normalizes it.
    • Select reduces noise.
    • Item is returned to the foreach loop.
    • Crucially, if the loop stopped halfway, the remaining raw data would never be processed.

Visualizing the Pipeline

The flow of data through the functional chain can be visualized as a directed acyclic graph:

This diagram depicts a functional pipeline as a directed acyclic graph, illustrating how data flows through foreach loops and processing steps to ensure that raw data is fully consumed.
Hold "Ctrl" to enable pan & zoom

This diagram depicts a functional pipeline as a directed acyclic graph, illustrating how data flows through `foreach` loops and processing steps to ensure that raw data is fully consumed.

Common Pitfalls

1. Modifying State in Select (Side Effects)

  • The Mistake: rawData.Select(d => { globalCounter++; return d; })
  • Why it's dangerous: LINQ queries are not guaranteed to execute in order or even execute at all (e.g., if optimized away). Relying on side effects inside a query makes debugging a nightmare. In the context of System.Numerics.Tensors, this can lead to race conditions if AsParallel() is used later.
  • The Fix: Keep lambdas pure. Calculate values and return new objects, as shown in the Normalize() method.

2. Calling .ToList() Too Early

  • The Mistake: var list = rawData.ToList(); ... then applying filters.
  • Why it's dangerous: You force the entire dataset into memory immediately, losing the memory efficiency of streaming. If the dataset is 10GB, your app crashes.
  • The Fix: Keep the data as IEnumerable<T> and chain operations. Only call .ToList() or .ToArray() at the very end, when you need to pass it to a method that requires a collection.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.