scikit-learn/sklearn/decomposition/factor_analysis.py

235 lines
7.4 KiB
Python
Raw Normal View History

"""Factor Analysis.
A latent linear variable model, similar to ProbabilisticPCA.
This implementation is based on David Barber's Book,
Bayesian Reasoning and Machine Learning,
http://www.cs.ucl.ac.uk/staff/d.barber/brml,
Algorithm 21.1
"""
# Author: Christian Osendorfer <osendorf@gmail.com>
2012-09-13 22:51:22 +08:00
# Alexandre Gramfort <alexandre.gramfort@inria.fr>
# Licence: BSD3
2012-09-13 22:51:22 +08:00
from math import sqrt
import numpy as np
from scipy import linalg
2012-10-12 21:06:39 +08:00
from ..base import BaseEstimator, TransformerMixin
2012-09-13 22:51:22 +08:00
from ..utils import array2d, check_arrays
2012-10-12 21:06:39 +08:00
from ..utils.extmath import fast_logdet
2012-09-13 22:51:22 +08:00
class FactorAnalysis(BaseEstimator, TransformerMixin):
"""Factor Analysis (FA)
2012-09-13 22:51:22 +08:00
A simple linear generative model with Gaussian latent variables.
The observations are assumed to be caused by a linear transformation of
lower dimensional latent factors and added Gaussian noise.
2012-09-13 22:51:22 +08:00
Without loss of generality the factors are distributed according to a
Gaussian with zero mean and unit covariance. The noise is also zero mean
and has an arbitrary diagonal covariance matrix.
2012-09-13 22:51:22 +08:00
If we would restrict the model further, by assuming that the Gaussian
noise is even isotropic (all diagonal entries are the same) we would obtain
:class:`PPCA`.
FactorAnalysis performs a maximum likelihood estimate of the so-called
`loading` matrix, the transformation of the latent variables to the
2012-09-13 22:51:22 +08:00
observed ones, using expectation-maximization (EM).
Parameters
----------
2012-09-13 22:51:22 +08:00
n_components : int | None
Dimensionality of latent space, the number of components
of ``X`` that are obtained after ``transform``.
2012-09-13 22:51:22 +08:00
If None, n_components is set to the number of features.
tol : float
Stopping tolerance for EM algorithm.
copy : bool
Whether to make a copy of X. If ``False``, the input X gets overwritten
during fitting.
2012-09-13 22:51:22 +08:00
max_iter : int
Maximum number of iterations.
2012-09-13 22:51:22 +08:00
verbose : int | bool
Print verbose output.
noise_variance_init : None | array, shape=(n_features,)
The initial guess of the noise variance for each feature.
If None, it defaults to np.ones(n_features)
2012-09-13 22:51:22 +08:00
Attributes
----------
`components_` : array, [n_components, n_features]
Components with maximum variance.
`loglike_` : list, [n_iterations]
The log likelihood at each iteration.
`noise_variance_` : array, shape=(n_features,)
The estimated noise variance for each feature.
References
----------
.. David Barber, Bayesian Reasoning and Machine Learning,
Algorithm 21.1
2012-09-15 22:29:08 +08:00
.. Christopher M. Bishop: Pattern Recognition and Machine Learning,
Chapter 12.2.4
See also
--------
PCA: Principal component analysis, a simliar non-probabilistic
model model that can be computed in closed form.
2012-09-13 22:51:22 +08:00
ProbabilisticPCA: probabilistic PCA.
FastICA: Independent component analysis, a latent variable model with
non-Gaussian latent variables.
"""
2012-09-13 22:51:22 +08:00
def __init__(self, n_components=None, tol=1e-2, copy=True, max_iter=1000,
verbose=0, noise_variance_init=None):
self.n_components = n_components
self.copy = copy
self.tol = tol
self.max_iter = max_iter
self.verbose = verbose
self.noise_variance_init = noise_variance_init
def fit(self, X, y=None):
2012-09-13 22:51:22 +08:00
"""Fit the FactorAnalysis model to X using EM
Parameters
----------
X : array-like, shape (n_samples, n_features)
Training data.
Returns
-------
self
"""
2012-09-13 22:51:22 +08:00
X = array2d(check_arrays(X, copy=self.copy, sparse_format='dense',
dtype=np.float)[0])
n_samples, n_features = X.shape
2012-09-13 22:51:22 +08:00
n_components = self.n_components
if n_components is None:
n_components = n_features
self.mean_ = np.mean(X, axis=0)
X -= self.mean_
# some constant terms
2012-09-13 22:51:22 +08:00
nsqrt = sqrt(n_samples)
llconst = n_features * np.log(2 * np.pi) + n_components
var = np.var(X, axis=0)
if self.noise_variance_init is None:
psi = np.ones(n_features, dtype=X.dtype)
else:
if len(self.noise_variance_init) != n_features:
raise ValueError("noise_variance_init dimension does not "
"with number of featueres : %d != %d" %
(len(self.noise_variance_init), n_features))
psi = np.array(self.noise_variance_init)
loglike = []
old_ll = -np.inf
SMALL = 1e-12
for i in xrange(self.max_iter):
# SMALL helps numerics
sqrt_psi = np.sqrt(psi) + SMALL
2012-09-13 22:51:22 +08:00
Xtilde = X / (sqrt_psi * nsqrt)
_, s, V = linalg.svd(Xtilde, full_matrices=False)
V = V[:n_components]
s **= 2
# Use 'maximum' here to avoid sqrt problems.
W = np.sqrt(np.maximum(s[:n_components] - 1, 0))[:, np.newaxis] * V
W *= sqrt_psi
# loglikelihood
2012-09-13 22:51:22 +08:00
ll = llconst + np.sum(np.log(s[:n_components]))
ll += np.sum(s[n_components:]) + np.sum(np.log(psi))
ll *= -n_samples / 2.
loglike.append(ll)
if (ll - old_ll) < self.tol:
break
old_ll = ll
psi = np.maximum(var - np.sum(W ** 2, axis=0), SMALL)
2012-09-13 22:51:22 +08:00
else:
if self.verbose:
print "Did not converge"
self.components_ = W
self.noise_variance_ = psi
2012-09-13 22:51:22 +08:00
self.loglike_ = loglike
return self
def transform(self, X):
"""Apply dimensionality reduction to X using the model.
2012-10-11 20:04:16 +08:00
Compute the expected mean of the latent variables.
See Barber, 21.2.33 (or Bishop, 12.66).
Parameters
----------
X : array-like, shape (n_samples, n_features)
Training data.
Returns
-------
X_new : array-like, shape (n_samples, n_components)
The latent variables of X.
"""
2012-09-13 22:51:22 +08:00
Ih = np.eye(len(self.components_))
X_transformed = X - self.mean_
Wpsi = self.components_ / self.noise_variance_
2012-09-13 22:51:22 +08:00
cov_z = linalg.inv(Ih + np.dot(Wpsi, self.components_.T))
tmp = np.dot(X_transformed, Wpsi.T)
X_transformed = np.dot(tmp, cov_z)
return X_transformed
2012-09-13 22:51:22 +08:00
def get_covariance(self):
2012-10-12 05:01:12 +08:00
"""Compute data covariance with the FactorAnalysis model.
2012-09-13 22:51:22 +08:00
cov = components_.T * components_ + diag(noise_variance)
Returns
-------
cov : array, shape=(n_features, n_features)
2012-10-12 05:01:12 +08:00
Estimated covariance of data.
2012-09-13 22:51:22 +08:00
"""
cov = np.dot(self.components_.T, self.components_) \
+ np.diag(self.noise_variance_)
2012-09-13 22:51:22 +08:00
return cov
2012-10-12 05:01:12 +08:00
def score(self, X):
"""Compute score of X under FactorAnalysis model.
2012-10-12 21:06:39 +08:00
Parameters
----------
X: array of shape(n_samples, n_features)
The data to test
2012-10-12 05:01:12 +08:00
2012-10-12 21:06:39 +08:00
Returns
-------
ll: array of shape (n_samples),
log-likelihood of each row of X under the current model
"""
Xr = X - self.mean_
cov = self.get_covariance()
n_features = X.shape[1]
log_like = np.zeros(X.shape[0])
self.precision_ = linalg.inv(cov)
log_like = -.5 * (Xr * (np.dot(Xr, self.precision_))).sum(axis=1)
log_like -= .5 * (fast_logdet(cov) + \
n_features * np.log(2 * np.pi))
return log_like