web-dev-qa-db-fra.com

Pyspark et PCA: Comment puis-je extraire les vecteurs propres de ce PCA? Comment puis-je calculer la variance expliquée?

Je réduis la dimensionnalité d'un Spark DataFrame avec PCA modèle avec pyspark (en utilisant la bibliothèque sparkml) comme suit:

pca = PCA(k=3, inputCol="features", outputCol="pca_features")
model = pca.fit(data)

data est un Spark DataFrame avec une colonne labellisée featuresqui est un DenseVector de 3 dimensions:

data.take(1)
Row(features=DenseVector([0.4536,-0.43218, 0.9876]), label=u'class1')

Après l'ajustement, je transforme les données:

transformed = model.transform(data)
transformed.first()
Row(features=DenseVector([0.4536,-0.43218, 0.9876]), label=u'class1', pca_features=DenseVector([-0.33256, 0.8668, 0.625]))

Ma question est: comment puis-je extraire les vecteurs propres de cette PCA? Comment puis-je calculer la variance expliquée?

21
nanounanue

[ MISE À JOUR: À partir de Spark 2.2 en avant, PCA et SVD sont tous les deux disponibles dans PySpark - voir ticket JIRA SPARK-6227 et PCA & PCAModel pour Spark ML 2.2; original la réponse ci-dessous est toujours applicable aux anciennes versions Spark.]

Eh bien, cela semble incroyable, mais en effet, il n'y a aucun moyen d'extraire de telles informations d'une décomposition PCA (au moins à partir de Spark 1.5). Mais encore une fois, il y a eu beaucoup de "plaintes" similaires - voir ici , par exemple, pour ne pas pouvoir extraire les meilleurs paramètres d'un CrossValidatorModel.

Heureusement, il y a quelques mois, j'ai assisté au 'Scalable Machine Learning' MOOC par AMPLab (Berkeley) & Databricks, c'est-à-dire les créateurs de Spark, où nous avons implémenté un pipeline PCA complet 'à la main' dans le cadre de les devoirs. J'ai modifié mes fonctions à l'époque (rassurez-vous, j'ai obtenu le plein crédit :-), afin de travailler avec des trames de données comme entrées (au lieu de RDD), du même format que le vôtre (c'est-à-dire des rangées de DenseVectors contenant les caractéristiques numériques).

Nous devons d'abord définir une fonction intermédiaire, estimatedCovariance, comme suit:

import numpy as np

def estimateCovariance(df):
    """Compute the covariance matrix for a given dataframe.

    Note:
        The multi-dimensional covariance array should be calculated using outer products.  Don't
        forget to normalize the data by first subtracting the mean.

    Args:
        df:  A Spark dataframe with a column named 'features', which (column) consists of DenseVectors.

    Returns:
        np.ndarray: A multi-dimensional array where the number of rows and columns both equal the
            length of the arrays in the input dataframe.
    """
    m = df.select(df['features']).map(lambda x: x[0]).mean()
    dfZeroMean = df.select(df['features']).map(lambda x:   x[0]).map(lambda x: x-m)  # subtract the mean

    return dfZeroMean.map(lambda x: np.outer(x,x)).sum()/df.count()

Ensuite, nous pouvons écrire une fonction principale pca comme suit:

from numpy.linalg import eigh

def pca(df, k=2):
    """Computes the top `k` principal components, corresponding scores, and all eigenvalues.

    Note:
        All eigenvalues should be returned in sorted order (largest to smallest). `eigh` returns
        each eigenvectors as a column.  This function should also return eigenvectors as columns.

    Args:
        df: A Spark dataframe with a 'features' column, which (column) consists of DenseVectors.
        k (int): The number of principal components to return.

    Returns:
        Tuple of (np.ndarray, RDD of np.ndarray, np.ndarray): A Tuple of (eigenvectors, `RDD` of
        scores, eigenvalues).  Eigenvectors is a multi-dimensional array where the number of
        rows equals the length of the arrays in the input `RDD` and the number of columns equals
        `k`.  The `RDD` of scores has the same number of rows as `data` and consists of arrays
        of length `k`.  Eigenvalues is an array of length d (the number of features).
     """
    cov = estimateCovariance(df)
    col = cov.shape[1]
    eigVals, eigVecs = eigh(cov)
    inds = np.argsort(eigVals)
    eigVecs = eigVecs.T[inds[-1:-(col+1):-1]]  
    components = eigVecs[0:k]
    eigVals = eigVals[inds[-1:-(col+1):-1]]  # sort eigenvals
    score = df.select(df['features']).map(lambda x: x[0]).map(lambda x: np.dot(x, components.T) )
    # Return the `k` principal components, `k` scores, and all eigenvalues

    return components.T, score, eigVals

