Skip to content

Example: Creating An Optimization Problem For Hill Climbing

Johnny Knighten edited this page Sep 20, 2023 · 12 revisions

This is an example of creating an optimization problem that will be used by the hill climbing optimization framework. There are three main steps: create a class that represents a possible solution, create a class that generates random solutions, and create a class that represents the problem itself.

The Example Problem

For this example we will use the problem of optimizing a function. To make this problem more constrained, we will focus on functions with a single independent variable and a single variable ie(the mapping from R -> R). Some example of these types of functions are: f(x)= x^2, f(x)=log(1/x), f(x)=x^4. These function are often referred to as a real valued function with a single variable. To make the problem even more constrained we will only focus on minimization and we will only search for solutions over a range.

Step 1 - Create A Representation Of A Solution

For this problem a solution is just a random x value since we are searching for a single x value that minimizes the function. To create a representation of the solution we create a class that implements IHillClimbSolution, but will will add a method to get the x value stored.

Here is the entire class:

public class OneVarSolution implements IHillClimbSolution {

    private double score;
    private double xValue;

    public OneVarSolution(double xValue) {
        this.xValue = xValue;
    }

    public void getXValue() {
        return this.xValue;
    }

    @Override
    public double getScore() {
        return this.score;
    }

    @Override
    public void setScore(double score) {
        this.score = score;
    }
    
    @Override
    public String toString() {
        return "X Value - " + this.getXValue();
    }

}

Step 2 - Create A Class That Represents The Problem

Since this framework is developed using Java 1.8 we can make the problem class flexible and easy to use by representing the function being optimized with a lambda. To do this we first must create a functional interface to represent the desired lambda(takes in a single double and returns a double).

Here is the entire interface:

@FunctionalInterface
public interface IOneVariableFunction {
    double getFuncValue(double xValue);
}

Now we create the class that represents the problem. This class will implement IHillClimbProblem.

Here is the boilerplate:

public class MinimizeOneVar implements IHillClimbProblem {

    private IHillClimbSolution initialGuess;

    @Override
    public IHillClimbSolution getInitialGuess() {
        return this.initialGuess;
    }

    @Override
    public IHillClimbSolution getBestSolution(List<IHillClimbSolution> possibleSolns) {
        return null
    }

    @Override
    public boolean atPeakOrPlateau(IHillClimbSolution currentSolution, IHillClimbSolution newSolution) {
        return false;
    }

    @Override
    public boolean firstSolutionBetterThanOther(IHillClimbSolution current, IHillClimbSolution best) {
        return false;
    }

    @Override
    public double scoreSolution(IHillClimbSolution solution) {
        return 0.0;
    }

    @Override
    public List<IHillClimbSolution> generateNextSolutions(IHillClimbSolution solution) {
        return null;
    }

}

Now we will create the constructor. For starters we will need to store the function we are optimizing, so will supply it to the constructor. Next we need to focus on how we will create new solutions later on in the class because we will need to store some parameters for that task. Since a solution only contains a single x value there is only two ways to move, increase the x value or decrease the x value. We are going to represent our x value as a double; the big question is how much should we increase or decrease the x value. To make the class more flexible, we will allow the user to specify this amount. We will refer to this amount as the step size. The value of step size will be supplied to the constructor. Also we will account for only searching in a limited range of values. The search range will be defined by two doubles a minDomain and a maxDomain.

private IHillClimbSolution initialGuess;
private IOneVariableFunction function;
private double minDomain;
private double maxDomain;
private double stepSize;

public MinimizeOneVar(IHillClimbSolution initialGuess, IOneVariableFunction function, double minDomain, double maxDomain, double stepSize) {
    this.initialGuess = initialGuess;
    this.function = function;
    this.minDomain = minDomain;
    this.maxDomain = maxDomain;
    this.stepSize = stepSize;
}

To get the best solution out of the list we will find the one with the smallest score since we are minimizing.

Here is the code using a stream:

@Override
public IHillClimbSolution getBestSolution(List<IHillClimbSolution> possibleSolns) {
    return possibleSolns.stream()
               .min(Comparator.comparing(IHillClimbSolution::getScore))
               .get();
}

Next, we must be able to determine if we have found a peak/valley or a plateau in order to terminate the hill climb or trigger a random restart. Since we are minimizing a valley or plateau occurs if the current solution is less than or equal to the best new solution.

