벡터, 행렬과 연산

모든 신경망은 몇 단계를 더 얹은 행렬 곱셈입니다.

유형: Build 언어: Python, Julia 선수 지식: Phase 1, Lesson 01 (선형대수 직관) 예상 시간: 약 60분

학습 목표

  • 원소별 연산(element-wise operation), 행렬 곱셈(matrix multiplication), 전치(transpose), 행렬식(determinant), 역행렬(inverse)을 포함한 Matrix class를 만듭니다.
  • 원소별 곱셈(element-wise multiplication)과 행렬 곱셈(matrix multiplication)을 구분하고, 각각 언제 쓰이는지 설명합니다.
  • 직접 만든 Matrix class만 사용해 dense neural network layer 하나, 즉 relu(W @ x + b)를 구현합니다.
  • 브로드캐스팅 규칙(broadcasting rule)과 신경망 프레임워크(neural network framework)에서 편향 더하기(bias addition)가 어떻게 동작하는지 설명합니다.

문제

신경망을 만들고 싶다고 합시다. 코드를 읽다가 다음 줄을 봅니다.

output = activation(weights @ input + bias)

여기서 @는 행렬 곱셈입니다. weights는 행렬이고, input은 벡터입니다. 이 연산들이 무엇을 하는지 모르면 이 줄은 마법처럼 보입니다. 하지만 알고 나면 층(layer)의 전체 순전파(forward pass)가 세 가지 연산으로 표현된 것임을 알 수 있습니다.

모델이 처리하는 모든 이미지는 픽셀 값으로 된 행렬입니다. 모든 단어 임베딩(word embedding)은 벡터입니다. 모든 신경망의 모든 층은 행렬 변환입니다. 변수를 이해하지 못하면 코드를 쓸 수 없듯이, 행렬 연산에 익숙하지 않으면 AI 시스템을 만들 수 없습니다.

이 강의에서는 그 익숙함을 직접 구현하면서 만듭니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.행렬 곱셈(matrix multiplication) `(m x n) @ (n x p)`에서 차원(dimension)은 어떤 조건을 만족해야 하나요?

2.항등행렬(Identity matrix)은 무엇인가요?

0/2 답변 완료

개념

벡터: 순서가 있는 숫자 목록

벡터는 방향과 크기를 가진 숫자 목록입니다. AI에서 벡터는 데이터 점(data point), 특징(feature), 파라미터(parameter)를 나타냅니다.

v = [3, 4]        -- 2D vector
w = [1, 0, -2]    -- 3D vector

2D 벡터 [3, 4]는 평면의 좌표 (3, 4)를 가리킵니다. 길이, 즉 크기(magnitude)는 5입니다. 3-4-5 삼각형입니다.

행렬: 숫자 격자

행렬은 2차원 격자(grid)입니다. 행(row)과 열(column)이 있습니다. m x n 행렬은 행이 m개, 열이 n개입니다.

A = | 1  2  3 |     -- 2x3 matrix (2 rows, 3 columns)
    | 4  5  6 |

신경망에서 가중치 행렬(weight matrix)은 입력 벡터를 출력 벡터로 변환합니다. 입력이 784개이고 출력이 128개인 층은 128x784 가중치 행렬을 사용합니다.

형태(shape)가 중요한 이유

행렬 곱셈에는 엄격한 규칙이 있습니다. (m x n) @ (n x p) = (m x p)입니다. 안쪽 차원(dimension)이 반드시 맞아야 합니다.

(128 x 784) @ (784 x 1) = (128 x 1)
  weights       input       output

Inner dimensions: 784 = 784  -- valid

PyTorch에서 shape mismatch error를 만나면 대부분 이 규칙 때문입니다.

연산 지도

연산하는 일신경망에서의 사용
Addition같은 위치의 element를 더함Output에 bias 더하기
Scalar multiply모든 element를 scaleLearning rate * gradient
Matrix multiply벡터를 변환Layer forward pass
TransposeRow와 column을 뒤집음Backpropagation
행렬식(Determinant)행렬을 요약하는 하나의 숫자Invertibility 확인
Inverse변환을 되돌림Linear system 풀기
Identity아무것도 하지 않는 행렬Initialization, residual connection

원소별 곱셈(element-wise multiplication)과 행렬 곱셈(matrix multiplication)

초보자가 자주 헷갈리는 지점입니다.

Element-wise multiplication은 같은 위치끼리 곱합니다. 두 행렬은 같은 shape이어야 합니다.

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

Matrix multiplication은 row와 column의 내적입니다. 안쪽 dimension이 맞아야 합니다.

| 1  2 |   | 5  6 |   | 1*5+2*7  1*6+2*8 |   | 19  22 |
| 3  4 | @ | 7  8 | = | 3*5+4*7  3*6+4*8 | = | 43  50 |

서로 다른 연산이고, 결과도 다르며, 규칙도 다릅니다.

