web-dev-qa-db-fra.com

Dendrogramme de tracé en utilisant sklearn.AgglomerativeClustering

J'essaie de créer un dendrogramme en utilisant l'attribut children_ fourni par AgglomerativeClustering, mais jusqu'à présent, je n'ai pas de chance. Je ne peux pas utiliser scipy.cluster car le regroupement agglomérant fourni dans scipy manque de certaines options qui sont importantes pour moi (comme l'option permettant de spécifier le nombre de clusters). Je serais vraiment reconnaissant pour tout conseil là-bas. 

    import sklearn.cluster
    clstr = cluster.AgglomerativeClustering(n_clusters=2)
    clusterer.children_
33
Shukhrat Khannanov

Voici une fonction simple pour prendre un modèle de classification hiérarchique de sklearn et le représenter à l’aide de la fonction scipy dendrogram. On dirait que les fonctions graphiques ne sont souvent pas directement prises en charge dans sklearn. Vous pouvez trouver une discussion intéressante sur celle liée à la demande d'extraction de cet extrait de code plot_dendrogramici .

Je précise que le cas d'utilisation que vous décrivez (définition du nombre de clusters) est disponible dans scipy: après avoir effectué la classification hiérarchique à l'aide de la variable linkage de scipy, vous pouvez couper la hiérarchie en un nombre quelconque de clusters à l'aide de fcluster avec le nombre de clusters spécifié dans les arguments t et criterion='maxclust'.

9
David Diaz

Utilisez plutôt la mise en œuvre scipy du clustering agglomérant. Voici un exemple. 

from scipy.cluster.hierarchy import dendrogram, linkage

data = [[0., 0.], [0.1, -0.1], [1., 1.], [1.1, 1.1]]

Z = linkage(data)

dendrogram(Z)  

Vous pouvez trouver la documentation pour linkageici et la documentation pour dendrogramici

6
sebastianspiegel

Je suis tombé sur le même problème il y a quelque temps. J'ai réussi à tracer ce fichu dendogramme en utilisant le logiciel ete3 . Ce paquet est capable de tracer de manière flexible des arbres avec diverses options. La seule difficulté consistait à convertir la sortie children_ de sklearn au format Newick Tree pouvant être lue et comprise par ete3. De plus, je dois calculer manuellement l'étendue de la dendrite car cette information n'a pas été fournie avec le children_. Voici un extrait du code que j'ai utilisé. Il calcule l'arborescence de Newick, puis affiche la structure de données de l'arborescence ete3. Pour plus de détails sur la façon de tracer, jetez un oeil ici

import numpy as np
from sklearn.cluster import AgglomerativeClustering
import ete3

def build_Newick_tree(children,n_leaves,X,leaf_labels,spanner):
    """
    build_Newick_tree(children,n_leaves,X,leaf_labels,spanner)

    Get a string representation (Newick tree) from the sklearn
    AgglomerativeClustering.fit output.

    Input:
        children: AgglomerativeClustering.children_
        n_leaves: AgglomerativeClustering.n_leaves_
        X: parameters supplied to AgglomerativeClustering.fit
        leaf_labels: The label of each parameter array in X
        spanner: Callable that computes the dendrite's span

    Output:
        ntree: A str with the Newick tree representation

    """
    return go_down_tree(children,n_leaves,X,leaf_labels,len(children)+n_leaves-1,spanner)[0]+';'

def go_down_tree(children,n_leaves,X,leaf_labels,nodename,spanner):
    """
    go_down_tree(children,n_leaves,X,leaf_labels,nodename,spanner)

    Iterative function that traverses the subtree that descends from
    nodename and returns the Newick representation of the subtree.

    Input:
        children: AgglomerativeClustering.children_
        n_leaves: AgglomerativeClustering.n_leaves_
        X: parameters supplied to AgglomerativeClustering.fit
        leaf_labels: The label of each parameter array in X
        nodename: An int that is the intermediate node name whos
            children are located in children[nodename-n_leaves].
        spanner: Callable that computes the dendrite's span

    Output:
        ntree: A str with the Newick tree representation

    """
    nodeindex = nodename-n_leaves
    if nodename<n_leaves:
        return leaf_labels[nodeindex],np.array([X[nodeindex]])
    else:
        node_children = children[nodeindex]
        branch0,branch0samples = go_down_tree(children,n_leaves,X,leaf_labels,node_children[0])
        branch1,branch1samples = go_down_tree(children,n_leaves,X,leaf_labels,node_children[1])
        node = np.vstack((branch0samples,branch1samples))
        branch0span = spanner(branch0samples)
        branch1span = spanner(branch1samples)
        nodespan = spanner(node)
        branch0distance = nodespan-branch0span
        branch1distance = nodespan-branch1span
        nodename = '({branch0}:{branch0distance},{branch1}:{branch1distance})'.format(branch0=branch0,branch0distance=branch0distance,branch1=branch1,branch1distance=branch1distance)
        return nodename,node

def get_cluster_spanner(aggClusterer):
    """
    spanner = get_cluster_spanner(aggClusterer)

    Input:
        aggClusterer: sklearn.cluster.AgglomerativeClustering instance

    Get a callable that computes a given cluster's span. To compute
    a cluster's span, call spanner(cluster)

    The cluster must be a 2D numpy array, where the axis=0 holds
    separate cluster members and the axis=1 holds the different
    variables.

    """
    if aggClusterer.linkage=='ward':
        if aggClusterer.affinity=='euclidean':
            spanner = lambda x:np.sum((x-aggClusterer.pooling_func(x,axis=0))**2)
    Elif aggClusterer.linkage=='complete':
        if aggClusterer.affinity=='euclidean':
            spanner = lambda x:np.max(np.sum((x[:,None,:]-x[None,:,:])**2,axis=2))
        Elif aggClusterer.affinity=='l1' or aggClusterer.affinity=='manhattan':
            spanner = lambda x:np.max(np.sum(np.abs(x[:,None,:]-x[None,:,:]),axis=2))
        Elif aggClusterer.affinity=='l2':
            spanner = lambda x:np.max(np.sqrt(np.sum((x[:,None,:]-x[None,:,:])**2,axis=2)))
        Elif aggClusterer.affinity=='cosine':
            spanner = lambda x:np.max(np.sum((x[:,None,:]*x[None,:,:]))/(np.sqrt(np.sum(x[:,None,:]*x[:,None,:],axis=2,keepdims=True))*np.sqrt(np.sum(x[None,:,:]*x[None,:,:],axis=2,keepdims=True))))
        else:
            raise AttributeError('Unknown affinity attribute value {0}.'.format(aggClusterer.affinity))
    Elif aggClusterer.linkage=='average':
        if aggClusterer.affinity=='euclidean':
            spanner = lambda x:np.mean(np.sum((x[:,None,:]-x[None,:,:])**2,axis=2))
        Elif aggClusterer.affinity=='l1' or aggClusterer.affinity=='manhattan':
            spanner = lambda x:np.mean(np.sum(np.abs(x[:,None,:]-x[None,:,:]),axis=2))
        Elif aggClusterer.affinity=='l2':
            spanner = lambda x:np.mean(np.sqrt(np.sum((x[:,None,:]-x[None,:,:])**2,axis=2)))
        Elif aggClusterer.affinity=='cosine':
            spanner = lambda x:np.mean(np.sum((x[:,None,:]*x[None,:,:]))/(np.sqrt(np.sum(x[:,None,:]*x[:,None,:],axis=2,keepdims=True))*np.sqrt(np.sum(x[None,:,:]*x[None,:,:],axis=2,keepdims=True))))
        else:
            raise AttributeError('Unknown affinity attribute value {0}.'.format(aggClusterer.affinity))
    else:
        raise AttributeError('Unknown linkage attribute value {0}.'.format(aggClusterer.linkage))
    return spanner

clusterer = AgglomerativeClustering(n_clusters=2,compute_full_tree=True) # You can set compute_full_tree to 'auto', but I left it this way to get the entire tree plotted
clusterer.fit(X) # X for whatever you want to fit
spanner = get_cluster_spanner(clusterer)
newick_tree = build_Newick_tree(clusterer.children_,clusterer.n_leaves_,X,leaf_labels,spanner) # leaf_labels is a list of labels for each entry in X
tree = ete3.Tree(newick_tree)
tree.show()
3
lucianopaz

Pour ceux qui souhaitent quitter Python et utiliser la bibliothèque robuste D3, il n’est pas très difficile d’utiliser les API d3.cluster() (ou, je suppose, d3.tree()) pour obtenir un résultat personnalisable.

Voir le jsfiddle pour une démonstration.

Le tableau children_ fonctionne heureusement facilement comme un tableau JS, et la seule étape intermédiaire consiste à utiliser d3.stratify() pour le transformer en une représentation hiérarchique. Plus précisément, nous avons besoin que chaque nœud ait une id et une parentId:

var N = 272;  // Your n_samples/corpus size.
var root = d3.stratify()
  .id((d,i) => i + N)
  .parentId((d, i) => {
    var parIndex = data.findIndex(e => e.includes(i + N));
    if (parIndex < 0) {
      return; // The root should have an undefined parentId.
    }
    return parIndex + N;
  })(data); // Your children_

Vous vous retrouvez avec au moins un comportement O (n ^ 2) ici en raison de la ligne findIndex, mais cela n'a probablement pas d'importance jusqu'à ce que votre n_samples devienne énorme, auquel cas, vous pourriez précalculer un index plus efficace.

Au-delà, c’est à peu près l'utilisation de d3.cluster() dans les moindres détails. Voir le bloc canonique de mbostock ou mon JSFiddle.

N.B. Pour mon cas d'utilisation, il suffisait simplement de montrer les nœuds non-feuilles; il est un peu plus difficile de visualiser les échantillons/feuilles, car ceux-ci peuvent ne pas tous être explicitement dans le tableau children_.

0
jagthebeetle