/*
* File: FactorizationMachine.java
* Authors: Justin Basilico
* Project: Cognitive Foundry
*
* Copyright 2013 Cognitive Foundry. All rights reserved.
*/
package gov.sandia.cognition.learning.algorithm.factor.machine;
import gov.sandia.cognition.annotation.PublicationReference;
import gov.sandia.cognition.annotation.PublicationReferences;
import gov.sandia.cognition.annotation.PublicationType;
import gov.sandia.cognition.learning.algorithm.gradient.ParameterGradientEvaluator;
import gov.sandia.cognition.learning.function.regression.AbstractRegressor;
import gov.sandia.cognition.math.matrix.Matrix;
import gov.sandia.cognition.math.matrix.MatrixFactory;
import gov.sandia.cognition.math.matrix.Vector;
import gov.sandia.cognition.math.matrix.VectorEntry;
import gov.sandia.cognition.math.matrix.VectorFactory;
import gov.sandia.cognition.math.matrix.VectorInputEvaluator;
import gov.sandia.cognition.util.ObjectUtil;
/**
* Implements a Factorization Machine. It implements a model of pairwise
* interactions between features by a reduced-rank approximation. It also
* includes a standard linear term and a bias term.
*
* The model is of the form:
* f(x) = b + w * x + \sum_{i=1}^{d} \sum_{j=i+1}^{d} x_i * x_j * v_i * v_j
* where b is the bias, w is the d-dimensional weight vector and v_i are
* k-dimensional factor vectors for the pairwise components.
*
* Factorization Machines allow for different types of Matrix Factorization
* algorithms to be implemented as feature encodings used as input to a
* Factorization Machine. As such, it unifies a family of algorithms that are
* traditionally used in Recommendation Systems under an interface that looks
* like a traditional Machine Learning algorithm.
*
* @author Justin Basilico
* @since 3.4.0
*/
@PublicationReferences(references={
@PublicationReference(
title="Factorization Machines",
author={"Steffen Rendle"},
year=2010,
type=PublicationType.Conference,
publication="Proceedings of the 10th IEEE International Conference on Data Mining (ICDM)",
url="http://www.inf.uni-konstanz.de/~rendle/pdf/Rendle2010FM.pdf"),
@PublicationReference(
title="Factorization Machines with libFM",
author="Steffen Rendle",
year=2012,
type=PublicationType.Journal,
publication="ACM Transactions on Intelligent Systems Technology",
url="http://www.csie.ntu.edu.tw/~b97053/paper/Factorization%20Machines%20with%20libFM.pdf")
})
public class FactorizationMachine
extends AbstractRegressor<Vector>
implements VectorInputEvaluator<Vector, Double>,
ParameterGradientEvaluator<Vector, Double, Vector>
{
/** The bias term (b). */
protected double bias;
/** The weight vector (w) for each dimension. */
protected Vector weights;
/** The k x d factor matrix (v) with k factors for each dimension.
* May be null. */
protected Matrix factors;
/**
* Creates a new, empty {@link FactorizationMachine}. It is initialized
* with a bias of zero and no weight or factors.
*/
public FactorizationMachine()
{
this(0.0, null, null);
}
/**
* Creates a new, empty {@link FactorizationMachine} of the given
* input dimensionality (d) and factor count (k). It initializes the
* internal d-dimensional weight vector and k-by-d factor matrix based on
* these sizes. All values are initialized to 0.
*
* @param dimensionality
* The input dimensionality (d). Cannot be negative.
* @param factorCount
* The number of factors for pairwise interactions (k). Cannot be
* negative.
*/
public FactorizationMachine(
final int dimensionality,
final int factorCount)
{
this(0.0, VectorFactory.getDenseDefault().createVector(dimensionality),
MatrixFactory.getDenseDefault().createMatrix(
factorCount, dimensionality));
}
/**
* Creates a new {@link FactorizationMachine} with the given parameters.
*
* @param bias
* The bias value.
* @param weights
* The weight vector of dimensionality d. May be null.
* @param factors
* The k-by-d pairwise factor matrix. May be null.
*/
public FactorizationMachine(
final double bias,
final Vector weights,
final Matrix factors)
{
super();
this.setBias(bias);
this.setWeights(weights);
this.setFactors(factors);
}
@Override
public FactorizationMachine clone()
{
final FactorizationMachine clone = (FactorizationMachine) super.clone();
clone.weights = ObjectUtil.cloneSafe(this.weights);
clone.factors = ObjectUtil.cloneSafe(this.factors);
return clone;
}
@Override
public double evaluateAsDouble(
final Vector input)
{
double result = this.bias;
if (this.weights != null)
{
result += input.dot(this.weights);
}
// else - No weights.
if (this.factors != null)
{
// We loop over k to do the performance improvement trick that
// allows O(kd) computation instead of O(kd^2).
final int factorCount = this.getFactorCount();
for (int k = 0; k < factorCount; k++)
{
double sum = 0.0;
double sumSquares = 0.0;
for (final VectorEntry entry : input)
{
final double product = entry.getValue()
* this.factors.getElement(k, entry.getIndex());
sum += product;
sumSquares += product * product;
}
result += 0.5 * (sum * sum - sumSquares);
}
}
// else - No factors.
return result;
}
@Override
public int getInputDimensionality()
{
if (this.weights != null)
{
return this.weights.getDimensionality();
}
else if (this.factors != null)
{
return this.factors.getNumColumns();
}
else
{
// No input.
return 0;
}
}
/**
* Gets the number of factors in the model.
*
* @return
* The number of factors. Cannot be negative.
*/
public int getFactorCount()
{
return this.factors == null ? 0 : this.factors.getNumRows();
}
@Override
public Vector computeParameterGradient(
final Vector input)
{
final int d = this.getInputDimensionality();
input.assertDimensionalityEquals(d);
final Vector gradient = VectorFactory.getSparseDefault().createVector(
this.getParameterCount());
// The gradient for the bias is 1.
gradient.setElement(0, 1.0);
int offset = 1;
if (this.hasWeights())
{
// The gradients for the linear terms are just the values from the
// input.
for (final VectorEntry entry : input)
{
gradient.setElement(offset + entry.getIndex(), entry.getValue());
}
offset += d;
}
if (this.hasFactors())
{
// Compute the gradients per factor.
final int factorCount = this.getFactorCount();
for (int k = 0; k < factorCount; k++)
{
double sum = 0.0;
for (final VectorEntry entry : input)
{
sum += entry.getValue() * this.factors.getElement(k,
entry.getIndex());
}
for (final VectorEntry entry : input)
{
final int index = entry.getIndex();
final double value = entry.getValue();
final double factorElement = this.factors.getElement(k, index);
gradient.setElement(offset + index,
value * (sum - value * factorElement));
}
offset += d;
}
}
return gradient;
}
@Override
public Vector convertToVector()
{
final int d = this.getInputDimensionality();
final Vector result = VectorFactory.getSparseDefault().createVector(
this.getParameterCount());
result.setElement(0, this.bias);
int offset = 1;
if (this.hasWeights())
{
// Sparse iteration.
for (final VectorEntry entry : this.weights)
{
result.setElement(offset + entry.getIndex(), entry.getValue());
}
offset += d;
}
if (this.hasFactors())
{
// Stack factors as sparse row-wise.
final int factorCount = this.getFactorCount();
for (int k = 0; k < factorCount; k++)
{
// Sparse iteration.
for (final VectorEntry entry : this.factors.getRow(k))
{
result.setElement(offset + entry.getIndex(), entry.getValue());
}
offset += d;
}
}
return result;
}
@Override
public void convertFromVector(
final Vector parameters)
{
parameters.assertDimensionalityEquals(this.getParameterCount());
final int d = this.getInputDimensionality();
// Get the bias.
this.setBias(parameters.getElement(0));
int offset = 1;
if (this.hasWeights())
{
// Set the weights.
this.setWeights(parameters.subVector(offset, offset + d - 1));
offset += d;
}
if (this.hasFactors())
{
final int factorCount = this.getFactorCount();
// Extract the factors for each row.
for (int k = 0; k < factorCount; k++)
{
this.factors.setRow(k,
parameters.subVector(offset, offset + d - 1));
offset += d;
}
}
}
/**
* Gets the number of parameters for this factorization machine. This is
* the size of the parameter vector returned by convertToVector(). This
* is not the number of factors (which is getFactorCount()) or the
* size of the input dimensionality (which is getInputDimensionality()).
*
* @return
* The number of parameters representing this factorization machine.
* It is 1 plus the size of the weight vector (if there is one)
* plus the size of the factors matrix (if there is one).
*/
public int getParameterCount()
{
final int d = this.getInputDimensionality();
int size = 1;
if (this.hasWeights())
{
size += d;
}
if (this.hasFactors())
{
size += d * this.getFactorCount();
}
return size;
}
/**
* Determines if this Factorization Machine has a linear weight term.
*
* @return
* True if this has a linear weight term; otherwise, false.
*/
public boolean hasWeights()
{
return this.weights != null;
}
/**
* Determines if this Factorization Machine has pairwise factor terms.
* Without pairwise factor terms, this becomes a linear model.
*
* @return
* True if this has pairwise factor terms; otherwise, false.
*/
public boolean hasFactors()
{
return this.factors != null && this.factors.getNumRows() > 0;
}
/**
* Gets the bias value.
*
* @return
* The bias value (b) of the model.
*/
public double getBias()
{
return this.bias;
}
/**
* Sets the bias value.
*
* @param bias
* The bias value (b) of the model.
*/
public void setBias(
final double bias)
{
this.bias = bias;
}
/**
* Gets the weight vector. It represents the linear term in the model
* equation.
*
* @return
* The weight vector. May be null.
*/
public Vector getWeights()
{
return this.weights;
}
/**
* Sets the weight vector. It represents the linear term in the model
* equation.
*
* @param weights
* The weight vector. May be null.
*/
public void setWeights(
final Vector weights)
{
this.weights = weights;
}
/**
* Gets the matrix of factors. It represents the pairwise terms in the
* model.
*
* @return
* The matrix of pairwise factors. May be null.
*/
public Matrix getFactors()
{
return this.factors;
}
/**
* Sets the matrix of factors. It represents the pairwise terms in the
* model.
*
* @param factors
* The matrix of pairwise factors. May be null.
*/
public void setFactors(
final Matrix factors)
{
this.factors = factors;
}
}