chumpy-fixed 0.71__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,455 @@
1
+ import sys
2
+ import warnings
3
+ import numpy as np
4
+ import scipy.sparse as sp
5
+ from . import ch, utils
6
+ from .ch import pif
7
+ from .utils import timer
8
+
9
+
10
+ def clear_cache_single(node):
11
+ node._cache['drs'].clear()
12
+ if hasattr(node, 'dr_cached'):
13
+ node.dr_cached.clear()
14
+
15
+ def vstack(x):
16
+ x = [a if not isinstance(a, sp.linalg.interface.LinearOperator) else a.dot(np.eye(a.shape[1])) for a in x]
17
+ return sp.vstack(x, format='csc') if any([sp.issparse(a) for a in x]) else np.vstack(x)
18
+ def hstack(x):
19
+ x = [a if not isinstance(a, sp.linalg.interface.LinearOperator) else a.dot(np.eye(a.shape[1])) for a in x]
20
+ return sp.hstack(x, format='csc') if any([sp.issparse(a) for a in x]) else np.hstack(x)
21
+
22
+
23
+ _giter = 0
24
+ class ChInputsStacked(ch.Ch):
25
+ dterms = 'x', 'obj'
26
+ terms = 'free_variables'
27
+
28
+ def compute_r(self):
29
+ if not hasattr(self, 'fevals'):
30
+ self.fevals = 0
31
+ self.fevals += 1
32
+ return self.obj.r.ravel()
33
+
34
+ def dr_wrt(self, wrt, profiler=None):
35
+ '''
36
+ Loop over free variables and delete cache for the whole tree after finished each one
37
+ '''
38
+ if wrt is self.x:
39
+ jacs = []
40
+ for fvi, freevar in enumerate(self.free_variables):
41
+ tm = timer()
42
+ if isinstance(freevar, ch.Select):
43
+ new_jac = self.obj.dr_wrt(freevar.a, profiler=profiler)
44
+ try:
45
+ new_jac = new_jac[:, freevar.idxs]
46
+ except:
47
+ # non-csc sparse matrices may not support column-wise indexing
48
+ new_jac = new_jac.tocsc()[:, freevar.idxs]
49
+ else:
50
+ new_jac = self.obj.dr_wrt(freevar, profiler=profiler)
51
+
52
+ pif('dx wrt {} in {}sec, sparse: {}'.format(freevar.short_name, tm(), sp.issparse(new_jac)))
53
+
54
+ if self._make_dense and sp.issparse(new_jac):
55
+ new_jac = new_jac.todense()
56
+ if self._make_sparse and not sp.issparse(new_jac):
57
+ new_jac = sp.csc_matrix(new_jac)
58
+
59
+ if new_jac is None:
60
+ raise Exception(
61
+ 'Objective has no derivative wrt free variable {}. '
62
+ 'You should likely remove it.'.format(fvi))
63
+
64
+ jacs.append(new_jac)
65
+ tm = timer()
66
+ utils.dfs_do_func_on_graph(self.obj, clear_cache_single)
67
+ pif('dfs_do_func_on_graph in {}sec'.format(tm()))
68
+ tm = timer()
69
+ J = hstack(jacs)
70
+ pif('hstack in {}sec'.format(tm()))
71
+ return J
72
+
73
+ def on_changed(self, which):
74
+ global _giter
75
+ _giter += 1
76
+ if 'x' in which:
77
+ pos = 0
78
+ for idx, freevar in enumerate(self.free_variables):
79
+ sz = freevar.r.size
80
+ rng = np.arange(pos, pos+sz)
81
+ if isinstance(self.free_variables[idx], ch.Select):
82
+ # Deal with nested selects
83
+ selects = []
84
+ a = self.free_variables[idx]
85
+ while isinstance(a, ch.Select):
86
+ selects.append(a.idxs)
87
+ a = a.a
88
+ newv = a.x.copy()
89
+ idxs = selects.pop()
90
+ while len(selects) > 0:
91
+ idxs = idxs[selects.pop()]
92
+ newv.ravel()[idxs] = self.x.r.ravel()[rng]
93
+ a.__setattr__('x', newv, _giter)
94
+ elif isinstance(self.free_variables[idx].x, np.ndarray):
95
+ self.free_variables[idx].__setattr__('x', self.x.r[rng].copy().reshape(self.free_variables[idx].x.shape), _giter)
96
+ else: # a number
97
+ self.free_variables[idx].__setattr__('x', self.x.r[rng], _giter)
98
+ pos += sz
99
+
100
+ @property
101
+ def J(self):
102
+ '''
103
+ Compute Jacobian. Analyze dr graph first to disable unnecessary caching
104
+ '''
105
+ result = self.dr_wrt(self.x, profiler=self.profiler).copy()
106
+ if self.profiler:
107
+ self.profiler.harvest()
108
+ return np.atleast_2d(result) if not sp.issparse(result) else result
109
+
110
+
111
+ def setup_sparse_solver(sparse_solver):
112
+ _solver_fns = {
113
+ 'cg': lambda A, x, M=None : sp.linalg.cg(A, x, M=M, tol=1e-10)[0],
114
+ 'spsolve': lambda A, x : sp.linalg.spsolve(A, x)
115
+ }
116
+ if callable(sparse_solver):
117
+ return sparse_solver
118
+ elif isinstance(sparse_solver, str) and sparse_solver in list(_solver_fns.keys()):
119
+ return _solver_fns[sparse_solver]
120
+ else:
121
+ raise Exception('sparse_solver argument must be either a string in the set (%s) or have the api of scipy.sparse.linalg.spsolve.' % ', '.join(list(_solver_fns.keys())))
122
+
123
+
124
+ def setup_objective(obj, free_variables, on_step=None, disp=True, make_dense=False):
125
+ '''
126
+ obj here can be a list of ch objects or a dict of label: ch objects. Either way, the ch
127
+ objects will be merged into one objective using a ChInputsStacked. The labels are just used
128
+ for printing out values per objective with each iteration. If make_dense is True, the
129
+ resulting object with return a desne Jacobian
130
+ '''
131
+ # Validate free variables
132
+ num_unique_ids = len(np.unique(np.array([id(freevar) for freevar in free_variables])))
133
+ if num_unique_ids != len(free_variables):
134
+ raise Exception('The "free_variables" param contains duplicate variables.')
135
+ # Extract labels
136
+ labels = {}
137
+ if isinstance(obj, list) or isinstance(obj, tuple):
138
+ obj = ch.concatenate([f.ravel() for f in obj])
139
+ elif isinstance(obj, dict):
140
+ labels = obj
141
+ obj = ch.concatenate([f.ravel() for f in list(obj.values())])
142
+ # build objective
143
+ x = np.concatenate([freevar.r.ravel() for freevar in free_variables])
144
+ obj = ChInputsStacked(obj=obj, free_variables=free_variables, x=x, make_dense=make_dense)
145
+ # build callback
146
+ def callback():
147
+ if on_step is not None:
148
+ on_step(obj)
149
+ if disp:
150
+ report_line = ['%.2e' % (np.sum(obj.r**2),)]
151
+ for label, objective in sorted(list(labels.items()), key=lambda x: x[0]):
152
+ report_line.append('%s: %.2e' % (label, np.sum(objective.r**2)))
153
+ report_line = " | ".join(report_line) + '\n'
154
+ sys.stderr.write(report_line)
155
+ return obj, callback
156
+
157
+
158
+ class DoglegState(object):
159
+ '''
160
+ Dogleg preserves a great deal of state from iteration to iteration. Many of the things
161
+ that we need to calculate are dependent only on this state (e.g. the various trust region
162
+ steps, the current jacobian and the A & g that depends on it, etc.). Holding the state and
163
+ the various methods based on that state here allows us to seperate a lot of the jacobian
164
+ based calculation from the flow control of the optmization.
165
+
166
+ There will be once instance of DoglegState per invocation of minimize_dogleg.
167
+ '''
168
+ def __init__(self, delta, solve):
169
+ self.iteration = 0
170
+ self._d_gn = None # gauss-newton
171
+ self._d_sd = None # steepest descent
172
+ self._d_dl = None # dogleg
173
+ self.J = None
174
+ self.A = None
175
+ self.g = None
176
+ self._p = None
177
+ self.delta = delta
178
+ self.solve = solve
179
+ self._r = None
180
+ self.rho = None
181
+ self.done = False
182
+
183
+ @property
184
+ def p(self):
185
+ '''p is the current proposed input vector'''
186
+ return self._p
187
+ @p.setter
188
+ def p(self, val):
189
+ self._p = val.reshape((-1, 1))
190
+
191
+ # induce some certainty about what the shape of the steps are
192
+ @property
193
+ def d_gn(self):
194
+ return self._d_gn
195
+ @d_gn.setter
196
+ def d_gn(self, val):
197
+ if val is not None:
198
+ val = val.reshape((-1, 1))
199
+ self._d_gn = val
200
+
201
+ @property
202
+ def d_sd(self):
203
+ return self._d_sd
204
+ @d_sd.setter
205
+ def d_sd(self, val):
206
+ if val is not None:
207
+ val = val.reshape((-1, 1))
208
+ self._d_sd = val
209
+
210
+ @property
211
+ def d_dl(self):
212
+ return self._d_dl
213
+ @d_dl.setter
214
+ def d_dl(self, val):
215
+ if val is not None:
216
+ val = val.reshape((-1, 1))
217
+ self._d_dl = val
218
+
219
+ @property
220
+ def step(self):
221
+ return self.d_dl.reshape((-1, 1))
222
+ @property
223
+ def step_size(self):
224
+ return np.linalg.norm(self.d_dl)
225
+
226
+ def start_iteration(self):
227
+ self.iteration += 1
228
+ pif('beginning iteration %d' % (self.iteration,))
229
+ self.d_sd = (np.linalg.norm(self.g)**2 / np.linalg.norm(self.J.dot(self.g))**2 * self.g).ravel()
230
+ self.d_gn = None
231
+
232
+ @property
233
+ def r(self):
234
+ '''r is the residual at the current p'''
235
+ return self._r
236
+ @r.setter
237
+ def r(self, val):
238
+ self._r = val.copy().reshape((-1, 1))
239
+ self.updateAg()
240
+
241
+ def updateAg(self):
242
+ tm = timer()
243
+ pif('updating A and g...')
244
+ JT = self.J.T
245
+ self.A = JT.dot(self.J)
246
+ self.g = JT.dot(-self.r).reshape((-1, 1))
247
+ pif('A and g updated in %.2fs' % tm())
248
+
249
+ def update_step(self):
250
+ # if the Cauchy point is outside the trust region,
251
+ # take that direction but only to the edge of the trust region
252
+ if self.delta is not None and np.linalg.norm(self.d_sd) >= self.delta:
253
+ pif('PROGRESS: Using stunted cauchy')
254
+ self.d_dl = np.array(self.delta/np.linalg.norm(self.d_sd) * self.d_sd).ravel()
255
+ else:
256
+ if self.d_gn is None:
257
+ # We only need to compute this once per iteration
258
+ self.updateGN()
259
+ # if the gauss-newton solution is within the trust region, use it
260
+ if self.delta is None or np.linalg.norm(self.d_gn) <= self.delta:
261
+ pif('PROGRESS: Using gauss-newton solution')
262
+ self.d_dl = np.array(self.d_gn).ravel()
263
+ if self.delta is None:
264
+ self.delta = np.linalg.norm(self.d_gn)
265
+ else: # between cauchy step and gauss-newton step
266
+ pif('PROGRESS: between cauchy and gauss-newton')
267
+ # apply step
268
+ self.d_dl = self.d_sd + self.beta_multiplier * (self.d_gn - self.d_sd)
269
+
270
+ @property
271
+ def beta_multiplier(self):
272
+ delta_sq = self.delta**2
273
+ diff = self.d_gn - self.d_sd
274
+ sqnorm_sd = np.linalg.norm(self.d_sd)**2
275
+ pnow = diff.T.dot(diff)*delta_sq + self.d_gn.T.dot(self.d_sd)**2 - np.linalg.norm(self.d_gn)**2 * sqnorm_sd
276
+ return float(delta_sq - sqnorm_sd) / float((diff).T.dot(self.d_sd) + np.sqrt(pnow))
277
+
278
+ def updateGN(self):
279
+ tm = timer()
280
+ if sp.issparse(self.A):
281
+ self.A.eliminate_zeros()
282
+ pif('sparse solve...sparsity infill is %.3f%% (hessian %dx%d)' % (100. * self.A.nnz / (self.A.shape[0] * self.A.shape[1]), self.A.shape[0], self.A.shape[1]))
283
+ if self.g.size > 1:
284
+ self.d_gn = self.solve(self.A, self.g).ravel()
285
+ if np.any(np.isnan(self.d_gn)) or np.any(np.isinf(self.d_gn)):
286
+ from scipy.sparse.linalg import lsqr
287
+ warnings.warn("sparse solve failed, falling back to lsqr")
288
+ self.d_gn = lsqr(self.A, self.g)[0].ravel()
289
+ else:
290
+ self.d_gn = np.atleast_1d(self.g.ravel()[0]/self.A[0,0])
291
+ pif('sparse solve...done in %.2fs' % tm())
292
+ else:
293
+ pif('dense solve...')
294
+ try:
295
+ self.d_gn = np.linalg.solve(self.A, self.g).ravel()
296
+ except Exception:
297
+ warnings.warn("dense solve failed, falling back to lsqr")
298
+ self.d_gn = np.linalg.lstsq(self.A, self.g)[0].ravel()
299
+ pif('dense solve...done in %.2fs' % tm())
300
+
301
+ def updateJ(self, obj):
302
+ tm = timer()
303
+ pif('computing Jacobian...')
304
+ self.J = obj.J
305
+ if self.J is None:
306
+ raise Exception("Computing Jacobian failed!")
307
+ if sp.issparse(self.J):
308
+ tm2 = timer()
309
+ self.J = self.J.tocsr()
310
+ pif('converted to csr in {}secs'.format(tm2()))
311
+ assert(self.J.nnz > 0)
312
+ elif ch.VERBOSE:
313
+ nonzero = np.count_nonzero(self.J)
314
+ pif('Jacobian dense with sparsity %.3f' % (nonzero/self.J.size))
315
+ pif('Jacobian (%dx%d) computed in %.2fs' % (self.J.shape[0], self.J.shape[1], tm()))
316
+ if self.J.shape[1] != self.p.size:
317
+ raise Exception('Jacobian size mismatch with objective input')
318
+ return self.J
319
+
320
+ class Trial(object):
321
+ '''
322
+ Inside each iteration of dogleg we propose a step and check to see if it's actually
323
+ an improvement before we accept it. This class encapsulates that trial and the
324
+ testing to see if it is actually an improvement.
325
+
326
+ There will be one instance of Trial per iteration in dogleg.
327
+ '''
328
+ def __init__(self, proposed_r, state):
329
+ self.r = proposed_r
330
+ self.state = state
331
+ # rho is the ratio of...
332
+ # (improvement in SSE) / (predicted improvement in SSE)
333
+ self.rho = np.linalg.norm(state.r)**2 - np.linalg.norm(proposed_r)**2
334
+ if self.rho > 0:
335
+ with warnings.catch_warnings():
336
+ warnings.filterwarnings('ignore',category=RuntimeWarning)
337
+ predicted_improvement = 2. * state.g.T.dot(state.d_dl) - state.d_dl.T.dot(state.A.dot(state.d_dl))
338
+ self.rho /= predicted_improvement
339
+
340
+ @property
341
+ def is_improvement(self):
342
+ return self.rho > 0
343
+
344
+ @property
345
+ def improvement(self):
346
+ return (np.linalg.norm(self.state.r)**2 - np.linalg.norm(self.r)**2) / np.linalg.norm(self.state.r)**2
347
+
348
+ def trial_r(self, proposed_r):
349
+ return self.Trial(proposed_r, self)
350
+
351
+ def updateRadius(self, rho, lb=.05, ub=.9):
352
+ if rho > ub:
353
+ self.delta = max(self.delta, 2.5*np.linalg.norm(self.d_dl))
354
+ elif rho < lb:
355
+ self.delta *= .25
356
+
357
+
358
+ def minimize_dogleg(obj, free_variables, on_step=None,
359
+ maxiter=200, max_fevals=np.inf, sparse_solver='spsolve',
360
+ disp=True, e_1=1e-15, e_2=1e-15, e_3=0., delta_0=None,
361
+ treat_as_dense=False):
362
+ """"Nonlinear optimization using Powell's dogleg method.
363
+ See Lourakis et al, 2005, ICCV '05, "Is Levenberg-Marquardt the
364
+ Most Efficient Optimization for Implementing Bundle Adjustment?":
365
+ http://www.ics.forth.gr/cvrl/publications/conferences/0201-P0401-lourakis-levenberg.pdf
366
+
367
+ e_N are stopping conditions:
368
+ e_1 is gradient magnatude threshold
369
+ e_2 is step size magnatude threshold
370
+ e_3 is improvement threshold (as a ratio; 0.1 means it must improve by 10%% at each step)
371
+
372
+ maxiter and max_fevals are also stopping conditions. Note that they're not quite the same,
373
+ as an iteration may evaluate the function more than once.
374
+
375
+ sparse_solver is the solver to use to calculate the Gauss-Newton step in the common case
376
+ that the Jacobian is sparse. It can be 'spsolve' (in which case scipy.sparse.linalg.spsolve
377
+ will be used), 'cg' (in which case scipy.sparse.linalg.cg will be used), or any callable
378
+ that matches the api of scipy.sparse.linalg.spsolve to solve `A x = b` for x where A is sparse.
379
+
380
+ cg, uses a Conjugate Gradient method, and will be faster if A is sparse but x is dense.
381
+ spsolve will be faster if x is also sparse.
382
+
383
+ delta_0 defines the initial trust region. Generally speaking, if this is set too low then
384
+ the optimization will never really go anywhere (to small a trust region to make any real
385
+ progress before running out of iterations) and if it's set too high then the optimization
386
+ will diverge immidiately and go wild (such a large trust region that the initial step so
387
+ far overshoots that it can't recover). If it's left as None, it will be automatically
388
+ estimated on the first iteration; it's always updated at each iteration, so this is treated
389
+ only as an initialization.
390
+
391
+ handle_as_dense explicitly converts all Jacobians of obj to dense matrices
392
+ """
393
+
394
+
395
+ solve = setup_sparse_solver(sparse_solver)
396
+ obj, callback = setup_objective(obj, free_variables, on_step=on_step, disp=disp,
397
+ make_dense=treat_as_dense)
398
+
399
+ state = DoglegState(delta=delta_0, solve=solve)
400
+ state.p = obj.x.r
401
+
402
+ #inject profiler if in DEBUG mode
403
+ if ch.DEBUG:
404
+ from .monitor import DrWrtProfiler
405
+ obj.profiler = DrWrtProfiler(obj)
406
+
407
+ callback()
408
+ state.updateJ(obj)
409
+ state.r = obj.r
410
+
411
+ def stop(msg):
412
+ if not state.done:
413
+ pif(msg)
414
+ state.done = True
415
+
416
+ if np.linalg.norm(state.g, np.inf) < e_1:
417
+ stop('stopping because norm(g, np.inf) < %.2e' % e_1)
418
+ while not state.done:
419
+ state.start_iteration()
420
+ while True:
421
+ state.update_step()
422
+ if state.step_size <= e_2 * np.linalg.norm(state.p):
423
+ stop('stopping because of small step size (norm_dl < %.2e)' % (e_2 * np.linalg.norm(state.p)))
424
+ else:
425
+ tm = timer()
426
+ obj.x = state.p + state.step
427
+ trial = state.trial_r(obj.r)
428
+ pif('Residuals computed in %.2fs' % tm())
429
+ # if the objective function improved, update input parameter estimate.
430
+ # Note that the obj.x already has the new parms,
431
+ # and we should not set them again to the same (or we'll bust the cache)
432
+ if trial.is_improvement:
433
+ state.p = state.p + state.step
434
+ callback()
435
+ if e_3 > 0. and trial.improvement < e_3:
436
+ stop('stopping because improvement < %.1e%%' % (100*e_3))
437
+ else:
438
+ state.updateJ(obj)
439
+ state.r = trial.r
440
+ if np.linalg.norm(state.g, np.inf) < e_1:
441
+ stop('stopping because norm(g, np.inf) < %.2e' % e_1)
442
+ else: # Put the old parms back
443
+ obj.x = ch.Ch(state.p)
444
+ obj.on_changed('x') # copies from flat vector to free variables
445
+ # update our trust region
446
+ state.updateRadius(trial.rho)
447
+ if state.delta <= e_2*np.linalg.norm(state.p):
448
+ stop('stopping because trust region is too small')
449
+ if state.done or trial.is_improvement or (obj.fevals >= max_fevals):
450
+ break
451
+ if state.iteration >= maxiter:
452
+ stop('stopping because max number of user-specified iterations (%d) has been met' % maxiter)
453
+ elif obj.fevals >= max_fevals:
454
+ stop('stopping because max number of user-specified func evals (%d) has been met' % max_fevals)
455
+ return obj.free_variables