파이썬에서 행렬을 곱하는 3가지 방법

파이썬에서 행렬 곱셈 완벽 가이드

이 튜토리얼은 파이썬을 이용하여 두 행렬을 곱하는 다양한 방법을 상세히 설명합니다. 행렬 곱셈의 유효성 조건부터 시작하여, 사용자 정의 함수, 중첩 리스트 컴프리헨션, 그리고 NumPy 라이브러리를 활용한 효율적인 곱셈 기법까지 폭넓게 다룹니다.

행렬 곱셈 유효성 검증

행렬 곱셈을 위한 파이썬 코드를 작성하기 전에, 행렬 곱셈의 기본 원리를 정확히 이해하는 것이 중요합니다. 핵심은 두 행렬 A와 B의 곱셈이 가능하려면, 행렬 A의 열의 개수가 행렬 B의 행의 개수와 일치해야 한다는 것입니다.

이 조건은 행렬 곱셈의 연산 방식에서 비롯됩니다. 행렬 A의 각 행과 행렬 B의 각 열의 내적을 계산하는 과정에서, 두 벡터(행과 열)의 요소 수가 같아야 내적이 정의될 수 있기 때문입니다.

결과 행렬의 형태

결과 행렬 C의 (i, j) 위치의 요소는 행렬 A의 i번째 행과 행렬 B의 j번째 열의 내적 값으로 결정됩니다. 결과적으로, 행렬 C는 행렬 A의 행의 수와 행렬 B의 열의 수를 각각 행과 열의 개수로 갖게 됩니다.

예를 들어, 행렬 A가 m x n 크기이고 행렬 B가 n x p 크기라면, 곱셈 결과인 행렬 C는 m x p 크기를 갖습니다.

두 벡터 a와 b의 내적은 아래와 같은 공식으로 표현됩니다.

  • 내적은 동일한 길이의 벡터 간에만 정의됩니다.
  • 행렬 곱셈에서 내적 연산이 유효하려면, 두 행렬의 행과 열이 같은 수의 요소를 가져야 합니다.
  • 행렬 A의 모든 행은 n개의 요소를 가지며, 행렬 B의 모든 열도 n개의 요소를 가집니다.

이러한 조건을 만족해야 행렬 A의 열의 개수와 행렬 B의 행의 개수가 같아지고, 결과적으로 행렬 곱셈이 유효하게 됩니다.

이제 행렬 곱셈의 유효성 조건과 결과 행렬의 각 요소를 어떻게 계산하는지 이해하셨을 것입니다.

사용자 정의 파이썬 함수를 이용한 행렬 곱셈

이제 행렬 곱셈을 수행하는 사용자 정의 파이썬 함수를 만들어 보겠습니다. 이 함수는 다음과 같은 기능을 수행해야 합니다:

  • 두 행렬 A와 B를 입력으로 받습니다.
  • 행렬 곱셈이 가능한지 확인합니다.
  • 가능한 경우, 곱셈 결과를 반환합니다.
  • 불가능한 경우, 오류 메시지를 반환합니다.

먼저 NumPy의 `random.randint()` 함수를 사용하여 정수 행렬을 생성합니다. 물론, 중첩된 리스트 형태로 직접 행렬을 정의할 수도 있습니다.

import numpy as np
np.random.seed(27)
A = np.random.randint(1,10,size = (3,3))
B = np.random.randint(1,10,size = (3,2))
print(f"Matrix A: {A} ")
print(f"Matrix B: {B} ")

# 출력
Matrix A:
 [[4 9 9]
 [9 1 6]
 [9 2 3]]

Matrix B:
 [[2 2]
 [5 7]
 [4 4]]

다음으로, `multiply_matrix(A, B)` 함수를 정의합니다. 이 함수는 행렬 곱셈이 유효할 때 결과 행렬 C를 반환합니다.

def multiply_matrix(A,B):
    global C
    if  A.shape[1] == B.shape[0]:
        C = np.zeros((A.shape[0],B.shape[1]),dtype = int)
        for row in range(A.shape[0]): 
            for col in range(B.shape[1]):
                for elt in range(A.shape[1]):
                    C[row, col] += A[row, elt] * B[elt, col]
        return C
    else:
        return "행렬 A와 B를 곱할 수 없습니다."

함수 정의 분석

함수 정의를 자세히 살펴보겠습니다:

C를 전역 변수로 선언: 파이썬 함수 내부에서 정의된 변수는 기본적으로 지역 범위를 갖습니다. 따라서, 함수 외부에서 `C`에 접근하려면 전역 변수로 선언해야 합니다. `global` 키워드를 변수 이름 앞에 추가하여 이를 수행할 수 있습니다.

