Đây có phải là một thuật toán ngẫu nhiên đủ tốt hay không; Tại sao nó không được sử dụng nếu nó nhanh hơn?


171

Tôi đã thực hiện một lớp được gọi QuickRandomvà công việc của nó là tạo ra các số ngẫu nhiên một cách nhanh chóng. Điều đó thực sự đơn giản: chỉ cần lấy giá trị cũ, nhân với a doublevà lấy phần thập phân.

Đây là QuickRandomtoàn bộ lớp học của tôi :

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

}

Và đây là đoạn mã tôi đã viết để kiểm tra nó:

public static void main(String[] args) {
        QuickRandom qr = new QuickRandom();

        /*for (int i = 0; i < 20; i ++) {
            System.out.println(qr.random());
        }*/

        //Warm up
        for (int i = 0; i < 10000000; i ++) {
            Math.random();
            qr.random();
            System.nanoTime();
        }

        long oldTime;

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            Math.random();
        }
        System.out.println(System.nanoTime() - oldTime);

        oldTime = System.nanoTime();
        for (int i = 0; i < 100000000; i ++) {
            qr.random();
        }
        System.out.println(System.nanoTime() - oldTime);
}

Đó là một thuật toán rất đơn giản, chỉ cần nhân đôi số trước với số nhân "số ma thuật". Tôi đã ném nó cùng nhau khá nhanh, vì vậy tôi có thể làm cho nó tốt hơn, nhưng kỳ lạ thay, nó dường như đang hoạt động tốt.

Đây là đầu ra mẫu của các dòng nhận xét trong mainphương thức:

0.612201846732229
0.5823974655091941
0.31062451498865684
0.8324473610354004
0.5907187526770246
0.38650264675748947
0.5243464344127049
0.7812828761272188
0.12417247811074805
0.1322738256858378
0.20614642573072284
0.8797579436677381
0.022122999476108518
0.2017298328387873
0.8394849894162446
0.6548917685640614
0.971667953190428
0.8602096647696964
0.8438709031160894
0.694884972852229

Hừm. Khá ngẫu nhiên. Trong thực tế, điều đó sẽ làm việc cho một trình tạo số ngẫu nhiên trong một trò chơi.

Đây là đầu ra mẫu của phần không bình luận:

5456313909
1427223941

Ồ Nó thực hiện nhanh hơn gần 4 lần Math.random.

Tôi nhớ đã đọc ở đâu đó Math.randomsử dụng System.nanoTime()và hàng tấn mô-đun điên và công cụ phân chia. Điều đó có thực sự cần thiết? Thuật toán của tôi thực hiện nhanh hơn rất nhiều và có vẻ khá ngẫu nhiên.

Tôi có hai câu hỏi:

  • Thuật toán của tôi có "đủ tốt" không (ví dụ, một trò chơi, trong đó các số thực sự ngẫu nhiên không quá quan trọng)?
  • Tại sao Math.randomlàm nhiều như vậy khi nó dường như chỉ là phép nhân đơn giản và cắt bỏ số thập phân sẽ đủ?

154
"Có vẻ khá ngẫu nhiên"; bạn nên tạo một biểu đồ và chạy một số tự động tương ứng trên chuỗi của bạn ...
Oliver Charlesworth

63
Ông có nghĩa là "có vẻ khá ngẫu nhiên" không thực sự là thước đo khách quan của tính ngẫu nhiên và bạn sẽ có được một số thống kê thực tế.
Matt H

23
@Doorknob: Theo thuật ngữ của giáo dân, bạn nên điều tra xem các số của bạn có phân phối "phẳng" trong khoảng từ 0 đến 1 hay không và xem liệu có bất kỳ mẫu định kỳ / lặp đi lặp lại theo thời gian hay không.
Oliver Charlesworth

22
Hãy thử new QuickRandom(0,5)hoặc new QuickRandom(.5, 2). Cả hai sẽ liên tục xuất 0 cho số của bạn.
FrankieTheKneeMan

119
Viết thuật toán tạo số ngẫu nhiên của riêng bạn cũng giống như viết thuật toán mã hóa của riêng bạn. Có rất nhiều nghệ thuật trước đây, bởi những người có trình độ siêu phàm, đến nỗi việc dành thời gian của bạn để cố gắng làm cho nó trở nên vô nghĩa. Không có lý do gì để không sử dụng các hàm thư viện Java và nếu bạn thực sự muốn tự viết vì lý do nào đó, hãy truy cập Wikipedia và tìm kiếm các thuật toán ở đó như Mersenne Twister.
steveha

Câu trả lời:


351

Việc QuickRandomthực hiện của bạn không thực sự là một phân phối thống nhất. Các tần số thường cao hơn ở các giá trị thấp hơn trong khi Math.random()có phân phối đồng đều hơn. Đây là một SSCCE cho thấy:

package com.stackoverflow.q14491966;

import java.util.Arrays;

public class Test {

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        int[] frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (qr.random() * 10)]++;
        }
        printDistribution("QR", frequencies);

        frequencies = new int[10];
        for (int i = 0; i < 100000; i++) {
            frequencies[(int) (Math.random() * 10)]++;
        }
        printDistribution("MR", frequencies);
    }

    public static void printDistribution(String name, int[] frequencies) {
        System.out.printf("%n%s distribution |8000     |9000     |10000    |11000    |12000%n", name);
        for (int i = 0; i < 10; i++) {
            char[] bar = "                                                  ".toCharArray(); // 50 chars.
            Arrays.fill(bar, 0, Math.max(0, Math.min(50, frequencies[i] / 100 - 80)), '#');
            System.out.printf("0.%dxxx: %6d  :%s%n", i, frequencies[i], new String(bar));
        }
    }

}

