[75] | 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 |
|
---|
| 19 | def update(self, bid: Bid):
|
---|
| 20 | # keep track of all bids received
|
---|
| 21 | self.offers.append(bid)
|
---|
| 22 |
|
---|
| 23 | # update all issue estimators with the value that is offered for that issue
|
---|
| 24 | for issue_id, issue_estimator in self.issue_estimators.items():
|
---|
| 25 | issue_estimator.update(bid.getValue(issue_id))
|
---|
| 26 |
|
---|
| 27 | def get_predicted_utility(self, bid: Bid):
|
---|
| 28 | if len(self.offers) == 0 or bid is None:
|
---|
| 29 | return 0
|
---|
| 30 |
|
---|
| 31 | # initiate
|
---|
| 32 | total_issue_weight = 0.0
|
---|
| 33 | value_utilities = []
|
---|
| 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 | issue_weights.append(issue_estimator.weight)
|
---|
| 44 |
|
---|
| 45 | total_issue_weight += issue_estimator.weight
|
---|
| 46 |
|
---|
| 47 | # normalise the issue weights such that the sum is 1.0
|
---|
| 48 | if total_issue_weight == 0.0:
|
---|
| 49 | issue_weights = [1 / len(issue_weights) for _ in issue_weights]
|
---|
| 50 | else:
|
---|
| 51 | issue_weights = [iw / total_issue_weight for iw in issue_weights]
|
---|
| 52 |
|
---|
| 53 | # calculate predicted utility by multiplying all value utilities with their issue weight
|
---|
| 54 | predicted_utility = sum(
|
---|
| 55 | [iw * vu for iw, vu in zip(issue_weights, value_utilities)]
|
---|
| 56 | )
|
---|
| 57 |
|
---|
| 58 | return predicted_utility
|
---|
| 59 |
|
---|
| 60 |
|
---|
| 61 | class IssueEstimator:
|
---|
| 62 | def __init__(self, value_set: DiscreteValueSet):
|
---|
| 63 | if not isinstance(value_set, DiscreteValueSet):
|
---|
| 64 | raise TypeError(
|
---|
| 65 | "This issue estimator only supports issues with discrete values"
|
---|
| 66 | )
|
---|
| 67 |
|
---|
| 68 | self.bids_received = 0
|
---|
| 69 | self.max_value_count = 0
|
---|
| 70 | self.num_values = value_set.size()
|
---|
| 71 | self.value_trackers = defaultdict(ValueEstimator)
|
---|
| 72 | self.weight = 0
|
---|
| 73 |
|
---|
| 74 | def update(self, value: Value):
|
---|
| 75 | self.bids_received += 1
|
---|
| 76 |
|
---|
| 77 | # get the value tracker of the value that is offered
|
---|
| 78 | value_tracker = self.value_trackers[value]
|
---|
| 79 |
|
---|
| 80 | # register that this value was offered
|
---|
| 81 | value_tracker.update()
|
---|
| 82 |
|
---|
| 83 | # update the count of the most common offered value
|
---|
| 84 | self.max_value_count = max([value_tracker.count, self.max_value_count])
|
---|
| 85 |
|
---|
| 86 | # update predicted issue weight
|
---|
| 87 | # the intuition here is that if the values of the receiverd offers spread out over all
|
---|
| 88 | # possible values, then this issue is likely not important to the opponent (weight == 0.0).
|
---|
| 89 | # If all received offers proposed the same value for this issue,
|
---|
| 90 | # then the predicted issue weight == 1.0
|
---|
| 91 | equal_shares = self.bids_received / self.num_values
|
---|
| 92 | self.weight = (self.max_value_count - equal_shares) / (
|
---|
| 93 | self.bids_received - equal_shares
|
---|
| 94 | )
|
---|
| 95 |
|
---|
| 96 | # recalculate all value utilities
|
---|
| 97 | for value_tracker in self.value_trackers.values():
|
---|
| 98 | value_tracker.recalculate_utility(self.max_value_count, self.weight)
|
---|
| 99 |
|
---|
| 100 | def get_value_utility(self, value: Value):
|
---|
| 101 | if value in self.value_trackers:
|
---|
| 102 | return self.value_trackers[value].utility
|
---|
| 103 |
|
---|
| 104 | return 0
|
---|
| 105 |
|
---|
| 106 |
|
---|
| 107 | class ValueEstimator:
|
---|
| 108 | def __init__(self):
|
---|
| 109 | self.count = 0
|
---|
| 110 | self.utility = 0
|
---|
| 111 |
|
---|
| 112 | def update(self):
|
---|
| 113 | self.count += 1
|
---|
| 114 |
|
---|
| 115 | def recalculate_utility(self, max_value_count: int, weight: float):
|
---|
| 116 | if weight < 1:
|
---|
| 117 | mod_value_count = ((self.count + 1) ** (1 - weight)) - 1
|
---|
| 118 | mod_max_value_count = ((max_value_count + 1) ** (1 - weight)) - 1
|
---|
| 119 |
|
---|
| 120 | self.utility = mod_value_count / mod_max_value_count
|
---|
| 121 | else:
|
---|
| 122 | self.utility = 1
|
---|