Code › codeit-ai-sprint

행렬 연산과 NumPy 기초

행렬 덧셈, 스칼라곱, 행렬 곱셈, 요소별 곱셈, 그리고 NumPy 연산 정리

머신러닝 기본기 강의를 계속 듣고 있다. 지난 글에서는 머신러닝과 선형대수가 왜 필요한지에 대한 큰 그림을 살펴봤다면, 이번에는 본격적으로 행렬(Matrix)과 관련된 연산들을 배웠다. 학부때 행렬을 배우긴 했지만 기억이 많이 남아 있지는 않아서, 처음부터 다시 정리한다는 느낌으로 들었다.

가장 먼저 배운 것은 행렬의 덧셈이다. 행렬끼리 더하려면 두 행렬의 크기가 완전히 같아야 한다. 예를 들어 2 x 2 행렬과 2 x 2 행렬은 더할 수 있지만, 2 x 2 행렬과 2 x 3 행렬은 더할 수 없다. 즉, 행의 개수와 열의 개수가 모두 일치해야 같은 위치의 요소끼리 더할 수 있다.

그 다음은 스칼라 곱(Scalar Multiplication)을 배웠다. 여기서 스칼라는 벡터나 행렬이 아닌 숫자 하나를 의미한다. 행렬에 스칼라 2를 곱하면 행렬 내부의 모든 원소에 각각 2가 곱해진다.

[1 2]   2를 곱하면   [2 4]
[3 4]       →       [6 8]

개념 자체는 단순하지만, 행렬 전체의 값을 일정한 비율로 조정할 때 사용되는 기본적인 연산이다.

이번 파트에서 가장 중요했던 내용은 행렬 곱셈(Matrix Multiplication)이었다. 행렬 곱셈은 덧셈과 달리 두 행렬의 크기가 같다고 해서 가능한 연산이 아니다. A와 B라는 두 행렬이 있을 때, 다음과 같은 형태를 만족해야 곱셈이 가능하다.

A의 크기: m × n
B의 크기: n × p

즉, 앞 행렬 A의 열 개수와 뒤 행렬 B의 행 개수가 같아야 한다.

이 연산의 결과 행렬은 앞 행렬의 행 개수와 뒤 행렬의 열 개수를 따라간다.

(m × n) × (n × p) = (m × p)

예를 들어 A가 2 x 3 행렬이고 B가 3 x 4 행렬이라면, A와 B의 곱셈은 가능하고 결과는 2 x 4 행렬이 된다.

(2 × 3) × (3 × 4) = (2 × 4)

행렬 곱셈에서는 순서도 중요하다. 일반적인 숫자 곱셈에서는 2 × 3과 3 × 2의 결과가 같지만, 행렬에서는 그렇지 않다.

예를 들어 다음과 같은 두 행렬 A와 B가 있다고 하자.

A = [1 2 3]
    [4 5 6]

A의 크기: 2 × 3
B = [ 1  2  3  4]
    [ 5  6  7  8]
    [ 9 10 11 12]

B의 크기: 3 × 4

먼저 AB를 계산해 보면 다음과 같다.

AB =

[1 2 3]   [ 1  2  3  4]
[4 5 6] × [ 5  6  7  8]
          [ 9 10 11 12]

이 경우에는 앞 행렬 A의 열 개수가 3이고, 뒤 행렬 B의 행 개수도 3이다.

A의 열 개수: 3
B의 행 개수: 3

그래서 A의 각 행과 B의 각 열을 서로 짝지어 계산할 수 있다.

예를 들어 결과 행렬의 첫 번째 값은 A의 첫 번째 행과 B의 첫 번째 열을 곱해서 더한 값이다.

A의 첫 번째 행: [1 2 3]
B의 첫 번째 열: [1]
              [5]
              [9]

계산:
1×1 + 2×5 + 3×9
= 1 + 10 + 27
= 38

같은 방식으로 모든 값을 계산하면 다음과 같은 결과가 나온다.

AB =

[1 2 3]   [ 1  2  3  4]
[4 5 6] × [ 5  6  7  8]
          [ 9 10 11 12]

