diff --git a/config_files/config_test.yml b/config_files/config_test.yml
index 2a8482ea1b855c9e53b261eb26c5a944dea5c9bb..979bdc8ae13c1dde5227746138952b4ec840f666 100644
--- a/config_files/config_test.yml
+++ b/config_files/config_test.yml
@@ -24,7 +24,7 @@ Classification:
   classes:
   type: ["multiview"]
   algos_monoview: ["all"]
-  algos_multiview: ["mumbo", "lp_norm_mkl"]
+  algos_multiview: ["mvml", "lp_norm_mkl",]
   stats_iter: 2
   metrics: ["accuracy_score", "f1_score"]
   metric_princ: "f1_score"
@@ -215,3 +215,13 @@ lp_norm_mkl:
   kernel_types: ["rbf_kernel"]
   kernel_configs:
     gamma: [0.1]
+
+mvml:
+  reg_params: [[0,1]]
+  nystrom_param: [1]
+  learn_A: [1]
+  learn_w: [0]
+  n_loops: [6]
+  kernel_types: ["rbf_kernel"]
+  kernel_configs:
+    gamma: [0.1]
diff --git a/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/additions/kernel_learning.py b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/additions/kernel_learning.py
new file mode 100644
index 0000000000000000000000000000000000000000..3eda3730f720c90403aff91ee3aa3757eae032e9
--- /dev/null
+++ b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/additions/kernel_learning.py
@@ -0,0 +1,98 @@
+from sklearn.metrics import pairwise
+import numpy as np
+
+from ...multiview.multiview_utils import BaseMultiviewClassifier
+from ...utils.hyper_parameter_search import CustomUniform, CustomRandint
+
+class KernelClassifier(BaseMultiviewClassifier):
+
+    def __init__(self, random_state=None,
+                 kernel_types=None, kernel_configs=None):
+        super().__init__(random_state)
+        self.kernel_configs=kernel_configs
+        self.kernel_types=kernel_types
+
+    def _compute_kernels(self, X, example_indices, view_indices, ):
+        new_X = {}
+        for index, (kernel_function, kernel_config, view_index) in enumerate(
+                zip(self.kernel_functions, self.kernel_configs, view_indices)):
+            new_X[index] = kernel_function(X.get_v(view_index,
+                                                   example_indices),
+                                           **kernel_config)
+        return new_X
+
+    def init_kernels(self, nb_view=2, ):
+        if isinstance(self.kernel_types, KernelDistribution):
+            self.kernel_functions = self.kernel_types.draw(nb_view)
+        elif isinstance(self.kernel_types, str):
+            self.kernel_functions = [getattr(pairwise, self.kernel_types)
+                                     for _ in range(nb_view)]
+        elif isinstance(self.kernel_types, list):
+            self.kernel_functions = [getattr(pairwise, kernel_type)
+                                     for kernel_type in self.kernel_types]
+
+        if isinstance(self.kernel_configs, KernelConfigDistribution):
+            self.kernel_configs = self.kernel_configs.draw(nb_view)
+            self.kernel_configs = [kernel_config[kernel_function.__name__]
+                                   for kernel_config, kernel_function
+                                   in zip(self.kernel_configs,
+                                          self.kernel_functions)]
+
+        elif isinstance(self.kernel_configs, dict):
+            self.kernel_configs = [self.kernel_configs for _ in range(nb_view)]
+        else:
+            pass
+
+
+class KernelConfigGenerator:
+
+    def __init__(self):
+        pass
+
+    def rvs(self, random_state=None):
+        return KernelConfigDistribution(seed=random_state.randint(1))
+
+
+class KernelConfigDistribution:
+
+    def __init__(self, seed=42):
+        self.random_state=np.random.RandomState(seed)
+        self.possible_config = {
+            "polynomial_kernel":{"degree": CustomRandint(low=1, high=7),
+                                 "gamma": CustomUniform(),
+                                 "coef0": CustomUniform()
+
+            },
+            "chi2_kernel": {"gamma": CustomUniform()},
+            "rbf_kernel": {"gamma": CustomUniform()},
+        }
+
+    def draw(self, nb_view):
+        drawn_params = [{} for _ in range(nb_view)]
+        for view_index in range(nb_view):
+            for kernel_name, params_dict in self.possible_config.items():
+                drawn_params[view_index][kernel_name] = {}
+                for param_name, distrib in params_dict.items():
+                    drawn_params[view_index][kernel_name][param_name] = distrib.rvs(self.random_state)
+        return drawn_params
+
+
+class KernelGenerator:
+
+    def __init__(self):
+        pass
+
+    def rvs(self, random_state=None):
+        return KernelDistribution(seed=random_state.randint(1))
+
+
+class KernelDistribution:
+
+    def __init__(self, seed=42):
+        self.random_state=np.random.RandomState(seed)
+        self.available_kernels = [pairwise.polynomial_kernel,
+                                  pairwise.chi2_kernel,
+                                  pairwise.rbf_kernel,]
+
+    def draw(self, nb_view):
+        return self.random_state.choice(self.available_kernels, nb_view)
diff --git a/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/lp_norm_mkl.py b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/lp_norm_mkl.py
index 6c83f84a20b1b77795e2c8ca8a2ca493ddfb8c45..f68973f8eb61546c2a7d0f58d783c009fe06366f 100644
--- a/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/lp_norm_mkl.py
+++ b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/lp_norm_mkl.py
@@ -2,6 +2,7 @@
 from sklearn.metrics import pairwise
 
 from ..multiview.multiview_utils import BaseMultiviewClassifier, get_examples_views_indices
