quantum-mind 2.0.0__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.
- quantum_mind/__init__.py +25 -0
- quantum_mind/_optional.py +14 -0
- quantum_mind/applications/README.md +19 -0
- quantum_mind/applications/__init__.py +1 -0
- quantum_mind/applications/calibration.py +477 -0
- quantum_mind/applications/fusion.py +259 -0
- quantum_mind/applications/handover.py +197 -0
- quantum_mind/applications/intent.py +196 -0
- quantum_mind/applications/orchestration.py +381 -0
- quantum_mind/applications/personalisation.py +242 -0
- quantum_mind/applications/questioning.py +339 -0
- quantum_mind/applications/robotics.py +428 -0
- quantum_mind/circuits/README.md +50 -0
- quantum_mind/circuits/__init__.py +5 -0
- quantum_mind/circuits/backends.py +198 -0
- quantum_mind/circuits/builders.py +414 -0
- quantum_mind/core/__init__.py +4 -0
- quantum_mind/core/fit.py +370 -0
- quantum_mind/core/linalg.py +341 -0
- quantum_mind/core/model.py +283 -0
- quantum_mind/data/__init__.py +58 -0
- quantum_mind/envs/__init__.py +14 -0
- quantum_mind/envs/hri.py +329 -0
- quantum_mind/families/__init__.py +8 -0
- quantum_mind/families/concepts/README.md +20 -0
- quantum_mind/families/concepts/__init__.py +3 -0
- quantum_mind/families/concepts/models.py +148 -0
- quantum_mind/families/conjunction/README.md +58 -0
- quantum_mind/families/conjunction/__init__.py +3 -0
- quantum_mind/families/conjunction/models.py +182 -0
- quantum_mind/families/contextuality/README.md +30 -0
- quantum_mind/families/contextuality/__init__.py +2 -0
- quantum_mind/families/contextuality/models.py +129 -0
- quantum_mind/families/decision/README.md +42 -0
- quantum_mind/families/decision/__init__.py +2 -0
- quantum_mind/families/decision/models.py +171 -0
- quantum_mind/families/dynamics/README.md +51 -0
- quantum_mind/families/dynamics/__init__.py +3 -0
- quantum_mind/families/dynamics/models.py +474 -0
- quantum_mind/families/game_theory/README.md +24 -0
- quantum_mind/families/game_theory/__init__.py +2 -0
- quantum_mind/families/game_theory/models.py +225 -0
- quantum_mind/families/interference/README.md +59 -0
- quantum_mind/families/interference/__init__.py +3 -0
- quantum_mind/families/interference/models.py +129 -0
- quantum_mind/families/memory/README.md +18 -0
- quantum_mind/families/memory/__init__.py +2 -0
- quantum_mind/families/memory/models.py +132 -0
- quantum_mind/families/order_effects/README.md +73 -0
- quantum_mind/families/order_effects/__init__.py +3 -0
- quantum_mind/families/order_effects/models.py +362 -0
- quantum_mind/families/perception/README.md +23 -0
- quantum_mind/families/perception/__init__.py +3 -0
- quantum_mind/families/perception/models.py +220 -0
- quantum_mind/families/qlbn/README.md +56 -0
- quantum_mind/families/qlbn/__init__.py +3 -0
- quantum_mind/families/qlbn/models.py +273 -0
- quantum_mind/families/similarity/README.md +35 -0
- quantum_mind/families/similarity/__init__.py +2 -0
- quantum_mind/families/similarity/models.py +200 -0
- quantum_mind/inspired/README.md +38 -0
- quantum_mind/inspired/__init__.py +27 -0
- quantum_mind/inspired/exploration.py +347 -0
- quantum_mind/inspired/optimisers.py +300 -0
- quantum_mind/inspired/rl.py +306 -0
- quantum_mind/inspired/tensor.py +299 -0
- quantum_mind/inspired/tensor_layers.py +270 -0
- quantum_mind/inspired/text.py +233 -0
- quantum_mind/problems/README.md +20 -0
- quantum_mind/problems/__init__.py +288 -0
- quantum_mind/quantum/README.md +62 -0
- quantum_mind/quantum/__init__.py +41 -0
- quantum_mind/quantum/algorithms.py +493 -0
- quantum_mind/quantum/ansatz.py +208 -0
- quantum_mind/quantum/classifiers.py +692 -0
- quantum_mind/quantum/estimation.py +224 -0
- quantum_mind/quantum/fourier.py +145 -0
- quantum_mind/quantum/policy.py +210 -0
- quantum_mind/quantum/quanvolution.py +173 -0
- quantum_mind/quantum/statevector.py +566 -0
- quantum_mind/quantum/walks.py +170 -0
- quantum_mind/viz/README.md +44 -0
- quantum_mind/viz/__init__.py +32 -0
- quantum_mind/viz/interactive.py +334 -0
- quantum_mind/viz/mpl.py +350 -0
- quantum_mind/viz/states.py +447 -0
- quantum_mind/viz/themes.py +30 -0
- quantum_mind-2.0.0.dist-info/METADATA +600 -0
- quantum_mind-2.0.0.dist-info/RECORD +93 -0
- quantum_mind-2.0.0.dist-info/WHEEL +5 -0
- quantum_mind-2.0.0.dist-info/licenses/LICENSE +201 -0
- quantum_mind-2.0.0.dist-info/licenses/NOTICE +5 -0
- quantum_mind-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
r"""Multimodal fusion for robots: from detector scores, speech and gaze to one belief over intents.
|
|
2
|
+
|
|
3
|
+
A request such as "bring me that cup" is resolved from several sources: an object detector's scores,
|
|
4
|
+
the speech recogniser's n-best list, where the person looks and where they point. This module turns
|
|
5
|
+
each source into a *likelihood over the same hypotheses* and offers three ways to combine them:
|
|
6
|
+
|
|
7
|
+
* :func:`bayes_fusion`: independent cues multiply (order-free);
|
|
8
|
+
* :func:`dempster_shafer_fusion`: Dempster's rule with discounted sources, which keeps an explicit
|
|
9
|
+
"don't know" mass when sources conflict;
|
|
10
|
+
* :class:`~quantum_mind.applications.intent.QuantumIntentResolver`: quantum-like fusion in which incompatible
|
|
11
|
+
cues are applied as rotated Kraus operators, so cue order matters (it reduces to Bayes when every
|
|
12
|
+
incompatibility angle is zero); :func:`fit_incompatibility` estimates the angles from logged
|
|
13
|
+
choices, and :func:`compare_fusion` compares the three rules on held-out data.
|
|
14
|
+
|
|
15
|
+
Adapters: :func:`detector_likelihood`, :func:`asr_likelihood`, :func:`direction_likelihood` (gaze or
|
|
16
|
+
pointing).
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> import numpy as np
|
|
21
|
+
>>> from quantum_mind.applications.fusion import detector_likelihood, direction_likelihood, bayes_fusion
|
|
22
|
+
>>> objects = ['red cup', 'blue cup', 'bowl']
|
|
23
|
+
>>> vision = detector_likelihood({'red cup': 0.6, 'blue cup': 0.5, 'bowl': 0.2}, objects)
|
|
24
|
+
>>> gaze = direction_likelihood([1, 0, 0], {'red cup': [0.9, 0.1, 0], 'blue cup': [0, 1, 0],
|
|
25
|
+
... 'bowl': [-1, 0, 0]}, objects, kappa=4)
|
|
26
|
+
>>> post = bayes_fusion([vision, gaze])
|
|
27
|
+
>>> objects[int(np.argmax(post))]
|
|
28
|
+
'red cup'
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
from scipy.optimize import minimize
|
|
34
|
+
|
|
35
|
+
__all__ = ['detector_likelihood', 'asr_likelihood', 'direction_likelihood', 'bayes_fusion',
|
|
36
|
+
'dempster_shafer_fusion', 'fit_incompatibility', 'compare_fusion']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _norm(v, floor):
|
|
40
|
+
"""Floor and normalise a non-negative vector."""
|
|
41
|
+
v = np.maximum(np.asarray(v, float), 0) + floor
|
|
42
|
+
return v / v.sum()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def detector_likelihood(scores, hypotheses, temperature=1.0, floor=1e-3):
|
|
46
|
+
"""Likelihood over hypotheses from an object detector's scores.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
scores : dict or array_like
|
|
51
|
+
``{hypothesis: score}`` (missing hypotheses score 0) or scores in the order of ``hypotheses``.
|
|
52
|
+
Scores are confidences in [0, 1] or logits.
|
|
53
|
+
hypotheses : sequence
|
|
54
|
+
Hypotheses (object or intent names).
|
|
55
|
+
temperature : float, optional
|
|
56
|
+
Softens (> 1) or sharpens (< 1) the scores before normalising.
|
|
57
|
+
floor : float, optional
|
|
58
|
+
Added to every entry so that no hypothesis is ruled out by one cue.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
numpy.ndarray
|
|
63
|
+
Normalised likelihood, one entry per hypothesis.
|
|
64
|
+
"""
|
|
65
|
+
s = np.array([scores.get(h, 0.0) for h in hypotheses], float) if isinstance(scores, dict) else np.asarray(scores, float)
|
|
66
|
+
if np.all((s >= 0) & (s <= 1)): # confidences: power-sharpen or soften
|
|
67
|
+
return _norm(s ** (1.0 / temperature), floor)
|
|
68
|
+
z = s / temperature # logits: softmax
|
|
69
|
+
return _norm(np.exp(z - z.max()), floor)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def asr_likelihood(nbest, keywords, hypotheses, floor=0.02):
|
|
73
|
+
"""Likelihood over hypotheses from a speech recogniser's n-best list.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
nbest : sequence of tuple
|
|
78
|
+
``[(transcript, confidence), ...]``.
|
|
79
|
+
keywords : dict
|
|
80
|
+
``{hypothesis: [words that refer to it]}``.
|
|
81
|
+
hypotheses : sequence
|
|
82
|
+
floor : float, optional
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
numpy.ndarray
|
|
87
|
+
Each transcript's confidence is shared among the hypotheses it mentions (or spread over all
|
|
88
|
+
hypotheses when it mentions none).
|
|
89
|
+
"""
|
|
90
|
+
L = np.zeros(len(hypotheses))
|
|
91
|
+
for text, conf in nbest:
|
|
92
|
+
words = set(text.lower().replace(',', ' ').split())
|
|
93
|
+
hit = np.array([any(k.lower() in words or k.lower() in text.lower() for k in keywords.get(h, [])) for h in hypotheses])
|
|
94
|
+
L += conf * (hit / hit.sum() if hit.any() else np.full(len(hypotheses), 1 / len(hypotheses)))
|
|
95
|
+
return _norm(L, floor)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def direction_likelihood(direction, targets, hypotheses, kappa=5.0, floor=1e-3):
|
|
99
|
+
"""Likelihood over hypotheses from a gaze or pointing direction (von Mises-Fisher).
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
direction : array_like
|
|
104
|
+
Direction vector (2D or 3D) from the person's head or hand.
|
|
105
|
+
targets : dict
|
|
106
|
+
``{hypothesis: direction to that object}`` from the same origin.
|
|
107
|
+
hypotheses : sequence
|
|
108
|
+
kappa : float, optional
|
|
109
|
+
Concentration: larger values trust the direction more.
|
|
110
|
+
floor : float, optional
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
numpy.ndarray
|
|
115
|
+
:math:`\\propto \\exp(\\kappa \\cos \\angle(d, t_h))`.
|
|
116
|
+
"""
|
|
117
|
+
d = np.asarray(direction, float)
|
|
118
|
+
d = d / np.linalg.norm(d)
|
|
119
|
+
cos = []
|
|
120
|
+
for h in hypotheses:
|
|
121
|
+
t = np.asarray(targets[h], float)
|
|
122
|
+
cos.append(float(d @ t / np.linalg.norm(t)))
|
|
123
|
+
return _norm(np.exp(kappa * (np.array(cos) - 1)), floor)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def bayes_fusion(likelihoods, prior=None):
|
|
127
|
+
"""Posterior of independent cues: prior times the product of the likelihoods.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
likelihoods : sequence of array_like
|
|
132
|
+
One likelihood vector per cue.
|
|
133
|
+
prior : array_like, optional
|
|
134
|
+
Prior over hypotheses (uniform by default).
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
numpy.ndarray
|
|
139
|
+
"""
|
|
140
|
+
L = np.asarray(likelihoods, float)
|
|
141
|
+
post = (np.full(L.shape[1], 1 / L.shape[1]) if prior is None else np.asarray(prior, float)) * np.prod(L, axis=0)
|
|
142
|
+
return post / post.sum()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def dempster_shafer_fusion(likelihoods, reliabilities=None):
|
|
146
|
+
"""Dempster's rule for singleton masses with discounting.
|
|
147
|
+
|
|
148
|
+
Each cue becomes a mass function on the single hypotheses, discounted by its reliability r: a
|
|
149
|
+
fraction 1 - r of its mass goes to "any hypothesis" (ignorance). Conflict between cues is
|
|
150
|
+
renormalised away by Dempster's rule.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
likelihoods : sequence of array_like
|
|
155
|
+
One normalised likelihood per cue.
|
|
156
|
+
reliabilities : sequence of float, optional
|
|
157
|
+
Reliability of each cue in [0, 1] (default 0.9).
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
belief : numpy.ndarray
|
|
162
|
+
Pignistic probability over hypotheses (ignorance shared equally).
|
|
163
|
+
ignorance : float
|
|
164
|
+
Remaining mass on "any hypothesis".
|
|
165
|
+
"""
|
|
166
|
+
L = np.asarray(likelihoods, float)
|
|
167
|
+
K = L.shape[1]
|
|
168
|
+
r = np.full(len(L), 0.9) if reliabilities is None else np.asarray(reliabilities, float)
|
|
169
|
+
m = np.zeros(K)
|
|
170
|
+
omega = 1.0 # mass on the whole frame (start: total ignorance)
|
|
171
|
+
for li, ri in zip(L, r):
|
|
172
|
+
mi = ri * li / li.sum()
|
|
173
|
+
wi = 1 - ri
|
|
174
|
+
# combine (m, omega) with (mi, wi) on singletons plus the frame
|
|
175
|
+
new = m * mi + m * wi + omega * mi
|
|
176
|
+
new_omega = omega * wi
|
|
177
|
+
z = new.sum() + new_omega # 1 - conflict
|
|
178
|
+
m, omega = new / z, new_omega / z
|
|
179
|
+
return m + omega / K, float(omega)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def fit_incompatibility(resolver_cls, intents, cue_likelihoods, trials, prior=None, bounds=(0.0, np.pi / 2)):
|
|
183
|
+
"""Fit the incompatibility angle of each cue of a quantum-like resolver from logged choices.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
resolver_cls : type
|
|
188
|
+
:class:`~quantum_mind.applications.intent.QuantumIntentResolver`.
|
|
189
|
+
intents : sequence of str
|
|
190
|
+
Intent names.
|
|
191
|
+
cue_likelihoods : dict
|
|
192
|
+
``{cue name: likelihood over intents}``.
|
|
193
|
+
trials : sequence of tuple
|
|
194
|
+
``[(cue order, chosen intent), ...]``: the order in which cues arrived and what the person
|
|
195
|
+
actually wanted (or chose).
|
|
196
|
+
prior : array_like, optional
|
|
197
|
+
Prior over intents.
|
|
198
|
+
bounds : tuple, optional
|
|
199
|
+
Range of the angles.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
resolver : QuantumIntentResolver
|
|
204
|
+
Resolver with the fitted angles.
|
|
205
|
+
angles : dict
|
|
206
|
+
``{cue name: fitted angle}``.
|
|
207
|
+
"""
|
|
208
|
+
names = list(cue_likelihoods)
|
|
209
|
+
idx = {h: i for i, h in enumerate(intents)}
|
|
210
|
+
|
|
211
|
+
def build(theta):
|
|
212
|
+
r = resolver_cls(intents, prior)
|
|
213
|
+
for n, t in zip(names, theta):
|
|
214
|
+
r.add_cue(n, cue_likelihoods[n], t)
|
|
215
|
+
return r
|
|
216
|
+
|
|
217
|
+
def nll(theta):
|
|
218
|
+
r = build(theta)
|
|
219
|
+
return -sum(np.log(max(r.posterior(order)[idx[c]], 1e-12)) for order, c in trials)
|
|
220
|
+
|
|
221
|
+
best = min((minimize(nll, np.full(len(names), x0), method='L-BFGS-B', bounds=[bounds] * len(names))
|
|
222
|
+
for x0 in (0.05, 0.4, 0.9)), key=lambda r: r.fun)
|
|
223
|
+
return build(best.x), dict(zip(names, (float(v) for v in best.x)))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def compare_fusion(intents, cue_likelihoods, train, test, prior=None):
|
|
227
|
+
"""Compare Bayesian, Dempster-Shafer and fitted quantum-like fusion on held-out trials.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
intents : sequence of str
|
|
232
|
+
cue_likelihoods : dict
|
|
233
|
+
``{cue name: likelihood over intents}``.
|
|
234
|
+
train, test : sequence of tuple
|
|
235
|
+
``[(cue order, chosen intent), ...]``; the quantum-like angles are fitted on ``train``.
|
|
236
|
+
prior : array_like, optional
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
dict
|
|
241
|
+
``{rule: {'log_loss': ..., 'accuracy': ...}}`` on ``test`` (lower log loss is better), plus
|
|
242
|
+
``'angles'``.
|
|
243
|
+
"""
|
|
244
|
+
from .intent import QuantumIntentResolver
|
|
245
|
+
idx = {h: i for i, h in enumerate(intents)}
|
|
246
|
+
q, angles = fit_incompatibility(QuantumIntentResolver, intents, cue_likelihoods, train, prior)
|
|
247
|
+
rules = {
|
|
248
|
+
'bayes': lambda order: bayes_fusion([cue_likelihoods[c] for c in order], prior),
|
|
249
|
+
'dempster_shafer': lambda order: dempster_shafer_fusion([cue_likelihoods[c] for c in order])[0],
|
|
250
|
+
'quantum_like': q.posterior,
|
|
251
|
+
}
|
|
252
|
+
out = {}
|
|
253
|
+
for name, rule in rules.items():
|
|
254
|
+
P = [rule(order) for order, _ in test]
|
|
255
|
+
y = [idx[c] for _, c in test]
|
|
256
|
+
out[name] = {'log_loss': float(-np.mean([np.log(max(p[i], 1e-12)) for p, i in zip(P, y)])),
|
|
257
|
+
'accuracy': float(np.mean([int(np.argmax(p)) == i for p, i in zip(P, y)]))}
|
|
258
|
+
out['angles'] = angles
|
|
259
|
+
return out
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
r"""Trust-aware hand-over: a robot policy built on the trust qubit.
|
|
2
|
+
|
|
3
|
+
The robot hands objects to a person. Whether the person takes the object smoothly depends on how much
|
|
4
|
+
they trust the robot at that moment, and trust changes with every success and failure
|
|
5
|
+
(:class:`~quantum_mind.families.dynamics.OpenSystemBelief`). The robot can also *ask* about trust, which tells it
|
|
6
|
+
where trust stands but, in the quantum-like model, also changes it (the question collapses the belief).
|
|
7
|
+
|
|
8
|
+
At each step the policy compares the expected cost of four actions:
|
|
9
|
+
|
|
10
|
+
================= ==========================================================================
|
|
11
|
+
``handover`` normal speed; fails with probability :math:`1 - p_{\text{trust}}`
|
|
12
|
+
``slow_handover`` slower and more legible; costs ``slow_cost`` but halves the failure risk
|
|
13
|
+
``ask`` costs ``ask_cost``; the answer collapses the belief, then the better of the
|
|
14
|
+
two hand-overs is chosen for the next step (one-step look-ahead)
|
|
15
|
+
``wait`` costs ``wait_cost``; the belief keeps dephasing toward its mixed state
|
|
16
|
+
================= ==========================================================================
|
|
17
|
+
|
|
18
|
+
The same policy can run on the classical :class:`~quantum_mind.families.dynamics.MarkovBelief`, where asking
|
|
19
|
+
only reads trust; comparing the two shows when the order-aware model changes the robot's behaviour.
|
|
20
|
+
|
|
21
|
+
Examples
|
|
22
|
+
--------
|
|
23
|
+
>>> from quantum_mind.families.dynamics import OpenSystemBelief
|
|
24
|
+
>>> from quantum_mind.applications.handover import TrustAwareHandover
|
|
25
|
+
>>> policy = TrustAwareHandover(OpenSystemBelief(phi0=0.6, a_pos=0.8, a_neg=1.2, gamma=0.3))
|
|
26
|
+
>>> policy.decide()['action'] # high trust: hand over normally
|
|
27
|
+
'handover'
|
|
28
|
+
>>> policy.observe(0); policy.observe(0) # two failures
|
|
29
|
+
>>> policy.decide()['action'] != 'handover'
|
|
30
|
+
True
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
__all__ = ['TrustAwareHandover', 'simulate_handover_session']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TrustAwareHandover:
|
|
40
|
+
"""Hand-over policy on a trust belief.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
model : OpenSystemBelief or MarkovBelief
|
|
45
|
+
Trust model, with its parameters (fitted to the person or the population).
|
|
46
|
+
fail_cost : float, optional
|
|
47
|
+
Cost of a failed hand-over (dropped object, startled person).
|
|
48
|
+
slow_cost : float, optional
|
|
49
|
+
Extra cost of a slow hand-over.
|
|
50
|
+
ask_cost : float, optional
|
|
51
|
+
Cost of asking "do you trust me to hand this over?".
|
|
52
|
+
wait_cost : float, optional
|
|
53
|
+
Cost of waiting one step.
|
|
54
|
+
slow_factor : float, optional
|
|
55
|
+
Failure probability of a slow hand-over relative to a normal one.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
rho : numpy.ndarray
|
|
60
|
+
Current trust belief as a 2 x 2 density matrix (open-system model), or ``None``.
|
|
61
|
+
p : float
|
|
62
|
+
Current P(trust) (Markov model), or ``None``.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, model, fail_cost=10.0, slow_cost=1.0, ask_cost=0.5, wait_cost=0.3, slow_factor=0.5):
|
|
66
|
+
self.model = model
|
|
67
|
+
self.fail_cost, self.slow_cost, self.ask_cost = fail_cost, slow_cost, ask_cost
|
|
68
|
+
self.wait_cost, self.slow_factor = wait_cost, slow_factor
|
|
69
|
+
self.quantum = hasattr(model, 'phi0') and hasattr(model, 'gamma')
|
|
70
|
+
if self.quantum:
|
|
71
|
+
v = np.array([np.cos(model.phi0 / 2), np.sin(model.phi0 / 2)])
|
|
72
|
+
self.rho = np.outer(v, v)
|
|
73
|
+
self.p = None
|
|
74
|
+
else:
|
|
75
|
+
self.rho = None
|
|
76
|
+
self.p = float(model.p0)
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------ belief
|
|
79
|
+
@property
|
|
80
|
+
def p_trust(self):
|
|
81
|
+
"""float: current probability that the person trusts the robot (says "yes")."""
|
|
82
|
+
return float(np.clip(self.rho[0, 0], 0, 1)) if self.quantum else self.p
|
|
83
|
+
|
|
84
|
+
def _rotate(self, rho, event):
|
|
85
|
+
"""Open-system update after one event: rotation, then dephasing."""
|
|
86
|
+
a = -self.model.a_pos if event else self.model.a_neg
|
|
87
|
+
c, s = np.cos(a / 2), np.sin(a / 2)
|
|
88
|
+
U = np.array([[c, -s], [s, c]])
|
|
89
|
+
r = U @ rho @ U.T
|
|
90
|
+
f = 1 - self.model.gamma
|
|
91
|
+
return np.array([[r[0, 0], f * r[0, 1]], [f * r[1, 0], r[1, 1]]])
|
|
92
|
+
|
|
93
|
+
def observe(self, event):
|
|
94
|
+
"""Update the belief after a hand-over outcome.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
event : int
|
|
99
|
+
1 for a success, 0 for a failure.
|
|
100
|
+
"""
|
|
101
|
+
if self.quantum:
|
|
102
|
+
self.rho = self._rotate(self.rho, event)
|
|
103
|
+
else:
|
|
104
|
+
self.p = self.model.step(self.p, event)
|
|
105
|
+
|
|
106
|
+
def answer(self, yes):
|
|
107
|
+
"""Update the belief after the person answers the trust question.
|
|
108
|
+
|
|
109
|
+
In the open-system model the answer collapses the belief onto "trust" or "no trust"; in the
|
|
110
|
+
Markov model it sets P(trust) to 1 or 0.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
yes : bool or int
|
|
115
|
+
"""
|
|
116
|
+
if self.quantum:
|
|
117
|
+
self.rho = np.diag([1.0, 0.0]) if yes else np.diag([0.0, 1.0])
|
|
118
|
+
else:
|
|
119
|
+
self.p = 1.0 if yes else 0.0
|
|
120
|
+
|
|
121
|
+
def wait(self):
|
|
122
|
+
"""Let one step pass without a hand-over (dephasing only, open-system model)."""
|
|
123
|
+
if self.quantum:
|
|
124
|
+
f = 1 - self.model.gamma
|
|
125
|
+
self.rho = np.array([[self.rho[0, 0], f * self.rho[0, 1]], [f * self.rho[1, 0], self.rho[1, 1]]])
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------ decision
|
|
128
|
+
def _handover_costs(self, p):
|
|
129
|
+
"""Expected cost of a normal and a slow hand-over at trust probability p."""
|
|
130
|
+
return self.fail_cost * (1 - p), self.slow_cost + self.fail_cost * (1 - p) * self.slow_factor
|
|
131
|
+
|
|
132
|
+
def decide(self):
|
|
133
|
+
"""Expected cost of each action and the best one.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
dict
|
|
138
|
+
``action`` (the cheapest), ``costs`` (``{action: expected cost}``) and ``p_trust``.
|
|
139
|
+
"""
|
|
140
|
+
p = self.p_trust
|
|
141
|
+
normal, slow = self._handover_costs(p)
|
|
142
|
+
# asking: pay ask_cost, then hand over optimally given the collapsed answer
|
|
143
|
+
after_yes, after_no = min(self._handover_costs(1.0)), min(self._handover_costs(0.0))
|
|
144
|
+
ask = self.ask_cost + p * after_yes + (1 - p) * after_no
|
|
145
|
+
costs = {'handover': normal, 'slow_handover': slow, 'ask': ask, 'wait': self.wait_cost + min(normal, slow)}
|
|
146
|
+
return {'action': min(costs, key=costs.get), 'costs': costs, 'p_trust': p}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def simulate_handover_session(policy, person, events_rng, n_steps=20):
|
|
150
|
+
"""Run a policy against a simulated person whose trust follows a model.
|
|
151
|
+
|
|
152
|
+
The person's real trust (a separate copy of the model) determines whether each hand-over succeeds
|
|
153
|
+
and how the trust question is answered; the policy only sees outcomes and answers.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
policy : TrustAwareHandover
|
|
158
|
+
The robot's policy (its own belief).
|
|
159
|
+
person : TrustAwareHandover
|
|
160
|
+
The simulated person's true trust state (only its belief is used).
|
|
161
|
+
events_rng : numpy.random.Generator
|
|
162
|
+
n_steps : int, optional
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
dict
|
|
167
|
+
``cost`` (total), ``failures``, ``asks`` and ``actions`` (list).
|
|
168
|
+
"""
|
|
169
|
+
total = 0.0
|
|
170
|
+
failures = asks = 0
|
|
171
|
+
actions = []
|
|
172
|
+
for _ in range(n_steps):
|
|
173
|
+
d = policy.decide()
|
|
174
|
+
a = d['action']
|
|
175
|
+
actions.append(a)
|
|
176
|
+
if a == 'ask':
|
|
177
|
+
total += policy.ask_cost
|
|
178
|
+
yes = events_rng.random() < person.p_trust
|
|
179
|
+
person.answer(yes) # asking changes the person's trust too
|
|
180
|
+
policy.answer(yes)
|
|
181
|
+
asks += 1
|
|
182
|
+
continue
|
|
183
|
+
if a == 'wait':
|
|
184
|
+
total += policy.wait_cost
|
|
185
|
+
person.wait()
|
|
186
|
+
policy.wait()
|
|
187
|
+
continue
|
|
188
|
+
p_fail = 1 - person.p_trust
|
|
189
|
+
if a == 'slow_handover':
|
|
190
|
+
total += policy.slow_cost
|
|
191
|
+
p_fail *= policy.slow_factor
|
|
192
|
+
ok = events_rng.random() >= p_fail
|
|
193
|
+
total += 0 if ok else policy.fail_cost
|
|
194
|
+
failures += int(not ok)
|
|
195
|
+
person.observe(int(ok))
|
|
196
|
+
policy.observe(int(ok))
|
|
197
|
+
return {'cost': total, 'failures': failures, 'asks': asks, 'actions': actions}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
r"""Intent resolution from ambiguous commands and context cues (robots, assistants, interfaces).
|
|
2
|
+
|
|
3
|
+
A user's intent is one of K options ("fetch the red cup", "fetch the blue cup", ...). Each context cue
|
|
4
|
+
(a word, a gesture, a gaze direction, the time of day) carries a likelihood over intents. Classical
|
|
5
|
+
Bayesian updating multiplies the likelihoods, so the order in which cues arrive does not matter.
|
|
6
|
+
|
|
7
|
+
:class:`QuantumIntentResolver` represents belief as amplitudes :math:`\psi = \sqrt{\text{prior}}` and
|
|
8
|
+
applies each cue c as a Kraus operator
|
|
9
|
+
|
|
10
|
+
.. math:: K_c = R(\theta_c)\,\mathrm{diag}\bigl(\sqrt{L_c}\bigr)\,R(\theta_c)^T,
|
|
11
|
+
|
|
12
|
+
where :math:`R(\theta_c)` rotates the intent basis by the cue's incompatibility angle
|
|
13
|
+
:math:`\theta_c`, in the plane of the cue's two most likely intents. With every
|
|
14
|
+
:math:`\theta_c = 0` the posterior is exactly Bayes' rule; :math:`\theta_c \neq 0` makes cues
|
|
15
|
+
incompatible, so their order matters, as in question-order effects. The angles can be fitted to
|
|
16
|
+
observed choices.
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> from quantum_mind.applications.intent import QuantumIntentResolver
|
|
21
|
+
>>> r = QuantumIntentResolver(['red cup', 'blue cup', 'bowl'])
|
|
22
|
+
>>> _ = r.add_cue('points left', [0.70, 0.20, 0.10], theta=0.5)
|
|
23
|
+
>>> _ = r.add_cue('looks right', [0.25, 0.60, 0.15], theta=0.5)
|
|
24
|
+
>>> r.posterior(['points left', 'looks right']).round(3)
|
|
25
|
+
array([0.482, 0.498, 0.02 ])
|
|
26
|
+
>>> round(r.order_effect('points left', 'looks right'), 3)
|
|
27
|
+
0.079
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
__all__ = ['QuantumIntentResolver', 'BayesIntentResolver']
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BayesIntentResolver:
|
|
37
|
+
"""Classical baseline: posterior proportional to the prior times the cue likelihoods.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
intents : sequence of str
|
|
42
|
+
Names of the K possible intents.
|
|
43
|
+
prior : array_like, optional
|
|
44
|
+
Prior over intents (normalised); uniform by default.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
cues : dict
|
|
49
|
+
Registered cues, by name.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, intents, prior=None):
|
|
53
|
+
self.intents = list(intents)
|
|
54
|
+
K = len(self.intents)
|
|
55
|
+
self.prior = np.full(K, 1 / K) if prior is None else _prob(prior, K)
|
|
56
|
+
self.cues = {}
|
|
57
|
+
|
|
58
|
+
def add_cue(self, name, likelihood, theta=0.0):
|
|
59
|
+
"""Register a cue.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
name : str
|
|
64
|
+
Cue name.
|
|
65
|
+
likelihood : array_like
|
|
66
|
+
Non-negative likelihood of the cue under each intent.
|
|
67
|
+
theta : float, optional
|
|
68
|
+
Ignored here (classical cues commute); used by :class:`QuantumIntentResolver`.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
BayesIntentResolver
|
|
73
|
+
``self``, so calls can be chained.
|
|
74
|
+
"""
|
|
75
|
+
self.cues[name] = _prob(likelihood, len(self.intents), normalise=False)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def posterior(self, cues):
|
|
79
|
+
"""Intent probabilities after a sequence of cues.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
cues : sequence of str
|
|
84
|
+
Names of registered cues.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
numpy.ndarray
|
|
89
|
+
Posterior over intents (independent of the order of ``cues``).
|
|
90
|
+
"""
|
|
91
|
+
p = self.prior.copy()
|
|
92
|
+
for c in cues:
|
|
93
|
+
p = p * self.cues[c]
|
|
94
|
+
p = p / p.sum()
|
|
95
|
+
return p
|
|
96
|
+
|
|
97
|
+
def order_effect(self, a, b):
|
|
98
|
+
"""Size of the order effect between two cues.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
a, b : str
|
|
103
|
+
Cue names.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
float
|
|
108
|
+
Total variation distance between the posteriors for orders (a, b) and (b, a); always 0 for
|
|
109
|
+
the Bayesian resolver.
|
|
110
|
+
"""
|
|
111
|
+
return 0.5 * float(np.abs(self.posterior([a, b]) - self.posterior([b, a])).sum())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class QuantumIntentResolver(BayesIntentResolver):
|
|
115
|
+
"""Quantum-like intent resolver; it reduces to :class:`BayesIntentResolver` when every angle is 0.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
intents : sequence of str
|
|
120
|
+
Names of the K possible intents.
|
|
121
|
+
prior : array_like, optional
|
|
122
|
+
Prior over intents; the initial amplitudes are its square roots.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def add_cue(self, name, likelihood, theta=0.0):
|
|
126
|
+
"""Register a cue as a Kraus operator.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
name : str
|
|
131
|
+
Cue name.
|
|
132
|
+
likelihood : array_like
|
|
133
|
+
Non-negative likelihood of the cue under each intent.
|
|
134
|
+
theta : float, optional
|
|
135
|
+
Incompatibility angle in radians (0: the cue commutes with every other cue).
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
QuantumIntentResolver
|
|
140
|
+
``self``.
|
|
141
|
+
"""
|
|
142
|
+
L = _prob(likelihood, len(self.intents), normalise=False)
|
|
143
|
+
i, j = np.argsort(L)[::-1][:2] # rotate in the plane of the two leading intents
|
|
144
|
+
R = np.eye(len(L))
|
|
145
|
+
c, s = np.cos(theta), np.sin(theta)
|
|
146
|
+
R[[i, i, j, j], [i, j, i, j]] = [c, -s, s, c]
|
|
147
|
+
self.cues[name] = R @ np.diag(np.sqrt(L)) @ R.T
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def posterior(self, cues):
|
|
151
|
+
"""Intent probabilities :math:`|\\psi|^2` after applying the cues in order.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
cues : sequence of str
|
|
156
|
+
Names of registered cues, in the order they arrived.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
numpy.ndarray
|
|
161
|
+
"""
|
|
162
|
+
psi = np.sqrt(self.prior)
|
|
163
|
+
for c in cues:
|
|
164
|
+
psi = self.cues[c] @ psi
|
|
165
|
+
psi = psi / np.linalg.norm(psi)
|
|
166
|
+
return psi ** 2
|
|
167
|
+
|
|
168
|
+
def resolve(self, cues):
|
|
169
|
+
"""Most likely intent after the cues.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
cues : sequence of str
|
|
174
|
+
Names of registered cues, in order.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
intent : str
|
|
179
|
+
Most likely intent.
|
|
180
|
+
probability : float
|
|
181
|
+
Its posterior probability.
|
|
182
|
+
entropy : float
|
|
183
|
+
Entropy of the posterior in bits (high values suggest asking for clarification).
|
|
184
|
+
"""
|
|
185
|
+
p = self.posterior(cues)
|
|
186
|
+
k = int(np.argmax(p))
|
|
187
|
+
H = float(-np.sum(p[p > 0] * np.log2(p[p > 0])))
|
|
188
|
+
return self.intents[k], float(p[k]), H
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _prob(v, K, normalise=True):
|
|
192
|
+
"""Validate K non-negative weights and optionally normalise them."""
|
|
193
|
+
v = np.asarray(v, float)
|
|
194
|
+
if v.shape != (K,) or np.any(v < 0) or v.sum() <= 0:
|
|
195
|
+
raise ValueError('expected %d non-negative weights' % K)
|
|
196
|
+
return v / v.sum() if normalise else v
|