Showing newest posts with label stochastic seach. Show older posts
Showing newest posts with label stochastic seach. Show older posts

Thursday, January 15, 2009

Adaptive Random Search: Varying step size based on performance

The Random Search algorithm involves the blind generation and evaluation of candidate solutions to a problem, where as the Localized Random Search algorithm makes the simple addition of successively varying the best solution found by the search so far. The Adaptive Random Search algorithm, like Localized Random Search is concerned with searching the neighborhood of the best solution found by the search so far, although unlike that algorithm the definition of neighborhood is not fixed. Instead it is changed dynamically over the course of the search.

When exploring the neighborhood of a candidate solution, the size of the steps taken within the neighborhood may be fixed in what is referred to as Fixed Step Size Random Search. The Adaptive Random Search algorithm involves varying the step size during the search in response to the relative improvements or non-improving steps as defined by the objective function evaluation. A small number of samples are taken each iteration with varied small and large step sizes in an attempt to approximate an optimal step size to converge the search to the local optimum. The step size that generate an improved candidate solution is adopted as the current step size for the search, and improved solutions are taken as the current working position. When a given step size stops producing improvements, its size is reduced to encourage the search algorithm to more carefully explore the smaller localized neighborhood. Larger step sizes are frequently explored in an effort to potentially escape local optima, and very large step sizes are sampled with a low frequency in an effort to explore distant regions of the search space.

Adaptive Random Search for Continuous Function Optimization
This guide provides an example of the Adaptive Random Search algorithm applied to an instance of the continuous function optimization problem.

The ExponentFunction class defines an instance of a function optimization problem with a variable number of function parameters or dimensions. The evaluate(vector) method expects a vector of real values in the problem bounds and evaluates it as the sum of the fourth power of each function parameter. The global optimum of this minimization function is located at 0.0 which is accessible via the optimal_score method and is used by is_optimal?(scoring) to test whether the algorithm has located to the global optimum. Finally, the problem provides a in_bounds?(vector) method for assessing whether a given vector falls within the hypercube of the valid problem space, useful for algorithms like Adaptive Random Search that generate new samples from existing samples and may stray out of the problem's bounds.
class ExponentFunction
attr_reader :dimensions, :min, :max

def initialize(dimensions=2)
@dimensions = dimensions
@min, @max = -5.12, +5.12
end

def evaluate(vector)
vector.inject(0) {|sum, x| sum + (x ** 4.0)}
end

def in_bounds?(vector)
vector.each {|x| return false if x>@max or x<@min}
return true
end

def is_optimal?(scoring)
scoring == optimal_score
end

def optimal_score
0.0
end

# true if s1 has a better score than s2
def is_better?(s1, s2)
s1 < s2 # minimizing
end
end
The Solution class provides a convenience container for holding immutable real-valued vectors in the @data instance variable, and a mutable @score instance variable for holing the costing of the vector assigned by a problems objective function.
class Solution
attr_reader :data
attr_accessor :score

def initialize(data)
@data = data
@score = 0.0/0.0 # NaN
end

def to_s
"[#{@data.inspect}] (#{@score})"
end
end
The AdaptiveRandomSearchAlgorithm class provides a reasonably generic implementation of the algorithm. The initialize(max_iterations) constructor expects a maximum number of algorithm iterations as an argument and goes on to define a set of algorithm configuration parameters as class instance variables. The search(problem) method is large and contains all the problem agnostic conditional logic of the algorithm. The search starts by generating a random starting point for the search (current) and setting an initial step size of 10% of the size of the search domain. The main loop of the algorithm is concerned with first generating a new sample using the step_size and a second sample using a larger step size. The larger step size is fixed at @small_factor * step_size although periodically (once every @large_factor_multiple iterations) a much larger factor (@large_factor) of the current step size is used.

If one of the generated steps results in a candidate solution better than the current solution it is replaced. If the larger step size resulted in the improvement, it is used the current step size. If neither generated step sizes resulted in an improved candidate solution, a count of non-improving steps is incremented. If this count exceeds @maximum_no_improvements then the current step size is reduced by a constant factor (step_size / @small_factor).

The generate_random_solution(problem) method used for the starting point of the search generates a random vector in the bounds of the problem space. The take_step(problem, current, step_size) is responsible for stochastically generating a new solution from a provided existing solution within a neighborhood defined by the provided step_size. This works by generating a random vector between +/- step_size which is then added to the current sample. This means the neighbourhood is a step_size sized hypercube around the origin. Steps are taken by this method until a valid (within the problem bounds) candidate solution is generated.
class AdaptiveRandomSearchAlgorithm
attr_accessor :max_iterations
attr_reader :best_solution

def initialize(max_iterations)
@max_iterations = max_iterations
@small_factor = 1.3 # normal step size incrase(*) and decrease(/) rate
@large_factor = 10 # step size factor increase for larger jump
@large_factor_multiple = 100 # max steps before larger jump is tried
@maximum_no_improvements = 50 # max non-improvement steps before size decreased
end

