sklearn DBSCAN内存相关问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/danengbinggan33/article/details/85251894

写在前面

其实在大规模数据集下(数据在百万级以上且特征在百维以上)进行聚类,最好是使用分布式进行计算,本人也没有太多经验,仅此稍稍提下。

对于中等规模数据集(数据在十万级左右且特征在百维以上),优先推荐的还是使用sklearn的MiniBatchKMeans,但是有时候类别个数参数调整远比最大距离参数调整来的困难时,自然而然会想到使用基于密度聚类的DBSCAN。

但是在sklearn.cluster.DBSCAN实际的使用过程中,有时会面临因为数据集规模扩大,重新聚类时占用内存过高,导致memory error,从而使程序被kill。本文就是说一说一些可选取的内存优化方案。

PS:仅针对内存相关问题,速度上的比如多进程相关内容不涵盖。

内存占用过高原因

其实这个问题,sklearn官方文档中也给出了对应说明。

This implementation bulk-computes all neighborhood queries, which increases the memory complexity to O(n.d) where d is the average number of neighbors, while original DBSCAN had memory complexity O(n). It may attract a higher memory complexity when querying these nearest neighborhoods, depending on the algorithm.
谷歌翻译:该实现对所有邻居查询进行批量计算,从而将内存复杂度增加到O(n*d),其中d是邻居的平均数量,而原始DBSCAN的内存复杂度为O(n)。根据选取算法的不同,在查询这些最近的邻域时,它可能会吸引更高的内存复杂度。

大概意思是,为了支持批量计算,在算法初期,就需要构建各点间的距离矩阵,此时会引入额外内存。注意这里的文档应该是在0.16版本之后进行优化的,距离矩阵使用稀疏矩阵表示,之前版本的内存复杂度应该是O(n^2)。(对于具体什么版本优化存疑,我只是从gayhub项目下issue中进行推断的,并非查阅提交记录。)

优化方案

方案一

One way to avoid the query complexity is to pre-compute sparse neighborhoods in chunks using NearestNeighbors.radius_neighbors_graph with mode=‘distance’, then using metric=‘precomputed’ here.
谷歌翻译:避免查询复杂性的一种方法是使用NearestNeighbors.radius_neighbors_graph方法并且设置参数mode='distance’预先计算的稀疏矩阵,然后传入并使用参数metric=‘precomputed’。

代码示例(使用余弦距离):

def dbscan(train_data):
    """
    聚类
    :param train_data: 训练数据np.array[[]]
    :return:
    """
    neigh = NearestNeighbors(radius=0.5, metric='cosine').fit(X=train_data)
    train_x = neigh.radius_neighbors_graph(mode='distance')
    cluster = DBSCAN(eps=0.5, min_samples=3, metric='precomputed', n_jobs=1).fit_predict(X=train_x)

个人理解上,这样做最大的好处可以节约原输入矩阵所需内存,尤其是在n_jobs>=2或者n_jobs=-1情况下可以直观感受到内存消耗的减少。

PS:其实也可以直接传入n*n的距离矩阵进行计算,同样metric=‘precomputed’。

方案二

Another way to reduce memory and computation time is to remove (near-)duplicate points and use sample_weight instead.
谷歌翻译:另一种减少内存和计算时间的方法是删除(接近)重复的点并使用sample_weight取代,有几个点就把权重设为几,默认为1。

代码示例:

train_x = np.array([[1, 2], [1, 3], [10, 1], [100, 1]])
cluster = DBSCAN(eps=0.1, min_samples=3, metric='cosine', n_jobs=1).fit_predict(X=train_x, sample_weight=np.array([2, 2, 1, 1]))
print(cluster)
>>[0,0,-1,-1]

PS:方案一和方案二可以结合使用,英文描述均取自官方文档。

方案三

方案三就是自己实现DBSCAN算法,如__内存占用过高原因__中所描述那样,这样可以不需要将距离矩阵载入内存,从而只需要原始输入矩阵,内存为O(n)。但是由于没有了距离矩阵,所有各点与各点之间的距离每次都需要重新计算,从而增加了耗时,简言之就是时间换空间。

熟悉的实现方式:

# -*- coding:utf-8 -*-
"""
Description: DBSCAN简易版实现,主要sklearn实现内存占用过高

@author: WangLeAi
@date: 2018/12/25
"""

import numpy as np

UNCLASSIFIED = False
NOISE = -1


def __dis(vector1, vector2):
    """
    余弦夹角距离
    :param vector1: 向量A
    :param vector2: 向量B
    :return:
    """
    distance = np.dot(vector1, vector2) / (np.linalg.norm(vector1) * (np.linalg.norm(vector2)))
    distance = max(0.0, 1.0 - float(distance))
    return distance


def __eps_neighborhood(vector1, vector2, eps):
    """
    是否邻居
    :param vector1: 向量A
    :param vector2: 向量B
    :param eps: 同一域下样本最大距离
    :return:
    """
    return __dis(vector1, vector2) < eps


def __region_query(data, point_id, eps):
    """
    核心函数,区域查询
    :param data: 数据集,array
    :param point_id: 核心点
    :param eps: 同一域下样本最大距离
    :return:
    """
    n_points = data.shape[0]
    seeds = []
    for i in range(0, n_points):
        if __eps_neighborhood(data[point_id, :], data[i, :], eps):
            seeds.append(i)
    return seeds


def __expand_cluster(data, classifications, point_id, cluster_id, eps, min_points):
    """
    类簇扩散
    :param data: 数据集,array
    :param classifications: 分类结果
    :param point_id: 当前点
    :param cluster_id: 分类类别
    :param eps: 同一域下样本最大距离
    :param min_points: 每个簇最小核心点数
    :return:
    """
    seeds = __region_query(data, point_id, eps)
    if len(seeds) < min_points:
        classifications[point_id] = NOISE
        mark = False
    else:
        classifications[point_id] = cluster_id
        for seed_id in seeds:
            classifications[seed_id] = cluster_id
        while len(seeds) > 0:
            current_point = seeds[0]
            results = __region_query(data, current_point, eps)
            if len(results) >= min_points:
                for i in range(0, len(results)):
                    result_point = results[i]
                    if classifications[result_point] == UNCLASSIFIED or classifications[result_point] == NOISE:
                        if classifications[result_point] == UNCLASSIFIED:
                            seeds.append(result_point)
                        classifications[result_point] = cluster_id
            seeds = seeds[1:]
        mark = True
    return mark


def dbscan(data, eps, min_points):
    """
    dbscan聚类
    :param data: 数据集,array
    :param eps: 同一域下样本最大距离
    :param min_points: 每个簇最小核心点数
    :return:
    """
    cluster_id = 1
    n_points = data.shape[0]
    classifications = [UNCLASSIFIED] * n_points
    for point_id in range(0, n_points):
        if classifications[point_id] == UNCLASSIFIED:
            if __expand_cluster(data, classifications, point_id, cluster_id, eps, min_points):
                cluster_id = cluster_id + 1
    return classifications


if __name__ == "__main__":
    m = np.array([[5, 20], [1.2, 0.8], [1.2, 0.8], [1.2, 0.8], [1.2, 0.8], [1.2, 0.8], [1.2, 0.8]])
    print(dbscan(m, 0.1, 3))
    >>[-1, 1, 1, 1, 1, 1, 1]

方案四(最好的方案)

升级到scikit-learn 0.20.2版本以上就完事了。

展开阅读全文

没有更多推荐了,返回首页