/
abstract_model.py
763 lines (633 loc) · 29.5 KB
/
abstract_model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# abstract_model.py: Abstract interfaces for models with different levels of
# functionality.
##
# © 2017, Chris Ferrie (csferrie@gmail.com) and
# Christopher Granade (cgranade@cgranade.com).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
##
## FEATURES ##################################################################
from __future__ import absolute_import
from __future__ import division, unicode_literals
## EXPORTS ###################################################################
__all__ = [
'Simulatable',
'Model',
'FiniteOutcomeModel',
'DifferentiableModel'
]
## IMPORTS ###################################################################
from builtins import range, map
from future.utils import with_metaclass
import abc
# Python standard library package for specifying abstract classes.
import numpy as np
import warnings
from qinfer.utils import safe_shape
from qinfer.domains import IntegerDomain
## CLASSES ###################################################################
class Simulatable(with_metaclass(abc.ABCMeta, object)):
r"""
Represents a system which can be simulated according to
various model parameters and experimental control parameters
in order to produce representative data.
See :ref:`models_guide` for more details.
:param bool allow_identical_outcomes: Whether the method ``outcomes`` should
be allowed to return multiple identical outcomes for a given ``expparam``.
It will be more efficient to set this to ``True`` whenever it is likely
that multiple identical outcomes will occur.
"""
def __init__(self):
"""
:param bool always_resample_outcomes: Resample outcomes stochastically with
each outcome call.
:param :class:`~numpy.ndarray` initial_outcomes: Initial set of outcomes
that may be supplied. Otherwise initial outcomes default to
zeros.
"""
self._sim_count = 0
# Initialize a default scale matrix.
self._Q = np.ones((self.n_modelparams,))
## ABSTRACT PROPERTIES ##
@abc.abstractproperty
def n_modelparams(self):
"""
Returns the number of real model parameters admitted by this model.
This property is assumed by inference engines to be constant for
the lifetime of a :class:`Model` instance.
"""
pass
@abc.abstractproperty
def expparams_dtype(self):
"""
Returns the dtype of an experiment parameter array. For a
model with single-parameter control, this will likely be a scalar dtype,
such as ``"float64"``. More generally, this can be an example of a
record type, such as ``[('time', py.'float64'), ('axis', 'uint8')]``.
This property is assumed by inference engines to be constant for
the lifetime of a Model instance.
"""
pass
## CONCRETE PROPERTIES ##
@property
def is_n_outcomes_constant(self):
"""
Returns ``True`` if and only if both the domain and ``n_outcomes``
are independent of the expparam.
This property is assumed by inference engines to be constant for
the lifetime of a Model instance.
"""
return True
@property
def model_chain(self):
"""
Returns a tuple of models upon which this model is based,
such that properties and methods of underlying models for
models that decorate other models can be accessed. For a
standalone model, this is always the empty tuple.
"""
return ()
@property
def base_model(self):
"""
Returns the most basic model that this model depends on.
For standalone models, this property satisfies ``model.base_model is model``.
"""
return self
@property
def underlying_model(self):
"""
Returns the model that this model is based on (decorates)
if such a model exists, or ``None`` if this model is
independent.
"""
return self.model_chain[-1] if self.model_chain else None
@property
def sim_count(self):
"""
Returns the number of data samples that have been produced by
this simulator.
:rtype: int
"""
return self._sim_count
@property
def Q(self):
r"""
Returns the diagonal of the scale matrix :math:`\matr{Q}` that
relates the scales of each of the model parameters. In particular,
the quadratic loss for this Model is defined as:
.. math::
L_{\matr{Q}}(\vec{x}, \hat{\vec{x}}) =
(\vec{x} - \hat{\vec{x}})^\T \matr{Q} (\vec{x} - \hat{\vec{x}})
If a subclass does not explicitly define the scale matrix, it is taken
to be the identity matrix of appropriate dimension.
:return: The diagonal elements of :math:`\matr{Q}`.
:rtype: :class:`~numpy.ndarray` of shape ``(n_modelparams, )``.
"""
return self._Q
@property
def modelparam_names(self):
"""
Returns the names of the various model parameters admitted by this
model, formatted as LaTeX strings.
"""
return list(map("x_{{{}}}".format, range(self.n_modelparams)))
## CONCRETE METHODS ##
def _repr_html_(self, suppress_base=False):
s = r"""
<strong>{type.__name__}</strong> at 0x{id:0x}: {n_mp} model parameter{plural}
""".format(
id=id(self), type=type(self),
n_mp=self.n_modelparams,
plural="" if self.n_modelparams == 1 else "s"
)
if not suppress_base and self.model_chain:
s += r"""<br>
<p>Model chain:</p>
<ul>{}
</ul>
""".format(r"\n".join(
u"<li>{}</li>".format(model._repr_html_(suppress_base=True))
for model in reversed(self.model_chain)
))
return s
def are_expparam_dtypes_consistent(self, expparams):
"""
Returns ``True`` iff all of the given expparams
correspond to outcome domains with the same dtype.
For efficiency, concrete subclasses should override this method
if the result is always ``True``.
:param np.ndarray expparams: Array of expparamms
of type ``expparams_dtype``
:rtype: ``bool``
"""
if self.is_n_outcomes_constant:
# This implies that all domains are equal, so this must be true
return True
# otherwise we have to actually check all the dtypes
if expparams.size > 0:
domains = self.domain(expparams)
first_dtype = domains[0].dtype
return all(domain.dtype == first_dtype for domain in domains[1:])
else:
return True
## ABSTRACT METHODS ##
@abc.abstractmethod
def n_outcomes(self, expparams):
"""
Returns an array of dtype ``uint`` describing the number of outcomes
for each experiment specified by ``expparams``.
If the number of outcomes does not depend on expparams (i.e.
``is_n_outcomes_constant`` is ``True``), this method
should return a single number.
If there are an infinite (or intractibly large) number of outcomes,
this value specifies the number of outcomes to randomly sample.
:param numpy.ndarray expparams: Array of experimental parameters. This
array must be of dtype agreeing with the ``expparams_dtype``
property.
"""
pass
@abc.abstractmethod
def domain(self, exparams):
"""
Returns a list of :class:`Domain` objects, one for each input expparam.
:param numpy.ndarray expparams: Array of experimental parameters. This
array must be of dtype agreeing with the ``expparams_dtype``
property, or, in the case where ``n_outcomes_constant`` is ``True``,
``None`` should be a valid input.
:rtype: list of :class:`Domain`
"""
pass
@abc.abstractmethod
def are_models_valid(self, modelparams):
"""
Given a shape ``(n_models, n_modelparams)`` array of model parameters,
returns a boolean array of shape ``(n_models)`` specifying whether
each set of model parameters represents is valid under this model.
"""
pass
@abc.abstractmethod
def simulate_experiment(self, modelparams, expparams, repeat=1):
"""
Produces data according to the given model parameters and experimental
parameters, structured as a NumPy array.
:param np.ndarray modelparams: A shape ``(n_models, n_modelparams)``
array of model parameter vectors describing the hypotheses under
which data should be simulated.
:param np.ndarray expparams: A shape ``(n_experiments, )`` array of
experimental control settings, with ``dtype`` given by
:attr:`~qinfer.Model.expparams_dtype`, describing the
experiments whose outcomes should be simulated.
:param int repeat: How many times the specified experiment should
be repeated.
:rtype: np.ndarray
:return: A three-index tensor ``data[i, j, k]``, where ``i`` is the repetition,
``j`` indexes which vector of model parameters was used, and where
``k`` indexes which experimental parameters where used. If ``repeat == 1``,
``len(modelparams) == 1`` and ``len(expparams) == 1``, then a scalar
datum is returned instead.
"""
self._sim_count += modelparams.shape[0] * expparams.shape[0] * repeat
assert(self.are_expparam_dtypes_consistent(expparams))
## CONCRETE METHODS ##
def clear_cache(self):
"""
Tells the model to clear any internal caches used in computing
likelihoods and drawing samples. Calling this method should not cause
any different results, but should only affect performance.
"""
# By default, no cache to clear.
pass
def experiment_cost(self, expparams):
"""
Given an array of experimental parameters, returns the cost associated
with performing each experiment. By default, this cost is constant
(one) for every experiment.
:param expparams: An array of experimental parameters for which the cost
is to be evaluated.
:type expparams: :class:`~numpy.ndarray` of ``dtype`` given by
:attr:`~Model.expparams_dtype`
:return: An array of costs corresponding to the specified experiments.
:rtype: :class:`~numpy.ndarray` of ``dtype`` ``float`` and of the
same shape as ``expparams``.
"""
return np.ones(expparams.shape)
def distance(self, a, b):
r"""
Gives the distance between two model parameter vectors :math:`\vec{a}` and
:math:`\vec{b}`. By default, this is the vector 1-norm of the difference
:math:`\mathbf{Q} (\vec{a} - \vec{b})` rescaled by
:attr:`~Model.Q`.
:param np.ndarray a: Array of model parameter vectors having shape
``(n_models, n_modelparams)``.
:param np.ndarray b: Array of model parameters to compare to, having
the same shape as ``a``.
:return: An array ``d`` of distances ``d[i]`` between ``a[i, :]`` and
``b[i, :]``.
"""
return np.apply_along_axis(
lambda vec: np.linalg.norm(vec, 1),
1,
self.Q * (a - b)
)
def update_timestep(self, modelparams, expparams):
r"""
Returns a set of model parameter vectors that is the update of an
input set of model parameter vectors, such that the new models are
conditioned on a particular experiment having been performed.
By default, this is the trivial function
:math:`\vec{x}(t_{k+1}) = \vec{x}(t_k)`.
:param np.ndarray modelparams: Set of model parameter vectors to be
updated.
:param np.ndarray expparams: An experiment parameter array describing
the experiment that was just performed.
:return np.ndarray: Array of shape
``(n_models, n_modelparams, n_experiments)`` describing the update
of each model according to each experiment.
"""
return np.tile(modelparams, (expparams.shape[0],1,1)).transpose((1,2,0))
def canonicalize(self, modelparams):
r"""
Returns a canonical set of model parameters corresponding to a given
possibly non-canonical set. This is used for models in which there
exist model parameters :math:`\vec{x}_i` and :\math:`\vec{x}_j` such
that
.. math::
\Pr(d | \vec{x}_i; \vec{e}) = \Pr(d | \vec{x}_j; \vec{e})
for all outcomes :math:`d` and experiments :math:`\vec{e}`. For
models admitting such an ambiguity, this
method should then be overridden to return a consistent choice
out of such vectors, hence avoiding supurious model degeneracies.
Note that, by default, :class:`~qinfer.smc.SMCUpdater` will *not*
call this method.
"""
return modelparams
class Model(Simulatable):
"""
Represents a system which can be simulated according to
various model parameters and experimental control parameters
in order to produce the probability of a hypothetical data
record. As opposed to :class:`~qinfer.Simulatable`, instances
of :class:`~qinfer.Model` not only produce data consistent
with the description of a system, but also evaluate the probability
of that data arising from the system.
:param bool allow_identical_outcomes: Whether the method
``representative_outcomes`` should be allowed to return multiple
identical outcomes for a given ``expparam``.
:param float outcome_warning_threshold: Threshold value below which
``representative_outcomes``
will issue a warning about the representative outcomes
not adequately covering the domain with respect to
the relevant distribution.
See :ref:`models_guide` for more details.
"""
## INITIALIZERS ##
def __init__(self, allow_identical_outcomes=False,
outcome_warning_threshold=0.99):
super(Model, self).__init__()
self._call_count = 0
self._allow_identical_outcomes = allow_identical_outcomes
self._outcome_warning_threshold = outcome_warning_threshold
## CONCRETE PROPERTIES ##
@property
def call_count(self):
"""
Returns the number of points at which
the probability of this model has been evaluated,
where a point consists of a hypothesis about the model (a vector of
model parameters), an experimental control setting (expparams) and
a hypothetical or actual datum.
:rtype: int
"""
return self._call_count
## ABSTRACT METHODS ##
@abc.abstractmethod
def likelihood(self, outcomes, modelparams, expparams):
r"""
Calculates the probability of each given outcome, conditioned on each
given model parameter vector and each given experimental control setting.
:param np.ndarray modelparams: A shape ``(n_models, n_modelparams)``
array of model parameter vectors describing the hypotheses for
which the likelihood function is to be calculated.
:param np.ndarray expparams: A shape ``(n_experiments, )`` array of
experimental control settings, with ``dtype`` given by
:attr:`~qinfer.Simulatable.expparams_dtype`, describing the
experiments from which the given outcomes were drawn.
:rtype: np.ndarray
:return: A three-index tensor ``L[i, j, k]``, where ``i`` is the outcome
being considered, ``j`` indexes which vector of model parameters was used,
and where ``k`` indexes which experimental parameters where used.
Each element ``L[i, j, k]`` then corresponds to the likelihood
:math:`\Pr(d_i | \vec{x}_j; e_k)`.
"""
# Count the number of times the inner-most loop is called.
self._call_count += (
safe_shape(outcomes) * safe_shape(modelparams) * safe_shape(expparams)
)
@property
def allow_identical_outcomes(self):
"""
Whether the method ``representative_outcomes`` should be allowed to return multiple
identical outcomes for a given ``expparam``.
It will be more efficient to set this to ``True`` whenever it is likely
that multiple identical outcomes will occur.
:return: Flag state.
:rtype: ``bool``
"""
return self._allow_identical_outcomes
@allow_identical_outcomes.setter
def allow_identical_outcomes(self, value):
"""
Whether the method ``representative_outcomes`` should be allowed to return multiple
identical outcomes for a given ``expparam``.
It will be more efficient to set this to ``True`` whenever it is likely
that multiple identical outcomes will occur.
:param bool allow_identical_outcomes: Value of flag.
"""
self._allow_identical_outcomes = value
@property
def outcome_warning_threshold(self):
"""
Threshold value below which ``representative_outcomes``
will issue a warning about the representative outcomes
not adequately covering the domain with respect to
the relevant distribution.
:return: Threshold value.
:rtype: ``float``
"""
return self._outcome_warning_threshold
@outcome_warning_threshold.setter
def outcome_warning_threshold(self, value):
"""
Threshold value below which ``representative_outcomes``
will issue a warning about the representative outcomes
not adequately covering the domain with respect to
the relevant distribution.
:param float value: Threshold value.
"""
self._outcome_warning_threshold = value
## CONCRETE METHODS ##
# These methods depend on the abstract methods, and thus their behaviors
# change in each inheriting class.
def is_model_valid(self, modelparams):
"""
Returns True if and only if the model parameters given are valid for
this model.
"""
return self.are_models_valid(modelparams[np.newaxis, :])[0]
class LinearCostModelMixin(Model):
# FIXME: move this mixin to a new module.
# TODO: test this mixin.
"""
This mixin implements :meth:`Model.experiment_cost` by setting the
cost of an experiment equal to the value of a given field of each
``expparams`` element (by default, ``t``).
"""
_field = "t"
def experiment_cost(self, expparams):
return expparams[self._field]
class FiniteOutcomeModel(Model):
"""
Represents a system in the same way that :class:`~qinfer.Model`,
except that it is demanded that the number of outcomes for any
experiment be known and finite.
:param bool allow_identical_outcomes: Whether the method
``representative_outcomes`` should be allowed to return multiple
identical outcomes for a given ``expparam``.
:param float outcome_warning_threshold: Threshold value below which
``representative_outcomes``
will issue a warning about the representative outcomes
not adequately covering the domain with respect to
the relevant distribution.
:param int n_outcomes_cutoff: If ``n_outcomes`` exceeds this value,
``representative_outcomes`` will use this
value in its place. This is useful in the case
of a finite yet untractible number of outcomes. Use ``None``
for no cutoff.
See :class:`~qinfer.Model` and :ref:`models_guide` for more details.
"""
## INITIALIZERS ##
def __init__(self,
allow_identical_outcomes=False,
outcome_warning_threshold=0.99,
n_outcomes_cutoff=None
):
super(FiniteOutcomeModel, self).__init__(
outcome_warning_threshold=outcome_warning_threshold,
allow_identical_outcomes=allow_identical_outcomes)
self._n_outcomes_cutoff = n_outcomes_cutoff
if self.is_n_outcomes_constant:
# predefine if we can
self._domain = IntegerDomain(min=0,max=self.n_outcomes(None)-1)
## CONCRETE PROPERTIES ##
@property
def n_outcomes_cutoff(self):
"""
If ``n_outcomes`` exceeds this value for
some expparm, ``representative_outcomes`` will use this
value in its place. This is useful in the case
of a finite yet untractible number of outcomes.
:return: Cutoff value.
:rtype: ``int``
"""
return self._n_outcomes_cutoff
@n_outcomes_cutoff.setter
def n_outcomes_cutoff(self, value):
"""
If ``n_outcomes`` exceeds this value,
``representative_outcomes`` will use this
value in its place. This is useful in the case
of a finite yet untractible number of outcomes.
:param int value: Cutoff value.
"""
self.n_outcomes_cutoff = value
## ABSTRACT METHODS ##
## CONCRETE METHODS ##
# These methods depend on the abstract methods, and thus their behaviors
# change in each inheriting class.
def domain(self, expparams):
"""
Returns a list of :class:`Domain` objects, one for each input expparam.
:param numpy.ndarray expparams: Array of experimental parameters. This
array must be of dtype agreeing with the ``expparams_dtype``
property, or, in the case where ``n_outcomes_constant`` is ``True``,
``None`` should be a valid input.
:rtype: list of ``Domain``
"""
# As a convenience to most users, we define domain for them. If a
# fancier domain is desired, this method can easily be overridden.
if self.is_n_outcomes_constant:
return self._domain if expparams is None else [self._domain for ep in expparams]
else:
return [IntegerDomain(min=0,max=n_o-1) for n_o in self.n_outcomes(expparams)]
def simulate_experiment(self, modelparams, expparams, repeat=1):
# Call the superclass simulate_experiment, not recording the result.
# This is used to count simulation calls.
super(FiniteOutcomeModel, self).simulate_experiment(modelparams, expparams, repeat)
if self.is_n_outcomes_constant:
# In this case, all expparams have the same domain
all_outcomes = self.domain(None).values
probabilities = self.likelihood(all_outcomes, modelparams, expparams)
cdf = np.cumsum(probabilities, axis=0)
randnum = np.random.random((repeat, 1, modelparams.shape[0], expparams.shape[0]))
outcome_idxs = all_outcomes[np.argmax(cdf > randnum, axis=1)]
outcomes = all_outcomes[outcome_idxs]
else:
# Loop over each experiment, sadly.
# Assume all domains have the same dtype
assert(self.are_expparam_dtypes_consistent(expparams))
dtype = self.domain(expparams[0, np.newaxis])[0].dtype
outcomes = np.empty((repeat, modelparams.shape[0], expparams.shape[0]), dtype=dtype)
for idx_experiment, single_expparams in enumerate(expparams[:, np.newaxis]):
all_outcomes = self.domain(single_expparams).values
probabilities = self.likelihood(all_outcomes, modelparams, single_expparams)
cdf = np.cumsum(probabilities, axis=0)[..., 0]
randnum = np.random.random((repeat, 1, modelparams.shape[0]))
outcomes[:, :, idx_experiment] = all_outcomes[np.argmax(cdf > randnum, axis=1)]
return outcomes[0, 0, 0] if repeat == 1 and expparams.shape[0] == 1 and modelparams.shape[0] == 1 else outcomes
## STATIC METHODS ##
# These methods are provided as a convienence to make it easier to write
# simple models.
@staticmethod
def pr0_to_likelihood_array(outcomes, pr0):
"""
Assuming a two-outcome measurement with probabilities given by the
array ``pr0``, returns an array of the form expected to be returned by
``likelihood`` method.
:param numpy.ndarray outcomes: Array of integers indexing outcomes.
:param numpy.ndarray pr0: Array of shape ``(n_models, n_experiments)``
describing the probability of obtaining outcome ``0`` from each
set of model parameters and experiment parameters.
"""
pr0 = pr0[np.newaxis, ...]
pr1 = 1 - pr0
if len(np.shape(outcomes)) == 0:
outcomes = np.array(outcomes)[None]
return np.concatenate([
pr0 if outcomes[idx] == 0 else pr1
for idx in range(safe_shape(outcomes))
])
class DifferentiableModel(with_metaclass(abc.ABCMeta, Model)):
@abc.abstractmethod
def score(self, outcomes, modelparams, expparams, return_L=False):
r"""
Returns the score of this likelihood function, defined as:
.. math::
q(d, \vec{x}; \vec{e}) = \vec{\nabla}_{\vec{x}} \log \Pr(d | \vec{x}; \vec{e}).
Calls are represented as a four-index tensor
``score[idx_modelparam, idx_outcome, idx_model, idx_experiment]``.
The left-most index may be suppressed for single-parameter models.
If return_L is True, both `q` and the likelihood `L` are returned as `q, L`.
"""
pass
def fisher_information(self, modelparams, expparams):
"""
Returns the covariance of the score taken over possible outcomes,
known as the Fisher information.
The result is represented as the four-index tensor
``fisher[idx_modelparam_i, idx_modelparam_j, idx_model, idx_experiment]``,
which gives the Fisher information matrix for each model vector
and each experiment vector.
.. note::
The default implementation of this method calls
:meth:`~DifferentiableModel.score()` for each possible outcome,
which can be quite slow. If possible, overriding this method can
give significant speed advantages.
"""
if self.is_n_outcomes_constant:
outcomes = np.arange(self.n_outcomes(expparams))
scores, L = self.score(outcomes, modelparams, expparams, return_L=True)
assert len(scores.shape) in (3, 4)
if len(scores.shape) == 3:
scores = scores[np.newaxis, :, :, :]
# Note that E[score] = 0 by regularity assumptions, so we only
# need the expectation over the outer product.
return np.einsum("ome,iome,jome->ijme",
L, scores, scores
)
else:
# Indexing will be a major pain here, so we need to start
# by making an empty array, so that index errors will be raised
# when (not if!) we make mistakes.
fisher = np.empty((
self.n_modelparams, self.n_modelparams,
modelparams.shape[0], expparams.shape[0]
))
# Now we loop over experiments, since we cannot vectorize the
# expectation value over data.
for idx_experiment, experiment in enumerate(expparams):
experiment = experiment.reshape((1,))
n_o = self.n_outcomes(experiment)
outcomes = np.arange(n_o)
scores, L = self.score(outcomes, modelparams, experiment, return_L=True)
fisher[:, :, :, idx_experiment] = np.einsum("ome,iome,jome->ijme",
L, scores, scores
)
return fisher