메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

에릭 하게만 시리즈 5 - 수치처리 파이썬으로 배우는 확산의 개념

한빛미디어

|

2002-06-10

|

by HANBIT

13,810

저자: 에릭 하게만, 역 전순재

들어가는 말

본 기사에서 알아볼 도전 과제는 NumPy의 연산 중 가장 두드러진 그러나 가끔 혼란스러운 면모를 보여주는 것들 중의 하나인 확산(broadcasting)에 대해 알아보는 것이다. 서로 다른 차원의 다중배열사이에 연산을 수행할 때 차원을 맞추기 위해 두 배열 중 하나 아니면 두 개 모두가 확산(broadcast)될 수 있다. 아주 단순한 형태라면 이 특징을 이해하기 쉽겠지만 혹시라도 복잡한 표현식에 사용되면 숙지하기가 쉽지않다. 이제부터 NumPy의 이러한 기본적인 능력을 살펴보고 사용하는 법을 살펴보도록 하자.

NumPy는 본래 선형 대수학을 지원하지는 않는다. 대신에 선형 대수학의 고유한 기능은 라이브러리 함수에 남겨 두었다. NumPy의 구현은 N차원 객체(다중배열)의 복잡한 잠재능력에 초점을 맞추었다(확산이 그러한 지원을 제공함). 이런 조합으로 접근하면 대단히 강력하고 유용한 패키지가 되어 방대한 개발 공동체를 만족시킬 수 있다. 그러나 그 때문에 용어상에 약간의 혼란을 초래하기도 한다.

지위(Rank)

NumPy 문서에서 "지위(rank)"라는 단어는 다중배열(multiarray) 차원의 개수를 가리킨다. 지위(rank)는 (예를 들어 선형적으로 독립적인 행/열의 개수, 등등과 같은) 객체 자체의 상태와는 아무런 상관이 없다. 열 벡터는 지위가 1이며, 행렬은 항상 지위가 2이고, N차원의 다중행렬은 항상 지위가 N이다. NumPy에서 지위는 다중행렬의 꼴을 가리킨다. 이런 개념은 기존의 주요 수학 형들과 혼란을 일으키기에 충분하다. 전통 선형 대수학에서 지위는 행열의 행 또는 열과 관련된 숫자를 가리킨다.

또한 NumPy 문서에서 "축(axis)"이라는 단어는 차원을 가리킨다. 그리하여 지위 5인 다중배열은 차원을 5개 가지며 1번째 축에서부터 5번째 축까지 가리킬 수 있다. 축과 차원은 길이가 똑같다.

이러한 정의가 혼란스러울 수도 있다. 특히 많은 용어들을 전통적인 수학적 표기법을 덮어써서 사용하게 될 경우에는 더욱 그러하다. 그렇다고 하더라도 위의 정의를 고수하자! 조만간 여러분은 NumPy라는 방언을 가장 잘 구사하게 될 테니까...

확산(Broadcasting)

확산(Broadcasting)[1]은 더 작은 지위의 다중배열이 반복되거나 확장되어 자연스럽게 더 높은 지위의 다중배열과 작동하는 것을 가리킨다. 기본값으로 수학적 이진 연산(덧셈, 뺄셈, 곱셈, 나눗셈)뿐만 아니라 논리적 이진 연산(and, or, xor)과 같은 모든 이진 연산들은 두 다중배열사이에 요소 대 요소로 작동한다. NumPy 초보가 제일 먼저 배우는 것은 이진 연산을 완수하려면 모든 차원의 개수와 크기가 일치해야만 한다는 것이다. 그렇지만 이것이 전적으로 참이 아니라는 것을 알게 될 것이다. 그리고 사실 알고 보면 참이 아닌 예들도 여럿 있다(모든 것은 확산의 실체[2] 중 하나).

