web-dev-qa-db-fra.com

Modèle de codage pour le pourcentage de branchement aléatoire?

Alors disons que nous avons un bloc de code que nous voulons exécuter 70% de fois et un autre 30% de fois.

if(Math.random() < 0.7)
    70percentmethod();
else
    30percentmethod();

Assez simple. Mais que faire si nous voulons qu’il soit facilement extensible, par exemple 30%/60%/10%, etc.? Ici, il faudrait ajouter et changer toutes les déclarations if si un changement n’est pas vraiment génial à utiliser, lent et induisant en erreur.

Jusqu’à présent, j’ai trouvé que les grands commutateurs étaient très utiles pour ce cas d’utilisation, par exemple:

switch(Rand(0, 10)){
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:70percentmethod();break;
    case 8:
    case 9:
    case 10:30percentmethod();break;
}

Ce qui peut être très facilement changé en:

switch(Rand(0, 10)){
    case 0:10percentmethod();break;
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:60percentmethod();break;
    case 8:
    case 9:
    case 10:30percentmethod();break;
}

Mais ceux-ci ont aussi leurs inconvénients, étant encombrants et divisés en un nombre prédéterminé de divisions.

Quelque chose d'idéal serait basé sur un système de "nombre de fréquences", je suppose, comme ceci:

(1,a),(1,b),(2,c) -> 25% a, 25% b, 50% c

alors si vous en avez ajouté un autre:

(1,a),(1,b),(2,c),(6,d) -> 10% a, 10% b, 20% c, 60% d

Donc, simplement en additionnant les nombres, en faisant la somme égale à 100% et ensuite diviser cela.

Je suppose que ce ne serait pas si difficile de créer un gestionnaire avec un hashmap personnalisé ou quelque chose du genre, mais je me demande s’il existe une méthode/un modèle établi ou un lambda pour cela avant que je ne fasse des spaghettis.

51
Moff Kalast

EDIT: Voir edit à la fin pour une solution plus élégante. Je vais laisser cela cependant.

Vous pouvez utiliser une NavigableMap pour stocker ces méthodes mappées sur leurs pourcentages.

NavigableMap<Double, Runnable> runnables = new TreeMap<>();

runnables.put(0.3, this::30PercentMethod);
runnables.put(1.0, this::70PercentMethod);

public static void runRandomly(Map<Double, Runnable> runnables) {
    double percentage = Math.random();
    for (Map.Entry<Double, Runnable> entry : runnables){
        if (entry.getKey() < percentage) {
            entry.getValue().run();
            return; // make sure you only call one method
        }
    }
    throw new RuntimeException("map not filled properly for " + percentage);
}

// or, because I'm still practicing streams by using them for everything
public static void runRandomly(Map<Double, Runnable> runnables) {
    double percentage = Math.random();
    runnables.entrySet().stream()
        .filter(e -> e.getKey() < percentage)
        .findFirst().orElseThrow(() -> 
                new RuntimeException("map not filled properly for " + percentage))
        .run();
}

La NavigableMap est triée (par exemple, HashMap ne donne aucune garantie des entrées) par clé, vous obtenez ainsi les entrées ordonnées selon leur pourcentage. Ceci est pertinent car si vous avez deux éléments (3, r1), (7, r2), ils génèrent les entrées suivantes: r1 = 0.3 et r2 = 1.0 et ils doivent être évalués dans cet ordre (par exemple s'ils sont évalués dans l'ordre inverse, le résultat toujours sera r2).

Pour ce qui est de la scission, cela devrait ressembler à ceci: Avec une classe Tuple comme celle-ci

static class Pair<X, Y>
{
    public Pair(X f, Y s)
    {
        first = f;
        second = s;
    }

    public final X first;
    public final Y second;
}

Vous pouvez créer une carte comme celle-ci

