_trustregion.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. """Trust-region optimization."""
  2. from __future__ import division, print_function, absolute_import
  3. import math
  4. import numpy as np
  5. import scipy.linalg
  6. from .optimize import (_check_unknown_options, wrap_function, _status_message,
  7. OptimizeResult)
  8. __all__ = []
  9. class BaseQuadraticSubproblem(object):
  10. """
  11. Base/abstract class defining the quadratic model for trust-region
  12. minimization. Child classes must implement the ``solve`` method.
  13. Values of the objective function, jacobian and hessian (if provided) at
  14. the current iterate ``x`` are evaluated on demand and then stored as
  15. attributes ``fun``, ``jac``, ``hess``.
  16. """
  17. def __init__(self, x, fun, jac, hess=None, hessp=None):
  18. self._x = x
  19. self._f = None
  20. self._g = None
  21. self._h = None
  22. self._g_mag = None
  23. self._cauchy_point = None
  24. self._newton_point = None
  25. self._fun = fun
  26. self._jac = jac
  27. self._hess = hess
  28. self._hessp = hessp
  29. def __call__(self, p):
  30. return self.fun + np.dot(self.jac, p) + 0.5 * np.dot(p, self.hessp(p))
  31. @property
  32. def fun(self):
  33. """Value of objective function at current iteration."""
  34. if self._f is None:
  35. self._f = self._fun(self._x)
  36. return self._f
  37. @property
  38. def jac(self):
  39. """Value of jacobian of objective function at current iteration."""
  40. if self._g is None:
  41. self._g = self._jac(self._x)
  42. return self._g
  43. @property
  44. def hess(self):
  45. """Value of hessian of objective function at current iteration."""
  46. if self._h is None:
  47. self._h = self._hess(self._x)
  48. return self._h
  49. def hessp(self, p):
  50. if self._hessp is not None:
  51. return self._hessp(self._x, p)
  52. else:
  53. return np.dot(self.hess, p)
  54. @property
  55. def jac_mag(self):
  56. """Magniture of jacobian of objective function at current iteration."""
  57. if self._g_mag is None:
  58. self._g_mag = scipy.linalg.norm(self.jac)
  59. return self._g_mag
  60. def get_boundaries_intersections(self, z, d, trust_radius):
  61. """
  62. Solve the scalar quadratic equation ||z + t d|| == trust_radius.
  63. This is like a line-sphere intersection.
  64. Return the two values of t, sorted from low to high.
  65. """
  66. a = np.dot(d, d)
  67. b = 2 * np.dot(z, d)
  68. c = np.dot(z, z) - trust_radius**2
  69. sqrt_discriminant = math.sqrt(b*b - 4*a*c)
  70. # The following calculation is mathematically
  71. # equivalent to:
  72. # ta = (-b - sqrt_discriminant) / (2*a)
  73. # tb = (-b + sqrt_discriminant) / (2*a)
  74. # but produce smaller round off errors.
  75. # Look at Matrix Computation p.97
  76. # for a better justification.
  77. aux = b + math.copysign(sqrt_discriminant, b)
  78. ta = -aux / (2*a)
  79. tb = -2*c / aux
  80. return sorted([ta, tb])
  81. def solve(self, trust_radius):
  82. raise NotImplementedError('The solve method should be implemented by '
  83. 'the child class')
  84. def _minimize_trust_region(fun, x0, args=(), jac=None, hess=None, hessp=None,
  85. subproblem=None, initial_trust_radius=1.0,
  86. max_trust_radius=1000.0, eta=0.15, gtol=1e-4,
  87. maxiter=None, disp=False, return_all=False,
  88. callback=None, inexact=True, **unknown_options):
  89. """
  90. Minimization of scalar function of one or more variables using a
  91. trust-region algorithm.
  92. Options for the trust-region algorithm are:
  93. initial_trust_radius : float
  94. Initial trust radius.
  95. max_trust_radius : float
  96. Never propose steps that are longer than this value.
  97. eta : float
  98. Trust region related acceptance stringency for proposed steps.
  99. gtol : float
  100. Gradient norm must be less than `gtol`
  101. before successful termination.
  102. maxiter : int
  103. Maximum number of iterations to perform.
  104. disp : bool
  105. If True, print convergence message.
  106. inexact : bool
  107. Accuracy to solve subproblems. If True requires less nonlinear
  108. iterations, but more vector products. Only effective for method
  109. trust-krylov.
  110. This function is called by the `minimize` function.
  111. It is not supposed to be called directly.
  112. """
  113. _check_unknown_options(unknown_options)
  114. if jac is None:
  115. raise ValueError('Jacobian is currently required for trust-region '
  116. 'methods')
  117. if hess is None and hessp is None:
  118. raise ValueError('Either the Hessian or the Hessian-vector product '
  119. 'is currently required for trust-region methods')
  120. if subproblem is None:
  121. raise ValueError('A subproblem solving strategy is required for '
  122. 'trust-region methods')
  123. if not (0 <= eta < 0.25):
  124. raise Exception('invalid acceptance stringency')
  125. if max_trust_radius <= 0:
  126. raise Exception('the max trust radius must be positive')
  127. if initial_trust_radius <= 0:
  128. raise ValueError('the initial trust radius must be positive')
  129. if initial_trust_radius >= max_trust_radius:
  130. raise ValueError('the initial trust radius must be less than the '
  131. 'max trust radius')
  132. # force the initial guess into a nice format
  133. x0 = np.asarray(x0).flatten()
  134. # Wrap the functions, for a couple reasons.
  135. # This tracks how many times they have been called
  136. # and it automatically passes the args.
  137. nfun, fun = wrap_function(fun, args)
  138. njac, jac = wrap_function(jac, args)
  139. nhess, hess = wrap_function(hess, args)
  140. nhessp, hessp = wrap_function(hessp, args)
  141. # limit the number of iterations
  142. if maxiter is None:
  143. maxiter = len(x0)*200
  144. # init the search status
  145. warnflag = 0
  146. # initialize the search
  147. trust_radius = initial_trust_radius
  148. x = x0
  149. if return_all:
  150. allvecs = [x]
  151. m = subproblem(x, fun, jac, hess, hessp)
  152. k = 0
  153. # search for the function min
  154. # do not even start if the gradient is small enough
  155. while m.jac_mag >= gtol:
  156. # Solve the sub-problem.
  157. # This gives us the proposed step relative to the current position
  158. # and it tells us whether the proposed step
  159. # has reached the trust region boundary or not.
  160. try:
  161. p, hits_boundary = m.solve(trust_radius)
  162. except np.linalg.linalg.LinAlgError as e:
  163. warnflag = 3
  164. break
  165. # calculate the predicted value at the proposed point
  166. predicted_value = m(p)
  167. # define the local approximation at the proposed point
  168. x_proposed = x + p
  169. m_proposed = subproblem(x_proposed, fun, jac, hess, hessp)
  170. # evaluate the ratio defined in equation (4.4)
  171. actual_reduction = m.fun - m_proposed.fun
  172. predicted_reduction = m.fun - predicted_value
  173. if predicted_reduction <= 0:
  174. warnflag = 2
  175. break
  176. rho = actual_reduction / predicted_reduction
  177. # update the trust radius according to the actual/predicted ratio
  178. if rho < 0.25:
  179. trust_radius *= 0.25
  180. elif rho > 0.75 and hits_boundary:
  181. trust_radius = min(2*trust_radius, max_trust_radius)
  182. # if the ratio is high enough then accept the proposed step
  183. if rho > eta:
  184. x = x_proposed
  185. m = m_proposed
  186. # append the best guess, call back, increment the iteration count
  187. if return_all:
  188. allvecs.append(np.copy(x))
  189. if callback is not None:
  190. callback(np.copy(x))
  191. k += 1
  192. # check if the gradient is small enough to stop
  193. if m.jac_mag < gtol:
  194. warnflag = 0
  195. break
  196. # check if we have looked at enough iterations
  197. if k >= maxiter:
  198. warnflag = 1
  199. break
  200. # print some stuff if requested
  201. status_messages = (
  202. _status_message['success'],
  203. _status_message['maxiter'],
  204. 'A bad approximation caused failure to predict improvement.',
  205. 'A linalg error occurred, such as a non-psd Hessian.',
  206. )
  207. if disp:
  208. if warnflag == 0:
  209. print(status_messages[warnflag])
  210. else:
  211. print('Warning: ' + status_messages[warnflag])
  212. print(" Current function value: %f" % m.fun)
  213. print(" Iterations: %d" % k)
  214. print(" Function evaluations: %d" % nfun[0])
  215. print(" Gradient evaluations: %d" % njac[0])
  216. print(" Hessian evaluations: %d" % nhess[0])
  217. result = OptimizeResult(x=x, success=(warnflag == 0), status=warnflag,
  218. fun=m.fun, jac=m.jac, nfev=nfun[0], njev=njac[0],
  219. nhev=nhess[0], nit=k,
  220. message=status_messages[warnflag])
  221. if hess is not None:
  222. result['hess'] = m.hess
  223. if return_all:
  224. result['allvecs'] = allvecs
  225. return result