Kết quả trung bình trông như thế này:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  11376  :#################################                 
0.1xxx:  11178  :###############################                   
0.2xxx:  11312  :#################################                 
0.3xxx:  10809  :############################                      
0.4xxx:  10242  :######################                            
0.5xxx:   8860  :########                                          
0.6xxx:   9004  :##########                                        
0.7xxx:   8987  :#########                                         
0.8xxx:   9075  :##########                                        
0.9xxx:   9157  :###########                                       

MR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  10097  :####################                              
0.1xxx:   9901  :###################                               
0.2xxx:  10018  :####################                              
0.3xxx:   9956  :###################                               
0.4xxx:   9974  :###################                               
0.5xxx:  10007  :####################                              
0.6xxx:  10136  :#####################                             
0.7xxx:   9937  :###################                               
0.8xxx:  10029  :####################                              
0.9xxx:   9945  :###################    

Nếu bạn lặp lại thử nghiệm, bạn sẽ thấy phân phối QR thay đổi rất nhiều, tùy thuộc vào các hạt giống ban đầu, trong khi phân phối MR ổn định. Đôi khi nó đạt đến phân phối thống nhất mong muốn, nhưng thường thì không. Đây là một trong những ví dụ cực đoan hơn, nó thậm chí còn vượt ra ngoài biên của biểu đồ:

QR distribution |8000     |9000     |10000    |11000    |12000
0.0xxx:  41788  :##################################################
0.1xxx:  17495  :##################################################
0.2xxx:  10285  :######################                            
0.3xxx:   7273  :                                                  
0.4xxx:   5643  :                                                  
0.5xxx:   4608  :                                                  
0.6xxx:   3907  :                                                  
0.7xxx:   3350  :                                                  
0.8xxx:   2999  :                                                  
0.9xxx:   2652  :                                                  

17
+1 cho dữ liệu số - mặc dù nhìn vào số thô có thể gây hiểu nhầm vì điều đó không có nghĩa là chúng có sự khác biệt có ý nghĩa thống kê.
Maciej Piechotka

16
Những kết quả này khác nhau rất nhiều với các hạt giống ban đầu được chuyển đến QuickRandom. Đôi khi, nó gần với đồng phục, đôi khi nó còn tệ hơn thế này nhiều.
Petr Janeček

68
@ BlueRaja-DannyPflughoeft Bất kỳ PRNG nào mà chất lượng đầu ra phụ thuộc nhiều vào giá trị hạt giống ban đầu (trái ngược với hằng số nội bộ) dường như bị phá vỡ đối với tôi.
CVn

22
Nguyên tắc thống kê đầu tiên: vẽ đồ thị dữ liệu . Phân tích của bạn là tại chỗ, nhưng vẽ biểu đồ cho thấy điều này nhanh hơn nhiều. ;-) (Và đó là hai dòng trong R.)
Konrad Rudolph

37
Tất nhiên, bất cứ ai xem xét các phương pháp sản xuất các chữ số ngẫu nhiên, tất nhiên, trong tình trạng tội lỗi. - John von Neumann (1951) Triệu Bất cứ ai chưa xem trích dẫn ở trên ít nhất 100 địa điểm có lẽ không phải là rất cũ. - DV Pryor (1993) Không nên chọn bộ tạo số ngẫu nhiên ngẫu nhiên. - Donald Knuth (1986)
Happy Green Kid Naps

133

Những gì bạn đang mô tả là một loại trình tạo ngẫu nhiên được gọi là trình tạo cộng hưởng tuyến tính . Máy phát điện hoạt động như sau:

  • Bắt đầu với một giá trị hạt giống và số nhân.
  • Để tạo một số ngẫu nhiên:
    • Nhân hạt giống theo cấp số nhân.
    • Đặt hạt giống bằng giá trị này.
    • Trả lại giá trị này.

Trình tạo này có nhiều thuộc tính đẹp, nhưng có vấn đề quan trọng như là một nguồn ngẫu nhiên tốt. Bài viết Wikipedia được liên kết ở trên mô tả một số điểm mạnh và điểm yếu. Tóm lại, nếu bạn cần các giá trị ngẫu nhiên tốt, đây có lẽ không phải là một cách tiếp cận tốt.

Hi vọng điêu nay co ich!


@ louism- Nó không thực sự "ngẫu nhiên", mỗi lần. Kết quả sẽ mang tính quyết định. Điều đó nói rằng, tôi đã không nghĩ về điều đó khi viết câu trả lời của mình; có lẽ ai đó có thể làm rõ chi tiết đó?
templatetypedef

2
Lỗi số học dấu phẩy động được thiết kế thực hiện. Theo tôi biết, chúng phù hợp với một nền tảng nhất định nhưng có thể khác nhau, ví dụ giữa các điện thoại di động khác nhau và giữa các kiến ​​trúc PC. Mặc dù có thêm 'bit bảo vệ' đôi khi được thêm vào khi thực hiện một loạt các phép tính dấu phẩy động liên tiếp và sự hiện diện hay vắng mặt của các bit bảo vệ này có thể làm cho phép tính khác biệt một cách tinh tế trong kết quả. (bảo vệ các bit đang tồn tại, ví dụ: việc mở rộng gấp đôi 64 bit thành 80 bit)
Patashu

2
Ngoài ra, hãy nhớ rằng lý thuyết đằng sau LCRNG đều cho rằng bạn đang làm việc với các số nguyên! Ném các số dấu phẩy động vào nó sẽ không mang lại chất lượng kết quả như nhau.
duskwuff -inactive-

1
@duskwuff, bạn nói đúng. Nhưng nếu phần cứng dấu phẩy động tuân theo các quy tắc lành mạnh, thì việc này cũng giống như thực hiện nó theo kích thước mantissa, và lý thuyết được áp dụng. Chỉ cần chăm sóc thêm trong những gì bạn đang làm.
vonbrand

