Completed
Push — master ( 9cffad...7a46cc )
by Andrei
01:25
created

kmeans_observer.get_clusters()   A

Complexity

Conditions 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
"""!
2
3
@brief Cluster analysis algorithm: K-Means
4
@details Based on book description:
5
         - J.B.MacQueen. Some Methods for Classification and Analysis of Multivariate Observations. 1967.
6
7
@authors Andrei Novikov ([email protected])
8
@date 2014-2018
9
@copyright GNU Public License
10
11
@cond GNU_PUBLIC_LICENSE
12
    PyClustering is free software: you can redistribute it and/or modify
13
    it under the terms of the GNU General Public License as published by
14
    the Free Software Foundation, either version 3 of the License, or
15
    (at your option) any later version.
16
    
17
    PyClustering is distributed in the hope that it will be useful,
18
    but WITHOUT ANY WARRANTY; without even the implied warranty of
19
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
    GNU General Public License for more details.
21
    
22
    You should have received a copy of the GNU General Public License
23
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
@endcond
25
26
"""
27
28
29
import numpy;
0 ignored issues
show
Configuration introduced by
The import numpy could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
30
31
import matplotlib.pyplot as plt;
0 ignored issues
show
Configuration introduced by
The import matplotlib.pyplot could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
32
import matplotlib.animation as animation;
0 ignored issues
show
Configuration introduced by
The import matplotlib.animation could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
33
34
import pyclustering.core.kmeans_wrapper as wrapper;
35
36
from pyclustering.core.wrapper import ccore_library;
37
38
from pyclustering.cluster.encoder import type_encoding;
39
from pyclustering.cluster import cluster_visualizer;
40
41
42
class kmeans_observer:
43
    """!
44
    @brief Observer of K-Means algorithm that is used to collect information about clustering process on each iteration of the algorithm.
45
    
46
    @see kmeans
47
    
48
    """
49
    
50
    def __init__(self):
51
        """!
52
        @brief Initializer of observer of K-Means algorithm.
53
        
54
        """
55
        self.__evolution_clusters   = [];
56
        self.__evolution_centers    = [];
57
        self.__initial_centers      = [];
58
59
60
    def __len__(self):
61
        """!
62
        @brief Returns amount of steps that were observer during clustering process in K-Means algorithm.
63
        
64
        """
65
        return len(self.__evolution_clusters);
66
67
68
    def notify(self, clusters, centers):
69
        """!
70
        @brief This method is called by K-Means algorithm to notify about changes.
71
        
72
        @param[in] clusters (array_like): Allocated clusters by K-Means algorithm.
73
        @param[in] centers (array_like): Allocated centers by K-Means algorithm.
74
        
75
        """
76
        self.__evolution_clusters.append(clusters);
77
        self.__evolution_centers.append(centers);
78
79
80
    def set_evolution_centers(self, evolution_centers):
81
        """!
82
        @brief Set evolution of changes of centers during clustering process.
83
        
84
        @param[in] evolution_centers (array_like): Evolution of changes of centers during clustering process.
85
        
86
        """
87
        self.__evolution_centers = evolution_centers;
88
89
90
    def get_centers(self, iteration):
91
        """!
92
        @brief Get method to return centers at specific iteration of clustering process.
93
        
94
        @param[in] iteration (uint): Clustering process iteration at which centers are required.
95
        
96
        @return (array_like) Centers at specific iteration.
97
        
98
        """
99
        return self.__evolution_centers[iteration];
100
101
102
    def set_evolution_clusters(self, evolution_clusters):
103
        """!
104
        @brief Set evolution of changes of centers during clustering process.
105
        
106
        @param[in] evolution_clusters (array_like): Evolution of changes of clusters during clustering process.
107
        
108
        """
109
        self.__evolution_clusters = evolution_clusters;
110
111
112
    def get_clusters(self, iteration):
113
        """!
114
        @brief Get method to return allocated clusters at specific iteration of clustering process.
115
        
116
        @param[in] iteration (uint): Clustering process iteration at which clusters are required.
117
        
118
        @return (array_like) Clusters at specific iteration.
119
        
120
        """
121
        return self.__evolution_clusters[iteration];
122
123
124
125
class kmeans_visualizer:
126
    """!
127
    @brief Visualizer of K-Means algorithm's results.
128
    @details K-Means visualizer provides visualization services that are specific for K-Means algorithm.
129
    
130
    """
131
    
132
    __default_2d_marker_size = 15;
133
    __default_3d_marker_size = 70;
134
    
135
    
136
    @staticmethod
137
    def show_clusters(sample, clusters, centers, initial_centers = None, **kwargs):
