lsq_linear.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. """Linear least squares with bound constraints on independent variables."""
  2. from __future__ import division, print_function, absolute_import
  3. import numpy as np
  4. from numpy.linalg import norm
  5. from scipy.sparse import issparse, csr_matrix
  6. from scipy.sparse.linalg import LinearOperator, lsmr
  7. from scipy.optimize import OptimizeResult
  8. from .common import in_bounds, compute_grad
  9. from .trf_linear import trf_linear
  10. from .bvls import bvls
  11. def prepare_bounds(bounds, n):
  12. lb, ub = [np.asarray(b, dtype=float) for b in bounds]
  13. if lb.ndim == 0:
  14. lb = np.resize(lb, n)
  15. if ub.ndim == 0:
  16. ub = np.resize(ub, n)
  17. return lb, ub
  18. TERMINATION_MESSAGES = {
  19. -1: "The algorithm was not able to make progress on the last iteration.",
  20. 0: "The maximum number of iterations is exceeded.",
  21. 1: "The first-order optimality measure is less than `tol`.",
  22. 2: "The relative change of the cost function is less than `tol`.",
  23. 3: "The unconstrained solution is optimal."
  24. }
  25. def lsq_linear(A, b, bounds=(-np.inf, np.inf), method='trf', tol=1e-10,
  26. lsq_solver=None, lsmr_tol=None, max_iter=None, verbose=0):
  27. r"""Solve a linear least-squares problem with bounds on the variables.
  28. Given a m-by-n design matrix A and a target vector b with m elements,
  29. `lsq_linear` solves the following optimization problem::
  30. minimize 0.5 * ||A x - b||**2
  31. subject to lb <= x <= ub
  32. This optimization problem is convex, hence a found minimum (if iterations
  33. have converged) is guaranteed to be global.
  34. Parameters
  35. ----------
  36. A : array_like, sparse matrix of LinearOperator, shape (m, n)
  37. Design matrix. Can be `scipy.sparse.linalg.LinearOperator`.
  38. b : array_like, shape (m,)
  39. Target vector.
  40. bounds : 2-tuple of array_like, optional
  41. Lower and upper bounds on independent variables. Defaults to no bounds.
  42. Each array must have shape (n,) or be a scalar, in the latter
  43. case a bound will be the same for all variables. Use ``np.inf`` with
  44. an appropriate sign to disable bounds on all or some variables.
  45. method : 'trf' or 'bvls', optional
  46. Method to perform minimization.
  47. * 'trf' : Trust Region Reflective algorithm adapted for a linear
  48. least-squares problem. This is an interior-point-like method
  49. and the required number of iterations is weakly correlated with
  50. the number of variables.
  51. * 'bvls' : Bounded-Variable Least-Squares algorithm. This is
  52. an active set method, which requires the number of iterations
  53. comparable to the number of variables. Can't be used when `A` is
  54. sparse or LinearOperator.
  55. Default is 'trf'.
  56. tol : float, optional
  57. Tolerance parameter. The algorithm terminates if a relative change
  58. of the cost function is less than `tol` on the last iteration.
  59. Additionally the first-order optimality measure is considered:
  60. * ``method='trf'`` terminates if the uniform norm of the gradient,
  61. scaled to account for the presence of the bounds, is less than
  62. `tol`.
  63. * ``method='bvls'`` terminates if Karush-Kuhn-Tucker conditions
  64. are satisfied within `tol` tolerance.
  65. lsq_solver : {None, 'exact', 'lsmr'}, optional
  66. Method of solving unbounded least-squares problems throughout
  67. iterations:
  68. * 'exact' : Use dense QR or SVD decomposition approach. Can't be
  69. used when `A` is sparse or LinearOperator.
  70. * 'lsmr' : Use `scipy.sparse.linalg.lsmr` iterative procedure
  71. which requires only matrix-vector product evaluations. Can't
  72. be used with ``method='bvls'``.
  73. If None (default) the solver is chosen based on type of `A`.
  74. lsmr_tol : None, float or 'auto', optional
  75. Tolerance parameters 'atol' and 'btol' for `scipy.sparse.linalg.lsmr`
  76. If None (default), it is set to ``1e-2 * tol``. If 'auto', the
  77. tolerance will be adjusted based on the optimality of the current
  78. iterate, which can speed up the optimization process, but is not always
  79. reliable.
  80. max_iter : None or int, optional
  81. Maximum number of iterations before termination. If None (default), it
  82. is set to 100 for ``method='trf'`` or to the number of variables for
  83. ``method='bvls'`` (not counting iterations for 'bvls' initialization).
  84. verbose : {0, 1, 2}, optional
  85. Level of algorithm's verbosity:
  86. * 0 : work silently (default).
  87. * 1 : display a termination report.
  88. * 2 : display progress during iterations.
  89. Returns
  90. -------
  91. OptimizeResult with the following fields defined:
  92. x : ndarray, shape (n,)
  93. Solution found.
  94. cost : float
  95. Value of the cost function at the solution.
  96. fun : ndarray, shape (m,)
  97. Vector of residuals at the solution.
  98. optimality : float
  99. First-order optimality measure. The exact meaning depends on `method`,
  100. refer to the description of `tol` parameter.
  101. active_mask : ndarray of int, shape (n,)
  102. Each component shows whether a corresponding constraint is active
  103. (that is, whether a variable is at the bound):
  104. * 0 : a constraint is not active.
  105. * -1 : a lower bound is active.
  106. * 1 : an upper bound is active.
  107. Might be somewhat arbitrary for the `trf` method as it generates a
  108. sequence of strictly feasible iterates and active_mask is determined
  109. within a tolerance threshold.
  110. nit : int
  111. Number of iterations. Zero if the unconstrained solution is optimal.
  112. status : int
  113. Reason for algorithm termination:
  114. * -1 : the algorithm was not able to make progress on the last
  115. iteration.
  116. * 0 : the maximum number of iterations is exceeded.
  117. * 1 : the first-order optimality measure is less than `tol`.
  118. * 2 : the relative change of the cost function is less than `tol`.
  119. * 3 : the unconstrained solution is optimal.
  120. message : str
  121. Verbal description of the termination reason.
  122. success : bool
  123. True if one of the convergence criteria is satisfied (`status` > 0).
  124. See Also
  125. --------
  126. nnls : Linear least squares with non-negativity constraint.
  127. least_squares : Nonlinear least squares with bounds on the variables.
  128. Notes
  129. -----
  130. The algorithm first computes the unconstrained least-squares solution by
  131. `numpy.linalg.lstsq` or `scipy.sparse.linalg.lsmr` depending on
  132. `lsq_solver`. This solution is returned as optimal if it lies within the
  133. bounds.
  134. Method 'trf' runs the adaptation of the algorithm described in [STIR]_ for
  135. a linear least-squares problem. The iterations are essentially the same as
  136. in the nonlinear least-squares algorithm, but as the quadratic function
  137. model is always accurate, we don't need to track or modify the radius of
  138. a trust region. The line search (backtracking) is used as a safety net
  139. when a selected step does not decrease the cost function. Read more
  140. detailed description of the algorithm in `scipy.optimize.least_squares`.
  141. Method 'bvls' runs a Python implementation of the algorithm described in
  142. [BVLS]_. The algorithm maintains active and free sets of variables, on
  143. each iteration chooses a new variable to move from the active set to the
  144. free set and then solves the unconstrained least-squares problem on free
  145. variables. This algorithm is guaranteed to give an accurate solution
  146. eventually, but may require up to n iterations for a problem with n
  147. variables. Additionally, an ad-hoc initialization procedure is
  148. implemented, that determines which variables to set free or active
  149. initially. It takes some number of iterations before actual BVLS starts,
  150. but can significantly reduce the number of further iterations.
  151. References
  152. ----------
  153. .. [STIR] M. A. Branch, T. F. Coleman, and Y. Li, "A Subspace, Interior,
  154. and Conjugate Gradient Method for Large-Scale Bound-Constrained
  155. Minimization Problems," SIAM Journal on Scientific Computing,
  156. Vol. 21, Number 1, pp 1-23, 1999.
  157. .. [BVLS] P. B. Start and R. L. Parker, "Bounded-Variable Least-Squares:
  158. an Algorithm and Applications", Computational Statistics, 10,
  159. 129-141, 1995.
  160. Examples
  161. --------
  162. In this example a problem with a large sparse matrix and bounds on the
  163. variables is solved.
  164. >>> from scipy.sparse import rand
  165. >>> from scipy.optimize import lsq_linear
  166. ...
  167. >>> np.random.seed(0)
  168. ...
  169. >>> m = 20000
  170. >>> n = 10000
  171. ...
  172. >>> A = rand(m, n, density=1e-4)
  173. >>> b = np.random.randn(m)
  174. ...
  175. >>> lb = np.random.randn(n)
  176. >>> ub = lb + 1
  177. ...
  178. >>> res = lsq_linear(A, b, bounds=(lb, ub), lsmr_tol='auto', verbose=1)
  179. # may vary
  180. The relative change of the cost function is less than `tol`.
  181. Number of iterations 16, initial cost 1.5039e+04, final cost 1.1112e+04,
  182. first-order optimality 4.66e-08.
  183. """
  184. if method not in ['trf', 'bvls']:
  185. raise ValueError("`method` must be 'trf' or 'bvls'")
  186. if lsq_solver not in [None, 'exact', 'lsmr']:
  187. raise ValueError("`solver` must be None, 'exact' or 'lsmr'.")
  188. if verbose not in [0, 1, 2]:
  189. raise ValueError("`verbose` must be in [0, 1, 2].")
  190. if issparse(A):
  191. A = csr_matrix(A)
  192. elif not isinstance(A, LinearOperator):
  193. A = np.atleast_2d(A)
  194. if method == 'bvls':
  195. if lsq_solver == 'lsmr':
  196. raise ValueError("method='bvls' can't be used with "
  197. "lsq_solver='lsmr'")
  198. if not isinstance(A, np.ndarray):
  199. raise ValueError("method='bvls' can't be used with `A` being "
  200. "sparse or LinearOperator.")
  201. if lsq_solver is None:
  202. if isinstance(A, np.ndarray):
  203. lsq_solver = 'exact'
  204. else:
  205. lsq_solver = 'lsmr'
  206. elif lsq_solver == 'exact' and not isinstance(A, np.ndarray):
  207. raise ValueError("`exact` solver can't be used when `A` is "
  208. "sparse or LinearOperator.")
  209. if len(A.shape) != 2: # No ndim for LinearOperator.
  210. raise ValueError("`A` must have at most 2 dimensions.")
  211. if len(bounds) != 2:
  212. raise ValueError("`bounds` must contain 2 elements.")
  213. if max_iter is not None and max_iter <= 0:
  214. raise ValueError("`max_iter` must be None or positive integer.")
  215. m, n = A.shape
  216. b = np.atleast_1d(b)
  217. if b.ndim != 1:
  218. raise ValueError("`b` must have at most 1 dimension.")
  219. if b.size != m:
  220. raise ValueError("Inconsistent shapes between `A` and `b`.")
  221. lb, ub = prepare_bounds(bounds, n)
  222. if lb.shape != (n,) and ub.shape != (n,):
  223. raise ValueError("Bounds have wrong shape.")
  224. if np.any(lb >= ub):
  225. raise ValueError("Each lower bound must be strictly less than each "
  226. "upper bound.")
  227. if lsq_solver == 'exact':
  228. x_lsq = np.linalg.lstsq(A, b, rcond=-1)[0]
  229. elif lsq_solver == 'lsmr':
  230. x_lsq = lsmr(A, b, atol=tol, btol=tol)[0]
  231. if in_bounds(x_lsq, lb, ub):
  232. r = A.dot(x_lsq) - b
  233. cost = 0.5 * np.dot(r, r)
  234. termination_status = 3
  235. termination_message = TERMINATION_MESSAGES[termination_status]
  236. g = compute_grad(A, r)
  237. g_norm = norm(g, ord=np.inf)
  238. if verbose > 0:
  239. print(termination_message)
  240. print("Final cost {0:.4e}, first-order optimality {1:.2e}"
  241. .format(cost, g_norm))
  242. return OptimizeResult(
  243. x=x_lsq, fun=r, cost=cost, optimality=g_norm,
  244. active_mask=np.zeros(n), nit=0, status=termination_status,
  245. message=termination_message, success=True)
  246. if method == 'trf':
  247. res = trf_linear(A, b, x_lsq, lb, ub, tol, lsq_solver, lsmr_tol,
  248. max_iter, verbose)
  249. elif method == 'bvls':
  250. res = bvls(A, b, x_lsq, lb, ub, tol, max_iter, verbose)
  251. res.message = TERMINATION_MESSAGES[res.status]
  252. res.success = res.status > 0
  253. if verbose > 0:
  254. print(res.message)
  255. print("Number of iterations {0}, initial cost {1:.4e}, "
  256. "final cost {2:.4e}, first-order optimality {3:.2e}."
  257. .format(res.nit, res.initial_cost, res.cost, res.optimality))
  258. del res.initial_cost
  259. return res