예제들로 더 깊이 들어가기 전에 앞으로 사용할 수치처리 패키지(NumPy)를 반드시 임포트해야 할 필요가 있다는 사실을 기억해두기 바란다. 일단 NumPy로 임포트되기만 하면 상호대화적인 파이썬 명령어 창에서 모든 예제가 실행될 수 있다. 파이썬을 작동시킨 후 다음 명령어를 실행해보자(이 작업은 각 세션에 대하여 한번만 실행하면 됨).
>>> from Numeric import *
아무 에러도 없다면 출발준비가 다 된것이고 예외(exception)가 발생했다면 NumPy가 올바르게 설치되지 않았다는 뜻일 것이다(도움말은 NumPy 문서 참조).

다중배열(Multiarray)과 스칼라 연산

확산이 무엇인지 살펴 보기 위해 스칼라 값 하나를 다중배열 하나에 더해 보자. 2ⅹ2 배열 a와 스칼라 b가 있다면 그 결과는 아래와 같다.
>>> a = array([[1,2],[3, 4]])
>>> a
array([[1, 2],
       [3, 4]])
>>> b=1
>>> a+b
array([[2, 3],
       [4, 5]])
결과는 예상대로이다. 여기서 잠깐! 두 피연산자의 순위가 일치하지 않는 거 같은데? 스칼라 값이 확산(반복)되어 자연스럽게 연산을 완수했다. 본질적으로 스칼라는 각 요소들이 b의 값을 가지는 2ⅹ2 다중배열로 변환되고 난 후 연산이 계속되었다. 이 예제에서 연산자는 덧셈이었다. 모든 연산자들이 동일한 행위(요소 대 요소 연산)를 보이므로, 다른 것들을 설명하는 목적으로는 이 예제 하나로 충분할 것이다. 어서 다른 연산자들을 시험해 보고 확인해 보자. 더하는 것 대신 ab를 곱하면 어떤 일이 일어날까?

Multiarray 그리고 multiarray

스칼라에 대해서는 이 정도로 충분하다. 다중 배열 두개를 가지는 더욱 복잡한 연산에 대해 알아보자.

다음 예제에서는 지위는 같지만 각 차원의 길이는 일치하지 않는다.
>>> a
array([[1, 2, 3],
       [4, 5, 6]])
>>> b
array([[1, 2],
       [3, 4],
       [5, 6]])
>>> a+b
Traceback (innermost last):
  File "", line 1, in ?
ValueError: frames are not aligned
우리가 염려하던 "프레임들이 정렬되지 않음(frames not aligned)"이라는 에러가 뜬다. 이 에러는 a가 2ⅹ3 다중배열이고 b가 3ⅹ2 다중배열이기 때문에 발생했다.

다중배열 하나를 더 시험해 보자.
>>> a
array([[1, 2, 3],
       [4, 5, 6]])
>>> c
array([7, 8, 9])
>>> a+c
array([[ 8, 10, 12],
       [11, 13, 15]])
차원의 길이가 일치하지 않음에도 불구하고 도데체 왜 위 예제는 작동했는가? 이 예제는 확산의 또 다른 예이다. a 배열은 지위가 2이고 c는 지위가 1이다. 이 경우에 각 차원(또는 축)의 길이가 따로따로 비교된다. 그리고 어떤 상황에서는 더 낮은 지위의 배열이 반복된다. ac에 대한 꼴 터플을 살펴보자. 기억하는가? .shape 속성은 각 축(터플안에 있는 값들)의 길이와 지위(터플의 길이)를 정의하는 터플을 반환한다.
>>> a.shape
(2, 3)
>>> c.shape
(3,)
서로 다른 지위의 다중배열 두 개를 연산하면 각 축의 길이는 제일 오른쪽에서부터 시작하여 비교된다. ac 모두 비교하여 가장 낮은(가장 오른쪽의) 축이 3이므로 계속해서 작업을 한다. c가 확산된다면 본질적으로 필요한 횟수만큼 반복되어 a의 상위 차원에 일치한다. 이 경우에는 2이다. 이러한 방법은 지위가 엄청나게 더 큰 객체들에도 역시 그대로 적용된다.