// the parameter contains the (1,m1), (1,m2), (3,m3) pairs
private static Map<Double,Runnable> splitToPercentageMap(Collection<Pair<Integer,Runnable>> runnables)
{

    // this adds all Runnables to lists of same int value,
    // overall those lists are sorted by that int (so least probable first)
    double total = 0;
    Map<Integer,List<Runnable>> byNumber = new TreeMap<>();
    for (Pair<Integer,Runnable> e : runnables)
    {
        total += e.first;
        List<Runnable> list = byNumber.getOrDefault(e.first, new ArrayList<>());
        list.add(e.second);
        byNumber.put(e.first, list);
    }

    Map<Double,Runnable> targetList = new TreeMap<>();
    double current = 0;
    for (Map.Entry<Integer,List<Runnable>> e : byNumber.entrySet())
    {
        for (Runnable r : e.getValue())
        {
            double percentage = (double) e.getKey() / total;
            current += percentage;
            targetList.put(current, r);
        }
    }

    return targetList;
}

Et tout cela ajouté à une classe

class RandomRunner {
    private List<Integer, Runnable> runnables = new ArrayList<>();
    public void add(int value, Runnable toRun) {
        runnables.add(new Pair<>(value, toRun));
    }
    public void remove(Runnable toRemove) {
        for (Iterator<Pair<Integer, Runnable>> r = runnables.iterator();
            r.hasNext(); ) {
            if (toRemove == r.next().second) {
               r.remove();
               break;
            }
        }
    }
    public void runRandomly() {
        // split list, use code from above
    }
}

MODIFIER :
En fait, ce qui précède correspond à ce que vous obtenez si vous avez une idée en tête et ne la remettez pas en question correctement . En gardant l'interface de classe RandomRunner, c'est beaucoup plus facile:

class RandomRunner {
    List<Runnable> runnables = new ArrayList<>();
    public void add(int value, Runnable toRun) {
        // add the methods as often as their weight indicates.
        // this should be fine for smaller numbers;
        // if you get lists with millions of entries, optimize
        for (int i = 0; i < value; i++) {
            runnables.add(toRun);
        }
    }
    public void remove(Runnable r) {
        Iterator<Runnable> myRunnables = runnables.iterator();
        while (myRunnables.hasNext()) {
            if (myRunnables.next() == r) {
                myRunnables.remove();
            }
    }
    public void runRandomly() {
        if (runnables.isEmpty()) return;
        // roll n-sided die
        int runIndex = ThreadLocalRandom.current().nextInt(0, runnables.size());
        runnables.get(runIndex).run();
    }
}
27
daniu

Toutes ces réponses semblent assez compliquées, je vais donc poster l’alternative Keep-it-Simple:

double rnd = Math.random()
if((rnd -= 0.6) < 0)
    60percentmethod();
else if ((rnd -= 0.3) < 0)
    30percentmethod();
else
    10percentmethod();

Il n'est pas nécessaire de changer d'autres lignes et on peut facilement voir ce qui se passe sans creuser dans les classes auxiliaires. Un petit inconvénient est qu'il n'impose pas que les pourcentages totalisent 100%.

25
jpa

Je ne sais pas s'il y a un nom commun à cela, mais je pense l'avoir appris comme la roue de la fortune à l'université.

Cela fonctionne fondamentalement comme vous le décrivez: il reçoit une liste de valeurs et de "nombres de fréquences" et l’un est choisi en fonction des probabilités pondérées.

list = (1,a),(1,b),(2,c),(6,d)

total = list.sum()
rnd = random(0, total)
sum = 0
for i from 0 to list.size():
    sum += list[i]
    if sum >= rnd:
        return list[i]
return list.last()

La liste peut être un paramètre de fonction si vous souhaitez généraliser cela.

Cela fonctionne également avec les nombres à virgule flottante et il n'est pas nécessaire que les nombres soient normalisés. Si vous normalisez (pour résumer à 1 par exemple), vous pouvez ignorer la partie list.sum().

MODIFIER:

En raison de la demande, voici une implémentation actuelle de la compilation Java et un exemple d'utilisation:

import Java.util.ArrayList;
import Java.util.Random;

public class RandomWheel<T>
{
  private static final class RandomWheelSection<T>
  {
    public double weight;
    public T value;

