트랙
/
Julia
Julia
/
학습 과정
/
다차원 배열
다차

다차원 배열 에서 Julia

2개의 연습 문제

다차원 배열 소개

훨씬 앞서 벡터 콘셉트에서 "배열은 임의의 크기를 가질 수 있고(하드웨어의 메모리 제약만 받을 뿐이에요), 임의로 많은 차원도 가질 수 있어요"라고 언급했어요.

그동안 우리는 단순함을 유지하기 위해, 차원이 둘 이상인 배열은 대부분 다루지 않았어요. 이 결정은 Julia 참조 문서를 원래의 복잡한 모습 그대로 읽어 보면 더 잘 이해가 될 거예요.

하지만 고차원 배열은 과학 계산에서 아주, 아주 중요하기 때문에, 잘 이해할 필요가 있어요.

용어: 수백 년에 걸친 수학의 관례를 따라, 1차원 배열은 Vectors, 2차원 배열은 Matrices라고 불러요.

이 문서의 예제는 대부분 행렬이에요. 3차원 이상을 다루는 것은 문법적으로 거의 같지만, 출력 결과는 (2차원 화면에서) 읽기 어렵고 헷갈려요.

eltype이 T인 N차원 배열의 type은 Array{T, N}이에요.

편의와 수학적 용어와의 일관성을 위해, Julia는 몇 가지 타입 별칭을 정의해요: Array{T, 1}에 대해 Vector{T}, Array{T, 2}에 대해 Matrix{T}.

# 3-D array
julia> m3 = ones(2, 3, 4);

julia> typeof(m3)
Array{Float64, 3}

# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
 1
 2

julia> typeof(v) == Array{Int64, 1}
true

# Matrix
julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})

julia> typeof(m) == Array{Int64, 2}
true

배열 만들기

지금까지 쉼표로 구분된 목록을 대괄호 안에 넣어서 많은 벡터를 만들어 왔어요. 세미콜론도 구분자로 쓸 수 있어요.

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> w = [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v == w
true

구분자로 공백(또는 탭)을 사용하면 결과가 달라져요.

julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

Julia는 이것을 1×3 Matrix로 정의해요 (다른 맥락에서는 row vector라고 부르겠죠).

일반적으로 공백은 요소를 가로로 이어 붙이고, 세미콜론(또는 줄바꿈)은 요소를 세로로 이어 붙여요.

"요소"라고 한 것은 일부러 모호하게 표현한 거예요. Julia는 주어진 무엇이든 다루려고 하니까요.

julia> [v 2v]
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> [v; 2v]
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

똑같은 일을 하는 hcat()과 vcat() 함수도 있어요. 이 이름은 각각 가로 방향과 세로 방향 연결이라는 점을 더 분명하게 드러내요. 고차원으로 일반화한 것은 cat() 함수예요.

jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> vcat(v, 2v)
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

명시적인 행렬을 직접 입력할 때는 행 우선 순서로 쓰는 것이 편리해요. 사람의 직관과 잘 맞기 때문이에요(가로로 글을 쓰는 문화권에서는 보기가 더 쉽죠):

julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

하지만 Julia는(Fortran, R, Matlab과 마찬가지로, C/C++이나 NumPy와는 달리) N차원 배열을 열 우선 순서로 저장한다는 점을 알아 두세요. 원소를 순회하는 방식에 따라 성능 차이가 아주 크게 날 수 있어요. CPU 캐시를 도와주면, CPU 캐시도 여러분을 도와줘요!

# put these integers in 2 rows and 3 columns

julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
 1  3  5
 2  4  6

위 예제는 정수 1부터 6까지를 가져다가 열 방향으로 2×3 Matrix를 채워요.

균일하거나 무작위인 흔한 종류의 배열을 만들기 위한 여러 유틸리티 함수도 있어요.

julia> zeros(2, 3)  # see also ones()
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> falses(2, 2)  # booleans, see also trues()
2×2 BitMatrix:
 0  0
 0  0

julia> rand(Float32, 2, 3)  # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
 0.768823  0.169633  0.632565
 0.388451  0.109176  0.850381

균등 간격 값 생성하기

NumPy를 즐겨 쓰는 분이라면 np.linspace가 널리 쓰이는 함수라는 걸 알 거예요. 지정한 길이로, 지정한 양 끝값을 가지며 값이 균등 간격으로 배치된 배열을 만들어 주죠. 이건 보통 그래프의 x축이나 선형 모델의 독립 변수로 사용돼요.

Julia에는 정확히 대응하는 함수가 없지만, 아주 유연한 range() 함수가 아래쪽과 위쪽 경계, 그리고 length 키워드 인자를 지정해서 이를 흉내낼 수 있어요.

Plots 패키지에 접근할 수 있는 로컬 환경에서 작업하는 분이라면 누구나 이 코드를 실행해 볼 수 있어요:

julia> using Plots

julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586

julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}

julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
 0.0         0.0           1.0
 0.0634665   0.0634239     0.997987
 0.126933    0.126592      0.991955
 0.1904      0.189251      0.981929
(...truncated)

julia> plot(vals[:, 1], vals[:, 2:3])

StepRangeLen은 벡터처럼 사용할 수 있어요. 행렬의 열로 변환하는 것도 가능하죠.

로그 축에서 균등 간격으로 값을 생성하는, 이에 대응하는 logrange() 함수도 참고하세요.

인덱싱

2차원 배열에서는 보통 [row, col] 순서로 두 개의 인덱스를 사용해요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[1, 2] # row 1, col 2
2

# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]

julia> m[3]
2

마지막 예제는 좀 놀라울 수 있어요: 인덱스가 하나 라고 해서 오류가 나는 게 아니라, 원소 하나를 반환해요.

설명은 열 우선 순서에 관한 앞의 이야기로 돌아가요. Julia는 1번 열을 따라 내려간 다음 2번 열로 가면서, 메모리상에서 세 번째 원소를 찾을 때까지 진행해요.

주의하세요: 범용 라이브러리를 작성할 때는 쓸모가 있기도 하지만, 대개는 헷갈리기 십상이에요!

배열의 크기를 조회할 때도 비슷한 문제가 생겨요. length()는 전체 원소 개수를 알려주고, size()는 각 차원의 길이를 담은 ndims()개의 원소로 이루어진 튜플을 알려줘요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> length(m)
6

julia> size(m)  # 2 rows, 3 cols
(2, 3)

julia> ndims(m)  # how many dimensions? Like `m |> size |> length`
2

범위와 배열로 인덱싱하기

부분 행렬을 손쉽게 복사할 수 있어요.

아래 예제에서 reshape() 함수는 열 방향으로 채워서 배열을 지정한 차원으로 맞춰 주고, 그다음 우리가 잘라내요.

julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12

julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
 2  6
 3  7
 4  8

# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
 2  6  10
 3  7  11
 4  8  12

: 하나만 쓰면 "이 차원의 모든 것을 복사하라"는 뜻이에요.

연속되지 않은 행/열에는 벡터를 사용하세요:

julia> m12[[1, 3], :]  # rows 1 and 3
2×3 Matrix{Int64}:
 1  5   9
 3  7  11

: 하나만 쓰면 어떤 배열이든 열 방향으로 펼쳐서 벡터로 만들어요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[:]
6-element Vector{Int64}:
 1
 4
 2
 5
 3
 6

배열에 함수 적용하기

앞에서 sum()이나 maximum() 같은 집계 함수를 1차원 컬렉션에 적용하는 것을 봤어요. 이 함수들은 모든 원소에 작용하고 스칼라 결과를 반환하죠.

이건 더 높은 차원에서도 똑같이 동작해요. 하지만 함수를 한 차원에만 적용하고 싶을 때가 있어요. 예를 들어 세로 방향이나 가로 방향으로 합을 구해서, 크기가 1인 singleton dimension을 가진 배열을 돌려받는 식이죠.

이를 위해 선택적인 dims 키워드 인자가 있어요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> sum(m)  # sum everything
21

julia> sum(m; dims=1)  # sum down
1×3 Matrix{Int64}:
 5  7  9

julia> sum(m; dims=2)  # sum across
2×1 Matrix{Int64}:
  6
 15

# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
 3
 6

dims의 값은 _싱글턴이 되는 차원_이에요. 그래서 위 예제에서 dims=1 => 1×3, dims=2 => 2×1이 되는 거죠.

확장하면, dims를 배열이나 범위로 지정하면 고차원 배열에서 여러 차원을 축소할 수도 있어요 (다시 말하지만, 출력을 이해하려면 좀 생각이 필요할 거예요!).

차원을 인식하는 함수 작성하기

dims 키워드 인자는 sum() 같은 내장 함수에서 흔히 볼 수 있어요. 그런데 우리가 직접 작성하는 코드에서 이와 동등한 것을 어떻게 만들 수 있을까요?