113

Hàm số ngẫu nhiên của bạn kém, vì nó có quá ít trạng thái bên trong - đầu ra số của hàm tại bất kỳ bước đã cho nào hoàn toàn phụ thuộc vào số trước đó. Chẳng hạn, nếu chúng ta giả sử đó magicNumberlà 2 (bằng ví dụ), thì chuỗi:

0.10 -> 0.20

được nhân đôi mạnh mẽ bởi các trình tự tương tự:

0.09 -> 0.18
0.11 -> 0.22

Trong nhiều trường hợp, điều này sẽ tạo ra các mối tương quan đáng chú ý trong trò chơi của bạn - ví dụ: nếu bạn thực hiện các cuộc gọi liên tiếp đến chức năng của mình để tạo tọa độ X và Y cho các đối tượng, các đối tượng sẽ tạo thành các mẫu đường chéo rõ ràng.

Trừ khi bạn có lý do chính đáng để tin rằng trình tạo số ngẫu nhiên đang làm chậm ứng dụng của bạn (và điều này rất khó xảy ra), không có lý do chính đáng nào để thử và tự viết.


36
+1 cho câu trả lời thực tế ... sử dụng điều này trong một cảnh quay và sinh ra kẻ thù dọc theo đường chéo cho nhiều cảnh quay hoành tráng? : D
wim

@wim: bạn không cần PRNG nếu bạn muốn những mẫu như vậy.
Nói dối Ryan

109

Vấn đề thực sự với điều này là biểu đồ đầu ra của nó phụ thuộc vào hạt giống ban đầu rất nhiều - phần lớn thời gian nó sẽ kết thúc với đầu ra gần như thống nhất nhưng rất nhiều thời gian sẽ có đầu ra không đồng nhất rõ ràng.

Lấy cảm hứng từ bài viết này về rand()chức năng của php tồi tệ như thế nào , tôi đã tạo ra một số hình ảnh ma trận ngẫu nhiên bằng cách sử dụng QuickRandomSystem.Random. Chạy này cho thấy đôi khi hạt giống có thể có tác động xấu (trong trường hợp này ủng hộ số lượng thấp hơn) trong đó System.Randomlà khá đồng đều.

QuickRandom

System.Random

Tệ hơn nữa

Nếu chúng ta khởi tạo QuickRandomkhi new QuickRandom(0.01, 1.03)chúng ta có được hình ảnh này:

Mật mã

using System;
using System.Drawing;
using System.Drawing.Imaging;

namespace QuickRandomTest
{
    public class QuickRandom
    {
        private double prevNum;
        private readonly double magicNumber;

        private static readonly Random rand = new Random();

        public QuickRandom(double seed1, double seed2)
        {
            if (seed1 >= 1 || seed1 < 0) throw new ArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
            prevNum = seed1;
            if (seed2 <= 1 || seed2 > 10) throw new ArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
            magicNumber = seed2;
        }

        public QuickRandom()
            : this(rand.NextDouble(), rand.NextDouble() * 10)
        {
        }

        public double Random()
        {
            return prevNum = (prevNum * magicNumber) % 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var rand = new Random();
            var qrand = new QuickRandom();
            int w = 600;
            int h = 600;
            CreateMatrix(w, h, rand.NextDouble).Save("System.Random.png", ImageFormat.Png);
            CreateMatrix(w, h, qrand.Random).Save("QuickRandom.png", ImageFormat.Png);
        }

        private static Image CreateMatrix(int width, int height, Func<double> f)
        {
            var bitmap = new Bitmap(width, height);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    var c = (int) (f()*255);
                    bitmap.SetPixel(x, y, Color.FromArgb(c,c,c));
                }
            }

            return bitmap;
        }
    }
}

2
Mã đẹp. Vâng, đó là mát mẻ. Tôi đã từng làm điều đó quá đôi khi, thật khó để có được một số đo có thể định lượng từ nó, nhưng đó là một cách tốt khác để xem xét trình tự. Và nếu bạn muốn xem các chuỗi dài hơn chiều rộng * chiều cao, bạn có thể xor hình ảnh tiếp theo với một pixel trên mỗi pixel này. Tôi nghĩ rằng hình ảnh QuickRandom đẹp hơn về mặt thẩm mỹ, vì nó được kết cấu như một tấm thảm rong biển.
Cris Stringfellow

Phần thẩm mỹ là cách trình tự có xu hướng tăng lên khi bạn đi dọc theo mỗi hàng (và sau đó quay lại bắt đầu lại) khi magicNumberphép nhân tạo ra một số tương tự prevNum, cho thấy sự thiếu ngẫu nhiên. Nếu chúng ta sử dụng hạt giống new QuickRandom(0.01, 1.03)thì chúng ta sẽ nhận được i.imgur.com/Q1Yunbe.png này !
Callum Rogers

Vâng, phân tích tuyệt vời. Vì nó chỉ nhân mod 1 với một hằng số rõ ràng trước khi gói xảy ra, sẽ có sự gia tăng mà bạn mô tả. Có vẻ như điều này có thể tránh được nếu chúng ta lấy các dấu thập phân ít quan trọng hơn bằng cách nhân với 1 tỷ sau đó giảm mod một bảng màu 256.
Cris Stringfellow

Bạn có thể cho tôi biết bạn đã sử dụng những gì để tạo ra những hình ảnh đầu ra? Matlab?
mai

@uDaY: Hãy xem mã, C # và System.Drawing.Bitmap.
Callum Rogers

37

