線性同餘生成隨機數的一點思考

今天下午 pk 和我討論了一個問題,他看到在另一個項目組的 lua 代碼裏有一段使用線性同餘產生隨機數的代碼,但是那個項目組的同事告訴他這個函數生成的隨機數是分佈不均的。於是他想到了我前兩天給他講的關於 lua 裏 % 這個取餘數的符號跟 c 語言裏的差別。由此展開了討論。


首先鋪陳背景,線性求餘生成隨機數的方式很普遍,對隨機數要求不高的代碼都可以使用這種方式,因爲它實現簡單,如果不知道原理的可以去 goole 一下。關於它所產生的隨機數是否在域上分佈均勻,暫且不論,權當均勻的吧。


我把代碼做了簡化,只留下最核心的幾行,我們來看一下,這個產生僞隨機數的算法是這樣的:

local IA = 3877
local IB = 29573

local g_seed = 42

function setseed(seed)
	g_seed = seed
end

function getrandom(max)
	max = max or 100
	g_seed = g_seed * IA + IB
	print(g_seed)
	return g_seed % max
end


還算是簡單的線性同餘,當然各種邊界條件的判斷我都省略了。


這段代碼的問題在哪裏呢?首先有個前提條件,就是在實際我們求隨機數的時候,不會去求一個負數域的隨機數,換句話說,調用 getrandom() 函數傳遞的 max 值肯定是大於 0 的。

線性求餘的公式倒是很簡單:y = kx + b。這裏我們的 k = IA = 3877,b = IB = 29573。爲什麼是這兩個數呢,自己 google 吧。

這種方法求隨機數,其實就是根據一個 x 值,來計算出 y 值,然後用 y 值模取隨機數範圍,並把這個 y 值作爲下一次調用時的 x 值。舉個例子,比如這裏最初的 x 值就是那個 g_seed = 42,爲什麼是 42 也自己 google 吧。然後我要求一個 100 內的隨機數,我調用一下 getrandom(100),計算過程如下:

g_seed = 42 * 3877 + 29573 = 192407

結果爲 g_seed % 100 = 7

接着我又調用一次 getrandom(100) 想得到第二個隨機數,計算過程就變成:

g_seed = 192407 * 3877 + 29573 = 745991512

結果爲 g_seed % 100 = 12

當然我們肯定不會讓 g_seed 超過一個 double 能表示的整數範圍,額外處理這裏就不談了,這裏說明的是工作的原理就是這樣。


如果這個原理在 C/C++ 裏面是不會有問題的,但是對於 lua 來說,% 號確實放這裏不太合適,因爲 lua 裏 % 操作符等價於: a % b == a - math.floor( a / b ) * b

可以看這裏 http://www.lua.org/manual/5.1/manual.html#2.5.1


這樣會有什麼問題?由於我們一般不會 % 一個負數,因爲求隨機數範圍一般是正數,但是 g_seed 卻是可以由用戶來設置的,所以我們無法保證 g_seed 始終爲正數,根據上面這個等式,如果 a 爲負數,b 爲正數,那麼 lua 裏這個 % 的結果就肯定是一個正數。

這一點對我們的線性同餘求隨機數會造成什麼問題呢?

問題就在於它使得隨機的分佈不在均勻,當然前提是我們假設之前的分佈是均勻的。它使得當 g_seed 爲負數時,原本應該得到負數隨機數結果的那部分值變爲了正數,導致隨機數分佈向一方傾斜。


我們先來看線性同餘的一些性質,令最初的 x 爲 x[0]:

我們發現線性同餘求得的隨機數分別是:

x[1] = kx[0] + b

x[2] = k^2x[0] + kb +b

……

r[1] = (kx[0] + b) % max

r[2] = (k^2x[0] + kb +b) % max

r[3] = (k^3x[0] + k^2b + kb + b) % max

……

我們來考慮 x[n], x[n] = k^nx[0] + b(k^n-1)/(k-1)

因爲我們來看函數 y = 3877x + 29573 的函數圖形:

wKioL1QmUEew23ldAANynD_piz8099.jpg


可以看到,如果 x[0] 取一個正數,那麼下一個 x[1] 會是一個更大的正數,之後的所有 x 都會更大;如果 x[0] 取一個比較大的負數的時候,之後的 x 都會是更大的負數,那麼必然存在一個轉折點,x[0] 取這個值的時候,之後的所有 x 都等於 x[0] ,這也就是這個函數的不動點,對於我們這段代碼來說,令 3877x + 29573 = x,可以求出 x = - 29573 / 3876,如果 g_seed 最開始取了這個不動點的值,那麼這個求隨機數的算法就廢了,因爲每次的隨機數都是一樣的。


我其實想知道的是,最開始 g_seed 需要取一個怎麼樣的數,能夠保證之後的所有 g_seed 都是單調增加的。當然只有當 g_seed 的取值範圍存在負數的時候,我們的隨機結果纔是分佈不均勻的。對這段代碼來說,想要分佈均勻,只需要最初的 g_seed 爲非負數就不會有問題。


我們來尋找更普遍的性質,如果不改變這段代碼的寫法,依然使用 lua 的這個 % 操作符的含義(a % b == a - math.floor( a / b ) * b)來線性求餘數產生僞隨機數時,如果要保證隨機數分佈均勻,那麼必須保證 x[n] 恆爲非負數(n>=1)。

也就是 x[n] = k^nx[0] + b(k^n-1)/(k-1) 要恆大於等於 0(n>=1) 。k,b,x[0] 均爲常數,我們整理得到:


x[n] = (x[0] + b/(k-1))k^n - b/(k-1)


對它求導得到:


x'[n] = (x[0] + b/(k-1))k^nlnk


我們想知道當 b 和 k 確定時(k~=0), x[0] 取什麼數時,(1)x[1] >= 0 且 (2)x'[n] > 0。

(1) =》x[0] >= -b/k

(1)帶入(2)得到:

wKioL1QmUH7CrrsbAAGG0l6Glu0880.jpg


並且我們還發現一點,就是當 k  > 1 的時候,x[n] 的絕對值隨着 n 的增大是越來越大的,當 0 < k < 1 的時候, x[n] 的絕對值是收斂到 b 的絕對值的。

應用這個表我們最後知道,對於這段代碼的 k,b 來說,x[0] 也就是 g_seed 最小不能小於 -29573 × 3878 / 3877^2,差不多是 -7.6 左右,只要最初的 g_seed 比這個值大,那麼這段代碼求出來的隨機數就可以看作是沒有問題的。


至此終於知道這段代碼風險在哪了,不過既然知道了,就動手改一下唄,可代碼是別人的動不得,囧。最近我要離職了,準備找一個新的環境,不知道朋友們有沒有覺得靠譜的地,向我推薦下唄:)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章