2
2
import numpy .random as nr
3
3
import theano
4
4
5
+ from ..distributions import draw_values
5
6
from .arraystep import ArrayStepShared , ArrayStep , metrop_select , Competence
6
7
import pymc3 as pm
7
8
8
9
9
- __all__ = ['Metropolis' , 'BinaryMetropolis' , 'BinaryGibbsMetropolis' , 'NormalProposal' ,
10
- 'CauchyProposal' , 'LaplaceProposal' , 'PoissonProposal' , 'MultivariateNormalProposal' ]
10
+ __all__ = ['Metropolis' , 'BinaryMetropolis' , 'BinaryGibbsMetropolis' ,
11
+ 'CategoricalGibbsMetropolis' , 'NormalProposal' , 'CauchyProposal' ,
12
+ 'LaplaceProposal' , 'PoissonProposal' , 'MultivariateNormalProposal' ]
11
13
12
14
# Available proposal distributions for Metropolis
13
15
@@ -239,41 +241,51 @@ def competence(var):
239
241
240
242
241
243
class BinaryGibbsMetropolis (ArrayStep ):
242
- """Metropolis-Hastings optimized for binary variables"""
244
+ """A Metropolis-within-Gibbs step method optimized for binary variables"""
243
245
244
246
def __init__ (self , vars , order = 'random' , model = None ):
245
247
246
248
model = pm .modelcontext (model )
247
249
248
250
self .dim = sum (v .dsize for v in vars )
249
- self .order = order
251
+
252
+ if order == 'random' :
253
+ self .shuffle_dims = True
254
+ self .order = list (range (self .dim ))
255
+ else :
256
+ if sorted (order ) != list (range (self .dim )):
257
+ raise ValueError ('Argument \' order\' has to be a permutation' )
258
+ self .shuffle_dims = False
259
+ self .order = order
250
260
251
261
if not all ([v .dtype in pm .discrete_types for v in vars ]):
252
262
raise ValueError (
253
- 'All variables must be Bernoulli for BinaryGibbsMetropolis' )
263
+ 'All variables must be binary for BinaryGibbsMetropolis' )
254
264
255
265
super (BinaryGibbsMetropolis , self ).__init__ (vars , [model .fastlogp ])
256
266
257
267
def astep (self , q0 , logp ):
258
- order = list ( range ( self .dim ))
259
- if self .order == 'random' :
268
+ order = self .order
269
+ if self .shuffle_dims :
260
270
nr .shuffle (order )
261
271
262
- q_prop = np .copy (q0 )
263
- q_cur = np . copy ( q0 )
272
+ q = np .copy (q0 )
273
+ logp_curr = logp ( q )
264
274
265
275
for idx in order :
266
- q_prop [idx ] = True - q_prop [idx ]
267
- q_cur = metrop_select (logp (q_prop ) - logp (q_cur ), q_prop , q_cur )
268
- q_prop = np .copy (q_cur )
276
+ curr_val , q [idx ] = q [idx ], True - q [idx ]
277
+ logp_prop = logp (q )
278
+ q [idx ] = metrop_select (logp_prop - logp_curr , q [idx ], curr_val )
279
+ if q [idx ] != curr_val :
280
+ logp_curr = logp_prop
269
281
270
- return q_cur
282
+ return q
271
283
272
284
@staticmethod
273
285
def competence (var ):
274
286
'''
275
- BinaryMetropolis is only suitable for binary (bool)
276
- and Categorical variables with k=1 .
287
+ BinaryMetropolis is only suitable for Bernoulli
288
+ and Categorical variables with k=2 .
277
289
'''
278
290
distribution = getattr (
279
291
var .distribution , 'parent_dist' , var .distribution )
@@ -283,6 +295,132 @@ def competence(var):
283
295
return Competence .IDEAL
284
296
return Competence .INCOMPATIBLE
285
297
298
+ class CategoricalGibbsMetropolis (ArrayStep ):
299
+ """A Metropolis-within-Gibbs step method optimized for categorical variables.
300
+ This step method works for Bernoulli variables as well, but it is not
301
+ optimized for them, like BinaryGibbsMetropolis is. Step method supports
302
+ two types of proposals: A uniform proposal and a proportional proposal,
303
+ which was introduced by Liu in his 1996 technical report
304
+ "Metropolized Gibbs Sampler: An Improvement".
305
+ """
306
+
307
+ def __init__ (self , vars , proposal = 'uniform' , order = 'random' , model = None ):
308
+
309
+ model = pm .modelcontext (model )
310
+ vars = pm .inputvars (vars )
311
+
312
+ dimcats = []
313
+ # The above variable is a list of pairs (aggregate dimension, number
314
+ # of categories). For example, if vars = [x, y] with x being a 2-D
315
+ # variable with M categories and y being a 3-D variable with N
316
+ # categories, we will have dimcats = [(0, M), (1, M), (2, N), (3, N), (4, N)].
317
+ for v in vars :
318
+ distr = getattr (v .distribution , 'parent_dist' , v .distribution )
319
+ if isinstance (distr , pm .Categorical ):
320
+ k = draw_values ([distr .k ])[0 ]
321
+ elif isinstance (distr , pm .Bernoulli ) or (v .dtype in pm .bool_types ):
322
+ k = 2
323
+ else :
324
+ raise ValueError ('All variables must be categorical or binary' +
325
+ 'for CategoricalGibbsMetropolis' )
326
+ start = len (dimcats )
327
+ dimcats += [(dim , k ) for dim in range (start , start + v .dsize )]
328
+
329
+ if order == 'random' :
330
+ self .shuffle_dims = True
331
+ self .dimcats = dimcats
332
+ else :
333
+ if sorted (order ) != list (range (len (dimcats ))):
334
+ raise ValueError ('Argument \' order\' has to be a permutation' )
335
+ self .shuffle_dims = False
336
+ self .dimcats = [dimcats [j ] for j in order ]
337
+
338
+ if proposal == 'uniform' :
339
+ self .astep = self .astep_unif
340
+ elif proposal == 'proportional' :
341
+ # Use the optimized "Metropolized Gibbs Sampler" described in Liu96.
342
+ self .astep = self .astep_prop
343
+ else :
344
+ raise ValueError ('Argument \' proposal\' should either be ' +
345
+ '\' uniform\' or \' proportional\' ' )
346
+
347
+ super (CategoricalGibbsMetropolis , self ).__init__ (vars , [model .fastlogp ])
348
+
349
+ def astep_unif (self , q0 , logp ):
350
+ dimcats = self .dimcats
351
+ if self .shuffle_dims :
352
+ nr .shuffle (dimcats )
353
+
354
+ q = np .copy (q0 )
355
+ logp_curr = logp (q )
356
+
357
+ for dim , k in dimcats :
358
+ curr_val , q [dim ] = q [dim ], sample_except (k , q [dim ])
359
+ logp_prop = logp (q )
360
+ q [dim ] = metrop_select (logp_prop - logp_curr , q [dim ], curr_val )
361
+ if q [dim ] != curr_val :
362
+ logp_curr = logp_prop
363
+
364
+ return q
365
+
366
+ def astep_prop (self , q0 , logp ):
367
+ dimcats = self .dimcats
368
+ if self .shuffle_dims :
369
+ nr .shuffle (dimcats )
370
+
371
+ q = np .copy (q0 )
372
+ logp_curr = logp (q )
373
+
374
+ for dim , k in dimcats :
375
+ logp_curr = self .metropolis_proportional (q , logp , logp_curr , dim , k )
376
+
377
+ return q
378
+
379
+ def metropolis_proportional (self , q , logp , logp_curr , dim , k ):
380
+ given_cat = int (q [dim ])
381
+ log_probs = np .zeros (k )
382
+ log_probs [given_cat ] = logp_curr
383
+ candidates = list (range (k ))
384
+ for candidate_cat in candidates :
385
+ if candidate_cat != given_cat :
386
+ q [dim ] = candidate_cat
387
+ log_probs [candidate_cat ] = logp (q )
388
+ probs = softmax (log_probs )
389
+ prob_curr , probs [given_cat ] = probs [given_cat ], 0.0
390
+ probs /= (1.0 - prob_curr )
391
+ proposed_cat = nr .choice (candidates , p = probs )
392
+ accept_ratio = (1.0 - prob_curr ) / (1.0 - probs [proposed_cat ])
393
+ if not np .isfinite (accept_ratio ) or nr .uniform () >= accept_ratio :
394
+ q [dim ] = given_cat
395
+ return logp_curr
396
+ q [dim ] = proposed_cat
397
+ return log_probs [proposed_cat ]
398
+
399
+ @staticmethod
400
+ def competence (var ):
401
+ '''
402
+ CategoricalGibbsMetropolis is only suitable for Bernoulli and
403
+ Categorical variables.
404
+ '''
405
+ distribution = getattr (
406
+ var .distribution , 'parent_dist' , var .distribution )
407
+ if isinstance (distribution , pm .Categorical ):
408
+ if distribution .k > 2 :
409
+ return Competence .IDEAL
410
+ return Competence .COMPATIBLE
411
+ elif isinstance (distribution , pm .Bernoulli ) or (var .dtype in pm .bool_types ):
412
+ return Competence .COMPATIBLE
413
+ return Competence .INCOMPATIBLE
414
+
415
+ def sample_except (limit , excluded ):
416
+ candidate = nr .choice (limit - 1 )
417
+ if candidate >= excluded :
418
+ candidate += 1
419
+ return candidate
420
+
421
+ def softmax (x ):
422
+ e_x = np .exp (x - np .max (x ))
423
+ return e_x / np .sum (e_x , axis = 0 )
286
424
287
425
def delta_logp (logp , vars , shared ):
288
426
[logp0 ], inarray0 = pm .join_nonshared_inputs ([logp ], vars , shared )
0 commit comments