View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.math4.examples.sofm.tsp;
19  
20  import java.util.List;
21  import java.util.ArrayList;
22  import java.util.Set;
23  import java.util.Collection;
24  import java.util.Iterator;
25  import java.util.NoSuchElementException;
26  import java.util.function.DoubleUnaryOperator;
27  import java.util.concurrent.Future;
28  import java.util.concurrent.Executors;
29  import java.util.concurrent.ExecutorService;
30  import java.util.concurrent.ExecutionException;
31  
32  import org.apache.commons.rng.UniformRandomProvider;
33  import org.apache.commons.rng.sampling.CollectionSampler;
34  import org.apache.commons.rng.sampling.distribution.ContinuousUniformSampler;
35  
36  import org.apache.commons.math4.neuralnet.DistanceMeasure;
37  import org.apache.commons.math4.neuralnet.EuclideanDistance;
38  import org.apache.commons.math4.neuralnet.FeatureInitializer;
39  import org.apache.commons.math4.neuralnet.FeatureInitializerFactory;
40  import org.apache.commons.math4.neuralnet.Network;
41  import org.apache.commons.math4.neuralnet.Neuron;
42  import org.apache.commons.math4.neuralnet.oned.NeuronString;
43  import org.apache.commons.math4.neuralnet.sofm.KohonenUpdateAction;
44  import org.apache.commons.math4.neuralnet.sofm.KohonenTrainingTask;
45  import org.apache.commons.math4.neuralnet.sofm.NeighbourhoodSizeFunctionFactory;
46  import org.apache.commons.math4.neuralnet.sofm.NeighbourhoodSizeFunction;
47  import org.apache.commons.math4.neuralnet.sofm.LearningFactorFunctionFactory;
48  import org.apache.commons.math4.neuralnet.sofm.LearningFactorFunction;
49  
50  /**
51   * Handles the <a href="https://en.wikipedia.org/wiki/Travelling_salesman_problem">
52   * "Travelling Salesman's Problem"</a> (i.e. trying to find the sequence of
53   * cities that minimizes the travel distance) using a 1D SOFM.
54   */
55  public final class TravellingSalesmanSolver {
56      /** The ID for the first neuron. */
57      private static final long FIRST_NEURON_ID = 0;
58      /** SOFM. */
59      private final Network net;
60      /** Distance function. */
61      private final DistanceMeasure distance = new EuclideanDistance();
62      /** Total number of neurons. */
63      private final int numberOfNeurons;
64  
65      /**
66       * @param numNeurons Number of neurons.
67       * @param init Neuron intializers.
68       */
69      private TravellingSalesmanSolver(int numNeurons,
70                                       FeatureInitializer[] init) {
71          // Total number of neurons.
72          numberOfNeurons = numNeurons;
73  
74          // Create a network with circle topology.
75          net = new NeuronString(numberOfNeurons, true, init).getNetwork();
76      }
77  
78      /**
79       * @param cities List of cities to be visited.
80       * @param neuronsPerCity Average number of neurons per city.
81       * @param numUpdates Number of updates for training the network.
82       * @param numTasks Number of concurrent tasks.
83       * @param random RNG for presenting samples to the trainer.
84       * @return the solution (list of cities in travel order).
85       */
86      public static City[] solve(City[] cities,
87                                 double neuronsPerCity,
88                                 long numUpdates,
89                                 int numTasks,
90                                 UniformRandomProvider random) {
91          if (cities.length <= 2) {
92              return cities;
93          }
94  
95          // Make sure that each city will appear only once in the list.
96          final Set<City> uniqueCities = City.unique(cities);
97  
98          final int numNeurons = (int) (neuronsPerCity * uniqueCities.size());
99          if (numNeurons < uniqueCities.size()) {
100             throw new IllegalArgumentException("Too few neurons");
101         }
102 
103         // Set up network.
104         final FeatureInitializer[] init = makeInitializers(numNeurons,
105                                                            uniqueCities,
106                                                            random);
107         final TravellingSalesmanSolver solver = new TravellingSalesmanSolver(numNeurons,
108                                                                              init);
109 
110         // Parallel execution.
111         final ExecutorService service = Executors.newCachedThreadPool();
112         final Runnable[] tasks = solver.createTasks(uniqueCities,
113                                                     random,
114                                                     numTasks,
115                                                     numUpdates / numTasks);
116         final List<Future<?>> execOutput = new ArrayList<>();
117         // Run tasks.
118         for (final Runnable r : tasks) {
119             execOutput.add(service.submit(r));
120         }
121         // Wait for completion (ignoring return value).
122         try {
123             for (final Future<?> f : execOutput) {
124                 f.get();
125             }
126         } catch (InterruptedException | ExecutionException e) {
127             if (e instanceof InterruptedException) {
128                 // Restore interrupted state...
129                 Thread.currentThread().interrupt();
130             }
131             throw new RuntimeException(e);
132         }
133         // Terminate all threads.
134         service.shutdown();
135 
136         return solver.getCityList(uniqueCities).toArray(new City[0]);
137     }
138 
139     /**
140      * Creates training tasks.
141      *
142      * @param cities List of cities to be visited.
143      * @param random RNG for presenting samples to the trainer.
144      * @param numTasks Number of tasks to create.
145      * @param numSamplesPerTask Number of training samples per task.
146      * @return the created tasks.
147      */
148     private Runnable[] createTasks(Set<City> cities,
149                                    UniformRandomProvider random,
150                                    int numTasks,
151                                    long numSamplesPerTask) {
152         final Runnable[] tasks = new Runnable[numTasks];
153         final LearningFactorFunction learning
154             = LearningFactorFunctionFactory.exponentialDecay(0.9,
155                                                              0.05,
156                                                              numSamplesPerTask / 2);
157         final NeighbourhoodSizeFunction neighbourhood
158             = NeighbourhoodSizeFunctionFactory.exponentialDecay(numberOfNeurons,
159                                                                 1,
160                                                                 numSamplesPerTask / 2);
161 
162         for (int i = 0; i < numTasks; i++) {
163             final KohonenUpdateAction action = new KohonenUpdateAction(distance,
164                                                                        learning,
165                                                                        neighbourhood);
166             tasks[i] = new KohonenTrainingTask(net,
167                                                createIterator(numSamplesPerTask,
168                                                               cities,
169                                                               random),
170                                                action);
171         }
172 
173         return tasks;
174     }
175 
176     /**
177      * Creates an iterator that will present a series of city's coordinates
178      * in random order.
179      *
180      * @param numSamples Number of samples.
181      * @param uniqueCities Cities.
182      * @param random RNG.
183      * @return the iterator.
184      */
185     private static Iterator<double[]> createIterator(final long numSamples,
186                                                      final Set<City> uniqueCities,
187                                                      final UniformRandomProvider random) {
188         final CollectionSampler<City> sampler = new CollectionSampler<>(random, uniqueCities);
189 
190         return new Iterator<double[]>() {
191             /** Number of samples. */
192             private long n;
193             /** {@inheritDoc} */
194             @Override
195             public boolean hasNext() {
196                 return n < numSamples;
197             }
198             /** {@inheritDoc} */
199             @Override
200             public double[] next() {
201                 if (!hasNext()) {
202                     throw new NoSuchElementException();
203                 }
204                 ++n;
205                 return sampler.sample().getCoordinates();
206             }
207             /** {@inheritDoc} */
208             @Override
209             public void remove() {
210                 throw new UnsupportedOperationException();
211             }
212         };
213     }
214 
215     /**
216      * @return the list of linked neurons (i.e. the one-dimensional SOFM).
217      */
218     private List<Neuron> getNeuronList() {
219         // Sequence of coordinates.
220         final List<Neuron> list = new ArrayList<>();
221 
222         // First neuron.
223         Neuron current = net.getNeuron(FIRST_NEURON_ID);
224         while (true) {
225             list.add(current);
226             final Collection<Neuron> neighbours
227                 = net.getNeighbours(current, list);
228 
229             final Iterator<Neuron> iter = neighbours.iterator();
230             if (!iter.hasNext()) {
231                 // All neurons have been visited.
232                 break;
233             }
234 
235             current = iter.next();
236         }
237 
238         return list;
239     }
240 
241     /**
242      * @return the list of features (coordinates) of linked neurons.
243      */
244     public List<double[]> getCoordinatesList() {
245         // Sequence of coordinates.
246         final List<double[]> coordinatesList = new ArrayList<>();
247 
248         for (final Neuron n : getNeuronList()) {
249             coordinatesList.add(n.getFeatures());
250         }
251 
252         return coordinatesList;
253     }
254 
255     /**
256      * Returns the travel proposed by the solver.
257      *
258      * @param cities Cities
259      * @return the list of cities in travel order.
260      */
261     private List<City> getCityList(Set<City> cities) {
262         final List<double[]> coord = getCoordinatesList();
263         final List<City> cityList = new ArrayList<>();
264         City previous = null;
265         final int max = coord.size();
266         for (int i = 0; i < max; i++) {
267             final double[] c = coord.get(i);
268             final City next = City.closest(c[0], c[1], cities);
269             if (!next.equals(previous)) {
270                 cityList.add(next);
271                 previous = next;
272             }
273         }
274         return cityList;
275     }
276 
277     /**
278      * Creates the features' initializers: an approximate circle around the
279      * barycentre of the cities.
280      *
281      * @param numNeurons Number of neurons.
282      * @param uniqueCities Cities.
283      * @param random RNG.
284      * @return an array containing the two initializers.
285      */
286     private static FeatureInitializer[] makeInitializers(final int numNeurons,
287                                                          final Set<City> uniqueCities,
288                                                          final UniformRandomProvider random) {
289         // Barycentre.
290         final double[] centre = City.barycentre(uniqueCities);
291         // Largest distance from centre.
292         final double radius = 0.5 * City.largestDistance(centre[0], centre[1], uniqueCities);
293 
294         final double omega = 2 * Math.PI / numNeurons;
295         final DoubleUnaryOperator h1 = new HarmonicOscillator(radius, omega, 0, centre[0]);
296         final DoubleUnaryOperator h2 = new HarmonicOscillator(radius, omega, 0.5 * Math.PI, centre[1]);
297 
298         final double r = 0.05 * radius;
299         final ContinuousUniformSampler u = new ContinuousUniformSampler(random, -r, r);
300 
301         return new FeatureInitializer[] {
302             FeatureInitializerFactory.randomize(u, FeatureInitializerFactory.function(h1, 0, 1)),
303             FeatureInitializerFactory.randomize(u, FeatureInitializerFactory.function(h2, 0, 1))
304         };
305     }
306 }
307 
308 /**
309  * Function.
310  */
311 class HarmonicOscillator implements DoubleUnaryOperator {
312     /** Amplitude. */
313     private final double amplitude;
314     /** Angular speed. */
315     private final double omega;
316     /** Phase. */
317     private final double phase;
318     /** Offset. */
319     private final double offset;
320 
321     /**
322      * @param amplitude Amplitude.
323      * @param omega Angular speed.
324      * @param phase Phase.
325      * @param offset Offset (ordinate).
326      */
327     HarmonicOscillator(double amplitude,
328                        double omega,
329                        double phase,
330                        double offset) {
331         this.amplitude = amplitude;
332         this.omega = omega;
333         this.phase = phase;
334         this.offset = offset;
335     }
336 
337     @Override
338     public double applyAsDouble(double x) {
339         return offset + amplitude * Math.cos(omega * x + phase);
340     }
341 }