scikit-learn/sklearn/decomposition/nmf.py

587 lines
19 KiB
Python
Raw Normal View History

2010-12-01 00:16:48 +08:00
""" Non-negative matrix factorization
"""
2011-04-03 01:07:05 +08:00
# Author: Vlad Niculae
2011-10-04 01:51:18 +08:00
# Lars Buitinck <L.J.Buitinck@uva.nl>
2011-04-03 01:07:05 +08:00
# Author: Chih-Jen Lin, National Taiwan University (original projected gradient
# NMF implementation)
# Author: Anthony Di Franco (original Python and NumPy port)
# License: BSD 3 clause
2010-12-01 00:16:48 +08:00
2011-03-26 23:25:05 +08:00
from __future__ import division
2010-12-01 00:16:48 +08:00
from math import sqrt
import warnings
2011-10-03 22:33:02 +08:00
import numpy as np
import scipy.sparse as sp
2013-07-27 21:55:18 +08:00
from scipy.optimize import nnls
from ..base import BaseEstimator, TransformerMixin
from ..utils import check_random_state, check_array
2014-04-08 05:18:28 +08:00
from ..utils.extmath import randomized_svd, safe_sparse_dot, squared_norm
from ..utils.validation import check_is_fitted
2012-11-02 20:57:22 +08:00
def safe_vstack(Xs):
2012-11-09 18:51:39 +08:00
if any(sp.issparse(X) for X in Xs):
2012-11-02 20:57:22 +08:00
return sp.vstack(Xs)
else:
return np.vstack(Xs)
def norm(x):
"""Dot product-based Euclidean norm implementation
See: http://fseoane.net/blog/2011/computing-the-vector-norm/
"""
2014-04-08 05:18:28 +08:00
return sqrt(squared_norm(x))
2010-12-01 00:16:48 +08:00
def trace_dot(X, Y):
"""Trace of np.dot(X, Y.T)."""
return np.dot(X.ravel(), Y.ravel())
2011-04-10 17:38:20 +08:00
def _sparseness(x):
"""Hoyer's measure of sparsity for a vector"""
sqrt_n = np.sqrt(len(x))
return (sqrt_n - np.linalg.norm(x, 1) / norm(x)) / (sqrt_n - 1)
2011-10-03 22:33:02 +08:00
def check_non_negative(X, whom):
X = X.data if sp.issparse(X) else X
if (X < 0).any():
raise ValueError("Negative values in data passed to %s" % whom)
2011-10-03 23:16:19 +08:00
def _initialize_nmf(X, n_components, variant=None, eps=1e-6,
random_state=None):
"""NNDSVD algorithm for NMF initialization.
2010-12-01 00:16:48 +08:00
Computes a good initial guess for the non-negative
rank k matrix approximation for X: X = WH
2010-12-01 00:16:48 +08:00
Parameters
----------
2013-03-11 01:33:21 +08:00
X : array, [n_samples, n_features]
2010-12-01 00:16:48 +08:00
The data matrix to be decomposed.
2013-03-11 01:33:21 +08:00
n_components : array, [n_components, n_features]
2013-01-08 22:29:33 +08:00
The number of components desired in the approximation.
2013-03-11 01:33:21 +08:00
variant : None | 'a' | 'ar'
The variant of the NNDSVD algorithm.
Accepts None, 'a', 'ar'
None: leaves the zero entries as zero
'a': Fills the zero entries with the average of X
'ar': Fills the zero entries with standard normal random variates.
Default: None
2013-01-08 22:29:33 +08:00
eps: float
Truncate all values less then this in output to zero.
2013-03-11 01:33:21 +08:00
random_state : numpy.RandomState | int, optional
The generator used to fill in the zeros, when using variant='ar'
Default: numpy.random
2010-12-01 00:16:48 +08:00
Returns
-------
2013-03-11 01:33:21 +08:00
(W, H) :
2010-12-01 00:16:48 +08:00
Initial guesses for solving X ~= WH such that
the number of columns in W is n_components.
References
----------
C. Boutsidis, E. Gallopoulos: SVD based initialization: A head start for
nonnegative matrix factorization - Pattern Recognition, 2008
2013-07-10 16:42:23 +08:00
http://tinyurl.com/nndsvd
2010-12-01 00:16:48 +08:00
"""
2011-10-03 22:33:02 +08:00
check_non_negative(X, "NMF initialization")
if variant not in (None, 'a', 'ar'):
raise ValueError("Invalid variant name")
U, S, V = randomized_svd(X, n_components)
2010-12-01 00:16:48 +08:00
W, H = np.zeros(U.shape), np.zeros(V.shape)
# The leading singular triplet is non-negative
# so it can be used as is for initialization.
W[:, 0] = np.sqrt(S[0]) * np.abs(U[:, 0])
H[0, :] = np.sqrt(S[0]) * np.abs(V[0, :])
for j in range(1, n_components):
2010-12-01 00:16:48 +08:00
x, y = U[:, j], V[j, :]
2010-12-01 00:16:48 +08:00
# extract positive and negative parts of column vectors
x_p, y_p = np.maximum(x, 0), np.maximum(y, 0)
2013-04-09 22:37:21 +08:00
x_n, y_n = np.abs(np.minimum(x, 0)), np.abs(np.minimum(y, 0))
2010-12-01 00:16:48 +08:00
# and their norms
x_p_nrm, y_p_nrm = norm(x_p), norm(y_p)
x_n_nrm, y_n_nrm = norm(x_n), norm(y_n)
2010-12-01 00:16:48 +08:00
m_p, m_n = x_p_nrm * y_p_nrm, x_n_nrm * y_n_nrm
2010-12-01 00:16:48 +08:00
# choose update
if m_p > m_n:
u = x_p / x_p_nrm
v = y_p / y_p_nrm
sigma = m_p
else:
u = x_n / x_n_nrm
v = y_n / y_n_nrm
sigma = m_n
2010-12-01 00:16:48 +08:00
lbd = np.sqrt(S[j] * sigma)
W[:, j] = lbd * u
H[j, :] = lbd * v
W[W < eps] = 0
H[H < eps] = 0
if variant == "a":
avg = X.mean()
W[W == 0] = avg
H[H == 0] = avg
elif variant == "ar":
random_state = check_random_state(random_state)
avg = X.mean()
2011-05-04 15:45:16 +08:00
W[W == 0] = abs(avg * random_state.randn(len(W[W == 0])) / 100)
H[H == 0] = abs(avg * random_state.randn(len(H[H == 0])) / 100)
2010-12-01 00:16:48 +08:00
return W, H
def _nls_subproblem(V, W, H, tol, max_iter, sigma=0.01, beta=0.1):
"""Non-negative least square solver
2010-12-01 00:16:48 +08:00
Solves a non-negative least squares subproblem using the
projected gradient descent algorithm.
min || WH - V ||_2
2010-12-01 00:16:48 +08:00
Parameters
----------
2012-11-02 20:57:22 +08:00
V, W : array-like
Constant matrices.
H : array-like
2012-11-02 20:57:22 +08:00
Initial guess for the solution.
2012-11-02 20:57:22 +08:00
tol : float
2010-12-01 00:16:48 +08:00
Tolerance of the stopping condition.
2012-11-02 20:57:22 +08:00
max_iter : int
Maximum number of iterations before timing out.
2010-12-01 00:16:48 +08:00
sigma : float
Constant used in the sufficient decrease condition checked by the line
search. Smaller values lead to a looser sufficient decrease condition,
thus reducing the time taken by the line search, but potentially
2013-05-02 21:46:25 +08:00
increasing the number of iterations of the projected gradient
procedure. 0.01 is a commonly used value in the optimization
literature.
beta : float
Factor by which the step size is decreased (resp. increased) until
(resp. as long as) the sufficient decrease condition is satisfied.
Larger values allow to find a better step size but lead to longer line
search. 0.1 is a commonly used value in the optimization literature.
2010-12-01 00:16:48 +08:00
Returns
-------
2012-11-02 20:57:22 +08:00
H : array-like
Solution to the non-negative least squares problem.
2012-11-02 20:57:22 +08:00
grad : array-like
2010-12-01 00:16:48 +08:00
The gradient.
2012-11-02 20:57:22 +08:00
n_iter : int
2010-12-01 00:16:48 +08:00
The number of iterations done by the algorithm.
References
----------
C.-J. Lin. Projected gradient methods for non-negative matrix factorization.
Neural Computation, 19(2007), 2756-2779.
http://www.csie.ntu.edu.tw/~cjlin/nmf/
2010-12-01 00:16:48 +08:00
"""
WtV = safe_sparse_dot(W.T, V)
WtW = np.dot(W.T, W)
2010-12-01 00:16:48 +08:00
# values justified in the paper
alpha = 1
for n_iter in range(1, max_iter + 1):
2010-12-01 00:16:48 +08:00
grad = np.dot(WtW, H) - WtV
# The following multiplication with a boolean array is more than twice
# as fast as indexing into grad.
if norm(grad * np.logical_or(grad < 0, H > 0)) < tol:
2010-12-01 00:16:48 +08:00
break
Hp = H
for inner_iter in range(19):
# Gradient step.
Hn = H - alpha * grad
# Projection step.
Hn *= Hn > 0
2010-12-01 00:16:48 +08:00
d = Hn - H
gradd = np.dot(grad.ravel(), d.ravel())
dQd = np.dot(np.dot(WtW, d).ravel(), d.ravel())
suff_decr = (1 - sigma) * gradd + 0.5 * dQd < 0
if inner_iter == 0:
2010-12-01 00:16:48 +08:00
decr_alpha = not suff_decr
if decr_alpha:
2010-12-01 00:16:48 +08:00
if suff_decr:
H = Hn
break
else:
2011-10-04 01:51:18 +08:00
alpha *= beta
elif not suff_decr or (Hp == Hn).all():
H = Hp
break
2010-12-01 00:16:48 +08:00
else:
2011-10-04 01:51:18 +08:00
alpha /= beta
Hp = Hn
2011-04-02 00:08:20 +08:00
if n_iter == max_iter:
warnings.warn("Iteration limit reached in nls subproblem.")
2011-04-02 00:08:20 +08:00
return H, grad, n_iter
2010-12-01 00:16:48 +08:00
2011-04-03 00:54:52 +08:00
class ProjectedGradientNMF(BaseEstimator, TransformerMixin):
"""Non-Negative matrix factorization by Projected Gradient (NMF)
2010-12-01 00:16:48 +08:00
Parameters
----------
2013-03-11 06:05:14 +08:00
n_components : int or None
2011-07-17 06:42:51 +08:00
Number of components, if n_components is not set all components
are kept
2013-03-11 01:33:21 +08:00
init : 'nndsvd' | 'nndsvda' | 'nndsvdar' | 'random'
Method used to initialize the procedure.
Default: 'nndsvdar' if n_components < n_features, otherwise random.
2011-07-17 06:42:51 +08:00
Valid options::
'nndsvd': Nonnegative Double Singular Value Decomposition (NNDSVD)
initialization (better for sparseness)
'nndsvda': NNDSVD with zeros filled with the average of X
(better when sparsity is not desired)
2011-04-02 21:54:02 +08:00
'nndsvdar': NNDSVD with zeros filled with small random values
(generally faster, less accurate alternative to NNDSVDa
for when sparsity is not desired)
'random': non-negative random matrices
2013-03-11 01:33:21 +08:00
sparseness : 'data' | 'components' | None, default: None
2011-04-02 21:54:02 +08:00
Where to enforce sparsity in the model.
2013-03-11 01:33:21 +08:00
beta : double, default: 1
2011-04-02 20:51:03 +08:00
Degree of sparseness, if sparseness is not None. Larger values mean
2011-04-02 21:54:02 +08:00
more sparseness.
2013-03-11 01:33:21 +08:00
eta : double, default: 0.1
2013-06-27 21:09:16 +08:00
Degree of correctness to maintain, if sparsity is not None. Smaller
2011-04-02 20:51:03 +08:00
values mean larger error.
2013-03-11 01:33:21 +08:00
tol : double, default: 1e-4
Tolerance value used in stopping conditions.
2013-03-11 01:33:21 +08:00
max_iter : int, default: 200
Number of iterations to compute.
2013-03-11 01:33:21 +08:00
nls_max_iter : int, default: 2000
2011-04-02 21:54:02 +08:00
Number of iterations in NLS subproblem.
random_state : int or RandomState
Random number generator seed control.
Attributes
----------
components_ : array, [n_components, n_features]
2013-03-11 01:33:21 +08:00
Non-negative components of the data.
2011-07-17 06:42:51 +08:00
reconstruction_err_ : number
Frobenius norm of the matrix difference between
the training data and the reconstructed data from
the fit produced by the model. ``|| X - WH ||_2``
n_iter_ : int
Number of iterations run.
2010-12-01 00:16:48 +08:00
Examples
--------
>>> import numpy as np
>>> X = np.array([[1,1], [2, 1], [3, 1.2], [4, 1], [5, 0.8], [6, 1]])
>>> from sklearn.decomposition import ProjectedGradientNMF
>>> model = ProjectedGradientNMF(n_components=2, init='random',
... random_state=0)
2011-08-25 01:23:44 +08:00
>>> model.fit(X) #doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
ProjectedGradientNMF(beta=1, eta=0.1, init='random', max_iter=200,
n_components=2, nls_max_iter=2000, random_state=0, sparseness=None,
tol=0.0001)
>>> model.components_
array([[ 0.77032744, 0.11118662],
[ 0.38526873, 0.38228063]])
>>> model.reconstruction_err_ #doctest: +ELLIPSIS
0.00746...
>>> model = ProjectedGradientNMF(n_components=2,
... sparseness='components', init='random', random_state=0)
2011-08-25 01:23:44 +08:00
>>> model.fit(X) #doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
ProjectedGradientNMF(beta=1, eta=0.1, init='random', max_iter=200,
n_components=2, nls_max_iter=2000, random_state=0,
sparseness='components', tol=0.0001)
>>> model.components_
array([[ 1.67481991, 0.29614922],
2013-04-09 23:32:16 +08:00
[ 0. , 0.4681982 ]])
>>> model.reconstruction_err_ #doctest: +ELLIPSIS
0.513...
References
----------
2011-12-27 06:09:36 +08:00
This implements
C.-J. Lin. Projected gradient methods
for non-negative matrix factorization. Neural
2010-12-01 00:16:48 +08:00
Computation, 19(2007), 2756-2779.
http://www.csie.ntu.edu.tw/~cjlin/nmf/
2011-12-27 06:09:36 +08:00
P. Hoyer. Non-negative Matrix Factorization with
Sparseness Constraints. Journal of Machine Learning
Research 2004.
2011-04-02 21:54:02 +08:00
NNDSVD is introduced in
2011-12-27 06:09:36 +08:00
2011-04-02 21:54:02 +08:00
C. Boutsidis, E. Gallopoulos: SVD based
initialization: A head start for nonnegative
matrix factorization - Pattern Recognition, 2008
2013-07-10 16:42:23 +08:00
http://tinyurl.com/nndsvd
2010-12-01 00:16:48 +08:00
"""
def __init__(self, n_components=None, init=None, sparseness=None, beta=1,
2012-12-22 20:02:50 +08:00
eta=0.1, tol=1e-4, max_iter=200, nls_max_iter=2000,
random_state=None):
self.n_components = n_components
2011-04-02 00:08:20 +08:00
self.init = init
self.tol = tol
if sparseness not in (None, 'data', 'components'):
raise ValueError(
'Invalid sparseness parameter: got %r instead of one of %r' %
(sparseness, (None, 'data', 'components')))
self.sparseness = sparseness
self.beta = beta
self.eta = eta
self.max_iter = max_iter
self.nls_max_iter = nls_max_iter
self.random_state = random_state
2011-12-27 06:25:35 +08:00
def _init(self, X):
n_samples, n_features = X.shape
init = self.init
if init is None:
if self.n_components_ < n_features:
init = 'nndsvd'
else:
init = 'random'
2014-03-12 20:22:00 +08:00
random_state = self.random_state
2011-12-27 06:25:35 +08:00
if init == 'nndsvd':
W, H = _initialize_nmf(X, self.n_components_)
elif init == 'nndsvda':
W, H = _initialize_nmf(X, self.n_components_, variant='a')
elif init == 'nndsvdar':
W, H = _initialize_nmf(X, self.n_components_, variant='ar')
elif init == "random":
rng = check_random_state(random_state)
W = rng.randn(n_samples, self.n_components_)
# we do not write np.abs(W, out=W) to stay compatible with
# numpy 1.5 and earlier where the 'out' keyword is not
# supported as a kwarg on ufuncs
np.abs(W, W)
H = rng.randn(self.n_components_, n_features)
np.abs(H, H)
2011-12-27 06:25:35 +08:00
else:
raise ValueError(
'Invalid init parameter: got %r instead of one of %r' %
(init, (None, 'nndsvd', 'nndsvda', 'nndsvdar', 'random')))
2011-12-27 06:25:35 +08:00
return W, H
def _update_W(self, X, H, W, tolW):
n_samples, n_features = X.shape
2012-12-22 20:02:50 +08:00
if self.sparseness is None:
2011-12-27 06:25:35 +08:00
W, gradW, iterW = _nls_subproblem(X.T, H.T, W.T, tolW,
self.nls_max_iter)
elif self.sparseness == 'data':
W, gradW, iterW = _nls_subproblem(
2012-12-22 20:02:50 +08:00
safe_vstack([X.T, np.zeros((1, n_samples))]),
safe_vstack([H.T, np.sqrt(self.beta) * np.ones((1,
self.n_components_))]),
2012-12-22 20:02:50 +08:00
W.T, tolW, self.nls_max_iter)
2011-12-27 06:25:35 +08:00
elif self.sparseness == 'components':
W, gradW, iterW = _nls_subproblem(
2012-12-22 20:02:50 +08:00
safe_vstack([X.T,
np.zeros((self.n_components_, n_samples))]),
2012-12-22 21:29:59 +08:00
safe_vstack([H.T,
np.sqrt(self.eta) * np.eye(self.n_components_)]),
2012-12-22 20:02:50 +08:00
W.T, tolW, self.nls_max_iter)
2011-12-27 06:25:35 +08:00
return W.T, gradW.T, iterW
2011-12-27 06:25:35 +08:00
def _update_H(self, X, H, W, tolH):
n_samples, n_features = X.shape
2012-12-22 20:02:50 +08:00
if self.sparseness is None:
2011-12-27 06:25:35 +08:00
H, gradH, iterH = _nls_subproblem(X, W, H, tolH,
self.nls_max_iter)
elif self.sparseness == 'data':
H, gradH, iterH = _nls_subproblem(
safe_vstack([X, np.zeros((self.n_components_, n_features))]),
2012-12-22 21:29:59 +08:00
safe_vstack([W,
np.sqrt(self.eta) * np.eye(self.n_components_)]),
2012-12-22 20:02:50 +08:00
H, tolH, self.nls_max_iter)
2011-12-27 06:25:35 +08:00
elif self.sparseness == 'components':
H, gradH, iterH = _nls_subproblem(
2012-12-22 20:02:50 +08:00
safe_vstack([X, np.zeros((1, n_features))]),
2012-12-22 21:29:59 +08:00
safe_vstack([W,
np.sqrt(self.beta)
* np.ones((1, self.n_components_))]),
2012-12-22 20:02:50 +08:00
H, tolH, self.nls_max_iter)
2011-12-27 06:25:35 +08:00
return H, gradH, iterH
2011-08-23 21:21:05 +08:00
def fit_transform(self, X, y=None):
"""Learn a NMF model for the data X and returns the transformed data.
This is more efficient than calling fit followed by transform.
Parameters
----------
2011-10-03 22:33:02 +08:00
X: {array-like, sparse matrix}, shape = [n_samples, n_features]
Data matrix to be decomposed
Returns
-------
data: array, [n_samples, n_components]
Transformed data
"""
X = check_array(X, accept_sparse='csr')
2011-10-03 22:33:02 +08:00
check_non_negative(X, "NMF.fit")
2010-12-13 15:51:57 +08:00
n_samples, n_features = X.shape
if not self.n_components:
self.n_components_ = n_features
else:
self.n_components_ = self.n_components
2011-12-27 06:25:35 +08:00
W, H = self._init(X)
2011-10-03 22:33:02 +08:00
gradW = (np.dot(W, np.dot(H, H.T))
- safe_sparse_dot(X, H.T, dense_output=True))
gradH = (np.dot(np.dot(W.T, W), H)
- safe_sparse_dot(W.T, X, dense_output=True))
2010-12-01 00:16:48 +08:00
init_grad = norm(np.r_[gradW, gradH.T])
tolW = max(0.001, self.tol) * init_grad # why max?
2010-12-01 00:16:48 +08:00
tolH = tolW
tol = self.tol * init_grad
for n_iter in range(1, self.max_iter + 1):
2010-12-01 00:16:48 +08:00
# stopping condition
# as discussed in paper
proj_norm = norm(np.r_[gradW[np.logical_or(gradW < 0, W > 0)],
gradH[np.logical_or(gradH < 0, H > 0)]])
if proj_norm < tol:
2010-12-01 00:16:48 +08:00
break
2010-12-01 00:16:48 +08:00
# update W
2011-12-27 06:25:35 +08:00
W, gradW, iterW = self._update_W(X, H, W, tolW)
if iterW == 1:
tolW = 0.1 * tolW
2010-12-01 00:16:48 +08:00
# update H
2011-12-27 06:25:35 +08:00
H, gradH, iterH = self._update_H(X, H, W, tolH)
if iterH == 1:
tolH = 0.1 * tolH
2011-12-27 06:25:35 +08:00
2013-09-01 22:46:56 +08:00
if not sp.issparse(X):
error = norm(X - np.dot(W, H))
else:
sqnorm_X = np.dot(X.data, X.data)
norm_WHT = trace_dot(np.dot(np.dot(W.T, W), H), H)
2013-09-01 22:46:56 +08:00
cross_prod = trace_dot((X * H.T), W)
error = sqrt(sqnorm_X + norm_WHT - 2. * cross_prod)
self.reconstruction_err_ = error
self.comp_sparseness_ = _sparseness(H.ravel())
self.data_sparseness_ = _sparseness(W.ravel())
2011-12-27 06:25:35 +08:00
H[H == 0] = 0 # fix up negative zeros
self.components_ = H
2011-04-02 00:08:20 +08:00
if n_iter == self.max_iter:
warnings.warn("Iteration limit reached during fit. Solving for W exactly.")
return self.transform(X)
2011-12-27 06:25:35 +08:00
self.n_iter_ = n_iter
2011-04-01 20:11:34 +08:00
return W
2011-04-02 20:52:04 +08:00
def fit(self, X, y=None, **params):
2011-04-02 21:32:54 +08:00
"""Learn a NMF model for the data X.
Parameters
----------
2011-10-03 22:33:02 +08:00
X: {array-like, sparse matrix}, shape = [n_samples, n_features]
2011-04-02 21:32:54 +08:00
Data matrix to be decomposed
Returns
-------
self
"""
self.fit_transform(X, **params)
2011-04-01 20:11:34 +08:00
return self
2010-12-01 00:16:48 +08:00
def transform(self, X):
"""Transform the data X according to the fitted NMF model
Parameters
----------
2011-10-03 22:33:02 +08:00
X: {array-like, sparse matrix}, shape = [n_samples, n_features]
Data matrix to be transformed by the model
Returns
-------
data: array, [n_samples, n_components]
Transformed data
2010-12-01 00:16:48 +08:00
"""
check_is_fitted(self, 'n_components_')
X = check_array(X, accept_sparse='csc')
Wt = np.zeros((self.n_components_, X.shape[0]))
check_non_negative(X, "ProjectedGradientNMF.transform")
2013-07-27 21:55:18 +08:00
if sp.issparse(X):
Wt, _, _ = _nls_subproblem(X.T, self.components_.T, Wt,
tol=self.tol,
max_iter=self.nls_max_iter)
else:
for j in range(0, X.shape[0]):
Wt[:, j], _ = nnls(self.components_.T, X[j, :])
return Wt.T
2011-04-03 00:54:52 +08:00
class NMF(ProjectedGradientNMF):
2011-04-08 00:24:47 +08:00
__doc__ = ProjectedGradientNMF.__doc__
2011-04-03 00:54:52 +08:00
pass