EVoC: Embedding Vector Oriented Clustering

repository·main·Indexed 43 days ago

https://github.com/tutteinstitute/evoc

A clustering library for high-dimensional embeddings, version 0.3.1. It provides tools for node embedding, label propagation, and graph construction, including K-Nearest Neighbor graphs and Numba-optimized KD-Trees. The core API features the EVoC class for high-level clustering tasks and utilities for cluster tree operations, peak identification, and performance benchmarking against KMeans and HDBSCAN.

Tokens
26.3K
Snippets
80
Records
112
Agent score
55%

What's inside EVoC

  1. Key Features of EVoC

    main

    EVoC provides several advantages for clustering dense vector representations:

    • Optimized for High-Dimensional Embeddings: Specifically designed for modern embedding vectors.
    • Multi-Layer Clustering: Provides hierarchical clustering with multiple granularity levels.
    • Performance Optimized: Uses Numba for high-performance computation.
    • Flexible Parameters: Extensive parameter set for fine-tuning clustering behavior.
    • Scikit-learn Compatible: Follows standard scikit-learn API conventions.
  2. How EVoC works: Algorithm Overview

    main

    EVoC (Embedding Vector Oriented Clustering) is a two-part algorithm designed specifically for clustering high-dimensional embedding vectors. It combines:

    1. Graph Embedding: Constructs a k-nearest neighbor (k-NN) graph and learns a lower-dimensional embedding (similar to UMAP).
    2. Density Clustering: Applies hierarchical density-based clustering to the resulting embedding (similar to HDBSCAN and PLSCAN).

    This approach is optimized for performance and quality when handling dense, high-dimensional data, providing multiple clustering granularities through hierarchical layers and robust handling of noise and outliers.

  3. Perform hierarchical clustering analysis

    main

    EVoC supports hierarchical analysis by allowing you to inspect multiple layers of clustering. By setting max_layers in the constructor, you can access:

    • cluster_layers_: An array containing the cluster assignments for each layer.
    • persistence_scores_: The persistence score for each layer.
    • cluster_tree_: The hierarchical tree structure representing the relationships between clusters.
    # Get multiple clustering granularities
    clusterer = EVoC(max_layers=5)
    clusterer.fit(X)
    
    # Analyze each layer
    for i, layer in enumerate(clusterer.cluster_layers_):
        n_clusters = len(np.unique(layer[layer >= 0]))
        persistence = clusterer.persistence_scores_[i]
    
        print(f"Layer {i}: {n_clusters} clusters, "
              f"persistence: {persistence:.3f}")
    
    # Access the hierarchical structure
    tree = clusterer.cluster_tree_
    print(f"Hierarchical structure: {tree}")
  4. Working with Hierarchical Output and Cluster Trees

    main

    EVoC produces multiple clustering layers. Layer 0 is the most fine-grained (determined by base_min_cluster_size or base_n_clusters). Subsequent layers are coarser.

    To analyze the hierarchy:

    1. Iterate through layers: Use clusterer.cluster_layers_ to inspect clusters, noise points, and stability.
    2. Check stability: Use clusterer.persistence_scores_ to see how robust each layer is.
    3. Analyze structure: Use clusterer.cluster_tree_ to see how clusters evolve across layers.

    Tip: You can create a "mixed" resolution layer by selecting specific clusters at a coarse layer and including their children from lower layers to get fine-grained detail in specific regions.

    clusterer = EVoC(max_layers=5)
    clusterer.fit(X)
    
    # Explore different granularities
    for i, layer in enumerate(clusterer.cluster_layers_):
        n_clusters = len(np.unique(layer[layer >= 0]))
        n_noise = np.sum(layer == -1)
        persistence = clusterer.persistence_scores_[i]
    
        print(f"Layer {i}: {n_clusters} clusters, {n_noise} noise points, "
              f"persistence: {persistence:.3f}")
    
    # Use cluster tree for hierarchical analysis
    tree = clusterer.cluster_tree_
    # ... analyze hierarchical structure ...
  5. Optimize EVoC for large datasets

    main

    When processing large datasets, you can improve memory efficiency by:

    1. Reducing graph density with a lower n_neighbors.
    2. Lowering the node_embedding_dim.
    3. Reducing the number of training epochs with n_epochs.
    4. Limiting hierarchy depth with max_layers.
    5. Processing data in chunks (though note that fit_predict on chunks will return labels for those specific chunks).
    # For large datasets, use smaller parameters
    clusterer = EVoC(
        n_neighbors=10,        # Reduce graph density
        node_embedding_dim=8,  # Lower embedding dimension  
        n_epochs=30,          # Fewer training epochs
        max_layers=3          # Limit hierarchy depth
    )
    
    # Process in chunks if needed
    chunk_size = 10000
    all_labels = []
    
    for i in range(0, len(X), chunk_size):
        chunk = X[i:i+chunk_size]
        chunk_labels = clusterer.fit_predict(chunk)
        all_labels.extend(chunk_labels)
  6. Quick Start with EVoC

    main

    EVoC (Embedding Vector Oriented Clustering) is designed for high-dimensional embedding vectors (e.g., CLIP, sentence-transformers). It follows the scikit-learn API convention, allowing you to initialize a clusterer and use fit_predict to obtain cluster labels. The algorithm provides multi-layer clustering, which can be inspected via the cluster_layers_ attribute.

    from evoc import EVoC
    import numpy as np
    
    # Generate sample data
    X = np.random.rand(1000, 512)  # 1000 samples, 512-dimensional embeddings
    
    # Initialize and fit the clusterer
    clusterer = EVoC()
    labels = clusterer.fit_predict(X)
    
    # Access cluster layers and membership strengths
    print(f"Number of clusters: {len(np.unique(labels[labels >= 0]))}")
    print(f"Number of cluster layers: {len(clusterer.cluster_layers_)}")
  7. Build the EVoC documentation

    main

    You can build the documentation using automated scripts or manual Sphinx commands.

    Quick Build

    For a fast build using the provided scripts:

    • Unix/macOS: Run ./build_docs.sh from the doc/ directory.
    • Windows: Run build_docs.bat from the doc/ directory.

    Manual Build

    To build manually using make:

    1. Install dependencies: pip install -r requirements.txt
    2. Install EVoC in development mode: pip install -e ../..
    3. Build the HTML: make html
    4. View the result in build/html/index.html.
    # Unix/macOS Quick Build
    cd doc
    ./build_docs.sh
    
    # Windows Quick Build
    cd doc
    build_docs.bat
    
    # Manual Build
    pip install -r requirements.txt
    pip install -e ../..
    make html
  8. Add new documentation pages

    main

    To add new content to the documentation:

    1. Create new .rst (reStructuredText) files in the source/ directory.
    2. Register the new files by adding them to the toctree (Table of Contents tree) in source/index.rst.
    3. Rebuild the documentation to see the changes.
  9. Add Jupyter Notebooks to documentation

    main

    The documentation uses Nbsphinx to integrate Jupyter notebooks. To include a notebook:

    1. Place your .ipynb files in source/notebooks/.
    2. Add the notebook files to source/notebooks/index.rst.
    3. During the build process, notebooks are automatically converted to documentation.

    Note on execution: By default, notebooks are NOT executed during the build (nbsphinx_execute = 'never'). To force execution of all cells during the build, change this setting to always in conf.py.

  10. Assess cluster quality and stability

    main

    To validate your clustering results, you can use several approaches:

    Quality Metrics: Use sklearn.metrics (e.g., silhouette_score, calinski_harabasz_score, davies_bouldin_score) on the labels. You can also inspect clusterer.membership_strengths_ to see how strongly points belong to their assigned clusters.

    Stability Analysis: Run the clustering multiple times with different random_state values to see if the resulting scores are consistent.

    from sklearn.metrics import (
        silhouette_score, 
        calinski_harabasz_score,
        davies_bouldin_score
    )
    
    # Fit the clusterer
    labels = clusterer.fit_predict(X)
    
    # Calculate quality metrics
    if len(np.unique(labels[labels >= 0])) > 1:
        silhouette = silhouette_score(X, labels)
        calinski_harabasz = calinski_harabasz_score(X, labels)  
        davies_bouldin = davies_bouldin_score(X, labels)
    
        print(f"Silhouette Score: {silhouette:.3f}")
        print(f"Calinski-Harabasz Score: {calinski_harabasz:.3f}")
        print(f"Davies-Bouldin Score: {davies_bouldin:.3f}")
    
    # Analyze membership strengths
    strengths = clusterer.membership_strengths_
    print(f"Average membership strength: {np.mean(strengths):.3f}")
    print(f"Std of membership strengths: {np.std(strengths):.3f}")
    
    # Test clustering stability across random seeds
    stability_scores = []
    for seed in range(10):
        clusterer = EVoC(random_state=seed)
        labels = clusterer.fit_predict(X)
        if len(np.unique(labels[labels >= 0])) > 1:
            score = silhouette_score(X, labels)
            stability_scores.append(score)
    
    print(f"Mean stability: {np.mean(stability_scores):.3f}")
    print(f"Std stability: {np.std(stability_scores):.3f}")