2020年11月19日 星期四

機率研究 (5) - 指定機率分佈生成之 - 區間分配法實作與分配驗證

這一篇研究的內容,是第 (1) 篇研究中的升級版,我們從 Golang 上進行這樣分佈生成的實作,並且嘗試使用統計學的方法來驗證指定機率的正確性。



進行機率的研究,最主要目的是了解機率學在博弈遊戲中的工程方法與數學方法,隨著研究的深入,也等於是從外行人的角度去了解博弈業的各種名詞、工程方法,所以,若我們從第 (1) 篇的研究繼續探討,其實有限個數、無限個數指的也是博弈遊戲中的【自然機率】、【非自然機率】。


回過頭來審視前幾篇研究在做的事情:

(1): 從幾個專案的程式碼中窺探中獎機率的設計

(2): 什麼是可測與不可測亂數,使用常見的線性同餘設計一個亂數生成函數

(3): 分佈對機率代表的意義與常被使用的解釋

(4): 工程上常見的機率分佈逆轉換,從均勻隨機分佈,做線性變換處理得到人為的機率生成函數

(5): 應用端常見的機率分佈生成,使用區間分配法製作亂數產生器本身,並且做分配驗證


進入正題,這一篇如敘述所說,是紀錄 Golang 如何做純應用面上的指定機率分配生成函數。


給定機率分配: [ 0.2, 0.6, 0.2 ] (這是合法的機率分配: 因為加總是 1)

按照順序 0, 1, 2 代表著選到 【0】 號數字的機率是 0.2、【1】 號數字的機率是 0.6、【3】 號數字的機率是 0.2。


在思路上,我們可以使用加總的方式來得到每個標號的區間 [1],直接列表如下:

  • 【0】: [0~0.2) 
  • 【1】: [0.2~0.8)
  • 【2】: [0.8~1)

不過,如果使用的亂數產生器是 rand int ,就必須用整數的做法,為了視覺化,所以以下的程式碼全部使用整數機率處理,也就是放大所有人的倍數 (包含 rand int 的倍數),放大區間後,因為線性變換並不會影響機率。

我們可以設定一個 accurcy = 100,表示使用 100 倍變成整數後,來處理亂數,得到新的區間表:

  • 【0】: 0~20
  • 【1】: 21~80
  • 【2】: 81~100
放大區間後,隨便給定一個均勻隨機變數,他會落在 0~100 之間,注意,此時使用的 rand 函數就是:

rand.Intn(100) //實際上是 rand.Intn(accurcy)

然後,我們在第(1)篇研究中,是手動去累加 【0】【1】【2】 所在的機率,可能會像:


if 0 <= x && x <= 20{
...
}else if 21 <= x && x <= 80{
...		
}

現在,我們希望他可以自動適應,自動計算,所以採用了這個方法:


func GENERATOR(p []float32) int {
	//given
	accuracy := 100

	// probabilities 開出整數的 N 個機率事件空間,各自機率加起來要等於 accuracy
	probabilities := []int{}
	
    // 把輸入進來的 p 機率陣列,放大 accuracy 倍後轉成整數,丟進機率事件空間
	for _, _p := range p {
		probabilities = append(probabilities, int(_p*float32(accuracy)))
	}
	
    // 設定起始計算機率點
	var start, end int = 0, 0
 	
    // 因為放大過,所以 rand 隨機數大小就會隨著 accuracy 大小變換
	rand := rand.Intn(accuracy)
    
    // 開始迭代每一個區間,每個區間都有上限下限,只是要看丟出來的機率在第幾個區間 (i)
	for i, probability := range probabilities {
    
		end += probability // 每一次都會增加區間上限,也就是區間浮動的概念
        
        // 如果這個隨機數,的確落在這個區間,就表示選到這個區間了
		if start <= rand && end > rand {
			//fmt.Println("選擇區間數: ", i, "; 區間起點:",start,";區間終點:", end, "; 機率值: ", rand)
			return i
		}
        
		start = end // 下一個 i 的下限就被提高了,也就等於區間浮動的概念
	}
	return -1
}

現在,我們必須做一點驗證,確定丟進來的機率是否用這個方式做,會正常:


