23  Algorithme K-means

L’algortihme K-means est un algorithme de clustering. À partir d’un ensemble d’exemples non étiquetés S = \{\boldsymbol{x}_1, \dots, \boldsymbol{x}_m\} il va trouver une répartition de ceux-ci en K groupes G_k, k=1,\dots, K, mais aussi les prototypes de ces groupes, c’est-à-dire des exemples (artificiels) et représentatifs des données de chaque groupe. Ces prototypes correspondent en fait simplement aux centres des groupes \boldsymbol{c}_k.

23.1 Détails de l’algorithme

  1. Initialiser les K centres c_k des groupes (typiquement aléatoirement)
  2. Classer les exemples avec y_i = \arg\min_{k\in\{1,\dots, K\}} \|\boldsymbol{x}_i - \boldsymbol{c}_k\| \tag{23.1} G_k = \{i : y_i = k\}
  3. Mettre à jour les centres des groupes avec \boldsymbol{c}_k = \frac{1}{|G_k|} \sum_{\boldsymbol{x}_i\in G_k} \boldsymbol{x}_i \tag{23.2}|G_k| désigne le cardinal du groupe G_k (le nombre d’exemples affectés à ce groupe).
  4. Répéter depuis 2. jusqu’à convergence.

def kmeans(X, K):
    m, d = X.shape
    C = np.min(X) + (np.random.rand(K,d)  * (np.max(X)-np.min(X)))
    
    Cprecedent = np.zeros(C.shape)
    Distances = np.zeros((m,K))
    history = []
    while np.linalg.norm(C - Cprecedent) > 1e-6:
        Cprecedent = np.copy(C)
        
        # Classer les données:
        for k in range(K):
            Distances[:,k] = np.linalg.norm(X-C[k,:], axis=1)
        y = np.argmin(Distances, axis=1)
        
        history.append([np.copy(y),np.copy(C)])
                
        # Mettre à jour les centres
        for k in range(K):
            C[k,:] = np.mean(X[y==k,:], axis=0)     
    return y, C, history
    
#### Test ###
X = np.random.rand(100,2) 
y = np.random.rand(100) > 0.5
X[y,:] += np.array([2,2])
plt.plot(X[:,0], X[:,1], "ok")
plt.title("Les données de départ")

ypred, C, history = kmeans(X, 2)
plt.figure()
plt.plot(X[ypred==0,0], X[ypred==0,1], 'ob', alpha=0.5)
plt.plot(X[ypred==1,0], X[ypred==1,1], 'or', alpha=0.5)
plt.plot(C[0,0], C[0,1], "*b", markersize=20)
plt.plot(C[1,0], C[1,1], "*r", markersize=20)
plt.title("La solution avec les centres des 2 groupes (étoiles)")   

for t in range(len(history)):
    ypred, C = history[t]
    plt.figure()
    plt.plot(X[ypred==0,0], X[ypred==0,1], 'ob', alpha=0.5)
    plt.plot(X[ypred==1,0], X[ypred==1,1], 'or', alpha=0.5)
    plt.plot(C[0,0], C[0,1], "*b", markersize=20)
    plt.plot(C[1,0], C[1,1], "*r", markersize=20)
    plt.title("Itération " + str(t))

Pour être complet, l’algorithme devrait inclure la gestion des cas où aucun point n’est affecté à un groupe. Typiquement, il est possible de réinitialiser le centre du groupe aléatoirement ou de supprimer complétement le groupe du reste de la procédure.

23.2 Critère optimisé par K-means

L’algorithme K-means est une méthode d’optimisation alternée pour minimiser le critère J((\boldsymbol{c}_k)_{k=1}^K, (G_k)_{k=1}^K) = \sum_{k=1}^K \sum_{i\in G_k} \|\boldsymbol{x}_i - \boldsymbol{c}_k\|^2 qui mesure la somme de toutes les distances des points \boldsymbol{x}_i au centre \boldsymbol{c}_k du groupe G_k auquel il est affecté.

