Tại sao giá trị ngẫu nhiên này có phân phối 25/75 thay vì 50/50?


139

Chỉnh sửa: Vì vậy, về cơ bản những gì tôi đang cố gắng viết là hàm băm 1 bit double.

Tôi muốn ánh xạ doubletới truehoặc falsecó cơ hội 50/50. Vì tôi đã viết mã chọn một số số ngẫu nhiên (như một ví dụ, tôi muốn sử dụng mã này trên dữ liệu một cách đều đặn và vẫn nhận được kết quả 50/50) , kiểm tra bit cuối cùng của chúng và tăng ynếu là 1 hoặc nnếu là 0.

Tuy nhiên, mã này liên tục cho kết quả 25% yvà 75% n. Tại sao nó không phải là 50/50? Và tại sao một phân phối kỳ lạ, nhưng thẳng (1/3) như vậy?

public class DoubleToBoolean {
    @Test
    public void test() {

        int y = 0;
        int n = 0;
        Random r = new Random();
        for (int i = 0; i < 1000000; i++) {
            double randomValue = r.nextDouble();
            long lastBit = Double.doubleToLongBits(randomValue) & 1;
            if (lastBit == 1) {
                y++;
            } else {
                n++;
            }
        }
        System.out.println(y + " " + n);
    }
}

Ví dụ đầu ra:

250167 749833

43
Tôi thực sự hy vọng câu trả lời là một cái gì đó hấp dẫn về việc tạo ra các biến thiên điểm ngẫu nhiên, thay vì "LCG có entropy thấp trong các bit thấp".
Sneftel

4
Tôi rất tò mò, mục đích của "băm 1 bit cho nhân đôi" là gì? Tôi thực sự không thể nghĩ ra bất kỳ ứng dụng hợp pháp nào của yêu cầu đó.
corsiKa

3
@corsiKa Trong các tính toán hình học thường có hai trường hợp chúng tôi đang tìm cách chọn từ hai câu trả lời có thể (ví dụ: chỉ về bên trái hoặc bên phải của dòng?), và đôi khi nó đưa ra trường hợp suy biến thứ ba (điểm là ngay trên dòng), nhưng bạn chỉ có hai câu trả lời có sẵn, vì vậy bạn phải giả danh chọn một trong những câu trả lời có sẵn trong trường hợp đó. Cách tốt nhất tôi có thể nghĩ đến là lấy hàm băm 1 bit của một trong các giá trị kép đã cho (hãy nhớ, đó là các tính toán hình học, do đó, có gấp đôi ở mọi nơi).
gvlasov

2
@corsiKa (bình luận chia làm hai vì quá dài) Chúng ta có thể bắt đầu ở một cái gì đó đơn giản hơn doubleValue % 1 > 0.5, nhưng điều đó sẽ quá thô vì nó có thể đưa ra các quy tắc có thể nhìn thấy trong một số trường hợp (tất cả các giá trị nằm trong phạm vi độ dài 1). Nếu đó là quá thô, thì có lẽ chúng ta nên thử phạm vi nhỏ hơn, như thế doubleValue % 1e-10 > 0.5e-10nào? Vâng, vâng. Và chỉ lấy bit cuối cùng làm hàm băm doublelà điều xảy ra khi bạn làm theo phương pháp này cho đến khi kết thúc, với modulo ít nhất có thể.
gvlasov

1
@kmote sau đó bạn vẫn có bit sai lệch đáng kể nhất và bit kia không bù cho nó - thực tế nó cũng thiên về 0 (nhưng ít hơn), vì lý do chính xác như vậy. Vì vậy, phân phối sẽ là khoảng 50, 12,5, 25, 12,5. (lastbit & 3) == 0sẽ làm việc mặc dù, lẻ như nó là.
harold

Câu trả lời:


165

Bởi vì nextDouble hoạt động như thế này: ( nguồn )

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

next(x)làm cho xcác bit ngẫu nhiên.

Bây giờ tại sao điều này lại quan trọng? Bởi vì khoảng một nửa số được tạo bởi phần đầu tiên (trước khi phân chia) nhỏ hơn 1L << 52, và do đó, ý nghĩa của chúng không hoàn toàn lấp đầy 53 bit mà nó có thể điền vào, có nghĩa là bit có ý nghĩa nhỏ nhất luôn luôn bằng không đối với những số đó.