Một vấn đề với trình tạo số ngẫu nhiên của bạn là không có 'trạng thái ẩn' - nếu tôi biết số ngẫu nhiên bạn đã trả về trong cuộc gọi cuối cùng, tôi biết mỗi số ngẫu nhiên duy nhất bạn sẽ gửi cho đến hết thời gian, vì chỉ có một kết quả tiếp theo có thể, và như vậy và vân vân.

Một điều khác cần xem xét là "thời gian" của trình tạo số ngẫu nhiên của bạn. Rõ ràng với kích thước trạng thái hữu hạn, bằng với phần mantissa của một đôi, nó sẽ chỉ có thể trả về tối đa 2 ^ 52 giá trị trước khi lặp. Nhưng đó là trong trường hợp tốt nhất - bạn có thể chứng minh rằng không có vòng lặp của giai đoạn 1, 2, 3, 4 ...? Nếu có, RNG của bạn sẽ có hành vi tồi tệ, suy đồi trong những trường hợp đó.

Ngoài ra, việc tạo số ngẫu nhiên của bạn có phân phối đồng đều cho tất cả các điểm bắt đầu không? Nếu không, RNG của bạn sẽ bị sai lệch - hoặc tệ hơn, sai lệch theo các cách khác nhau tùy thuộc vào hạt giống bắt đầu.

Nếu bạn có thể trả lời tất cả những câu hỏi này, thật tuyệt vời. Nếu bạn không thể, thì bạn sẽ biết tại sao hầu hết mọi người không phát minh lại bánh xe và sử dụng trình tạo số ngẫu nhiên đã được chứng minh;)

(Nhân tiện, một câu ngạn ngữ hay là: Mã nhanh nhất là mã không chạy. Bạn có thể tạo ngẫu nhiên () nhanh nhất trên thế giới, nhưng sẽ không tốt nếu nó không ngẫu nhiên lắm)


8
Có ít nhất một vòng lặp nhỏ trên máy phát này cho tất cả các hạt : 0 -> 0. Tùy thuộc vào hạt giống, có thể có nhiều người khác. (Ví dụ, với một hạt giống 3.0, 0.5 -> 0.5, 0.25 -> 0.75 -> 0.25, 0.2 -> 0.6 -> 0.8 -> 0.4 -> 0.2, vv)
duskwuff -inactive-

36

Một thử nghiệm phổ biến tôi luôn làm khi phát triển PRNG là:

  1. Chuyển đổi đầu ra thành giá trị char
  2. Viết giá trị ký tự vào một tệp
  3. Nén tập tin

Điều này cho phép tôi nhanh chóng lặp lại các ý tưởng PRNG "đủ tốt" cho các chuỗi khoảng 1 đến 20 megabyte. Nó cũng cho hình ảnh từ trên xuống tốt hơn là chỉ kiểm tra bằng mắt, vì bất kỳ PRNG "đủ tốt" nào với một nửa từ trạng thái có thể nhanh chóng vượt quá khả năng của bạn để nhìn thấy điểm chu kỳ.

Nếu tôi thực sự kén chọn, tôi có thể thực hiện các thuật toán tốt và chạy các bài kiểm tra DIEHARD / NIST trên chúng, để hiểu rõ hơn, sau đó quay lại và chỉnh sửa thêm một số thứ.

Ưu điểm của kiểm tra nén, trái ngược với phân tích tần số là, rất dễ để xây dựng một phân phối tốt: chỉ cần xuất ra một khối dài 256 có chứa tất cả các ký tự giá trị 0 - 255 và thực hiện điều này 100.000 lần. Nhưng chuỗi này có chu kỳ dài 256.

Một phân phối bị lệch, thậm chí bởi một lề nhỏ, nên được chọn bởi thuật toán nén, đặc biệt nếu bạn cung cấp đủ (giả sử 1 megabyte) của chuỗi để làm việc. Nếu một số ký tự hoặc bigram hoặc n-gram xảy ra thường xuyên hơn, thuật toán nén có thể mã hóa phân phối này thành các mã có lợi cho các lần xuất hiện thường xuyên với các từ mã ngắn hơn và bạn có được mức độ nén.

Vì hầu hết các thuật toán nén đều nhanh và chúng không yêu cầu triển khai (vì các hệ điều hành chỉ nằm ở đó), thử nghiệm nén là một thuật toán rất hữu ích để nhanh chóng đánh giá vượt qua / thất bại cho PRNG mà bạn có thể đang phát triển.

Chúc may mắn với các thí nghiệm của bạn!

Ồ, tôi đã thực hiện kiểm tra này trên rng bạn có ở trên, sử dụng mod nhỏ sau đây của mã của bạn:

import java.io.*;

public class QuickRandom {
    private double prevNum;
    private double magicNumber;

    public QuickRandom(double seed1, double seed2) {
        if (seed1 >= 1 || seed1 < 0) throw new IllegalArgumentException("Seed 1 must be >= 0 and < 1, not " + seed1);
        prevNum = seed1;
        if (seed2 <= 1 || seed2 > 10) throw new IllegalArgumentException("Seed 2 must be > 1 and <= 10, not " + seed2);
        magicNumber = seed2;
    }

    public QuickRandom() {
        this(Math.random(), Math.random() * 10);
    }

    public double random() {
        return prevNum = (prevNum*magicNumber)%1;
    }

    public static void main(String[] args) throws Exception {
        QuickRandom qr = new QuickRandom();
        FileOutputStream fout = new FileOutputStream("qr20M.bin");

        for (int i = 0; i < 20000000; i ++) {
            fout.write((char)(qr.random()*256));
        }
    }
}

Kết quả là:

Cris-Mac-Book-2:rt cris$ zip -9 qr20M.zip qr20M.bin2
adding: qr20M.bin2 (deflated 16%)
Cris-Mac-Book-2:rt cris$ ls -al
total 104400
drwxr-xr-x   8 cris  staff       272 Jan 25 05:09 .
drwxr-xr-x+ 48 cris  staff      1632 Jan 25 05:04 ..
-rw-r--r--   1 cris  staff      1243 Jan 25 04:54 QuickRandom.class
-rw-r--r--   1 cris  staff       883 Jan 25 05:04 QuickRandom.java
-rw-r--r--   1 cris  staff  16717260 Jan 25 04:55 qr20M.bin.gz
-rw-r--r--   1 cris  staff  20000000 Jan 25 05:07 qr20M.bin2
-rw-r--r--   1 cris  staff  16717402 Jan 25 05:09 qr20M.zip

Tôi sẽ xem xét một PRNG tốt nếu tập tin đầu ra không thể nén được. Thành thật mà nói, tôi không nghĩ PRNG của bạn sẽ làm tốt như vậy, chỉ 16% trên ~ 20 Megs là khá ấn tượng cho một công trình đơn giản như vậy. Nhưng tôi vẫn coi đó là một thất bại.


2
Hình ảnh đó hay không, tôi có cùng ý tưởng với zip năm trước khi tôi thử nghiệm các trình tạo ngẫu nhiên của mình.
Aristos

1
Cảm ơn @Alexandre C. và Aristos và Aidan. Tôi tin bạn.
Cris Stringfellow

33

Trình tạo ngẫu nhiên nhanh nhất bạn có thể thực hiện là:

nhập mô tả hình ảnh ở đây

XD, nói đùa, ngoài mọi thứ được nói ở đây, tôi muốn đóng góp trích dẫn rằng thử nghiệm các chuỗi ngẫu nhiên "là một nhiệm vụ khó khăn" [1], và có một số thử nghiệm kiểm tra các thuộc tính nhất định của các số giả ngẫu nhiên, bạn có thể tìm thấy rất nhiều trong số họ ở đây: http://www.random.org/analysis/#2005

Một cách đơn giản để đánh giá "chất lượng" của trình tạo ngẫu nhiên là kiểm tra Chi Square cũ.

static double chisquare(int numberCount, int maxRandomNumber) {
    long[] f = new long[maxRandomNumber];
    for (long i = 0; i < numberCount; i++) {
        f[randomint(maxRandomNumber)]++;
    }

    long t = 0;
    for (int i = 0; i < maxRandomNumber; i++) {
        t += f[i] * f[i];
    }
    return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
}

Trích dẫn [1]

Ý tưởng của phép thử χ² là kiểm tra xem các số được tạo ra có được trải ra hợp lý hay không. Nếu chúng ta tạo ra N số dương nhỏ hơn r , thì chúng ta sẽ mong nhận được về N / r số của mỗi giá trị. Nhưng --- và đây là bản chất của vấn đề --- tần số xuất hiện của tất cả các giá trị không nên giống hệt nhau: điều đó sẽ không ngẫu nhiên!

Chúng tôi chỉ đơn giản là tính tổng các bình phương của sự xuất hiện của từng giá trị, được chia tỷ lệ theo tần số dự kiến ​​và sau đó trừ đi kích thước của chuỗi. Con số này, "thống kê χ²", có thể được biểu thị dưới dạng toán học như

công thức bình phương

Nếu thống kê χ² gần với r , thì các số là ngẫu nhiên; nếu nó quá xa, thì họ không có. Các khái niệm "gần" và "ở xa" có thể được định nghĩa chính xác hơn: các bảng tồn tại cho biết chính xác mối liên hệ giữa các thống kê với các thuộc tính của các chuỗi ngẫu nhiên. Đối với bài kiểm tra đơn giản mà chúng tôi đang thực hiện, số liệu thống kê phải trong vòng 2√r

Sử dụng lý thuyết này và mã sau đây:

abstract class RandomFunction {
    public abstract int randomint(int range); 
}

public class test {
    static QuickRandom qr = new QuickRandom();

    static double chisquare(int numberCount, int maxRandomNumber, RandomFunction function) {
        long[] f = new long[maxRandomNumber];
        for (long i = 0; i < numberCount; i++) {
            f[function.randomint(maxRandomNumber)]++;
        }

        long t = 0;
        for (int i = 0; i < maxRandomNumber; i++) {
            t += f[i] * f[i];
        }
        return (((double) maxRandomNumber * t) / numberCount) - (double) (numberCount);
    }

    public static void main(String[] args) {
        final int ITERATION_COUNT = 1000;
        final int N = 5000000;
        final int R = 100000;

        double total = 0.0;
        RandomFunction qrRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (qr.random() * range);
            }
        }; 
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, qrRandomInt);
        }
        System.out.printf("Ave Chi2 for QR: %f \n", total / ITERATION_COUNT);        

        total = 0.0;
        RandomFunction mathRandomInt = new RandomFunction() {
            @Override
            public int randomint(int range) {
                return (int) (Math.random() * range);
            }
        };         
        for (int i = 0; i < ITERATION_COUNT; i++) {
            total += chisquare(N, R, mathRandomInt);
        }
        System.out.printf("Ave Chi2 for Math.random: %f \n", total / ITERATION_COUNT);
    }
}

Tôi đã nhận được kết quả sau:

Ave Chi2 for QR: 108965,078640
Ave Chi2 for Math.random: 99988,629040

Mà, đối với QuickRandom, cách xa r (bên ngoài r ± 2 * sqrt(r))

Điều đó đã được nói, QuickRandom có ​​thể nhanh nhưng (như đã nêu trong các câu trả lời khác) không tốt như một trình tạo số ngẫu nhiên


[1] SEDGEWICK ROBERT, Thuật toán trong C , Công ty xuất bản Addinson Wesley, 1990, trang 516 đến 518


9
+1 cho xkcd, một wobsite tuyệt vời (ồ, và câu trả lời tuyệt vời): P
tckmn

