Tensor Regression

Hint: This topic is currently under development. The next update will include an improvement in performance and dimension handling.

Tensor regression is an advanced feature of GeneExpressionProgramming.jl that enables symbolic regression with vector and matrix data. This capability is particularly valuable for applications in computational mechanics, fluid dynamics, computer vision, medical data from (MRI), and other fields where the relationships involve higher-dimensional mathematical objects. The GeneExpressionProgramming.jl provides this capability either by the implementation of the M-GEP[1] or a general exploration.

Understanding Tensor Operations

Mathematical Background

Tensors are generalizations of scalars, vectors, and matrices to higher dimensions:

  • Rank 0 (Scalar): Single number
  • Rank 1 (Vector): Array of numbers with direction
  • Rank 2 (Matrix): 2D array of numbers
  • Rank n (Tensor): n-dimensional array

In the context of symbolic regression, tensor operations allow us to discover relationships involving:

  • Vector addition and subtraction
  • Dot products and cross products
  • Matrix multiplication and operations
  • Element-wise operations on tensors
  • Contraction
  • Norms and other tensor properties

Tensor Operations in GeneExpressionProgramming.jl

The package supports tensor operations through the GepTensorRegressor, which uses Flux.jl as the backend for efficient tensor computations. This enables the evolution of expressions that can handle complex mathematical structures while maintaining the interpretability of symbolic expressions.

Tensor Regression Example

This example demonstrates how to use tensor regression to discover relationships involving vector operations, simulating a scenario from computational fluid dynamics where we want to find a relationship between velocity vectors.

using GeneExpressionProgramming
using Random
using Tensors
using LinearAlgebra
using Statistics
using Plots

# Set random seed for reproducibility
Random.seed!(789)

println("=== Tensor Regression Example ===")
println("Discovering vector relationships in 3D space")
println("Target: a = 0.5*u1 + x2*u2 + 2*u3")
println("where u1, u2, u3 are 3D vectors and x2 is a scalar")
println()

# Problem setup
number_features = 5  # x1, x2, u1, u2, u3
n_samples = 1000

println("Generating tensor data...")

# Generate scalar features
x1 = [2.0 for _ in 1:n_samples]  # Constant scalar
x2 = randn(n_samples)            # Variable scalar

# Generate 3D vector features using Tensors.jl
u1 = [Tensor{1,3}(randn(3)) for _ in 1:n_samples]
u2 = [Tensor{1,3}(randn(3)) for _ in 1:n_samples]  
u3 = [Tensor{1,3}(randn(3)) for _ in 1:n_samples]

# Define the true relationship: a = 0.5*u1 + x2*u2 + 2*u3
a_true = [0.5 * u1[i] + x2[i] * u2[i] + 2.0 * u3[i] for i in 1:n_samples]

# Add small amount of noise to make it more realistic
noise_level = 0.01
a_noisy = [a_true[i] + noise_level * Tensor{1,3}(randn(3)) for i in 1:n_samples]

println("Data characteristics:")
println("  Samples: $n_samples")
println("  Scalar features: x1 (constant=2.0), x2 (variable)")
println("  Vector features: u1, u2, u3 (3D vectors)")
println("  Target: a (3D vector)")
println("  Noise level: $noise_level")
println()

# Organize input data
inputs = (x1, x2, u1, u2, u3)

println("Input data structure:")
println("  x1: $(length(x1)) scalars")
println("  x2: $(length(x2)) scalars") 
println("  u1: $(length(u1)) 3D vectors")
println("  u2: $(length(u2)) 3D vectors")
println("  u3: $(length(u3)) 3D vectors")
println()

# Create tensor regressor
regressor = GepTensorRegressor(
    number_features,
    gene_count=2,           # Number of genes (complexity control)
    head_len=3;             # Head length (expression depth control)
    feature_names=["x1", "x2", "U1", "U2", "U3"]  # Names for interpretability
)

println("Tensor regressor configuration:")
println("  Features: $number_features")
println("  Gene count: 2")
println("  Head length: 3")
println()

