트랙
/
Julia
Julia
/
학습 과정
/
무작위성
무작

무작위성 에서 Julia

2개의 연습 문제

무작위성 소개

많은 프로그램은 실제 세계의 사건을 시뮬레이션하기 위해 (겉보기에는) 무작위 값이 필요해요.

흔히 접하는 예로는 다음과 같은 것들이 있어요.

  • 동전 던지기: ('H', 'T')에서 하나를 무작위로 고르기.
  • 주사위 굴리기: 1부터 6까지의 무작위 정수.
  • 카드 한 벌 섞기: 카드 목록의 무작위 순서.

컴퓨터로 진짜 무작위 값을 만들어 내는 것은 놀랄 만큼 어려운 기술적 과제라서, 이런 결과를 두고 "의사난수"라고 부르는 걸 볼 수 있어요.

중요: 이 개념에서는 암호학적으로 안전한 난수를 다루지 않아요. 그건 훨씬 더 어려운 과제예요.

하지만 Julia 표준 라이브러리의 Random 모듈처럼 잘 설계된 라이브러리는 빠르고 유연하며, 모델링, 시뮬레이션, 게임 등 대부분의 응용 분야에 충분히 좋은 결과를 줘요.

Julia는 무작위 기능을 여러 위치에 나눠 두었어요.

  • Base에 있는 몇 안 되는 기본 함수지만 매우 다재다능한 함수들로, 항상 사용할 수 있어요.
  • Random 모듈에 있는 더 다양한 옵션.
  • 사용하기 전에 설치해야 하는 패키지에 있는 더 특화된 기능 (Exercism에서는 사용할 수 없어요).

Random은 표준 라이브러리의 일부이고 대부분 미리 설치되어 있지만, 그 내용을 네임스페이스로 가져오려면 프로그램 맨 위에 using Random을 추가해야 해요.

rand() 함수

이 함수가 하는 일은 어떤 인자를 주느냐에 따라 달라져요. 선택할 수 있는 방법이 아주 많아요.

인자 없이 호출하면 0 (포함)과 1 사이의 실수를 만들어요. 이것은 모든 값이 똑같이 나올 가능성이 있는 uniform 분포예요. 아래의 "분포 다루기" 부분에서 설명해요.

정수 인자 하나를 주면 그 길이만큼의 벡터를 만들어요.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

다른 범위가 필요하면 결과를 적절히 이동하고 크기를 조정하면 돼요.

아래 예제는 뺄셈에 브로드캐스팅을 사용해요. 이는 벡터 연산 개념에서 다뤄요. .-는 이 연산을 각 벡터 원소에 적용할 뿐이에요.

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

인자로 타입 하나만 주면 rand는 typemin과 typemax를 한계로 사용해요. 아마 원하는 결과가 아닐 거예요!

무작위 정수가 필요하면 범위를 주고, 원한다면 생성할 값의 개수도 함께 줄 수 있어요.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

위의 rand(1:10, 5) 예제에서는 (우연히) 같은 값이 반복되는 걸 볼 수 있는데, 각 선택이 독립적이기 때문이에요. 이것이 "복원 추출"이고, 아래에서 더 자세히 다뤄요.

범위 안의 실수 값을 얻으려면 보통 단계 크기를 지정해야 해요. 그렇지 않으면 단계가 기본값 1.0이 되는데, 이게 쓸모 있는 경우는 거의 없어요.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

또는 배열이나 튜플을 주면 rand가 무작위로 항목 하나를 반환해요.

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

복원 추출과 비복원 추출

빨간 공 3개와 초록 공 4개가 든 주머니에서 공 하나를 무작위로 꺼낸다고 상상해 봐요. 두 번째 공을 꺼내는 방법은 두 가지예요.

  1. 첫 번째 공을 주머니에 다시 넣고 잘 흔든 다음 하나를 더 꺼내요. 이제 공의 개수는 이전과 같은 7개이고, 빨간 공과 초록 공의 비율도 그대로예요.
  2. 첫 번째 공을 탁자 위에 두고 두 번째 공을 꺼내요. 이제 주머니에는 공이 6개뿐이고, 빨간 공과 초록 공의 비율은 첫 번째 공의 색에 따라 달라져요.

1번은 복원 추출이고 2번은 비복원 추출이며, 두 방법은 서로 다른 결과를 줘요.

Julia에서 비복원 추출을 시뮬레이션하는 방법은 몇 가지가 있어요.

가장 간단한 방법(그리고 Exercism에서는 유일한 방법)은 Random.shuffle()로 항목을 무작위 순서로 만든 다음, 처음 n개 원소를 사용하는 거예요. 작은 문제에는 괜찮지만 큰 컬렉션에는 잘 맞지 않을 수 있어요. 일부만 필요해도 shuffle은 전체 배열을 만들어야 하거든요.

복원 추출을 "제대로" 하려면 StatsBase.jl 패키지를 설치해요. 그 패키지가 다양한 옵션을 갖춘 sample() 함수를 제공해요.

비슷한 기능이 앞으로 Random에 추가되어 표준 라이브러리의 일부가 되기를 기대해 볼 만해요 (이 문서의 코드 예제는 Julia 1.11에서 테스트했어요).

분포 다루기

지금까지는 모든 결과가 똑같이 나올 가능성이 있는 경우에 집중했어요. 예를 들어 rand(1:100)은 1부터 100까지의 어떤 정수든 똑같은 확률로 줘요.

실제 세계의 많은 상황은 이보다 훨씬 단순하지 않아요. 그래서 통계학자들은 "실제 세계"의 결과를 수학적으로 설명하기 위해 아주 다양한 distributions를 만들어 냈어요.