@Override
public boolean atPeakOrPlateau(IHillClimbSolution currentSolution, IHillClimbSolution newSolution) {
    return newSolution.getScore() >= currentSolution.getScore();
}

We need a method that compares two solutions and determines if the first is greater than the second. Since we are minimizing the first param is better than the second if it has a smaller score.

@Override
public boolean firstSolutionBetterThanOther(IHillClimbSolution current, IHillClimbSolution best) {
    return current.getScore() < best.getScore();
}

We need to be able to score a solution. Score for this problem is just the value of the function after plugging in a solution's x value. We just use the lambda to get the value.

@Override
public double scoreSolution(IHillClimbSolution solution) {
    return this.function.getFuncValue(((OneVarSolution) solution).getXValue());
}

Finally, we will implement the generateNextSolutions() method. We are going to generate a new OneVarSolution for each direction. So create one for xValue-stepSize and one for xValue+stepSize. Also if a solution is generated that goes past the search range we will not add it to the list of new solutions

@Override
public List<IHillClimbSolution> generateNextSolutions(IHillClimbSolution solution) {
    OneVarSolution solutionAsOneVar = (OneVarSolution) solution;
    List<IHillClimbSolution> list = new ArrayList<>();

    double largerValue = solutionAsOneVar.getXValue() + this.stepSize;
    if(largerValue <= this.maxDomain)
        list.add(new OneVarSolution(largerValue));

    double smallerValue = solutionAsOneVar.getXValue() - this.stepSize;
    if(smallerValue >= this.minDomain)
        list.add(new OneVarSolution(smallerValue));

    return list;
}

3 - Create A Class That Creates Random Solutions

Since our solution class is really simple, we just need to create a random number and use it to create a OneVarSolution. To make the generator consistent with the search range and step size, we will generate numbers that are in units of the step size and fit within the search range. This class must implement IHillClimbSolnGenerator.

Here is the entire class:

public class OneVarSolnGenerator implements IHillClimbSolnGenerator {

    private double minDomain;
    private double maxDomain;
    private double stepSize;
    private Random random;
    private int numberOfStepsInSearchRange;

    public OneVarSolnGenerator(double minDomain, double maxDomain, double stepSize, Random random) {
        this.minDomain = minDomain;
        this.maxDomain = maxDomain;
        this.stepSize = stepSize;
        this.random = random;

        this.numberOfStepsInSearchRange = (int) Math.floor((maxDomain-minDomain)/stepSize);
    }

    @Override
    public IHillClimbSolution randomSolution() {
        double randomValue = this.minDomain + this.stepSize * this.random.nextInt(numberOfStepsInSearchRange+1);
        return new OneVarSolution(randomValue, minDomain, maxDomain, stepSize);
    }

}

Step 4 - Putting It All Together And Starting Optimization

Here are two examples, one using normal hill climb and the other hill climb with random restarts

Here is an example of using normal hill climb for the function f(x)=x^2:

HillClimbParams params = new HillClimbParams();
params.setMaxIterations(10000);

IHillClimbSolution initialState = new OneVarSolution(-8.0, -10, 10, 1);

IHillClimbProblem problem = new MinimizeOneVar(initialState, (x) -> Math.pow(x, 2));

HillClimb climber = new HillClimb(problem, params);
IHillClimbSolution optimal = climber.optimize();

Here is an example of using hill climb with random restarts for the function f(x)=4x^6-5x^2+x-1:

HillClimbParams params = new HillClimbParams();
params.setMaxIterations(1000000);

IOneVariableFunction function = (x) -> 4*Math.pow(x, 6) - 5*Math.pow(x, 2) + x - 1;

IHillClimbSolution initialState = new OneVarSolution(2, -2, 2, .01);

IHillClimbSolnGenerator generator = new OneVarSolnGenerator(-2, 2, .01, new Random());
IHillClimbProblem problem = new MinimizeOneVar(initialState, function);

HillClimbRandRestart climber = new HillClimbRandRestart(problem, params, generator);
IHillClimbSolution optimal = climber.optimize();

Gists Of These Files (Links Now Dead After Account Migration)

Here are links to gists of the files created in this write up:

If you have any questions feel free to contact me!