# Define custom loss function for tensor regression
@inline function tensor_loss(elem, validate::Bool)
    if isnan(mean(elem.fitness)) || validate
        try
            # Get the compiled model
            model = elem.compiled_function
            
            # Make predictions
            a_pred = model(inputs)
            
            # Check for valid predictions
            if !isfinite(norm(a_pred)) || size(a_pred) != size(a_noisy)
                elem.fitness = (typemax(Float64),)
                return
            end
            
            # Check individual vector sizes
            if length(a_pred) != length(a_noisy) || 
               (length(a_pred) > 0 && size(a_pred[1]) != size(a_noisy[1]))
                elem.fitness = (typemax(Float64),)
                return
            end
            
            # Calculate loss as norm of difference
            total_error = 0.0
            for i in 1:length(a_noisy)
                error_vec = a_pred[i] - a_noisy[i]
                total_error += norm(error_vec)^2
            end
            
            loss = total_error / length(a_noisy)  # Mean squared error
            elem.fitness = (loss,)
            
        catch e
            # Handle any evaluation errors
            elem.fitness = (typemax(Float64),)
        end
    end
end

# Evolution parameters
epochs = 100  # Reduced for tensor regression (computationally intensive)
population_size = 1000

println("Evolution parameters:")
println("  Epochs: $epochs")
println("  Population size: $population_size")
println("  Loss function: Custom tensor MSE")
println()

println("Starting tensor evolution...")
println("Note: Tensor operations are computationally intensive")
start_time = time()

# Train the tensor regressor
fit!(regressor, epochs, population_size, tensor_loss)

training_time = time() - start_time
println("Training completed in $(round(training_time, digits=2)) seconds")
println()

# Analyze results
println("=== Results Analysis ===")

# Get the best evolved solution
best_solution = regressor.best_models_[1]
best_model = best_solution.compiled_function
best_fitness = best_solution.fitness[1]

println("Best evolved expression:")
print_karva_strings(best_solution)  # Print in Karva notation
println()
println("Best fitness (MSE): $best_fitness")
println()

