隨機數有多隨機?

轉自:http://blog.codingnow.com/2007/11/random.html#comments

 

作爲一個常識,每個程序員在做入門學習時,都會被老師諄諄教導:我們用的編程語言中的隨機函數,只能產生出僞隨機數。它有它的內在規律,只能作爲對顯示世界的隨機事件的近似模擬。接下來,我們通常會被傳授隨機種子的概念。以及用物理上更隨機的量做種子。比如系統時間、兩次敲擊鍵盤的時間間隔、多次移動鼠標的偏移、甚至系統出錯的出錯信息碼等等。

作爲遊戲數值策劃,除了加減乘除,用的最多的數學概念恐怕就是隨機數了。有經驗的數值策劃或許從他的前輩那得知計算機中程序產生的隨機數並不太可靠;或者他本身就受過程序方面的訓練。如果遊戲項目更幸運一點,擔當數值策劃的他是一個數學愛好者,並讀過諸如《計算機程序設計藝術》這樣的技術書籍,那麼事情會好的多。可惜大多數境遇下,策劃們從不深究計算機隨機數背後的細節,也不太關心所謂“僞”隨機數究竟“僞”到什麼程度。

最近幾天,有測試人員向我抱怨,我們遊戲中某些概率設定總感覺有點怪怪的。似乎跟文檔上的不同。

這種抱怨並不少見,許多網絡遊戲玩家都在抱怨系統生成的隨機數不太對勁。善良點的玩家會歸咎到自己的 RP 上,陰謀論者則指責系統作弊。運營着的遊戲背後,數值策劃和程序員們有苦說不出。

有必要科普一些數學常識,也作爲我週末讀書的一些筆記。

鑑於我所接觸過的多數遊戲策劃大多沒有很高的數學素養(這裏用於對比的參照物——我自己,在數學方面的修養已經夠差了),下面不列公式,只列常識。如果涉及一些數學上的結論,也迴避證明過程。

以扔硬幣爲例來看概率:

如果硬幣本身沒有問題,那麼每次實驗的結果,正面和反面的出現概率理應一致,都是 50% 。

btw ,如果碰巧連丟了 10 次都是正面的話,認爲第 11 次出現反面的概率更高的讀者可以不必看下去了。根據基本的概率理論,作爲獨立實驗,相互間是不會造成概率影響的。無論前面出現多少次正面,下一次正面的概率依舊是 50% ,不會多也不會少。當然,如果是現實中出現這種情況,我會認爲是道具本身出了問題,或許這個硬幣兩面都是正面。這樣的話,第 11 次倒是極有可能再次出現正面。

對獨立的隨機事件的單次預測做不到準確,但卻可以從統計上得到一個穩定的數值。我們知道,大量做丟硬幣的實驗 n 次。隨着 n 的增大,出現正面和反面的次數會趨與一致。

OK ,以上都是作爲一個遊戲數值策劃應該具有的常識。只是人們往往忽略了一點:若是正面和反面在多次實驗中出現次數嚴格一致的話,又反過來是一件小概率事件。例如:丟一萬次硬幣,正好出現 5000 次正面的概率大概不會比你買彩票中個小獎的概率高多少。


補充:

在 10000 次一組的丟硬幣的實驗中,正好出現 5000 次正面的概率依舊是比所以其它情況概率更大一些的。其它情況可能是 10000 次 …… 5001 次、4999 次、4998 次 …… 甚至零次正面。

正好出現 5000 次正面有最大的可能性,但是絕對概率卻不大(也不算太小)。

前段時間寫過一篇 blog 談到了用交換法洗牌 ,那篇文章中提及的一篇論文的結論談到:

“當 N 大於等於 18 時,用這個方法洗牌後,居然恆等排列(identity permutation)是最有可能出現的。(所謂恆等排列大概是指第 n 張排在第 n 個位置)”

這句話很容易被人誤解。概率最大並不等於很容易出現,正如 10000 次丟硬幣剛好出現 5000 次正面不易出現一樣。


如何看一個均勻分佈的隨機數發生器產生的數值到底隨機不隨機,簡單的用統計產生出來的數字的分佈是否接近均等是遠遠不夠的。接下來介紹一下統計學中最著名的 χ 方檢驗。

假設我們投一個六面骰。每次 1 ~ 6 的點數出現的概率均爲 1/6 。如果用計算機來模擬它,採用函數 random (1,6) 來產生一個 1 到 6 之間的隨機整數。怎樣判斷產生的數字夠不夠隨機呢?

我們可以投 n 次(n 很大,比如 n=10000 ),統計出每個點數出現的次數:Y1,Y2,Y3,Y4,Y5,Y6 。理想的次數則都應該接近於 p=n/6 次。

取 V=(Y1 - p)^2 / n + (Y2 - p)^2 / n + (Y3 - p)^2 / n + (Y4 - p)^2 / n + (Y5 - p)^2 / n + (Y6 - p)^2 / n

