Hooks
Hooks enable you to dynamically choose to inject functionality into algorithms. Some hooks are mandatory for certain algorithms, such as having a hook with the SemioticOpt.IsStoppingCondition trait when using SemioticOpt.GradientDescent. Others are purely there for you to use at your discretion. In this section, we'll take you through Traits, Using Predefined Hooks and how to Create Custom Hooks.
Traits
Hooks have traits. This is how our code knows when to execute which hook. For example, the code will execute a hook that has the SemioticOpt.IsStoppingCondition trait when evaluating whether it has finished optimising. A full list of traits follows.
StopTrait
This trait tells the code if it should execute a hook when checking stopping conditions. By default, the code automatically gives all hooks the SemioticOpt.IsNotStoppingCondition trait except in certain situations, which are explicitly documented with the hooks in question. We decide to stop code execution on an OR basis. This means that if any hook with SemioticOpt.IsStoppingCondition returns true, the code breaks out of the optimisation loop. Hooks with this trait must implement SemioticOpt.stophook.
SemioticOpt.StopTrait — TypeAbstract type for stopping conditions traits
SemioticOpt.IsStoppingCondition — TypeExhibited by hooks that are stopping conditions.
Stopping condition hooks must emit a boolean value when SemioticOpt.stophook is called. If multiple hooks meet IsStoppingCondition are instantiated at the same time, we assume that they are meant to be OR'ed, so if any of them is true, optimisation is finished. For more complex behaviour, consider defining a more complex function using a single StopWhen.
To set this trait for a hook, run
julia> StopTrait(::Type{MyHook}) = IsStoppingCondition()SemioticOpt.IsNotStoppingCondition — TypeExhibited by hooks that are not stopping conditions.
This is the default case. You should not have to set it manually for any hooks.
SemioticOpt.stophook — Functionstophook(::IsStoppingCondition, h::Hook, a::OptAlgorithm; locals...)Raise an error if the hook is a stopping condition but has not implemented SemioticOpt.stophook.
stophook(h::IsNotStoppingCondition, a::OptAlgorithm; locals...)If the hook isn't a stopping condition, it shouldn't be considered in the OR, so return false.
stophook(::IsStoppingCondition, h::StopWhen, a::OptAlgorithm; locals...)Call the stop-function on a and ;locals.
PostIterationTrait
This trait tells the code if it should execute the hook after it calls SemioticOpt.iteration. All hooks default to the SemioticOpt.DontRunAfterIteration trait unless otherwise documented. Hooks with this positive variant of this trait SemioticOpt.RunAfterIteration must return z, the output of iteration. Hooks with this trait must also implement SemioticOpt.postiterationhook.
SemioticOpt.PostIterationTrait — TypeAbstract type for trait denoting hooks that should run after an iteration finishes.
SemioticOpt.RunAfterIteration — TypeExhibited by hooks that run after an iteration finishes.
Such hooks must take the output of SemioticOpt.iteration z as input and return z back, potentially modified.
To set this trait for a hook, run
julia> PostIterationTrait(::Type{MyHook}) = RunAfterIteration()SemioticOpt.DontRunAfterIteration — TypeExhibited by hooks that don't run after an iteration finishes.
This is the default case. You should not have to set it manually for any hooks.
SemioticOpt.postiterationhook — Functionpostiterationhook(
::RunAfterIteration, h::Hook, a::OptAlgorithm, z::AbstractVector{T}; locals...
) where {T<:Real}Raise an error if the hook exhibits SemioticOpt.RunAfterIteration but has not implemented SemioticOpt.postiterationhook.
postiterationhook( ::DontRunAfterIteration, h::Hook, a::OptAlgorithm, z::AbstractVector{T}; locals... ) where {T<:Real}
If the hook shouldn't run after an iteration, just return z unmodified.
Using Predefined Hooks
Hooks always descend from the same abstract type.
SemioticOpt.Hook — TypeAbstract type for hooks.
SemioticOpt.Hooks — TypeA collection of hooks.Generally speaking, pre-defined hooks won't exhibit any positive traits unless explicitly documented otherwise. This serves two purposes. Firstly, it prevents any unexpected behaviour when the code executes. Secondly, it gives you more flexibility when choosing where you want a hook to be executed.
This section is incomplete! We will fill this out more once we have more traits and hooks to work with.
We recommend you read the below StopWhen section as it explains details that we won't cover in the later sections since it'd be too repetitive.
StopWhen
This hook has the SemioticOpt.IsStoppingCondition trait. To use it, you would specify a function that returns a boolean value. If said value is true, then the code breaks out of the optimisation loop.
Let's take an example from our tests to demonstrate how to specify this hook. We'll also implement a dummy optimisation function that just implements a counter for illustration purposes.
julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
juila> a = FakeOptAlg()
julia> function counter(h, a)
i = 0
while !shouldstop(h, a; Base.@locals()...)
i += 1
end
return i
end
julia> h = StopWhen((a; locals...) -> locals[:i] ≥ 5, Dict()) # Stop when i ≥ 5
julia> i = counter((h,), a)
5One thing you may not have seen is Base.@locals. This takes variables from the local scope (in this case, from the counter scope), and tracks them as a dictionary of symbols. Thus, since i is a local variable inside of counter, :i becomes a key in the Base.@locals dictionary. We pass this dictionary to the anonymous function stored by StopWhen. Then, we can use locals[:i] to get the value of i from the counter scope and check it against some condition. This is a powerful trick you may find yourself using a lot when dealing with hooks.
SemioticOpt.StopWhen — TypeStopWhen{F<:Function}(f::F)Stops optimisation when some condition is met.
The condition is set by f. Note that f gets access to variables in the SemioticOpt.minimize scope. This means, for example, that it can use locals[:z] to compute residuals. This has the SemioticOpt.IsStoppingCondition trait.
Logging
There are a few different types of Loggers. All exhibit the SemioticOpt.RunAfterIteration trait. They're used to log values from the optimisation loop. To use them, specify a function that gets a value from the SemioticOpt.maybeminimize! scope.
SemioticOpt.VectorLogger — TypeVectorLogger{I<:Integer,T,V<:AbstractVector{T},F<:Function} <: LoggerA hook name for logging a value specified by f into a vector data at some frequency. This hook exhibits the RunAfterIteration trait.
The value we want to log is returned by f. Note that f gets access to variables in the SemioticOpt.minimize scope. This means, for example, that it can use locals[:i] to store the iteration number.
You must also be careful to correctly set the type of the data vector.
julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
julia> a = FakeOptAlg()
julia> function counter(h, a)
i = 0
while !shouldstop(h, a; Base.@locals()...)
z = [1, 2]
i += 1
z = postiteration(h, a, z; Base.@locals()...)
end
return i
end
julia> stop = StopWhen((a; kws...) -> kws[:i] ≥ 5) # Stop when i ≥ 5
julia> h = VectorLogger(name="i", frequency=1, data=Int32[], f=(a; kws...) -> kws[:i])
julia> i = counter((h, stop), a)
julia> SemioticOpt.data(h)
5-element Vector{Int32}:
1
2
3
4
5SemioticOpt.ConsoleLogger — TypeConsoleLogger{I<:Integer,F<:Function} <: LoggerA hook name for logging a value specified by f to the console at some frequency. This hook exhibits the RunAfterIteration trait.
The value we want to log is returned by f. Note that f gets access to variables in the SemioticOpt.minimize scope. This means, for example, that it can use locals[:i] to store the iteration number.
julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
julia> a = FakeOptAlg()
julia> function counter(h, a)
i = 0
while !shouldstop(h, a; Base.@locals()...)
z = [1, 2]
i += 1
z = postiteration(h, a, z; Base.@locals()...)
end
return i
end
julia> stop = StopWhen((a; kws...) -> kws[:i] ≥ 5) # Stop when i ≥ 5
julia> h = ConsoleLogger(name="i", frequency=1, f=(a; kws...) -> kws[:i])
julia> _ = counter((h, stop), a);
i: 1
i: 2
i: 3
i: 4
i: 5Create Custom Hooks
When you create a custom hook, you need to follow three steps. The first is that you need to descend from SemioticOpt.Hook.
julia> using SemioticOpt
julia> struct MyHook <: Hook endThe second is that you need to ensure that you specify which traits you want that hook to exhibit. For example, let's say MyHook is a stopping condition. You'd want to implement.
julia> StopTrait(::Type{MyHook}) = IsStoppingCondition()Finally, if you need non-default behaviour for when the hook executes, you'll need to implement whatever function(s) the code calls for that trait-type. For IsStoppingCondition, that's stophook. Say we want MyHook to immediately cause optimisation to finish. We'd implement
julia> stophook(h::MyHook, a::SemioticOpt.OptAlgorithm; locals...) = trueThat's it! As long as your follow those three steps, you should be able to implement whatever hook you want!