1
Cảm ơn, và có xkcd kệ! XD
higuaro

Lý thuyết là tốt nhưng thực thi là kém: mã dễ bị tràn số nguyên. Trong java tất cả int[]được khởi tạo về 0, vì vậy không cần phần này. Đúc để nổi là vô nghĩa khi bạn làm việc gấp đôi. Cuối cùng: gọi tên phương thức Random1 và Random2 khá buồn cười.
bestsss

@bestsss Cảm ơn các quan sát! Tôi đã thực hiện một bản dịch trực tiếp từ mã C và không chú ý đến nó = (. Tôi đã thực hiện một số sửa đổi và cập nhật câu trả lời. Tôi đánh giá cao bất kỳ đề xuất bổ sung nào
higuaro

14

Tôi kết hợp nhanh chóng thuật toán của bạn trong JavaScript để đánh giá kết quả. Nó tạo ra 100.000 số nguyên ngẫu nhiên từ 0 - 99 và theo dõi thể hiện của từng số nguyên.

Điều đầu tiên tôi nhận thấy là bạn có nhiều khả năng nhận được số thấp hơn số cao. Bạn thấy điều này nhiều nhất khi seed1cao và seed2thấp. Trong một vài trường hợp, tôi chỉ có 3 số.

Tốt nhất, thuật toán của bạn cần một số tinh chỉnh.


8

Nếu Math.Random()chức năng gọi hệ điều hành để lấy thời gian trong ngày, thì bạn không thể so sánh nó với chức năng của mình. Hàm của bạn là PRNG, trong khi đó hàm đó đang phấn đấu cho các số ngẫu nhiên thực sự. Táo và cam.

PRNG của bạn có thể nhanh, nhưng nó không có đủ thông tin trạng thái để đạt được một khoảng thời gian dài trước khi nó lặp lại (và logic của nó không đủ tinh vi để thậm chí đạt được các giai đoạn có thể với nhiều thông tin trạng thái đó).

Khoảng thời gian là độ dài của chuỗi trước khi PRNG của bạn bắt đầu lặp lại. Điều này xảy ra ngay khi máy PRNG thực hiện chuyển trạng thái sang trạng thái giống hệt với trạng thái trước đây. Từ đó, nó sẽ lặp lại quá trình chuyển đổi bắt đầu ở trạng thái đó. Một vấn đề khác với PRNG có thể là một số ít các chuỗi duy nhất, cũng như sự hội tụ suy biến trên một chuỗi cụ thể lặp lại. Cũng có thể có những mẫu không mong muốn. Ví dụ, giả sử rằng PRNG trông khá ngẫu nhiên khi các số được in ở dạng thập phân, nhưng việc kiểm tra các giá trị trong nhị phân cho thấy bit 4 chỉ đơn giản là chuyển từ 0 đến 1 trên mỗi cuộc gọi. Giáo sư!

Hãy xem Mersenne Twister và các thuật toán khác. Có nhiều cách để cân bằng giữa thời lượng và chu kỳ CPU. Một cách tiếp cận cơ bản (được sử dụng trong Mersenne Twister) là xoay quanh trong vectơ trạng thái. Điều đó có nghĩa là, khi một số được tạo ra, nó không dựa trên toàn bộ trạng thái, chỉ dựa trên một vài từ từ mảng trạng thái cho đến một vài thao tác bit. Nhưng ở mỗi bước, thuật toán cũng di chuyển xung quanh trong mảng, xáo trộn nội dung một chút.


5
Tôi chủ yếu đồng ý, ngoại trừ với đoạn đầu tiên của bạn. Các cuộc gọi ngẫu nhiên tích hợp (và / dev / ngẫu nhiên trên các hệ thống giống Unix) cũng là PRNG. Tôi sẽ gọi bất cứ thứ gì tạo ra các số ngẫu nhiên theo thuật toán là PRNG, ngay cả khi hạt giống là thứ khó dự đoán. Có một vài bộ tạo số ngẫu nhiên "thật" sử dụng phân rã phóng xạ, nhiễu khí quyển, v.v. nhưng chúng thường tạo ra tương đối ít bit / giây.
Matt Krause

Trên các hộp Linux, /dev/randomlà một nguồn ngẫu nhiên thực sự có được từ trình điều khiển thiết bị chứ không phải PRNG. Nó chặn khi không đủ bit có sẵn. Thiết bị chị em /dev/urandomcũng không chặn, nhưng nó vẫn không chính xác là PRNG vì nó được cập nhật với các bit ngẫu nhiên khi chúng có sẵn.
Kaz

Nếu hàm Math.Random () gọi hệ điều hành để lấy thời gian trong ngày - điều này hoàn toàn sai sự thật. (trong bất kỳ hương vị / phiên bản java nào tôi biết)
bestsss

@bestsss Đây là từ câu hỏi ban đầu: Tôi nhớ đã đọc ở đâu đó rằng Math.random đã sử dụng System.nanoTime () . Kiến thức của bạn có thể có giá trị thêm vào đó hoặc trong câu trả lời của bạn. Tôi đã sử dụng nó một cách có điều kiện với một nếu . :)
Kaz

Kaz, cả nanoTime()+ bộ đếm / hàm băm được sử dụng cho hạt giống mặc định java.util.Randomcủa oracle / OpenJDK. Đó chỉ là hạt giống, đó là LCG tiêu chuẩn. Trong thực tế, trình tạo OP lấy 2 số ngẫu nhiên cho hạt giống, điều này là ổn - vì vậy không có gì khác biệt hơn java.util.Random. System.currentTimeMillis()là hạt giống mặc định trong JDK1.4-
bestsss

7

