小樣本大概率事件的正確處理方式 - 1. 概率的含義和誤差產生的原因

版權聲明:本文爲博主原創文章,轉載請註明出處。 https://blog.csdn.net/xmousez/article/details/72260285

問題描述

在很多時候——特別是在遊戲中——我們經常需要對某一事件進行隨機觸發處理,例如:攻擊有機率觸發暴擊,怪物有機率掉出裝備,武器有機率強化失敗。通常我們的做法就是,從0~1中取隨機數,然後判斷是否大於給出的機率(實際上大多數時候爲了計算效率會用0~100的隨機整數)。

當我們從整個遊戲世界上去統計這個隨機事件時,無論這個概率是小概率還是大概率,得到的結果與我們的期望都不會有很大的差別。但是從單一玩家的角度,或者是某一短時間段的角度去統計這個事件時,對於結果的直觀感受會有很大的波動性——換句話說,得到的實際概率往往並不是我們所想要的概率。

實際上從概率論的角度能給出非常合理的解釋,期望方差標準差啥的一套公式概念拿出來,似乎就“原來如此”了。但是我們要的是直觀的解釋和合理的算法,而不是一堆晦澀難懂的公式概念。

問題分析

舉個栗子:
玩家在攻擊時有30%的機率觸發暴擊。從遊戲世界中統計一個大樣本,得到的觸發概率基本不會與預期有很大的差別。但是對於一個玩家十次攻擊的結果來說呢?
這裏寫圖片描述
從上圖模擬能很明顯看到,對於一個小樣本模擬,其概率與我們的期望相差甚遠。你完全能夠腦補出玩家在心中反覆唸叨的“我擦,這人品”。事實上,0/10或者10/10這樣的樣本,在獨立隨機事件中是完全合乎邏輯的。初中課本上就出現過的概念:對於獨立隨機事件,任何一次事件都不會影響到另一次事件的發生。

煎蛋一菊花的解釋就是:概率值的本質上是對一個樣本中某個特定事件的統計計算結果,而並非對某一事件是否發生的約束條件。樣本和事件是前提,概率纔是結果,反過來從某一統計概率上去預測樣本事件的發生情況,並不能得到我們所想要的結果(所以彩票預測和股票週四會漲啥的完全就是扯淡)。

從心理感官方面,如果是一個概率非常小的事件,我們並不會從小樣本上去分析,千分之一的概率在一百次中出現一次以上的概率太小,而在一千次中沒有一次出現,或者出現了兩次,看起來都並沒有什麼奇怪。所以我們需要解決的,是“小樣本大概率事件”在程序中的邏輯優化。

解決方案

首先,我們有一個隨機概率P,要讓其在樣本數量S中體現出來,我們必須將事件觸發的次數限制爲P*S。也就是說,我們需要將S次樣本大小爲1的發生概率爲P的獨立重複隨機事件,變成一次樣本大小爲S的,觸發次數爲P*S的事件,從而把該樣本的統計概率約束在P

但是,作爲一個合理的隨機事件,“隨機”是必須存在一定概率波動的,只是獨立重複隨機事件的“隨機”並不可控,所以我們無法將波動控制在我們的可接受範圍內。而對固定次數的事件做優化,給定一個波動值D,將觸發次數變成P*S ±D中的隨機數,那麼就能夠對該事件進行“可控隨機化”了。

需求分析

初始化參數

  1. 樣本數量quantity;
  2. 期望次數expectation;
  3. 波動大小deviation;

公共接口

  1. bool GetNext():下次事件是否觸發;
  2. void Reset():重新計算隨機事件;
  3. void Reset(int deviation):以給定的波動值重新計算隨機事件;

邏輯流程

  1. 以expectation和deviation得出一個隨機數作爲樣本中觸發事件數量count;
  2. 從0到quantity中取count個隨機數保存到selection[];
  3. 每次用GetNext()取結果時,樣本編號加一(如果超過樣本數量,則重新計算),判斷該樣本編號是否存在於selection[]中,存在則返回true;

代碼編寫(可略過不看)

