mirror of
https://github.com/furyfire/trueskill.git
synced 2025-01-16 01:47:39 +00:00
Start of factor graph port. Things don't work yet, but a lot of syntax updates towards PHP
This commit is contained in:
46
HashMap.php
Normal file
46
HashMap.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
class HashMap
|
||||
{
|
||||
private $_hashToValue = array();
|
||||
private $_hashToKey = array();
|
||||
|
||||
public function getValue($key)
|
||||
{
|
||||
return $this->_hashToValue[self::getHash($key)];
|
||||
}
|
||||
|
||||
public function setValue($key, $value)
|
||||
{
|
||||
$hash = self::getHash($key);
|
||||
$this->_hashToKey[$hash] = $key;
|
||||
$this->_hashToValue[$hash] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllKeys()
|
||||
{
|
||||
return \array_values($this->_hashToKey);
|
||||
}
|
||||
|
||||
public function getAllValues()
|
||||
{
|
||||
return \array_values($this->_hashToValue);
|
||||
}
|
||||
|
||||
public function count()
|
||||
{
|
||||
return \count($this->_hashToKey);
|
||||
}
|
||||
|
||||
private static function getHash($key)
|
||||
{
|
||||
if(\is_object($key))
|
||||
{
|
||||
return \spl_object_hash($key);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
?>
|
96
PHPSkills/FactorGraphs/Factor.php
Normal file
96
PHPSkills/FactorGraphs/Factor.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
require_once(dirname(__FILE__) . "../Guard.php");
|
||||
require_once(dirname(__FILE__) . "../HashMap.php");
|
||||
|
||||
use Moserware\Skills\Guard;
|
||||
use Moserware\Skills\HashMap;
|
||||
|
||||
class Factor
|
||||
{
|
||||
private $_messages = array();
|
||||
private $_messageToVariableBinding;
|
||||
|
||||
private $_name;
|
||||
private $_variables = array();
|
||||
|
||||
protected function __construct($name)
|
||||
{
|
||||
$this->_name = "Factor[" . $name . "]";
|
||||
$this->_messagesToVariableBinding = new HashMap();
|
||||
}
|
||||
|
||||
/// Returns the log-normalization constant of that factor
|
||||
public function getLogNormalization()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns the number of messages that the factor has
|
||||
public function getNumberOfMessages()
|
||||
{
|
||||
return count($this->_messages);
|
||||
}
|
||||
|
||||
protected function getVariables()
|
||||
{
|
||||
return $this->_variables;
|
||||
}
|
||||
|
||||
protected function getMessages()
|
||||
{
|
||||
return $this->_messages;
|
||||
}
|
||||
|
||||
/// Update the message and marginal of the i-th variable that the factor is connected to
|
||||
public function updateMessageIndex($messageIndex)
|
||||
{
|
||||
Guard::argumentIsValidIndex($messageIndex, count($this->_messages), "messageIndex");
|
||||
return $this->updateMessageVariable($this->_messages[$messageIndex], $this->_messageToVariableBinding->getValue($messageIndex));
|
||||
}
|
||||
|
||||
protected function updateMessageVariable($message, $variable)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
/// Resets the marginal of the variables a factor is connected to
|
||||
public function resetMarginals()
|
||||
{
|
||||
foreach ($this->_messageToVariableBindings->getAllValues() as $currentVariable)
|
||||
{
|
||||
$currentVariable->resetToPrior();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the ith message to the marginal and returns the log-normalization constant
|
||||
public function sendMessageIndex($messageIndex)
|
||||
{
|
||||
Guard::argumentIsValidIndex($messageIndex, count($_messages), "messageIndex");
|
||||
|
||||
$message = $this->_messages[$messageIndex];
|
||||
$variable = $this->_messageToVariableBinding->getValue($message);
|
||||
return $this->sendMessageVariable($message, $variable);
|
||||
}
|
||||
|
||||
protected abstract function sendMessageVariable($message, $variable);
|
||||
|
||||
public abstract function createVariableToMessageBinding($variable);
|
||||
|
||||
protected function createVariableToMessageBinding($variable, $message)
|
||||
{
|
||||
$index = count($this->_messages);
|
||||
$this->_messages[] = $message;
|
||||
$this->_messageToVariableBinding->setValue($message) = $variable;
|
||||
$this->_variables[] = $variable;
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return ($this->_name != null) ? $this->_name : base::__toString();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
18
PHPSkills/FactorGraphs/FactorGraph.php
Normal file
18
PHPSkills/FactorGraphs/FactorGraph.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
class FactorGraph
|
||||
{
|
||||
private $_variableFactory;
|
||||
|
||||
public function getVariableFactory()
|
||||
{
|
||||
return $this->_variableFactory;
|
||||
}
|
||||
|
||||
public function setVariableFactory($factory)
|
||||
{
|
||||
$this->_variableFactory = $factory;
|
||||
}
|
||||
}
|
||||
?>
|
67
PHPSkills/FactorGraphs/FactorGraphLayer.php
Normal file
67
PHPSkills/FactorGraphs/FactorGraphLayer.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
abstract class FactorGraphLayer
|
||||
{
|
||||
private $_localFactors = array();
|
||||
private $_outputVariablesGroups = array();
|
||||
private $_inputVariablesGroups = array();
|
||||
private $_parentFactorGraph;
|
||||
|
||||
protected function __construct($parentGraph)
|
||||
{
|
||||
$this->_parentFactorGraph = $parentGraph;
|
||||
}
|
||||
|
||||
protected function getInputVariablesGroups()
|
||||
{
|
||||
return $this->_inputVariablesGroups;
|
||||
}
|
||||
|
||||
// HACK
|
||||
|
||||
public function getParentFactorGraph()
|
||||
{
|
||||
return $this->_parentFactorGraph;
|
||||
}
|
||||
|
||||
public function getOutputVariablesGroups()
|
||||
{
|
||||
return $this->_outputVariablesGroups;
|
||||
}
|
||||
|
||||
public function getLocalFactors()
|
||||
{
|
||||
return $this->_localFactors;
|
||||
}
|
||||
|
||||
public function setInputVariablesGroups($value)
|
||||
{
|
||||
$this->_inputVariablesGroups = $value;
|
||||
}
|
||||
|
||||
protected function scheduleSequence($itemsToSequence)
|
||||
{
|
||||
return new ScheduleSequence($itemsToSequence);
|
||||
}
|
||||
|
||||
protected function addLayerFactor($factor)
|
||||
{
|
||||
$this->_localFactors[] = $factor;
|
||||
}
|
||||
|
||||
public abstract function buildLayer();
|
||||
|
||||
public function createPriorSchedule()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function createPosteriorSchedule()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
?>
|
57
PHPSkills/FactorGraphs/FactorList.php
Normal file
57
PHPSkills/FactorGraphs/FactorList.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for computing the factor graph's normalization constant.
|
||||
/// </summary>
|
||||
class FactorList
|
||||
{
|
||||
private $_list = array();
|
||||
|
||||
public function getLogNormalization()
|
||||
{
|
||||
foreach($this->_list as $currentFactor)
|
||||
{
|
||||
$currentFactor->resetMarginals();
|
||||
}
|
||||
|
||||
$sumLogZ = 0.0;
|
||||
|
||||
$listCount = count($this->_list);
|
||||
|
||||
for ($i = 0; $i < $listCount; $i++)
|
||||
{
|
||||
$f = $this->_list[$i];
|
||||
|
||||
$numberOfMessages = $f->getNumberOfMessages();
|
||||
|
||||
for ($j = 0; $j < $numberOfMessages; $j++)
|
||||
{
|
||||
$sumLogZ += $f->sendMessageIndex($j);
|
||||
}
|
||||
}
|
||||
|
||||
$sumLogS = 0;
|
||||
|
||||
foreach($this->_list as $currentFactor)
|
||||
{
|
||||
$sumLogS = $sumLogS + $currentFactor->getLogNormalization();
|
||||
}
|
||||
|
||||
return $sumLogZ + $sumLogS;
|
||||
}
|
||||
|
||||
public function count()
|
||||
{
|
||||
return count($this->_list);
|
||||
}
|
||||
|
||||
public function addFactor(Factor $factor)
|
||||
{
|
||||
$this->_list[] = $factor;
|
||||
return $factor;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
28
PHPSkills/FactorGraphs/Message.php
Normal file
28
PHPSkills/FactorGraphs/Message.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
class Message
|
||||
{
|
||||
private $_nameFormat;
|
||||
private $_nameFormatArgs;
|
||||
private $_value;
|
||||
|
||||
public function __construct($value = null, $nameFormat = null, $args = null)
|
||||
{
|
||||
$this->_nameFormat = $nameFormat;
|
||||
$this->_nameFormatArgs = $args;
|
||||
$this->_value = $value;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return $this->_value;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->_nameFormat; //return (_NameFormat == null) ? base.ToString() : String.Format(_NameFormat, _NameFormatArgs);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
89
PHPSkills/FactorGraphs/Schedule.php
Normal file
89
PHPSkills/FactorGraphs/Schedule.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
abstract class Schedule
|
||||
{
|
||||
private $_name;
|
||||
|
||||
protected function __construct($name)
|
||||
{
|
||||
$this->_name = $name;
|
||||
}
|
||||
|
||||
public abstract function visit($depth = -1, $maxDepth = 0);
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduleStep extends Schedule
|
||||
{
|
||||
private $_factor;
|
||||
private $_index;
|
||||
|
||||
public function __construct($name, $factor, $index)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->_factor = $factor;
|
||||
$this->_index = $index;
|
||||
}
|
||||
|
||||
public function visit($depth, $maxDepth)
|
||||
{
|
||||
$delta = $this->_factor->updateMessageIndex($this->_index);
|
||||
return $delta;
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduleSequence extends Schedule
|
||||
{
|
||||
private $_schedules;
|
||||
|
||||
public function __construct($name, $schedules)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->_schedules = $schedules;
|
||||
}
|
||||
|
||||
public function visit($depth, $maxDepth)
|
||||
{
|
||||
$maxDelta = 0;
|
||||
|
||||
foreach ($this->_schedules as $currentSchedule)
|
||||
{
|
||||
$maxDelta = max($currentSchedule->visit($depth + 1, $maxDepth), $maxDelta);
|
||||
}
|
||||
|
||||
return $maxDelta;
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduleLoop extends Schedule
|
||||
{
|
||||
private $_maxDelta;
|
||||
private $_scheduleToLoop;
|
||||
|
||||
public function __construct($name, Schedule $scheduleToLoop, $maxDelta)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->_scheduleToLoop = $scheduleToLoop;
|
||||
$this->_maxDelta = $maxDelta;
|
||||
}
|
||||
|
||||
public function visit($depth, $maxDepth)
|
||||
{
|
||||
$totalIterations = 1;
|
||||
$delta = $this->_scheduleToLoop->visit($depth + 1, $maxDepth);
|
||||
while ($delta > $this->_maxDelta)
|
||||
{
|
||||
$delta = $this->_scheduleToLoop->visit($depth + 1, $maxDepth);
|
||||
$totalIterations++;
|
||||
}
|
||||
|
||||
return $delta;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
71
PHPSkills/FactorGraphs/Variable.php
Normal file
71
PHPSkills/FactorGraphs/Variable.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
class Variable
|
||||
{
|
||||
private $_name;
|
||||
private $_prior;
|
||||
private $_value;
|
||||
|
||||
public function __construct($name, $prior)
|
||||
{
|
||||
$this->_name = "Variable[" . $name . "]";
|
||||
$this->_prior = $prior;
|
||||
$this->resetToPrior();
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return $this->_value;
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->_value = $value;
|
||||
}
|
||||
|
||||
public function resetToPrior()
|
||||
{
|
||||
$this->_value = $this->_prior;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultVariable extends Variable
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("Default", null);
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
class KeyedVariable extends Variable
|
||||
{
|
||||
private $_key;
|
||||
public function __construct($key, $name, $prior)
|
||||
{
|
||||
parent::__construct($name, $prior);
|
||||
$this->_key = $key;
|
||||
}
|
||||
|
||||
public function getKey()
|
||||
{
|
||||
return $this->_key;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
28
PHPSkills/FactorGraphs/VariableFactory.php
Normal file
28
PHPSkills/FactorGraphs/VariableFactory.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\FactorGraphs;
|
||||
|
||||
class VariableFactory
|
||||
{
|
||||
// using a Func<TValue> to encourage fresh copies in case it's overwritten
|
||||
private $_variablePriorInitializer;
|
||||
|
||||
public function __construct($variablePriorInitializer)
|
||||
{
|
||||
$this->_variablePriorInitializer = $variablePriorInitializer;
|
||||
}
|
||||
|
||||
public function createBasicVariable()
|
||||
{
|
||||
$newVar = new Variable($this->_variablePriorInitializer());
|
||||
return $newVar;
|
||||
}
|
||||
|
||||
public function createKeyedVariable($key)
|
||||
{
|
||||
$newVar = new KeyedVariable($key, $this->_variablePriorInitializer());
|
||||
return $newVar;
|
||||
}
|
||||
}s
|
||||
|
||||
?>
|
@ -49,6 +49,16 @@ class GaussianDistribution
|
||||
return $this->_standardDeviation;
|
||||
}
|
||||
|
||||
public function getPrecision()
|
||||
{
|
||||
return $this->_precision;
|
||||
}
|
||||
|
||||
public function getPrecisionMean()
|
||||
{
|
||||
return $this->_precisionMean;
|
||||
}
|
||||
|
||||
public function getNormalizationConstant()
|
||||
{
|
||||
// Great derivation of this is at http://www.astro.psu.edu/~mce/A451_2/A451/downloads/notes0.pdf
|
||||
|
@ -1,46 +1,40 @@
|
||||
<?php
|
||||
namespace Moserware\Skills;
|
||||
|
||||
require_once(dirname(__FILE__) . "/HashMap.php");
|
||||
|
||||
class RatingContainer
|
||||
{
|
||||
private $_playerHashToRating = array();
|
||||
private $_playerHashToPlayer = array();
|
||||
private $_playerToRating;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->_playerToRating = new \HashMap();
|
||||
}
|
||||
|
||||
public function getRating($player)
|
||||
{
|
||||
return $this->_playerHashToRating[self::getHash($player)];
|
||||
return $this->_playerToRating->getValue($player);
|
||||
}
|
||||
|
||||
public function setRating($player, $rating)
|
||||
{
|
||||
$hash = self::getHash($player);
|
||||
$this->_playerHashToPlayer[$hash] = $player;
|
||||
$this->_playerHashToRating[$hash] = $rating;
|
||||
return $this;
|
||||
return $this->_playerToRating->setValue($player, $rating);
|
||||
}
|
||||
|
||||
public function getAllPlayers()
|
||||
{
|
||||
return \array_values($this->_playerHashToPlayer);
|
||||
return $this->_playerToRating->getAllKeys();
|
||||
}
|
||||
|
||||
public function getAllRatings()
|
||||
{
|
||||
return \array_values($this->_playerHashToRating);
|
||||
return $this->_playerToRating->getAllValues();
|
||||
}
|
||||
|
||||
public function count()
|
||||
{
|
||||
return \count($this->_playerHashToPlayer);
|
||||
}
|
||||
private static function getHash($player)
|
||||
{
|
||||
if(\is_object($player))
|
||||
{
|
||||
return \spl_object_hash($player);
|
||||
}
|
||||
|
||||
return $player;
|
||||
return \count($this->_playerToRating->count());
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
168
PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php
Normal file
168
PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates TrueSkill using a full factor graph.
|
||||
/// </summary>
|
||||
class FactorGraphTrueSkillCalculator extends SkillCalculator
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(SkillCalculatorSupportedOptions::PARTIAL_PLAY | SkillCalculatorSupportedOptions::PARTIAL_UPDATE, TeamsRange::atLeast(2), PlayersRange::atLeast(1));
|
||||
}
|
||||
|
||||
public function CalculateNewRatings(GameInfo $gameInfo,
|
||||
array $teams,
|
||||
array $teamRanks)
|
||||
{
|
||||
Guard::argumentNotNull($gameInfo, "gameInfo");
|
||||
$this->validateTeamCountAndPlayersCountPerTeam($teams);
|
||||
|
||||
RankSorter::sort($teams, $teamRanks);
|
||||
|
||||
$factorGraph = new TrueSkillFactorGraph($gameInfo, $teams, $teamRanks);
|
||||
$factorGraph->buildGraph();
|
||||
$factorGraph->runSchedule();
|
||||
|
||||
$probabilityOfOutcome = $factorGraph->getProbabilityOfRanking();
|
||||
|
||||
return $factorGraph->getUpdatedRatings();
|
||||
}
|
||||
|
||||
|
||||
public function calculateMatchQuality(GameInfo $gameInfo,
|
||||
array $teams)
|
||||
{
|
||||
// We need to create the A matrix which is the player team assigments.
|
||||
$teamAssignmentsList = $teams.ToList();
|
||||
$skillsMatrix = $this->getPlayerCovarianceMatrix($teamAssignmentsList);
|
||||
$meanVector = $this->getPlayerMeansVector($teamAssignmentsList);
|
||||
$meanVectorTranspose = $meanVector->getTranspose();
|
||||
|
||||
$playerTeamAssignmentsMatrix = $this->createPlayerTeamAssignmentMatrix($teamAssignmentsList, $meanVector->getRowCount());
|
||||
$playerTeamAssignmentsMatrixTranspose = $playerTeamAssignmentsMatrix->getTranspose();
|
||||
|
||||
$betaSquared = square($gameInfo->getBeta());
|
||||
|
||||
$start = Matrix::multiply($meanVectorTranspose, $playerTeamAssignmentsMatrix);
|
||||
$aTa = Matrix::multiply(
|
||||
Matrix::scalarMultiply($betaSquared,
|
||||
$playerTeamAssignmentsMatrixTranspose),
|
||||
$playerTeamAssignmentsMatrix);
|
||||
|
||||
$aTSA = Matrix::multiply(
|
||||
Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $skillsMatrix),
|
||||
$playerTeamAssignmentsMatrix);
|
||||
|
||||
$middle = Matrix::add($aTa, $aTSA);
|
||||
|
||||
$middleInverse = $middle->getInverse();
|
||||
|
||||
$end = Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $meanVector);
|
||||
|
||||
$expPartMatrix = Matrix::scalarMultiply(-0.5, (Matrix::multiply(Matrix::multiply($start, $middleInverse), $end)));
|
||||
$expPart = $expPartMatrix->getDeterminant();
|
||||
|
||||
$sqrtPartNumerator = $aTa->getDeterminant();
|
||||
$sqrtPartDenominator = $middle->getDeterminant();
|
||||
$sqrtPart = $sqrtPartNumerator / $sqrtPartDenominator;
|
||||
|
||||
$result = exp($expPart) * sqrt($sqrtPart);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function getPlayerMeansVector(array $teamAssignmentsList)
|
||||
{
|
||||
// A simple vector of all the player means.
|
||||
return new Vector($this->getPlayerRatingValues($teamAssignmentsList,
|
||||
function($rating)
|
||||
{
|
||||
return $rating->getMean();
|
||||
}));
|
||||
}
|
||||
|
||||
private static function getPlayerCovarianceMatrix(array $teamAssignmentsList)
|
||||
{
|
||||
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
|
||||
// players.
|
||||
return new DiagonalMatrix(
|
||||
$this->getPlayerRatingValues($teamAssignmentsList,
|
||||
function($rating)
|
||||
{
|
||||
return square($rating->getStandardDeviation());
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper function that gets a list of values for all player ratings
|
||||
private static function getPlayerRatingValues(array $teamAssignmentsList,
|
||||
$playerRatingFunction)
|
||||
{
|
||||
$playerRatingValues = array();
|
||||
|
||||
foreach ($teamAssignmentsList as $currentTeam)
|
||||
{
|
||||
foreach (currentTeam.Values as $currentRating)
|
||||
{
|
||||
$playerRatingValues[] = $playerRatingFunction($currentRating);
|
||||
}
|
||||
}
|
||||
|
||||
return $playerRatingValues;
|
||||
}
|
||||
|
||||
private static function createPlayerTeamAssignmentMatrix($teamAssignmentsList, $totalPlayers)
|
||||
{
|
||||
// The team assignment matrix is often referred to as the "A" matrix. It's a matrix whose rows represent the players
|
||||
// and the columns represent teams. At Matrix[row, column] represents that player[row] is on team[col]
|
||||
// Positive values represent an assignment and a negative value means that we subtract the value of the next
|
||||
// team since we're dealing with pairs. This means that this matrix always has teams - 1 columns.
|
||||
// The only other tricky thing is that values represent the play percentage.
|
||||
|
||||
// For example, consider a 3 team game where team1 is just player1, team 2 is player 2 and player 3, and
|
||||
// team3 is just player 4. Furthermore, player 2 and player 3 on team 2 played 25% and 75% of the time
|
||||
// (e.g. partial play), the A matrix would be:
|
||||
|
||||
// A = this 4x2 matrix:
|
||||
// | 1.00 0.00 |
|
||||
// | -0.25 0.25 |
|
||||
// | -0.75 0.75 |
|
||||
// | 0.00 -1.00 |
|
||||
|
||||
$playerAssignments = array();
|
||||
$totalPreviousPlayers = 0;
|
||||
|
||||
$teamAssignmentsListCount = count($teamAssignmentsList);
|
||||
|
||||
for ($i = 0; $i < $teamAssignmentsListCount - 1; $i++)
|
||||
{
|
||||
$currentTeam = $teamAssignmentsList[$i];
|
||||
|
||||
// Need to add in 0's for all the previous players, since they're not
|
||||
// on this team
|
||||
$currentRowValues = array();
|
||||
$playerAssignments[] = &$currentRowValues;
|
||||
|
||||
foreach ($currentTeam as $currentRating)
|
||||
{
|
||||
$currentRowValues[] = PartialPlay::getPartialPlayPercentage($currentRating->getKey());
|
||||
// indicates the player is on the team
|
||||
$totalPreviousPlayers++;
|
||||
}
|
||||
|
||||
$nextTeam = $teamAssignmentsList[$i + 1];
|
||||
foreach ($nextTeam as $nextTeamPlayerPair)
|
||||
{
|
||||
// Add a -1 * playing time to represent the difference
|
||||
$currentRowValues[] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayerPair->getKey());
|
||||
}
|
||||
}
|
||||
|
||||
$playerTeamAssignmentsMatrix = new Matrix($totalPlayers, $teamAssignmentsListCount - 1, $playerAssignments);
|
||||
|
||||
return $playerTeamAssignmentsMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
32
PHPSkills/TrueSkill/Factors/GaussianFactor.php
Normal file
32
PHPSkills/TrueSkill/Factors/GaussianFactor.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
abstract class GaussianFactor extends Factor
|
||||
{
|
||||
protected function __construct($name)
|
||||
{
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
/// Sends the factor-graph message with and returns the log-normalization constant
|
||||
protected function sendMessageVariable(Message $message, Variable $variable)
|
||||
{
|
||||
$marginal = $variable->getValue();
|
||||
$messageValue = $message->getValue();
|
||||
$logZ = GaussianDistribution::logProductNormalization($marginal, $messageValue);
|
||||
$variable->setValue($marginal*$messageValue);
|
||||
return $logZ;
|
||||
}
|
||||
|
||||
public function createVariableToMessageBinding(Variable $variable)
|
||||
{
|
||||
return parent::createVariableToMessageBinding($variable,
|
||||
new Message(
|
||||
GaussianDistribution::fromPrecisionMean(0, 0),
|
||||
"message from {0} to {1}", $this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
?>
|
73
PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php
Normal file
73
PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
/// <summary>
|
||||
/// Factor representing a team difference that has exceeded the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
class GaussianGreaterThanFactor extends GaussianFactor
|
||||
{
|
||||
private $_epsilon;
|
||||
|
||||
public function __construct($epsilon, Variable $variable)
|
||||
{
|
||||
parent::_construct("{0} > {1:0.000}");
|
||||
$this->_epsilon = $epsilon;
|
||||
$this->createVariableToMessageBinding($variable);
|
||||
}
|
||||
|
||||
public function getLogNormalization()
|
||||
{
|
||||
$vars = $this->getVariables();
|
||||
$marginal = $vars[0]->getValue();
|
||||
$messages = $this->getMessages();
|
||||
$message = $messages[0]->getValue();
|
||||
$messageFromVariable = GaussianDistribution::divide($marginal, $message);
|
||||
return -GaussianDistribution::logProductNormalization($messageFromVariable, $message)
|
||||
+
|
||||
log(
|
||||
GaussianDistribution::cumulativeTo(($messageFromVariable->getMean() - $this->_epsilon)/
|
||||
$messageFromVariable->getStandardDeviation()));
|
||||
|
||||
}
|
||||
|
||||
protected function updateMessageVariable(Message $message, Variable $variable)
|
||||
{
|
||||
$oldMarginal = clone $variable->getValue();
|
||||
$oldMessage = clone $message->getValue();
|
||||
$messageFromVar = GaussianDistribution::divide($oldMarginal, $oldMessage);
|
||||
|
||||
$c = $messageFromVar->getPrecision();
|
||||
$d = $messageFromVar->getPrecisionMean();
|
||||
|
||||
$sqrtC = sqrt($c);
|
||||
|
||||
$dOnSqrtC = $d/$sqrtC;
|
||||
|
||||
$epsilsonTimesSqrtC = $this->_epsilon*$sqrtC;
|
||||
$d = $messageFromVar->getPrecisionMean();
|
||||
|
||||
$denom = 1.0 - TruncatedGaussianCorrectionFunctions::vExceedsMargin($dOnSqrtC, $epsilsonTimesSqrtC);
|
||||
|
||||
$newPrecision = $c/$denom;
|
||||
$newPrecisionMean = ($d +
|
||||
$sqrtC*
|
||||
TruncatedGaussianCorrectionFunctions::vExceedsMargin($dOnSqrtC, $epsilsonTimesSqrtC))/
|
||||
$denom;
|
||||
|
||||
$newMarginal = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
|
||||
|
||||
$newMessage = GaussianDistribution::divide(
|
||||
GaussianDistribution::multiply($oldMessage, $newMarginal),
|
||||
$oldMarginal);
|
||||
|
||||
/// Update the message and marginal
|
||||
$message->setValue($newMessage);
|
||||
|
||||
$variable->setValue($newMarginal);
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return GaussianDistribution::subtract($newMarginal, $oldMarginal);
|
||||
}
|
||||
}
|
||||
?>
|
78
PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php
Normal file
78
PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
/// <summary>
|
||||
/// Connects two variables and adds uncertainty.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
class GaussianLikelihoodFactor extends GaussianFactor
|
||||
{
|
||||
private $_precision;
|
||||
|
||||
public function __construct($betaSquared, Variable $variable1, Variable $variable2)
|
||||
{
|
||||
parent::__construct("Likelihood of {0} going to {1}");
|
||||
$this->_precision = 1.0/$betaSquared;
|
||||
$this->createVariableToMessageBinding($variable1);
|
||||
$this->createVariableToMessageBinding($variable2);
|
||||
}
|
||||
|
||||
public function getLogNormalization()
|
||||
{
|
||||
$vars = $this->getVariables();
|
||||
$messages = $this->getMessages();
|
||||
|
||||
return GaussianDistribution::logRatioNormalization(
|
||||
$vars[0]->getValue(),
|
||||
$messages[0]->getValue());
|
||||
}
|
||||
|
||||
private function updateHelper(Message $message1, Message $message2,
|
||||
Variable $variable1, Variable $variable2)
|
||||
{
|
||||
$message1Value = clone $message1->getValue();
|
||||
$message2Value = clone $message2->getValue();
|
||||
|
||||
$marginal1 = clone $variable1->getValue();
|
||||
$marginal2 = clone $variable2->getValue();
|
||||
|
||||
$a = $this->_precision/($this->_precision + $marginal2->getPrecision() - $message2Value->getPrecision());
|
||||
|
||||
$newMessage = GaussianDistribution::fromPrecisionMean(
|
||||
$a*($marginal2->getPrecisionMean() - $message2Value->getPrecisionMean()),
|
||||
$a*($marginal2->getPrecision() - $message2Value->getPrecision()));
|
||||
|
||||
$oldMarginalWithoutMessage = GaussianDistribution::divide($marginal1, $message1Value);
|
||||
|
||||
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMessage, $newMessage);
|
||||
|
||||
/// Update the message and marginal
|
||||
|
||||
$message1->setValue($newMessage);
|
||||
$variable1->setValue($newMarginal);
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return GaussianDistribution::subtract($newMarginal, $marginal1);
|
||||
}
|
||||
|
||||
public function updateMessageIndex($messageIndex)
|
||||
{
|
||||
$messages = $this->getMessages();
|
||||
$vars = $this->getVariables();
|
||||
|
||||
switch ($messageIndex)
|
||||
{
|
||||
case 0:
|
||||
return $this->updateHelper($messages[0], $messages[1],
|
||||
$vars[0], $vars[1]);
|
||||
case 1:
|
||||
return $this->updateHelper($messages[1], $messages[0],
|
||||
$vars[1], $vars[0]);
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
40
PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php
Normal file
40
PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies the factor graph with prior information.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
class GaussianPriorFactor extends GaussianFactor
|
||||
{
|
||||
private $_newMessage;
|
||||
|
||||
public function __construct($mean, $variance, Variable $variable)
|
||||
{
|
||||
parent::__construct("Prior value going to {0}");
|
||||
$this->_newMessage = new GaussianDistribution($mean, sqrt($variance));
|
||||
$this->createVariableToMessageBinding($variable,
|
||||
new Message(
|
||||
GaussianDistribution::fromPrecisionMean(0, 0),
|
||||
"message from {0} to {1}",
|
||||
this, variable));
|
||||
}
|
||||
|
||||
protected function updateMessageVariable(Message $message, Variable $variable)
|
||||
{
|
||||
$oldMarginal = clone $variable->getValue();
|
||||
$oldMessage = $message;
|
||||
$newMarginal =
|
||||
GaussianDistribution::fromPrecisionMean(
|
||||
$oldMarginal->getPrecisionMean() + $this->_newMessage->getPrecisionMean() - $oldMessage->getValue()->getPrecisionMean(),
|
||||
$oldMarginal->getPrecision() + $this->_newMessage->getPrecision() - $oldMessage->getValue()->getPrecision());
|
||||
|
||||
$variable->setValue($newMarginal);
|
||||
$message->setValue($this->_newMessage);
|
||||
return GaussianDistribution::subtract($oldMarginal, $newMarginal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
?>
|
216
PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php
Normal file
216
PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php
Normal file
@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
/// <summary>
|
||||
/// Factor that sums together multiple Gaussians.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
class GaussianWeightedSumFactor extends GaussianFactor
|
||||
{
|
||||
private $_variableIndexOrdersForWeights = array();
|
||||
|
||||
// This following is used for convenience, for example, the first entry is [0, 1, 2]
|
||||
// corresponding to v[0] = a1*v[1] + a2*v[2]
|
||||
private $_weights;
|
||||
private $_weightsSquared;
|
||||
|
||||
public function __construct(Variable $sumVariable, array $variablesToSum, array $variableWeights = null)
|
||||
{
|
||||
parent::__construct($this->createName($sumVariable, $variablesToSum, $variableWeights));
|
||||
$this->_weights = array();
|
||||
$this->_weightsSquared = array();
|
||||
|
||||
// The first weights are a straightforward copy
|
||||
// v_0 = a_1*v_1 + a_2*v_2 + ... + a_n * v_n
|
||||
$this->_weights[0] = array();
|
||||
|
||||
$variableWeightsLength = count($variableWeights);
|
||||
|
||||
for($i = 0; $i < $variableWeightsLength; $i++)
|
||||
{
|
||||
$weight = $variableWeights[$i];
|
||||
$this->_weights[0][$i] = $weight;
|
||||
$this->_weightsSquared[0][i] = $weight * $weight;
|
||||
}
|
||||
|
||||
$variablesToSumLength = count($variablesToSum);
|
||||
|
||||
// 0..n-1
|
||||
for($i = 0; $i < ($variablesToSumLength + 1); $i++)
|
||||
{
|
||||
$this->_variableIndexOrdersForWeights[] = $i;
|
||||
}
|
||||
|
||||
// The rest move the variables around and divide out the constant.
|
||||
// For example:
|
||||
// v_1 = (-a_2 / a_1) * v_2 + (-a3/a1) * v_3 + ... + (1.0 / a_1) * v_0
|
||||
// By convention, we'll put the v_0 term at the end
|
||||
|
||||
$weightsLength = $variableWeightsLength + 1;
|
||||
for ($weightsIndex = 1; $weightsIndex < $weightsLength; $weightsIndex++)
|
||||
{
|
||||
$this->_weights[$weightsIndex] = array();
|
||||
|
||||
$variableIndices = array();
|
||||
$variableIndices[] = $weightsIndex;
|
||||
|
||||
$currentWeightsSquared = array();
|
||||
$this->_WeightsSquared[$weightsIndex] = $currentWeightsSquared;
|
||||
|
||||
// keep a single variable to keep track of where we are in the array.
|
||||
// This is helpful since we skip over one of the spots
|
||||
$currentDestinationWeightIndex = 0;
|
||||
|
||||
for ($currentWeightSourceIndex = 0;
|
||||
$currentWeightSourceIndex < $variableWeights.Length;
|
||||
$currentWeightSourceIndex++)
|
||||
{
|
||||
if ($currentWeightSourceIndex == ($weightsIndex - 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentWeight = (-$variableWeights[$currentWeightSourceIndex]/$variableWeights[$weightsIndex - 1]);
|
||||
|
||||
if ($variableWeights[$weightsIndex - 1] == 0)
|
||||
{
|
||||
// HACK: Getting around division by zero
|
||||
$currentWeight = 0;
|
||||
}
|
||||
|
||||
$currentWeights[$currentDestinationWeightIndex] = $currentWeight;
|
||||
$currentWeightsSquared[$currentDestinationWeightIndex] = $currentWeight*currentWeight;
|
||||
|
||||
$variableIndices[$currentDestinationWeightIndex + 1] = $currentWeightSourceIndex + 1;
|
||||
$currentDestinationWeightIndex++;
|
||||
}
|
||||
|
||||
// And the final one
|
||||
$finalWeight = 1.0/$variableWeights[$weightsIndex - 1];
|
||||
|
||||
if ($variableWeights[$weightsIndex - 1] == 0)
|
||||
{
|
||||
// HACK: Getting around division by zero
|
||||
$finalWeight = 0;
|
||||
}
|
||||
$currentWeights[$currentDestinationWeightIndex] = finalWeight;
|
||||
$currentWeightsSquared[currentDestinationWeightIndex] = finalWeight*finalWeight;
|
||||
$variableIndices[$variableIndices.Length - 1] = 0;
|
||||
$this->_variableIndexOrdersForWeights[] = $variableIndices;
|
||||
}
|
||||
|
||||
$this->createVariableToMessageBinding($sumVariable);
|
||||
|
||||
foreach ($variablesToSum as $currentVariable)
|
||||
{
|
||||
$this->createVariableToMessageBinding($currentVariable);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogNormalization()
|
||||
{
|
||||
$vars = $this->getVariables();
|
||||
$messages = $this->getMessages();
|
||||
|
||||
$result = 0.0;
|
||||
|
||||
// We start at 1 since offset 0 has the sum
|
||||
$varCount = count($vars);
|
||||
for ($i = 1; $i < $varCount; $i++)
|
||||
{
|
||||
$result += GaussianDistribution::logRatioNormalization($vars[i]->getValue(), $messages[i]->getValue());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function updateHelper(array $weights, array $weightsSquared,
|
||||
array $messages,
|
||||
array $variables)
|
||||
{
|
||||
// Potentially look at http://mathworld.wolfram.com/NormalSumDistribution.html for clues as
|
||||
// to what it's doing
|
||||
|
||||
$messages = $this->getMessages();
|
||||
$message0 = clone $messages[0]->getValue();
|
||||
$marginal0 = clone $variables[0]->getValue();
|
||||
|
||||
// The math works out so that 1/newPrecision = sum of a_i^2 /marginalsWithoutMessages[i]
|
||||
$inverseOfNewPrecisionSum = 0.0;
|
||||
$anotherInverseOfNewPrecisionSum = 0.0;
|
||||
$weightedMeanSum = 0.0;
|
||||
$anotherWeightedMeanSum = 0.0;
|
||||
|
||||
$weightsSquaredLength = count($weightsSquared);
|
||||
|
||||
for ($i = 0; $i < $weightsSquaredLength; $i++)
|
||||
{
|
||||
// These flow directly from the paper
|
||||
|
||||
$inverseOfNewPrecisionSum += $weightsSquared[i]/
|
||||
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
|
||||
|
||||
$diff = GaussianDistribution::divide($variables[$i + 1]->getValue(), $messages[$i + 1]->getValue());
|
||||
$anotherInverseOfNewPrecisionSum += $weightsSquared[i]/$diff->getPrecision();
|
||||
|
||||
$weightedMeanSum += $weights[i]
|
||||
*
|
||||
($variables[$i + 1]->getValue()->getPrecisionMean() - $messages[$i + 1]->getValue()->getPrecisionMean())
|
||||
/
|
||||
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
|
||||
|
||||
$anotherWeightedMeanSum += $weights[$i]*$diff->getPrecisionMean()/$diff->getPrecision();
|
||||
}
|
||||
|
||||
$newPrecision = 1.0/$inverseOfNewPrecisionSum;
|
||||
$anotherNewPrecision = 1.0/$anotherInverseOfNewPrecisionSum;
|
||||
|
||||
$newPrecisionMean = $newPrecision*$weightedMeanSum;
|
||||
$anotherNewPrecisionMean = $anotherNewPrecision*$anotherWeightedMeanSum;
|
||||
|
||||
$newMessage = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
|
||||
$oldMarginalWithoutMessage = GaussianDistribution::divide($marginal0, $message0);
|
||||
|
||||
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMessage, $newMessage);
|
||||
|
||||
/// Update the message and marginal
|
||||
|
||||
$messages[0]->setValue($newMessage);
|
||||
$variables[0]->setValue($newMarginal);
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
$finalDiff = GaussianDistribution::subtract($newMarginal, $marginal0);
|
||||
return $finalDiff;
|
||||
}
|
||||
|
||||
public function updateMessageIndex($messageIndex)
|
||||
{
|
||||
$allMessages = $this->getMessages();
|
||||
$allVariables = $this->getVariables();
|
||||
|
||||
Guard::argumentIsValidIndex($messageIndex, count($allMessages),"messageIndex");
|
||||
|
||||
$updatedMessages = array();
|
||||
$updatedVariables = array();
|
||||
|
||||
$indicesToUse = $this->_variableIndexOrdersForWeights[$messageIndex];
|
||||
|
||||
// The tricky part here is that we have to put the messages and variables in the same
|
||||
// order as the weights. Thankfully, the weights and messages share the same index numbers,
|
||||
// so we just need to make sure they're consistent
|
||||
$allMessagesCount = count($allMessages);
|
||||
for ($i = 0; i < $allMessagesCount; $i++)
|
||||
{
|
||||
$updatedMessages[] =$allMessages[$indicesToUse[$i]];
|
||||
$updatedVariables[] = $allVariables[$indicesToUse[$i]];
|
||||
}
|
||||
|
||||
return updateHelper($this->_weights[$messageIndex],
|
||||
$this->_weightsSquared[$messageIndex],
|
||||
$updatedMessages,
|
||||
$updatedVariables);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
72
PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php
Normal file
72
PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Factors;
|
||||
|
||||
/// <summary>
|
||||
/// Factor representing a team difference that has not exceeded the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
class GaussianWithinFactor extends GaussianFactor
|
||||
{
|
||||
private $_epsilon;
|
||||
|
||||
public function __construct($epsilon, Variable $variable)
|
||||
{
|
||||
$this->_epsilon = $epsilon;
|
||||
$this->createVariableToMessageBinding($variable);
|
||||
}
|
||||
|
||||
public function getLogNormalization()
|
||||
{
|
||||
$variables = $this->getVariables();
|
||||
$marginal = $variables[0]->getValue();
|
||||
|
||||
$messages = $this->getMessages();
|
||||
$message = $messages[0]->getValue();
|
||||
$messageFromVariable = GaussianDistribution::divide($marginal, $message);
|
||||
$mean = $messageFromVariable->getMean();
|
||||
$std = $messageFromVariable->getStandardDeviation();
|
||||
$z = GaussianDistribution::cumulativeTo(($this->_epsilon - $mean)/$std)
|
||||
-
|
||||
GaussianDistribution::cumulativeTo((-$this->_epsilon - $mean)/$std);
|
||||
|
||||
return -GaussianDistribution::logProductNormalization($messageFromVariable, $message) + log($z);
|
||||
}
|
||||
|
||||
protected function updateMessage(Message $message, Variable $variable)
|
||||
{
|
||||
$oldMarginal = clone $variable->getValue();
|
||||
$oldMessage = clone $message->getValue();
|
||||
$messageFromVariable = GaussianDistribution::divide($oldMarginal, $oldMessage);
|
||||
|
||||
$c = $messageFromVariable->getPrecision();
|
||||
$d = $messageFromVariable->getPrecisionMean();
|
||||
|
||||
$sqrtC = sqrt($c);
|
||||
$dOnSqrtC = $d/$sqrtC;
|
||||
|
||||
$epsilonTimesSqrtC = $this->_epsilon*$sqrtC;
|
||||
$d = $messageFromVariable->getPrecisionMean();
|
||||
|
||||
$denominator = 1.0 - TruncatedGaussianCorrectionFunctions::wWithinMargin($dOnSqrtC, $epsilonTimesSqrtC);
|
||||
$newPrecision = $c/$denominator;
|
||||
$newPrecisionMean = ($d +
|
||||
$sqrtC*
|
||||
TruncatedGaussianCorrectionFunctions::vWithinMargin($dOnSqrtC, $epsilonTimesSqrtC))/
|
||||
$denominator;
|
||||
|
||||
$newMarginal = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
|
||||
$newMessage = GaussianDistribution::divide(
|
||||
GaussianDistribution::multiply($oldMessage, $newMarginal),
|
||||
$oldMarginal);
|
||||
|
||||
/// Update the message and marginal
|
||||
$message->setValue($newMessage);
|
||||
$variable->setValue($newMarginal);
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return GaussianDistribution::subtract($newMarginal, $oldMarginal);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
147
PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php
Normal file
147
PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
// The whole purpose of this is to do a loop on the bottom
|
||||
class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
private $_TeamDifferencesComparisonLayer;
|
||||
private $_TeamPerformancesToTeamPerformanceDifferencesLayer;
|
||||
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph,
|
||||
TeamPerformancesToTeamPerformanceDifferencesLayer $teamPerformancesToPerformanceDifferences,
|
||||
TeamDifferencesComparisonLayer $teamDifferencesComparisonLayer)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer = $teamPerformancesToPerformanceDifferences;
|
||||
$this->_TeamDifferencesComparisonLayer = $teamDifferencesComparisonLayer;
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->setRawInputVariablesGroups($this->getInputVariablesGroups());
|
||||
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->buildLayer();
|
||||
|
||||
$this->_TeamDifferencesComparisonLayer->setRawInputVariablesGroups(
|
||||
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getRawOutputVariablesGroups());
|
||||
$this->_TeamDifferencesComparisonLayer->buildLayer();
|
||||
}
|
||||
|
||||
public function createPriorSchedule()
|
||||
{
|
||||
// BLOG about $loop
|
||||
switch (count($this->getInputVariablesGroups()))
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
throw new InvalidOperationException();
|
||||
case 2:
|
||||
$loop = $this->createTwoTeamInnerPriorLoopSchedule();
|
||||
break;
|
||||
default:
|
||||
$loop = $this->createMultipleTeamInnerPriorLoopSchedule();
|
||||
break;
|
||||
}
|
||||
|
||||
// When dealing with differences, there are always (n-1) differences, so add in the 1
|
||||
$totalTeamDifferences = count($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
|
||||
$totalTeams = $totalTeamDifferences + 1;
|
||||
|
||||
$innerSchedule = new ScheduleSequence(
|
||||
"inner schedule",
|
||||
array(
|
||||
$loop,
|
||||
new ScheduleStep(
|
||||
"teamPerformanceToPerformanceDifferenceFactors[0] @ 1",
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[0], 1),
|
||||
new ScheduleStep(
|
||||
"teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = {0} - 1] @ 2",
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1], 2)
|
||||
)
|
||||
);
|
||||
|
||||
return innerSchedule;
|
||||
}
|
||||
|
||||
private function createTwoTeamInnerPriorLoopSchedule()
|
||||
{
|
||||
return $this->scheduleSequence(
|
||||
array(
|
||||
new ScheduleStep(
|
||||
"send team perf to perf differences",
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[0],
|
||||
0),
|
||||
new ScheduleStep(
|
||||
"send to greater than or within factor",
|
||||
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[0],
|
||||
0)
|
||||
),
|
||||
"loop of just two teams inner sequence");
|
||||
}
|
||||
|
||||
private function createMultipleTeamInnerPriorLoopSchedule()
|
||||
{
|
||||
$totalTeamDifferences = count($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
|
||||
|
||||
$forwardScheduleList = array();
|
||||
|
||||
for ($i = 0; $i < $totalTeamDifferences - 1; $i++)
|
||||
{
|
||||
$currentForwardSchedulePiece =
|
||||
$this->scheduleSequence(
|
||||
array(
|
||||
new ScheduleStep(
|
||||
sprintf("team perf to perf diff %d", $i),
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$i], 0),
|
||||
new ScheduleStep(
|
||||
sprintf("greater than or within result factor %d", $i),
|
||||
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[$i], 0),
|
||||
new ScheduleStep(
|
||||
sprintf("team perf to perf diff factors [%d], 2", $i),
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$i], 2)
|
||||
), sprintf("current forward schedule piece %d", $i);
|
||||
|
||||
$forwardScheduleList[] = $currentForwardSchedulePiece;
|
||||
}
|
||||
|
||||
$forwardSchedule = new ScheduleSequence("forward schedule", $forwardScheduleList);
|
||||
|
||||
$backwardScheduleList = array();
|
||||
|
||||
for ($i = 0; $i < $totalTeamDifferences - 1; $i++)
|
||||
{
|
||||
$currentBackwardSchedulePiece = new ScheduleSequence(
|
||||
"current backward schedule piece",
|
||||
array(
|
||||
new ScheduleStep(
|
||||
sprintf("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 0", $i),
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 0),
|
||||
new ScheduleStep(
|
||||
sprintf("greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - %d] @ 0", $i),
|
||||
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 0),
|
||||
new ScheduleStep(
|
||||
sprintf("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 1", $i),
|
||||
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 1)
|
||||
);
|
||||
$backwardScheduleList[] = $currentBackwardSchedulePiece;
|
||||
}
|
||||
|
||||
$backwardSchedule = new ScheduleSequence("backward schedule", $backwardScheduleList);
|
||||
|
||||
$forwardBackwardScheduleToLoop =
|
||||
new ScheduleSequence(
|
||||
"forward Backward Schedule To Loop",
|
||||
array($forwardSchedule, $backwardSchedule));
|
||||
|
||||
$initialMaxDelta = 0.0001;
|
||||
|
||||
$loop = new ScheduleLoop(
|
||||
sprintf("loop with max delta of %f", $initialMaxDelta),
|
||||
$forwardBackwardScheduleToLoop,
|
||||
$initialMaxDelta);
|
||||
|
||||
return $loop;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
foreach ($this->getInputVariablesGroups() as $currentTeam)
|
||||
{
|
||||
$teamPerformance = $this->createOutputVariable($currentTeam);
|
||||
$this->addLayerFactor($this->createPlayerToTeamSumFactor($currentTeam, $teamPerformance));
|
||||
|
||||
// REVIEW: Does it make sense to have groups of one?
|
||||
$this->getOutputVariablesGroups() = $teamPerformance;
|
||||
}
|
||||
}
|
||||
|
||||
public function createPriorSchedule()
|
||||
{
|
||||
return $this->scheduleSequence(
|
||||
array_map(
|
||||
function($weightedSumFactor)
|
||||
{
|
||||
return new ScheduleStep("Perf to Team Perf Step", $weightedSumFactor, 0);
|
||||
},
|
||||
$this->getLocalFactors()),
|
||||
"all player perf to team perf schedule");
|
||||
}
|
||||
|
||||
protected function createPlayerToTeamSumFactor($teamMembers, $sumVariable)
|
||||
{
|
||||
return new GaussianWeightedSumFactor(
|
||||
$sumVariable,
|
||||
$teamMembers,
|
||||
array_map(
|
||||
function($v)
|
||||
{
|
||||
return PartialPlay::getPartialPlayPercentage($v->getKey());
|
||||
},
|
||||
$teamMembers));
|
||||
|
||||
}
|
||||
|
||||
public function createPosteriorSchedule()
|
||||
{
|
||||
// BLOG
|
||||
return $this->scheduleSequence(
|
||||
from currentFactor in LocalFactors
|
||||
from currentIteration in
|
||||
Enumerable.Range(1, currentFactor.NumberOfMessages - 1)
|
||||
select new ScheduleStep<GaussianDistribution>(
|
||||
"team sum perf @" + currentIteration,
|
||||
currentFactor,
|
||||
currentIteration),
|
||||
"all of the team's sum iterations");
|
||||
}
|
||||
|
||||
private function createOutputVariable($team)
|
||||
{
|
||||
$teamMemberNames = String.Join(", ", team.Select(teamMember => teamMember.Key.ToString()).ToArray());
|
||||
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team[{0}]'s performance", teamMemberNames);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
// We intentionally have no Posterior schedule since the only purpose here is to
|
||||
class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
private $_teams;
|
||||
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph, $teams)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
$this->_teams = $teams;
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
foreach ($this->_teams as $currentTeam)
|
||||
{
|
||||
$currentTeamSkills = array();
|
||||
|
||||
foreach ($currentTeam as $currentTeamPlayer)
|
||||
{
|
||||
$playerSkill = $this->createSkillOutputVariable($currentTeamPlayer.Key);
|
||||
$this->addLayerFactor($this->createPriorFactor($currentTeamPlayer.Key, $currentTeamPlayer.Value, $playerSkill));
|
||||
$currentTeamSkills[] = $playerSkill;
|
||||
}
|
||||
|
||||
OutputVariablesGroups.Add(currentTeamSkills);
|
||||
}
|
||||
}
|
||||
|
||||
public function createPriorSchedule()
|
||||
{
|
||||
return $this->scheduleSequence(
|
||||
array_map(
|
||||
function($prior)
|
||||
{
|
||||
return new ScheduleStep("Prior to Skill Step", $prior, 0);
|
||||
},
|
||||
$this->getLocalFactors()),
|
||||
"All priors");
|
||||
}
|
||||
|
||||
private function createPriorFactor($player, $priorRating, $skillsVariable)
|
||||
{
|
||||
return new GaussianPriorFactor($priorRating->getMean(),
|
||||
square($priorRating->getStandardDeviation()) +
|
||||
square($this->getParentFactorGraph()->getGameInfo()->getDynamicsFactor()),
|
||||
$skillsVariable);
|
||||
}
|
||||
|
||||
private function createSkillOutputVariable($key)
|
||||
{
|
||||
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key, "{0}'s skill", $key);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
foreach ($this->getInputVariablesGroups() as $currentTeam)
|
||||
{
|
||||
$currentTeamPlayerPerformances = array();
|
||||
|
||||
foreach ($currentTeam as $playerSkillVariable)
|
||||
{
|
||||
$playerPerformance = $this->createOutputVariable($playerSkillVariable->getKey());
|
||||
$this->addLayerFactor($this->createLikelihood($playerSkillVariable, $playerPerformance));
|
||||
$currentTeamPlayerPerformances[] = $playerPerformance;
|
||||
}
|
||||
|
||||
$this->getOutputVariablesGroups()[] = $currentTeamPlayerPerformances;
|
||||
}
|
||||
}
|
||||
|
||||
private function createLikelihood($playerSkill, $playerPerformance)
|
||||
{
|
||||
return new GaussianLikelihoodFactor(square($this->getParentFactorGraph()->getGameInfo()->getBeta()), $playerPerformance, $playerSkill);
|
||||
}
|
||||
|
||||
private function createOutputVariable($key)
|
||||
{
|
||||
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key, "{0}'s performance", $key);
|
||||
}
|
||||
|
||||
public function createPriorSchedule()
|
||||
{
|
||||
return $this->scheduleSequence(
|
||||
array_map(
|
||||
function($likelihood)
|
||||
{
|
||||
return $this->scheduleStep("Skill to Perf step", $likelihood, 0);
|
||||
},
|
||||
$this->getLocalFactors()),
|
||||
"All skill to performance sending");
|
||||
}
|
||||
|
||||
public function createPosteriorSchedule()
|
||||
{
|
||||
return $this->scheduleSequence(
|
||||
array_map(
|
||||
function($likelihood)
|
||||
{
|
||||
return new ScheduleStep("name", $likelihood, 1);
|
||||
},
|
||||
$this->getLocalFactors()),
|
||||
"All skill to performance sending");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
private $_epsilon;
|
||||
private $_teamRanks;
|
||||
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph, array $teamRanks)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
$this->_teamRanks = $teamRanks;
|
||||
$gameInfo = $this->getParentFactorGraph()->getGameInfo();
|
||||
$this->_epsilon = DrawMargin::getDrawMarginFromDrawProbability($gameInfo->getDrawProbability(), $gameInfo->getBeta());
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
$inputVarGroups = $this->getInputVariablesGroups();
|
||||
$inputVarGroupsCount = count($inputVarGroups);
|
||||
|
||||
for ($i = 0; $i < $inputVarGroupsCount; $i++)
|
||||
{
|
||||
$isDraw = ($this->_teamRanks[$i] == $this->_teamRanks[$i + 1]);
|
||||
$teamDifference = $inputVarGroups[$i][0];
|
||||
|
||||
$factor =
|
||||
$isDraw
|
||||
? new GaussianWithinFactor($this->_epsilon, $teamDifference)
|
||||
: new GaussianGreaterThanFactor($this->_epsilon, $teamDifference);
|
||||
|
||||
$this->addLayerFactor($factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorGraphLayer
|
||||
{
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
}
|
||||
|
||||
public function buildLayer()
|
||||
{
|
||||
$inputVariablesGroup = $this->getInputVariablesGroups();
|
||||
$inputVariablesGroupCount = count($inputVariablesGroup);
|
||||
|
||||
for ($i = 0; $i < $inputVariablesGroupCount - 1; $i++)
|
||||
{
|
||||
$strongerTeam = $inputVariablesGroups[$i][0];
|
||||
$weakerTeam = $inputVariablesGroups[$i + 1][0];
|
||||
|
||||
$currentDifference = $this->createOutputVariable();
|
||||
$this->addLayerFactor($this->createTeamPerformanceToDifferenceFactor($strongerTeam, $weakerTeam, currentDifference));
|
||||
|
||||
// REVIEW: Does it make sense to have groups of one?
|
||||
$this->getOutputVariablesGroups()[] = $currentDifference;
|
||||
}
|
||||
}
|
||||
|
||||
private function createTeamPerformanceToDifferenceFactor(
|
||||
Variable $strongerTeam, Variable $weakerTeam, Variable $output)
|
||||
{
|
||||
return new GaussianWeightedSumFactor($output, array($strongerTeam, $weakerTeam), array(1.0, -1.0));
|
||||
}
|
||||
|
||||
private function createOutputVariable()
|
||||
{
|
||||
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable("Team performance difference");
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
13
PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php
Normal file
13
PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Moserware\Skills\TrueSkill\Layers;
|
||||
|
||||
abstract class TrueSkillFactorGraphLayer extends FactorGraphLayer
|
||||
{
|
||||
public function __construct(TrueSkillFactorGraph $parentGraph)
|
||||
{
|
||||
parent::__construct($parentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
Reference in New Issue
Block a user