001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.math3.ml.clustering;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.List;
023
024import org.apache.commons.math3.exception.MathIllegalArgumentException;
025import org.apache.commons.math3.exception.MathIllegalStateException;
026import org.apache.commons.math3.exception.NumberIsTooSmallException;
027import org.apache.commons.math3.linear.MatrixUtils;
028import org.apache.commons.math3.linear.RealMatrix;
029import org.apache.commons.math3.ml.distance.DistanceMeasure;
030import org.apache.commons.math3.ml.distance.EuclideanDistance;
031import org.apache.commons.math3.random.JDKRandomGenerator;
032import org.apache.commons.math3.random.RandomGenerator;
033import org.apache.commons.math3.util.FastMath;
034import org.apache.commons.math3.util.MathArrays;
035import org.apache.commons.math3.util.MathUtils;
036
037/**
038 * Fuzzy K-Means clustering algorithm.
039 * <p>
040 * The Fuzzy K-Means algorithm is a variation of the classical K-Means algorithm, with the
041 * major difference that a single data point is not uniquely assigned to a single cluster.
042 * Instead, each point i has a set of weights u<sub>ij</sub> which indicate the degree of membership
043 * to the cluster j.
044 * <p>
045 * The algorithm then tries to minimize the objective function:
046 * <pre>
047 * J = &#8721;<sub>i=1..C</sub>&#8721;<sub>k=1..N</sub> u<sub>ik</sub><sup>m</sup>d<sub>ik</sub><sup>2</sup>
048 * </pre>
049 * with d<sub>ik</sub> being the distance between data point i and the cluster center k.
050 * <p>
051 * The algorithm requires two parameters:
052 * <ul>
053 *   <li>k: the number of clusters
054 *   <li>fuzziness: determines the level of cluster fuzziness, larger values lead to fuzzier clusters
055 * </ul>
056 * Additional, optional parameters:
057 * <ul>
058 *   <li>maxIterations: the maximum number of iterations
059 *   <li>epsilon: the convergence criteria, default is 1e-3
060 * </ul>
061 * <p>
062 * The fuzzy variant of the K-Means algorithm is more robust with regard to the selection
063 * of the initial cluster centers.
064 *
065 * @param <T> type of the points to cluster
066 * @version $Id: FuzzyKMeansClusterer.html 885258 2013-11-03 02:46:49Z tn $
067 * @since 3.3
068 */
069public class FuzzyKMeansClusterer<T extends Clusterable> extends Clusterer<T> {
070
071    /** The default value for the convergence criteria. */
072    private static final double DEFAULT_EPSILON = 1e-3;
073
074    /** The number of clusters. */
075    private final int k;
076
077    /** The maximum number of iterations. */
078    private final int maxIterations;
079
080    /** The fuzziness factor. */
081    private final double fuzziness;
082
083    /** The convergence criteria. */
084    private final double epsilon;
085
086    /** Random generator for choosing initial centers. */
087    private final RandomGenerator random;
088
089    /** The membership matrix. */
090    private double[][] membershipMatrix;
091
092    /** The list of points used in the last call to {@link #cluster(Collection)}. */
093    private List<T> points;
094
095    /** The list of clusters resulting from the last call to {@link #cluster(Collection)}. */
096    private List<CentroidCluster<T>> clusters;
097
098    /**
099     * Creates a new instance of a FuzzyKMeansClusterer.
100     * <p>
101     * The euclidean distance will be used as default distance measure.
102     *
103     * @param k the number of clusters to split the data into
104     * @param fuzziness the fuzziness factor, must be &gt; 1.0
105     * @throws NumberIsTooSmallException if {@code fuzziness <= 1.0}
106     */
107    public FuzzyKMeansClusterer(final int k, final double fuzziness) throws NumberIsTooSmallException {
108        this(k, fuzziness, -1, new EuclideanDistance());
109    }
110
111    /**
112     * Creates a new instance of a FuzzyKMeansClusterer.
113     *
114     * @param k the number of clusters to split the data into
115     * @param fuzziness the fuzziness factor, must be &gt; 1.0
116     * @param maxIterations the maximum number of iterations to run the algorithm for.
117     *   If negative, no maximum will be used.
118     * @param measure the distance measure to use
119     * @throws NumberIsTooSmallException if {@code fuzziness <= 1.0}
120     */
121    public FuzzyKMeansClusterer(final int k, final double fuzziness,
122                                final int maxIterations, final DistanceMeasure measure)
123            throws NumberIsTooSmallException {
124        this(k, fuzziness, maxIterations, measure, DEFAULT_EPSILON, new JDKRandomGenerator());
125    }
126
127    /**
128     * Creates a new instance of a FuzzyKMeansClusterer.
129     *
130     * @param k the number of clusters to split the data into
131     * @param fuzziness the fuzziness factor, must be &gt; 1.0
132     * @param maxIterations the maximum number of iterations to run the algorithm for.
133     *   If negative, no maximum will be used.
134     * @param measure the distance measure to use
135     * @param epsilon the convergence criteria (default is 1e-3)
136     * @param random random generator to use for choosing initial centers
137     * @throws NumberIsTooSmallException if {@code fuzziness <= 1.0}
138     */
139    public FuzzyKMeansClusterer(final int k, final double fuzziness,
140                                final int maxIterations, final DistanceMeasure measure,
141                                final double epsilon, final RandomGenerator random)
142            throws NumberIsTooSmallException {
143
144        super(measure);
145
146        if (fuzziness <= 1.0d) {
147            throw new NumberIsTooSmallException(fuzziness, 1.0, false);
148        }
149        this.k = k;
150        this.fuzziness = fuzziness;
151        this.maxIterations = maxIterations;
152        this.epsilon = epsilon;
153        this.random = random;
154
155        this.membershipMatrix = null;
156        this.points = null;
157        this.clusters = null;
158    }
159
160    /**
161     * Return the number of clusters this instance will use.
162     * @return the number of clusters
163     */
164    public int getK() {
165        return k;
166    }
167
168    /**
169     * Returns the fuzziness factor used by this instance.
170     * @return the fuzziness factor
171     */
172    public double getFuzziness() {
173        return fuzziness;
174    }
175
176    /**
177     * Returns the maximum number of iterations this instance will use.
178     * @return the maximum number of iterations, or -1 if no maximum is set
179     */
180    public int getMaxIterations() {
181        return maxIterations;
182    }
183
184    /**
185     * Returns the convergence criteria used by this instance.
186     * @return the convergence criteria
187     */
188    public double getEpsilon() {
189        return epsilon;
190    }
191
192    /**
193     * Returns the random generator this instance will use.
194     * @return the random generator
195     */
196    public RandomGenerator getRandomGenerator() {
197        return random;
198    }
199
200    /**
201     * Returns the {@code nxk} membership matrix, where {@code n} is the number
202     * of data points and {@code k} the number of clusters.
203     * <p>
204     * The element U<sub>i,j</sub> represents the membership value for data point {@code i}
205     * to cluster {@code j}.
206     *
207     * @return the membership matrix
208     * @throws MathIllegalStateException if {@link #cluster(Collection)} has not been called before
209     */
210    public RealMatrix getMembershipMatrix() {
211        if (membershipMatrix == null) {
212            throw new MathIllegalStateException();
213        }
214        return MatrixUtils.createRealMatrix(membershipMatrix);
215    }
216
217    /**
218     * Returns an unmodifiable list of the data points used in the last
219     * call to {@link #cluster(Collection)}.
220     * @return the list of data points, or {@code null} if {@link #cluster(Collection)} has
221     *   not been called before.
222     */
223    public List<T> getDataPoints() {
224        return points;
225    }
226
227    /**
228     * Returns the list of clusters resulting from the last call to {@link #cluster(Collection)}.
229     * @return the list of clusters, or {@code null} if {@link #cluster(Collection)} has
230     *   not been called before.
231     */
232    public List<CentroidCluster<T>> getClusters() {
233        return clusters;
234    }
235
236    /**
237     * Get the value of the objective function.
238     * @return the objective function evaluation as double value
239     * @throws MathIllegalStateException if {@link #cluster(Collection)} has not been called before
240     */
241    public double getObjectiveFunctionValue() {
242        if (points == null || clusters == null) {
243            throw new MathIllegalStateException();
244        }
245
246        int i = 0;
247        double objFunction = 0.0;
248        for (final T point : points) {
249            int j = 0;
250            for (final CentroidCluster<T> cluster : clusters) {
251                final double dist = distance(point, cluster.getCenter());
252                objFunction += (dist * dist) * FastMath.pow(membershipMatrix[i][j], fuzziness);
253                j++;
254            }
255            i++;
256        }
257        return objFunction;
258    }
259
260    /**
261     * Performs Fuzzy K-Means cluster analysis.
262     *
263     * @param dataPoints the points to cluster
264     * @return the list of clusters
265     * @throws MathIllegalArgumentException if the data points are null or the number
266     *     of clusters is larger than the number of data points
267     */
268    @Override
269    public List<CentroidCluster<T>> cluster(final Collection<T> dataPoints)
270            throws MathIllegalArgumentException {
271
272        // sanity checks
273        MathUtils.checkNotNull(dataPoints);
274
275        final int size = dataPoints.size();
276
277        // number of clusters has to be smaller or equal the number of data points
278        if (size < k) {
279            throw new NumberIsTooSmallException(size, k, false);
280        }
281
282        // copy the input collection to an unmodifiable list with indexed access
283        points = Collections.unmodifiableList(new ArrayList<T>(dataPoints));
284        clusters = new ArrayList<CentroidCluster<T>>();
285        membershipMatrix = new double[size][k];
286        final double[][] oldMatrix = new double[size][k];
287
288        // if no points are provided, return an empty list of clusters
289        if (size == 0) {
290            return clusters;
291        }
292
293        initializeMembershipMatrix();
294
295        // there is at least one point
296        final int pointDimension = points.get(0).getPoint().length;
297        for (int i = 0; i < k; i++) {
298            clusters.add(new CentroidCluster<T>(new DoublePoint(new double[pointDimension])));
299        }
300
301        int iteration = 0;
302        final int max = (maxIterations < 0) ? Integer.MAX_VALUE : maxIterations;
303        double difference = 0.0;
304
305        do {
306            saveMembershipMatrix(oldMatrix);
307            updateClusterCenters();
308            updateMembershipMatrix();
309            difference = calculateMaxMembershipChange(oldMatrix);
310        } while (difference > epsilon && ++iteration < max);
311
312        return clusters;
313    }
314
315    /**
316     * Update the cluster centers.
317     */
318    private void updateClusterCenters() {
319        int j = 0;
320        final List<CentroidCluster<T>> newClusters = new ArrayList<CentroidCluster<T>>(k);
321        for (final CentroidCluster<T> cluster : clusters) {
322            final Clusterable center = cluster.getCenter();
323            int i = 0;
324            double[] arr = new double[center.getPoint().length];
325            double sum = 0.0;
326            for (final T point : points) {
327                final double u = FastMath.pow(membershipMatrix[i][j], fuzziness);
328                final double[] pointArr = point.getPoint();
329                for (int idx = 0; idx < arr.length; idx++) {
330                    arr[idx] += u * pointArr[idx];
331                }
332                sum += u;
333                i++;
334            }
335            MathArrays.scaleInPlace(1.0 / sum, arr);
336            newClusters.add(new CentroidCluster<T>(new DoublePoint(arr)));
337            j++;
338        }
339        clusters.clear();
340        clusters = newClusters;
341    }
342
343    /**
344     * Updates the membership matrix and assigns the points to the cluster with
345     * the highest membership.
346     */
347    private void updateMembershipMatrix() {
348        for (int i = 0; i < points.size(); i++) {
349            final T point = points.get(i);
350            double maxMembership = 0.0;
351            int newCluster = -1;
352            for (int j = 0; j < clusters.size(); j++) {
353                double sum = 0.0;
354                final double distA = FastMath.abs(distance(point, clusters.get(j).getCenter()));
355
356                for (final CentroidCluster<T> c : clusters) {
357                    final double distB = FastMath.abs(distance(point, c.getCenter()));
358                    sum += FastMath.pow(distA / distB, 2.0 / (fuzziness - 1.0));
359                }
360
361                membershipMatrix[i][j] = 1.0 / sum;
362
363                if (membershipMatrix[i][j] > maxMembership) {
364                    maxMembership = membershipMatrix[i][j];
365                    newCluster = j;
366                }
367            }
368            clusters.get(newCluster).addPoint(point);
369        }
370    }
371
372    /**
373     * Initialize the membership matrix with random values.
374     */
375    private void initializeMembershipMatrix() {
376        for (int i = 0; i < points.size(); i++) {
377            for (int j = 0; j < k; j++) {
378                membershipMatrix[i][j] = random.nextDouble();
379            }
380            membershipMatrix[i] = MathArrays.normalizeArray(membershipMatrix[i], 1.0);
381        }
382    }
383
384    /**
385     * Calculate the maximum element-by-element change of the membership matrix
386     * for the current iteration.
387     *
388     * @param matrix the membership matrix of the previous iteration
389     * @return the maximum membership matrix change
390     */
391    private double calculateMaxMembershipChange(final double[][] matrix) {
392        double maxMembership = 0.0;
393        for (int i = 0; i < points.size(); i++) {
394            for (int j = 0; j < clusters.size(); j++) {
395                double v = FastMath.abs(membershipMatrix[i][j] - matrix[i][j]);
396                maxMembership = FastMath.max(v, maxMembership);
397            }
398        }
399        return maxMembership;
400    }
401
402    /**
403     * Copy the membership matrix into the provided matrix.
404     *
405     * @param matrix the place to store the membership matrix
406     */
407    private void saveMembershipMatrix(final double[][] matrix) {
408        for (int i = 0; i < points.size(); i++) {
409            System.arraycopy(membershipMatrix[i], 0, matrix[i], 0, clusters.size());
410        }
411    }
412
413}