邏輯優化:對於判斷當前樣本是否爲觸發點,即樣本編號是否在selection中,可以對selection進行排序後進行判斷;

/*
 * Author:      熊哲
 * CreateTime:  5/14/2017 2:20:04 PM
 * Description:
 * 
*/
using System.Text;
using UnityEngine; // 只用了unity引擎的Random,非unity環境改掉就行

public class RandomTrigger
{
    public int quantity { get; private set; }
    public int expectation { get; private set; }
    public int deviation { get; private set; }

    public int sampleIndex { get; private set; }
    public int triggerIndex { get; private set; }
    public int[] triggers { get; private set; }

    public RandomTrigger(int quantity, int expectation, int deviation = 0)
    {
        this.quantity = quantity;
        this.expectation = expectation;
        this.deviation = deviation;
        Reset();
    }

    public void Reset()
    {
        sampleIndex = 0;
        triggerIndex = 0;
        int count = Random.Range(expectation - deviation, expectation + deviation + 1);
        if (count < quantity)
        {
            triggers = RandomSelect(quantity, count);
            Sort(triggers, 0, triggers.Length);
        }
        else // 觸發次數大於樣本數量,必定每次都會返回true
        {
            triggers = new int[quantity];
        }
    }
    public void Reset(int deviation)
    {
        this.deviation = deviation;
        Reset();
    }
    public bool GetNext()
    {
        // 重新取樣
        if (sampleIndex >= quantity) Reset();

        // 觸發次數大於樣本數或者是觸發點則返回true;
        if (triggers.Length >= quantity || (triggerIndex < triggers.Length && sampleIndex == triggers[triggerIndex]))
        {
            sampleIndex++;
            triggerIndex++;
            return true;
        }
        else
        {
            sampleIndex++;
            return false;
        }
    }

    // 從0~amount中得到count個不重複的隨機數
    protected int[] RandomSelect(int amount, int count)
    {
        int[] array = new int[amount];
        int[] result = new int[count];
        for (int i = 0; i < count; i++)
        {
            int left = amount - i;
            int random = Random.Range(0, left);

            if (array[random] > 0)
            {
                result[i] = array[random];
            }
            else
            {
                result[i] = random;
            }

            if (array[left - 1] == 0)
            {
                array[random] = left - 1;
            }
            else
            {
                array[random] = array[left - 1];
            }
        }
        return result;
    }
    // 快速排序算法
    protected void Sort(int[] array, int left, int right)
    {
        if (left < right)
        {
            int low = left;
            int high = right - 1;
            int key = array[low];
            while (low < high)
            {
                while (array[high] > key)
                    high--;
                Swap(array, low, high);
                while (array[low] < key)
                    low++;
                Swap(array, low, high);
            }
            Sort(array, left, high - 1);
            Sort(array, high + 1, right);
        }
    }
    // 數組元素交換
    protected void Swap<T>(T[] array, int index1, int index2)
    {
        T temp = array[index1];
        array[index1] = array[index2];
        array[index2] = temp;
    }

    public override string ToString()
    {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < triggers.Length; i++)
        {
            str.Append(triggers[i] + " ");
        }
        return str.ToString();
    }
}

結果驗證

對於每次判斷某概率是否發生,所得的概率與取樣數量的關係曲線會在期望值上波動,並且波動幅度並不受控制,誤差也很大。
而修正後的算法,波動會有所減小並且可控,如果波動值爲0,每當樣本數量是某一值的倍數時,概率會修正到期望值(詳見小樣本大概率事件的正確處理方式 - 2. 結果分析)。

注意事項

  • 任何概率性事件都可以使用該方法進行概率約束,並不僅限於小樣本事件。
  • 在該約束下3/10與6/20會稍有不同,最明顯的區別是取樣20,前者連續觸發最高爲6次,而後者最高爲12次(前一次觸發集中於最後而後一次觸發集中於最前)。
  • 該文章的算法只是爲了方便讀者理解概念,不應該在實際工作中應用。

小樣本大概率事件的正確處理方式 - 2. 結果分析
小樣本大概率事件的正確處理方式 - 3. 實際使用

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