_onenormest.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. """Sparse block 1-norm estimator.
  2. """
  3. import numpy as np
  4. from scipy.sparse.linalg import aslinearoperator
  5. __all__ = ['onenormest']
  6. def onenormest(A, t=2, itmax=5, compute_v=False, compute_w=False):
  7. """
  8. Compute a lower bound of the 1-norm of a sparse matrix.
  9. Parameters
  10. ----------
  11. A : ndarray or other linear operator
  12. A linear operator that can be transposed and that can
  13. produce matrix products.
  14. t : int, optional
  15. A positive parameter controlling the tradeoff between
  16. accuracy versus time and memory usage.
  17. Larger values take longer and use more memory
  18. but give more accurate output.
  19. itmax : int, optional
  20. Use at most this many iterations.
  21. compute_v : bool, optional
  22. Request a norm-maximizing linear operator input vector if True.
  23. compute_w : bool, optional
  24. Request a norm-maximizing linear operator output vector if True.
  25. Returns
  26. -------
  27. est : float
  28. An underestimate of the 1-norm of the sparse matrix.
  29. v : ndarray, optional
  30. The vector such that ||Av||_1 == est*||v||_1.
  31. It can be thought of as an input to the linear operator
  32. that gives an output with particularly large norm.
  33. w : ndarray, optional
  34. The vector Av which has relatively large 1-norm.
  35. It can be thought of as an output of the linear operator
  36. that is relatively large in norm compared to the input.
  37. Notes
  38. -----
  39. This is algorithm 2.4 of [1].
  40. In [2] it is described as follows.
  41. "This algorithm typically requires the evaluation of
  42. about 4t matrix-vector products and almost invariably
  43. produces a norm estimate (which is, in fact, a lower
  44. bound on the norm) correct to within a factor 3."
  45. .. versionadded:: 0.13.0
  46. References
  47. ----------
  48. .. [1] Nicholas J. Higham and Francoise Tisseur (2000),
  49. "A Block Algorithm for Matrix 1-Norm Estimation,
  50. with an Application to 1-Norm Pseudospectra."
  51. SIAM J. Matrix Anal. Appl. Vol. 21, No. 4, pp. 1185-1201.
  52. .. [2] Awad H. Al-Mohy and Nicholas J. Higham (2009),
  53. "A new scaling and squaring algorithm for the matrix exponential."
  54. SIAM J. Matrix Anal. Appl. Vol. 31, No. 3, pp. 970-989.
  55. Examples
  56. --------
  57. >>> import numpy as np
  58. >>> from scipy.sparse import csc_matrix
  59. >>> from scipy.sparse.linalg import onenormest
  60. >>> A = csc_matrix([[1., 0., 0.], [5., 8., 2.], [0., -1., 0.]], dtype=float)
  61. >>> A.toarray()
  62. array([[ 1., 0., 0.],
  63. [ 5., 8., 2.],
  64. [ 0., -1., 0.]])
  65. >>> onenormest(A)
  66. 9.0
  67. >>> np.linalg.norm(A.toarray(), ord=1)
  68. 9.0
  69. """
  70. # Check the input.
  71. A = aslinearoperator(A)
  72. if A.shape[0] != A.shape[1]:
  73. raise ValueError('expected the operator to act like a square matrix')
  74. # If the operator size is small compared to t,
  75. # then it is easier to compute the exact norm.
  76. # Otherwise estimate the norm.
  77. n = A.shape[1]
  78. if t >= n:
  79. A_explicit = np.asarray(aslinearoperator(A).matmat(np.identity(n)))
  80. if A_explicit.shape != (n, n):
  81. raise Exception('internal error: ',
  82. 'unexpected shape ' + str(A_explicit.shape))
  83. col_abs_sums = abs(A_explicit).sum(axis=0)
  84. if col_abs_sums.shape != (n, ):
  85. raise Exception('internal error: ',
  86. 'unexpected shape ' + str(col_abs_sums.shape))
  87. argmax_j = np.argmax(col_abs_sums)
  88. v = elementary_vector(n, argmax_j)
  89. w = A_explicit[:, argmax_j]
  90. est = col_abs_sums[argmax_j]
  91. else:
  92. est, v, w, nmults, nresamples = _onenormest_core(A, A.H, t, itmax)
  93. # Report the norm estimate along with some certificates of the estimate.
  94. if compute_v or compute_w:
  95. result = (est,)
  96. if compute_v:
  97. result += (v,)
  98. if compute_w:
  99. result += (w,)
  100. return result
  101. else:
  102. return est
  103. def _blocked_elementwise(func):
  104. """
  105. Decorator for an elementwise function, to apply it blockwise along
  106. first dimension, to avoid excessive memory usage in temporaries.
  107. """
  108. block_size = 2**20
  109. def wrapper(x):
  110. if x.shape[0] < block_size:
  111. return func(x)
  112. else:
  113. y0 = func(x[:block_size])
  114. y = np.zeros((x.shape[0],) + y0.shape[1:], dtype=y0.dtype)
  115. y[:block_size] = y0
  116. del y0
  117. for j in range(block_size, x.shape[0], block_size):
  118. y[j:j+block_size] = func(x[j:j+block_size])
  119. return y
  120. return wrapper
  121. @_blocked_elementwise
  122. def sign_round_up(X):
  123. """
  124. This should do the right thing for both real and complex matrices.
  125. From Higham and Tisseur:
  126. "Everything in this section remains valid for complex matrices
  127. provided that sign(A) is redefined as the matrix (aij / |aij|)
  128. (and sign(0) = 1) transposes are replaced by conjugate transposes."
  129. """
  130. Y = X.copy()
  131. Y[Y == 0] = 1
  132. Y /= np.abs(Y)
  133. return Y
  134. @_blocked_elementwise
  135. def _max_abs_axis1(X):
  136. return np.max(np.abs(X), axis=1)
  137. def _sum_abs_axis0(X):
  138. block_size = 2**20
  139. r = None
  140. for j in range(0, X.shape[0], block_size):
  141. y = np.sum(np.abs(X[j:j+block_size]), axis=0)
  142. if r is None:
  143. r = y
  144. else:
  145. r += y
  146. return r
  147. def elementary_vector(n, i):
  148. v = np.zeros(n, dtype=float)
  149. v[i] = 1
  150. return v
  151. def vectors_are_parallel(v, w):
  152. # Columns are considered parallel when they are equal or negative.
  153. # Entries are required to be in {-1, 1},
  154. # which guarantees that the magnitudes of the vectors are identical.
  155. if v.ndim != 1 or v.shape != w.shape:
  156. raise ValueError('expected conformant vectors with entries in {-1,1}')
  157. n = v.shape[0]
  158. return np.dot(v, w) == n
  159. def every_col_of_X_is_parallel_to_a_col_of_Y(X, Y):
  160. for v in X.T:
  161. if not any(vectors_are_parallel(v, w) for w in Y.T):
  162. return False
  163. return True
  164. def column_needs_resampling(i, X, Y=None):
  165. # column i of X needs resampling if either
  166. # it is parallel to a previous column of X or
  167. # it is parallel to a column of Y
  168. n, t = X.shape
  169. v = X[:, i]
  170. if any(vectors_are_parallel(v, X[:, j]) for j in range(i)):
  171. return True
  172. if Y is not None:
  173. if any(vectors_are_parallel(v, w) for w in Y.T):
  174. return True
  175. return False
  176. def resample_column(i, X):
  177. X[:, i] = np.random.randint(0, 2, size=X.shape[0])*2 - 1
  178. def less_than_or_close(a, b):
  179. return np.allclose(a, b) or (a < b)
  180. def _algorithm_2_2(A, AT, t):
  181. """
  182. This is Algorithm 2.2.
  183. Parameters
  184. ----------
  185. A : ndarray or other linear operator
  186. A linear operator that can produce matrix products.
  187. AT : ndarray or other linear operator
  188. The transpose of A.
  189. t : int, optional
  190. A positive parameter controlling the tradeoff between
  191. accuracy versus time and memory usage.
  192. Returns
  193. -------
  194. g : sequence
  195. A non-negative decreasing vector
  196. such that g[j] is a lower bound for the 1-norm
  197. of the column of A of jth largest 1-norm.
  198. The first entry of this vector is therefore a lower bound
  199. on the 1-norm of the linear operator A.
  200. This sequence has length t.
  201. ind : sequence
  202. The ith entry of ind is the index of the column A whose 1-norm
  203. is given by g[i].
  204. This sequence of indices has length t, and its entries are
  205. chosen from range(n), possibly with repetition,
  206. where n is the order of the operator A.
  207. Notes
  208. -----
  209. This algorithm is mainly for testing.
  210. It uses the 'ind' array in a way that is similar to
  211. its usage in algorithm 2.4. This algorithm 2.2 may be easier to test,
  212. so it gives a chance of uncovering bugs related to indexing
  213. which could have propagated less noticeably to algorithm 2.4.
  214. """
  215. A_linear_operator = aslinearoperator(A)
  216. AT_linear_operator = aslinearoperator(AT)
  217. n = A_linear_operator.shape[0]
  218. # Initialize the X block with columns of unit 1-norm.
  219. X = np.ones((n, t))
  220. if t > 1:
  221. X[:, 1:] = np.random.randint(0, 2, size=(n, t-1))*2 - 1
  222. X /= float(n)
  223. # Iteratively improve the lower bounds.
  224. # Track extra things, to assert invariants for debugging.
  225. g_prev = None
  226. h_prev = None
  227. k = 1
  228. ind = range(t)
  229. while True:
  230. Y = np.asarray(A_linear_operator.matmat(X))
  231. g = _sum_abs_axis0(Y)
  232. best_j = np.argmax(g)
  233. g.sort()
  234. g = g[::-1]
  235. S = sign_round_up(Y)
  236. Z = np.asarray(AT_linear_operator.matmat(S))
  237. h = _max_abs_axis1(Z)
  238. # If this algorithm runs for fewer than two iterations,
  239. # then its return values do not have the properties indicated
  240. # in the description of the algorithm.
  241. # In particular, the entries of g are not 1-norms of any
  242. # column of A until the second iteration.
  243. # Therefore we will require the algorithm to run for at least
  244. # two iterations, even though this requirement is not stated
  245. # in the description of the algorithm.
  246. if k >= 2:
  247. if less_than_or_close(max(h), np.dot(Z[:, best_j], X[:, best_j])):
  248. break
  249. ind = np.argsort(h)[::-1][:t]
  250. h = h[ind]
  251. for j in range(t):
  252. X[:, j] = elementary_vector(n, ind[j])
  253. # Check invariant (2.2).
  254. if k >= 2:
  255. if not less_than_or_close(g_prev[0], h_prev[0]):
  256. raise Exception('invariant (2.2) is violated')
  257. if not less_than_or_close(h_prev[0], g[0]):
  258. raise Exception('invariant (2.2) is violated')
  259. # Check invariant (2.3).
  260. if k >= 3:
  261. for j in range(t):
  262. if not less_than_or_close(g[j], g_prev[j]):
  263. raise Exception('invariant (2.3) is violated')
  264. # Update for the next iteration.
  265. g_prev = g
  266. h_prev = h
  267. k += 1
  268. # Return the lower bounds and the corresponding column indices.
  269. return g, ind
  270. def _onenormest_core(A, AT, t, itmax):
  271. """
  272. Compute a lower bound of the 1-norm of a sparse matrix.
  273. Parameters
  274. ----------
  275. A : ndarray or other linear operator
  276. A linear operator that can produce matrix products.
  277. AT : ndarray or other linear operator
  278. The transpose of A.
  279. t : int, optional
  280. A positive parameter controlling the tradeoff between
  281. accuracy versus time and memory usage.
  282. itmax : int, optional
  283. Use at most this many iterations.
  284. Returns
  285. -------
  286. est : float
  287. An underestimate of the 1-norm of the sparse matrix.
  288. v : ndarray, optional
  289. The vector such that ||Av||_1 == est*||v||_1.
  290. It can be thought of as an input to the linear operator
  291. that gives an output with particularly large norm.
  292. w : ndarray, optional
  293. The vector Av which has relatively large 1-norm.
  294. It can be thought of as an output of the linear operator
  295. that is relatively large in norm compared to the input.
  296. nmults : int, optional
  297. The number of matrix products that were computed.
  298. nresamples : int, optional
  299. The number of times a parallel column was observed,
  300. necessitating a re-randomization of the column.
  301. Notes
  302. -----
  303. This is algorithm 2.4.
  304. """
  305. # This function is a more or less direct translation
  306. # of Algorithm 2.4 from the Higham and Tisseur (2000) paper.
  307. A_linear_operator = aslinearoperator(A)
  308. AT_linear_operator = aslinearoperator(AT)
  309. if itmax < 2:
  310. raise ValueError('at least two iterations are required')
  311. if t < 1:
  312. raise ValueError('at least one column is required')
  313. n = A.shape[0]
  314. if t >= n:
  315. raise ValueError('t should be smaller than the order of A')
  316. # Track the number of big*small matrix multiplications
  317. # and the number of resamplings.
  318. nmults = 0
  319. nresamples = 0
  320. # "We now explain our choice of starting matrix. We take the first
  321. # column of X to be the vector of 1s [...] This has the advantage that
  322. # for a matrix with nonnegative elements the algorithm converges
  323. # with an exact estimate on the second iteration, and such matrices
  324. # arise in applications [...]"
  325. X = np.ones((n, t), dtype=float)
  326. # "The remaining columns are chosen as rand{-1,1},
  327. # with a check for and correction of parallel columns,
  328. # exactly as for S in the body of the algorithm."
  329. if t > 1:
  330. for i in range(1, t):
  331. # These are technically initial samples, not resamples,
  332. # so the resampling count is not incremented.
  333. resample_column(i, X)
  334. for i in range(t):
  335. while column_needs_resampling(i, X):
  336. resample_column(i, X)
  337. nresamples += 1
  338. # "Choose starting matrix X with columns of unit 1-norm."
  339. X /= float(n)
  340. # "indices of used unit vectors e_j"
  341. ind_hist = np.zeros(0, dtype=np.intp)
  342. est_old = 0
  343. S = np.zeros((n, t), dtype=float)
  344. k = 1
  345. ind = None
  346. while True:
  347. Y = np.asarray(A_linear_operator.matmat(X))
  348. nmults += 1
  349. mags = _sum_abs_axis0(Y)
  350. est = np.max(mags)
  351. best_j = np.argmax(mags)
  352. if est > est_old or k == 2:
  353. if k >= 2:
  354. ind_best = ind[best_j]
  355. w = Y[:, best_j]
  356. # (1)
  357. if k >= 2 and est <= est_old:
  358. est = est_old
  359. break
  360. est_old = est
  361. S_old = S
  362. if k > itmax:
  363. break
  364. S = sign_round_up(Y)
  365. del Y
  366. # (2)
  367. if every_col_of_X_is_parallel_to_a_col_of_Y(S, S_old):
  368. break
  369. if t > 1:
  370. # "Ensure that no column of S is parallel to another column of S
  371. # or to a column of S_old by replacing columns of S by rand{-1,1}."
  372. for i in range(t):
  373. while column_needs_resampling(i, S, S_old):
  374. resample_column(i, S)
  375. nresamples += 1
  376. del S_old
  377. # (3)
  378. Z = np.asarray(AT_linear_operator.matmat(S))
  379. nmults += 1
  380. h = _max_abs_axis1(Z)
  381. del Z
  382. # (4)
  383. if k >= 2 and max(h) == h[ind_best]:
  384. break
  385. # "Sort h so that h_first >= ... >= h_last
  386. # and re-order ind correspondingly."
  387. #
  388. # Later on, we will need at most t+len(ind_hist) largest
  389. # entries, so drop the rest
  390. ind = np.argsort(h)[::-1][:t+len(ind_hist)].copy()
  391. del h
  392. if t > 1:
  393. # (5)
  394. # Break if the most promising t vectors have been visited already.
  395. if np.in1d(ind[:t], ind_hist).all():
  396. break
  397. # Put the most promising unvisited vectors at the front of the list
  398. # and put the visited vectors at the end of the list.
  399. # Preserve the order of the indices induced by the ordering of h.
  400. seen = np.in1d(ind, ind_hist)
  401. ind = np.concatenate((ind[~seen], ind[seen]))
  402. for j in range(t):
  403. X[:, j] = elementary_vector(n, ind[j])
  404. new_ind = ind[:t][~np.in1d(ind[:t], ind_hist)]
  405. ind_hist = np.concatenate((ind_hist, new_ind))
  406. k += 1
  407. v = elementary_vector(n, ind_best)
  408. return est, v, w, nmults, nresamples