    public RandomWheelSection(double weight, T value)
    {
      this.weight = weight;
      this.value = value;
    }
  }

  private ArrayList<RandomWheelSection<T>> sections = new ArrayList<>();
  private double totalWeight = 0;
  private Random random = new Random();

  public void addWheelSection(double weight, T value)
  {
    sections.add(new RandomWheelSection<T>(weight, value));
    totalWeight += weight;
  }

  public T draw()
  {
    double rnd = totalWeight * random.nextDouble();

    double sum = 0;
    for (int i = 0; i < sections.size(); i++)
    {
      sum += sections.get(i).weight;
      if (sum >= rnd)
        return sections.get(i).value;
    }
    return sections.get(sections.size() - 1).value;
  }

  public static void main(String[] args)
  {
    RandomWheel<String> wheel = new RandomWheel<String>();
    wheel.addWheelSection(1, "a");
    wheel.addWheelSection(1, "b");
    wheel.addWheelSection(2, "c");
    wheel.addWheelSection(6, "d");

    for (int i = 0; i < 100; i++)
        System.out.print(wheel.draw());
  }
}
15
SteakOverflow

Bien que la réponse sélectionnée fonctionne, elle est malheureusement asymptotiquement lente pour votre cas d'utilisation. Au lieu de cela, vous pouvez utiliser quelque chose appelé Alias ​​Sampling . L'échantillonnage d'alias (ou méthode d'alias) est une technique utilisée pour la sélection d'éléments avec une distribution pondérée. Si le poids de choisir ces éléments ne change pas, vous pouvez faire la sélection dans le temps O(1)! Si ce n'est pas le cas, vous pouvez toujours obtenir amorti O(1) temps si le rapport entre le nombre de sélections que vous effectuez et les modifications apportées au tableau d'alias ( changer les poids) est élevé. La réponse actuellement sélectionnée suggère un algorithme O(N), la meilleure chose à faire étant O(log(N)) étant donné les probabilités triées et la recherche binaire , mais rien ne va battre le O(1) temps que j'ai suggéré. 

Ce site fournit un bon aperçu de la méthode Alias, qui est essentiellement agnostique. Vous créez essentiellement un tableau dans lequel chaque entrée représente le résultat de deux probabilités. Il y a un seuil unique pour chaque entrée de la table, en dessous du seuil auquel vous obtenez une valeur, au-dessus d'une autre valeur. Vous répartissez des probabilités plus grandes sur plusieurs valeurs de table afin de créer un graphique de probabilité avec une zone de une pour toutes les probabilités combinées. 

Supposons que vous avez les probabilités A, B, C et D, qui ont respectivement les valeurs 0,1, 0,1, 0,1 et 0,7. La méthode Alias ​​étendrait la probabilité de 0,7 à tous les autres. Un indice correspondrait à chaque probabilité, où vous auriez 0,1 et 0,15 pour ABC et 0,25 pour l'indice D's. Avec cela, vous normalisez chaque probabilité de manière à obtenir 0,4 chance d'obtenir A et 0,6 chance d'obtenir D dans l'indice de A (0,1/(0,1 + 0,15) et 0,15/(0,1 + 0,15) respectivement), ainsi que B et C indice, et 100% de chances d’obtenir D dans l’indice D (0,25/0,25 est 1). 

Avec un uniforme PRNG non biaisé (Math.Random ()) pour l'indexation, vous obtenez une probabilité égale de choisir chaque index, mais vous effectuez également un retournement de pièce par index qui fournit la probabilité pondérée. Vous avez 25% de chance d’atteindre l’emplacement A ou D, mais dans ce cas, vous n’avez que 40% de chance de choisir A et 60% de D. 0,40 * 0,25 = 0,1, notre probabilité initiale, et si vous: additionnez toutes les probabilités de D parsemant les autres indices, vous obtiendriez à nouveau 0,70.

