6.11. 리덕션(reduction)

리덕션(reduction) 은 합산, 평균, 최솟값 구하기 등을 통해 하나 이상의 축을 따라 배열을 축소합니다. 각 리덕션은 배열 전체에 대한 단일 라이브러리 호출이며, 동일한 작업을 수행하는 Python 루프보다 훨씬 빠릅니다. numpy 는 일상적으로 쓰이는 것들을 다룹니다:

  • sum() – 모든 요소의 합계

  • mean() – 산술 평균(합계를 요소 개수로 나눈 값)

  • std() – 표준편차이며, ddof= 로 나눗수(N - ddof)를 조정합니다

  • min() / max() – 가장 작은 요소와 가장 큰 요소

  • median() – 요소를 정렬했을 때의 중앙값(50번째 백분위수)

  • argmin() / argmax() – 최솟값 또는 최댓값 요소의 인덱스

  • all() / any() – 불리언 배열에 대한 진릿값 리덕션

6.11.1. axis 키워드 없이

axis= 없이 호출하면 리덕션은 배열 전체를 대상으로 한 스칼라를 반환합니다:

a = np.array([1, 2, 3, 4], dtype=np.float)
np.sum(a)           # 10.0
np.mean(a)          # 2.5
np.std(a)           # 1.118...
np.median(a)        # 2.5

b = np.array([40, 10, 30, 20], dtype=np.float)
np.max(b)           # 40.0
np.argmax(b)        # 0  (index of the maximum)

6.11.2. axis 키워드와 함께

axis= 는 지정한 하나의 축을 축소하고 나머지 축은 그대로 둡니다. 결과는 입력보다 랭크가 하나 낮은 배열입니다:

m = np.arange(12, dtype=np.float).reshape((3, 4))

np.sum(m)               # 66.0          - scalar
np.sum(m, axis=0)       # length-4      - column sums
np.sum(m, axis=1)       # length-3      - row sums

동일한 형태 규칙이 모든 리덕션에 적용됩니다: axis=0 은 첫 번째 축을 축소하고, axis=1 은 두 번째 축을 축소하는 식입니다. 예를 들어 행을 따라 평균이나 표준편차를 구하려면 np.mean(m, axis=1)np.std(m, axis=1) 로 작성합니다. 결과는 다른 축의 길이를 가집니다.

keepdims=True 키워드는 축소된 축을 제거하는 대신 길이 1로 그대로 유지합니다. 이 차이는 축소된 결과를 원본에 대해 다시 브로드캐스트해야 할 때 중요해집니다: keepdims 는 랭크를 보존하므로 브로드캐스팅 규칙이 축별로 정렬된 상태를 유지합니다.

각 행에서 그 행의 평균을 빼는 것이 대표적인 사용 예입니다:

m = np.arange(12, dtype=np.float).reshape((3, 4))
row_means = np.mean(m, axis=1, keepdims=True)
# row_means has shape (3, 1)
centred = m - row_means
# (3, 4) - (3, 1) -> (3, 4), each row centred on its own mean

keepdims 없이 np.mean(m, axis=1) 은 형태가 (3,) 인 1차원 결과를 반환합니다. (3, 4) - (3,) 를 브로드캐스트하면 랭크 앞쪽 추가 후 (3,)(1, 3) 으로 정렬되는데, 이는 (3, 4) 와 호환되지 않습니다: 마지막 축이 일치하지 않고(4 대 3) 어느 쪽도 1이 아니므로 numpyValueError 를 발생시킵니다. keepdims=True 가 바로 이 빼기 연산을 유효하게 유지해 줍니다.

6.11.3. 레이아웃이 중요하다

형태(shape)와 스트라이드(strides) 에서 다룬 행 우선(row-major) 레이아웃과 결합하면, 마지막 축을 따라 축소하는 것이 가장 비용이 적은 경우입니다. 이때 리덕션은 데이터 블록을 저장된 방향대로 따라가며, 행에서 행으로 건너뛰는 일이 없습니다:

m = np.arange(2000, dtype=np.float).reshape((2, 1000))
np.sum(m, axis=1)       # cheap - long axis is the inner one
np.sum(m, axis=0)       # has to jump rows on every step

애플리케이션이 버퍼를 어떻게 배치할지 선택할 수 있다면, 긴 축을 마지막에 두어 그 축을 따른 리덕션이 빠른 방향으로 실행되도록 하십시오.

6.11.4. 입력으로서의 이터러블

대부분의 리덕션은 ndarray 대신 Python 이터러블(list, range, 튜플)을 받아들입니다. 이 편의성은 암묵적 변환에 몇 마이크로초의 비용이 드는데, 루프 안에서는 이것이 빠르게 누적됩니다. 동일한 데이터를 여러 번 축소하는 경우, ndarray 를 한 번만 만들어 두고 그것을 전달하십시오.