def test_factor_graph_agrees_with_two_team_explicit(self): # given p1 = Gaussian(mu=MU, sigma=SIGMA) p2 = Gaussian(mu=MU, sigma=SIGMA) p3 = Gaussian(mu=MU, sigma=SIGMA) losing_team = {"player3": p3, "player2": p2} winning_team = {"player1": p1} teams = [winning_team] + [losing_team] # when # we update skills with the factor graph ts = TrueSkillEnv(teams) new_ratings = ts.update_ratings() # and update skills with the analytic method update_ratings_in_team(winning_team, losing_team) # then self.assertAlmostEqual(new_ratings["player1"].mu, winning_team["player1"].mu, 2) self.assertAlmostEqual(new_ratings["player2"].mu, losing_team["player2"].mu, 2) self.assertAlmostEqual(new_ratings["player3"].mu, losing_team["player3"].mu, 2) self.assertAlmostEqual(new_ratings["player1"].sigma, winning_team["player1"].sigma, 2) self.assertAlmostEqual(new_ratings["player2"].sigma, new_ratings["player2"].sigma, 2) self.assertAlmostEqual(new_ratings["player3"].sigma, losing_team["player3"].sigma, 2)
def test_three_team_converges(self): # given p1 = Gaussian(mu=MU, sigma=SIGMA) p2 = Gaussian(mu=MU, sigma=SIGMA) p3 = Gaussian(mu=MU, sigma=SIGMA) teams = [{"player1": p1}, {"player2": p2}, {"player3": p3}] # when ts = TrueSkillEnv(teams) ts.update_ratings()
def test_gaussian_multiply(self): # given x = Gaussian(pi=3, tau=4) y = Gaussian(pi=5, tau=6) # when z = x * y # then self.assertEqual(z.tau, x.tau + y.tau) self.assertEqual(z.pi, x.pi + y.pi)
def test_update_player_rating(self): # given player = 'foo' rating = Gaussian(mu=1, sigma=1) self.source.data[player] = Gaussian(mu=0, sigma=10) # when self.source.update_player_rating(player, rating) # then self.assertEqual(self.source.data[player], rating)
def test_gaussian_divide(self): # given x = Gaussian(pi=4, tau=8) y = Gaussian(pi=2, tau=2) # when z = x / y # then self.assertEqual(z.tau, x.tau - y.tau) self.assertEqual(z.pi, x.pi - y.pi)
def test_prior_factor_down(self): # given var = Variable("player1") dyn_fact = 0.5 val = Gaussian(mu=MU, sigma=SIGMA) pf = PriorFactor(var, val, dyn_fact) # when pf.down() # then expected_pi = 1 / (SIGMA**2 + dyn_fact**2) expected_marginal = Gaussian(pi=expected_pi, tau=val.tau) self.assertEqual(expected_marginal, pf.var.marginal)
def test_sum_factor_helper(self): # given var = Variable() var1, var2 = Variable(), Variable() var1.pi, var2.pi, var1.tau, var2.tau = [1] * 4 perf_vars = [var1, var2] perf_messages = [Gaussian(), Gaussian()] coeffs = [1, 2] sf = SumFactor(var, perf_vars, coeffs) # when sf._update_helper(var, perf_vars, perf_messages, coeffs) # then expected_pi = 1 / (1 / 1 + 1 / 2) expected_tau = expected_pi * (1 + 2) var.messages[sf] = Gaussian(pi=expected_pi, tau=expected_tau)
def update_ratings_in_team(winning_team: Dict[str, Gaussian], losing_team: Dict[str, Gaussian], perf_noise_sigma=PERFORMANCE_NOISE, dynamics_factor=DYNAMIC_FACTOR): """Does TrueSkill update for players in a two team game. Args: winning_team: A dictionary of the skills of each player losing_team: A dictionary of the skills of each player perf_noise_sigma: Standard deviation of the performance noise dynamics_factor: Additional factor which allows uncertainty in skill to vary over time """ total_players = len(winning_team) + len(losing_team) winning_mu = sum(winning_team[p].mu for p in winning_team) losing_mu = sum(losing_team[p].mu for p in losing_team) delta_mu = winning_mu - losing_mu c = math.sqrt( sum(winning_team[p].sigma**2 for p in winning_team) + sum(losing_team[p].sigma**2 for p in losing_team) + total_players * perf_noise_sigma**2) # compute the additive and multiplicative correction factors v_game = v_truncate(delta_mu / c) w_game = w_truncate(delta_mu / c) # update the winning teams skills in place for player in winning_team: skill = winning_team[player] adjusted_var = skill.sigma**2 + dynamics_factor**2 mu_multiplier = adjusted_var / c sigma_multiplier = adjusted_var / c**2 mu = skill.mu + mu_multiplier * v_game sigma = math.sqrt(adjusted_var * (1 - w_game * sigma_multiplier)) winning_team[player] = Gaussian(mu=mu, sigma=sigma) # update the losing teams skills in place for player in losing_team: skill = losing_team[player] adjusted_var = skill.sigma**2 + dynamics_factor**2 mu_multiplier = adjusted_var / c sigma_multiplier = adjusted_var / c**2 mu = skill.mu - mu_multiplier * v_game sigma = math.sqrt(adjusted_var * (1 - w_game * sigma_multiplier)) losing_team[player] = Gaussian(mu=mu, sigma=sigma)
def test_gaussian_multiply_throws_type_error(self): # given x = Gaussian() y = 3 # when, then with self.assertRaises(TypeError): x * y
def test_sum_factor_down(self, mock_helper): # given var = Variable() var1, var2 = Variable(), Variable() var1.pi, var2.pi, var1.tau, var2.tau = [1] * 4 perf_vars = [var1, var2] perf_messages = [Gaussian(), Gaussian()] coeffs = [1, 2] sf = SumFactor(var, perf_vars, coeffs) # when sf.down() # then expected_messages = [var1.messages[sf], var2.messages[sf]] mock_helper.assert_called_with(var, perf_vars, expected_messages, coeffs)
def update_rating(winner: Gaussian, loser: Gaussian, perf_noise_sigma=PERFORMANCE_NOISE, dynamics_factor=DYNAMIC_FACTOR) -> Tuple[Gaussian, Gaussian]: """Updates the skills of two players in a 1vs1 match. Args: winner: Skill of the winner loser: Skill of the loser perf_noise_sigma: The standard devication of the performance noise. dynamics_factor: The standard deviation of the dynamics factor on the prior skill which allows uncertainty in skill to vary over time. Returns: Two new Gaussian objects containing the updated winner skill and updated loser skill respectively. """ c = math.sqrt(winner.sigma**2 + loser.sigma**2 + 2 * perf_noise_sigma**2) winner_adjusted_var = winner.sigma**2 + dynamics_factor**2 loser_adjusted_var = loser.sigma**2 + dynamics_factor**2 winning_mu = winner.mu losing_mu = loser.mu delta_mu = winning_mu - losing_mu # calculate the additive and multiplicative correction factors v_game = v_truncate(delta_mu / c) w_game = w_truncate(delta_mu / c) # update the winner mu_multiplier = winner_adjusted_var / c sigma_multiplier = winner_adjusted_var / c**2 new_mu = winning_mu + mu_multiplier * v_game new_sigma = math.sqrt(winner_adjusted_var * (1 - w_game * sigma_multiplier)) updated_winner = Gaussian(mu=new_mu, sigma=new_sigma) # update the loser mu_multiplier = loser_adjusted_var / c sigma_multiplier = loser_adjusted_var / c**2 new_mu = losing_mu - mu_multiplier * v_game new_sigma = math.sqrt(loser_adjusted_var * (1 - w_game * sigma_multiplier)) updated_loser = Gaussian(mu=new_mu, sigma=new_sigma) return updated_winner, updated_loser
def up(self): c = self.var.pi - self.var.messages[self].pi d = self.var.tau - self.var.messages[self].tau pi = c / (1 - w_truncate(d / math.sqrt(c))) tau = ((d + math.sqrt(c) * v_truncate(d / math.sqrt(c))) / (1 - w_truncate(d / math.sqrt(c)))) old_marginal = self.var.marginal self.var.update_marginal(self, Gaussian(pi=pi, tau=tau)) return old_marginal.kl_divergence(self.var.marginal)
def connect_to_source(self, data_dir='.'): if os.path.exists(os.path.join(data_dir, self.DATA_SOURCE)): with open(os.path.join(data_dir, self.DATA_SOURCE), 'r') as f: line = f.readline().strip('\n') while line: name, mu, sigma = line.split(',') mu, sigma = float(mu), float(sigma) rating = Gaussian(mu=mu, sigma=sigma) self.data[name] = rating line = f.readline().strip('\n')
def test_load_player_ratings_for_new_player(self): # given new_player = 'hello TrueSkill' # when self.source.load_player_ratings(new_player) # then expected_rating = Gaussian(mu=MU, sigma=SIGMA) self.assertEqual(self.source.data[new_player], expected_rating)
def update_ratings(self): self._run() # run the message passing algorithm new_ratings = {} for player in self.players: name = player.name pi = player.pi tau = player.tau new_ratings[name] = Gaussian(pi=pi, tau=tau) return new_ratings
def test_sum_factor_up(self, mock_helper): # given var = Variable() var1, var2 = Variable(), Variable() var1.pi, var2.pi, var1.tau, var2.tau = [1] * 4 perf_vars = [var1, var2] perf_messages = [Gaussian(), Gaussian()] coeffs = [1, 2] sf = SumFactor(var, perf_vars, coeffs) # when sf.up(0) # then # expect all variables except that at index 0 expected_vars = [var2, var] expected_messages = [var2.messages[sf], var.messages[sf]] expected_coeffs = [-2, 1] # coefficients of rearranged equation mock_helper.assert_called_with(var1, expected_vars, expected_messages, expected_coeffs)
def test_load_player_ratings_for_existing_player(self): # given existing_player = 'A True Skill Veteran' existing_rating = Gaussian(mu=1, sigma=1) self.source.data[existing_player] = existing_rating # when self.source.load_player_ratings(existing_player) # then self.assertEqual(self.source.data[existing_player], existing_rating)
def test_factor_graph_agrees_with_two_player_explicit(self): # given winner = Gaussian(mu=MU, sigma=SIGMA) loser = Gaussian(mu=MU, sigma=SIGMA) teams = [{"winner": winner}, {"loser": loser}] # when # we update the skills with the analytic method updated_winner, updated_loser = update_rating(winner, loser) # and we update the skills with the factor graph ts = TrueSkillEnv(teams) new_ratings = ts.update_ratings() # then # expect the results to be almost equal due to floating point errors # TODO check that this level of error is expected. self.assertAlmostEqual(new_ratings["winner"].mu, updated_winner.mu, 2) self.assertAlmostEqual(new_ratings["loser"].mu, updated_loser.mu, 2) self.assertAlmostEqual(new_ratings["winner"].sigma, updated_loser.sigma, 2) self.assertAlmostEqual(new_ratings["loser"].sigma, updated_loser.sigma, 2)
def _update_helper(self, var: Variable, perf_vars: List[Variable], perf_messages: List[Gaussian], coeffs: List[float]): # TODO: Make variables, messages and coeffs a list of named tuples. assert len(perf_vars) == len(perf_messages) == len(coeffs) pi = 1.0 / sum([ coeffs[i]**2 / (perf_vars[i].pi - perf_messages[i].pi) for i in range(len(coeffs)) ]) tau = pi * sum([ coeffs[i] * (perf_vars[i].tau - perf_messages[i].tau) / (perf_vars[i].pi - perf_messages[i].pi) for i in range(len(coeffs)) ]) new_message = Gaussian(pi=pi, tau=tau) var.update_message(self, new_message)
def test_truncate_factor_updates_marginal(self, mock_v, mock_w, mock_kl): # given var = Variable() var.pi, var.tau = [1] * 2 tf = TruncateFactor(var) # when tf.up() # then expected_tau = 1 expected_pi = 1 expected_marginal = Gaussian(pi=expected_pi, tau=expected_tau) self.assertEqual(var.marginal, expected_marginal)
def test_truncate_factor_calls_kl_divergence(self, mock_v, mock_w, mock_kl): # given var = Variable() var.pi, var.tau = [1] * 2 tf = TruncateFactor(var) # when tf.up() # then expected_tau = 1 expected_pi = 1 expected_marginal = Gaussian(pi=expected_pi, tau=expected_tau) mock_kl.assert_called_with(expected_marginal)
def marginal(self): return Gaussian(pi=self.pi, tau=self.tau)
def _update_helper(self, variable_one: Variable, variable_two: Variable): msg = variable_one / variable_one.messages[self] a = 1 / (1 + self.beta**2 * msg.pi) message = Gaussian(pi=a * msg.pi, tau=a * msg.tau) variable_two.update_message(self, message)
def down(self): pi = 1 / (self.value.pi**-1 + self.dynamic**2) value = Gaussian(pi=pi, tau=self.value.tau) self.var.update_marginal(self, value)
def __init__(self, variables): self.vars = variables for var in self.vars: var.messages[self] = Gaussian()
def load_player_ratings(self, player_name): if player_name not in self.data: self.data[player_name] = Gaussian(mu=MU, sigma=SIGMA) return self.data[player_name]