균등 분포

위에서 설명한 rand() 함수는 모든 확률이 같을 때 사용해요. 이것을 [uniform][uniform-distribution] 분포라고 해요.

가우스 분포

"정규" 분포 또는 "종 모양" 곡선이라고도 하는데, 측정값의 부정확함을 설명하는 아주 흔한 방법이에요.

예를 들어, 다니는 공장에서 똑같아야 하는 볼트 10,000개를 막 사 왔다고 해요. 이 볼트를 다루도록 공장 로봇을 설정하려고 100개를 표본으로 달아 봤더니 평균(또는 mean) 무게가 4.731g이었어요. 그렇다고 해서 볼트가 모두 정확히 4.731g이라는 뜻은 전혀 아니에요. 아마 값이 4.627g에서 4.794g 사이에 분포하지만 4.731g 근처에 몰려 있을 거예요.

이것이 Gaussian distribution이고, 평균에서 확률이 가장 높고 양쪽으로 대칭적으로 줄어들어요 (그래서 "종 모양"이에요). 소프트웨어에서 이것을 시뮬레이션하려면 곡선의 폭을 지정할 방법이 필요해요 (보통 비싼 볼트가 싼 볼트보다 평균 근처에 더 촘촘히 몰려 있어요!).

관례적으로 standard deviation을 사용하는데, 값이 작으면 뾰족하고 좁은 곡선, 값이 크면 낮고 넓은 곡선이 돼요. 수학자들은 그리스 문자를 좋아해서, 평균은 μ('뮤'), 표준 편차는 σ('시그마')로 나타내요. 그래서 "값의 95%가 μ에서 2σ 이내에 있다"거나 "힉스 보손이 5시그마 신뢰도로 검출되었다"는 글을 읽었다면, 이런 표현은 표준 편차와 관련된 거예요.

이 내용은 Statistics 개념에서 더 다뤄요.

randn() 함수

"random normal"의 줄임말로, 값이 평균 0, 표준 편차 1인 가우스 분포를 따르는 점만 빼면 rand()의 실수 버전과 비슷해요.

마찬가지로 randn이 내놓는 원래 값에 표준 편차를 곱하고 평균만큼 옮기고 싶을 수 있어요. 아래 예제는 평균 30, 표준 편차 5로 바꿔요.

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

출력만 봐서는 원래 값이 균등 분포보다 0 근처에 더 몰려 있다는 걸 알아내기 어려워요. 믿기 어렵다면 1000개 이상을 만들어 그래프로 그려 보면 더 분명해져요.

Random 모듈

이 모듈에는 다음 단계의 기능이 들어 있어요. Julia 기본 구성의 크기를 줄이기 위해 Base에서는 빠져 있어요.

Random은 Base의 rand와 randn에 제자리에서 값을 바꾸는 rand!와 randn!을 추가로 제공해요.

쓸모 있는 추가 기능으로 randstring이 있는데, 주어진 길이의 문자열을 만들어요. 기본적으로 대문자와 소문자, 그리고 숫자 0부터 9까지를 사용하지만, 다른 문자를 지정할 수도 있어요.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

또한 지정한 길이의 무작위 BitArray를 만드는 bitrand 함수도 있어요.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

섞기와 순열

Vector의 항목을 무작위로 섞으려면 shuffle을 쓰면 돼요. 입력 벡터를 제자리에서 바꾸는 shuffle!도 있어요.

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

때로는 섞인 결과 대신 섞인 인덱스가 필요할 때가 있어요. 그럴 때는 randperm(n)을 사용하는데, 여기서 n은 시퀀스의 길이예요.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

사실상 위 예제는 shuffle(1:6)과 같은 결과를 줘요.

관련 함수로는 고정된 확률로 항목을 뽑아내는 randsubseq와 순환 순열을 위한 randcycle이 있어요. 이런 함수들은 좀 특화된 지식이 필요하니, 관심 있다면 문서를 확인해 봐요.

시드와 알고리즘

여러 난수 생성기(RNG) 알고리즘이 Random에 기본으로 들어 있고, 알맞은 수학 실력만 있으면 누구나 더 추가할 수 있어요. 그런 건 이 문서의 범위를 한참 벗어나요!

RNG를 다루는 더 흔한 이유는 seed를 지정하는 건데, 그러면 "무작위" 출력의 나열을 다음 실행에서도 똑같이 재현할 수 있어요.

이런 재현성은 프로덕션 코드에는 적합하지 않지만, 테스트와 디버깅에는 도움이 돼요.

그 밖의 패키지

Exercism 밖에는 무작위성, 확률, 통계와 관련된 설치 가능한 패키지가 많이 있어요. 더 자세한 내용은 Statistics 개념을 참고해요.

StatsBase.jl 패키지

StatsBase의 함수 대부분은 상당히 전문적이라 이 문서와는 관련이 없어요.

예외는 StatsBase.sample인데, 복원 추출과 비복원 추출을 완전히 구현해 줘요 (위의 앞부분 참고). 가중치를 둔 (비균등) 추출을 위한 함수도 있어요.

Distributions.jl 패키지

uniform 분포와 normal(또는 가우스) 분포는 위에서 설명했어요.

Random 모듈에는 지수 분포에서 추출하는 randexp도 있는데, 지수 분포는 (아주 흔한) 푸아송 분포와 관련이 있어요.

훨씬 다양한 선택지가 필요하면 통계학 배경이 있는 분들을 위한 Distributions.jl 패키지가 있어요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

무작위성 배우기

연습이 잠겨 있어요

무작위성 개념을 연습하려면 연습 문제 1개를 더 잠금 해제해요