Test

Voyons d'abord les résultats avec la méthode existante, en utilisant les données d'exemple de la Spark ML PCA documentation (en les modifiant pour être toutes DenseVectors ):

 from pyspark.ml.feature import *
 from pyspark.mllib.linalg import Vectors
 data = [(Vectors.dense([0.0, 1.0, 0.0, 7.0, 0.0]),),
         (Vectors.dense([2.0, 0.0, 3.0, 4.0, 5.0]),),
         (Vectors.dense([4.0, 0.0, 0.0, 6.0, 7.0]),)]
 df = sqlContext.createDataFrame(data,["features"])
 pca_extracted = PCA(k=2, inputCol="features", outputCol="pca_features")
 model = pca_extracted.fit(df)
 model.transform(df).collect()

 [Row(features=DenseVector([0.0, 1.0, 0.0, 7.0, 0.0]), pca_features=DenseVector([1.6486, -4.0133])),
  Row(features=DenseVector([2.0, 0.0, 3.0, 4.0, 5.0]), pca_features=DenseVector([-4.6451, -1.1168])),
  Row(features=DenseVector([4.0, 0.0, 0.0, 6.0, 7.0]), pca_features=DenseVector([-6.4289, -5.338]))]

Ensuite, avec notre méthode:

 comp, score, eigVals = pca(df)
 score.collect()

 [array([ 1.64857282,  4.0132827 ]),
  array([-4.64510433,  1.11679727]),
  array([-6.42888054,  5.33795143])]

Permettez-moi de souligner que nous n'utilisons aucune méthode collect() dans les fonctions que nous avons définies - score is un RDD, comme il se doit.

Notez que les signes de notre deuxième colonne sont tous opposés à ceux dérivés par la méthode existante; mais ce n'est pas un problème: selon le (téléchargeable gratuitement) An Introduction to Statistical Learning , co-écrit par Hastie & Tibshirani, p. 382

Chaque vecteur de chargement de composant principal est unique, jusqu'à un retournement de signe. Cela signifie que deux progiciels différents produiront les mêmes vecteurs de chargement de composants principaux, bien que les signes de ces vecteurs de chargement puissent différer. Les signes peuvent différer car chaque vecteur de chargement de composant principal spécifie une direction dans l'espace p-dimensionnel: le retournement du signe n'a aucun effet car la direction ne change pas. [...] De même, les vecteurs de score sont uniques jusqu'à un flip de signe, puisque la variance de Z est la même que la variance de −Z.

Enfin, maintenant que nous avons les valeurs propres disponibles, il est trivial d'écrire une fonction pour le pourcentage de la variance expliqué:

 def varianceExplained(df, k=1):
     """Calculate the fraction of variance explained by the top `k` eigenvectors.

     Args:
         df: A Spark dataframe with a 'features' column, which (column) consists of DenseVectors.
         k: The number of principal components to consider.

     Returns:
         float: A number between 0 and 1 representing the percentage of variance explained
             by the top `k` eigenvectors.
     """
     components, scores, eigenvalues = pca(df, k)  
     return sum(eigenvalues[0:k])/sum(eigenvalues)


 varianceExplained(df,1)
 # 0.79439325322305299

À titre de test, nous vérifions également si la variance expliquée dans nos données d'exemple est de 1,0, pour k = 5 (puisque les données d'origine sont à 5 dimensions):

 varianceExplained(df,5)
 # 1.0

Cela devrait faire votre travail efficacement ; n'hésitez pas à apporter les clarifications dont vous pourriez avoir besoin.

[Développé et testé avec Spark 1.5.0 & 1.5.1]

28
desertnaut

MODIFIER:

PCA et SVD sont enfin disponibles dans pyspark à partir de spark 2.2. 0 selon ce ticket JIRA résolu SPARK-6227 .

Réponse originale:

La réponse donnée par @desertnaut est en fait excellente d'un point de vue théorique, mais je voulais présenter une autre approche sur la façon de calculer la SVD et d'extraire ensuite les vecteurs propres.

from pyspark.mllib.common import callMLlibFunc, JavaModelWrapper
from pyspark.mllib.linalg.distributed import RowMatrix

class SVD(JavaModelWrapper):
    """Wrapper around the SVD scala case class"""
    @property
    def U(self):
        """ Returns a RowMatrix whose columns are the left singular vectors of the SVD if computeU was set to be True."""
        u = self.call("U")
        if u is not None:
        return RowMatrix(u)

    @property
    def s(self):
        """Returns a DenseVector with singular values in descending order."""
        return self.call("s")

    @property
    def V(self):
        """ Returns a DenseMatrix whose columns are the right singular vectors of the SVD."""
        return self.call("V")

