# Source code for hpelm.modules.mrsr2

# -*- coding: utf-8 -*-

import numpy as np
from scipy import optimize
from scipy.linalg import lu_factor, lu_solve

[docs]def mrsr2(X, T, kmax, norm=2):
"""Multi-Responce Sparse Regression implementation with linear scaling in number of outputs.

Basically an L1-regularized regression with multiple outputs, regularization considers all outputs together,
method returns the best input features one by one and can be stopped early. Compared to an original MRSR this method
is slower for small problems, but has a linear complexity in the number of outputs instead of exponential one,
so it is suitable for auto-encoders and other tasks with large output dimensionality.

Args:
T (matrix): an (n x p) matrix of targets. The columns of T should have zero mean and same scale (e.g. equal variance).
X (matrix): an (n x m) matrix of regressors. The columns of X should have zero mean and same scale (e.g. equal variance).
kmax (int): an integer fixing the number of steps to be run, which equals to the maximum number of regressors in the model.
norm (from Numpy.linalg.norm): norm to use in MRSR2, can be 1 for L1 or 2 for L2 norm, default 2.

Returns:
i1 (vector): a (1 x kmax) vector of indices revealing the order in which the regressors enter model.

Reference:
Better MRSR implementation according to:
"Common subset selection of inputs in multiresponse regression" by
Timo SimilĂ¤ and Jarkko Tikka, International Joint Conference on Neural Networks 2006

Created on Sun Jan 26 13:48:54 2014
@author: Anton Akusok
"""

# initializing
ins = X.shape[1]
outs = T.shape[1]
X = np.array(X, order='F')  # Fortran ordering is good for operating columns
XA = np.empty(X.shape, order='F')
XX = np.dot(X.T, X)
XT = np.dot(X.T, T)
rank = []  # active inputs list
nonrank = range(ins)
W = np.zeros((ins, outs))  # current projection estimator
Y = np.zeros(T.shape)  # current target estimator
Yk1 = np.zeros(T.shape)
Wk1 = np.zeros((1, outs))
j_current = None  # currently added input dimension
kmax = min(kmax, X.shape[0])

# get ranking
for _ in xrange(kmax):

# first step
if len(rank) == 0:
c_max = -1
for j in nonrank:
c_kj = np.linalg.norm(np.dot(T.T, X[:,j]), norm)
if c_kj > c_max:
c_max = c_kj
j_max = j
j_current = j_max
# save new input
rank.append(j_current)
nonrank.remove(j_current)
# swap columns
idx = len(rank)-1
XA[:,idx] = X[:, j_current]
y_min = 1

# last step
elif len(nonrank) == 0:
Y = Yk1
W[rank] = Wk1
y_min = 1

# intermediate step
else:
Yk2 = (Yk1 - Y).T
T2 = (T - Y).T

X9 = X[:, j_current]
c_max = np.linalg.norm(np.dot(T2, X9), norm)
#c_max = np.linalg.norm(np.dot(T2, X[:, j_current]), norm)

#fun = lambda y,x_new: (1-y)*c_max - np.linalg.norm(T2.dot(x_new) - y*Yk2.dot(x_new))
fun_p = lambda y,p1,p2: (1-y)*c_max - np.linalg.norm(p1 - y*p2)  # super fast parametrized function

# find optimal step (minimum over possible additional inputs)
y_min = 1  # upper interval
for j_new in nonrank:
x_new = X[:,j_new]
# pre-calculate constant parts of the optimization function for the given x_new
p1 = T2.dot(x_new)
p2 = Yk2.dot(x_new)
if (1-y_min)*c_max < np.linalg.norm(p1 - y_min*p2):  # skip optimization if min(fun) > y_min
try:
zero = 1E-15  # finding a value greater than zero
y_kj = optimize.brentq(fun_p, zero, y_min, xtol=1E-6, args=(p1,p2))
y_min = y_kj
j_min = j_new
except ValueError:
# ValueError: f(a) and f(b) must have different signs
# here f(a) < 0 and f(b) < 0; does not fit our purposes anyway
# ignoring this case
pass

if y_min == 1:  # if no suitable solution was found
j_min = j_new
j_current = j_min

# add new input into model
rank.append(j_current)
nonrank.remove(j_current)
# add new input to X matrix
idx = len(rank)-1
XA[:,idx] = X[:,j_current]

# post-update ELM estimation with current set of inputs, with LU-ELM
XtX = XX[rank,:][:,rank]
XtT = XT[rank,:]
LU, piv = lu_factor(XtX)#, overwrite_a=True)
Wk1 = lu_solve((LU, piv), XtT)#, overwrite_b=True)
X1 = XA[:,:len(rank)]  # replace fancy indexing with simple one
Yk1 = np.dot(X1, Wk1)

if len(rank) > 1:
# perform variable length step
Y = (1-y_min)*Y + y_min*Yk1
W = (1-y_min)*W
W[rank] += y_min*Wk1

# done, return ranking
return rank