# Make predictions with the best model
println("Making predictions...")
try
    a_pred = best_model(inputs)
    
    # Calculate comprehensive metrics
    println("Prediction analysis:")
    println("  Predicted vectors: $(length(a_pred))")
    println("  Expected vectors: $(length(a_noisy))")
    
    if length(a_pred) == length(a_noisy)
        # Calculate vector-wise metrics
        vector_errors = [norm(a_pred[i] - a_noisy[i]) for i in 1:length(a_noisy)]
        component_errors = []
        
        for i in 1:length(a_noisy)
            for j in 1:3  # 3D vectors
                push!(component_errors, abs(a_pred[i][j] - a_noisy[i][j]))
            end
        end
        
        println("  Mean vector error: $(round(mean(vector_errors), digits=6))")
        println("  Max vector error: $(round(maximum(vector_errors), digits=6))")
        println("  Mean component error: $(round(mean(component_errors), digits=6))")
        println("  Max component error: $(round(maximum(component_errors), digits=6))")
        
        # Calculate R² equivalent for vectors
        total_variance = sum([norm(a_noisy[i] - mean(a_noisy))^2 for i in 1:length(a_noisy)])
        residual_variance = sum([norm(a_pred[i] - a_noisy[i])^2 for i in 1:length(a_noisy)])
        r2_equivalent = 1 - residual_variance / total_variance
        
        println("  R² equivalent: $(round(r2_equivalent, digits=6))")
        println()
        
        # Detailed component analysis
        println("Component-wise analysis:")
        for comp in 1:3
            true_comp = [a_noisy[i][comp] for i in 1:length(a_noisy)]
            pred_comp = [a_pred[i][comp] for i in 1:length(a_pred)]
            
            comp_correlation = cor(true_comp, pred_comp)
            comp_mse = mean((true_comp .- pred_comp).^2)
            
            println("  Component $comp:")
            println("    Correlation: $(round(comp_correlation, digits=4))")
            println("    MSE: $(round(comp_mse, digits=6))")
        end
        println()
        
        # Visualization
        println("Creating visualizations...")
        
        # 1. Component-wise accuracy plots
        p_components = plot(layout=(1,3), size=(1200, 300))
        
        for comp in 1:3
            true_comp = [a_noisy[i][comp] for i in 1:length(a_noisy)]
            pred_comp = [a_pred[i][comp] for i in 1:length(a_pred)]
            
            scatter!(p_components[comp], true_comp, pred_comp,
                     xlabel="True Component $comp",
                     ylabel="Predicted Component $comp",
                     title="Component $comp Accuracy",
                     alpha=0.6,
                     markersize=2,
                     legend=false)
            
            # Perfect prediction line
            min_val = min(minimum(true_comp), minimum(pred_comp))
            max_val = max(maximum(true_comp), maximum(pred_comp))
            plot!(p_components[comp], [min_val, max_val], [min_val, max_val],
                  color=:red, linestyle=:dash, linewidth=2)
        end
        
        savefig(p_components, "tensor_component_accuracy.png")
        println("Component accuracy plot saved as 'tensor_component_accuracy.png'")
        
        # 2. Vector magnitude comparison
        true_magnitudes = [norm(a_noisy[i]) for i in 1:length(a_noisy)]
        pred_magnitudes = [norm(a_pred[i]) for i in 1:length(a_pred)]
        
        p_magnitude = scatter(true_magnitudes, pred_magnitudes,
                             xlabel="True Vector Magnitude",
                             ylabel="Predicted Vector Magnitude",
                             title="Vector Magnitude Accuracy",
                             alpha=0.6,
                             markersize=3,
                             legend=false)
        
        min_mag = min(minimum(true_magnitudes), minimum(pred_magnitudes))
        max_mag = max(maximum(true_magnitudes), maximum(pred_magnitudes))
        plot!(p_magnitude, [min_mag, max_mag], [min_mag, max_mag],
              color=:red, linestyle=:dash, linewidth=2)
        
        savefig(p_magnitude, "tensor_magnitude_accuracy.png")
        println("Vector magnitude plot saved as 'tensor_magnitude_accuracy.png'")
        
        # 3. Error distribution
        p_error = histogram(vector_errors,
                           xlabel="Vector Error (L2 Norm)",
                           ylabel="Frequency",
                           title="Distribution of Vector Errors",
                           bins=30,
                           alpha=0.7,
                           legend=false)
        
        savefig(p_error, "tensor_error_distribution.png")
        println("Error distribution plot saved as 'tensor_error_distribution.png'")
        
        # 4. 3D visualization of a subset of vectors
        n_viz = min(50, length(a_noisy))  # Visualize subset for clarity
        indices = 1:n_viz
        
        p_3d = plot(layout=(1,2), size=(1000, 400))
        
        # True vectors
        for i in indices
            vec = a_noisy[i]
            plot!(p_3d[1], [0, vec[1]], [0, vec[2]], [0, vec[3]],
                  color=:blue, alpha=0.6, linewidth=1, legend=false)
        end
        plot!(p_3d[1], title="True Vectors (Sample)", xlabel="X", ylabel="Y", zlabel="Z")
        
        # Predicted vectors
        for i in indices
            vec = a_pred[i]
            plot!(p_3d[2], [0, vec[1]], [0, vec[2]], [0, vec[3]],
                  color=:red, alpha=0.6, linewidth=1, legend=false)
        end
        plot!(p_3d[2], title="Predicted Vectors (Sample)", xlabel="X", ylabel="Y", zlabel="Z")
        
        savefig(p_3d, "tensor_3d_visualization.png")
        println("3D vector visualization saved as 'tensor_3d_visualization.png'")
        
    else
        println("Warning: Prediction size mismatch")
        println("Expected: $(length(a_noisy)) vectors")
        println("Got: $(length(a_pred)) vectors")
    end
    
catch e
    println("Error during prediction analysis: $e")
end

println()

# Compare with true relationship
println("=== Comparison with True Relationship ===")
println("True relationship: a = 0.5*u1 + x2*u2 + 2*u3")

# Calculate predictions using true relationship
a_true_clean = [0.5 * u1[i] + x2[i] * u2[i] + 2.0 * u3[i] for i in 1:n_samples]

