C# OOP – Class Hierarchy for Generic Parameter Optimization

cobject-oriented-design

I am working on a code library containing data structures and algorithms for solving parameter optimization problems. A parameter optimization problem is a problem of the form: given a vector of parameters P, and a loss function L(P), find the vector components of P such that L(P) is minimized. The implementation of the parameters and the loss function are specific to the problem, but they can all be represented as floating point data types. So far I have an abstract base class called Parameters like so:

public abstract class Parameters
{
    /// <summary>
    /// Maps the names of the parameters to the values.
    /// </summary>
    public IImmutableDictionary<string, double> Params { get; }

    public Parameters(IImmutableDictionary<string, double> parameters)
    {
        Params = parameters;
    }

    /// <summary>
    /// Returns a string representation of the parameters.
    /// </summary>
    public override string ToString()
    {
        string parametersStr = $"{String.Join('\n', Params.Select(p => $"{p.Key} = {p.Value}"))}";
        return parametersStr;
    }
}

I also have a definition of a loss function like so:

/// <summary>
/// Calculates a fitness score for the given parameters.
/// <returns>The fitness of the parameters (higher values are better).</returns>
public delegate double FitnessFunction(Parameters parameters);

I can then define the contract for an IParameterOptimizer like so:

/// <summary>
/// Represents an object capable of optimizing parameters.
/// </summary>
public interface IParameterOptimizer
{
    /// <summary>
    /// Optimizes parameters using the given fitness function.
    /// </summary>
    /// <returns>A list of 2-tuples of parameters and fitness scores.</returns>
    public Task<IList<(Parameters, double)>> OptimizeParameters(FitnessFunction fitnessFunction);
}

So an object implementing IParameterOptimizer can return an ordered list of tuples mapping parameters to loss values, with more fit values appearing later in the list.

Now consider as a concrete example, a SalesParameters class, and a MaximizeProfit fitness function like so:

public class SalesParameters : Parameters
{
    public int ManufacturedUnits { get; }
    public double Price { get; }
    public double MarketingCost { get; }

    public SalesParameters(int units, double price, double marketing) : 
    base(new Dictionary<string, double>() {
        { "ManufacturedUnits", (double)ManufacturedUnits },
        { "Price", Price  },
        { "MarketingCost", MarketingCost },
    }.ToImmutableDictionary())
    {
        ManufacturedUnits = units;
        Price = price;
        MarketingCost = marketing;
    }
}

public double MaximizeProfit(SalesParameters p)
{
    return (p.ManufacturedUnits * p.Price) - p.MarketingCost;
}

The consumer of my library could then call one of my many implementations of IParameterOptimizer to find SalesParameters that will maximize their profits.

My question is – is this a sensible design? I find the Parameters dictionary mapping parameter names to values to be necessary to allow the optimization algorithms to know the type of operations that can be performed on the parameters (addition, subtraction, etc.). But it seems a little confusing and cumbersome for the user of the library to need to subclass Parameters and set the Params dictionary. Still, I can't think of a better solution as I would like to avoid using reflection.

Best Answer

I think the simplest and clearest solution is to avoid subclassing altogether. Instead, my package will contain a concrete Parameters class like so ->

public class Parameters
{
    /// <summary>
    /// Contains real number parameter values.
    /// </summary>
    public IImmutableList<double> Params { get; }

    public Parameters(IImmutableList<double> parameters)
    {
        Params = parameters;
    }
}

The consumer of my package will then be responsible for converting their problem specific data (SalesParameters in my example) into a Parameters object to interface with an IParameterOptimizer implementation.

This solution has the advantage of decoupling the data structures of the specific problem domain from the data structures and algorithms of the parameter optimization package. The only disadvantage is the consumer must write code to convert their specific object properties into a IImmutableList to instantiate a Parmaters object.

Related Topic