# execute a random search on the provided problem
def search(problem)
# random starting point
current = generate_random_solution(problem)
evaluate_candidate_solution(current, problem)
curr_it = 0
step_size = (problem.max-problem.min)*0.1 # 10% of the domain
no_change_cnt = 0
begin
# current step
step = take_step(problem, current, step_size)
evaluate_candidate_solution(step, problem)
# take second larger step
factor = @small_factor
factor = @large_factor if curr_it.modulo(@large_factor_multiple) == 0
larger_step_size = step_size * factor
larger_step = take_step(problem, current, larger_step_size)
evaluate_candidate_solution(larger_step, problem)
# check for improvement
if problem.is_better?(step.score, current.score) or
problem.is_better?(larger_step.score, current.score)
# select new step size if larger step is better
if problem.is_better?(larger_step.score, step.score)
step_size = larger_step_size # increase step size
current = larger_step # new best solution
else
current = step # new best solution
end
no_change_cnt = 0 # reset counter
# check for step size decrease
elsif (no_change_cnt+=1) >= @maximum_no_improvements
no_change_cnt = 0 # reset counter
step_size /= @small_factor # decrease step size
end
curr_it += 1
end until should_stop?(curr_it, problem)
return @best_solution
end

def should_stop?(curr_it, problem)
(curr_it >= @max_iterations) or problem.is_optimal?(best_solution.score)
end

def generate_random_solution(problem)
real_vector = Array.new(problem.dimensions) do
next_bfloat(problem.min, problem.max)
end
return Solution.new(real_vector)
end

def take_step(problem, current, step_size)
vector = nil
begin # keep stepping until a valid point is generated
step = Array.new(problem.dimensions) do
v = next_bfloat(-step_size, +step_size)
end
data = current.data
vector = Array.new(data.length) {|i| data[i] + step[i]}
end while !problem.in_bounds?(vector)
return Solution.new(vector)
end

def next_bfloat(min, max)
min + ((max - min) * rand)
end

def evaluate_candidate_solution(solution, problem)
solution.score = problem.evaluate(solution.data)
# keep track of the best solution found
if @best_solution.nil? or
problem.is_better?(solution.score, @best_solution.score)
@best_solution = solution
puts "> new best: #{solution.score}"
end
end
end
The algorithm is demonstrated by first seeding the global random number number generator to 1 to ensure the same sequence of random numbers is ued for each run. A new instance of the algorithm is created allowing a maximum of 5000 iterations, and an instance of the ExponentFunction problem is created in 5 dimensions. The search is executed by passing the problem to the algorithm, and the best result achieved by the algorithm is printed once the search is completed.
srand(1) # set the random number seed to 1
algorithm = AdaptiveRandomSearchAlgorithm.new(5000) # limit to 5000 iterations
problem = ExponentFunction.new(5) # create a problem with 5 dimensions
best = algorithm.search(problem) # execute the search
puts "Best Solution: #{best}" # display the best solution
Once the general approach is understood, one may begin playing with the parameters defined in the algorithms constructor that govern the dynamic adaptation of the step size. For example, smaller or larger factors may be used, and the frequency of large or small step size increase and decreases may be varied. The acceptance criteria for when the current working solution is replaced may be made probabilistic rather than deterministically greedy. Finally, one may experiment with hyper-spherical sampling neighbourhoods in the next_step function and with sampling distributions other than uniform such as Gaussian with the origin as the mean and the step_size as the standard deviation.

Source Code
Download: AdaptiveRandomSearch.rb

Further Reading

Papers:
Sites:

Have you found a bug? Know of a great reference? Got an idea for a guide? Please send an email to jasonb@InspiredAlgorithms.com.

Need Help? Want to discuss this post? Please visit the Inspired Algorithms User Group.

Saturday, January 10, 2009

Random Search: A baseline technique

The classical baseline for stochastic optimization is the random search algorithm, also referred to as blind search and stochastic generate and test. The strategy of this algorithm involves randomly generating viable solutions from the problems search space and evaluating them. It is used as a baseline for comparison because it is general enough to be applied to any problem for which candidate solutions can be generated and tested.

Each new randomly generated candidate solution is independent of the generated candidate solutions that came before it. Importantly, the algorithm keeps track of the candidate solution found so far using a simple ratcheting function (current best is only replaced by something as good as or better). Given that the random search algorithm samples the problem search space in an unbiased (uniform) manner it may discover areas of interest not considered by conventional wisdom.

The penalty of the algorithms simplicity and lack of memory is its inefficiency. The algorithm does not make use of any problem specific information such as gradient of the response surface. It may perform worse than a complete enumeration of the search space given that there are no mechanisms to bias or prevent the procedure from re-investigating already visited regions of the search space.