행렬 곱셈 가능성 검증: `shape` 속성을 사용하여 행렬 곱셈이 가능한지 확인합니다. `arr.shape[0]`과 `arr.shape[1]`은 각각 배열의 행과 열의 수를 나타냅니다. 따라서, `A.shape[1] == B.shape[0]` 조건이 참일 때만 행렬 곱셈이 수행됩니다. 그렇지 않으면 오류 메시지를 반환합니다.

중첩 루프를 사용한 값 계산: 결과 행렬의 각 요소를 계산하기 위해 중첩된 for 루프를 사용합니다. 외부 루프는 행렬 A의 행을, 중간 루프는 행렬 B의 열을, 내부 루프는 선택된 열의 각 요소에 접근합니다.

이제 함수가 어떻게 동작하는지 이해했으니, 앞에서 생성한 행렬 A와 B를 인수로 전달하여 함수를 호출해 보겠습니다.

multiply_matrix(A,B)

# 출력
array([[ 89, 107],
       [ 47,  49],
       [ 40,  44]])

행렬 A와 B의 곱셈이 가능하므로, `multiply_matrix()` 함수는 곱셈 결과를 정상적으로 반환합니다.

파이썬 리스트 컴프리헨션을 사용한 행렬 곱셈

이제 중첩된 리스트 컴프리헨션을 사용하여 동일한 행렬 곱셈 작업을 수행해 보겠습니다.

처음에는 복잡해 보일 수 있지만, 리스트 컴프리헨션을 단계별로 분해하여 이해해 봅시다.

리스트 컴프리헨션의 일반적인 형태는 다음과 같습니다.

[<do-this> for <item> in <iterable>]

where,
<do-this>: 수행할 연산 또는 표현식
<item>: 연산을 적용할 각 항목
<iterable>: 반복 가능한 객체 (리스트, 튜플 등)

파이썬 리스트 컴프리헨션 심층 가이드를 통해 더 자세한 내용을 확인할 수 있습니다.

리스트 컴프리헨션을 사용해서 행렬 C의 각 행을 차례대로 생성해 보겠습니다.

중첩 리스트 컴프리헨션 설명

1단계: 행렬 C의 단일 값 계산

행렬 A의 i번째 행과 행렬 B의 j번째 열이 주어졌을 때, 다음 표현식은 행렬 C의 (i, j) 위치의 요소를 계산합니다.