try
    a_pred = best_model(inputs)
    
    if length(a_pred) == length(a_true_clean)
        # Compare evolved expression with true relationship
        agreement_errors = [norm(a_pred[i] - a_true_clean[i]) for i in 1:length(a_true_clean)]
        mean_agreement_error = mean(agreement_errors)
        
        println("Agreement with true relationship:")
        println("  Mean error: $(round(mean_agreement_error, digits=6))")
        println("  Max error: $(round(maximum(agreement_errors), digits=6))")
        
        # Component-wise agreement
        for comp in 1:3
            true_comp = [a_true_clean[i][comp] for i in 1:length(a_true_clean)]
            pred_comp = [a_pred[i][comp] for i in 1:length(a_pred)]
            agreement_corr = cor(true_comp, pred_comp)
            println("  Component $comp correlation: $(round(agreement_corr, digits=4))")
        end
    end
catch e
    println("Error in true relationship comparison: $e")
end

println()

# Performance analysis
println("=== Performance Analysis ===")
println("Computational considerations:")
println("  Training time: $(round(training_time, digits=2)) seconds")
println("  Time per epoch: $(round(training_time/epochs, digits=2)) seconds")
println("  Population evaluations: $(epochs * population_size)")

# Memory usage estimation
vector_size = 3 * 8  # 3 components × 8 bytes per Float64
total_vector_memory = n_samples * 3 * vector_size  # 3 vector features
println("  Estimated vector memory: $(round(total_vector_memory/1024/1024, digits=2)) MB")

println()

# Advanced tensor operations example
println("=== Advanced Tensor Operations Example ===")
println("Demonstrating additional tensor capabilities...")

# Example with matrix operations
println("Matrix operation example:")
n_matrix_samples = 100

# Generate 2x2 matrices
M1 = [randn(2, 2) for _ in 1:n_matrix_samples]
M2 = [randn(2, 2) for _ in 1:n_matrix_samples]
scalars = randn(n_matrix_samples)

# True relationship: Result = scalar * M1 * M2
true_matrices = [scalars[i] * M1[i] * M2[i] for i in 1:n_matrix_samples]

println("  Generated $n_matrix_samples 2x2 matrices")
println("  Relationship: R = s * M1 * M2")
println("  This demonstrates matrix multiplication capabilities")

# Example with cross products
println()
println("Cross product example:")
v1_cross = [randn(3) for _ in 1:100]
v2_cross = [randn(3) for _ in 1:100]
cross_results = [cross(v1_cross[i], v2_cross[i]) for i in 1:100]

println("  Generated 100 3D vector pairs")
println("  Relationship: c = v1 × v2 (cross product)")
println("  This demonstrates vector cross product capabilities")

Acceleration

  • [ ] Under developement!

Best Practices Summary

1. Problem Formulation

  • Start with simple tensor operations before moving to complex ones
  • Ensure tensor dimensions are consistent throughout the problem
  • Consider the physical or mathematical meaning of tensor operations

2. Data Preparation

  • Normalize tensor components to similar scales
  • Handle tensor symmetries appropriately (e.g., stress/strain tensors)
  • Validate tensor data for physical plausibility

3. Algorithm Configuration

  • Use smaller populations initially due to computational cost
  • Reduce gene count and head length for faster convergence
  • Monitor memory usage and use batch processing if needed

4. Performance Optimization

  • Consider GPU acceleration for large problems
  • Use parallel processing when available
  • Implement efficient tensor operations

5. Result Validation

  • Validate results on independent test data
  • Check component-wise accuracy
  • Verify tensor properties (symmetry, positive definiteness, etc.)
  • Compare with known analytical solutions when available

Tensor regression in GeneExpressionProgramming.jl opens up further possibilities for discovering complex mathematical relationships involving higher-dimensional data structures. While computationally more demanding than scalar regression, it provides unique capabilities for applications in physics, engineering, and computer science where tensor operations are fundamental.

References

[1] Weatheritt, J., Sandberg, R. D. (2016) A novel evolutionary algorithm applied to algebraic modifications of the RANS stress–strain relationship. Journal of Computational Physics, 325, 22-37