Donc, pour faire une sélection aléatoire, il vous suffit de générer un index aléatoire de 0 à N, puis de lancer une pièce de monnaie, quel que soit le nombre d'éléments que vous ajoutez, il s'agit de very fast et d'un coût constant. Créer une table d'alias ne prend pas non plus autant de lignes de code, ma version python prend 80 lignes, y compris les instructions d'importation et les sauts de ligne, et la version présentée dans l'article Pandas est de la même taille (et en C++). 

Pour votre implémentation Java, vous pouvez associer des probabilités et des index de liste de tableaux à vos fonctions, créant ainsi un tableau de fonctions exécutés au fur et à mesure que vous les indexez. Vous pouvez également utiliser des objets fonction ( foncteurs ) qui ont une méthode que vous utilisez pour passer des paramètres à exécuter. 

ArrayList<(YourFunctionObject)> function_list;
// add functions
AliasSampler aliassampler = new AliasSampler(listOfProbabilities);
// somewhere later with some type T and some parameter values. 
int index = aliassampler.sampleIndex();
T result = function_list[index].apply(parameters);

MODIFIER:

J'ai créé une version en Java de la méthode AliasSampler, à l'aide de classes. Celle-ci utilise la méthode d'index exemple et devrait pouvoir être utilisée comme ci-dessus. 

import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.Random;

public class AliasSampler {
    private ArrayList<Double> binaryProbabilityArray;
    private ArrayList<Integer> aliasIndexList;
    AliasSampler(ArrayList<Double> probabilities){
        // Java 8 needed here
        assert(DoubleStream.of(probabilities).sum() == 1.0);
        int n = probabilities.size();
        // probabilityArray is the list of probabilities, this is the incoming probabilities scaled
        // by the number of probabilities.  This allows us to figure out which probabilities need to be spread 
        // to others since they are too large, ie [0.1 0.1 0.1 0.7] = [0.4 0.4 0.4 2.80]
        ArrayList<Double> probabilityArray;
        for(Double probability : probabilities){
            probabilityArray.add(probability);
        }
        binaryProbabilityArray = new ArrayList<Double>(Collections.nCopies(n, 0.0));
        aliasIndexList = new ArrayList<Integer>(Collections.nCopies(n, 0));
        ArrayList<Integer> lessThanOneIndexList = new ArrayList<Integer>();
        ArrayList<Integer> greaterThanOneIndexList = new ArrayList<Integer>();
        for(int index = 0; index < probabilityArray.size(); index++){
            double probability = probabilityArray.get(index);
            if(probability < 1.0){
                lessThanOneIndexList.add(index);
            }
            else{
                greaterThanOneIndexList.add(index);
            }
        }

        // while we still have indices to check for in each list, we attempt to spread the probability of those larger
        // what this ends up doing in our first example is taking greater than one elements (2.80) and removing 0.6, 
        // and spreading it to different indices, so (((2.80 - 0.6) - 0.6) - 0.6) will equal 1.0, and the rest will
        // be 0.4 + 0.6 = 1.0 as well. 
        while(lessThanOneIndexList.size() != 0 && greaterThanOneIndexList.size() != 0){
            //https://stackoverflow.com/questions/16987727/removing-last-object-of-arraylist-in-Java
            // last element removal is equivalent to pop, Java does this in constant time
            int lessThanOneIndex = lessThanOneIndexList.remove(lessThanOneIndexList.size() - 1);
            int greaterThanOneIndex = greaterThanOneIndexList.remove(greaterThanOneIndexList.size() - 1);
            double probabilityLessThanOne = probabilityArray.get(lessThanOneIndex);
            binaryProbabilityArray.set(lessThanOneIndex, probabilityLessThanOne);
            aliasIndexList.set(lessThanOneIndex, greaterThanOneIndex);
            probabilityArray.set(greaterThanOneIndex, probabilityArray.get(greaterThanOneIndex) + probabilityLessThanOne - 1);
            if(probabilityArray.get(greaterThanOneIndex) < 1){
                lessThanOneIndexList.add(greaterThanOneIndex);
            }
            else{
                greaterThanOneIndexList.add(greaterThanOneIndex);
            }
        }
        //if there are any probabilities left in either index list, they can't be spread across the other 
        //indicies, so they are set with probability 1.0. They still have the probabilities they should at this step, it works out mathematically.
        while(greaterThanOneIndexList.size() != 0){
            int greaterThanOneIndex = greaterThanOneIndexList.remove(greaterThanOneIndexList.size() - 1);
            binaryProbabilityArray.set(greaterThanOneIndex, 1.0);
        }
        while(lessThanOneIndexList.size() != 0){
            int lessThanOneIndex = lessThanOneIndexList.remove(lessThanOneIndexList.size() - 1);
            binaryProbabilityArray.set(lessThanOneIndex, 1.0);
        }
    }
    public int sampleIndex(){
        int index = new Random().nextInt(binaryProbabilityArray.size());
        double r = Math.random();
        if( r < binaryProbabilityArray.get(index)){
            return index;
        }
        else{
            return aliasIndexList.get(index);
        }
    }

}
8
opa

