트랙
/
Julia
Julia
/
연습 문제
/
Exercism 행렬
Exercism 행렬

Exercism 행렬

학습 연습 문제

소개

아주 앞에서 Vectors 개념을 다룰 때, "배열은 임의의 크기를 가질 수 있고(하드웨어의 메모리 제약만 받아요), 임의의 개수의 차원을 가질 수 있어요."라고 언급했어요.

그 이후로는 설명을 단순하게 유지하기 위해 차원이 둘 이상인 배열은 거의 다뤄 오지 않았어요. Julia 공식 문서를 그 온전한 복잡함 그대로 읽어 보면 이 결정이 더 납득이 갈 거예요.

하지만 고차원 배열은 과학 계산에서 정말, 정말 중요하기 때문에 꼭 이해해야 해요.

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

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

배열 만들기

쉼표로 구분한 목록을 대괄호 안에 넣어 벡터를 많이 만들어 왔어요. 구분자로 세미콜론을 써도 돼요.

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 행렬로 정의해요 (다른 맥락에서는 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 캐시를 도와주면, 캐시가 여러분을 도와줘요!

# 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 행렬을 열 방향으로 채워요.

흔히 쓰는 배열(균일한 값이나 난수)을 만드는 다양한 유틸리티 함수도 있어요.

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

인덱싱

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

배열에 함수 적용하기

앞에서 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를 배열이나 범위로 지정하면 고차원 배열에서 여러 차원을 한꺼번에 줄일 수도 있어요 (물론 출력 결과를 이해하려면 머리를 좀 써야 해요!).

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

sum() 같은 내장 함수에는 dims 키워드 인자가 흔히 있어요. 그런데 우리 코드에서 이와 비슷한 기능을 직접 작성하려면 어떻게 해야 할까요?

좋은 방법 하나는 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 배열은 크기가 테라바이트에 이를 수도 있어서, 복사는 성능에 악몽이 될 수 있어요!

이런 뷰는 반복, 브로드캐스팅, 또는 지금까지 교육 과정에서 본 다른 모든 연산에 사용할 수 있어요.

또한 컴프리헨션은 배열을 입력으로 받을 때 강력하고 다재다능해요. 간단한 경우는 Loops 개념에서 언급했지만, 더 자세한 이야기는 나중 개념에서 다뤄요.

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

1차원 경우는 Vector Operations 개념에서 다뤘어요.

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에도 꽤 잘 통한답니다.

지침

도트 매트릭스는 점과 여백으로 이루어진 2차원 이미지예요. 점들이 여백을 배경으로 의도한 이미지를 만들어요.

도트 매트릭스 이미지는 메모리에 2차원 Matrix로 저장하고 다룰 수 있어요. 앞으로 행렬에서 "여백"은 0으로, "점"은 0이 아닌 값으로 나타내요.

1. Exercism 로고 Matrix 정의하기

Matrix는 다음과 같아요. 0은 여백, 1은 점이에요:

[
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
    0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
    0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]

이걸 "렌더링"하면 다음과 같아요:

  XX          XX  
 X              X 
 X   X      X   X 
 X  X X    X X  X 
 X              X 
X                X
 X    X    X    X 
 X     X  X     X 
 X      XX      X 
 X              X 
  XX          XX

2. 로고가 찡그리게 하는 함수 정의하기

Exercism 로고 Matrix를 입력으로 받는 frown!() 함수와 frown() 함수를 정의해요. 웃는 입이 찡그린 입으로 바뀐 Matrix를 반환해요.

결과로 나온 Matrix를 렌더링하면 다음과 같아요:

  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X      XX      X
 X     X  X     X
 X    X    X    X
 X              X
  XX          XX

3. 스티커 월 만들기

Exercism 행렬을 입력으로 받는 stickerwall() 함수를 정의해요. 다음과 같이 렌더링되는 도트 매트릭스 Matrix를 반환해요:

  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X    X    X    X  X      XX      X
 X     X  X     X  X     X  X     X
 X      XX      X  X    X    X    X
 X              X  X              X
  XX          XX    XX          XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X      XX      X  X    X    X    X
 X     X  X     X  X     X  X     X
 X    X    X    X  X      XX      X
 X              X  X              X
  XX          XX    XX          XX

4. 점을 각 열의 픽셀 수로 바꾸기

점을 나타내는 값이 꼭 1일 필요는 없어요. 그래서 원한다면 다른 유용한 정보를 담을 수도 있어요. 1을 점으로 사용하는 아무 도트 매트릭스나 입력으로 받는 colpixelcount() 함수를 정의해요. 각 열의 점을 그 열에 있는 점의 개수로 바꾼, 같은 크기의 도트 매트릭스를 반환해요.

Exercism 로고 Matrix를 입력으로 하면 출력은 다음과 같은 Matrix예요:

[
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
    0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
    0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]

5. 도트 매트릭스 렌더링하기

우리가 만든 것을 렌더링하는 이야기만 하는 대신, 편하게 보려고 실제로 렌더링할 수 있으면 좋겠어요. 도트 매트릭스를 입력으로 받는 render() 함수를 정의해요. 점은 'X'로, 0은 ' '로 렌더링하고 각 행을 줄바꿈으로 이어 붙인 문자열을 반환해요.

julia> render(E)
"  XX          XX  \n X              X \n X   X      X   X \n X  X X    X X  X \n X              X \nX                X\n X    X    X    X \n X     X  X     X \n X      XX      X \n X              X \n  XX          XX  "

출력하면 Exercism Matrix가 예상대로 렌더링돼야 해요.

julia> print(render(E))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX

1이 아닌 "점"으로도 똑같이 동작해야 해요:

julia> print(render(colpixelcount(E)))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Julia Exercism

Exercism 행렬 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Julia 트랙을 개념 35개연습 문제 128개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.