많은 프로그램은 실제 세계의 사건을 시뮬레이션하기 위해 (겉보기에) 무작위한 값이 필요해요.
흔하고 익숙한 예로는 다음과 같은 것들이 있어요:
('H', 'T')에서 무작위로 고른 값.컴퓨터로 진짜 무작위한 값을 생성하는 것은 놀랄 만큼 어려운 기술적 과제라서, 이런 결과를 "의사 난수"라고 부르는 것을 볼 수 있어요.
중요: 이 개념은 암호학적으로 안전한 난수를 다루지 않아요. 그건 훨씬 더 어려운 과제예요.
하지만 Julia 표준 라이브러리의 Random 모듈처럼 잘 설계된 라이브러리는 빠르고 유연하며, 모델링, 시뮬레이션, 게임 같은 대부분의 응용 분야에 충분히 좋은 결과를 줘요.
Julia는 무작위 기능을 여러 위치에 나누어 두었어요:
Base에는 항상 사용할 수 있는, 기본적이지만 아주 다재다능한 몇 가지 함수만 있어요.Random 모듈에는 더 다양한 선택지가 있어요.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은 복원 추출, 시나리오 2는 비복원 추출이며, 둘은 서로 다른 결과를 줘요.
Julia에서 비복원 추출을 시뮬레이션하는 방법은 몇 가지가 있어요.
가장 간단한 방법(그리고 Exercism 안에서는 유일한 방법)은 Random.shuffle()로 항목들을 무작위 순서로 배치한 다음, 앞의 n개 원소를 사용하는 거예요.
작은 문제에는 괜찮지만 큰 컬렉션에는 잘 맞지 않을 수 있어요. shuffle은 일부만 필요하더라도 전체 배열을 생성해야 하니까요.
복원 추출을 "제대로" 하려면 StatsBase.jl 패키지를 설치해요.
이 패키지는 다양한 옵션을 갖춘 sample() 함수를 제공해요.
비슷한 기능이 나중에 Random에 추가되어 표준 라이브러리의 일부가 되기를 기대해 볼 만해요(이 문서의 코드 예제는 Julia 1.11로 테스트했어요).
지금까지는 모든 결과가 똑같이 나올 가능성이 있는 경우에 집중했어요.
예를 들어 rand(1:100)은 1부터 100까지 어떤 정수든 똑같은 확률로 줘요.
실제 세계의 많은 상황은 이보다 훨씬 복잡해요.
그래서 통계학자들은 "실제 세계"의 결과를 수학적으로 설명하기 위해 다양한 distributions를 만들어 냈어요.
위에서 설명한 rand() 함수는 모든 확률이 같을 때 사용해요.
이것을 uniform 분포라고 해요.
"정규" 분포나 "종 모양" 곡선이라고도 불리는 이것은, 측정값의 부정확함을 설명하는 아주 흔한 방법이에요.
예를 들어, 일하는 공장에서 똑같아야 할 볼트 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)과 같은 결과를 줘요.
메리는 TV 시리즈 _스타트렉: 넥스트 제너레이션_의 열렬한 팬이에요. 친구들과 함께 _엔터프라이즈호_의 승무원인 척하는 펜과 종이 역할극 게임을 자주 하죠. 메리의 캐릭터는 피카드 함장이라서 함장 일지를 써야 해요. 메리는 게임에서 창의적인 부분을 좋아하지만, 즉석에서 무작위 데이터를 만들어 내는 건 싫어해요.
함장 일지에 자주 등장하는 데이터를 위한 무작위 생성기를 만들어서 메리를 도와줘요.
_엔터프라이즈호_는 여행 중에 수많은 행성을 만나요. 스타트렉 세계관의 행성은 특성에 따라 등급이 나뉘어요. 예를 들어 지구는 M등급 행성이에요. 가능한 모든 행성 등급은 D, H, J, K, L, M, N, R, T, Y예요.
random_planet 함수를 구현해요. 이 함수는 행성 등급 중 하나를 무작위로 반환해야 해요.
julia> random_planet_class()
'K'
NCC-1701이라는 등록 번호를 가진 엔터프라이즈호만 우주를 나는 게 아니에요! 다른 우주선과 조우하면 메리는 그 우주선의 등록 번호를 일지에 적어야 해요.
등록 번호는 "NCC-" 접두사로 시작하고, 그 뒤에 1000부터 9999까지(양 끝 포함)의 숫자가 와요.
무작위 우주선 등록 번호를 반환하는 random_ship_registry_number 함수를 구현해요.
julia> random_ship_registry_number()
"NCC-1947"
날짜가 빠져 있다면 일지가 무슨 소용이 있을까요?
스타데이트는 실수예요. 넥스트 제너레이션 시즌 1에 나오는 _엔터프라이즈호_의 모험은 스타데이트 41000.0과 42000.0 사이에서 펼쳐져요. "4"는 24세기를, "1"은 첫 번째 시즌을 나타내요.
41000.0(포함)과 42000.0 사이의 실수를 반환하는 random_stardate 함수를 구현해요.
julia> random_stardate()
41458.15721310934
스타데이트에 소수점이 너무 많다는 불만이 많아서, 메리는 일지를 소수점 한 자리로 반올림하기로 해요.
random_stardate()의 값을 반올림해서 처리할 수도 있지만, 훨씬 더 간단한 방법이 있다는 걸 알게 돼요.
41000.0과 42000.0 사이(양 끝 포함)의 실수를 소수점 한 자리로 반환하는 random_stardate_v2 함수를 구현해요.
julia> random_stardate_v2()
41732.6
게임의 한 버전에서는 다른 우주선과 상호작용하는데, 메리는 그중 몇 척을 골라야 해요.
가능한 우주선의 전체 목록이 있지만, 게임마다 그중 몇 척만 필요하고 무작위로 골라야 해요.
고유한 우주선 등록 번호의 벡터를 받아서 길이가 number_needed인 벡터를 반환하는 pick_starships(starships, number_needed) 함수를 구현해요. 결과 값은 starships에서 가져와야 하고, 중복이 없어야 해요.
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
starships 인자를 벡터로 제한했어요. 튜플이나 집합처럼 다른 반복 가능 객체에서도 동작하도록 pick_starships()를 어떻게 구현할 수 있을까요?
이 부분에는 테스트가 없어요.
Exercism에 가입하고 Julia 트랙을 개념 35개연습 문제 128개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.