138
        """!
139
        @brief Display K-Means clustering results.
140
        
141
        @param[in] sample (list): Dataset that were used for clustering.
142
        @param[in] clusters (array_like): Clusters that were allocated by the algorithm.
143
        @param[in] centers (array_like): Centers that were allocated by the algorithm.
144
        @param[in] initial_centers (array_like): Initial centers that were used by the algorithm, if 'None' then initial centers are not displyed.
145
        @param[in] **kwargs: Arbitrary keyword arguments (available arguments: 'figure', 'display').
146
        
147
        Keyword Args:
148
            figure (figure): If 'None' then new is figure is creater, otherwise specified figure is used for visualization.
149
            display (bool): If 'True' then figure will be shown by the method, otherwise it should be shown manually using matplotlib function 'plt.show()'.
150
            offset (uint): Specify axes index on the figure where results should be drawn (only if argument 'figure' is specified).
151
        
152
        @return (figure) Figure where clusters were drawn.
153
        
154
        """
155
156
        visualizer = cluster_visualizer();
157
        visualizer.append_clusters(clusters, sample);
158
        
159
        offset = kmeans_visualizer.__get_argument('offset', 0, **kwargs);
160
        figure = kmeans_visualizer.__get_argument('figure', None, **kwargs);
161
        if (figure is None):
162
            figure = visualizer.show(display = False);
163
        else:
164
            visualizer.show(figure = figure, display = False);
165
        
166
        kmeans_visualizer.__draw_centers(figure, offset, visualizer, centers, initial_centers);
167
        kmeans_visualizer.__draw_rays(figure, offset, visualizer, sample, clusters, centers);
168
        
169
        if (kmeans_visualizer.__get_argument('display', True, **kwargs) is True):
170
            plt.show();
171
172
        return figure;
173
174
175
    @staticmethod
176
    def __draw_rays(figure, offset, visualizer, sample, clusters, centers):
177
        ax = figure.get_axes()[offset];
178
        
179
        for index_cluster in range(len(clusters)):
180
            color = visualizer.get_cluster_color(index_cluster, 0);
181
            kmeans_visualizer.__draw_cluster_rays(ax, color, sample, clusters[index_cluster], centers[index_cluster])
182
183
184
    @staticmethod
185
    def __draw_cluster_rays(ax, color, sample, cluster, center):
186
        dimension = len(sample[0]);
187
        
188
        for index_point in cluster:
189
            point = sample[index_point];
190
            if (dimension == 1):
191
                ax.plot([point[0], center[0]], [0.0, 0.0], '-', color=color, linewidth=0.5);
192
            elif (dimension == 2):
193
                ax.plot([point[0], center[0]], [point[1], center[1]], '-', color=color, linewidth=0.5);
194
            elif (dimension == 3):
195
                ax.plot([point[0], center[0]], [point[1], center[1]], [point[2], center[2]], '-', color=color, linewidth=0.5)
196
197
198
    @staticmethod
199
    def __draw_center(ax, center, color, marker, alpha):
200
        dimension = len(center);
201
        
202
        if (dimension == 1):
203
            ax.plot(center[0], 0.0, color=color, alpha=alpha, marker=marker, markersize=kmeans_visualizer.__default_2d_marker_size);
204
        elif (dimension == 2):
205
            ax.plot(center[0], center[1], color=color, alpha=alpha, marker=marker, markersize=kmeans_visualizer.__default_2d_marker_size);
206
        elif (dimension == 3):
207
            ax.scatter(center[0], center[1], center[2], c=color, alpha=alpha, marker=marker, s=kmeans_visualizer.__default_3d_marker_size);
208
209
210
    @staticmethod
211
    def __draw_centers(figure, offset, visualizer, centers, initial_centers):
212
        ax = figure.get_axes()[offset];
213
        
214
        for index_center in range(len(centers)):
215
            color = visualizer.get_cluster_color(index_center, 0);
216
            kmeans_visualizer.__draw_center(ax, centers[index_center], color, '*', 1.0);
217
            
218
            if initial_centers is not None:
219
                kmeans_visualizer.__draw_center(ax, initial_centers[index_center], color, '*', 0.4);
220
221
222
    @staticmethod
223
    def __get_argument(argument_name, default_value, **kwargs):
224
        if (argument_name in kwargs):
225
            return kwargs[argument_name];
226
        
227
        return default_value;
228
229
230
    @staticmethod
231
    def animate_cluster_allocation(data, observer, animation_velocity = 500, movie_fps = 1, save_movie = None):
232
        """!
233
        @brief Animates clustering process that is performed by K-Means algorithm.
234
235
        @param[in] data (list): Dataset that is used for clustering.
236
        @param[in] observer (kmeans_observer): EM observer that was used for collection information about clustering process.
237
        @param[in] animation_velocity (uint): Interval between frames in milliseconds (for run-time animation only).
238
        @param[in] movie_fps (uint): Defines frames per second (for rendering movie only).
239
        @param[in] save_movie (string): If it is specified then animation will be stored to file that is specified in this parameter.
240
241
        """
