root/projects/AsynCluster/trunk/svpmc/params.py

Revision 207, 19.9 kB (checked in by edsuom, 6 months ago)

Got Rao-Blackwellized version working locally & remotely, with better but still disappointing degeneracy

Line 
1 # svpmc: Stochastic Volatility Inference via Population Monte Carlo
2 #
3 # Copyright (C) 2007-2008 by Edwin A. Suominen, http://www.eepatents.com
4 #
5 # This program is free software; you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE.  See the file COPYING for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # this program; if not, write to the Free Software Foundation, Inc., 51
16 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
17
18 """
19 Model parameters
20 """
21
22 import os.path
23 import scipy as s
24 from scipy import linalg
25 from scipy.stats import distributions
26
27 from twisted_goodies.pybywire.params import Parameterized
28
29
30 SHAPE_MISMATCH = ValueError(
31     "shape mismatch: objects cannot be broadcast to a single shape")
32
33
34 class FlexArray(object):
35     """
36     I am an array of arbitrary objects, accessible in much the same manner as the
37     elements of a SciPy array object. Initially, all my objects are C{None}.
38
39     Call an instance of me with a hash array and you'll get a new instance with
40     just the subset of objects referenced by the array. Call the instance with
41     a single object ID and you'll get that object, just like you'd get a
42     numeric scalar by addressing a single element of a SciPy array. In either
43     case, you can supply a raveled list of the new values (objects, numeric
44     scalars, whatever) to be used instead of the instance's current objects.
45
46     You can add two like-dimensioned instances of me. Each element of the sum
47     will be the sum of the respective objects of the added L{FlexArray}
48     instances.
49
50     You can call the L{call} method on an instance of me with a method name and
51     zero or more arguments. The result will be a numeric array of the same
52     shape as the instance, each element of which will have the numeric result
53     of a call of the named method on each of the corresponding objects in the
54     instance. Any argument that is an array (or L{FlexArray} instance) of the
55     same shape as my instance will have each the value of each element
56     corresponding to the called object's element extracted and used as the
57     method argument instead of the original array argument.
58
59     You can get and set an attribute of an instance's objects in array fashion
60     by simply setting or getting the named attribute of the instance.
61
62     B{Caveat}: Unlike the behavior of SciPy arrays, Setting elements of a
63     subset or raveled version of an instance of me does I{not} set the
64     corresponding element of the original instance.
65     """
66     @classmethod
67     def concatenate(cls, FA_list):
68         """
69         Returns an instance of me that is a concatentation of the instances in
70         the supplied list I{FA_list}. Analogous to SciPy's C{concatenate}
71         function.
72         """
73         length = 0
74         shape = None
75         for FA in FA_list:
76             length += len(FA)
77             if shape is None:
78                 shape = FA.shape
79             elif FA.shape[1:] != shape[1:]:
80                 raise ValueError(
81                     "FlexArray dimensions must agree except for d_0")
82         shape = list(shape)
83         shape[0] = length
84         newVersion = cls(*shape)
85         k = 0
86         for FA in FA_list:
87             for FA_sub in FA:
88                 newVersion[k] = FA_sub
89                 k += 1
90         return newVersion
91
92     def __init__(self, *shape):
93         self._shape = tuple([int(x) for x in shape])
94         self._N = 1
95         for dim in self._shape:
96             self._N *= dim
97         # Object list
98         self._O = [None] * self._N
99         # Index array
100         self._I = s.arange(self._N).reshape(*self._shape)
101
102     def __call__(self, I, valueList=None):
103         if valueList is None:
104             valueList = self._O
105         if isinstance(I, int):
106             return valueList[I]
107         newVersion = self.__class__(*I.shape)
108         for j, k in enumerate(I.ravel()):
109             newVersion._O[j] = valueList[k]
110         return newVersion
111
112     def __len__(self):
113         return self.shape[0]
114    
115     def __iter__(self):
116         self._iterator = iter(self._I)
117         return self
118
119     def next(self):
120         return self(self._iterator.next())
121    
122     def __getitem__(self, key):
123         return self(self._I[key])
124
125     def __setitem__(self, key, value):
126         I = self._I[key]
127         if isinstance(I, int):
128             self._O[I] = value
129         elif I.shape == s.shape(value):
130             if not isinstance(value, (list, tuple)):
131                 value = value.ravel()
132             for j, k in enumerate(I.ravel()):
133                 self._O[k] = value[j]
134         else:
135             raise SHAPE_MISMATCH
136
137     def ravel(self):
138         return self(self._I.ravel())
139
140     def reshape(self, *shape):
141         return self(self._I.reshape(*shape))
142
143     def __add__(self, other):
144         if other.shape != self.shape:
145             raise SHAPE_MISMATCH
146         newVersion = self(self._I)
147         for k, thisObj in enumerate(newVersion._O):
148             thisObj += other._O[k]
149         return newVersion
150
151     def __getattr__(self, name):
152         """
153         For each of my objects, get the specified numeric attribute.
154
155         Returns the result of each attribute value in an array of the same
156         shape as my object array.
157
158         If any value is not a numeric scalar, the result will be another
159         instance of me with the results (of whatever type) as its objects.
160         """
161         if name == 'shape':
162             return self._shape
163         x = []
164         allNumeric = True
165         for k in self._I.ravel():
166             thisValue = getattr(self._O[k], name)
167             if allNumeric and not isinstance(thisValue, (int, long, float)):
168                 allNumeric = False
169             x.append(thisValue)
170         if allNumeric:
171             return s.array(x).reshape(*self._shape)
172         return self(self._I, x)
173
174     def __setattr__(self, name, value):
175         """
176         For each of my objects, sets the specified numeric attribute to the
177         supplied value.
178
179         If the value is an array (or L{FlexArray}) with the same shape as me
180         (not a list or tuple), each object's attribute value will be set to the
181         value of the corresponding array element.
182         """
183         if name.startswith('_'):
184             object.__setattr__(self, name, value)
185         else:
186             if hasattr(value, 'shape') and value.shape == self.shape:
187                 values = value.ravel()
188             else:
189                 values = [value] * self._N
190             for k, thisValue in enumerate(values):
191                 setattr(self._O[k], name, thisValue)
192    
193     def call(self, methodName, *args, **kw):
194         """
195         For each of my objects, call the method specified by I{methodName} with
196         any further args and keywords supplied.
197
198         For any argument that is an array (or L{FlexArray}) with the same shape
199         as me (not a list or tuple), only the corresponding array element will
200         be supplied as the argument to each object's method call. You can force
201         an array argument to be supplied to the call in its entirety (not
202         element-by-element) even if its shape matches mine by including its
203         position index (with 0 as the first argument after the method name) in
204         a sequence defined via the keyword I{arrayArgs}. For example, you can
205         force the first argument (after the method name) to be an array
206         argument to all method calls with C{arrayArgs=(1,)}.
207         
208         Returns the result of each call in an array of the same shape as my
209         object array. The results must all be numeric scalars.
210
211         If the result of any call is not a numeric scalar, the result will be
212         another instance of me with the results (of whatever type) as its
213         objects.
214         """
215         indicesOfArrayArgs = [
216             k for k, arg in enumerate(args)
217             if hasattr(arg, 'shape') and \
218                 arg.shape == self.shape and \
219                 k not in kw.get('arrayArgs', [])]
220         # This is a shallow list copy
221         theseArgs = [arg for arg in args]
222         x = []
223         allNumeric = True
224         for j in self._I.ravel():
225             for k in indicesOfArrayArgs:
226                 theseArgs[k] = args[k].ravel()[j]
227             thisValue = getattr(self._O[j], methodName)(*theseArgs)
228             if allNumeric and not isinstance(thisValue, (int, long, float)):
229                 allNumeric = False
230             x.append(thisValue)
231         if allNumeric:
232             return s.array(x).reshape(*self._shape)
233         return self(self._I, x)
234
235
236 class ParameterContainer(Parameterized):
237     """
238     I am a container for arrays of model parameters.
239
240     In addition to the parameter arrays, I house as key attributes:
241
242         - A scalar I{Lx} with the log-likelihood of the model's data given my
243           parameters and some externally supplied normal variates underlying
244           sets of simulated log-volatilites.
245
246         - A scalar I{Lp} with the prior log-probability density of the
247           parameters.
248
249         - A scalar I{Lj} with the log-probability density of the proposal jump
250           resulting in my parameters.
251     
252     """
253     keyAttrs = {'Lx':None, 'Lp':None, 'Lj':None}
254
255
256 class Prior(Parameterized):
257     """
258     You must define the name of an underlying dist object from the scipy.stats
259     L{distributions} module via my parameter I{dname}.
260
261     For all underlying dist objects, you must also specify I{loc} and I{scale} as
262     additional parameters. Some of the objects also require I{a} and I{b}.
263
264     The exception is that you can use 'constant' as a distribution name. Then
265     the only pertinent parameter is the constant, unvarying I{loc}.
266
267     """
268     paramNames = ['dname', 'loc', 'scale', 'a', 'b']
269     N_draw = 1000
270
271     class Constant(object):
272         def __init__(self, loc):
273             self.loc = loc
274         def pdf(self, x):
275             return 1.0 * (x == self.loc)
276         def rvs(self, N):
277             return self.loc * s.ones(N)
278         def ppf(self, *args):
279             return 0.0
280
281     class Jump(object):
282         __slots__ = ('x', 'px', 'pJump')
283         def __init__(self, x, px, pJump):
284             self.x = x
285             self.px = px
286             self.pJump = pJump
287    
288     def _get_distObj(self):
289         if 'dist' not in self.cache:
290             if self.dname == 'constant':
291                 self.cache['dist'] = self.Constant(self.loc)
292             elif not hasattr(distributions, self.dname):
293                 raise ImportError(
294                     "No distribution '%s' in scipy.stats.distributions" \
295                     % self.dname)
296             else:
297                 distClass = getattr(distributions, self.dname)
298                 args = []
299                 for xName in ('a', 'b'):
300                     xValue = getattr(self, xName, None)
301                     if xValue is not None:
302                         args.append(xValue)
303                 kw = {'loc':self.loc, 'scale':self.scale}
304                 self.cache['dist'] = distClass(*args, **kw)
305         return self.cache['dist']
306     distObj = property(_get_distObj)
307
308     def _get_rdsObj(self):
309         if 'rds' not in self.cache:
310             width = s.diff(self.distObj.ppf([0.15, 0.85]))[0]
311             self.cache['rds'] = distributions.norm(0, width)
312         return self.cache['rds']
313     rdsObj = property(_get_rdsObj)
314
315     def _newDraw(self):
316         rJumps = self.rdsObj.rvs(self.N_draw)
317         pJumps = self.rdsObj.pdf(rJumps)
318         return {'k':-1, 'N':self.N_draw, 'R':rJumps, 'P':pJumps}
319    
320     def _rJumpDraw(self):
321         if 'draw' not in self.cache:
322             draw = self.cache['draw'] = self._newDraw()
323         else:
324             draw = self.cache['draw']
325             draw['k'] += 1
326             if draw['k'] >= draw['N']:
327                 draw = self.cache['draw'] = self._newDraw()
328         k = draw['k']
329         return draw['R'][k], draw['P'][k]
330    
331     def pdf(self, x):
332         """
333         Returns probability density of the supplied parameter value I{x}.
334         """
335         if x.shape:
336             return self.distObj.pdf(x)
337         return float(self.distObj.pdf(x))
338    
339     def rvs(self):
340         """
341         Returns a parameter value drawn from my distribution.
342         """
343         return self.distObj.rvs(1)[0]
344
345     def rJump(self, startValue, v, N_attempts=200):
346         """
347         Call this method with a I{startValue} and the jump deviation I{v} to
348         use for the jump.
349
350         Draws a jump value from a normal distribution with a standard deviation
351         that is set to approximate the 70% probability width of my prior
352         distribution, scaled by the jump deviation. (The 70% width is the +/- 1
353         standard deviation width of a normal distribution.)
354         
355         Returns a tiny object with three attributes, conveniently accessible as
356         part of a L{FlexArray}:
357
358             - B{x}: the value after the jump, and
359
360             - B{px}: the probability density of I{x}, given the prior
361               distribution.
362
363             - B{pj}: the probability density of the jump, given the jump
364               deviation used.
365         
366         For my Gaussian-PDF deviate generator, the probability density of a
367         jump is inversely proportional to the scaling to its standard deviation
368         imparted by the jump deviation. Thus, the unit-normal PDF is divided by
369         each possible jump deviation in the I{p} array of the result.
370         """
371         vInverse = 1.0 / v
372         for k in xrange(N_attempts):
373             rJump, pJump = self._rJumpDraw()
374             rJump *= v
375             pJump *= vInverse
376             x = startValue + rJump
377             px = self.pdf(x)
378             if px > 0:
379                 break
380         else:
381             print "Oops"
382             # Could't come up with a jump from this point, fall back to an
383             # independent draw from the prior distribution
384             x = self.rvs()
385             px = self.pdf(x)
386             pJump = 1.0  # No jump involved
387         return self.Jump(x, px, pJump)
388
389
390 class PriorContainer(object):
391     """
392     I am a container for array-like instances of L{FlexArray} that contain
393     L{Prior} objects for the array elements of my model's parameters.
394
395     @ivar n: The number of samples.
396     
397     @ivar p: The number of time series.
398
399     @ivar primaryParamNames: A list of the primary parameter names in sequence.
400
401     @ivar derivedParamNames: A list of the derived parameter names in sequence.
402     
403     """
404     typeChecking = True
405     N_attempts = 200
406    
407     def __init__(self, p, n):
408         self.p = p
409         self.n = n
410    
411     def priorArray(self, paramName, *shape):
412         array = getattr(self, paramName, None)
413         if array is None:
414             array = FlexArray(*shape)
415             setattr(self, paramName, array)
416         return array
417
418     def covarMatrix(self, cs, cr):
419         """
420         Returns a covariance matrix from the vector or sequence of I{cs}
421         standard deviations and I{cr} cross-correlations.
422
423         The I{cs} argument is in the form s0, s1,..., sp. It must have C{p}
424         elements.
425
426         The I{cr} argument must have C{0.5*(p**2 + p)} elements and is in the
427         form r00, r01, r02,..., r0p, r11, r12,..., r1p,..., rpp
428         """
429         i = 0
430         p = len(cs)
431         cv = s.zeros((p, p))
432         for j in xrange(p):
433             cv[j,j] = cs[j]**2
434             for k in xrange(j+1, p):
435                 cv[j, k] = cv[k, j] = cr[i] * cs[j] * cs[k]
436                 i += 1
437         return cv
438    
439     def derivedParams(self, paramContainer):
440         """
441         Sets derived parameters in the supplied I{paramContainer} based on its
442         primary parameters, unless there is an error in generating the derived
443         parameters.
444
445         Returns C{True} if everything went OK, C{False} if not.
446         """
447         # Concurrent cross-correlations between volatility shocks and inverse
448         # of concurrent cross-correlations between innovation shocks
449         try:
450             rz = linalg.cholesky(self.covarMatrix(
451                 paramContainer.fs, paramContainer.fr), lower=True)
452             ri = linalg.inv(linalg.cholesky(self.covarMatrix(
453                 s.ones(self.p), paramContainer.cr), lower=True))
454         except linalg.LinAlgError:
455             return False
456         paramContainer.rz = rz
457         paramContainer.ri = ri
458         # Scaling constants for multivariate normal pdf
459         sc = s.empty((self.p, 2))
460         sc[:,0] = 1.0 / s.sqrt(1.0 - paramContainer.g**2)
461         sc[:,1] = s.log(s.sqrt(2*s.pi) / sc[:,0])
462         paramContainer.sc = sc
463         return True
464
465     def new(self):
466         """
467         Returns a new parameter container with values intialized from a random
468         draw of my priors.
469
470         The method will generate derived parameters based on the primary
471         parameter values, repeating as needed to get primary values that
472         work. The derived values are included in the returned parameter
473         container.
474         """
475         paramContainer = ParameterContainer(
476             paramNames=self.primaryParamNames+self.derivedParamNames)
477         for k in xrange(self.N_attempts):
478             Lp = 0
479             for name in self.primaryParamNames:
480                 priorFlexArray = getattr(self, name)
481                 paramArray = priorFlexArray.call('rvs')
482                 setattr(paramContainer, name, paramArray)
483                 Lp += s.log(priorFlexArray.call('pdf', paramArray)).sum()
484             if self.derivedParams(paramContainer):
485                 break
486         else:
487             raise ValueError(
488                 "Couldn't generate a valid set of derived parameters")
489         paramContainer.Lp = Lp
490         paramContainer.Lj = 0 # There was no jump
491         return paramContainer
492
493     def proposal(self, paramContainer, v):
494         """
495         Returns a new instance of L{ParameterContainer} with a random-walk
496         proposal based on the supplied I{paramContainer}, my priors, and the
497         supplied jump deviation I{v}.
498
499         The returned parameter container object will have new values of I{Lp}
500         and I{Lj} corresponding to the proposal.
501
502         The value of I{Lj} is computed for each parameter element from a normal
503         jump distribution whose width is proportional to the jump deviation
504         I{v} and the width of the parameter's prior distribution.
505
506         The method will generate derived parameters based on the primary
507         parameter values of the proposal, repeating as needed to get primary
508         values that work. The derived values are included in the returned
509         parameter container.
510         """
511         if self.typeChecking and \
512                not isinstance(paramContainer, ParameterContainer):
513             raise TypeError(
514                 "You must supply a ParameterContainer as the first "+\
515                 "argument, not a '%s'" % type(paramContainer))
516         newVersion = ParameterContainer(
517             paramNames=self.primaryParamNames + self.derivedParamNames)
518         for k in xrange(self.N_attempts):
519             Lp, Lj = 0, 0
520             # Define a 1-D array holding the probability density for each jump,
521             # joint across all parameters. Since the densities will be scaled
522             # by the weight for each jump deviation, just start the array with
523             # the weights.
524             for name in self.primaryParamNames:
525                 priorsFA = getattr(self, name)
526                 startValues = getattr(paramContainer, name)
527                 jumpsFA = priorsFA.call(
528                     'rJump', startValues, v, N_attempts=self.N_attempts)
529                 setattr(newVersion, name, jumpsFA.x)
530                 Lp += s.log(jumpsFA.px).sum()
531                 Lj += s.log(jumpsFA.pJump).sum()
532             if self.derivedParams(newVersion):
533                 break
534         else:
535             raise ValueError(
536                 "Couldn't generate a valid set of derived parameters")
537         newVersion.Lp = Lp
538         newVersion.Lj = Lj
539         return newVersion
Note: See TracBrowser for help on using the browser.