바로 다음예제와 그 다음 예제들에는 zeros() 함수를 사용하여 주어진 꼴로 다중배열을 만드는 것을 보여준다. 여러분이 짐작하듯이 0으로 채워진다! 논의의 목적을 위해, 다중배열 안에 있는 값들에 대해서는 신경쓰지 않겠다. 그 보다는 그 객체의 꼴에 더 관심을 기울일 것이다. zeros() 함수를 사용하면 간단하고 편리하게 원하는 꼴의 다중배열을 만들 수 있다.
>>> x=zeros((3,4,5,6,7))
>>> y=zeros((7,))
>>> z=x+y
이 예제에서는 지위가 5인 다중배열 x와 지위가 1인 다중배열 y를 만들었다. 덧셈 연산자가 이 값들을 더하고자 할 때, x 다중배열에 대하여 y 다중배열을 필수적으로 필요한 횟수만큼 반복하여 확산할 필요가 있었다. 이 경우에는 y 다중배열에 대하여 축이 4개 더 "생성되었다".

전혀 예상 밖으로 위의 논의에 대해서는 예외가 하나 있다. 각 축의 크기를 비교할 때 만약 비교되는 축들 중의 하나라도 길이가 1이면, 확산이 발생할 수도 있다. 예를 들어 보면 다음과 같다.
>>> z
array([1, 2])
>>> z.shape
(2,)
>>> v
array([[3],
       [4],
       [5]])
>>> v.shape
(3, 1)
>>> z+v
array([[4, 5],
       [5, 6],
[6, 7]])
이 형태에서 첫 번째 다중배열 z는 (3,2) 다중배열로 확장되었고 두 번째 다중배열 v는 (3,2) 다중배열로 확장되었다. 필수적으로 확산은 두 피연산자 모두에서 발생했다! 이런 일은 오직 두 다중배열 중에 한 배열의 축 크기가 1이라는 값을 가질 때만 발생한다.

새축(NewAxis)

비슷한 예제 하나를 살펴 보자. 다음과 같이 다중배열 두 개를 만들어보자.
>>> z
array([1, 2])
>>> z.shape
(2,)
>>> w
array([3, 4, 5])
>>> w.shape
(3,)
우리가 지금까지 배운 규칙으로는 이러한 두 객체를 더하게 되면 예외(exception)가 일어날 것이다. 이 연산을 성공시키고 확산에 성공하려면 w 다중배열이 변경될 필요가 있다. reshape 함수를 사용하면 w을 꼴을 변경하여 v라는 배열을 만들 수 있다.
>>> v=reshape(w,(3,1)) 
>>> v
array([[3],
       [4],
       [5]])
>>> v.shape
(3, 1)
이제 연산은 확산의 도움을 받아 계속 진행될 수 있다.

이러한 종류의 연산을 지원하기 위해 그리고 reshape 함수에 의존하면 코드를 지저분하게 할 가능성이 있기 때문에 NumPy의 구현자들은 NewAxis라는 지표를 고안해 냈다.

NewAxis는 임시로 축을 다중배열에 더할 수 있도록 해주는 가짜 지표(pseudo-index)이다. 이 전의 예제를 한 줄짜리 코드로 시험해 보려면 다음과 같이 수행하면 된다.
>>> z = array([1,2])
>>> w = array([3,4,5])
>>> z+reshape(w,(3,1))
array([[4, 5],
       [5, 6],
[6, 7]])
NewAxis 사용하면 reshape 함수를 호출할 필요가 없으며 코드는 더욱 간단해진다.
>>> z+w[:,NewAxis]
array([[4, 5],
       [5, 6],
[6, 7]])
위의 두 예제는 같은 일을 하지만 그 구현은 서로 다르다. 첫 번째 예제는 배열의 꼴을 원하는 형태로 바꾼다. 두 번째 예제는 슬라이싱 연산자를 사용하여 NewAxis를 사용하는 축 하나를 더하면서 그 배열을 다시 만드는 것을 보여준다. 슬라이싱이 무엇인지 잘 모르겠다면 지난 번 기사(에릭하게만 시리즈 4 - 수치처리 파이썬 모듈에 대하여)를 참고하기 바란다.

다음은 더 흥미로운 예제로 NewAxis를 사용한 슬라이싱을 보여주는 것이다.
>>> a=zeros((3,4,5,6))
>>> b=zeros((4,6))
>>> c=a+b[:,NewAxis,:]
이 경우에 (확산을 지원하는) 임시 축 하나를 b의 첫 번째 축과 두 번째 축 사이에 삽입했다. 주목할 것은 결과로 나온 다중배열 b는 지위가 3이고 다중배열 a는 지위가 4이기 때문에 확산(broadcasting)이 제일 왼쪽 지표에서 발생했다는 것이다!

