sammon icon indicating copy to clipboard operation
sammon copied to clipboard

error doesn't decrease

Open codeprof opened this issue 7 years ago • 3 comments

To me it seems there is something wrong with the sammon mapping. The error does not decrease and it seems to do not deliver good results if pca is not used.

codeprof avatar May 24 '17 12:05 codeprof

Thanks @codeprof, I'll look into this when I get a chance to see if I can work out what's happened. It may be a result of a recently merged pull request.

tompollard avatar May 24 '17 16:05 tompollard

At the moment I use the following code (old) with small changes. I do not use the Heassian (commented out) as this seems to result in problems with PCA initialisation. The values in H are "nan", because the result of PCA seems to be complex numbers...

`def sammon(x, n = 2, display = 2, inputdist = 'raw', maxhalves = 20, maxiter = 500, tolfun = 1e-9, init = 'pca'):

import numpy as np 
from scipy.spatial.distance import cdist

"""Perform Sammon mapping on dataset x

y = sammon(x) applies the Sammon nonlinear mapping procedure on
multivariate data x, where each row represents a pattern and each column
represents a feature.  On completion, y contains the corresponding
co-ordinates of each point on the map.  By default, a two-dimensional
map is created.  Note if x contains any duplicated rows, SAMMON will
fail (ungracefully). 

[y,E] = sammon(x) also returns the value of the cost function in E (i.e.
the stress of the mapping).

An N-dimensional output map is generated by y = sammon(x,n) .

A set of optimisation options can be specified using optional
arguments, y = sammon(x,n,[OPTS]):

   maxiter        - maximum number of iterations
   tolfun         - relative tolerance on objective function
   maxhalves      - maximum number of step halvings
   input          - {'raw','distance'} if set to 'distance', X is 
                    interpreted as a matrix of pairwise distances.
   display        - 0 to 2. 0 least verbose, 2 max verbose.
   init           - {'pca', 'random'}

The default options are retrieved by calling sammon(x) with no
parameters.

File        : sammon.py
Date        : 18 April 2014
Authors     : Tom J. Pollard ([email protected])
            : Ported from MATLAB implementation by 
              Gavin C. Cawley and Nicola L. C. Talbot

Description : Simple python implementation of Sammon's non-linear
              mapping algorithm [1].

References  : [1] Sammon, John W. Jr., "A Nonlinear Mapping for Data
              Structure Analysis", IEEE Transactions on Computers,
              vol. C-18, no. 5, pp 401-409, May 1969.

Copyright   : (c) Dr Gavin C. Cawley, November 2007.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""

# Create distance matrix unless given by parameters
if inputdist == 'distance':
    D = x
else:
    D = cdist(x,x)

# Remaining initialisation
N = x.shape[0] # hmmm, shape[1]?
scale = 0.5 / D.sum()
D = D + np.eye(N)
Dinv = 1 / D # Returns inf where D = 0.
Dinv[np.isinf(Dinv)] = 0 # Fix by replacing inf with 0 (default Matlab behaviour).
if init == 'pca':
    [UU,DD,_] = np.linalg.svd(x)
    y = UU[:,:n]*DD[:n] 
else:
    y = np.random.normal(0.0,1.0,[N,n])
one = np.ones([N,n])
d = cdist(y,y) + np.eye(N)
dinv = 1. / d # Returns inf where d = 0. 
dinv[np.isinf(dinv)] = 0 # Fix by replacing inf with 0 (default Matlab behaviour).
delta = D-d 
E = ((delta**2)*Dinv).sum() 

# Get on with it
for i in range(maxiter):

    # Compute gradient, Hessian and search direction (note it is actually
    # 1/4 of the gradient and Hessian, but the step size is just the ratio
    # of the gradient and the diagonal of the Hessian so it doesn't
    # matter).
    delta = dinv - Dinv
    deltaone = np.dot(delta,one)
    g = np.dot(delta,y) - (y * deltaone)
    dinv3 = dinv ** 3
    y2 = y ** 2
    H = np.dot(dinv3,y2) - deltaone - np.dot(2,y) * np.dot(dinv3,y) + y2 * np.dot(dinv3,one)       
    s = -g.flatten(order='F') #/ np.abs(H.flatten(order='F'))
    y_old    = y

    # Use step-halving procedure to ensure progress is made
    for j in range(maxhalves):
        s_reshape = s.reshape(2,len(s)/2).T
        y = y_old + s_reshape
        d = cdist(y, y) + np.eye(N)
        dinv = 1 / d # Returns inf where D = 0. 
        dinv[np.isinf(dinv)] = 0 # Fix by replacing inf with 0 (default Matlab behaviour).
        delta = D - d
        E_new = ((delta**2)*Dinv).sum()
        if E_new < E:
            break
        else:
            s = np.dot(0.5,s)

    # Bomb out if too many halving steps are required
    if j == maxhalves:
        print('Warning: maxhalves exceeded. Sammon mapping may not converge...')

    # Evaluate termination criterion
    if np.abs((E - E_new) / E) < tolfun:
        if display:
            print('TolFun exceeded: Optimisation terminated')
        break

    # Report progress
    E = E_new
    if display > 1:
        print('epoch = ' + str(i) + ': E = ' + str(E * scale))

# Fiddle stress to match the original Sammon paper
E = E * scale

return [y,E]

`

codeprof avatar May 26 '17 00:05 codeprof

Unfortunately this is common with Quasi-Newton. Since the Hessian is not a true Hessian but an approximation, there is a tendency to bounce around a local or global minimum. Sammon NLM only works well if you have a good initial approximation (as from PCA or boiler MDS schemes). Try keeping track of a few iterations of E and stop if you are seeing them again and again.

BenWutzke avatar Aug 14 '17 22:08 BenWutzke