=

[ 38  44  50  56]
[ 83  98 113 128]

즉, AB의 결과는 2 × 4 행렬이 된다.

(2 × 3) × (3 × 4) = (2 × 4)

반대로 BA를 계산하려고 하면 다음과 같은 형태가 된다.

BA =

[ 1  2  3  4]   [1 2 3]
[ 5  6  7  8] × [4 5 6]
[ 9 10 11 12]

여기서는 앞 행렬 B의 열 개수가 4이고, 뒤 행렬 A의 행 개수가 2이다.

B의 열 개수: 4
A의 행 개수: 2

행렬 곱셈은 앞 행렬의 한 행과 뒤 행렬의 한 열을 짝지어 계산한다. 그런데 BA에서는 B의 한 행에는 숫자가 4개 있고, A의 한 열에는 숫자가 2개밖에 없다.

B의 첫 번째 행: [1 2 3 4]

A의 첫 번째 열: [1]
              [4]

이 둘을 곱해서 더하려고 하면 개수가 맞지 않는다.

1×1 + 2×4 + 3×? + 4×?

뒤쪽 두 값에 대응되는 숫자가 없기 때문에 계산을 진행할 수 없다.

그래서 BA는 계산할 수 없다.

(3 × 4) × (2 × 3)

앞 행렬 B의 열 개수: 4
뒤 행렬 A의 행 개수: 2

→ 4와 2가 일치하지 않으므로 계산 불가능

이처럼 같은 A와 B를 사용하더라도 AB는 가능하지만 BA는 불가능할 수 있다. 행렬 곱셈은 앞 행렬의 열 개수와 뒤 행렬의 행 개수가 일치해야 하므로, 곱셈의 순서에 따라 연산 가능 여부와 결과 행렬의 크기가 달라진다.

또 하나 배운 개념은 요소별 곱셈(Element-wise Multiplication)이다. 이름 그대로 같은 위치에 있는 요소끼리 곱한다.

예를 들어 두 행렬 A와 B가 다음과 같다고 하면,

A = [1 2]
    [3 4]

B = [5 6]
    [7 8]

요소별 곱셈의 결과는 다음과 같다.

[1*5  2*6]   =   [ 5  12]
[3*7  4*8]       [21  32]

이 연산은 같은 위치의 요소끼리 곱하는 방식이기 때문에, 행렬 덧셈과 마찬가지로 두 행렬의 크기가 같아야 한다.

강의 후반에는 NumPy를 이용한 실습도 진행했다. NumPy에서는 곱셈 연산자에 따라 의미가 달라진다.

A * B

위 코드는 요소별 곱셈(Element-wise Multiplication)을 의미한다.

반면 아래 코드는 행렬 곱셈(Matrix Multiplication)을 의미한다.

A @ B

즉, NumPy에서 *@는 모두 곱셈처럼 보이지만 서로 다른 연산이다.

간단한 예제로 보면 다음과 같다.

import numpy as np

A = np.array([
    [1, 2],
    [3, 4]
])

B = np.array([
    [5, 6],
    [7, 8]
])

print(A * B)
# [[ 5 12]
#  [21 32]]

print(A @ B)
# [[19 22]
#  [43 50]]

A * B는 같은 위치의 요소끼리 곱한 결과이고, A @ B는 행렬 곱셈 규칙에 따라 계산한 결과다. 앞으로 NumPy 코드를 읽거나 작성할 때 이 차이를 명확히 구분해야 할 것 같다.

이번 강의에서는 행렬 연산 자체보다도, 어떤 연산이 어떤 조건에서 가능한지를 구분하는 것이 중요하다는 점을 배웠다. 행렬 덧셈과 요소별 곱셈은 두 행렬의 크기가 같아야 하고, 행렬 곱셈은 앞 행렬의 열 개수와 뒤 행렬의 행 개수가 같아야 한다.

아직은 기본적인 연산을 배우는 단계지만, 머신러닝에서 데이터를 벡터와 행렬로 표현한다는 점을 생각하면 앞으로 계속 사용하게 될 개념들인 것 같다.