Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First NMF function #2

Open
wants to merge 1 commit into
base: NMF
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions nmf/sparse_nmf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
""" Sparse Nonnegative matrix factorization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. But since we are doing it within scikit-learn, better to make this nmf as a module under sklearn, right? so move it there, add also an __init__.py file so Python recognizes it as a submodule

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually better as sklearn/decomposition/sparse_nmf.py I guess

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we will have implementations which work in Spark clusters as well as
GPU machines.
So, not sure if we want it in the sklearn branch.
For example:

https://github.com/arnlaugsson/sparkIt

Okay, will do the tests similar to the NMF function in scikits-learn.

On Sun, May 3, 2015 at 10:27 AM, Yaroslav Halchenko <
[email protected]> wrote:

In nmf/sparse_nmf.py
#2 (comment):

@@ -0,0 +1,172 @@
+""" Sparse Nonnegative matrix factorization

actually better as sklearn/decomposition/nmf


Reply to this email directly or view it on GitHub
https://github.com/scicubator/scikit-learn/pull/2/files#r29556542.

"""
# Author: Vamsi Krishna Potluru
# License: BSD 3 clause

from __future__ import division
import numpy as np
import scipy.io


def sparse_nmf(X, r, maxiter, spar, W=None, H=None):
"""Learns a sparse NMF model given data X.

Parameters
----------

X : {array}, shape = [n_features, n_samples]
r : rank of factorization
maxiter : number of updates of the factor matrices
spar : sparsity of the features given by measure
sp(x) = (sqrt(n)-|x|_1/|x|_2 )/(sqrt(n)-1)

Returns
-------

W : {array}
Feature matrix to the sparse NMF problem.

Reference
---------

Block Coordinate Descent for Sparse NMF
Vamsi K. Potluru, Sergey M. Plis, Jonathan Le Roux,
Barak A. Pearlmutter, Vince D. Calhoun, Thomas P. Hayes
ICLR 2013.
http://arxiv.org/abs/1301.3527
"""
m, n = np.shape(X)
if not W and not H:
W, H = init_nmf(X, r, spar)
Obj = np.zeros(maxiter)
for i in range(maxiter):
Obj[i] = np.linalg.norm(X - np.dot(W, H), 'fro')
print 'iter:', i + 1, 'Obj: ', Obj[i]
W = update_W(X, W, H, spar)
H = update_H(X, W, H)

return W


def init_nmf(X, r, spar):
""" Initialize the matrix factors for NMF.

Parameters
----------

X: {array}, shape = [n_features, n_samples]
r: rank of factorization

Returns
-------

W : {array}
Feature matrix of the factorization
H : {array}
Weight matrix of the factorization

where X ~ WH
"""
m, n = np.shape(X)
W = np.zeros((m, r))
k = np.sqrt(m) - spar * (np.sqrt(m) - 1)
for i in range(r):
W[:, i] = sparse_opt(np.sort(np.random.rand(m))[::-1], k)

W = np.random.rand(m, r)
H = np.random.rand(r, n)
return (W, H)


def update_W(X, W, H, spar):
"""Update the feature matrix based on user-defined sparsity"""
m, n = np.shape(X)
m, r = np.shape(W)
cach = np.zeros((m, r))
HHt = np.dot(H, H.T)
cach = -np.dot(X, H.T) + np.dot(W, np.dot(H, H.T))
for i in range(r):
W, cach = W_sparse_ith(W, HHt, cach, spar, i)

return W


def update_H(X, W, H):
"""Update the weight matrix using the regular multiplicative updates"""
m, n = np.shape(X)
WtX = np.dot(W.T, X)
WtW = np.dot(W.T, W)
for j in range(10):
H = H * WtX / (np.dot(WtW, H) + np.spacing(1))

return H


def W_sparse_ith(W, HHt, cach, spar, i):
""" Update the columns sequentially"""
m, r = np.shape(W)
C = cach[:, i] - W[:, i] * HHt[i, i]
V = np.zeros(m)
k = np.sqrt(m) - spar * (np.sqrt(m) - 1)
a = sparse_opt(np.sort(-C)[::-1], k)
ind = np.argsort(-C)[::-1]
V[ind] = a
cach = cach + np.outer(V - W[:, i], HHt[i, :])
W[:, i] = V
return (W, cach)


def sparse_opt(b, k):
""" Project a vector onto a sparsity constraint

Solves the projection problem by taking into account the
symmetry of l1 and l2 constraints.

Parameters
---------
b : sorted vector in decreasing value
k : Ratio of l1/l2 norms of a vector

Returns
-------
z : closest vector satisfying the required sparsity constraint.

"""
n = len(b)
sumb = np.cumsum(b)
normb = np.cumsum(b * b)
pnormb = np.arange(1, n + 1) * normb
y = (pnormb - sumb * sumb) / (np.arange(1, n + 1) - k * k)
bot = np.ceil(k * k)
z = np.zeros(n)
if bot > n:
print 'Looks like the sparsity measure is not between 0 and 1\n'
return
obj = (-np.sqrt(y) * (np.arange(1, n + 1) + k) + sumb) / \
np.arange(1, n + 1)
indx = np.argmax(obj[bot:n])
p = indx + bot - 1
p = min(p, n - 1)
p = max(p, bot)
lam = np.sqrt(y[p])
mue = -sumb[p] / (p + 1) + k / (p + 1) * lam
z[:p + 1] = (b[:p + 1] + mue) / lam
return z


if __name__ == '__main__':
r = 25
spar = 0.5
maxiter = 200
X = scipy.io.loadmat('../data/orlfaces.mat')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to simulate some simple cases where you even know the ground truth of original "bases"

W = sparse_nmf(X['V'], r, maxiter, spar)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and that is the point of "unit-tests": to verify/guarantee that things work as they should. Next step would be to create sklearn/nmf/tests/test_sparse_nmf.py and write some basic tests: that functions work as they should and produce correct or at least sensible results.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see.
Yes, I have been reading about test driven development in Python and it is
starting to make sense.

Btw, I am not sure if we want to make it necessarily part of scikit-learn.
We should discuss that.
There are couple of important high-level questions which we should hammer
out.

On Sun, May 3, 2015 at 10:24 AM, Yaroslav Halchenko <
[email protected]> wrote:

In nmf/sparse_nmf.py
#2 (comment):

  • indx = np.argmax(obj[bot:n])
  • p = indx + bot - 1
  • p = min(p, n - 1)
  • p = max(p, bot)
  • lam = np.sqrt(y[p])
  • mue = -sumb[p] / (p + 1) + k / (p + 1) * lam
  • z[:p + 1] = (b[:p + 1] + mue) / lam
  • return z

+if name == 'main':

  • r = 25
  • spar = 0.5
  • maxiter = 200
  • X = scipy.io.loadmat('../data/orlfaces.mat')
  • W = sparse_nmf(X['V'], r, maxiter, spar)

and that is the point of "unit-tests": to verify/guarantee that things
work as they should. Next step would be to create
sklearn/nmf/tests/test_sparse_nmf.py and write some basic tests: that
functions work as they should and produce correct or at least sensible
results.


Reply to this email directly or view it on GitHub
https://github.com/scicubator/scikit-learn/pull/2/files#r29556519.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually sklearn/decomposition/tests/test_sparse_nmf.py

#import pylab as plt
#for i in range(r):
# plt.subplot(np.sqrt(r), np.sqrt(r), i + 1)
# plt.imshow(np.reshape(W.T[i], [92, 112]).T)
# plt.axis('off')
# plt.ion()

#plt.show(True)
#import ipdb
#ipdb.set_trace()