sum(a*b for a,b in zip(A_row, B_col)

# zip(A_row, B_col)은 튜플의 반복자를 반환합니다
# A_row = [a1, a2, a3] & B_col = [b1, b2, b3] 일 때
# zip(A_row, B_col)은 (a1, b1), (a2, b2) 등을 반환합니다.

i와 j가 모두 1일 때, 이 표현식은 행렬 C의 `c_11` 요소를 반환합니다. 즉, 이 방법을 통해 행렬 C의 한 행에서 하나의 요소를 계산할 수 있습니다.

2단계: 행렬 C의 한 행 만들기

다음 목표는 행렬 C의 전체 행을 만드는 것입니다. 이를 위해, 행렬 A의 첫 번째 행에 대해 행렬 B의 모든 열을 반복해야 합니다.

다시 한번 리스트 컴프리헨션 템플릿으로 돌아가서:

  • `<do-this>`를 1단계의 표현식으로 대체합니다.
  • `<item>`을 `B_col` (행렬 B의 각 열)로 대체합니다.
  • `<iterable>`을 행렬 B의 모든 열을 포함하는 리스트인 `zip(*B)`로 대체합니다.

다음은 첫 번째 리스트 컴프리헨션입니다.

[sum(a*b for a,b in zip(A_row, B_col)) for B_col in zip(*B)] 

# zip(*B): *는 언패킹 연산자
# zip(*B)는 행렬 B의 열 리스트를 반환합니다.

3단계: 모든 행을 만들고 행렬 C 완성

다음으로 나머지 행을 계산하여 곱셈 행렬 C를 완성해야 합니다. 이를 위해 행렬 A의 모든 행을 반복합니다.

리스트 컴프리헨션으로 돌아가서 다음을 수행합니다.

  • `<do-this>`를 2단계의 리스트 컴프리헨션으로 대체합니다. 이전에 전체 행을 계산했음을 기억하십시오.
  • `<item>`을 행렬 A의 각 행인 `A_row`로 대체합니다.
  • `<iterable>`을 행렬 A 자체로 대체합니다. 행을 반복하기 때문입니다.

그리고 최종적인 중첩 리스트 컴프리헨션은 다음과 같습니다. 🎊

[[sum(a*b for a,b in zip(A_row, B_col)) for B_col in zip(*B)]
     for A_row in A]

이제 결과를 확인해 볼 시간입니다! ✔

# np.array()를 사용해서 NumPy 배열로 변환합니다.
C = np.array([[sum(a*b for a,b in zip(A_row, B_col)) for B_col in zip(*B)] 
    for A_row in A])

# 출력:
[[ 89 107]
 [ 47  49]
 [ 40  44]]

결과를 살펴보면, 이전에 사용했던 중첩 for 루프와 동일한 결과를 얻을 수 있습니다. 다만, 코드가 더욱 간결해졌을 뿐입니다.

또한, 일부 내장 함수를 사용하여 더 효율적으로 행렬 곱셈을 수행할 수 있습니다. 다음 섹션에서 이 방법을 알아보겠습니다.

NumPy `matmul()` 함수를 사용한 행렬 곱셈

`np.matmul()` 함수는 두 행렬을 입력으로 받아, 행렬 곱셈이 유효한 경우 결과 행렬을 반환합니다.

C = np.matmul(A,B)
print(C)

# 출력:
[[ 89 107]
 [ 47  49]
 [ 40  44]]

이 방법이 이전의 두 방법에 비해 훨씬 간단합니다. 사실, `np.matmul()` 대신 동일한 기능을 하는 `@` 연산자를 사용할 수도 있습니다.

`@` 연산자를 사용한 행렬 곱셈

파이썬에서 `@` 연산자는 행렬 곱셈을 위한 이항 연산자입니다.

일반적으로 N차원 NumPy 배열에서 작동하며, 두 행렬의 곱셈 결과를 반환합니다.

참고: `@` 연산자를 사용하려면 파이썬 3.5 이상이 필요합니다.

사용 방법은 다음과 같습니다.

C = A @ B
print(C)

# 출력
array([[ 89, 107],
       [ 47,  49],
       [ 40,  44]])

곱셈 결과 행렬 C는 이전에 얻은 결과와 동일합니다.

`np.dot()` 함수를 사용한 행렬 곱셈

`np.dot()` 함수를 사용하여 두 행렬을 곱하는 코드를 본 적이 있을 수 있습니다. 사용법은 다음과 같습니다.

C = np.dot(A,B)
print(C)

# 출력:
[[ 89 107]
 [ 47  49]
 [ 40  44]]

`np.dot(A, B)` 또한 예상대로 곱셈 결과를 반환한다는 것을 알 수 있습니다.

하지만 NumPy 공식 문서에서는 `np.dot()` 함수는 행렬 곱셈보다는 두 1차원 벡터의 내적을 계산할 때 사용하는 것이 좋다고 명시하고 있습니다.

이전 섹션에서 언급했듯이, 행렬 C의 (i, j) 위치의 요소는 행렬 A의 i번째 행과 행렬 B의 j번째 열의 내적입니다.

NumPy는 이러한 내적 연산을 모든 행과 열에 대해 암묵적으로 브로드캐스팅하여 행렬 곱셈 결과를 제공합니다. 그러나 코드 가독성을 유지하고 모호성을 피하려면 `np.matmul()` 함수 또는 `@` 연산자를 사용하는 것이 좋습니다.

결론

이 튜토리얼을 통해 다음 내용을 배웠습니다:

  • 행렬 곱셈이 유효하기 위한 조건: 행렬 A의 열 개수 = 행렬 B의 행 개수
  • 행렬 곱셈이 유효한지 확인하고, 중첩 for 루프를 사용하여 행렬 곱셈 결과를 반환하는 사용자 정의 파이썬 함수를 만드는 방법
  • 중첩 리스트 컴프리헨션을 사용하여 행렬을 곱하는 방법: for 루프보다 간결하지만 가독성 문제가 발생할 수 있습니다.
  • NumPy 내장 함수 `np.matmul()`을 사용하여 행렬을 곱하는 방법: 속도 측면에서 가장 효율적인 방법입니다.
  • 파이썬에서 두 행렬을 곱하는 `@` 연산자에 대한 이해

이것으로 파이썬에서 행렬 곱셈에 대한 논의를 마치겠습니다. 다음 단계로, 파이썬에서 숫자가 소수인지 확인하는 방법이나 흥미로운 파이썬 문자열 문제를 해결해 보세요.

즐거운 학습 되세요! 🎉