+from .additions.kernel_learning import KernelClassifier, KernelConfigGenerator, KernelGenerator
 from ..utils.hyper_parameter_search import CustomUniform, CustomRandint
 
 classifier_class_name = "LPNormMKL"
@@ -204,121 +205,35 @@ class MKL(BaseEstimator, ClassifierMixin):
             self.W_sqrootinv_dict[v] = np.dot(Ue[:, 0:n_approx], inVa)
 
 
-class LPNormMKL(BaseMultiviewClassifier, MKL):
+class LPNormMKL(KernelClassifier, MKL):
     def __init__(self, random_state=None, lmbda=0.1, m_param=1, max_rounds=50,
                  max_diff=0.0001, use_approx=True, kernel_types="rbf_kernel",
                  kernel_configs=None, p=2, prev_alpha=False):
-        super().__init__(random_state)
+        super().__init__(random_state, kernel_configs=kernel_configs,
+                         kernel_types=kernel_types)
         super(BaseMultiviewClassifier, self).__init__(lmbda, m_param,
                                                       use_approx, max_rounds,
                                                       max_diff, p)
         self.param_names = ["lmbda", "kernel_types", "kernel_configs"]
         self.distribs = [CustomUniform(), KernelGenerator(),
                          KernelConfigGenerator()]
-        self.kernel_types = kernel_types
-        self.kernel_configs = kernel_configs
 
         self.prev_alpha = prev_alpha
 
     def fit(self, X, y, train_indices=None, view_indices=None):
         train_indices, view_indices = get_examples_views_indices(X, train_indices,
                                                                  view_indices)
-        nb_view, n = len(view_indices), len(train_indices)
-
-        self.init_kernels(nb_view=nb_view, )
-        new_X = {}
-        for index, (kernel_function, kernel_config, view_index) in enumerate(
-                zip(self.kernel_functions, self.kernel_configs, view_indices)):
-            new_X[index] = kernel_function(X.get_v(view_index,
-                                                   train_indices),
-                                          **kernel_config)
+        self.init_kernels(nb_view= len(view_indices), )
+        new_X = self._compute_kernels(X,
+                                      train_indices, view_indices)
         return super(LPNormMKL, self).fit(new_X, y[train_indices])
 
     def predict(self, X, example_indices=None, view_indices=None):
         example_indices, view_indices = get_examples_views_indices(X,
                                                                    example_indices,
                                                                    view_indices)