劃簡後 V=6/n * ( Y1 到 Y6 的平方和) - n

這個 V 即量化表示了這 n 次實驗反應出來的數字隨機性。當 V 過大時,骰子可能偏向某幾個特定點數更多一些。而 V 過小的話,則可能是隨機數發生器有一些明顯的規律(事實上,所有僞隨機數產生算法都是一定有規律的)。

我們現在需要知道的是,對於一個隨機數列,什麼樣的 V 是比較合理的。對於用隨機數做骰子的實驗,V 的值也是隨機的,當然不是均勻分佈。實際上它符合 χ 方分佈。如果我們投骰子用的隨機數是真正的均勻分佈的話,V 值出現特別小或特別大的概率都很小。

實際上,在這個六面骰的例子中,V 有 1% 的可能小於 0.5543 ,有 5% 的可能小於 1.1455 ,有 25% 的可能小於 2.675 ,有 50% 的可能小於 4.351 ,有 75% 的可能小於 6.626 ,有 95% 的可能小於 11.07 ,有 99% 的可能小於 15.09 。

我實驗了 gcc 3.4.2 的 libc 中帶的隨機函數去模擬六面骰。做了五組實驗,每組分別爲 2000, 4000, 6000, 8000, 10000 次。得到的 V 的值分別爲:

9.088 3.371 6.432 15.805 1.7204

注:我使用了系統時間做種子,每次做此實驗都會得到不同結果,本質上這些值也是隨機的。

每組次數不同是因爲:僞隨機數列往往具有一定的週期性,當不知道它的週期特性時,n 選的不合適可能導致多段非隨機帶有某中傾向性的區間相互抵消其影響。

我們再次分析上面得到的五個數據,若隨機數真的隨機,他們出現的概率大約落在這樣五個區間75%~95%) ,(25%~50%) ,(50%~75%),(99%~100%),(5%~25%)

表現不太壞,但也不算好。尤其是第四組實驗數據,V=15.805 ,這是個很大的值。因爲 V 值本應有 99% 的概率小於 15.09 ,所以出現這個值的概率應該只有不到 1% 。當然這也可能是個巧合(1% 的事件發生並不算太奇怪)。我後來又反覆取不同的 n 測算了幾次,有一次 V 又小於了 0.55 ,這也不太正常(也只有不到 1% 的可能性)。

最後,我的結論是:我用的 gcc 這個版本的 rand 函數不算很好。至少不能應用於極端要求隨機性的場合。它對大量模擬六面骰這件事情上做的不太成功。

ps. 關於 χ 方分佈的選定的百分值,可以參考《計算機程序設計藝術 第二卷 半數值算法》的 P39 頁。上面我摘取的第五行。因爲這裏六面骰的點數分佈有五個自由度(第六種點數的出現次數可以用其它五種出現次數推算出來)。


前幾天寫過一篇 blog ,裏面隨手舉了個例子:遊戲設計人員設計了一個 10 萬分之一的凋落率 。一個做策劃的朋友在 gtalk 上問我,這並不是個複雜的問題呀。難道有什麼玄機?

按這個朋友的思路,產生兩次隨機數就夠了,一次 random(1,1000) 一次 random(1,100) ,只有兩個數值都爲 1 時,這個十萬分之一個小概率事件才發生。

不錯,理論上是這樣,實際我們大多也這樣的。而且很高興看到,作爲一個數值策劃,他迴避了直接用 random(1,100000) 。有編程常識的兄弟們都知道,大多數語言提供的標準數學函數庫中不提供十萬分之一級別精度的隨機函數。

但是精度問題依然存在。我反過來問如果產生一個十萬分之七的隨機數時,他出現了一點小失誤:random(1,100) * random(1,1000) <=7 是不對的。當然這個小錯誤很容易被修正。

實際應用中,更爲正確的做法是迴避直接使用高分辨率的隨機數列。因爲通常所用的線形同餘序列產生的僞隨機數列都不適用(道理很簡單,每個隨機數的精度要求提高意味着僞隨機數列的週期變短,過短的週期性必然帶來隨機性的喪失)。如果真的有這類需求,我們應該讓程序員去專門寫一個了。

不光是這種超高精度隨機數的需求應該儘量被迴避。單次隨機量的自由度也最好被限制。計算機模擬丟硬幣總比模擬投六面骰看起來要真實一些。而模擬六面骰又好過二十面。用固定面數的骰子模擬出來,也比數值設定時八卦一個百分比概率要強。

原因並非完全源於它們需要的精度不同。更重要的是自由度小一些的隨機量,更容易被統計方法檢驗是否合理。一旦程序產生的僞隨機數隨機性被檢驗出來不太好,我們總有機會去嘗試一個更好的,不是嗎?

最後推薦一個隨機數發生器:http://www.agner.org/random/

 

 

 

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