브로드캐스팅(Broadcasting)

Output matrix에 bias vector를 더할 때 shape가 딱 맞지 않을 수 있습니다. 브로드캐스팅(Broadcasting)은 더 작은 array를 필요한 모양으로 늘려 맞춥니다.

| 1  2  3 |   +   [10, 20, 30]
| 4  5  6 |

브로드캐스팅은 벡터를 row 방향으로 늘립니다.

| 1  2  3 |   | 10  20  30 |   | 11  22  33 |
| 4  5  6 | + | 10  20  30 | = | 14  25  36 |

현대 framework는 이를 자동으로 처리합니다. 브로드캐스팅을 이해하면 shape가 이상해 보이는데도 코드가 실행되는 상황을 이해할 수 있습니다.

직접 만들기

Step 1: Vector class

class Vector:
    def __init__(self, data):
        self.data = list(data)
        self.size = len(self.data)

    def __repr__(self):
        return f"Vector({self.data})"

    def __add__(self, other):
        return Vector([a + b for a, b in zip(self.data, other.data)])

    def __sub__(self, other):
        return Vector([a - b for a, b in zip(self.data, other.data)])

    def __mul__(self, scalar):
        return Vector([x * scalar for x in self.data])

    def dot(self, other):
        return sum(a * b for a, b in zip(self.data, other.data))

    def magnitude(self):
        return sum(x ** 2 for x in self.data) ** 0.5

Step 2: 핵심 연산이 있는 Matrix class