Ceci définit notre objet SVD. Nous pouvons maintenant définir notre méthode computeSVD en utilisant le Java Wrapper.

def computeSVD(row_matrix, k, computeU=False, rCond=1e-9):
    """
    Computes the singular value decomposition of the RowMatrix.
    The given row matrix A of dimension (m X n) is decomposed into U * s * V'T where
    * s: DenseVector consisting of square root of the eigenvalues (singular values) in descending order.
    * U: (m X k) (left singular vectors) is a RowMatrix whose columns are the eigenvectors of (A X A')
    * v: (n X k) (right singular vectors) is a Matrix whose columns are the eigenvectors of (A' X A)
    :param k: number of singular values to keep. We might return less than k if there are numerically zero singular values.
    :param computeU: Whether of not to compute U. If set to be True, then U is computed by A * V * sigma^-1
    :param rCond: the reciprocal condition number. All singular values smaller than rCond * sigma(0) are treated as zero, where sigma(0) is the largest singular value.
    :returns: SVD object
    """
    Java_model = row_matrix._Java_matrix_wrapper.call("computeSVD", int(k), computeU, float(rCond))
    return SVD(Java_model)

Maintenant, appliquons cela à un exemple:

from pyspark.ml.feature import *
from pyspark.mllib.linalg import Vectors

data = [(Vectors.dense([0.0, 1.0, 0.0, 7.0, 0.0]),), (Vectors.dense([2.0, 0.0, 3.0, 4.0, 5.0]),), (Vectors.dense([4.0, 0.0, 0.0, 6.0, 7.0]),)]
df = sqlContext.createDataFrame(data,["features"])

pca_extracted = PCA(k=2, inputCol="features", outputCol="pca_features")

model = pca_extracted.fit(df)
features = model.transform(df) # this create a DataFrame with the regular features and pca_features

# We can now extract the pca_features to prepare our RowMatrix.
pca_features = features.select("pca_features").rdd.map(lambda row : row[0])
mat = RowMatrix(pca_features)

# Once the RowMatrix is ready we can compute our Singular Value Decomposition
svd = computeSVD(mat,2,True)
svd.s
# DenseVector([9.491, 4.6253])
svd.U.rows.collect()
# [DenseVector([0.1129, -0.909]), DenseVector([0.463, 0.4055]), DenseVector([0.8792, -0.0968])]
svd.V
# DenseMatrix(2, 2, [-0.8025, -0.5967, -0.5967, 0.8025], 0)
14
eliasah

Dans spark 2.2+, vous pouvez maintenant facilement obtenir la variance expliquée comme suit:

from pyspark.ml.feature import VectorAssembler
assembler = VectorAssembler(inputCols=<columns of your original dataframe>, outputCol="features")
df = assembler.transform(<your original dataframe>).select("features")
from pyspark.ml.feature import PCA
pca = PCA(k=10, inputCol="features", outputCol="pcaFeatures")
model = pca.fit(df)
sum(model.explainedVariance)
5
Sameer Mahajan

La réponse la plus simple à votre question est de saisir une matrice d'identité dans votre modèle.

identity_input = [(Vectors.dense([1.0, .0, 0.0, .0, 0.0]),),(Vectors.dense([.0, 1.0, .0, .0, .0]),), \
              (Vectors.dense([.0, 0.0, 1.0, .0, .0]),),(Vectors.dense([.0, 0.0, .0, 1.0, .0]),),
              (Vectors.dense([.0, 0.0, .0, .0, 1.0]),)]
df_identity = sqlContext.createDataFrame(identity_input,["features"])
identity_features = model.transform(df_identity)

Cela devrait vous donner des composants principaux.

Je pense que la réponse d'Eliasah est meilleure en termes de cadre Spark parce que desertnaut résout le problème en utilisant les fonctions de Numpy au lieu des actions de Spark. Cependant, la réponse d'Eliaah manque de normaliser les données. Donc, je ajoutez les lignes suivantes à la réponse d'Eliasah:

from pyspark.ml.feature import StandardScaler
standardizer = StandardScaler(withMean=True, withStd=False,
                          inputCol='features',
                          outputCol='std_features')
model = standardizer.fit(df)
output = model.transform(df)
pca_features = output.select("std_features").rdd.map(lambda row : row[0])
mat = RowMatrix(pca_features)
svd = computeSVD(mat,5,True)

Evidemment, svd.V et identity_features.select ("pca_features"). Collect () devraient avoir des valeurs identiques.

Edit: j'ai résumé PCA et son utilisation dans Spark et sklearn dans ce ici

2
sergulaydore