/* * Copyright 2004-2010 Information & Software Engineering Group (188/1) * Institute of Software Technology and Interactive Systems * Vienna University of Technology, Austria * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package at.tuwien.ifs.somtoolbox.visualization; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.logging.Logger; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import cern.colt.matrix.DoubleMatrix2D; import cern.colt.matrix.impl.DenseDoubleMatrix2D; import cern.jet.math.Functions; import flanagan.interpolation.BiCubicSplineFast; import at.tuwien.ifs.somtoolbox.SOMToolboxException; import at.tuwien.ifs.somtoolbox.data.InputData; import at.tuwien.ifs.somtoolbox.data.SOMVisualisationData; import at.tuwien.ifs.somtoolbox.input.SOMLibDataWinnerMapping; import at.tuwien.ifs.somtoolbox.layers.Unit; import at.tuwien.ifs.somtoolbox.models.GrowingSOM; import at.tuwien.ifs.somtoolbox.util.StdErrProgressWriter; import at.tuwien.ifs.somtoolbox.util.VectorTools; import at.tuwien.ifs.somtoolbox.util.comparables.UnitDistance; /** * This visualizer provides an implementation of the <i>Smoothed Data Histograms</i> in three variants. * <ol> * <li>Implementation of the Smoothed Data Histograms as described in <i><b>E. Pampalk, A. Rauber, and D. Merkl.</b> * Proceedings of the International Conference on Artificial Neural Networks (ICANN'02), pp 871-876, LNCS 2415, Madrid, * Spain, August 27-30, 2002, Springer Verlag.</i></li> * <li>An extension of the Smoothed Data Histograms. Not the rank is taken into account for histogram calculation, but * distances "between input vectors and weight vectors.</li> * <li>As 2., but additionally values are normalized per datum.</li> * </ol> * * @author Michael Dittenbach * @author Rudolf Mayer * @version $Id: SmoothedDataHistograms.java 3888 2010-11-02 17:42:53Z frank $ */ public class SmoothedDataHistograms extends AbstractMatrixVisualizer implements BackgroundImageVisualizer, ChangeListener { /** * The minimum value for the smoothing factor (1), resulting in only the best winning units to get a hit counted. */ protected static final int MIN_SMOOTHING_VALUE = 1; /** * The maximum value for the smoothing factor (300). */ protected static int MAX_SMOOTHING_VALUE = 300; /** * The default value for the smoothing factor (15). */ protected static int DEFAULT_SMOOTHING_VALUE = 15; /** * The currently used smoothing factor. The smoothing factor decides how many n-best matching units get a hit * counted. */ protected int s = DEFAULT_SMOOTHING_VALUE; /** * A cache for the different smoothing factors. */ protected Hashtable<Integer, Histogram>[] smoothingCache = null; protected SOMLibDataWinnerMapping dataWinnerMapping = null; @SuppressWarnings("unchecked") public SmoothedDataHistograms() { NUM_VISUALIZATIONS = 3; VISUALIZATION_NAMES = new String[] { "Smoothed Data Histograms", "Weighted SDH", "Weighted SDH (norm.)" }; VISUALIZATION_SHORT_NAMES = new String[] { "SDH", "WeightedSDH", "WeightedSDHNorm" }; VISUALIZATION_DESCRIPTIONS = new String[] { "Implementation of Smoothed Data Histograms as described in \"E. Pampalk, A. Rauber, and D. Merkl.\n" + "Proceedings of the International Conference on Artificial Neural Networks (ICANN'02),\n" + "pp 871-876, LNCS 2415, Madrid, Spain, August 27-30, 2002, Springer Verlag.", "Extension of Smoothed Data Histograms. Not rank is taken into account for histogram calculation, but distances\n" + "between input vectors and weight vectors.", "Extension of Smoothed Data Histograms. Not rank is taken into account for histogram calculation, but distances\n" + "between input vectors and weight vectors. Values are normalized per datum." }; neededInputObjects = new String[] { SOMVisualisationData.DATA_WINNER_MAPPING, SOMVisualisationData.INPUT_VECTOR }; reversePalette(); smoothingCache = new Hashtable[NUM_VISUALIZATIONS]; // don't initialise the control panel if we have no graphics environment (e.g. in server applications) if (!GraphicsEnvironment.isHeadless()) { controlPanel = new SDHControlPanel(); } } @Override protected String getCacheKey(GrowingSOM gsom, int index, int width, int height) { return super.getCacheKey(gsom, index, width, height) + CACHE_KEY_SECTION_SEPARATOR + "smoothing:" + s; } /** Visualisation for a specific smoothing factor */ public BufferedImage getVisualization(int index, int smoothingFactor, GrowingSOM gsom, int width, int height) throws SOMToolboxException { int oldSmoothingFactor = s; s = smoothingFactor; controlPanel.updateZDim(gsom.getLayer().getZSize()); String cacheKey = getCacheKey(gsom, index, width, height); logImageCache(cacheKey); if (cache.get(cacheKey) == null) { cache.put(cacheKey, createVisualization(index, gsom, width, height)); } s = oldSmoothingFactor; return cache.get(cacheKey); } @Override public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height) throws SOMToolboxException { return createVisualization(index, gsom, width, height, 1, 1, false, true); } public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height, int blockWidth, int blockHeight, boolean forceSmoothingCacheInitialisation, boolean shallDrawBackground) throws SOMToolboxException { checkNeededObjectsAvailable(gsom); ceckInitSmoothingCache(gsom, forceSmoothingCacheInitialisation); // FIXME: this part is not working anymore, but has to be generally in AbstractBackgroundImageVisualizer BufferedImage res = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = (Graphics2D) res.getGraphics(); if (shallDrawBackground) { drawBackground(width, height, g); } return super.createImage(gsom, smoothingCache[index].get(s).mh, width, height, interpolate); } @Override protected BiCubicSplineFast computeSpline(GrowingSOM gsom, DoubleMatrix2D matrix, int width, int height, int unitWidth, int unitHeight) { int xSize = gsom.getLayer().getXSize(); int ySize = gsom.getLayer().getYSize(); double[] x1 = new double[xSize + 2]; x1[0] = 0; for (int x = 0; x < xSize; x++) { x1[x + 1] = x * unitWidth + unitWidth / 2; } x1[xSize + 1] = width; double[] x2 = new double[ySize + 2]; x2[0] = 0; for (int y = 0; y < ySize; y++) { x2[y + 1] = y * unitHeight + unitHeight / 2; } x2[ySize + 1] = height; DenseDoubleMatrix2D sdhWithBorders = new DenseDoubleMatrix2D(ySize + 2, xSize + 2); // top-left corner sdhWithBorders.setQuick(0, 0, matrix.getQuick(0, 0) - (matrix.getQuick(1, 1) - matrix.getQuick(0, 0)) / 2); // top-right corner sdhWithBorders.setQuick(0, xSize + 1, matrix.getQuick(0, xSize - 1) - (matrix.getQuick(1, xSize - 2) - matrix.getQuick(0, xSize - 1)) / 2); // bottom-left corner sdhWithBorders.setQuick(ySize + 1, 0, matrix.getQuick(ySize - 1, 0) - (matrix.getQuick(ySize - 2, 1) - matrix.getQuick(ySize - 1, 0)) / 2); // FIXME ? was sdhWithBorders.setQuick(xSize, ySize) for former method 'createVisualization3'. // bottom-right corner sdhWithBorders.setQuick( ySize + 1, xSize + 1, matrix.getQuick(ySize - 1, xSize - 1) - (matrix.getQuick(ySize - 2, xSize - 2) - matrix.getQuick(ySize - 1, xSize - 1)) / 2); for (int x = 1; x < xSize + 1; x++) { // top row sdhWithBorders.setQuick(0, x, matrix.getQuick(0, x - 1) - (matrix.getQuick(1, x - 1) - matrix.getQuick(0, x - 1)) / 2); // bottom row sdhWithBorders.setQuick( ySize + 1, x, matrix.getQuick(ySize - 1, x - 1) - (matrix.getQuick(ySize - 2, x - 1) - matrix.getQuick(ySize - 1, x - 1)) / 2); } for (int y = 1; y < ySize + 1; y++) { // left column sdhWithBorders.setQuick(y, 0, matrix.getQuick(y - 1, 0) - (matrix.getQuick(y - 1, 1) - matrix.getQuick(y - 1, 0)) / 2); // right column sdhWithBorders.setQuick( y, xSize + 1, matrix.getQuick(y - 1, xSize - 1) - (matrix.getQuick(y - 1, xSize - 2) - matrix.getQuick(y - 1, xSize - 1)) / 2); } for (int y = 0; y < ySize; y++) { for (int x = 0; x < xSize; x++) { sdhWithBorders.setQuick(y + 1, x + 1, matrix.getQuick(y, x)); } } BiCubicSplineFast bcs = new BiCubicSplineFast(x2, x1, sdhWithBorders.toArray()); return bcs; } @Override protected void checkNeededObjectsAvailable(GrowingSOM gsom) throws SOMToolboxException { if (gsom.getSharedInputObjects().getData(neededInputObjects[0]) == null && gsom.getSharedInputObjects().getData(neededInputObjects[1]) == null) { throw new SOMToolboxException("You need to specify at least one out of " + neededInputObjects[0] + " or " + neededInputObjects[1]); } } protected void ceckInitSmoothingCache(GrowingSOM gsom, boolean forceSmoothingCacheInitialisation) throws SOMToolboxException { if (forceSmoothingCacheInitialisation || smoothingCache[0] == null || smoothingCache[1] == null || smoothingCache[2] == null) { initSmoothingCache(gsom); } } @Override public void stateChanged(ChangeEvent e) { if (e.getSource().getClass() == JSpinner.class) { JSpinner src = (JSpinner) e.getSource(); s = ((Integer) src.getValue()).intValue(); } if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } /** * Return the currently used smoothing factor. * * @return the smoothing factor */ public int getSmoothingFactor() { return s; } protected void computeDefaultAndMaxSmoothingValues(int xSize, int ySize) { if (xSize * ySize < MAX_SMOOTHING_VALUE) { MAX_SMOOTHING_VALUE = xSize * ySize; DEFAULT_SMOOTHING_VALUE = (MAX_SMOOTHING_VALUE - MIN_SMOOTHING_VALUE) / 2; s = DEFAULT_SMOOTHING_VALUE; if (controlPanel != null) { // check needed for headless environments ((SDHControlPanel) controlPanel).spinnerSmoothingFactor.setModel(new SpinnerNumberModel(s, MIN_SMOOTHING_VALUE, MAX_SMOOTHING_VALUE, 1)); } } } protected void initSmoothingCache(GrowingSOM gsom) throws SOMToolboxException { computeDefaultAndMaxSmoothingValues(gsom.getLayer().getXSize(), gsom.getLayer().getYSize()); Logger.getLogger("at.tuwien.ifs.somtoolbox").info( "Initialization of SDH cache (s=" + MIN_SMOOTHING_VALUE + ",...," + MAX_SMOOTHING_VALUE + ") started."); for (int i = 0; i < smoothingCache.length; i++) { smoothingCache[i] = new Hashtable<Integer, Histogram>(); } // get max number of winners for each datum int numVectors = 0; Unit[][] winners = null; int[][] xPos = null; int[][] yPos = null; int[][] zPos = null; double[][] dists = null; if (gsom.getSharedInputObjects().getDataWinnerMapping() != null) { // we have mapping or file dataWinnerMapping = gsom.getSharedInputObjects().getDataWinnerMapping(); numVectors = dataWinnerMapping.getNumVectors(); winners = new Unit[numVectors][]; xPos = new int[numVectors][]; yPos = new int[numVectors][]; zPos = new int[numVectors][]; dists = new double[numVectors][]; for (int d = 0; d < numVectors; d++) { xPos[d] = dataWinnerMapping.getXPos(d); yPos[d] = dataWinnerMapping.getYPos(d); zPos[d] = dataWinnerMapping.getZPos(d); dists[d] = dataWinnerMapping.getDists(d); } MAX_SMOOTHING_VALUE = dataWinnerMapping.getNumBMUs(); DEFAULT_SMOOTHING_VALUE = (MAX_SMOOTHING_VALUE - MIN_SMOOTHING_VALUE) / 2; s = DEFAULT_SMOOTHING_VALUE; if (controlPanel != null) { // check needed for headless environments ((SDHControlPanel) controlPanel).spinnerSmoothingFactor.setModel(new SpinnerNumberModel(s, MIN_SMOOTHING_VALUE, MAX_SMOOTHING_VALUE, 1)); } } else if (gsom.getSharedInputObjects().getInputData() != null) { // we have an input vector file InputData data = gsom.getSharedInputObjects().getInputData(); // FIXME: sparsity!!!!!!!!!!!!!!!!!!!!!!!!!!! // FIXME: sparsity!!!!!!!!!!!!!!!!!!!!!!!!!!! // FIXME: sparsity!!!!!!!!!!!!!!!!!!!!!!!!!!! // FIXME: sparsity!!!!!!!!!!!!!!!!!!!!!!!!!!! numVectors = data.numVectors(); winners = new Unit[numVectors][]; xPos = new int[numVectors][]; yPos = new int[numVectors][]; zPos = new int[numVectors][]; dists = new double[numVectors][]; StdErrProgressWriter progressWriter = new StdErrProgressWriter(numVectors, "Getting winners for datum ", 10); for (int d = 0; d < numVectors; d++) { UnitDistance[] winnDist = gsom.getLayer().getWinnersAndDistances(data.getInputDatum(d), MAX_SMOOTHING_VALUE); winners[d] = new Unit[winnDist.length]; dists[d] = new double[winnDist.length]; for (int i = 0; i < winnDist.length; i++) { winners[d][i] = winnDist[i].getUnit(); dists[d][i] = winnDist[i].getDistance(); } xPos[d] = new int[MAX_SMOOTHING_VALUE]; yPos[d] = new int[MAX_SMOOTHING_VALUE]; zPos[d] = new int[MAX_SMOOTHING_VALUE]; for (int w = 0; w < MAX_SMOOTHING_VALUE; w++) { xPos[d][w] = winners[d][w].getXPos(); yPos[d][w] = winners[d][w].getYPos(); zPos[d][w] = winners[d][w].getZPos(); } progressWriter.progress(d + 1); // TODO: store the generated data winner mapping for sub-sequent use. } } else { // throw an exception that will later be handled throw new SOMToolboxException("You need to specify at least one out of " + neededInputObjects[0] + " or " + neededInputObjects[1]); } // create and cache histograms for each smoothing factor StdErrProgressWriter progressWriter = new StdErrProgressWriter(MAX_SMOOTHING_VALUE, "Smoothing factor: "); for (int svar = MIN_SMOOTHING_VALUE; svar <= MAX_SMOOTHING_VALUE; svar++) { DenseDoubleMatrix2D sdhs[] = new DenseDoubleMatrix2D[NUM_VISUALIZATIONS]; for (int k = 0; k < NUM_VISUALIZATIONS; k++) { sdhs[k] = new DenseDoubleMatrix2D(gsom.getLayer().getYSize(), gsom.getLayer().getXSize()); } int cs = 0; for (int i = 0; i < svar; i++) { cs += svar - i; } for (int d = 0; d < numVectors; d++) { // normalization needed for sdh variant 3 double max = 0; double min = Double.MAX_VALUE; for (int w = 0; w < svar; w++) { if (dists[d][w] > max) { max = dists[d][w]; } if (dists[d][w] < min) { min = dists[d][w]; } } // create sdh matrix entries for (int w = 0; w < svar; w++) { final int row = xPos[d][w]; final int column = yPos[d][w]; sdhs[0].setQuick(column, row, sdhs[0].getQuick(column, row) + ((double) svar - (double) w) / cs); sdhs[1].setQuick(column, row, sdhs[1].getQuick(column, row) + 1.0d / dists[d][w]); sdhs[2].setQuick(column, row, sdhs[2].getQuick(column, row) + 1.0d - (dists[d][w] - min) / (max - min)); } } // determine max and min value of matrices double[] maxValues = new double[NUM_VISUALIZATIONS]; double[] minValues = new double[NUM_VISUALIZATIONS]; for (int i = 0; i < maxValues.length; i++) { maxValues[i] = 0; minValues[i] = Double.MAX_VALUE; } for (int k = 0; k < NUM_VISUALIZATIONS; k++) { minValues[k] = sdhs[k].aggregate(Functions.min, Functions.identity); maxValues[k] = sdhs[k].aggregate(Functions.max, Functions.identity); } // normalize sdh matrix for (int k = 0; k < NUM_VISUALIZATIONS; k++) { VectorTools.normalise(sdhs[k]); } // set the generated smoothing caches. for (int k = 0; k < NUM_VISUALIZATIONS; k++) { smoothingCache[k].put(new Integer(svar), new Histogram(sdhs[k])); } progressWriter.progress(); } winners = null; Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Initialization of SDH cache finished."); } protected class Histogram { public DenseDoubleMatrix2D mh; public Histogram(DenseDoubleMatrix2D h) { mh = h; } } /** * A control panel extending the generic {@link AbstractBackgroundImageVisualizer.VisualizationControlPanel}, adding * additionally a {@link JSpinner} for controlling the smoothing factor. * * @author Rudolf Mayer */ public class SDHControlPanel extends VisualizationControlPanel { private static final long serialVersionUID = 1L; /** * The {@link JSpinner} controlling the smoothing factor. */ public JSpinner spinnerSmoothingFactor = null; private SDHControlPanel() { super("SDH Control"); spinnerSmoothingFactor = new JSpinner(new SpinnerNumberModel(getSmoothingFactor(), MIN_SMOOTHING_VALUE, MAX_SMOOTHING_VALUE, 1)); spinnerSmoothingFactor.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { s = (Integer) spinnerSmoothingFactor.getValue(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); JPanel smoothingFactorPanel = new JPanel(); smoothingFactorPanel.add(new JLabel("Smoothing factor: ")); smoothingFactorPanel.add(spinnerSmoothingFactor); add(smoothingFactorPanel, c); } } /** * Overrides {@link AbstractBackgroundImageVisualizer#needsAdditionalFiles()}, as we need only one of the two * possible input files to create this visualisation. If the data winner mapping is present, it will be used * directly, otherwise it can be created from the input vectors. */ @Override public String[] needsAdditionalFiles() { String[] dataFiles = super.needsAdditionalFiles(); if (dataFiles.length < 2) { // we need only one of the files return null; } else { return dataFiles; } } /** * Sets the smoothing factor. * * @param smoothingFactor the new smoothing factor */ public void setSmoothingFactor(int smoothingFactor) { s = smoothingFactor; } @Override @SuppressWarnings("rawtypes") public String getHTMLVisualisationControl(Map params) { StringBuffer b = new StringBuffer(); b.append("Smoothing factor:\n"); b.append("<select name=\"smoothingFactor\">\n"); for (int i = MIN_SMOOTHING_VALUE; i < MAX_SMOOTHING_VALUE; i++) { b.append("<option value=\"" + i + "\""); if (params.get("smoothingFactor") != null && params.get("smoothingFactor").equals(String.valueOf(i))) { b.append(" selected"); } b.append(">" + i + "</option>\n"); } b.append("</select>\n"); return b.toString(); } @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int variantIndex, GrowingSOM gsom, int width, int height) throws SOMToolboxException { return getVisualizationFlavours(variantIndex, gsom, width, height, MAX_SMOOTHING_VALUE); } @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int variantIndex, GrowingSOM gsom, int width, int height, int maxFlavours) throws SOMToolboxException { HashMap<String, BufferedImage> result = new HashMap<String, BufferedImage>(); // we need to set the default values, otherwise in the first iteration, we won't be using smoothing factor 1, // but the computed"optimal" value initSmoothingCache(gsom); int currentSF = getSmoothingFactor(); int count = 0; for (int i = 1; i <= MAX_SMOOTHING_VALUE;) { count++; if (variantIndex == 2 && i == 1) { continue; // smoothing factor 1 doesn't work with normalised version, just skip the image } String key = String.format("_smooth%d", i); BufferedImage val = getVisualization(variantIndex, i, gsom, width, height); result.put(key, val); if (count >= maxFlavours) { break; } if (i < 20) { i += 1; } if (20 <= i && i < 50) { i += 2; } else if (50 <= i) { i += 5; } } setSmoothingFactor(currentSF); return result; } @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int variantIndex, GrowingSOM gsom, int width, int height, Map<String, String> flavourParameters) throws SOMToolboxException { // FIXME: Implement Method return getVisualizationFlavours(variantIndex, gsom, width, height); } }