Función K-means en Python

Oct 02 2020

Escribí una función k-means en Python para comprender la metodología. Estoy tratando de usar esto en un conjunto de datos más complejo con un valor mayor para k, pero se está ejecutando muy lento. ¿Alguien tiene alguna recomendación sobre cómo puedo mejorar esto? He proporcionado el código a continuación, junto con la carga en un conjunto de datos de ejemplo y la aplicación del algoritmo.

def Euc(x,y):
    return math.sqrt(sum([(a - b) ** 2 for a,b in zip(x,y)]))

def K_means(TE,k):
    Iteration = 0
    R = []
    O_a = []
    Epoch = 0
    Tol = 1
    Old_Tol = 2
    Tol_r = []
    start_time = time.time()
    mean_cl = [[random.uniform(TE.iloc[i].min(),TE.iloc[i].max()) for i in range(len(TE.columns))] for c in range(0,k)]
    for n in range(len(TE)):
        D = [Euc(TE.iloc[n].tolist(),mean_cl[c]) for c in range(0,k)]  
        O_a.append(D.index(min(D)))
    while(abs(Old_Tol - Tol) > 0.005):
        Old_Tol = Tol
        Epoch = Epoch + 1
        mean_cl = [TE.iloc[[j for j, x in enumerate(O_a) if x == i]].mean() for i in range(0,k)]
        N_a = []
        for n in range(len(TE)):
            Iteration = Iteration + 1
            D = [Euc(TE.iloc[n].tolist(),mean_cl[c]) for c in range(0,k)] 
            N_a.append(D.index(min(D)))
       Tol = np.mean([x != y for x,y in zip(O_a,N_a)])
        Tol_r.append(Tol)
        O_a = N_a
    R.append(time.time() - start_time)
    R.append(Tol_r)
    R.append(N_a)
    R.append(Iteration)
    return R

def load_Pima():
    url = "http://www.stats.ox.ac.uk/pub/PRNN/pima.tr"
    Pima_training = pd.read_csv(url,sep = '\s+')
    url = "http://www.stats.ox.ac.uk/pub/PRNN/pima.te"
    Pima_testing = pd.read_csv(url,sep = '\s+')
    Pima_training = Pima_training.iloc[1:]
    Pima_testing = Pima_testing.iloc[1:]
    Pima_training.loc[:,"type"] = Pima_training.loc[:,"type"].apply(lambda x : 0 if x == 'Yes' else 1)
    Pima_testing.loc[:,"type"] = Pima_testing.loc[:,"type"].apply(lambda x : 0 if x == 'Yes' else 1)
    Features = Pima_training.loc[:,Pima_training.columns != "type"]
    Means = Features.mean()
    SDs = Features.std()
    for name in Features.columns:
        Pima_training[name] = (Pima_training[name]-Means[name])/SDs[name]
        Pima_testing[name] = (Pima_testing[name]-Means[name])/SDs[name]
    return Pima_training, Pima_testing

Pima_training, Pima_testing = load_Pima()

class_var = "type"
random.seed(2031)
k = 2
TE = Pima_testing
TE = TE.loc[:,TE.columns != class_var]
km = K_means(TE,k)

La función devuelve el tiempo de ejecución del algoritmo, la tolerancia en cada época (% de cambios en la asignación de conglomerados), las asignaciones de conglomerados finales y el número total de iteraciones. Ya eliminé cuatro bucles for, lo que lo ha acelerado bastante. Pero temo que mi falta de programación en Python me está impidiendo hacer esto más eficiente. ¡Se agradece cualquier ayuda!

Respuestas

6 Stuart Oct 02 2020 at 05:13

Puede vectorizar esto en varios puntos para aplicar aritmética a todo el marco de datos en lugar de fila por fila.

def min_euclidean(df, options):
    """ Returns the index of the series in iterable options for which df - row has minimum
    Euclidean distance """
    return pd.DataFrame(((df - series) ** 2).sum(axis=1, skipna=False) for series in options).idxmin()

def k_means(TE, k):
    iteration = 0
    epoch = 0
    tol = 1
    old_tol = 2
    tols = []
    start_time = time.time()
    mean_cl = [random.uniform(TE.min(), TE.max()) for _ in range(k)]
    o_a = min_euclidean(TE, mean_cl)
    while abs(old_tol - tol) > 0.005:
        old_tol = tol
        epoch += 1
        mean_cl = [TE[o_a == i].mean() for i in range(k)]
        n_a = min_euclidean(TE, mean_cl)
        iteration += len(TE)
        tol = (o_a != n_a).mean()
        tols.append(tol)
        o_a = n_a
    return time.time() - start_time, tols, n_a, iteration

Tenga en cuenta que:

  • siempre que sea posible, trabajamos con series de Pandas o marcos de datos en lugar de listas
  • Calculo mean_clcomo una lista de series de Pandas en lugar de una lista de listas. De esta manera evitamos iterar a través de los índices de columna o fila de TE, que es más lento. Puede haber alguna forma de optimizar esto aún más almacenándolo mean_clcomo un marco de datos, pero no veo una forma obvia de trabajar con él de esa manera.
  • no es necesario sacar la raíz cuadrada de la distancia euclidiana; los índices asociados con la distancia mínima serán los mismos trabajando con los valores al cuadrado
  • o_ay n_atambién se almacenan como series. Luego podemos hacer (o_a!= n_a).mean()para calcular la disparidad entre ellos.
  • La mayor optimización aquí es probablemente la min_euclideanfunción que calcula la diferencia cuadrada para todo el marco de datos de una manera optimizada, en lugar de iterar a través de él.

Esta puede ser una explicación útil de la vectorización. Trabajar con series de Pandas y marcos de datos de manera óptima a menudo implica una forma de pensar ligeramente diferente a la de Python normal.

EDITAR: Aquí hay una versión que usa numpy en lugar de pandas. Esto vectoriza completamente todas las operaciones y nuevamente es sustancialmente más rápido. Utiliza una optimización de esta respuesta de StackOverflow .

def k_means(TE, k):
    epoch = 0
    tol = 1
    old_tol = 2
    tols = []
    start_time = time.time()
    te = np.array(TE)
    rows, columns = te.shape
    te3 = te[:, np.newaxis]  # 3d version of te for calculating euclidean more easily
    k_range = np.arange(k)[:, np.newaxis]
    mean_cl = np.random.uniform(te.min(axis=0), te.max(axis=0), size=(k, columns))
    o_a = np.nanargmin(((te3 - mean_cl) ** 2).sum(axis=2), axis=1)
    while abs(old_tol - tol) > 0.005:
        old_tol = tol
        epoch += 1
        m = o_a == k_range     # masks for each value in range 0 to k-1
        mean_cl = m.dot(te) / m.sum(1, keepdims=True)
        n_a = np.nanargmin(((te3 - mean_cl) ** 2).sum(axis=2), axis=1)
        tol = (o_a != n_a).mean()
        tols.append(tol)
        o_a = n_a
    return time.time() - start_time, tols, n_a, epoch * rows