ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ Python ] 선형대수학 X 파이썬 | 행렬의 연산(직관적, 비직관적)
    Study/Python 2025. 6. 8. 23:42

    군 내에서 아이패드, 사지방 컴퓨터를 이용하여 작성함. 본 내용은 자기개발 목적으로 책 '개발자를 위한 실전 선형대수학-한빛미디어' 과정, 유튜브 등을 참고하였음

     

     

     행렬의 수학적 연산은 직관적인 것비직관적인 것 두 가지 범주로 나뉜다.

     

     

    직관적인 연산
    행렬의 덧셈

     두 행렬을 더할 때, 대응되는 원소끼리 더한다. 행렬의 덧셈은 크기가 같은 두 행렬 사이에서만 성립된다.

     

    $$\begin{bmatrix}0 & 1 & 2 \\4 & 5 & 6 \end{bmatrix}\,+\,\begin{bmatrix}0 & -1 &-2 \\4 & 5&-6 \end{bmatrix}\,=\,\begin{bmatrix} 0& (1-1)&(2-2) \\(4+4) & (5+5) &(6-6)\end{bmatrix}\,=\,\begin{bmatrix}0 & 0&0 \\8 & 10&0 \end{bmatrix}$$

     

    1. Numpy를 사용한 두 행렬끼리의 덧셈 예시 (이중 for문을 사용하였다.)

    import numpy as np
    
    def addMatrix(A,B):
      C = np.zeros(A.shape)  #행렬의 덧셈 결과를 초기화한다.
      for i in range(A.shape[0]):
        for j in range(A.shape[1]):
          C[i,j] = A[i,j] + B[i,j]
      return C
    
    
    M1 = np.zeros((6,4))     #함수 테스트. 두 행렬의 크기는 같아야 한다.
    M2 = np.ones((6,4))
    
    addMatrix(M1,M2)
    
    
    """
    실행결과
    array([[1., 1., 1., 1.],
           [1., 1., 1., 1.],
           [1., 1., 1., 1.],
           [1., 1., 1., 1.],
           [1., 1., 1., 1.],
           [1., 1., 1., 1.]])
    """

     

     

    2. Numpy를 사용하지 않은 예시

    def addMatrix(A,B):
        C = [[A[i][j] + B[i][j] for j in range(len(A[0]))] for i in range(len(A))]
        return C
    
    print(addMatrix([[1,2], [3,4]], [[3,4],[5,6]])) #함수 테스트
    
    
    """
    실행결과
    [[4, 6], [8, 10]]
    """

     

     2차원 배열은 [행][열]로도 표현되므로 행, 열 순서로 접근한다. 두 예시에서 처음 for문은 행, 두 번째 for문은 열을 의미한다. 또한 len() 함수는 행의 개수를 리턴하는데, 2 예시에서 len(A)을 통해 행렬 A의 행의 개수를 알고 len(A[0])을 통해 행렬 A의 첫 번째 행의 길이를 리턴하여 반복한다.

     

     

     

     

    행렬 이동

     벡터와 마찬가지로 행렬에서도 스칼라를 더할 수 없다. 그러나 파이썬에서는 행렬의 요소에 스칼라를 추가하는 브로드캐스팅 연산이 가능하다.(브로드캐스팅 연산은 이전 글에서 확인할 수 있다.)

     

     선형대수학에서 정방 행렬에 스칼라를 더하는 방식을 행렬 이동이라고 한다. 대각에 상숫값을 더하는 것처럼 단위행렬에 스칼라를 곱하여 더하는 방식이다.

     

    $$\mathrm{A\;+\;\lambda I}$$

     

    다음은 행렬 이동의 예제이다.

     

    $$\begin{bmatrix}6 & 2 &8\\2 & 4 &10\\0 &7&9 \end{bmatrix}\,+\,5\begin{bmatrix}1 & 0 &0\\0 & 1 &0\\0 &0&1 \end{bmatrix}\,=\,\begin{bmatrix}11 & 2 &8\\2 & 9 &10\\0 &7&14 \end{bmatrix}$$

     

    위 예제를 코드로 바꾸면 다음과 같다.

    import numpy as np
    
    A = np.array([ [6,2,8],[2,4,10],[0,7,9] ])
    s = 5
    A + s*np.eye(len(A))

     

     np.eye()를 통해 단위 행렬을 생성하여 행렬 이동을 한 것이다.

     

     행렬을 이동하면 대각 원소만 변경된다. 이는 데이터 과학 분야에서 행렬의 수치적 안정성을 높이고 행렬의 가능한 많은 정보를 보존하기 위해 적은 양을 이동한다. 일반적으로 행렬 자체로 정의될 수 있는 값의 일부를 $\lambda$로 설정한다.(노름, 고윳값, 평균과 같은)

     행렬 이동의 주요한 응용으로 행렬의 고윳값을 찾는 메커니즘과 모델을 데이터에 적합시킬 때 행렬을 정규화하는 메커니즘이 있다.

     

     

     

     

    스칼라 곱셈과 아다마르곱

     이 두 곱셈은 벡터와 마찬가지로 행렬에서도 원소별로 작용한다. 먼저 스칼라ㅡ행렬 곱셈은행렬의 각 원소에 동일한 스칼라를 곱한다.

     

    $$\gamma\begin{bmatrix}a & b \\c & d \end{bmatrix}\,=\,\begin{bmatrix}\gamma a &\gamma  b \\\gamma c & \gamma d \end{bmatrix}$$

     

     아다마르곱도 마찬가지로 두 행렬을 요소별로 곱한다.

     

    $$\begin{bmatrix}1 & 2 \\3 & 4 \end{bmatrix}\,⊙\,\begin{bmatrix}a & b \\c & d \end{bmatrix}\,=\,\begin{bmatrix}a & 2b \\3c & 4d \end{bmatrix}$$

     

     아다마르곱은 np.multiply()를 사용해서 구현할 수 있는데, 곱하려는 두 행렬 사이 *를 사용하여 간단하게 구현할 수도 있다. 표준 행렬 곱셈(@)기호와 헷갈리지 않도록 주의한다. 다음은 이 함수를 이용한 코드 예제이다.

    import numpy as np
    
    A = np.random.randn(3,4)
    B = np.random.randn(3,4)
    
    A*B              # 아다마르곱
    np.multiply(A,B) # 아다마르곱

     

     아다마르곱은 개별 곱셈을 하는 과정이 편리하기에 역행렬 계산 등 여러 주제에서 자주 응용된다.

     

     

     

     

    비직관적인 연산
    표준 행렬 곱셈

     표준 행렬 곱셈은 원소별이 아닌 행과 열 단위로 동작한다. 한 행렬의 행과 다른 행렬의 열 사이 스칼라 곱셈의 조직접 집합으로도 말할 수 있다. 이를 행렬 곱셈(matrix multiplication)이라고 한다. 아다마르곱, 스칼라 곱셈과 구별하기 위해 표준이란 용어를 넣었다.

     

     두 행렬을 곱하기 위해서는 행렬의 크기가 서로 짝이 맞을 때 곱할 수 있다. 행렬의 크기는 $M\times N$처럼 행과 열로 나타내는데, 두 행렬의 크기가 서로 다를 수 있으므로 두 번째 행렬의 크기를 $N\times K$로 표기하자. 이 두 행렬의 크기를 나열했을 때 내부 차원의 수로 $N$, 외부 차원의 수로 $M$과 $K$가 있다는 걸 알 수 있다. 행렬 곱셈은 내부 차원의 수가 일치할 때만 유효하고 곱셈의 결과인 행렬의 크기는 외부 차원의 수로 정의 된다.

     

    $$\mathrm{[\; M × N \;]\; [\; N × K\; ]\; =\; [\; M × K \;]}$$

     

     쉽게 얘기해서 행렬 곱셈은 첫 번째 행렬의 열 수와 두 번째 행렬의 행 수가 같을 때 유효하다. 곱셈 행렬의 크기는 첫 번째 행렬의 행 수와 두 번째 행렬의 열 수로 정의된다.

     행렬 곱셈에서 두 행렬 A, B가 있을 때 A B는 유효하더라도 B A 는 유효하지 않을 수 있다. 또 두 곱셈이 유효하더라도(두 행렬이 정방 행렬일 때) 두 곱셈 행렬이 서로 다를 수 있다.

     

     첫 번째 행렬의 열 수가 두 번째 행렬의 행 수와 일치하는 경우에만 행렬 곱셈이 유효하다는 건데, 그 이유로 곱셈 행렬의 $(i,\,j)$번째 원소가 첫 번째 행렬의 $i$번째 행과 두 번째 행렬의 $j$번째 열 사이의 내적이기 때문이다.

    다음은 아다마르곱에서 사용했던 예제를 이용하여 행렬 곱셈을 나타낸 것이다.

     

    $$\begin{bmatrix}1 & 2 \\3 & 4 \end{bmatrix}\,\begin{bmatrix}a & b \\c & d \end{bmatrix}\,=\,\begin{bmatrix}(a\,+\,2c) & (b\,+\,2d) \\(3a\,+\,4c) & (3b\,+\,4d)\end{bmatrix}$$

     

     내적은 두 벡터 사이의 관계를 인코딩한 숫자이다. 그러므로 행렬 곱셈의 결과는 첫 번째 행렬의 행과 두 번째 행렬의 열 사이의 모든 쌍에 대한 선형 관계를 저장하는 행렬이다. 따라서 행렬 곱셈은 공분산, 상관 행렬, 일반 선형 모델 등 수많은 응용의 기초가 되기에 중요하다.

     

    아래 코드는 행렬 곱셈의 예시이다.

    import numpy as np
    
    A = np.array(
        [ [1, 2],[3, 4] ]
    )
    
    B = np.array(
        [ [2, 3],[4, 5] ]
    )
    
    C = A@B
    print(C)
    
    
    """
    실행결과
    [[10 13]
     [22 29]]
    """

     

     

     

     

    행렬과 벡터의 곱셈

     행렬과 벡터를 곱하는 것은 행렬 하나가 벡터인 행렬 곱셈인 것이다. 행렬ㅡ벡터 곱셈은 데이터 과학, 머신러닝, 컴퓨터 그래픽 등 많은 곳에서 사용된다. 통계에서 모델 예측 데이터값을 설계 행렬에 회귀 계수를 곱하여 구하고, 기하학적 구조나 컴퓨터 그래픽에서 이미지 좌표 집합은 수학적 변환 행렬을 통해 변환될수 있으며 $\mathrm{T\; p}$로 나타낸다. $\mathrm{T}$는 변환 행렬이고 $\mathrm{p}$는 기하학적 좌표 집합이다.

     

     행렬ㅡ벡터 곱셈에서 행벡터가 아닌 열벡터만 행렬의 오른쪽에 곱할 수 있고, 열벡터가 아닌 행벡터만 행렬의 왼쪽에 곱할 수 있다. 즉 $\mathrm{A\,v}$와 $\mathrm{v \; ^T A}$는 유효하나 $\mathrm{A\, v\;^T}$나 $\mathrm{v\, A}$는 유효하지 않다. 행렬 크기를 보면 $M\times N$ 행렬의 왼쪽에 $1\times M$ 행렬(행벡터)을 곱하거나 오른쪽에 $N\times 1$ 행렬(열벡터)을 곱할 수 있다. (행렬벡터의 곱셈에서 내적을 하려면 서로 짝짓는 쌍이 필요하기 때문이다.)

     행렬ㅡ벡터 곱셈의 결과는 항상 벡터이고 결과 벡터의 방향은 곱하는 벡터의 방향에 따라 결정된다. 행렬에 행벡터를 앞에서 곱하면 다른 행벡터가 생성되나 열벡터를 뒤에서 곱하면 다른 열벡터가 생성된다.

     

     


     

    행렬의 연산
    전치 연산

     행렬의 전치는 벡터의 전치와 마찬가지로 단순히 행과 열을 바꾸는 것이다. ($\mathrm{C\,^T}$는 $\mathrm{C}$의 전치이다.) 행렬의 이중 전치는 원래 행렬이 된다.($\mathrm{C\,^{TT}\,=\,C}$)

     

    $$a_{i,\,j}^\mathrm{T}\,=\,a_{j,\,i}$$

     

     이전 게시글에서 전치에 대해 정의했으므로 이 글에서는 행렬 전치에 관해 자세히 살펴보겠다.

     다음은 행렬 전치의 예제이다.

     

    $$\begin{bmatrix}2 & 1 &4\\7 & 3&6 \end{bmatrix}^\mathrm{T}\,=\,\begin{bmatrix}2 & 7 \\1 & 3 \\4&6\end{bmatrix}$$

     

     Numpy에서 메서드를 통해 전치하는 방법이 존재한다.

    import numpy as np
    
    A = np.array([ [2,1,4], [7,3,6] ])
    A_T1 = A.T
    A_T2 = np.transpose(A)
    
    print(A)
    print(A_T1)
    print(A_T2)
    
    
    """
    실행결과
    [[2 1 4]
     [7 3 6]]
    [[2 7]
     [1 3]
     [4 6]]
    [[2 7]
     [1 3]
     [4 6]]
     """

     

     벡터의 내적을 $\mathrm{a^Tb}$로 표기하는 이유가 있다. 크기가 $M\times 1$인 두 열벡터에서 첫 번째 벡터를 전치하면 크기가 $1\times M$이 되므로 내부 차원이 일치하고 외부 차원을 통해 곱셈의 결과가 $1\times 1$, 즉 스칼라임을 알 수 있다. 마찬가지로 벡터의 외적을 동일하게 추론할 수 있는데, 크기가 $M\times 1$과 $1\times N$인 열벡터와 행벡터를 곱하면 내부 차원이 일치하므로 결과의 크기는 $M\times N$이 된다.

     

     

     

     

    LIVE EVIL(연산 순서)

     LIVE EVIL이란 팰린드롬(뒤집어도 철자가 동일한 단어, 구) 행렬의 곱셈을 전치할 때 순서가 어떻게 되는지 기억하기 위한 연상법이다. 아래 식이 성립하기 위해 L, I, V, E는 각각 행렬이며 크기는 서로 짝이 맞다고 가정한다.

     

    $$\mathrm{(L\,I\,V\,E)^T\,=\,E^T\,V^T\,I^T\,L^T}$$

     

     이 순서는 여러 행렬의 곱을 전치하는 유일한 방법이다.

     다음은 네 개의 난수 행렬을 통해 LIVE EVIL 규칙에 따라 전치했을 때 각각의 결과가 일치하는지 확인하는 예시이다. (네 개의 코드는 다음과 같다. $\mathrm{L}\in \mathbb{R}^{3\times 4}$, $\mathrm{I}\in \mathbb{R}^{4\times 5}$, $\mathrm{V}\in \mathbb{R}^{5\times 6}$, $\mathrm{V}\in \mathbb{R}^{6\times 3}$)

    #네 개의 난수 행렬을 생성
    L = np.random.randn(3,4)
    I = np.random.randn(4,5)
    V = np.random.randn(5,6)
    E = np.random.randn(6,3)
    
    #res1, res2, res3은 모두 같다.
    res1 = ( L@I@V@E ).T
    res2 = L.T @ I.T @ V.T @ E.T
    res3 = E.T @ V.T @ I.T @ L.T

     

     

     

     

    대칭 행렬

     대칭 행렬(Symmetric matrix)은 수치적으로 안정적인 경향이 있고 컴퓨터 알고리즘을 처리하는 등 여러 작업에 편리하다. 행렬이 대칭이라는 것은 대응되는 행과 열이 같다는 것을 의미한다. 즉 대칭 행렬은 자신의 전치 행렬과 같다.($\mathrm{A^T\,=\,A}$이면 행렬 $\mathrm{A}$는 대칭이다.)

     

    $$\begin{bmatrix}a & e &f&g \\e & b &h &i\\f&h&c&j\\g&i&j&d \end{bmatrix}$$

     

     $M=N$이 아니면 즉 행렬이 정방이 아니라면 대칭일 수 없다.

     

     어떤 행렬이든 자신의 전치를 곱하면 정방 대칭 행렬이 생성된다. 즉 $\mathrm{A\,^TA}$와 $\mathrm{A\,A\,^T}$는 모두 정방 대칭 행렬이다.(행렬 $\mathrm{A}$는 비대칭(비정방)이어도 상관없다.)

     

     정방의 대칭이 정방이라는 것을 증명할 필요가 없기에 비정방의 대칭이 비정방이라는 것을 증명해보자. 만약 $\mathrm{A}$가 $M\times N$이라면 $\mathrm{A\,^TA}$는 $(N\times M)(M\times N)$이므로 곱셈 행렬의 크기는 $N\times N$이다. $\mathrm{A\,A\,^T}$에서도 동일하다. 다음으로 대칭성을 증명하기 위해 대칭 행렬은 어떤 행렬과 그 전치 행렬이 동일하므로 $\mathrm{A\,^TA}$을 LIVE EVIL 규칙에 따라 전치하면 다음과 같다.

     

    $$\mathrm{(A\,^TA)^T\,=\, A^T\,A\,^{TT}\,=\,A\,^TA}$$

     

    즉 $\mathrm{(A\,^TA)^T\,=\,A\,^TA}$이므로 이 행렬은 대칭이다. 따라서 $\mathrm{A\,^TA}$는 정방 대칭이다. $\mathrm{A\,A\,^T}$도 마찬가지이나 서로 크기가 다르기에 $\mathrm{A\,^TA}$ 와 같은 행렬이라 볼 수 없다.

     

    $\mathrm{A\,^TA}$로 대칭 행렬을 만드는 것을 곱셈 기법(Multiplicative method)이라 한다.

     

     

    다음 코드는 난수 정방 행렬을 생성하여 그 행렬이 대칭이면 True, 비대칭이면 Fasle 불리언을 출력한다. 정밀도 오류나 대칭 행렬이 생성되는 경우 불리언 값이 다르게 나올 수 있다. 적절한 오차(SSE)를 통해 동일성을 테스트 하면 된다.

    def isMatrixSymmetric(S):
      D = S-S.T #행렬과 전치 행렬 사이의 차이를 구한다.
    
      #오차의 제곱값이 임계값 보다 작은지 확인하는 과정.
      sse = np.sum(D**2)
      return sse<10**-15  #오차의 제곱값(SSE)이 매우 작으면 TRUE, 즉 대칭이고 비대칭이면 FALSE를 출력한다.
    
    
    #실행예시
    A = np.random.randn(4,4)
    AtA = A.T@A
    
    print(isMatrixSymmetric(A))
    print(isMatrixSymmetric(AtA))
    
    
    """
    실행결과
    False
    True
    """

     

제목 없는 코딩 블로그