예제들에서 패턴이 보이는가? 확산(Broadcasting)은 해당 다중배열 두개의 지위들이 같지 않을 때 일어난다. 이러한 일이 일어나면, 일정 규칙들이 작용을 하여 거기에서부터 각 축은 길이가 비교되고 조정이 된다. 빠진 축은 채워 넣어져 연산이 작동하도록 만들 수 있다; 길이가 1인 축이라면 덮여 쓰여질 수도 있다.

도대체 확산(broadcasting)은 무엇이 유익한가?

확산의 사용은 NumPy 연산에서 분리할 수 없는 고유의 기능이기 때문에 확산이 없이 사는 것은 불가능할 것이다. 한 다중배열의 각 값에 1을 더하는 연산 또는 모든 값들을 2배 만큼 확대하는 연산을 생각해 보자. 이러한 연산들이 성공하려면 확산에 의존해야 한다. (불가능하지는 않겠지만) 확산 없이 이러한 연산들을 달성하는 법을 상상하기란 쉽지 않은 일이다.

각 요소가 5로 설정되고 지위가 3인 다중배열을 확산으로 만든다고 생각해 보자.
>>> a = ones((1,2,3)) * 5
확산이 없다면 다음과 같이 보기에 아주 안좋다!
>>> a = ones((2,3,4))
>>> tmp = a.shape
>>> for i in range(tmp[0]):
...     for j in range(tmp[1]):
...             for k in range(tmp[2]):
...                     a[i,j,k] = a[i,j,k] * 5
더욱 복잡한 경우에 확산이 없이 한 행렬의 각 행을 벡터 하나만큼 곱하려면 그 벡터가 먼저 한 행렬로 반복되어 들어가는 것이 필요할 것이다. 그래야 연산이 수행될 수 있기 때문이다. 확산을 사용하면 반복해야 하는 처리과정을 피할 수 있다. 거대한 다중배열을 처리하는 경우 확산은 상당한 메모리와 시간을 절약할 수 있다.

결론적으로 확산 덕분에 프로그래머는 직접적으로 차원이 일치하는 배열을 만들어야 하는 단계를 피할 수 있다. 그러나 머리만 너무 믿지않는 것이 좋을 것이다. 여러분 자신과 후학들이 이후에 언젠가 복잡한 형태의 확산을 사용하게 될 때 당면한 처리연산을 이해할 수 있도록 도와주는 것이 좋을 것이다. 이때, 주석은 큰 도움이 된다!

게임 끝

본 기사에서는 NumPy가 지원하는 확산(broadcasting)의 예들을 살펴 보았다. 이러한 규칙들 덕분에 다중배열들은 지위가 같지 않음에도 불구하고 서로 상호작용 할 수 있다. 규칙들이 약간 혼란스럽지만 이용만 잘하면 NumPy의 잠재능력을 모두 다 사용하는데 크게 도움이 될 것이다.

다음 기사에서는 더 방대한 크기의 애플리케이션을 하나로 합쳐 에릭하게만 시리즈에서 배운 특징들을 실행해 보겠다. 필가는 NumPy를 여러분의 컴퓨터가 가진 멀티미디어 잠재 능력에 통합하여 실무 훈련용 애플리케이션을 만들 것이다. 우리가 만들 그 애플리케이션에는 (확산을 포함하여) NumPy, FFT 모듈, DISLIN 도표화 패키지가 채용될 것이다! 다음 기사를 기대하기 바란다.
[1] Broadcasting: 확산[擴散], 널리 퍼져 흩어짐
[2] instance(실체): 개념이 본체라면 사물은 실체다. 개념이 영원이라면 사물은 순간이다. 개념은 요원이 다(多)이고 사물은 요원이 일(一)이다. 일(一)인 사물이 모여 다(多)인 개념을 표현한다.
TAG :
댓글 입력
자료실

최근 본 상품0