Vous pouvez calculer la probabilité cumulée pour chaque classe et choisir un nombre aléatoire parmi [0; 1) et voir où ce nombre tombe.

class WeightedRandomPicker {

    private static Random random = new Random();

    public static int choose(double[] probabilties) {
        double randomVal = random.nextDouble();
        double cumulativeProbability = 0;
        for (int i = 0; i < probabilties.length; ++i) {
            cumulativeProbability += probabilties[i];
            if (randomVal < cumulativeProbability) {
                return i;
            }
        }
        return probabilties.length - 1; // to account for numerical errors
    }

    public static void main (String[] args) {
        double[] probabilties = new double[]{0.1, 0.1, 0.2, 0.6}; // the final value is optional
        for (int i = 0; i < 20; ++i) {
            System.out.printf("%d\n", choose(probabilties));
        }
    }
}
6
NPE

Ce qui suit est un peu comme @daniu answer mais utilise les méthodes fournies par TreeMap:

private final NavigableMap<Double, Runnable> map = new TreeMap<>();
{
    map.put(0.3d, this::branch30Percent);
    map.put(1.0d, this::branch70Percent);
}
private final SecureRandom random = new SecureRandom();

private void branch30Percent() {}

private void branch70Percent() {}

public void runRandomly() {
    final Runnable value = map.tailMap(random.nextDouble(), true).firstEntry().getValue();
    value.run();
}

De cette façon, il n’est pas nécessaire d’itérer l’ensemble de la carte tant que l’entrée correspondante n’est pas trouvée, mais les capacités de TreeSet pour rechercher une entrée avec une clé spécifiquement comparée à une autre clé sont utilisées. Cela ne fera toutefois la différence que si le nombre d'entrées dans la carte est élevé. Cependant, il enregistre quelques lignes de code.

1
SpaceTrucker

Je ferais quelque chose comme ça:

class RandomMethod {
    private final Runnable method;
    private final int probability;

    RandomMethod(Runnable method, int probability){
        this.method = method;
        this.probability = probability;
    }

    public int getProbability() { return probability; }
    public void run()      { method.run(); }
}

class MethodChooser {
    private final List<RandomMethod> methods;
    private final int total;

    MethodChooser(final List<RandomMethod> methods) {
        this.methods = methods;
        this.total = methods.stream().collect(
            Collectors.summingInt(RandomMethod::getProbability)
        );
    }

    public void chooseMethod() {
        final Random random = new Random();
        final int choice = random.nextInt(total);

        int count = 0;
        for (final RandomMethod method : methods)
        {
            count += method.getProbability();
            if (choice < count) {
                method.run();
                return;
            }
        }
    }
}

Exemple d'utilisation:

MethodChooser chooser = new MethodChooser(Arrays.asList(
    new RandomMethod(Blah::aaa, 1),
    new RandomMethod(Blah::bbb, 3),
    new RandomMethod(Blah::ccc, 1)
));

IntStream.range(0, 100).forEach(
    i -> chooser.chooseMethod()
);

Lancez le ici .

0
Michael