242
        figure = plt.figure();
243
244
        def init_frame():
245
            return frame_generation(0);
246
247
        def frame_generation(index_iteration):
248
            figure.clf();
249
250
            figure.suptitle("K-Means algorithm (iteration: " + str(index_iteration) + ")", fontsize=18, fontweight='bold');
251
252
            clusters = observer.get_clusters(index_iteration);
253
            centers = observer.get_centers(index_iteration);
254
            kmeans_visualizer.show_clusters(data, clusters, centers, None, figure=figure, display=False);
255
256
            figure.subplots_adjust(top=0.85);
257
258
            return [figure.gca()];
259
260
        iterations = len(observer);
261
        cluster_animation = animation.FuncAnimation(figure, frame_generation, iterations, interval=animation_velocity,
262
                                                    init_func=init_frame, repeat_delay=5000);
263
264
        if (save_movie is not None):
265
            cluster_animation.save(save_movie, writer='ffmpeg', fps=movie_fps, bitrate=3000);
266
        else:
267
            plt.show();
268
269
270
271
class kmeans:
272
    """!
273
    @brief Class represents K-Means clustering algorithm.
274
    @details CCORE option can be used to use the pyclustering core - C/C++ shared library for processing that significantly increases performance.
275
    
276
    CCORE implementation of the algorithm uses thread pool to parallelize the clustering process.
277
    
278
    K-Means clustering results depend on initial centers. Algorithm K-Means++ can used for initialization 
279
    initial centers from module 'pyclustering.cluster.center_initializer'.
280
    
281
    @image html kmeans_example_clustering.png "K-Means clustering results. At the left - 'Simple03.data' sample, at the right - 'Lsun.data' sample."
282
    
283
    Example #1 - Trivial clustering:
284
    @code
285
        # load list of points for cluster analysis
286
        sample = read_sample(path);
287
        
288
        # create instance of K-Means algorithm
289
        kmeans_instance = kmeans(sample, [ [0.0, 0.1], [2.5, 2.6] ]);
290
        
291
        # run cluster analysis and obtain results
292
        kmeans_instance.process();
293
        clusters = kmeans_instance.get_clusters();
294
    @endcode
295
    
296
    Example #2 - Clustering using K-Means++ for center initialization:
297
    @code
298
        # load list of points for cluster analysis
299
        sample = read_sample(SIMPLE_SAMPLES.SAMPLE_SIMPLE2);
300
        
301
        # initialize initial centers using K-Means++ method
302
        initial_centers = kmeans_plusplus_initializer(sample, 3).initialize();
303
        
304
        # create instance of K-Means algorithm with prepared centers
305
        kmeans_instance = kmeans(sample, initial_centers);
306
        
307
        # run cluster analysis and obtain results
308
        kmeans_instance.process();
309
        clusters = kmeans_instance.get_clusters();
310
        final_centers = kmeans_instance.get_centers();
311
    @endcode
312
    
313
    @see center_initializer
314
    
315
    """
316
    
317
    def __init__(self, data, initial_centers, tolerance = 0.001, ccore = True, **kwargs):
318
        """!
319
        @brief Constructor of clustering algorithm K-Means.
320
        @details Center initializer can be used for creating initial centers, for example, K-Means++ method.
321
        
322
        @param[in] data (array_like): Input data that is presented as array of points (objects), each point should be represented by array_like data structure.
323
        @param[in] initial_centers (array_like): Initial coordinates of centers of clusters that are represented by array_like data structure: [center1, center2, ...].
324
        @param[in] tolerance (double): Stop condition: if maximum value of change of centers of clusters is less than tolerance then algorithm stops processing.
325
        @param[in] ccore (bool): Defines should be CCORE library (C++ pyclustering library) used instead of Python code or not.
326
        @param[in] **kwargs: Arbitrary keyword arguments (available arguments: 'observer').
327
        
328
        Keyword Args:
329
            observer (kmeans_observer): Observer of the algorithm to collect information about clustering process on each iteration.
330
        
331
        @see center_initializer
332
        
333
        """
334
        self.__pointer_data = numpy.matrix(data);
335
        self.__clusters = [];
336
        self.__centers = numpy.matrix(initial_centers);
337
        self.__tolerance = tolerance;
338
        
339
        self.__observer = None;
340
        if 'observer' in kwargs:
341
            self.__observer = kwargs['observer'];
342
        
343
        self.__ccore = ccore;
344
        if (self.__ccore is True):
345
            self.__ccore = ccore_library.workable();
346
347
348
    def process(self):