Do mức độ chú ý mà điều này nhận được, đây là một số giải thích thêm về những gì doubletrong Java (và nhiều ngôn ngữ khác) thực sự trông như thế nào và tại sao nó lại quan trọng trong câu hỏi này.

Về cơ bản, một doublecái nhìn như thế này: ( nguồn )

bố trí đôi

Một chi tiết rất quan trọng không thể nhìn thấy trong hình này là các số được "chuẩn hóa" 1 sao cho phân số 53 bit bắt đầu bằng 1 (bằng cách chọn số mũ sao cho như vậy), sau đó 1 bị bỏ qua. Đó là lý do tại sao hình ảnh hiển thị 52 bit cho phân số (có ý nghĩa) nhưng có hiệu quả 53 bit trong đó.

Chuẩn hóa có nghĩa là nếu trong mã cho nextDoublebit thứ 53 được đặt, bit đó là đầu 1 ẩn và nó biến mất, và 52 bit còn lại được sao chép theo nghĩa đen của kết quả double. Tuy nhiên, nếu bit đó không được đặt, các bit còn lại phải được dịch chuyển sang trái cho đến khi nó được đặt.

Trung bình, một nửa số được tạo rơi vào trường hợp số ý nghĩa không bị dịch chuyển trái (và khoảng một nửa số có 0 là bit có ý nghĩa nhỏ nhất của chúng) và nửa còn lại được dịch chuyển ít nhất 1 (hoặc hoàn toàn thay đổi không) vì vậy bit có ý nghĩa nhỏ nhất của chúng luôn là 0.

1: không phải luôn luôn, rõ ràng không thể thực hiện bằng 0, không có giá trị cao nhất 1. Những số này được gọi là số không bình thường hoặc số không bình thường, xem wikipedia: số không bình thường .


16
Hoan hô! Chỉ là những gì tôi đã hy vọng.
Sneftel

3
@Matt Có lẽ đó là một tối ưu hóa tốc độ. Thay thế sẽ là tạo ra số mũ với phân bố hình học, và sau đó là lớp phủ riêng biệt.
Sneftel

7
@Matt: Xác định "tốt nhất". random.nextDouble()thường là cách "tốt nhất" cho mục đích của nó, nhưng hầu hết mọi người không cố gắng tạo ra hàm băm 1 bit từ gấp đôi ngẫu nhiên của họ. Bạn đang tìm kiếm sự phân phối đồng đều, khả năng chống lại tiền điện tử, hay gì?
StriplingWar Warrior

1
Câu trả lời này cho thấy rằng nếu OP đã nhân số ngẫu nhiên với 2 ^ 53 và kiểm tra xem số nguyên kết quả có phải là số lẻ hay không, thì sẽ có phân phối 50/50.
rici

4
@ The111 nó nói ở đâynextphải trả lại int, vì vậy nó chỉ có thể có tối đa 32 bit nào
harold

48

Từ các tài liệu :

Phương thức nextDouble được lớp Thực hiện ngẫu nhiên như thể bởi:

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

Nhưng nó cũng nêu sau đây (nhấn mạnh của tôi):

[Trong các phiên bản đầu của Java, kết quả được tính không chính xác là:

 return (((long)next(27) << 27) + next(27))
     / (double)(1L << 54);

Điều này có vẻ tương đương, nếu không tốt hơn, nhưng trên thực tế, nó đã đưa ra một sự không đồng nhất lớn do sự sai lệch trong cách làm tròn các số có dấu phẩy động: có khả năng gấp ba lần số bit bậc thấp của số có nghĩa là 0 hơn thế nó sẽ là 1 ! Sự không đồng nhất này có lẽ không quan trọng lắm trong thực tế, nhưng chúng tôi cố gắng hoàn thiện.]

Lưu ý này đã có từ khi Java 5 ít nhất (tài liệu cho Java <= 1.4 nằm sau một tường đăng nhập, quá lười để kiểm tra). Điều này thật thú vị, vì vấn đề rõ ràng vẫn còn tồn tại ngay cả trong Java 8. Có lẽ phiên bản "đã sửa" chưa bao giờ được thử nghiệm?


4
Lạ thật. Tôi vừa sao chép lại điều này trên Java 8.
aioobe

1
Bây giờ điều đó thật thú vị, bởi vì tôi chỉ lập luận rằng sự thiên vị vẫn áp dụng cho phương pháp mới. Liệu tôi có sai?
harold

3
@harold: Không, tôi nghĩ bạn đúng và bất cứ ai cố gắng khắc phục sai lệch này có thể đã phạm sai lầm.
Thomas