Random Search for Continuous Function Optimization
This guide demonstrates the random search algorithm applied to a simple summed squaring function. We start by defining the problem as a class with methods for assessing and comparing candidate solutions. The function is defined as the sum of the squared parameters, where the number of function parameters defines the difficulty or number of dimensions of the problem. Each function parameter is constrained between -5.12 and +5.12.

Candidate solutions are assessed via the evaluate method and are expected as arrays of floating point values. The global solution to the problem is known at 0.0 in each dimension which evaluates to the optimal score of 0.0. In one-dimension the function's response surface can be graphed which looks like the letter 'u' with the optima in the middle of the function. In two-dimensions the response surface looks like a basin. The known optimal scoring is made available in the is_optimal?(scoring) function, useful for an algorithm for accessing whether the optimal solution has been achieved. The problem also defines a solution score comparison, enforcing the minimization constrain of the problem.
class SquaringFunction
attr_reader :dimensions, :min, :max

def initialize(dimensions=2)
@dimensions = dimensions
@min, @max = -5.12, +5.12
end

def evaluate(vector)
vector.inject(0) {|sum, x| sum + (x ** 2.0)}
end

def is_optimal?(scoring)
scoring == optimal_score
end

def optimal_score
0.0
end

# true if s1 has a better score than s2
def is_better?(s1, s2)
s1 < s2 # minimizing
end
end
In order to address this problem using the random search algorithm, a candidate solution class and algorithm class are defined. The Solution class is not problem specific, proving a immutable container for solution data in an instance variable which must be set on construction, as well as a score which can be assigned at any time. A default score of NaN (not a number) is assigned defensively to ensure that an error occurs if the solution is not evaluated and is compared to another solution that is evaluated.
class Solution
attr_reader :data
attr_accessor :score

def initialize(data)
@data = data
@score = 0.0/0.0 # NaN
end

def to_s
"[#{@data.inspect}] (#{@score})"
end
end
The RandomSearchAlgorithm class requires a maximum number of iterations as parameters on construction which is stored as an instance variable. The core method of the class is search(problem) that executes a random search on the provided problem definition. A single iteration of the algorithm involves the generation of a random candidate solution and its evaluation. The algorithm loops until the stop condition should_stop?(curr_it, problem) is triggered which occurs if the maximum number of iterations have elapsed or if the optimal solution is discovered. The generation of random solutions in the generate_random_solution(problem) method is the only problem-specific logic in the algorithm, creating a random floating point vector within there bounds of the problem space. Finally, the evaluate_candidate_solution(solution, problem) method evaluates a given candidate solution against the problem definition and keeps track of the best solution found.
class RandomSearchAlgorithm
attr_accessor :max_iterations
attr_reader :best_solution

def initialize(max_iterations)
@max_iterations = max_iterations
end

# execute a random search on the provided problem
def search(problem)
@best_solution = nil
curr_it = 0
begin
candidate = generate_random_solution(problem)
evaluate_candidate_solution(candidate, problem)
curr_it += 1
end until should_stop?(curr_it, problem)
return @best_solution
end

def should_stop?(curr_it, problem)
(curr_it >= @max_iterations) or problem.is_optimal?(best_solution.score)
end

def generate_random_solution(problem)
real_vector = Array.new(problem.dimensions) do
next_bfloat(problem.min, problem.max)
end
return Solution.new(real_vector)
end

def next_bfloat(min, max)
min + ((max - min) * rand)
end

def evaluate_candidate_solution(solution, problem)
solution.score = problem.evaluate(solution.data)
# keep track of the best solution found
if @best_solution.nil? or
problem.is_better?(solution.score, @best_solution.score)
@best_solution = solution
puts " > new best: #{solution.score}"
end
end
end
Now we can execute the algorithm on the problem. This involves initializing the global random number generator to 1 and initializing the algorithm with the maximum number of 1000 iterations to execute. The problem instance is created with 5 decision variables (dimensions) and the algorithm is executed on the instance.
srand(1) # set the random number seed to 1
algorithm = RandomSearchAlgorithm.new(1000) # limit to 1000 iterations
problem = SquaringFunction.new(5) # create a problem with 5 dimensions
best = algorithm.search(problem) # execute the search
puts "Best Solution: #{best}" # display the best solution
This example provides a demonstration of a standard separation of problem, algorithm, and solution, as well as modularized responsibility within each class. For example, the RandomSearchAlgorithm class can be directly applied to any function optimization problem, or with a replacement of the generate_random_solution(problem) method it can be applied to any problem definition that aligns to the SquaringFunction classes simple interface.

Source Code
Download: RandomSearch.rb

Further Reading
Given the simplicity of the approach there are few resources dedicated to describing it. Instead the following provide some general resources for the broader field of stochastic optimization.

Books
Sites
To get help with or discuss this post, please visit the Inspired Algorithms User Group.