-        new_X = {}
-        for index, (kernel_function, kernel_config, view_index) in enumerate(
-                zip(self.kernel_functions, self.kernel_configs, view_indices)):
-            new_X[index] = kernel_function(X.get_v(view_index,
-                                                   example_indices),
-                                           **kernel_config)
+        new_X = self._compute_kernels(X, example_indices, view_indices)
         return super(LPNormMKL, self).predict(new_X)
 
-    def init_kernels(self, nb_view=2, ):
-        if isinstance(self.kernel_types, KernelDistribution):
-            self.kernel_functions = self.kernel_types.draw(nb_view)
-        elif isinstance(self.kernel_types, str):
-            self.kernel_functions = [getattr(pairwise, self.kernel_types)
-                                     for _ in range(nb_view)]
-        elif isinstance(self.kernel_types, list):
-            self.kernel_functions = [getattr(pairwise, kernel_type)
-                                     for kernel_type in self.kernel_types]
-
-        if isinstance(self.kernel_configs, KernelConfigDistribution):
-            self.kernel_configs = self.kernel_configs.draw(nb_view)
-            self.kernel_configs = [kernel_config[kernel_function.__name__]
-                                   for kernel_config, kernel_function
-                                   in zip(self.kernel_configs,
-                                          self.kernel_functions)]
-
-        elif isinstance(self.kernel_configs, dict):
-            self.kernel_configs = [self.kernel_configs for _ in range(nb_view)]
-        else:
-            pass
-
-
-class KernelConfigGenerator:
-
-    def __init__(self):
-        pass
-
-    def rvs(self, random_state=None):
-        return KernelConfigDistribution(seed=random_state.randint(1))
-
-
-class KernelConfigDistribution:
-
-    def __init__(self, seed=42):
-        self.random_state=np.random.RandomState(seed)
-        self.possible_config = {
-            "polynomial_kernel":{"degree": CustomRandint(low=1, high=7),
-                                 "gamma": CustomUniform(),
-                                 "coef0": CustomUniform()
-
-            },
-            "chi2_kernel": {"gamma": CustomUniform()},
-            "rbf_kernel": {"gamma": CustomUniform()},
-        }
-
-    def draw(self, nb_view):
-        drawn_params = [{} for _ in range(nb_view)]
-        for view_index in range(nb_view):
-            for kernel_name, params_dict in self.possible_config.items():
-                drawn_params[view_index][kernel_name] = {}
-                for param_name, distrib in params_dict.items():
-                    drawn_params[view_index][kernel_name][param_name] = distrib.rvs(self.random_state)
-        return drawn_params
-
-
-class KernelGenerator:
-
-    def __init__(self):
-        pass
-
-    def rvs(self, random_state=None):
-        return KernelDistribution(seed=random_state.randint(1))
-
-
-class KernelDistribution:
 
-    def __init__(self, seed=42):
-        self.random_state=np.random.RandomState(seed)
-        self.available_kernels = [pairwise.polynomial_kernel,
-                                  pairwise.chi2_kernel,
-                                  pairwise.rbf_kernel,
-                                 ]
 
