mirror of
https://github.com/furyfire/trueskill.git
synced 2025-03-30 04:34:16 +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:
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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
Reference in New Issue
Block a user