
A New Algorithm for Controlled Randomness - ingve
https://probablydance.com/2019/08/28/a-new-algorithm-for-controlled-randomness/
======
dahart
> Together we came up with a solution that solves this with O(n) memory usage
> where n is the number of choices, and O(log n) CPU cost. And it allows you
> to decide if you want to be more random or more deterministic.

I think you can do the same thing and solve all the same problems in O(1) time
per sample using the alias method plus shuffling while blending between
regular uniform samples and jittered uniform samples.

\- Build your alias map of M buckets, the distribution you want to sample

\- Take a set of N uniform samples, and use a knob to blend between regular
and jittered. It looks like: r=lerp(knob, 0.5, rand())/N

\- Shuffle the uniform samples, e.g. std::random_shuffle(). This can be done
incrementally, per sample
([https://en.wikipedia.org/wiki/Fisher–Yates_shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle)).

\- Plug the uniform sample into the alias map to find your sampled choice.

\- Repeat when you run out of the N uniform samples.

[https://en.wikipedia.org/wiki/Alias_method](https://en.wikipedia.org/wiki/Alias_method)

[http://www.keithschwarz.com/darts-dice-
coins/](http://www.keithschwarz.com/darts-dice-coins/)

~~~
Strilanc
For some reason alias sampling is not well known, even though it strictly
dominates using a search tree when generating randomness (it's faster, it uses
less memory, and IMO it's easier to code). It even works better when
generalized to the case of preparing superpositions on quantum computers (
[https://algassert.com/post/1805](https://algassert.com/post/1805) ).

~~~
ur-whale
Alias sampling is a really beautiful algorithm, but implementing it correctly
and efficiently isn't as easy as you seem to imply.

I would also very much like to know what a generalization of this algorithm to
continuous distribution would look like.

~~~
Strilanc
You make a list of below-average bars and a list of above-average bars.
Iteratively pop the top of both, donate from the larger to the smaller until
the smaller is average, then put the larger back into the appropriate list
based on its new value. Stop when there's no too-large bars left. Compared to
writing a red black tree it's trivial.

~~~
ur-whale
A correct implementation wrt probabilities being exact given a frequency table
(the "average" can be a very large rational number), as well as efficient when
constructing the data structure to be sampled from during sampling phase is
IMO not as trivial as you seem to imply.

I have asked people to implement this algorithm during engineering interviews
on a regular basis for the better part of five years, and I can promise you,
strictly none of the candidate who were subjected to it would describe their
experience as "I was asked a trivial question".

But then everyone has their own bar for the word "trivial". I know for example
that I have my own bar for the word "arrogance".

~~~
dahart
Oh wow, how do you prompt the interviewees? Do you tell them about the average
value threshold or the guarantee you only need 2N buckets? If not, I’d guess
this is probably _way_ too hard of an interview question, even though a decent
implementation really isn’t that difficult. Is this like a bonus difficult
question for someone who aces all your other questions?

The implementation isn’t hard once you’ve read the Vose paper or the Keith
Schwarz link I posted. There’s only one tricky part about handling rounding
that is pretty easy to fix.

I would never expect anyone who hasn’t heard of the idea to be able to
implement it on the spot without being more or less given a recipe for the
math and how to use it. If you ever find someone who can, hire them
immediately!

This is one of those problems that only feels somewhat easy in retrospect, but
almost nobody could derive any of it on their own. The alias method is
extremely clever, and not at all trivial.

~~~
ur-whale
> Oh wow, how do you prompt the interviewees?

Back when I was still doing this, I used to start the interview like this:

    
    
            - given: a non-uniform, finite, frequency table with actual observations of some real-world phenomenon (and if that's too generic, just tell the candidate you've thrown a dice a million time an recorded the outcomes).
            - given: a uniform pseudo random number generator
            - please design a pseudo random number generator that will, if sampled for long enough, produce the same distribution (non generic version: RNG that statistically behave like the dice).
    

Unfortunately, a non-negligible proportion of candidates fail to produce any
solution at all :(

Decent candidates quickly produce the standard: "build the CDF once and then
at sample time, draw a uniform number in the correct range and walk the CDF
until we're above that random number, return the bucket where that occurred"

I then ask for time and space complexity for the sampling phase.

Slightly better than average candidate notice that the CDF is naturally sorted
and can be binary searched.

I then ask those if they can improve what we have to a O(1) solution
(independent of the size of the histogram).

The top 20% candidates usually manage to put their intuition at work: from the
fact that O(1) almost always implies a simple table lookup, they come up with
the: "build a very large array, where the index of each bucket B of the
histogram is replicated histo[B] times and then sample uniformly from it".

We then discuss when this algorithm is/isn't practical.

The candidates that get to this point within ~15mn can then be gently walked
towards redesigning the alias method using some hints.

The starting hint goes like this: what if you were to completely ignore the
fact that the histogram is not uniform, and just stubbornly sample from it,
can you quantify how much of a mistake you would make ?

The second hint is: for some buckets, picking them will be an overshoot, and
for some we will undershoot. Explain when that is.

The third hint: say we picked a bucket that's chosen too often, can we decide
to only keep it some of the time (as in: with some carefully chosen
probability)

The fourth hint: and if we decide not keep it, why waste the dice throw, why
not pick another, carefully chosen bucket

etc ...

The nice property of this question is: it's very gradual, starting with a
problem most people with a CS degree should be able to solve, then going to a
solution that's less standard and requires more than base regurgitation of CS
lessons, to finally get to what I consider to be (at the risk of repeating
myself) the non-trivial state of the art solution to the problem.

The point where the candidate start to struggle gives you a nice reading on
their strength.

And there's the rare few new grads that would vaguely recall having seen the
alias method and could rebuild it cleanly on the white board from scratch and
without help. These rare occurrences were very enjoyable.

------
klipt
> I don’t know if this problem has a proper name, but in game development you
> usually don’t want truly random numbers. I’ve seen the solution named a
> “pseudo random distribution” but “pseudo random” already has a different
> meaning outside of game design, so I will just call it controlled randomness
> for this blog post.

The correct term is probably quasirandom or low-discrepancy sequences:
[https://en.wikipedia.org/wiki/Low-
discrepancy_sequence](https://en.wikipedia.org/wiki/Low-discrepancy_sequence)

~~~
dahart
While those are good terms for deterministic uniform random number generators,
that is not what the author’s talking about.

He’s referring to how in games, you sometimes don’t want random patterns. But
even when you do, you don’t want the pattern to be truly random, because true
randomness sometimes feels unfair and sometimes feels un-fun.

It’s a concept with a broader scope than low-discrepancy sequences.

~~~
klipt
They don't have to be deterministic, the wiki article has a section on
constructing low-discrepancy sequences from independent random numbers by
imposing negative correlation between subsequent numbers:
[https://en.wikipedia.org/wiki/Low-
discrepancy_sequence#Rando...](https://en.wikipedia.org/wiki/Low-
discrepancy_sequence#Random_numbers)

Which sounds pretty similar to what the author is doing - removing some of the
independence from a sequence of independent random numbers to make them feel
"fairer" (basically making them more in line with misconceptions people have
about truly independent random sequences
[https://en.wikipedia.org/wiki/Randomness#Misconceptions_and_...](https://en.wikipedia.org/wiki/Randomness#Misconceptions_and_logical_fallacies))

~~~
dahart
> They don’t have to be deterministic.

Right. Agreed. Still, the idea here is about the perception of randomness at
the expense of actual randomness, it’s related to but different from quasi-
random ideas.

Edit: Actually I don’t know, maybe you’re totally right about this article,
maybe quasi-random is the same thing in this case. I was projecting a broader
idea based on having worked on games and run into this problem... my
philosophy on it is that often you don’t want any randomness, but sequences
that are random seeming at first but are predictable and learnable by the
player. Depending on the game feature, that’s sometimes more fun.

In this particular case, they are trying to preserve the intended
distribution, and sample a given distribution without shuffling. A low
discrepancy sequence does do that, so I’m changing my mind here.

------
richard_todd
By the time you are tracking misses on each class of event and adjusting the
probabilities, you might as well just use random permutations of the desired
distribution of outcomes. Like if the sword is supposed to hit 90%, just run
through a 10-bit sequence with 9 ones and a zero, and shuffle it after every
10 attempts.

~~~
XCSme
Are you talking about randomly shuffling the bits or getting a new
permutation? Any suggestions on how to quickly randomly shuffle the bits of a
number?

~~~
richard_todd
Randomly shuffling the bits _is_ a new permutation (of those bits). There may
be a faster way to shuffle the bits of whole bytes/words on some
architectures, but for the general case I suspect a normal shuffling algorithm
(like Fisher-Yates) and shifts+and+xor swapping is best.

~~~
XCSme
Oh, sorry for not making it clear, when I meant permutation I was referring to
the the immediately next permutation. I guess after current set is over you
could also take the Xth permutation of the current state, where X is a random
integer. (so you randomly skip some permutations to not make it predictable).

~~~
richard_todd
Yeah jumping ahead by X permutations would be good as well.

------
unnouinceput
At one of my projects I had a similar problem.

The problem: My customer was renting items (to his customers) and wanted those
items to be given random, but not too random to avoid giving the same (free to
rent) item too many times because it will worn out too fast vs. rest of them.
Replacing that item in the overall pool of those items was more expensive than
replacing the entire pool (think doors on small lockers, and the entire panel
of lockers was cheaper to just get replaced altogether - don't ask me why,
modern society is weird)

The solution: Mark each item with a score that goes up each time the item gets
rented. And apply random to those that are at bottom. In the beginning all
items had zero as score, one gets rented and its score is now 1. I applied
random to only those with zero score until all of them were rented once. Of
course, renting times were variables so overall the algorithm that I've
implemented in the end was more complex, but this was the basic idea.

~~~
bmm6o
How is that better than just renting out the next one with the fewest rentals?
Maybe it's easier to pick at random than to track an order? But then the
randomness is for ease of implementation and not a fundamental part of your
solution.

~~~
TeMPOraL
I'd guess the scoring was not linear with number of rentals, because wear
isn't. E.g. 2 long rentals will generate more wear than 2 short rentals;
probably even more than 3, depending on how much wear is caused by shipping &
handling.

I wonder why not derive the score from human input? I assume someone is
handling returns; that someone could be trained to eyeball the condition of
the item and input that into a database.

~~~
unnouinceput
No human to oversee this, was the goal. The only human was cleaning lady that
was cleaning them at the closing time, and she definitely was not trained to
do anything else, hence everything algorithm driven. When something was broken
a technician was send to repair, or replace, but no person to stay onsite.

------
ribrars
> How do you arrive at that 5% base chance? I actually don’t know an analytic
> way of arriving there, but you can get there experimentally pretty easily by
> just writing a loop that runs through this a million times and then tells
> you how often it succeeded or failed. Then you build a lookup table that
> contains the percentages.

I'm no stats expert by any means, but couldn't you calculate the percentages
of each event occuring (and the amount to increment) using bayesian modeling?

Simply taking the product of a string of actions and calculating those seems
like an acceptable solution, however I'm not sure.

The author seems to use a calculation approach, which if I understand
correctly is a valid method and is sampling the distribution. But then again,
not sure.

~~~
fenomas
An easy way to do the odds analytically is to calculate the average expected
number of trials needed for a success, and then note that the overall average
success rate is the inverse of that.

E.g. in the author's example, where the rate starts at 5% and increases by 5%
with each failure, the expected number of trials needed for a success would
be:

    
    
        sum = 1 * 0.05 +                // hit the first trial
              2 * 0.95 * 0.10 +         // missed first, hit second
              3 * (0.95*0.9) * 0.15 +   // missed first two, hit third
              // ..and so on
    

Summing up that loop gives 5.29 for the expected number of trials, the inverse
of which is 18.9%, matching the author's observed result.

~~~
andreareina
The problem is the inverse: starting with 18.9%, how do you get the 5+5%

~~~
fenomas
The author doesn't examine that question. They comment that one can run a
large number of trials to estimate the 18.9 given the 5+5, and my post is how
to get that answer analytically for any sequence of probabilities.

For the reverse case, there are arbitrarily many such sequences that would
average out to any given value. In practice someone using this technique for
gamedev wouldn't necessarily only want X+X%.

------
bjornsing
AFAICT this is what’s called conditional probability in the scientific
literature. The algorithms described would probably be called autoregressive
samplers.

------
wruza
Oh, I remember good old fallout 1-2 when you have 42% hit chance but still
miss 7 times in a row.

There is also a good Numberphile video on what we think random really is (it
is not) [https://youtube.com/watch?v=tP-
Ipsat90c](https://youtube.com/watch?v=tP-Ipsat90c)

------
ryacko
It all comes down to generating deterministic permutations instead of purely
random numbers, since the intent is to avoid bad luck.

The most common permutation used is the AES S-Box, commonly available through
various ISA extensions. There are many ways to generate permutations though,
and it wouldn’t require a large complex algorithm to avoid a 1 in 10 chance
event happening three times in a row or generating the entire permutation to
be stored in memory.

~~~
HelloNurse
S-boxes compute fixed permutations of the bits in a small fixed length buffer,
which is entirely inappropriate for accurately picking random permutations of
a variable and usually large number of choices.

~~~
ryacko
For a RNG, the maximum run length of same values would be the period length
divided by the output size (in general).

The blog post is horrifying, it feeds the output of the Mersenne Twister into
another function to reduce the run length of seemingly biased outcomes. I
think to generate a single value the whole CPU’s cache is cleared from the
size and complexity of the code. The AES S-box, with the seed XORed before and
after, would still be a better solution (especially since the probably of
certain runs of similar values should be somewhat low).

The theory behind LCGs is a lot simpler, and using a suitably small prime, I
think, one could have an appropriately short period without excessive
predictability.

------
ggggtez
I dunno, seems silly. I'm sure there are already proven methods, and the
mathematical analysis in the article leaves something to be desired. You
should probably just lean on whatever is used in Progressive Slot Machines.

------
daenz
Super interesting. It seems to tap into the psychology of some people who
gamble... they think the more times they lose, the more likely they win the
next time. Why are humans wired this way?

~~~
throwawaywego
Human brains are optimized for prediction of future events, because this helps
with survival (eg: you can predict a winter coming up, so you stock up on
food).

Randomness is by (some) definition unpredictable. But humans are so eager for
pattern recognition that they will see, or expect, patterns that just are not
there.

"Pareidolia is the tendency to interpret a vague stimulus as something known
to the observer, such as seeing shapes in clouds, seeing faces in inanimate
objects or abstract patterns, or hearing hidden messages in music." and also:
[https://en.wikipedia.org/wiki/Apophenia#Causes](https://en.wikipedia.org/wiki/Apophenia#Causes)

On a similar note, humans are terrible at coming up with random/unpredictable
sequences. If you ask a group of test subjects to pick a random number between
1 and 10, you get a huge edge when you guess 3 or 7.