Có rất nhiều, rất nhiều máy phát số ngẫu nhiên giả ra khỏi đó. Ví dụ của Knuth ranarray , các twister Mersenne , hoặc nhìn cho phát LFSR. "Thuật toán chuyên đề" hoành tráng của Knuth phân tích khu vực và đề xuất một số máy phát đồng quy tuyến tính (đơn giản để thực hiện, nhanh chóng).

Nhưng tôi khuyên bạn chỉ nên bám vào java.util.Randomhoặc Math.random, chúng nhanh và ít nhất là OK để sử dụng không thường xuyên (ví dụ: trò chơi và những thứ khác). Nếu bạn chỉ hoang tưởng về phân phối (một số chương trình Monte Carlo hoặc thuật toán di truyền), hãy kiểm tra việc triển khai của chúng (nguồn có sẵn ở đâu đó) và gieo chúng với một số thực sự ngẫu nhiên, từ hệ điều hành của bạn hoặc từ Random.org . Nếu điều này là bắt buộc đối với một số ứng dụng có bảo mật quan trọng, bạn sẽ phải tự đào. Và như trong trường hợp đó, bạn không nên tin vào những gì hình vuông có màu bị thiếu ở đây, tôi sẽ im lặng ngay bây giờ.


7

Rất khó có khả năng hiệu suất tạo số ngẫu nhiên sẽ là một vấn đề đối với bất kỳ trường hợp sử dụng nào mà bạn đã đưa ra trừ khi truy cập vào một phiên bản duy nhất Randomtừ nhiều luồng (vì Randomsynchronized).

Tuy nhiên, nếu đó thực sự là trường hợp và bạn cần rất nhiều số ngẫu nhiên nhanh chóng, giải pháp của bạn là quá không đáng tin cậy. Đôi khi nó cho kết quả tốt, đôi khi nó cho kết quả khủng khiếp (dựa trên các cài đặt ban đầu).

Nếu bạn muốn các số giống như Randomlớp cung cấp cho bạn, chỉ nhanh hơn, bạn có thể thoát khỏi sự đồng bộ hóa trong đó:

public class QuickRandom {

    private long seed;

    private static final long MULTIPLIER = 0x5DEECE66DL;
    private static final long ADDEND = 0xBL;
    private static final long MASK = (1L << 48) - 1;

    public QuickRandom() {
        this((8682522807148012L * 181783497276652981L) ^ System.nanoTime());
    }

    public QuickRandom(long seed) {
        this.seed = (seed ^ MULTIPLIER) & MASK;
    }

    public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27)) / (double)(1L << 53);
    }

    private int next(int bits) {
        seed = (seed * MULTIPLIER + ADDEND) & MASK;
        return (int)(seed >>> (48 - bits));
    }

}

Tôi chỉ đơn giản lấy java.util.Randommã và loại bỏ đồng bộ hóa mà hiệu suất gấp đôi so với bản gốc trên Oracle HotSpot JVM 7u9 của tôi. Nó vẫn chậm hơn so với của bạn QuickRandom, nhưng nó cho kết quả phù hợp hơn nhiều. Nói chính xác, đối với cùng seedcác giá trị và các ứng dụng luồng đơn, nó cung cấp các số giả ngẫu nhiên giống như Randomlớp ban đầu .


Mã này dựa trên hiện tại java.util.Randomtrong OpenJDK 7u , được cấp phép theo GNU GPL v2 .


EDIT 10 tháng sau:

Tôi mới phát hiện ra rằng bạn thậm chí không phải sử dụng mã của tôi ở trên để có được một Randomcá thể không đồng bộ . Có một cái trong JDK nữa!

Nhìn vào ThreadLocalRandomlớp của Java 7 . Mã bên trong nó gần giống với mã của tôi ở trên. Lớp này chỉ đơn giản là một Randomphiên bản phân tách luồng cục bộ phù hợp để tạo số ngẫu nhiên một cách nhanh chóng. Nhược điểm duy nhất tôi có thể nghĩ là bạn không thể đặt seedthủ công.

Ví dụ sử dụng:

Random random = ThreadLocalRandom.current();

2
@Edit Hmm, đôi khi tôi có thể so sánh QR, Math.random và ThreadLocalRandom khi tôi không quá lười biếng :)Điều đó thật thú vị, cảm ơn!
tckmn