349
        """!
350
        @brief Performs cluster analysis in line with rules of K-Means algorithm.
351
        
352
        @remark Results of clustering can be obtained using corresponding get methods.
353
        
354
        @see get_clusters()
355
        @see get_centers()
356
        
357
        """
358
359
        if (len(self.__pointer_data[0]) != len(self.__centers[0])):
360
            raise NameError('Dimension of the input data and dimension of the initial cluster centers must be equal.');
361
362
        if (self.__ccore is True):
363
            self.__process_by_ccore();
364
        else:
365
            self.__process_by_python();
366
367
368
    def __process_by_ccore(self):
369
        """!
370
        @brief Performs cluster analysis using CCORE (C/C++ part of pyclustering library).
371
372
        """
373
        results = wrapper.kmeans(self.__pointer_data, self.__centers, self.__tolerance, (self.__observer is not None));
374
        self.__clusters = results[0];
375
        self.__centers = results[1];
376
377
        if self.__observer is not None:
378
            self.__observer.set_evolution_clusters(results[2]);
379
            self.__observer.set_evolution_centers(results[3]);
380
381
382
    def __process_by_python(self):
383
        """!
384
        @brief Performs cluster analysis using python code.
385
386
        """
387
388
        maximum_change = float('inf');
389
        stop_condition = self.__tolerance * self.__tolerance;
390
391
        if (self.__observer is not None):
392
            initial_clusters = self.__update_clusters();
393
            self.__observer.notify(initial_clusters, self.__centers.tolist());
394
395
        while (maximum_change > stop_condition):
396
            self.__clusters = self.__update_clusters();
397
            updated_centers = self.__update_centers();  # changes should be calculated before assignment
398
399
            if self.__observer is not None:
400
                self.__observer.notify(self.__clusters, updated_centers.tolist());
401
402
            if (len(self.__centers) != len(updated_centers)):
403
                maximum_change = float('inf');
404
405
            else:
406
                changes = numpy.sum(numpy.square(self.__centers - updated_centers), axis=1);
407
                maximum_change = numpy.max(changes);
408
409
            self.__centers = updated_centers.tolist();
410
411
412
    def get_clusters(self):
413
        """!
414
        @brief Returns list of allocated clusters, each cluster contains indexes of objects in list of data.
415
        
416
        @see process()
417
        @see get_centers()
418
        
419
        """
420
        
421
        return self.__clusters;
422
    
423
    
424
    def get_centers(self):
425
        """!
426
        @brief Returns list of centers of allocated clusters.
427
        
428
        @see process()
429
        @see get_clusters()
430
        
431
        """
432
433
        if isinstance(self.__centers, list):
434
            return self.__centers;
435
        
436
        return self.__centers.tolist();
437
438
439
    def get_cluster_encoding(self):
440
        """!
441
        @brief Returns clustering result representation type that indicate how clusters are encoded.
442
        
443
        @return (type_encoding) Clustering result representation.
444
        
445
        @see get_clusters()
446
        
447
        """
448
        
449
        return type_encoding.CLUSTER_INDEX_LIST_SEPARATION;
450
451
452
    def __update_clusters(self):
453
        """!
454
        @brief Calculate Euclidean distance to each point from the each cluster. Nearest points are captured by according clusters and as a result clusters are updated.
455
        
456
        @return (list) updated clusters as list of clusters. Each cluster contains indexes of objects from data.
457
        
458
        """
459
        
460
        clusters = [[] for _ in range(len(self.__centers))];
461
        
462
        dataset_differences = numpy.zeros((len(clusters), len(self.__pointer_data)));
463
        for index_center in range(len(self.__centers)):
464
            dataset_differences[index_center] = numpy.sum(numpy.square(self.__pointer_data - self.__centers[index_center]), axis=1).T;
465
        
466
        optimum_indexes = numpy.argmin(dataset_differences, axis=0);
467
        for index_point in range(len(optimum_indexes)):
468
            index_cluster = optimum_indexes[index_point];
469
            clusters[index_cluster].append(index_point);
470
        
471
        clusters = [cluster for cluster in clusters if len(cluster) > 0];
472
        
473
        return clusters;
474
    
475
    
476
    def __update_centers(self):
477
        """!
478
        @brief Calculate centers of clusters in line with contained objects.
479
        
480
        @return (numpy.matrix) Updated centers as list of centers.
481
        
482
        """
483
        
484
        dimension = self.__pointer_data.shape[1];
485
        centers = numpy.zeros((len(self.__clusters), dimension));
486
        
487
        for index in range(len(self.__clusters)):
488
            cluster_points = self.__pointer_data[self.__clusters[index], :];
489
            centers[index] = cluster_points.mean(axis=0);
490
491
        return numpy.matrix(centers);
492