I implemented the algorithm from the Wikipedia page. I hope it can be of help. I decided to write it down as a sort of guide, but read on Wikipedia for the details and for ways to further tune it.
A SOM has two types of data, weights and units. The weights are data points, we use the following:
img = Import["https://i.sstatic.net/eoAEt.png"] // Binarize;
weights = Join[
N@Rescale@PixelValuePositions[img, 1],
RandomReal[1, {500, 2}]
];
ListPlot[weights]
The units are objects that have a weight and a position in a grid. The weight is in the same space as the weights previously defined, the grid describes the spatial relationships between the units. We can initialize units by picking weights randomly from the general area where the data points are:
units = N@MapThread[unit, {Subdivide[39], RandomReal[{0.2, 0.8}, {40, 2}]}];
We proceed now to update the units by feeding them weights from the dataset one by one. One update consists of:
- Identifying the unit whose weight is closest to the input weight. This the so-called best matching unit, the BMU.
- Identifying units that are within a pre-defined range of the BMU on the grid (not in the weight space), the so-called activated units.
- Applying the update formula.
We start by defining the following helper functions:
weightDistance[unit[_, w_], weight_] := EuclideanDistance[w, weight]
findBMU[units_, weight_] := First@MinimalBy[units, weightDistance[#, weight] &]
gridDistance[unit[pos1_, _], unit[pos2_, _]] := EuclideanDistance[pos1, pos2]
findActivatedUnits[units_, bmu_, range_] := Transpose@Nearest[
units -> {"Index", "Element"}, bmu, {All, range},
DistanceFunction -> gridDistance
]
update[unit[pos_, w_], weight_, eta_] := unit[pos, w + eta (weight - w)]
getWeights[units_] := Last /@ units
The following is the iteration with eta
the initial learning rate, nIterations
the number of iterations, neighborhoodRadius
the range that decides how close units must be to the BMU to be affected, and nUnits
the number of units:
eta = 1;
neighborhoodRadius = 0.1;
nIterations = 20000;
nUnits = 50;
units = N@MapThread[unit, {Subdivide[nUnits - 1], RandomReal[{0.45, 0.55}, {nUnits, 2}]}];
Do[
weight = RandomChoice[weights];
bmu = findBMU[units, weight];
{indices, activatedUnits} = findActivatedUnits[units, bmu, neighborhoodRadius];
units = MapAt[update[#, weight, eta - s eta/nIterations] &, units, List /@ indices];
,
{s, nIterations}
];
This produces the following output, where the dark yellow/orange line represents the SOM units:
ListPlot[{
weights,
getWeights[units]
}, Joined -> {False, True}]
This looks pretty good, but note that I had to play with the parameters to get it to work. I needed a lot of iterations, a sufficient number of units, and the neighborhood radius needed to be small enough to make the updates localized. If the parameters are off it will probably still return a solution that makes sense in that it has spread out to cover the points pretty well, but it won't have found the optimal solution, which is the S shape.