1. Bạn có thể tăng thêm một số tốc độ bằng cách thả mặt nạ vì 16 bit cao nhất không ảnh hưởng đến các bit đã sử dụng. 2. Bạn có thể sử dụng các bit đó, lưu một phép trừ và có được một trình tạo tốt hơn (trạng thái lớn hơn; các bit quan trọng nhất của sản phẩm được phân phối độc đáo nhất, nhưng sẽ cần một số đánh giá). 3. Những kẻ mặt trời chỉ đơn giản thực hiện một RNG cổ xưa bằng Knuth và thêm đồng bộ hóa. :(
maaartinus

3

'Ngẫu nhiên' không chỉ đơn thuần là nhận số .... những gì bạn có là giả ngẫu nhiên

Nếu giả ngẫu nhiên đủ tốt cho mục đích của bạn, thì chắc chắn, nó nhanh hơn (và XOR + Bitshift sẽ nhanh hơn những gì bạn có)

Rolf

Biên tập:

OK, sau khi quá vội vàng trong câu trả lời này, hãy để tôi trả lời lý do thực sự tại sao mã của bạn nhanh hơn:

Từ JavaDoc cho Math.Random ()

Phương pháp này được đồng bộ hóa đúng để cho phép sử dụng đúng bởi nhiều hơn một luồng. Tuy nhiên, nếu nhiều luồng cần tạo số giả ngẫu nhiên với tốc độ lớn, điều đó có thể làm giảm sự tranh chấp cho mỗi luồng để có trình tạo số giả ngẫu nhiên riêng.

Đây có thể là lý do tại sao mã của bạn nhanh hơn.


3
Khá nhiều thứ không liên quan đến bộ tạo nhiễu phần cứng hoặc đường truyền trực tiếp vào công cụ I / O của HĐH, sẽ là giả ngẫu nhiên. Tính ngẫu nhiên thực sự không thể được tạo ra bởi một thuật toán một mình; bạn cần tiếng ồn từ đâu đó (RNGs Một số hệ điều hành get đầu vào của họ bằng cách đo những thứ như thế nào / khi bạn di chuyển chuột, loại thứ vv đo trên thang điểm từ micro để nano giây, có thể được đánh giá cao không thể đoán trước.)
Chao

@OliCharlesworth: thực sự, theo như tôi biết thì chỉ có các giá trị ngẫu nhiên thực sự được tìm thấy bằng cách sử dụng tiếng ồn trong khí quyển.
Jeroen Vannevel

@ tôi ... ngu ngốc trả lời vội vàng. Math.random là giả danh, và đồng thời, nó được đồng bộ hóa .
rolfl

@rolfl: Đồng bộ hóa có thể giải thích rất rõ tại sao Math.random()chậm hơn. Nó sẽ phải đồng bộ hóa hoặc tạo một cái mới Randommỗi lần, và cả hai đều không hấp dẫn về mặt hiệu suất. Nếu tôi quan tâm đến hiệu suất, tôi sẽ tự tạo new Randomvà chỉ sử dụng nó. : P
cHao

@JeroenVannevel phân rã phóng xạ là ngẫu nhiên quá.
RxS

3

java.util.Random không khác nhiều, một LCG cơ bản được mô tả bởi Knuth. Tuy nhiên, nó có 2 ưu điểm / khác biệt chính:

  • chuỗi an toàn - mỗi bản cập nhật là một CAS đắt hơn một lần viết đơn giản và cần một nhánh (ngay cả khi dự đoán hoàn toàn một luồng). Tùy thuộc vào CPU, nó có thể là sự khác biệt đáng kể.
  • trạng thái nội bộ không được tiết lộ - điều này rất quan trọng đối với bất cứ điều gì không tầm thường. Bạn muốn những con số ngẫu nhiên không thể dự đoán được.

Bên dưới đó là thói quen chính tạo ra các số nguyên 'ngẫu nhiên' trong java.util.Random.


  protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
          oldseed = seed.get();
          nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

Nếu bạn loại bỏ AtomicLong và sate không được tiết lộ (nghĩa là sử dụng tất cả các bit của long), bạn sẽ nhận được hiệu suất cao hơn so với phép nhân / modulo kép.

Lưu ý cuối cùng: Math.randomkhông nên được sử dụng cho bất cứ điều gì ngoại trừ các thử nghiệm đơn giản, nó dễ bị tranh chấp và nếu bạn thậm chí có một vài luồng gọi nó đồng thời thì hiệu suất sẽ giảm. Một đặc điểm lịch sử ít được biết đến của nó là giới thiệu CAS trong java - để đánh bại một chuẩn mực khét tiếng (đầu tiên là bởi IBM thông qua nội tại và sau đó Sun đã tạo ra "CAS từ Java")


0

Đây là chức năng ngẫu nhiên tôi sử dụng cho các trò chơi của mình. Nó khá nhanh và có phân phối (đủ) tốt.

public class FastRandom {

    public static int randSeed;

      public static final int random()
      {
        // this makes a 'nod' to being potentially called from multiple threads
        int seed = randSeed;

        seed    *= 1103515245;
        seed    += 12345;
        randSeed = seed;
        return seed;
      }

      public static final int random(int range)
      {
        return ((random()>>>15) * range) >>> 17;
      }

      public static final boolean randomBoolean()
      {
         return random() > 0;
      }

       public static final float randomFloat()
       {
         return (random()>>>8) * (1.f/(1<<24));
       }

       public static final double randomDouble() {
           return (random()>>>8) * (1.0/(1<<24));
       }
}

1
Điều này không cung cấp một câu trả lời cho câu hỏi. Để phê bình hoặc yêu cầu làm rõ từ một tác giả, hãy để lại nhận xét bên dưới bài đăng của họ.
John Willemse

Tôi nghĩ rằng nó đã được thiết lập rằng thuật toán ban đầu là không đủ tốt? Có lẽ một ví dụ về những gì đủ tốt có thể dẫn đến cảm hứng về cách cải thiện nó?
Terje

Có, có thể, nhưng nó hoàn toàn không trả lời câu hỏi và không có dữ liệu hỗ trợ thuật toán của bạn thực sự "đủ tốt". Nói chung, các thuật toán số ngẫu nhiên và các thuật toán mã hóa liên quan chặt chẽ không bao giờ tốt như các thuật toán của các chuyên gia đã triển khai chúng trong ngôn ngữ lập trình. Vì vậy, nếu bạn có thể hỗ trợ cho yêu cầu của mình và giải thích lý do tại sao nó tốt hơn thuật toán trong Câu hỏi, ít nhất bạn sẽ trả lời một câu hỏi được hỏi.
John Willemse

Chà ... Các chuyên gia thực hiện chúng trong một ngôn ngữ lập trình nhằm phân phối "hoàn hảo", trong khi trong một trò chơi, bạn không bao giờ cần điều đó. Bạn muốn tốc độ và phân phối "đủ tốt". Mã này cung cấp điều này. Nếu nó không phù hợp ở đây, tôi sẽ xóa câu trả lời, không vấn đề gì.
Terje

Liên quan đến đa luồng, việc sử dụng biến cục bộ của bạn là không có, vì nếu không volatile, trình biên dịch có thể tự do loại bỏ (hoặc giới thiệu) các biến cục bộ theo ý muốn.
maaartinus
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.