func main() {

	forZero := 0.0 //統計 【0】 發生次數機率
	forONE := 0.0  //統計 【1】 發生次數機率
	forTwo := 0.0  //統計 【2】 發生次數機率

	// 直接跑 1000000 次
	for a := 0; a < 1000000; a++ {
		p := Algorithm([]float32{0.2, 0.6, 0.2})
		//fmt.Println(p)
		
        //如果結果是 【0】、【1】或是【2】,就統計他們發生的次數
		if p == 0 {
			forZero += 1.0
		}
		if p == 1 {
			forONE += 1.0
		}
		if p == 2 {
			forTwo += 1.0
		}
	}
	
    // 古典機率就告訴我們發生的次數除上母數,就是該事件的機率:
	fmt.Println(forZero / 1000000.0)
	fmt.Println(forONE / 1000000.0)
	fmt.Println(forTwo / 1000000.0)
}

經過直接的驗證,我已經得到相近的結果了:



不過,在這裡我碰到一個小插曲,有關於 golang 的 rand 沒有指定 seed,所以整個 default 機率序列長得一模一樣 [10],所以請記得在設計 golang rand 的時候,要做隨機種子:




var r1 *rand.Rand
func init(){
	s1 := rand.NewSource(time.Now().UnixNano())
    r1 = rand.New(s1)
}

func GENERATOR_WITH_SEED(p []float32) int {
	
    // 不要把 randon seed 在這邊產生,因為每
    // 一次都要重新 new 新的 seed 會很容易早成效能瓶頸
    // NO NO NO NO: s1 := rand.NewSource(time.Now().UnixNano())
    // NO NO NO NO: r1 = rand.New(s1)

	//given
	accuracy := 100

	// probabilities 開出整數的 N 個機率事件空間,各自機率加起來要等於 accuracy
	probabilities := []int{}
	
    // 把輸入進來的 p 機率陣列,放大 accuracy 倍後轉成整數,丟進機率事件空間
	for _, _p := range p {
		probabilities = append(probabilities, int(_p*float32(accuracy)))
	}
	
    // 設定起始計算機率點
	var start, end int = 0, 0
 	
    // 因為放大過,所以 rand 隨機數大小就會隨著 accuracy 大小變換
	rand := r1.Intn(accuracy)
    
    // 開始迭代每一個區間,每個區間都有上限下限,只是要看丟出來的機率在第幾個區間 (i)
	for i, probability := range probabilities {
    
		end += probability // 每一次都會增加區間上限,也就是區間浮動的概念
        
        // 如果這個隨機數,的確落在這個區間,就表示選到這個區間了
		if start <= rand && end > rand {
			//fmt.Println("選擇區間數: ", i, "; 區間起點:",start,";區間終點:", end, "; 機率值: ", rand)
			return i
		}
        
		start = end // 下一個 i 的下限就被提高了,也就等於區間浮動的概念
	}
	return -1
}

在上面,也不過是把 rand := 換成 r1 來產生。

同時也要注意上述所說的, random seed 可以動態變換,但不要每一次都重新產生,會讓速度差異很大。



Reference:

[1]: https://studygolang.com/articles/23067

[2]: https://blog.csdn.net/m0_37710023/article/details/108061125?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduend~default-1-108061125.nonecase&utm_term=go%20%E7%94%9F%E6%88%90%E6%AD%A3%E6%80%81%E5%88%86%E5%B8%83%E7%9A%84%E9%9A%8F%E6%9C%BA%E6%95%B0&spm=1000.2123.3001.4430

[3]: https://blog.csdn.net/aaaadong/article/details/92839996

[4]: https://blog.sqrtthree.com/articles/random-number-in-golang/

[5]: https://ezslotdesign.com/%e8%87%aa%e7%84%b6%e8%88%87%e9%9d%9e%e8%87%aa%e7%84%b6%e6%a9%9f%e7%8e%87/

[6]: https://ch-hsieh.blogspot.com/2016/01/bernoulli-binomial.html

[7]: https://fu-sheng-wang.blogspot.com/2017/01/statistics15-binomial-distribution.html

[8]: http://web.ntnu.edu.tw/~algo/Estimation.html

[9]: https://www.taodabai.com/how/641240802.html

[10]: https://github.com/golang/go/issues/11871

[11]: https://github.com/golang/go/issues/4509

沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014