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 */
017
018package org.apache.commons.math4.neuralnet.twod;
019
020import java.util.List;
021import java.util.ArrayList;
022import java.util.Iterator;
023import java.util.Collection;
024
025import org.apache.commons.math4.neuralnet.DistanceMeasure;
026import org.apache.commons.math4.neuralnet.EuclideanDistance;
027import org.apache.commons.math4.neuralnet.FeatureInitializer;
028import org.apache.commons.math4.neuralnet.Network;
029import org.apache.commons.math4.neuralnet.Neuron;
030import org.apache.commons.math4.neuralnet.SquareNeighbourhood;
031import org.apache.commons.math4.neuralnet.MapRanking;
032import org.apache.commons.math4.neuralnet.internal.NeuralNetException;
033import org.apache.commons.math4.neuralnet.twod.util.LocationFinder;
034
035/**
036 * Neural network with the topology of a two-dimensional surface.
037 * Each neuron defines one surface element.
038 * <br>
039 * This network is primarily intended to represent a
040 * <a href="http://en.wikipedia.org/wiki/Kohonen">
041 *  Self Organizing Feature Map</a>.
042 *
043 * @see org.apache.commons.math4.neuralnet.sofm
044 * @since 3.3
045 */
046public class NeuronSquareMesh2D
047    implements Iterable<Neuron> {
048    /** Minimal number of rows or columns. */
049    private static final int MIN_ROWS = 2;
050    /** Underlying network. */
051    private final Network network;
052    /** Number of rows. */
053    private final int numberOfRows;
054    /** Number of columns. */
055    private final int numberOfColumns;
056    /** Wrap. */
057    private final boolean wrapRows;
058    /** Wrap. */
059    private final boolean wrapColumns;
060    /** Neighbourhood type. */
061    private final SquareNeighbourhood neighbourhood;
062    /**
063     * Mapping of the 2D coordinates (in the rectangular mesh) to
064     * the neuron identifiers (attributed by the {@link #network}
065     * instance).
066     */
067    private final long[][] identifiers;
068
069    /**
070     * Horizontal (along row) direction.
071     * @since 3.6
072     */
073    public enum HorizontalDirection {
074        /** Column at the right of the current column. */
075       RIGHT,
076       /** Current column. */
077       CENTER,
078       /** Column at the left of the current column. */
079       LEFT,
080    }
081    /**
082     * Vertical (along column) direction.
083     * @since 3.6
084     */
085    public enum VerticalDirection {
086        /** Row above the current row. */
087        UP,
088        /** Current row. */
089        CENTER,
090        /** Row below the current row. */
091        DOWN,
092    }
093
094    /**
095     * @param wrapRowDim Whether to wrap the first dimension (i.e the first
096     * and last neurons will be linked together).
097     * @param wrapColDim Whether to wrap the second dimension (i.e the first
098     * and last neurons will be linked together).
099     * @param neighbourhoodType Neighbourhood type.
100     * @param featuresList Arrays that will initialize the features sets of
101     * the network's neurons.
102     * @throws IllegalArgumentException if {@code numRows < 2} or
103     * {@code numCols < 2}.
104     */
105    public NeuronSquareMesh2D(boolean wrapRowDim,
106                              boolean wrapColDim,
107                              SquareNeighbourhood neighbourhoodType,
108                              double[][][] featuresList) {
109        numberOfRows = featuresList.length;
110        numberOfColumns = featuresList[0].length;
111
112        if (numberOfRows < MIN_ROWS) {
113            throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfRows, MIN_ROWS);
114        }
115        if (numberOfColumns < MIN_ROWS) {
116            throw new NeuralNetException(NeuralNetException.TOO_SMALL, numberOfColumns, MIN_ROWS);
117        }
118
119        wrapRows = wrapRowDim;
120        wrapColumns = wrapColDim;
121        neighbourhood = neighbourhoodType;
122
123        final int fLen = featuresList[0][0].length;
124        network = new Network(0, fLen);
125        identifiers = new long[numberOfRows][numberOfColumns];
126
127        // Add neurons.
128        for (int i = 0; i < numberOfRows; i++) {
129            for (int j = 0; j < numberOfColumns; j++) {
130                identifiers[i][j] = network.createNeuron(featuresList[i][j]);
131            }
132        }
133
134        // Add links.
135        createLinks();
136    }
137
138    /**
139     * Creates a two-dimensional network composed of square cells:
140     * Each neuron not located on the border of the mesh has four
141     * neurons linked to it.
142     * <br>
143     * The links are bi-directional.
144     * <br>
145     * The topology of the network can also be a cylinder (if one
146     * of the dimensions is wrapped) or a torus (if both dimensions
147     * are wrapped).
148     *
149     * @param numRows Number of neurons in the first dimension.
150     * @param wrapRowDim Whether to wrap the first dimension (i.e the first
151     * and last neurons will be linked together).
152     * @param numCols Number of neurons in the second dimension.
153     * @param wrapColDim Whether to wrap the second dimension (i.e the first
154     * and last neurons will be linked together).
155     * @param neighbourhoodType Neighbourhood type.
156     * @param featureInit Array of functions that will initialize the
157     * corresponding element of the features set of each newly created
158     * neuron. In particular, the size of this array defines the size of
159     * feature set.
160     * @throws IllegalArgumentException if {@code numRows < 2} or
161     * {@code numCols < 2}.
162     */
163    public NeuronSquareMesh2D(int numRows,
164                              boolean wrapRowDim,
165                              int numCols,
166                              boolean wrapColDim,
167                              SquareNeighbourhood neighbourhoodType,
168                              FeatureInitializer[] featureInit) {
169        if (numRows < MIN_ROWS) {
170            throw new NeuralNetException(NeuralNetException.TOO_SMALL, numRows, MIN_ROWS);
171        }
172        if (numCols < MIN_ROWS) {
173            throw new NeuralNetException(NeuralNetException.TOO_SMALL, numCols, MIN_ROWS);
174        }
175
176        numberOfRows = numRows;
177        wrapRows = wrapRowDim;
178        numberOfColumns = numCols;
179        wrapColumns = wrapColDim;
180        neighbourhood = neighbourhoodType;
181        identifiers = new long[numberOfRows][numberOfColumns];
182
183        final int fLen = featureInit.length;
184        network = new Network(0, fLen);
185
186        // Add neurons.
187        for (int i = 0; i < numRows; i++) {
188            for (int j = 0; j < numCols; j++) {
189                final double[] features = new double[fLen];
190                for (int fIndex = 0; fIndex < fLen; fIndex++) {
191                    features[fIndex] = featureInit[fIndex].value();
192                }
193                identifiers[i][j] = network.createNeuron(features);
194            }
195        }
196
197        // Add links.
198        createLinks();
199    }
200
201    /**
202     * Constructor with restricted access, solely used for making a
203     * {@link #copy() deep copy}.
204     *
205     * @param wrapRowDim Whether to wrap the first dimension (i.e the first
206     * and last neurons will be linked together).
207     * @param wrapColDim Whether to wrap the second dimension (i.e the first
208     * and last neurons will be linked together).
209     * @param neighbourhoodType Neighbourhood type.
210     * @param net Underlying network.
211     * @param idGrid Neuron identifiers.
212     */
213    private NeuronSquareMesh2D(boolean wrapRowDim,
214                               boolean wrapColDim,
215                               SquareNeighbourhood neighbourhoodType,
216                               Network net,
217                               long[][] idGrid) {
218        numberOfRows = idGrid.length;
219        numberOfColumns = idGrid[0].length;
220        wrapRows = wrapRowDim;
221        wrapColumns = wrapColDim;
222        neighbourhood = neighbourhoodType;
223        network = net;
224        identifiers = idGrid;
225    }
226
227    /**
228     * Performs a deep copy of this instance.
229     * Upon return, the copied and original instances will be independent:
230     * Updating one will not affect the other.
231     *
232     * @return a new instance with the same state as this instance.
233     * @since 3.6
234     */
235    public synchronized NeuronSquareMesh2D copy() {
236        final long[][] idGrid = new long[numberOfRows][numberOfColumns];
237        for (int r = 0; r < numberOfRows; r++) {
238            System.arraycopy(identifiers[r], 0, idGrid[r], 0, numberOfColumns);
239        }
240
241        return new NeuronSquareMesh2D(wrapRows,
242                                      wrapColumns,
243                                      neighbourhood,
244                                      network.copy(),
245                                      idGrid);
246    }
247
248    /** {@inheritDoc} */
249    @Override
250    public Iterator<Neuron> iterator() {
251        return network.iterator();
252    }
253
254    /**
255     * Retrieves the underlying network.
256     * A reference is returned (enabling, for example, the network to be
257     * trained).
258     * This also implies that calling methods that modify the {@link Network}
259     * topology may cause this class to become inconsistent.
260     *
261     * @return the network.
262     */
263    public Network getNetwork() {
264        return network;
265    }
266
267    /**
268     * Gets the number of neurons in each row of this map.
269     *
270     * @return the number of rows.
271     */
272    public int getNumberOfRows() {
273        return numberOfRows;
274    }
275
276    /**
277     * Gets the number of neurons in each column of this map.
278     *
279     * @return the number of column.
280     */
281    public int getNumberOfColumns() {
282        return numberOfColumns;
283    }
284
285    /**
286     * Indicates whether the map is wrapped along the first dimension.
287     *
288     * @return {@code true} if the last neuron of a row is linked to
289     * the first neuron of that row.
290     */
291    public boolean isWrappedRow() {
292        return wrapRows;
293    }
294
295    /**
296     * Indicates whether the map is wrapped along the second dimension.
297     *
298     * @return {@code true} if the last neuron of a column is linked to
299     * the first neuron of that column.
300     */
301    public boolean isWrappedColumn() {
302        return wrapColumns;
303    }
304
305    /**
306     * Indicates the {@link SquareNeighbourhood type of connectivity}
307     * between neurons.
308     *
309     * @return the neighbourhood type.
310     */
311    public SquareNeighbourhood getSquareNeighbourhood() {
312        return neighbourhood;
313    }
314
315    /**
316     * Retrieves the neuron at location {@code (i, j)} in the map.
317     * The neuron at position {@code (0, 0)} is located at the upper-left
318     * corner of the map.
319     *
320     * @param i Row index.
321     * @param j Column index.
322     * @return the neuron at {@code (i, j)}.
323     * @throws IllegalArgumentException if {@code i} or {@code j} is
324     * out of range.
325     *
326     * @see #getNeuron(int,int,HorizontalDirection,VerticalDirection)
327     */
328    public Neuron getNeuron(int i,
329                            int j) {
330        if (i < 0 ||
331            i >= numberOfRows) {
332            throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE,
333                                         i, 0, numberOfRows - 1);
334        }
335        if (j < 0 ||
336            j >= numberOfColumns) {
337            throw new NeuralNetException(NeuralNetException.OUT_OF_RANGE,
338                                         i, 0, numberOfColumns - 1);
339        }
340
341        return network.getNeuron(identifiers[i][j]);
342    }
343
344    /**
345     * Retrieves the requested neuron relative to the given {@code (row, col)}
346     * position.
347     * The neuron at position {@code (0, 0)} is located at the upper-left
348     * corner of the map.
349     *
350     * @param row Row index.
351     * @param col Column index.
352     * @param alongRowDir Direction along the given {@code row} (i.e. an
353     * offset will be added to the given <em>column</em> index.
354     * @param alongColDir Direction along the given {@code col} (i.e. an
355     * offset will be added to the given <em>row</em> index.
356     * @return the neuron at the requested location, or {@code null} if
357     * the location is not on the map.
358     *
359     * @see #getNeuron(int,int)
360     */
361    public Neuron getNeuron(int row,
362                            int col,
363                            HorizontalDirection alongRowDir,
364                            VerticalDirection alongColDir) {
365        final int[] location = getLocation(row, col, alongRowDir, alongColDir);
366
367        return location == null ? null : getNeuron(location[0], location[1]);
368    }
369
370    /**
371     * Computes various {@link DataVisualization indicators} of the quality
372     * of the representation of the given {@code data} by this map.
373     *
374     * @param data Features.
375     * @return a new instance holding quality indicators.
376     */
377    public DataVisualization computeQualityIndicators(Iterable<double[]> data) {
378        return DataVisualization.from(copy(), data);
379    }
380
381    /**
382     * Computes the location of a neighbouring neuron.
383     * Returns {@code null} if the resulting location is not part
384     * of the map.
385     * Position {@code (0, 0)} is at the upper-left corner of the map.
386     *
387     * @param row Row index.
388     * @param col Column index.
389     * @param alongRowDir Direction along the given {@code row} (i.e. an
390     * offset will be added to the given <em>column</em> index.
391     * @param alongColDir Direction along the given {@code col} (i.e. an
392     * offset will be added to the given <em>row</em> index.
393     * @return an array of length 2 containing the indices of the requested
394     * location, or {@code null} if that location is not part of the map.
395     *
396     * @see #getNeuron(int,int)
397     */
398    private int[] getLocation(int row,
399                              int col,
400                              HorizontalDirection alongRowDir,
401                              VerticalDirection alongColDir) {
402        final int colOffset;
403        switch (alongRowDir) {
404        case LEFT:
405            colOffset = -1;
406            break;
407        case RIGHT:
408            colOffset = 1;
409            break;
410        case CENTER:
411            colOffset = 0;
412            break;
413        default:
414            // Should never happen.
415            throw new IllegalStateException();
416        }
417        int colIndex = col + colOffset;
418        if (wrapColumns) {
419            if (colIndex < 0) {
420                colIndex += numberOfColumns;
421            } else {
422                colIndex %= numberOfColumns;
423            }
424        }
425
426        final int rowOffset;
427        switch (alongColDir) {
428        case UP:
429            rowOffset = -1;
430            break;
431        case DOWN:
432            rowOffset = 1;
433            break;
434        case CENTER:
435            rowOffset = 0;
436            break;
437        default:
438            // Should never happen.
439            throw new IllegalStateException();
440        }
441        int rowIndex = row + rowOffset;
442        if (wrapRows) {
443            if (rowIndex < 0) {
444                rowIndex += numberOfRows;
445            } else {
446                rowIndex %= numberOfRows;
447            }
448        }
449
450        if (rowIndex < 0 ||
451            rowIndex >= numberOfRows ||
452            colIndex < 0 ||
453            colIndex >= numberOfColumns) {
454            return null;
455        } else {
456            return new int[] {rowIndex, colIndex};
457        }
458    }
459
460    /**
461     * Creates the neighbour relationships between neurons.
462     */
463    private void createLinks() {
464        // "linkEnd" will store the identifiers of the "neighbours".
465        final List<Long> linkEnd = new ArrayList<>();
466        final int iLast = numberOfRows - 1;
467        final int jLast = numberOfColumns - 1;
468        for (int i = 0; i < numberOfRows; i++) {
469            for (int j = 0; j < numberOfColumns; j++) {
470                linkEnd.clear();
471
472                switch (neighbourhood) {
473
474                case MOORE:
475                    // Add links to "diagonal" neighbours.
476                    if (i > 0) {
477                        if (j > 0) {
478                            linkEnd.add(identifiers[i - 1][j - 1]);
479                        }
480                        if (j < jLast) {
481                            linkEnd.add(identifiers[i - 1][j + 1]);
482                        }
483                    }
484                    if (i < iLast) {
485                        if (j > 0) {
486                            linkEnd.add(identifiers[i + 1][j - 1]);
487                        }
488                        if (j < jLast) {
489                            linkEnd.add(identifiers[i + 1][j + 1]);
490                        }
491                    }
492                    if (wrapRows) {
493                        if (i == 0) {
494                            if (j > 0) {
495                                linkEnd.add(identifiers[iLast][j - 1]);
496                            }
497                            if (j < jLast) {
498                                linkEnd.add(identifiers[iLast][j + 1]);
499                            }
500                        } else if (i == iLast) {
501                            if (j > 0) {
502                                linkEnd.add(identifiers[0][j - 1]);
503                            }
504                            if (j < jLast) {
505                                linkEnd.add(identifiers[0][j + 1]);
506                            }
507                        }
508                    }
509                    if (wrapColumns) {
510                        if (j == 0) {
511                            if (i > 0) {
512                                linkEnd.add(identifiers[i - 1][jLast]);
513                            }
514                            if (i < iLast) {
515                                linkEnd.add(identifiers[i + 1][jLast]);
516                            }
517                        } else if (j == jLast) {
518                            if (i > 0) {
519                                linkEnd.add(identifiers[i - 1][0]);
520                            }
521                            if (i < iLast) {
522                                linkEnd.add(identifiers[i + 1][0]);
523                            }
524                        }
525                    }
526                    if (wrapRows &&
527                        wrapColumns) {
528                        if (i == 0 &&
529                            j == 0) {
530                            linkEnd.add(identifiers[iLast][jLast]);
531                        } else if (i == 0 &&
532                                   j == jLast) {
533                            linkEnd.add(identifiers[iLast][0]);
534                        } else if (i == iLast &&
535                                   j == 0) {
536                            linkEnd.add(identifiers[0][jLast]);
537                        } else if (i == iLast &&
538                                   j == jLast) {
539                            linkEnd.add(identifiers[0][0]);
540                        }
541                    }
542
543                    // Case falls through since the "Moore" neighbourhood
544                    // also contains the neurons that belong to the "Von
545                    // Neumann" neighbourhood.
546
547                    // fallthru (CheckStyle)
548                case VON_NEUMANN:
549                    // Links to preceding and following "row".
550                    if (i > 0) {
551                        linkEnd.add(identifiers[i - 1][j]);
552                    }
553                    if (i < iLast) {
554                        linkEnd.add(identifiers[i + 1][j]);
555                    }
556                    if (wrapRows) {
557                        if (i == 0) {
558                            linkEnd.add(identifiers[iLast][j]);
559                        } else if (i == iLast) {
560                            linkEnd.add(identifiers[0][j]);
561                        }
562                    }
563
564                    // Links to preceding and following "column".
565                    if (j > 0) {
566                        linkEnd.add(identifiers[i][j - 1]);
567                    }
568                    if (j < jLast) {
569                        linkEnd.add(identifiers[i][j + 1]);
570                    }
571                    if (wrapColumns) {
572                        if (j == 0) {
573                            linkEnd.add(identifiers[i][jLast]);
574                        } else if (j == jLast) {
575                            linkEnd.add(identifiers[i][0]);
576                        }
577                    }
578                    break;
579
580                default:
581                    throw new IllegalStateException(); // Cannot happen.
582                }
583
584                final Neuron aNeuron = network.getNeuron(identifiers[i][j]);
585                for (final long b : linkEnd) {
586                    final Neuron bNeuron = network.getNeuron(b);
587                    // Link to all neighbours.
588                    // The reverse links will be added as the loop proceeds.
589                    network.addLink(aNeuron, bNeuron);
590                }
591            }
592        }
593    }
594
595    /**
596     * Miscellaneous indicators of the map quality.
597     * <ul>
598     *  <li>Hit histogram</li>
599     *  <li>Quantization error</li>
600     *  <li>Topographic error</li>
601     *  <li>Unified distance matrix</li>
602     * </ul>
603     */
604    public static final class DataVisualization {
605        /** Distance function. */
606        private static final DistanceMeasure DISTANCE = new EuclideanDistance();
607        /** Total number of samples. */
608        private final int numberOfSamples;
609        /** Hit histogram. */
610        private final double[][] hitHistogram;
611        /** Quantization error. */
612        private final double[][] quantizationError;
613        /** Mean quantization error. */
614        private final double meanQuantizationError;
615        /** Topographic error. */
616        private final double[][] topographicError;
617        /** Mean topographic error. */
618        private final double meanTopographicError;
619        /** U-matrix. */
620        private final double[][] uMatrix;
621
622        /**
623         * @param numberOfSamples Number of samples.
624         * @param hitHistogram Hit histogram.
625         * @param quantizationError Quantization error.
626         * @param topographicError Topographic error.
627         * @param uMatrix U-matrix.
628         */
629        private DataVisualization(int numberOfSamples,
630                                  double[][] hitHistogram,
631                                  double[][] quantizationError,
632                                  double[][] topographicError,
633                                  double[][] uMatrix) {
634            this.numberOfSamples = numberOfSamples;
635            this.hitHistogram = hitHistogram;
636            this.quantizationError = quantizationError;
637            meanQuantizationError = hitWeightedMean(quantizationError, hitHistogram);
638            this.topographicError = topographicError;
639            meanTopographicError = hitWeightedMean(topographicError, hitHistogram);
640            this.uMatrix = uMatrix;
641        }
642
643        /**
644         * @param map Map
645         * @param data Data.
646         * @return the metrics.
647         */
648        static DataVisualization from(NeuronSquareMesh2D map,
649                                      Iterable<double[]> data) {
650            final LocationFinder finder = new LocationFinder(map);
651            final MapRanking rank = new MapRanking(map, DISTANCE);
652            final Network net = map.getNetwork();
653            final int nR = map.getNumberOfRows();
654            final int nC = map.getNumberOfColumns();
655
656            // Hit bins.
657            final int[][] hitCounter = new int[nR][nC];
658            // Hit bins.
659            final double[][] hitHistogram = new double[nR][nC];
660            // Quantization error bins.
661            final double[][] quantizationError = new double[nR][nC];
662            // Topographic error bins.
663            final double[][] topographicError = new double[nR][nC];
664            // U-matrix.
665            final double[][] uMatrix = new double[nR][nC];
666
667            int numSamples = 0;
668            for (final double[] sample : data) {
669                ++numSamples;
670
671                final List<Neuron> winners = rank.rank(sample, 2);
672                final Neuron best = winners.get(0);
673                final Neuron secondBest = winners.get(1);
674
675                final LocationFinder.Location locBest = finder.getLocation(best);
676                final int rowBest = locBest.getRow();
677                final int colBest = locBest.getColumn();
678                // Increment hit counter.
679                hitCounter[rowBest][colBest] += 1;
680
681                // Aggregate quantization error.
682                quantizationError[rowBest][colBest] += DISTANCE.applyAsDouble(sample, best.getFeatures());
683
684                // Aggregate topographic error.
685                if (!net.getNeighbours(best).contains(secondBest)) {
686                    // Increment count if first and second best matching units
687                    // are not neighbours.
688                    topographicError[rowBest][colBest] += 1;
689                }
690            }
691
692            for (int r = 0; r < nR; r++) {
693                for (int c = 0; c < nC; c++) {
694                    final Neuron neuron = map.getNeuron(r, c);
695                    final Collection<Neuron> neighbours = net.getNeighbours(neuron);
696                    final double[] features = neuron.getFeatures();
697                    double uDistance = 0;
698                    int neighbourCount = 0;
699                    for (final Neuron n : neighbours) {
700                        ++neighbourCount;
701                        uDistance += DISTANCE.applyAsDouble(features, n.getFeatures());
702                    }
703
704                    final int hitCount = hitCounter[r][c];
705                    if (hitCount != 0) {
706                        hitHistogram[r][c] = hitCount / (double) numSamples;
707                        quantizationError[r][c] /= hitCount;
708                        topographicError[r][c] /= hitCount;
709                    }
710
711                    uMatrix[r][c] = uDistance / neighbourCount;
712                }
713            }
714
715            return new DataVisualization(numSamples,
716                                         hitHistogram,
717                                         quantizationError,
718                                         topographicError,
719                                         uMatrix);
720        }
721
722        /**
723         * @return the total number of samples.
724         */
725        public int getNumberOfSamples() {
726            return numberOfSamples;
727        }
728
729        /**
730         * @return the quantization error.
731         * Each bin will contain the average of the distances between samples
732         * mapped to the corresponding unit and the weight vector of that unit.
733         * @see #getMeanQuantizationError()
734         */
735        public double[][] getQuantizationError() {
736            return copy(quantizationError);
737        }
738
739        /**
740         * @return the topographic error.
741         * Each bin will contain the number of data for which the first and
742         * second best matching units are not adjacent in the map.
743         * @see #getMeanTopographicError()
744         */
745        public double[][] getTopographicError() {
746            return copy(topographicError);
747        }
748
749        /**
750         * @return the hits histogram (normalized).
751         * Each bin will contain the number of data for which the corresponding
752         * neuron is the best matching unit.
753         */
754        public double[][] getNormalizedHits() {
755            return copy(hitHistogram);
756        }
757
758        /**
759         * @return the U-matrix.
760         * Each bin will contain the average distance between a unit and all its
761         * neighbours will be computed (and stored in the pixel corresponding to
762         * that unit of the 2D-map).  The number of neighbours taken into account
763         * depends on the network {@link org.apache.commons.math4.neuralnet.SquareNeighbourhood
764         * neighbourhood type}.
765         */
766        public double[][] getUMatrix() {
767            return copy(uMatrix);
768        }
769
770        /**
771         * @return the mean (hit-weighted) quantization error.
772         * @see #getQuantizationError()
773         */
774        public double getMeanQuantizationError() {
775            return meanQuantizationError;
776        }
777
778        /**
779         * @return the mean (hit-weighted) topographic error.
780         * @see #getTopographicError()
781         */
782        public double getMeanTopographicError() {
783            return meanTopographicError;
784        }
785
786        /**
787         * @param orig Source.
788         * @return a deep copy of the original array.
789         */
790        private static double[][] copy(double[][] orig) {
791            final double[][] copy = new double[orig.length][];
792            for (int i = 0; i < orig.length; i++) {
793                copy[i] = orig[i].clone();
794            }
795
796            return copy;
797        }
798
799        /**
800         * @param metrics Metrics.
801         * @param normalizedHits Hits histogram (normalized).
802         * @return the hit-weighted mean of the given {@code metrics}.
803         */
804        private static double hitWeightedMean(double[][] metrics,
805                                              double[][] normalizedHits) {
806            double mean = 0;
807            final int rows = metrics.length;
808            final int cols = metrics[0].length;
809            for (int i = 0; i < rows; i++) {
810                for (int j = 0; j < cols; j++) {
811                    mean += normalizedHits[i][j] * metrics[i][j];
812                }
813            }
814
815            return mean;
816        }
817    }
818}