From 1fbe12149dfeae360f93e1c7d2c5d2da29aaa5eb Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 25 Oct 2020 19:31:12 +0300 Subject: [PATCH] Advanced configuration API for cm-optimization --- .space.kts | 4 +- .../DerivativeStructureExpression.kt | 2 +- .../optimization/CMOptimizationProblem.kt | 100 +++++++++++++++++ .../optimization/OptimizationProblem.kt | 17 +++ .../kmath/commons/optimization/optimize.kt | 105 +++--------------- .../commons/optimization/OptimizeTest.kt | 18 +-- .../expressions/DifferentiableExpression.kt | 21 +++- .../kmath/expressions/SimpleAutoDiff.kt | 17 +-- .../kmath/expressions/SymbolIndexer.kt | 18 ++- 9 files changed, 188 insertions(+), 114 deletions(-) create mode 100644 kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/CMOptimizationProblem.kt create mode 100644 kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/OptimizationProblem.kt diff --git a/.space.kts b/.space.kts index 9dda0cbf7..d70ad6d59 100644 --- a/.space.kts +++ b/.space.kts @@ -1 +1,3 @@ -job("Build") { gradlew("openjdk:11", "build") } +job("Build") { + gradlew("openjdk:11", "build") +} diff --git a/kmath-commons/src/main/kotlin/kscience/kmath/commons/expressions/DerivativeStructureExpression.kt b/kmath-commons/src/main/kotlin/kscience/kmath/commons/expressions/DerivativeStructureExpression.kt index a1ee91419..376fea7a3 100644 --- a/kmath-commons/src/main/kotlin/kscience/kmath/commons/expressions/DerivativeStructureExpression.kt +++ b/kmath-commons/src/main/kotlin/kscience/kmath/commons/expressions/DerivativeStructureExpression.kt @@ -106,7 +106,7 @@ public class DerivativeStructureExpression( /** * Get the derivative expression with given orders */ - public override fun derivative(orders: Map): Expression = Expression { arguments -> + public override fun derivativeOrNull(orders: Map): Expression = Expression { arguments -> with(DerivativeStructureField(orders.values.maxOrNull() ?: 0, arguments)) { function().derivative(orders) } } } diff --git a/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/CMOptimizationProblem.kt b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/CMOptimizationProblem.kt new file mode 100644 index 000000000..f7c136ed2 --- /dev/null +++ b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/CMOptimizationProblem.kt @@ -0,0 +1,100 @@ +package kscience.kmath.commons.optimization + +import kscience.kmath.expressions.* +import org.apache.commons.math3.optim.* +import org.apache.commons.math3.optim.nonlinear.scalar.GoalType +import org.apache.commons.math3.optim.nonlinear.scalar.MultivariateOptimizer +import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunction +import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunctionGradient +import org.apache.commons.math3.optim.nonlinear.scalar.gradient.NonLinearConjugateGradientOptimizer +import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.AbstractSimplex +import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.NelderMeadSimplex +import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer +import kotlin.reflect.KClass + +public operator fun PointValuePair.component1(): DoubleArray = point +public operator fun PointValuePair.component2(): Double = value + +public class CMOptimizationProblem( + override val symbols: List, +) : OptimizationProblem, SymbolIndexer { + protected val optimizationData: HashMap, OptimizationData> = HashMap() + private var optimizatorBuilder: (() -> MultivariateOptimizer)? = null + + public var convergenceChecker: ConvergenceChecker = SimpleValueChecker(DEFAULT_RELATIVE_TOLERANCE, + DEFAULT_ABSOLUTE_TOLERANCE, DEFAULT_MAX_ITER) + + private fun addOptimizationData(data: OptimizationData) { + optimizationData[data::class] = data + } + + init { + addOptimizationData(MaxEval.unlimited()) + } + + public fun initialGuess(map: Map): Unit { + addOptimizationData(InitialGuess(map.toDoubleArray())) + } + + public fun expression(expression: Expression): Unit { + val objectiveFunction = ObjectiveFunction { + val args = it.toMap() + expression(args) + } + addOptimizationData(objectiveFunction) + } + + public fun derivatives(expression: DifferentiableExpression): Unit { + expression(expression) + val gradientFunction = ObjectiveFunctionGradient { + val args = it.toMap() + DoubleArray(symbols.size) { index -> + expression.derivative(symbols[index])(args) + } + } + addOptimizationData(gradientFunction) + if (optimizatorBuilder == null) { + optimizatorBuilder = { + NonLinearConjugateGradientOptimizer( + NonLinearConjugateGradientOptimizer.Formula.FLETCHER_REEVES, + convergenceChecker + ) + } + } + } + + public fun simplex(simplex: AbstractSimplex) { + addOptimizationData(simplex) + //Set optimization builder to simplex if it is not present + if (optimizatorBuilder == null) { + optimizatorBuilder = { SimplexOptimizer(convergenceChecker) } + } + } + + public fun simplexSteps(steps: Map) { + simplex(NelderMeadSimplex(steps.toDoubleArray())) + } + + public fun goal(goalType: GoalType) { + addOptimizationData(goalType) + } + + public fun optimizer(block: () -> MultivariateOptimizer) { + optimizatorBuilder = block + } + + override fun optimize(): OptimizationResult { + val optimizer = optimizatorBuilder?.invoke() ?: error("Optimizer not defined") + val (point, value) = optimizer.optimize(*optimizationData.values.toTypedArray()) + return OptimizationResult(point.toMap(), value) + } + + public companion object { + public const val DEFAULT_RELATIVE_TOLERANCE: Double = 1e-4 + public const val DEFAULT_ABSOLUTE_TOLERANCE: Double = 1e-4 + public const val DEFAULT_MAX_ITER: Int = 1000 + } +} + +public fun CMOptimizationProblem.initialGuess(vararg pairs: Pair): Unit = initialGuess(pairs.toMap()) +public fun CMOptimizationProblem.simplexSteps(vararg pairs: Pair): Unit = simplexSteps(pairs.toMap()) diff --git a/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/OptimizationProblem.kt b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/OptimizationProblem.kt new file mode 100644 index 000000000..56291e09c --- /dev/null +++ b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/OptimizationProblem.kt @@ -0,0 +1,17 @@ +package kscience.kmath.commons.optimization + +import kscience.kmath.expressions.Symbol +import kotlin.reflect.KClass + +public typealias ParameterSpacePoint = Map + +public class OptimizationResult( + public val point: ParameterSpacePoint, + public val value: T, + public val extra: Map, Any> = emptyMap() +) + +public interface OptimizationProblem { + public fun optimize(): OptimizationResult +} + diff --git a/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/optimize.kt b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/optimize.kt index 3bf6354ea..a49949b93 100644 --- a/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/optimize.kt +++ b/kmath-commons/src/main/kotlin/kscience/kmath/commons/optimization/optimize.kt @@ -1,103 +1,32 @@ package kscience.kmath.commons.optimization -import kscience.kmath.expressions.* -import org.apache.commons.math3.optim.* -import org.apache.commons.math3.optim.nonlinear.scalar.GoalType -import org.apache.commons.math3.optim.nonlinear.scalar.MultivariateOptimizer -import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunction -import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunctionGradient -import org.apache.commons.math3.optim.nonlinear.scalar.gradient.NonLinearConjugateGradientOptimizer -import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.NelderMeadSimplex -import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer +import kscience.kmath.expressions.DifferentiableExpression +import kscience.kmath.expressions.Expression +import kscience.kmath.expressions.Symbol -public typealias ParameterSpacePoint = Map - -public class OptimizationResult(public val point: ParameterSpacePoint, public val value: Double) - -public operator fun PointValuePair.component1(): DoubleArray = point -public operator fun PointValuePair.component2(): Double = value - -public object Optimization { - public const val DEFAULT_RELATIVE_TOLERANCE: Double = 1e-4 - public const val DEFAULT_ABSOLUTE_TOLERANCE: Double = 1e-4 - public const val DEFAULT_MAX_ITER: Int = 1000 -} - - -private fun SymbolIndexer.objectiveFunction(expression: Expression) = ObjectiveFunction { - val args = it.toMap() - expression(args) -} - -private fun SymbolIndexer.objectiveFunctionGradient( - expression: DifferentiableExpression, -) = ObjectiveFunctionGradient { - val args = it.toMap() - DoubleArray(symbols.size) { index -> - expression.derivative(symbols[index])(args) - } -} - -private fun SymbolIndexer.initialGuess(point: ParameterSpacePoint) = InitialGuess(point.toArray()) /** * Optimize expression without derivatives */ public fun Expression.optimize( - startingPoint: ParameterSpacePoint, - goalType: GoalType = GoalType.MAXIMIZE, - vararg additionalArguments: OptimizationData, - optimizerBuilder: () -> MultivariateOptimizer = { - SimplexOptimizer( - SimpleValueChecker( - Optimization.DEFAULT_RELATIVE_TOLERANCE, - Optimization.DEFAULT_ABSOLUTE_TOLERANCE, - Optimization.DEFAULT_MAX_ITER - ) - ) - }, -): OptimizationResult = withSymbols(startingPoint.keys) { - val optimizer = optimizerBuilder() - val objectiveFunction = objectiveFunction(this@optimize) - val (point, value) = optimizer.optimize( - objectiveFunction, - initialGuess(startingPoint), - goalType, - MaxEval.unlimited(), - NelderMeadSimplex(symbols.size, 1.0), - *additionalArguments - ) - OptimizationResult(point.toMap(), value) + vararg symbols: Symbol, + configuration: CMOptimizationProblem.() -> Unit, +): OptimizationResult { + require(symbols.isNotEmpty()) { "Must provide a list of symbols for optimization" } + val problem = CMOptimizationProblem(symbols.toList()).apply(configuration).apply(configuration) + problem.expression(this) + return problem.optimize() } /** * Optimize differentiable expression */ public fun DifferentiableExpression.optimize( - startingPoint: ParameterSpacePoint, - goalType: GoalType = GoalType.MAXIMIZE, - vararg additionalArguments: OptimizationData, - optimizerBuilder: () -> NonLinearConjugateGradientOptimizer = { - NonLinearConjugateGradientOptimizer( - NonLinearConjugateGradientOptimizer.Formula.FLETCHER_REEVES, - SimpleValueChecker( - Optimization.DEFAULT_RELATIVE_TOLERANCE, - Optimization.DEFAULT_ABSOLUTE_TOLERANCE, - Optimization.DEFAULT_MAX_ITER - ) - ) - }, -): OptimizationResult = withSymbols(startingPoint.keys) { - val optimizer = optimizerBuilder() - val objectiveFunction = objectiveFunction(this@optimize) - val objectiveGradient = objectiveFunctionGradient(this@optimize) - val (point, value) = optimizer.optimize( - objectiveFunction, - objectiveGradient, - initialGuess(startingPoint), - goalType, - MaxEval.unlimited(), - *additionalArguments - ) - OptimizationResult(point.toMap(), value) + vararg symbols: Symbol, + configuration: CMOptimizationProblem.() -> Unit, +): OptimizationResult { + require(symbols.isNotEmpty()) { "Must provide a list of symbols for optimization" } + val problem = CMOptimizationProblem(symbols.toList()).apply(configuration).apply(configuration) + problem.derivatives(this) + return problem.optimize() } \ No newline at end of file diff --git a/kmath-commons/src/test/kotlin/kscience/kmath/commons/optimization/OptimizeTest.kt b/kmath-commons/src/test/kotlin/kscience/kmath/commons/optimization/OptimizeTest.kt index 779f37dad..65d61dcd1 100644 --- a/kmath-commons/src/test/kotlin/kscience/kmath/commons/optimization/OptimizeTest.kt +++ b/kmath-commons/src/test/kotlin/kscience/kmath/commons/optimization/OptimizeTest.kt @@ -1,10 +1,7 @@ package kscience.kmath.commons.optimization import kscience.kmath.commons.expressions.DerivativeStructureExpression -import kscience.kmath.expressions.Expression -import kscience.kmath.expressions.Symbol import kscience.kmath.expressions.symbol -import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer import org.junit.jupiter.api.Test internal class OptimizeTest { @@ -14,22 +11,25 @@ internal class OptimizeTest { val normal = DerivativeStructureExpression { val x = bind(x) val y = bind(y) - exp(-x.pow(2)/2) + exp(-y.pow(2)/2) + exp(-x.pow(2) / 2) + exp(-y.pow(2) / 2) } - val startingPoint: Map = mapOf(x to 1.0, y to 1.0) - @Test fun testOptimization() { - val result = normal.optimize(startingPoint) + val result = normal.optimize(x, y) { + initialGuess(x to 1.0, y to 1.0) + //no need to select optimizer. Gradient optimizer is used by default + } println(result.point) println(result.value) } @Test fun testSimplexOptimization() { - val result = (normal as Expression).optimize(startingPoint){ - SimplexOptimizer(1e-4,1e-4) + val result = normal.optimize(x, y) { + initialGuess(x to 1.0, y to 1.0) + simplexSteps(x to 2.0, y to 0.5) + //this sets simplex optimizer } println(result.point) println(result.value) diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/DifferentiableExpression.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/DifferentiableExpression.kt index 841531d01..5fe31caca 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/DifferentiableExpression.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/DifferentiableExpression.kt @@ -4,9 +4,15 @@ package kscience.kmath.expressions * And object that could be differentiated */ public interface Differentiable { - public fun derivative(orders: Map): T + public fun derivativeOrNull(orders: Map): T? } +public fun Differentiable.derivative(orders: Map): T = + derivativeOrNull(orders) ?: error("Derivative with orders $orders not provided") + +/** + * An expression that provid + */ public interface DifferentiableExpression : Differentiable>, Expression public fun DifferentiableExpression.derivative(vararg orders: Pair): Expression = @@ -14,8 +20,19 @@ public fun DifferentiableExpression.derivative(vararg orders: Pair DifferentiableExpression.derivative(symbol: Symbol): Expression = derivative(symbol to 1) -public fun DifferentiableExpression.derivative(name: String): Expression = derivative(StringSymbol(name) to 1) +public fun DifferentiableExpression.derivative(name: String): Expression = + derivative(StringSymbol(name) to 1) //public interface DifferentiableExpressionBuilder>: ExpressionBuilder { // public override fun expression(block: A.() -> E): DifferentiableExpression //} + +public abstract class FirstDerivativeExpression : DifferentiableExpression { + + public abstract fun derivativeOrNull(symbol: Symbol): Expression? + + public override fun derivativeOrNull(orders: Map): Expression? { + val dSymbol = orders.entries.singleOrNull { it.value == 1 }?.key ?: return null + return derivativeOrNull(dSymbol) + } +} \ No newline at end of file diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SimpleAutoDiff.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SimpleAutoDiff.kt index e5ea33c81..6231a40c1 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SimpleAutoDiff.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SimpleAutoDiff.kt @@ -221,23 +221,16 @@ private class AutoDiffContext>( public class SimpleAutoDiffExpression>( public val field: F, public val function: AutoDiffField.() -> AutoDiffValue, -) : DifferentiableExpression { +) : FirstDerivativeExpression() { public override operator fun invoke(arguments: Map): T { //val bindings = arguments.entries.map { it.key.bind(it.value) } return AutoDiffContext(field, arguments).function().value } - /** - * Get the derivative expression with given orders - */ - public override fun derivative(orders: Map): Expression { - val dSymbol = orders.entries.singleOrNull { it.value == 1 } - ?: error("SimpleAutoDiff supports only first order derivatives") - return Expression { arguments -> - //val bindings = arguments.entries.map { it.key.bind(it.value) } - val derivationResult = AutoDiffContext(field, arguments).derivate(function) - derivationResult.derivative(dSymbol.key) - } + override fun derivativeOrNull(symbol: Symbol): Expression = Expression { arguments -> + //val bindings = arguments.entries.map { it.key.bind(it.value) } + val derivationResult = AutoDiffContext(field, arguments).derivate(function) + derivationResult.derivative(symbol) } } diff --git a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SymbolIndexer.kt b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SymbolIndexer.kt index aef30c6dd..6c61c7c7d 100644 --- a/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SymbolIndexer.kt +++ b/kmath-core/src/commonMain/kotlin/kscience/kmath/expressions/SymbolIndexer.kt @@ -1,7 +1,12 @@ package kscience.kmath.expressions +import kscience.kmath.linear.Point +import kscience.kmath.structures.BufferFactory +import kscience.kmath.structures.Structure2D + /** * An environment to easy transform indexed variables to symbols and back. + * TODO requires multi-receivers to be beutiful */ public interface SymbolIndexer { public val symbols: List @@ -22,15 +27,26 @@ public interface SymbolIndexer { return get(this@SymbolIndexer.indexOf(symbol)) } + public operator fun Point.get(symbol: Symbol): T { + require(size == symbols.size) { "The input buffer size for indexer should be ${symbols.size} but $size found" } + return get(this@SymbolIndexer.indexOf(symbol)) + } + public fun DoubleArray.toMap(): Map { require(size == symbols.size) { "The input array size for indexer should be ${symbols.size} but $size found" } return symbols.indices.associate { symbols[it] to get(it) } } + public operator fun Structure2D.get(rowSymbol: Symbol, columnSymbol: Symbol): T = + get(indexOf(rowSymbol), indexOf(columnSymbol)) + public fun Map.toList(): List = symbols.map { getValue(it) } - public fun Map.toArray(): DoubleArray = DoubleArray(symbols.size) { getValue(symbols[it]) } + public fun Map.toPoint(bufferFactory: BufferFactory): Point = + bufferFactory(symbols.size) { getValue(symbols[it]) } + + public fun Map.toDoubleArray(): DoubleArray = DoubleArray(symbols.size) { getValue(symbols[it]) } } public inline class SimpleSymbolIndexer(override val symbols: List) : SymbolIndexer