class Matrix:
    def __init__(self, data):
        self.data = [list(row) for row in data]
        self.rows = len(self.data)
        self.cols = len(self.data[0])
        self.shape = (self.rows, self.cols)

    def __repr__(self):
        rows_str = "\n  ".join(str(row) for row in self.data)
        return f"Matrix({self.shape}):\n  {rows_str}"

    def __add__(self, other):
        return Matrix([
            [self.data[i][j] + other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def __sub__(self, other):
        return Matrix([
            [self.data[i][j] - other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def scalar_multiply(self, scalar):
        return Matrix([
            [self.data[i][j] * scalar for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def element_wise_multiply(self, other):
        return Matrix([
            [self.data[i][j] * other.data[i][j] for j in range(self.cols)]
            for i in range(self.rows)
        ])

    def matmul(self, other):
        return Matrix([
            [
                sum(self.data[i][k] * other.data[k][j] for k in range(self.cols))
                for j in range(other.cols)
            ]
            for i in range(self.rows)
        ])

    def transpose(self):
        return Matrix([
            [self.data[j][i] for j in range(self.rows)]
            for i in range(self.cols)
        ])

    def determinant(self):
        if self.shape == (1, 1):
            return self.data[0][0]
        if self.shape == (2, 2):
            return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0]
        det = 0
        for j in range(self.cols):
            minor = Matrix([
                [self.data[i][k] for k in range(self.cols) if k != j]
                for i in range(1, self.rows)
            ])
            det += ((-1) ** j) * self.data[0][j] * minor.determinant()
        return det

    def inverse_2x2(self):
        det = self.determinant()
        if det == 0:
            raise ValueError("행렬이 singular라 inverse가 없습니다")
        return Matrix([
            [self.data[1][1] / det, -self.data[0][1] / det],
            [-self.data[1][0] / det, self.data[0][0] / det]
        ])

    @staticmethod
    def identity(n):
        return Matrix([
            [1 if i == j else 0 for j in range(n)]
            for i in range(n)
        ])

Step 3: 동작 확인

A = Matrix([[1, 2], [3, 4]])
B = Matrix([[5, 6], [7, 8]])

print("A + B =", (A + B).data)
print("A @ B =", A.matmul(B).data)
print("A^T =", A.transpose().data)
print("det(A) =", A.determinant())
print("A^-1 =", A.inverse_2x2().data)

I = Matrix.identity(2)
print("A @ A^-1 =", A.matmul(A.inverse_2x2()).data)

Step 4: Neural network와 연결하기

import random

inputs = Matrix([[0.5], [0.8], [0.2]])
weights = Matrix([
    [random.uniform(-1, 1) for _ in range(3)]
    for _ in range(2)
])
bias = Matrix([[0.1], [0.1]])

def relu_matrix(m):
    return Matrix([[max(0, val) for val in row] for row in m.data])

pre_activation = weights.matmul(inputs) + bias
output = relu_matrix(pre_activation)

print(f"입력 shape: {inputs.shape}")
print(f"Weight shape: {weights.shape}")
print(f"출력 shape: {output.shape}")
print(f"출력: {output.data}")

이것이 하나의 dense layer입니다. output = relu(W @ x + b). 모든 신경망의 모든 dense layer는 정확히 이 일을 합니다.

사용해보기

NumPy는 위의 모든 일을 훨씬 적은 줄로, 훨씬 빠르게 처리합니다.

import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print("A + B =\n", A + B)
print("A * B (element-wise multiplication) =\n", A * B)
print("A @ B (matrix multiplication) =\n", A @ B)
print("A^T =\n", A.T)
print("det(A) =", np.linalg.det(A))
print("A^-1 =\n", np.linalg.inv(A))
print("I =\n", np.eye(2))

inputs = np.random.randn(3, 1)
weights = np.random.randn(2, 3)
bias = np.array([[0.1], [0.1]])
output = np.maximum(0, weights @ inputs + bias)

print(f"\n신경망 layer: {weights.shape} @ {inputs.shape} = {output.shape}")
print(f"출력:\n{output}")

Python의 @ 연산자는 __matmul__을 호출합니다. NumPy는 C와 Fortran으로 작성된 최적화된 BLAS 루틴(routine)으로 이를 구현합니다. 같은 수학이지만 100배 이상 빠를 수 있습니다.

NumPy에서 브로드캐스팅(broadcasting)은 다음처럼 동작합니다.

matrix = np.array([[1, 2, 3], [4, 5, 6]])
bias = np.array([10, 20, 30])
print(matrix + bias)

NumPy는 1D 편향을 두 행 전체에 자동으로 브로드캐스트합니다. 모든 신경망 프레임워크에서 편향 더하기가 이런 방식으로 동작합니다.

산출물 만들기

이 강의의 산출물은 행렬 연산을 기하학적 직관으로 설명하도록 돕는 프롬프트입니다.

  • outputs/prompt-matrix-operations.md

또한 여기서 만든 Matrix class는 Phase 3, Lesson 10에서 만들 간단한 인공신경망 프레임워크(mini neural network framework)의 기반이 됩니다.

연습문제

  1. 역행렬 검증: A @ A.inverse_2x2()를 곱해 항등행렬(identity matrix)이 나오는지 확인합니다. 서로 다른 2x2 행렬 세 개로 시도합니다. 행렬식이 0이면 어떤 일이 일어나는지 확인합니다.
  2. 3x3 역행렬 구현: 수반행렬(adjugate) 방법을 사용해 3x3 행렬 역행렬을 계산하도록 Matrix 클래스를 확장합니다. NumPy의 np.linalg.inv와 비교해 테스트합니다.
  3. 2층(two-layer) 신경망 만들기: NumPy 없이 직접 만든 Matrix 클래스만 사용해 input (3) -> hidden (4) -> output (2) 구조의 2층 신경망을 만듭니다. 무작위 가중치를 초기화하고 순전파를 실행한 뒤 모든 형태가 맞는지 확인합니다.

핵심 용어

용어흔한 설명실제 의미
벡터(Vector)화살표순서가 있는 숫자 목록입니다. AI에서는 고차원 공간(high-dimensional space)의 점입니다.
행렬(Matrix)숫자 표선형 변환(linear transformation)입니다. 벡터를 한 공간에서 다른 공간으로 매핑합니다.
행렬 곱셈(Matrix multiplication)그냥 숫자를 곱하는 것첫 번째 행렬의 각 행과 두 번째 행렬의 각 열 사이의 내적입니다. 순서가 중요합니다.
전치(Transpose)뒤집기행과 열을 바꿉니다. m x n 행렬을 n x m으로 바꾸며 역전파(backpropagation)에서 중요합니다.
행렬식(Determinant)행렬에서 나오는 어떤 숫자행렬이 면적(2D)이나 부피(3D)를 얼마나 스케일링하는지 나타냅니다. 0이면 변환이 한 차원을 눌러 없앤다는 뜻입니다.
역행렬(Inverse)행렬을 되돌리는 것변환을 되돌리는 행렬입니다. 행렬식이 0이 아닐 때만 존재합니다.
항등행렬(Identity matrix)지루한 행렬숫자 1을 곱하는 것과 같은 행렬입니다. 잔차 연결(residual connection, ResNet)에도 쓰입니다.
브로드캐스팅(Broadcasting)마법 같은 형태 수정작은 배열(array)을 누락된 차원(missing dimension) 방향으로 반복해 큰 배열에 맞추는 규칙입니다.
원소별 연산(Element-wise)일반적인 곱셈같은 위치끼리 곱합니다. 두 배열은 같은 형태이거나 브로드캐스트 가능해야 합니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 2개

matrices
Code
matrices
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

prompt-matrix-operations

Teaches matrix operations through geometric intuition, connecting abstract math to neural network mechanics

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.원소별 곱셈(element-wise multiplication)과 행렬 곱셈(matrix multiplication)의 핵심 차이는 무엇인가요?

2.`output = relu(W @ x + b)` 식에서 브로드캐스팅(broadcasting)은 어떤 역할을 하나요?

3.행렬의 행렬식(determinant)이 0이라는 것은 무엇을 의미하나요?

0/3 답변 완료