한 가지 좋은 방법은 reduce() 같은 고차 함수를 사용하는 것이고, 이는 이후 콘셉트에서 어느 정도 자세히 다뤄요.

또는 Julia는 N차원 배열을 마치 벡터의 벡터처럼 중첩된 것처럼 다룰 수 있게 해 주는 여러 함수를 제공해요.

행렬에는 eachrow()와 eachcol()이 편리하지만, 더 일반적인 함수는 임의의 차원을 다룰 수 있는 eachslice()예요.

# m is as in the previous examples

julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
 [1, 2, 3]
 [4, 5, 6]

julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
 [1, 4]
 [2, 5]
 [3, 6]

타입이 조금 무시무시해 보이지만, 이는 원본 배열에 대한 뷰 이기 때문일 뿐이에요. 복사하지 않고도 작업할 수 있게 해 주죠. Julia 배열은 테라바이트 크기가 될 수도 있어서, 복사는 자칫 성능에 악몽이 될 수 있어요!

이런 뷰는 루프, 브로드캐스팅, 또는 지금까지 실라버스에서 봐 온 다른 모든 연산에 사용할 수 있어요.

또한 컴프리헨션은 배열 입력에 대해 강력하고 다재다능할 수 있어요. 간단한 경우는 루프 콘셉트에서 언급했지만, 더 자세한 논의는 이후 콘셉트에서 다뤄요.

여러 차원에서의 브로드캐스팅

1차원인 경우는 벡터 연산 콘셉트에서 다뤘어요.

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v .- 0.5
3-element Vector{Float64}:
 0.7
 1.0
 1.2

점이 붙은 연산자 .-는 싱글턴인 0.5를 broadcasting해서 차원을 맞춰 [0.5, 0.5, 0.5]와 동등하게 취급한 다음, 원소별로 뺄셈을 해요.

이를 더 높은 차원으로 확장하는 것은 사실상 같은 이야기를 조금 더 하는 것뿐이에요.

예를 들어, 2x1 행 벡터 [1.0 1.5 2.0]와 2x3 행렬 [1 2 3; 4 5 6]의 곱셈을 브로드캐스팅하면, [1.0 1.5 2.0]를 [1.0 1.5 2.0; 1.0 1.5 2.0]로 브로드캐스팅한 다음 곱셈을 원소별로 수행하는 것과 같아요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m .* [1.0 1.5 2.0]  # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0]  # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m ./ [1, 2]  # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

julia> m ./ [1 1 1; 2 2 2]  # 2x3 elementwise division
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

참고: 브로드캐스팅에서는 싱글턴이 아닌 차원의 크기가 일치해야 해요 (예: 2x3 Matrix .* 2x1 Matrix).

게다가, 함수는 Vector에 하는 것과 똑같은 방식으로 Matrix의 각 원소에 브로드캐스팅할 수 있어요.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> (x -> x^2).(m)  # broadcast a function to all elements
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

처음에는 프로그래머가 헷갈릴 여지가 차원 수에 따라 기하급수적으로 늘어난다고 느낄 수 있지만, 연습하면 훨씬 나아져요. 또한 NumPy 배열에 익숙하다면 문법은 다르더라도 Julia로 꽤 잘 옮겨 와요.

더 읽어보기

벡터와 행렬에 대한 많은 흔한 연산은 선형대수라는 큰 범주에 속해요.

(현재) Exercism 웹사이트에는 두 개의 추가 콘셉트가 있는데, 이들은 함께할 연습 문제를 설계할 수 있을 때까지 문서만 있어요.

  1. 선형대수 기초는 주로 여러 종류의 곱셈을 다뤄요.
  2. 선형 방정식 풀이는 연립 일차 방정식과 이를 효율적으로 푸는 방법을 다뤄요.
Advanced

앞으로 행렬 분해에 관한 세 번째 콘셉트가 생길 수도 있어요.

다만 선형대수 기초는 상당히 수학적이어서 모든 학습자에게 맞지는 않을 수 있다는 점을 알아 두세요.

뒤의 콘셉트들(방정식 풀이와 분해)은 더 고급이라, 대학 수준의 수학에 익숙한 분들을 주로 대상으로 해요.

죄송하지만 이것은 이 주제의 특성이고, Julia의 중요한 활용 사례이기도 해요.

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

다차원 배열 배우기

연습이 잠겨 있어요

다차원 배열 개념을 연습하려면 연습 문제 1개를 더 잠금 해제해요