-    def draw(self, nb_view):
-        return self.random_state.choice(self.available_kernels, nb_view)
diff --git a/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/mvml.py b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/mvml.py
new file mode 100644
index 0000000000000000000000000000000000000000..5054ec54eb565f31488c29d4f312fafc2b03133b
--- /dev/null
+++ b/multiview_platform/mono_multi_view_classifiers/multiview_classifiers/mvml.py
@@ -0,0 +1,519 @@
+import numpy as np
+from sklearn.base import BaseEstimator
+from sklearn.base import ClassifierMixin
+from sklearn.utils.multiclass import unique_labels
+from sklearn.utils.validation import check_X_y
+from sklearn.utils.validation  import check_array
+from sklearn.utils.validation  import check_is_fitted
+from .additions.data_sample import DataSample, Metriclearn_array
+
+"""
+    Copyright (C) 2018  Riikka Huusari
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+This file contains algorithms for Multi-View Metric Learning (MVML) as introduced in
+
+Riikka Huusari, Hachem Kadri and Cécile Capponi:
+Multi-View Metric Learning in Vector-Valued Kernel Spaces
+in International Conference on Artificial Intelligence and Statistics (AISTATS) 2018
+
+Usage (see also demo.py for a more detailed example):
+    create a MVML object via:
+        mvml = MVML(kernel_dict, label_vector, regression_parameter_list, nystrom_param)
+    learn the model:
+        A, g, w = mvml.learn_mvml()
+    predict with the model:
+        predictions = predict_mvml(test_kernel_dict, g, w)
+
+(parameter names as in the paper)
+
+Code is tested with Python 3.5.2 and numpy 1.12.1
+"""
+
+
+class MVML(BaseEstimator, ClassifierMixin):
+    r"""
+    The MVML Classifier
+
+    Parameters
+    ----------
+    regression_params: array/list of regression parameters, first for basic regularization, second for
+        regularization of A (not necessary if A is not learned)
+
+    nystrom_param: value between 0 and 1 indicating level of nyström approximation; 1 = no approximation
+
+    learn_A :  integer (default 1) choose if A is learned or not: 1 - yes (default);
+               2 - yes, sparse; 3 - no (MVML_Cov); 4 - no (MVML_I)
+
+    learn_w : integer (default 0) where learn w is needed
+
+    n_loops : (default 0) number of itterions
+
+
+    Attributes
+    ----------
+    reg_params : array/list of regression parameters
+
+    learn_A :  1 where Learn matrix A is needded
+
+    learn_w : integer where learn w is needed
+
+    n_loops : number of itterions
+
+    n_approx : number of samples in approximation, equals n if no approx.
+
+    X_ : :class:`metriclearning.datasets.data_sample.Metriclearn_array` array of input sample
+
+    y_ : array-like, shape = (n_samples,)
+         Target values (class labels).
+
+    """
+
+    def __init__(self, regression_params, nystrom_param, learn_A=1, learn_w=0, n_loops=6):
+
+        # calculate nyström approximation (if used)
+        self.nystrom_param = nystrom_param
+
+        self.reg_params = regression_params
+        self.learn_A = learn_A
+        self.learn_w = learn_w
+        self.n_loops = n_loops
+
+    def fit(self, X, y= None, views_ind=None):
+        """
+        Fit the MVML classifier
+        Parameters
+        ----------
+
+        X : Metriclearn_array {array-like, sparse matrix}, shape = (n_samples, n_features)
+            Training multi-view input samples.
+
+
+        y : array-like, shape = (n_samples,)
+            Target values (class labels).
+            array of length n_samples containing the classification/regression labels
+            for training data
+
+        views_ind : array-like (default=[0, n_features//2, n_features])
+            Paramater specifying how to extract the data views from X:
+
+            - If views_ind is a 1-D array of sorted integers, the entries
+              indicate the limits of the slices used to extract the views,
+              where view ``n`` is given by
+              ``X[:, views_ind[n]:views_ind[n+1]]``.
+
+              With this convention each view is therefore a view (in the NumPy
+              sense) of X and no copy of the data is done.
+
+
+        Returns
+        -------
+
+        self : object
+            Returns self.
+        """
+        # Check that X and y have correct shape
+
+        # Store the classes seen during fit
+        if isinstance(X, Metriclearn_array):
+            self.X_ = X
+        elif isinstance(X, np.ndarray) :
+            self.X_= Metriclearn_array(X, views_ind)
+        elif isinstance(X, dict):
+            self.X_= Metriclearn_array(X)
+        else:
+            raise TypeError("Input format is not reconized")
+        check_X_y(self.X_, y)
+        self.classes_ = unique_labels(y)
+        self.y_ = y
+
+        # n = X[0].shape[0]
+        n = self.X_.shape[0]
+        self.n_approx = int(np.floor(self.nystrom_param * n))  # number of samples in approximation, equals n if no approx.
+
+        if self.nystrom_param < 1:
+            self._calc_nystrom(self.X_)
+        else:
+            self.U_dict = self.X_.to_dict()
+
+        # Return the classifier
+        self.learn_mvml(learn_A=self.learn_A, learn_w=self.learn_w, n_loops=self.n_loops)
+        return self
+
+    def learn_mvml(self, learn_A=1, learn_w=0, n_loops=6):
+        """
+
+        Parameters
+        ----------
+        learn_A: int choose if A is learned or not (default: 1):
+                 1 - yes (default);
+                 2 - yes, sparse;
+                 3 - no (MVML_Cov);
+                 4 - no (MVML_I)
+        learn_w: int choose if w is learned or not (default: 0):
+                 0 - no (uniform 1/views, default setting),
+                 1 - yes
+        n_loops: int maximum number of iterations in MVML, (default: 6)
+                 usually something like default 6 is already converged
+
+        Returns
+        -------
+        tuple (A, g, w) with A (metrcic matrix - either fixed or learned),
+                             g (solution to learning problem),
+                             w (weights - fixed or learned)
+        """
+        views = len(self.U_dict)
+        n = self.U_dict[0].shape[0]
+        lmbda = self.reg_params[0]
+        if learn_A < 3:
+            eta = self.reg_params[1]
+
+        # ========= initialize A =========
+
+        # positive definite initialization (with multiplication with the U matrices if using approximation)
+        A = np.zeros((views * self.n_approx, views * self.n_approx))
+        if learn_A < 3:
+            for v in range(views):
+                if self.nystrom_param < 1:
+                    A[v * self.n_approx:(v + 1) * self.n_approx, v * self.n_approx:(v + 1) * self.n_approx] = \
+                        np.dot(np.transpose(self.U_dict[v]), self.U_dict[v])
+                else:
+                    A[v * self.n_approx:(v + 1) * self.n_approx, v * self.n_approx:(v + 1) * self.n_approx] = np.eye(n)
+        # otherwise initialize like this if using MVML_Cov
+        elif learn_A == 3:
+            for v in range(views):
+                for vv in range(views):
+                    if self.nystrom_param < 1:
+                        A[v * self.n_approx:(v + 1) * self.n_approx, vv * self.n_approx:(vv + 1) * self.n_approx] = \
+                            np.dot(np.transpose(self.U_dict[v]), self.U_dict[vv])
+                    else:
+                        A[v * self.n_approx:(v + 1) * self.n_approx, vv * self.n_approx:(vv + 1) * self.n_approx] = \
+                            np.eye(n)
+        # or like this if using MVML_I
+        elif learn_A == 4:
+            for v in range(views):
+                if self.nystrom_param < 1:
+                    A[v * self.n_approx:(v + 1) * self.n_approx, v * self.n_approx:(v + 1) * self.n_approx] = \
+                        np.eye(self.n_approx)
+                else:
+                    # it might be wise to make a dedicated function for MVML_I if using no approximation
+                    # - numerical errors are more probable this way using inverse
+                    A[v * self.n_approx:(v + 1) * self.n_approx, v * self.n_approx:(v + 1) * self.n_approx] = \
+                        np.linalg.pinv(self.U_dict[v])  # U_dict holds whole kernels if no approx
+
+        # ========= initialize w, allocate g =========
+        w = (1 / views) * np.ones((views, 1))
+        g = np.zeros((views * self.n_approx, 1))
+
+        # ========= learn =========
+        loop_counter = 0
+        while True:
+
+            if loop_counter > 0:
+                g_prev = np.copy(g)
+                A_prev = np.copy(A)
+                w_prev = np.copy(w)
+
+            # ========= update g =========
+
+            # first invert A
+            try:
+                A_inv = np.linalg.pinv(A + 1e-09 * np.eye(views * self.n_approx))
+            except np.linalg.linalg.LinAlgError:
+                try:
+                    A_inv = np.linalg.pinv(A + 1e-06 * np.eye(views * self.n_approx))
+                except ValueError:
+                    return A_prev, g_prev
+            except ValueError:
+                return A_prev, g_prev
+
+            # then calculate g (block-sparse multiplications in loop) using A_inv
+            for v in range(views):
+                for vv in range(views):
+                    A_inv[v * self.n_approx:(v + 1) * self.n_approx, vv * self.n_approx:(vv + 1) * self.n_approx] = \
+                        w[v] * w[vv] * np.dot(np.transpose(self.U_dict[v]), self.U_dict[vv]) + \
+                        lmbda * A_inv[v * self.n_approx:(v + 1) * self.n_approx,
+                                      vv * self.n_approx:(vv + 1) * self.n_approx]
+                g[v * self.n_approx:(v + 1) * self.n_approx, 0] = np.dot(w[v] * np.transpose(self.U_dict[v]), self.y_)
+
+            try:
+                g = np.dot(np.linalg.pinv(A_inv), g)  # here A_inv isn't actually inverse of A (changed in above loop)
+            except np.linalg.linalg.LinAlgError:
+                g = np.linalg.solve(A_inv, g)
+
+            # ========= check convergence =========
+
+            if learn_A > 2 and learn_w != 1:  # stop at once if only g is to be learned
+                break
+
+            if loop_counter > 0:
+
+                # convergence criteria
+                g_diff = np.linalg.norm(g - g_prev) / np.linalg.norm(g_prev)
+                A_diff = np.linalg.norm(A - A_prev, ord='fro') / np.linalg.norm(A_prev, ord='fro')
+                if g_diff < 1e-4 and A_diff < 1e-4:
+                    break
+
+            if loop_counter >= n_loops:  # failsafe
+                break
+
+            # ========= update A =========
+            if learn_A == 1:
+                A = self._learn_A_func(A, g, lmbda, eta)
+            elif learn_A == 2:
+                A = self._learn_blocksparse_A(A, g, views, self.n_approx, lmbda, eta)
+
+            # ========= update w =========
+            if learn_w == 1:
+                Z = np.zeros((n, views))
+                for v in range(views):
+                    Z[:, v] = np.dot(self.U_dict[v], g[v * self.n_approx:(v + 1) * self.n_approx]).ravel()
+                w = np.dot(np.linalg.pinv(np.dot(np.transpose(Z), Z)), np.dot(np.transpose(Z), self.y_))
+
+            loop_counter += 1
+        self.g = g
+        self.w = w
+        self.A = A
+        return A, g, w
+
+
+    def predict(self, X, views_ind=None):
+        """
+
+        Parameters
+        ----------
+        X
+
+        Returns
+        -------
+
+        """
+
+        """
+
+
+        :param X:
+        :return: 
+        """
+        if isinstance(X, Metriclearn_array):
+            self.X_ = X
+        elif isinstance(X, np.ndarray) :
+            self.X_= Metriclearn_array(X, views_ind)
+        elif isinstance(X, dict):
+            self.X_= Metriclearn_array(X)
+        else:
+            raise TypeError("Input format is not reconized")
+        check_is_fitted(self, ['X_', 'y_'])
+        check_array(self.X_)
+        check_is_fitted(self, ['X_', 'y_'])
+        return self.predict_mvml(self.X_, self.g, self.w)
+
+    def predict_mvml(self, test_kernels, g, w):
+
+        """
+        :param test_kernels: dictionary of test kernels (as the dictionary of kernels in __init__)
+        :param g: g, learning solution that is learned in learn_mvml
+        :param w: w, weights for combining the solutions of views, learned in learn_mvml
+        :return: (regression) predictions, array of size  test_samples*1
+        """
+
+        views = len(self.U_dict)
+        # t = test_kernels[0].shape[0]
+        t = test_kernels.shape[0]
+        X = np.zeros((t, views * self.n_approx))
+        for v in range(views):
+            if self.nystrom_param < 1:
+                X[:, v * self.n_approx:(v + 1) * self.n_approx] = w[v] * \
+                                                                  np.dot(test_kernels.get_view(v)[:, 0:self.n_approx],
+                                                                         self.W_sqrootinv_dict[v])
+            else:
+                X[:, v * self.n_approx:(v + 1) * self.n_approx] = w[v] * test_kernels[v]
+
+        return np.dot(X, g)
+
+    def _calc_nystrom(self, kernels):
+        # calculates the nyström approximation for all the kernels in the given dictionary
+        self.W_sqrootinv_dict = {}
+        self.U_dict = {}
+        for v in range(len(kernels.shapes_int)):
+            kernel = kernels.get_view(v)
+            E = kernel[:, 0:self.n_approx]
+            W = E[0:self.n_approx, :]
+            Ue, Va, _ = np.linalg.svd(W)
+            vak = Va[0:self.n_approx]
+            inVa = np.diag(vak ** (-0.5))
+            U_v = np.dot(E, np.dot(Ue[:, 0:self.n_approx], inVa))
+            self.U_dict[v] = U_v
+            self.W_sqrootinv_dict[v] = np.dot(Ue[:, 0:self.n_approx], inVa)
+
+    def _learn_A_func(self, A, g, lmbda, eta):
+
+        # basic gradient descent
+
+        stepsize = 0.5
+        if stepsize*eta >= 0.5:
+            stepsize = 0.9*(1/(2*eta))  # make stepsize*eta < 0.5
+
+        loops = 0
+        not_converged = True
+        while not_converged:
+
+            A_prev = np.copy(A)
+
+            A_pinv = np.linalg.pinv(A)
+            A = (1-2*stepsize*eta)*A + stepsize*lmbda*np.dot(np.dot(A_pinv, g), np.dot(np.transpose(g), A_pinv))
+
+            if loops > 0:
+                prev_diff = diff
+            diff = np.linalg.norm(A - A_prev) / np.linalg.norm(A_prev)
+
+            if loops > 0 and prev_diff > diff:
+                A = A_prev
+                stepsize = stepsize*0.1
+
+            if diff < 1e-5:
+                not_converged = False
+
+            if loops > 10:
+                not_converged = False
+
+            loops += 1
+
+        return A
+
+    def _learn_blocksparse_A(self, A, g, views, m, lmbda, eta):
+
+        # proximal gradient update method
+
+        converged = False
+        rounds = 0
+
+        L = lmbda * np.linalg.norm(np.dot(g, g.T))
+        # print("L ", L)
+
+        while not converged and rounds < 100:
+
+            # no line search - this has worked well enough experimentally
+            A = self._proximal_update(A, views, m, L, g, lmbda, eta)
+
+            # convergence
+            if rounds > 0:
+                A_diff = np.linalg.norm(A - A_prev) / np.linalg.norm(A_prev)
+
+                if A_diff < 1e-3:
+                    converged = True
+
+            A_prev = np.copy(A)
+
+            rounds += 1
+
+        return A
+
+    def _proximal_update(self, A_prev, views, m, L, D, lmbda, gamma):
+
+        # proximal update
+
+        # the inverse is not always good to compute - in that case just return the previous one and end the search
+        try:
+            A_prev_inv = np.linalg.pinv(A_prev)
+        except np.linalg.linalg.LinAlgError:
+            try:
+                A_prev_inv = np.linalg.pinv(A_prev + 1e-6 * np.eye(views * m))
+            except np.linalg.linalg.LinAlgError:
+                return A_prev
+            except ValueError:
+                return A_prev
+        except ValueError:
+            return A_prev
+
+        if np.any(np.isnan(A_prev_inv)):
+            # just in case the inverse didn't return a proper solution (happened once or twice)
+            return A_prev
+
+        A_tmp = A_prev + (lmbda / L) * np.dot(np.dot(A_prev_inv.T, D), np.dot(np.transpose(D), A_prev_inv.T))
+
+        # if there is one small negative eigenvalue this gets rid of it
+        try:
+            val, vec = np.linalg.eigh(A_tmp)
+        except np.linalg.linalg.LinAlgError:
+            return A_prev
+        except ValueError:
+            return A_prev
+        val[val < 0] = 0
+
+        A_tmp = np.dot(vec, np.dot(np.diag(val), np.transpose(vec)))
+        A_new = np.zeros((views*m, views*m))
+
+        # proximal update, group by group (symmetric!)
+        for v in range(views):
+            for vv in range(v + 1):
+                if v != vv:
+                    if np.linalg.norm(A_tmp[v * m:(v + 1) * m, vv * m:(vv + 1) * m]) != 0:
+                        multiplier = 1 - gamma / (2 * np.linalg.norm(A_tmp[v * m:(v + 1) * m, vv * m:(vv + 1) * m]))
+                        if multiplier > 0:
+                            A_new[v * m:(v + 1) * m, vv * m:(vv + 1) * m] = multiplier * A_tmp[v * m:(v + 1) * m,
+                                                                                               vv * m:(vv + 1) * m]
+                            A_new[vv * m:(vv + 1) * m, v * m:(v + 1) * m] = multiplier * A_tmp[vv * m:(vv + 1) * m,
+                                                                                               v * m:(v + 1) * m]
+                else:
+                    if (np.linalg.norm(A_tmp[v * m:(v + 1) * m, v * m:(v + 1) * m])) != 0:
+                        multiplier = 1 - gamma / (np.linalg.norm(A_tmp[v * m:(v + 1) * m, v * m:(v + 1) * m]))
+                        if multiplier > 0:
+                            A_new[v * m:(v + 1) * m, v * m:(v + 1) * m] = multiplier * A_tmp[v * m:(v + 1) * m,
+                                                                                             v * m:(v + 1) * m]
+
+        return A_new
+
+
+from ..multiview.multiview_utils import BaseMultiviewClassifier, get_examples_views_indices
+from .additions.kernel_learning import KernelClassifier, KernelConfigGenerator, KernelGenerator
+from ..utils.hyper_parameter_search import CustomUniform, CustomRandint
+
+classifier_class_name = "MVMLClassifier"
+
+class MVMLClassifier(KernelClassifier, MVML):
+
+    def __init__(self, random_state=None, reg_params=None,
+                 nystrom_param=1, learn_A=1, learn_w=0, n_loops=6, kernel_types="rbf_kernel",
+                 kernel_configs=None):
+        super().__init__(random_state, kernel_types=kernel_types,
+                         kernel_configs=kernel_configs)
+        super(BaseMultiviewClassifier, self).__init__(reg_params,
+                                                      nystrom_param,
+                                                      learn_A=learn_A,
+                                                      learn_w=learn_w,
+                                                      n_loops=n_loops)
+        self.param_names = ["nystrom_param", "kernel_types", "kernel_configs",
+                            "learn_A", "learn_w", "n_loops", "reg_params"]
+        self.distribs = [CustomUniform(), KernelGenerator(),
+                         KernelConfigGenerator(), CustomRandint(low=1, high=5),
+                         [0,1], CustomRandint(low=1, high=100), [[0.1,0.9]]]
+
+    def fit(self, X, y, train_indices=None, view_indices=None):
+        train_indices, view_indices = get_examples_views_indices(X,
+                                                                 train_indices,
+                                                                 view_indices)
+        self.init_kernels(nb_view=len(view_indices), )
+        new_X = self._compute_kernels(X,
+                                      train_indices, view_indices)
+        return super(MVMLClassifier, self).fit(new_X, y[train_indices])
+
+    def predict(self, X, example_indices=None, view_indices=None):
+        example_indices, view_indices = get_examples_views_indices(X,
+                                                                   example_indices,
+                                                                   view_indices)
+        new_X = self._compute_kernels(X, example_indices, view_indices)
+        return super(MVMLClassifier, self).predict(new_X)
\ No newline at end of file