6
@harold Thời gian để gửi email đến các chàng trai Java.
Daniel

8
"Có lẽ phiên bản cố định chưa bao giờ được thử nghiệm?" Trên thực tế, khi đọc lại điều này, tôi nghĩ rằng tài liệu là về một vấn đề khác. Lưu ý rằng nó đề cập đến làm tròn , điều này cho thấy rằng họ không coi "vấn đề gấp ba lần" là vấn đề, nhưng điều này dẫn đến phân phối không đồng đều khi các giá trị được làm tròn . Lưu ý rằng trong câu trả lời của tôi, các giá trị tôi liệt kê được phân phối đồng đều, nhưng bit thứ tự thấp như được trình bày ở định dạng IEEE không đồng nhất. Tôi nghĩ rằng vấn đề họ đã sửa phải liên quan đến tính đồng nhất tổng thể, chứ không phải tính đồng nhất của bit thấp.
ajb

33

Kết quả này không làm tôi ngạc nhiên khi biết các số dấu phẩy động được biểu diễn như thế nào. Giả sử chúng ta có một loại dấu phẩy động rất ngắn chỉ với 4 bit chính xác. Nếu chúng ta tạo một số ngẫu nhiên từ 0 đến 1, được phân phối đồng đều, sẽ có 16 giá trị có thể:

0.0000
0.0001
0.0010
0.0011
0.0100
...
0.1110
0.1111

Nếu đó là cách họ nhìn trong máy, bạn có thể kiểm tra bit thứ tự thấp để có phân phối 50/50. Tuy nhiên, phao IEEE được thể hiện như một sức mạnh gấp 2 lần so với lớp phủ; một trường trong float là lũy thừa của 2 (cộng với phần bù cố định). Sức mạnh của 2 được chọn sao cho phần "mantissa" luôn là một số> = 1.0 và <2.0. Điều này có nghĩa là, trên thực tế, các số khác hơn 0.0000sẽ được biểu diễn như thế này:

0.0001 = 2^(-4) x 1.000
0.0010 = 2^(-3) x 1.000
0.0011 = 2^(-3) x 1.100
0.0100 = 2^(-2) x 1.000
... 
0.0111 = 2^(-2) x 1.110
0.1000 = 2^(-1) x 1.000
0.1001 = 2^(-1) x 1.001
...
0.1110 = 2^(-1) x 1.110
0.1111 = 2^(-1) x 1.111

(Điểm 1trước nhị phân là một giá trị ngụ ý; đối với số float 32 và 64 bit, thực tế không có bit nào được phân bổ để giữ giá trị này 1.)

Nhưng nhìn vào phần trên sẽ chứng minh tại sao, nếu bạn chuyển đổi biểu diễn thành bit và nhìn vào bit thấp, bạn sẽ nhận được không 75% thời gian. Điều này là do tất cả các giá trị nhỏ hơn 0,5 (nhị phân 0.1000), bằng một nửa các giá trị có thể, có mantissas của chúng bị dịch chuyển, làm cho 0 xuất hiện trong bit thấp. Tình huống về cơ bản là giống nhau khi lớp phủ có 52 bit (không bao gồm hàm 1) như doublehiện tại.

(Trên thực tế, như @sneftel đã đề xuất trong một nhận xét, chúng tôi có thể bao gồm hơn 16 giá trị có thể có trong phân phối, bằng cách tạo:

0.0001000 with probability 1/128
0.0001001 with probability 1/128
...
0.0001111 with probability 1/128
0.001000  with probability 1/64
0.001001  with probability 1/64
...
0.01111   with probability 1/32 
0.1000    with probability 1/16
0.1001    with probability 1/16
...
0.1110    with probability 1/16
0.1111    with probability 1/16

Nhưng tôi không chắc đó là loại phân phối mà hầu hết các lập trình viên mong đợi, vì vậy nó có thể không đáng giá. Thêm vào đó, nó không mang lại cho bạn nhiều khi các giá trị được sử dụng để tạo số nguyên, như các giá trị dấu phẩy động ngẫu nhiên thường là.)


5
Sử dụng dấu phẩy động để nhận bit / byte / ngẫu nhiên bất cứ điều gì làm tôi rùng mình hơn. Ngay cả đối với các phân phối ngẫu nhiên giữa 0 và n, chúng tôi có các lựa chọn thay thế tốt hơn (xem arc4random_uniform) so với ngẫu nhiên * n
mirabilos
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.