1 | from collections import defaultdict
|
---|
2 |
|
---|
3 | from geniusweb.issuevalue.Bid import Bid
|
---|
4 | from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
|
---|
5 | from geniusweb.issuevalue.Domain import Domain
|
---|
6 | from geniusweb.issuevalue.Value import Value
|
---|
7 |
|
---|
8 |
|
---|
9 | class OpponentModel:
|
---|
10 | def __init__(self, domain: Domain):
|
---|
11 | self.offers = []
|
---|
12 | self.domain = domain
|
---|
13 |
|
---|
14 | self.issue_estimators = {
|
---|
15 | i: IssueEstimator(v) for i, v in domain.getIssuesValues().items()
|
---|
16 | }
|
---|
17 |
|
---|
18 | def update(self, bid: Bid, time: float):
|
---|
19 | # keep track of all bids received
|
---|
20 | self.offers.append(bid)
|
---|
21 |
|
---|
22 | # update all issue estimators with the value that is offered for that issue
|
---|
23 | for issue_id, issue_estimator in self.issue_estimators.items():
|
---|
24 | issue_estimator.update(bid.getValue(issue_id), time)
|
---|
25 |
|
---|
26 | def get_predicted_utility(self, bid: Bid):
|
---|
27 | if len(self.offers) == 0 or bid is None:
|
---|
28 | return 0
|
---|
29 |
|
---|
30 | # initiate
|
---|
31 | total_issue_weight = 0.0
|
---|
32 | value_utilities = []
|
---|
33 | value_counts = []
|
---|
34 | issue_weights = []
|
---|
35 |
|
---|
36 | for issue_id, issue_estimator in self.issue_estimators.items():
|
---|
37 | # get the value that is set for this issue in the bid
|
---|
38 | value: Value = bid.getValue(issue_id)
|
---|
39 |
|
---|
40 | # collect both the predicted weight for the issue and
|
---|
41 | # predicted utility of the value within this issue
|
---|
42 | value_utilities.append(issue_estimator.get_value_utility(value))
|
---|
43 | value_counts.append(issue_estimator.value_trackers[value].count)
|
---|
44 | issue_weights.append(issue_estimator.weight)
|
---|
45 |
|
---|
46 | total_issue_weight += issue_estimator.weight
|
---|
47 |
|
---|
48 | # normalise the issue weights such that the sum is 1.0
|
---|
49 | if total_issue_weight == 0.0:
|
---|
50 | issue_weights = [1 / len(issue_weights) for _ in issue_weights]
|
---|
51 | else:
|
---|
52 | issue_weights = [iw / total_issue_weight for iw in issue_weights]
|
---|
53 |
|
---|
54 | # calculate predicted utility by multiplying all value utilities with their issue weight
|
---|
55 | predicted_utility = sum(
|
---|
56 | [iw * vu for iw, vu in zip(issue_weights, value_utilities)]
|
---|
57 | )
|
---|
58 | prediction_uncertainty = sum(
|
---|
59 | [iw * (2.0**(-max(vc, 1))) for iw, vc in zip(issue_weights, value_counts)]
|
---|
60 | )
|
---|
61 |
|
---|
62 | return predicted_utility, prediction_uncertainty
|
---|
63 |
|
---|
64 | def get_issue_weights(self):
|
---|
65 | total = sum(issue_estimator.weight for issue, issue_estimator in self.issue_estimators.items())
|
---|
66 | issue_weights = {issue: issue_estimator.weight / total for issue, issue_estimator in self.issue_estimators.items()}
|
---|
67 | return issue_weights
|
---|
68 |
|
---|
69 | def get_value_utils(self, issue: str):
|
---|
70 | value_utils = {value: value_estimator.utility for value, value_estimator in self.issue_estimators[issue].value_trackers.items()}
|
---|
71 | return value_utils
|
---|
72 |
|
---|
73 |
|
---|
74 | class IssueEstimator:
|
---|
75 | def __init__(self, value_set: DiscreteValueSet):
|
---|
76 | if not isinstance(value_set, DiscreteValueSet):
|
---|
77 | raise TypeError(
|
---|
78 | "This issue estimator only supports issues with discrete values"
|
---|
79 | )
|
---|
80 |
|
---|
81 | self.bids_received = 0
|
---|
82 | self.total_adjusted_value_count = 0
|
---|
83 | self.max_value_count = 0
|
---|
84 | self.max_adjusted_value_count = 0
|
---|
85 | self.num_values = value_set.size()
|
---|
86 | self.value_trackers = defaultdict(ValueEstimator)
|
---|
87 | self.weight = 0
|
---|
88 |
|
---|
89 | def update(self, value: Value, time: float):
|
---|
90 | self.bids_received += 1
|
---|
91 |
|
---|
92 | # get the value tracker of the value that is offered
|
---|
93 | value_tracker = self.value_trackers[value]
|
---|
94 |
|
---|
95 | # register that this value was offered
|
---|
96 | update_amount = value_tracker.update(time)
|
---|
97 | self.total_adjusted_value_count += update_amount
|
---|
98 |
|
---|
99 | # update the count of the most common offered value
|
---|
100 | self.max_value_count = max([value_tracker.count, self.max_value_count])
|
---|
101 | self.max_adjusted_value_count = max([value_tracker.adjusted_count, self.max_adjusted_value_count])
|
---|
102 |
|
---|
103 | # update predicted issue weight
|
---|
104 | # the intuition here is that if the values of the receiverd offers spread out over all
|
---|
105 | # possible values, then this issue is likely not important to the opponent (weight == 0.0).
|
---|
106 | # If all received offers proposed the same value for this issue,
|
---|
107 | # then the predicted issue weight == 1.0
|
---|
108 | equal_shares = self.bids_received / self.num_values
|
---|
109 | adjusted_equal_shares = self.total_adjusted_value_count / self.num_values
|
---|
110 | self.old_weight = (self.max_value_count - equal_shares) / (
|
---|
111 | self.bids_received - equal_shares
|
---|
112 | )
|
---|
113 | self.weight = (self.max_adjusted_value_count - adjusted_equal_shares) / (
|
---|
114 | self.total_adjusted_value_count - adjusted_equal_shares
|
---|
115 | )
|
---|
116 |
|
---|
117 | # recalculate all value utilities
|
---|
118 | for value_tracker in self.value_trackers.values():
|
---|
119 | value_tracker.recalculate_utility(self.max_adjusted_value_count, self.weight)
|
---|
120 |
|
---|
121 | def get_value_utility(self, value: Value):
|
---|
122 | if value in self.value_trackers:
|
---|
123 | return self.value_trackers[value].utility
|
---|
124 |
|
---|
125 | return 0
|
---|
126 |
|
---|
127 |
|
---|
128 | class ValueEstimator:
|
---|
129 | def __init__(self):
|
---|
130 | self.count = 0
|
---|
131 | self.adjusted_count = 0
|
---|
132 | self.utility = 0
|
---|
133 |
|
---|
134 | def update(self, time):
|
---|
135 | self.count += 1
|
---|
136 | update_amount = (1.0 - 0.9 * time) / (self.count + 1.0)
|
---|
137 | # update_amount = 1 # TODO Consider Removing
|
---|
138 | self.adjusted_count += update_amount
|
---|
139 | return update_amount
|
---|
140 |
|
---|
141 | def recalculate_utility(self, max_adjusted_value_count: int, weight: float):
|
---|
142 | if weight < 1:
|
---|
143 | mod_value_count = ((self.adjusted_count + 1) ** (1 - weight)) - 1
|
---|
144 | mod_max_value_count = ((max_adjusted_value_count + 1) ** (1 - weight)) - 1
|
---|
145 |
|
---|
146 | self.utility = mod_value_count / mod_max_value_count
|
---|
147 | else:
|
---|
148 | self.utility = 1 if self.adjusted_count != 0 else 0
|
---|