L’algorithme alterne en fait entre

  1. la minimisation de cette fonction par rapport aux groupes G_k (à centres fixés),
  2. puis la minimisation par rapport aux centres \boldsymbol{c}_k (à groupes G_k fixés).

Il est aisé de voir que, pour minimiser ce critère, il est nécessaire d’affecter chaque point \boldsymbol{x}_i au centre \boldsymbol{c}_k le plus proche, ce qui donne la formule de classification Équation 23.1.

Pour minimiser par rapport aux centres, on remarque que le critère est une somme de K termes, chacun n’impliquant qu’un centre \boldsymbol{c}_k de manière indépendante. Il suffit donc de résoudre les K sous-problèmes d’optimisation \min_{\boldsymbol{c}_k} J_k(\boldsymbol{c}_k),\qquad J_k(\boldsymbol{c}_k) = \sum_{i\in G_k} \|\boldsymbol{x}_i - \boldsymbol{c}_k\|^2 Le gradient de l’objectif J_k(\boldsymbol{c}_k) est ici \frac{\text{d} J_k(\boldsymbol{c}_k)}{\text{d} \boldsymbol{c}_k} = - 2 \sum_{i\in G_k} (\boldsymbol{x}_i - \boldsymbol{c}_k) et la solution est obtenue lorsque ce gradient est nul, ce qui donne |G_k| \boldsymbol{c}_k = \sum_{i\in G_k} \boldsymbol{x}_i et la formule Équation 23.2 qui calcule \boldsymbol{c}_k comme la moyenne des points.

On peut montrer qu’à chaque itération de l’algorithme, le critère J((\boldsymbol{c}_k)_{k=1}^K, (G_k)_{k=1}^K) décroît de manière monotone. Cependant, aucune garantie n’est donnée sur l’optimalité de la solution, car la solution converge vers un point qui dépend fortement de l’initialisation.

def J(C, y):
    cost = 0
    for k in range(len(C)):
        cost += np.sum( ( X[y==k,:] - C[k,:] )**2 )
    return cost
    
# Données en grande dimension
m = 200
d = 20
K = 5
X = np.random.randn(m,d) 
y = np.random.randint(0,K,size=m)
Copt = np.zeros((K,d))
for k in range(K):
    X[y==k,:] += 3 * k * np.ones(d)/np.sqrt(d)
    Copt[k,:] = np.mean(X[y==k,:],0)

print("Coût optimal =", J(Copt,y))

ypred, C, _ = kmeans(X, K)
print("Coût de la solution K-means =", J(C,ypred))
Coût optimal = 3858.100221643007
Coût de la solution K-means = 3919.2812324658676

23.3 Amélioration par multiples initialisations

Pour améliorer la qualité de la solution, une approche simple consiste à répéter l’algorithme N fois à partir d’initialisation aléatoires et différentes pour ne conserver au final que la solution associée à la plus petite valeur de J((\boldsymbol{c}_k)_{k=1}^K, (G_k)_{k=1}^K).

Cependant, le nombre d’initialisations requis croît rapidement avec la dimension du problème, comme prédit par la malédiction de la dimension.

23.4 K-means++ : initialisation dispersée

L’algorithme K-means++, proposé par Arthur et Vassilvitskii (2007), fournit une initialisation particulièrement efficace pour K-means. Celle-ci repose sur l’idée que les résultats de K-means sont meilleurs lorsque les centres initiaux sont bien dispersés dans l’espace.

K-means++ choisi les centres initiaux parmi les m points \boldsymbol{x}_i de la base d’apprentissage ainsi :

  1. Tirer \boldsymbol{c}_1 aléatoirement de manière uniforme parmi tous les exemples \boldsymbol{x}_i, i=1,\dots, m.
  2. Pour k=2,\dots,K :
  • Pour chaque point \boldsymbol{x}_i, calculer la distance au centre le plus proche D_i = \min_{1\leq j< k} \|\boldsymbol{x}_i - \boldsymbol{c}_k\|
  • Choisir \boldsymbol{c}_k aléatoirement selon la loi de probabilité P(\boldsymbol{c}_k=\boldsymbol{x}_i) = \frac{D_i^2}{ \sum_{i} D_i^2}