본문 바로가기

딥러닝/딥러닝 수학 기초

선형대수 핵심 (벡터, 행렬, 행렬 연산)

들어가며

딥러닝을 공부하기 시작하면서 가장 먼저 막히는 게 수학이었다. 특히 선형대수는 딥러닝 논문이나 코드 어디서나 등장하는데, 웹 개발만 하다 보니 거의 접할 일이 없었다. 처음에는 수식만 보면 막막했는데, 결국 선형대수는 데이터를 숫자 배열로 표현하고 변환하는 방법이라는 걸 이해하고 나서 훨씬 수월해졌다. 딥러닝을 이해하는 데 필요한 선형대수 핵심 개념을 정리해둔다.


왜 선형대수인가

딥러닝 모델이 하는 일을 한 문장으로 표현하면 이렇다.

입력 데이터를 숫자 배열로 변환 → 여러 번 수학적 변환 → 원하는 출력

이 과정에서 데이터를 표현하고 변환하는 도구가 바로 선형대수다. 이미지 하나도, 문장 하나도, 음성 데이터도 결국 숫자 배열로 변환해서 처리한다. 신경망의 각 레이어도 행렬 연산의 연속이다.

웹 개발에 비유하면 이렇다.

웹 개발: JSON 데이터를 파싱하고 가공해서 원하는 형태로 변환
딥러닝: 숫자 배열(텐서)을 행렬 연산으로 가공해서 원하는 출력으로 변환

스칼라, 벡터, 행렬, 텐서

딥러닝에서 데이터는 이 네 가지 형태로 표현된다.

스칼라 (Scalar) : 숫자 하나         → let x = 3
벡터  (Vector)  : 숫자의 1차원 배열  → [1, 2, 3]
행렬  (Matrix)  : 숫자의 2차원 배열  → [[1,2],[3,4]]
텐서  (Tensor)  : n차원 배열         → 이미지, 영상 등

스칼라 (Scalar)

단순한 숫자 하나다. 방향이 없고 크기만 있다.

x = 3.14         # 스칼라
lr = 0.001       # 학습률(learning rate)도 스칼라
loss = 0.423     # 손실값(loss)도 스칼라
accuracy = 0.95  # 정확도도 스칼라

딥러닝에서 스칼라는 모델의 성능을 하나의 숫자로 나타낼 때 주로 등장한다. 손실 함수의 최종 출력값이 스칼라이고, 이 값을 최소화하는 게 학습의 목표다.

벡터 (Vector)

숫자가 일렬로 나열된 1차원 배열이다. 크기(magnitude)와 방향(direction)을 동시에 가진다.

import numpy as np

# 행벡터 (1차원 배열)
v = np.array([1, 2, 3])
print(v.shape)  # (3,)

# 딥러닝에서 벡터의 예
user_features = np.array([170, 65, 28])     # 키, 몸무게, 나이
word_embedding = np.random.randn(512)        # 단어 임베딩 (512차원)
layer_output = np.random.randn(256)          # 신경망 레이어 출력

벡터의 차원(dimension)은 원소의 개수다. [170, 65, 28]은 3차원 벡터다. 딥러닝에서 "512차원 임베딩"이라고 하면 원소가 512개인 벡터라는 뜻이다.

기하학적 의미: 벡터는 공간에서의 방향과 거리를 나타낸다. 딥러닝에서 유사한 의미를 가진 단어들은 임베딩 공간에서 비슷한 방향을 가리키는 벡터로 표현된다. "왕"과 "여왕"의 벡터가 비슷한 방향을 가리키는 식이다.

행렬 (Matrix)

행(row)과 열(column)로 이루어진 2차원 배열이다.

# 3 x 2 행렬 (3행 2열)
M = np.array([
    [1, 2],
    [3, 4],
    [5, 6]
])
print(M.shape)  # (3, 2) → (행 수, 열 수)

# 딥러닝에서 행렬의 예
batch_data = np.random.randn(32, 128)   # 32개 샘플, 각 128개 특성
weights = np.random.randn(128, 64)      # 128 입력 → 64 출력 가중치
image = np.random.randint(0, 256, (28, 28))  # 흑백 이미지

기하학적 의미: 행렬은 공간 변환(transformation)을 나타낸다. 벡터에 행렬을 곱하면 그 벡터를 다른 공간으로 변환하는 것이다. 신경망의 각 레이어가 하는 일이 바로 이 변환이다.

텐서 (Tensor)

3차원 이상의 다차원 배열이다. PyTorch의 핵심 자료구조가 텐서다.

# 3차원 텐서: 컬러 이미지 1장 (채널, 높이, 너비)
image = np.zeros((3, 224, 224))
print(image.shape)  # (3, 224, 224)

# 4차원 텐서: 배치 단위 이미지 (배치, 채널, 높이, 너비)
batch_images = np.zeros((32, 3, 224, 224))
print(batch_images.shape)  # (32, 3, 224, 224)

# 3차원 텐서: 텍스트 배치 (배치, 시퀀스 길이, 임베딩 차원)
text_batch = np.zeros((32, 128, 512))
print(text_batch.shape)  # (32, 128, 512)

# PyTorch 텐서로 변환
import torch
x_np = np.array([1.0, 2.0, 3.0])
x_torch = torch.tensor(x_np)
print(x_torch)  # tensor([1., 2., 3.])

벡터 연산

벡터 덧셈과 뺄셈

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

print(a + b)  # [5, 7, 9]
print(a - b)  # [-3, -3, -3]

딥러닝에서 편향(bias)을 더하는 연산이 바로 벡터 덧셈이다.

layer_output = np.array([0.5, -0.3, 0.8, 0.1])
bias = np.array([0.1, 0.2, -0.1, 0.3])
result = layer_output + bias
print(result)  # [0.6, -0.1, 0.7, 0.4]

스칼라 곱

벡터의 모든 원소에 스칼라를 곱한다. 방향은 유지하면서 크기(길이)만 바꾼다.

a = np.array([1, 2, 3])
print(2 * a)    # [2, 4, 6]    → 벡터 크기가 2배
print(0.5 * a)  # [0.5, 1.0, 1.5] → 벡터 크기가 절반
print(-1 * a)   # [-1, -2, -3] → 방향이 반대

딥러닝에서 가장 중요한 활용이 바로 가중치 업데이트다.

# 경사하강법: w = w - lr * gradient
w = np.array([0.5, -0.3, 0.8, 0.2])
gradient = np.array([0.1, -0.2, 0.3, -0.1])
lr = 0.01

w_new = w - lr * gradient
print(w_new)  # [0.499, -0.298, 0.797, 0.201]

# lr이 크면 업데이트 폭이 크고
# lr이 작으면 업데이트 폭이 작다

내적 (Dot Product)

두 벡터를 같은 위치의 원소끼리 곱한 후 모두 더한다. 딥러닝에서 가장 자주 쓰이는 연산 중 하나다.

a = [a1, a2, a3]
b = [b1, b2, b3]
a · b = a1*b1 + a2*b2 + a3*b3
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

dot = np.dot(a, b)      # 또는 a @ b
print(dot)  # 1*4 + 2*5 + 3*6 = 32

기하학적 의미: 내적은 두 벡터가 얼마나 같은 방향을 가리키는지를 나타낸다.
θ = 0° (같은 방향): 내적 최대 / θ = 90° (수직): 내적 = 0 / θ = 180° (반대 방향): 내적 최소

# 같은 방향: 내적이 크다
print(np.dot(np.array([1,0]), np.array([1,0])))   # 1.0
# 수직: 내적이 0
print(np.dot(np.array([1,0]), np.array([0,1])))   # 0.0
# 반대 방향: 내적이 음수
print(np.dot(np.array([1,0]), np.array([-1,0])))  # -1.0

딥러닝에서 내적의 활용:

# 추천 시스템: 사용자와 아이템의 유사도 계산
user_vector = np.array([0.8, 0.2, 0.9, 0.1])
item_vector = np.array([0.7, 0.3, 0.8, 0.2])
similarity = np.dot(user_vector, item_vector)
print(f"유사도: {similarity:.4f}")

# 신경망 한 뉴런의 출력
inputs = np.array([1.0, 2.0, 3.0])
weights = np.array([0.5, -0.3, 0.8])
bias = 0.1
output = np.dot(inputs, weights) + bias
print(f"뉴런 출력: {output:.4f}")

벡터의 크기 (Norm)

a = np.array([3.0, 4.0])

# L2 Norm (유클리드 거리): √(3² + 4²) = 5
l2_norm = np.linalg.norm(a)
print(f"L2 Norm: {l2_norm}")  # 5.0

# L1 Norm (맨해튼 거리): |3| + |4| = 7
l1_norm = np.linalg.norm(a, ord=1)
print(f"L1 Norm: {l1_norm}")  # 7.0

# L2 정규화 손실 (가중치가 너무 커지면 패널티)
weights = np.array([0.5, -1.2, 0.8, -0.3])
lambda_reg = 0.01
l2_penalty = lambda_reg * np.linalg.norm(weights) ** 2
print(f"L2 패널티: {l2_penalty:.4f}")

행렬 연산

행렬 덧셈과 스칼라 곱

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

print(A + B)
# [[ 6,  8],
#  [10, 12]]

print(2 * A)
# [[2, 4],
#  [6, 8]]

원소별 곱셈 (Element-wise Multiplication)

같은 위치의 원소끼리 곱한다. 행렬 곱셈과 다르다. 드롭아웃, LSTM 게이트 등에서 사용된다.

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

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

행렬 곱셈 (Matrix Multiplication)

딥러닝에서 가장 핵심적인 연산이다. 신경망의 순전파(forward pass)가 사실상 행렬 곱의 연속이다.

(m x k) 행렬 × (k x n) 행렬 = (m x n) 행렬
앞 행렬의 열 수 = 뒤 행렬의 행 수가 반드시 일치해야 한다
A = np.array([[1, 2, 3],
              [4, 5, 6]])   # shape: (2, 3)

B = np.array([[7,  8],
              [9,  10],
              [11, 12]])    # shape: (3, 2)

C = A @ B   # (2, 3) @ (3, 2) = (2, 2)
print(C)
# [[ 58,  64],
#  [139, 154]]

# 배치 처리: 여러 샘플을 한 번에 처리
batch = np.random.randn(32, 128)   # 32개 샘플, 128개 특성
W = np.random.randn(128, 64)       # 128 → 64로 변환
b = np.random.randn(64)

output = batch @ W + b             # (32, 128) @ (128, 64) = (32, 64)
print(output.shape)                # (32, 64)

기하학적 의미: 행렬 곱은 공간 변환이다. 입력 특성을 다른 표현 공간으로 바꾸면서 점점 더 추상적인 특징을 학습한다. 신경망의 각 레이어가 하는 일이 바로 이것이다.

전치 행렬 (Transpose)

행과 열을 뒤집는다. 역전파(backpropagation)에서 핵심적으로 사용된다.

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

A_T = A.T
print(A_T)
# [[1, 4],
#  [2, 5],
#  [3, 6]]
print(A_T.shape)  # (3, 2)

# 역전파에서 가중치 그래디언트 계산
x = np.random.randn(32, 128)
dL_dy = np.random.randn(32, 64)
dL_dW = x.T @ dL_dy               # (128, 32) @ (32, 64) = (128, 64)
print(dL_dW.shape)  # (128, 64) → 가중치와 동일한 shape

브로드캐스팅 (Broadcasting)

크기가 다른 배열 간 연산을 자동으로 처리하는 기능이다.

A = np.array([[1, 2, 3],
              [4, 5, 6]])   # (2, 3)
b = np.array([10, 20, 30])  # (3,) → 자동으로 (2, 3)으로 확장

print(A + b)
# [[11, 22, 33],
#  [14, 25, 36]]

# 스칼라 브로드캐스팅
print(A + 100)
# [[101, 102, 103],
#  [104, 105, 106]]

딥러닝에서 선형대수가 어떻게 쓰이는가

신경망의 한 레이어를 수식으로 표현하면 이렇다.

y = f(Wx + b)

x: 입력 벡터
W: 가중치 행렬 (공간 변환)
b: 편향 벡터 (이동)
f: 활성화 함수 (비선형성 추가)
y: 출력 벡터
def relu(x):
    return np.maximum(0, x)

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 3레이어 신경망 순전파
np.random.seed(42)

X = np.array([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, 9.0],
    [2.0, 4.0, 1.0]
])  # (4, 3) - 4개 샘플, 3개 특성

# 레이어 1: 3 → 8
W1 = np.random.randn(3, 8) * 0.1
b1 = np.zeros(8)
h1 = relu(X @ W1 + b1)          # (4, 3) @ (3, 8) = (4, 8)

# 레이어 2: 8 → 4
W2 = np.random.randn(8, 4) * 0.1
b2 = np.zeros(4)
h2 = relu(h1 @ W2 + b2)         # (4, 8) @ (8, 4) = (4, 4)

# 출력 레이어: 4 → 2 (이진 분류)
W3 = np.random.randn(4, 2) * 0.1
b3 = np.zeros(2)
output = sigmoid(h2 @ W3 + b3)  # (4, 4) @ (4, 2) = (4, 2)

print(f"입력 shape: {X.shape}")
print(f"레이어1 출력 shape: {h1.shape}")
print(f"레이어2 출력 shape: {h2.shape}")
print(f"최종 출력 shape: {output.shape}")

NumPy 실습 요약

딥러닝 코드를 읽을 때 자주 등장하는 NumPy 함수들이다.

# 배열 생성
a = np.zeros((3, 4))           # 0으로 채운 배열
b = np.ones((3, 4))            # 1로 채운 배열
c = np.random.randn(3, 4)      # 정규분포 난수
d = np.eye(4)                  # 단위 행렬

# shape 관련
x = np.random.randn(32, 3, 28, 28)
print(x.shape)   # (32, 3, 28, 28)
print(x.ndim)    # 4 (차원 수)
print(x.size)    # 75264 (원소 수)

# shape 변환
x = np.random.randn(32, 784)
x_reshaped = x.reshape(32, 28, 28)  # shape 변경
x_flat = x.reshape(32, -1)          # -1은 자동 계산

# 축 관련 연산
x = np.random.randn(32, 10)
print(x.sum(axis=0))   # 행 방향 합 → (10,)
print(x.sum(axis=1))   # 열 방향 합 → (32,)
print(x.mean(axis=0))  # 행 방향 평균

# 인덱싱
x = np.random.randn(100, 5)
print(x[0])          # 첫 번째 행
print(x[:10])        # 처음 10개 행
print(x[:, 0])       # 첫 번째 열 전체
print(x[x > 0])      # 0보다 큰 원소만

정리

개념 설명 딥러닝 활용
스칼라 숫자 하나 학습률, 손실값
벡터 1차원 배열, 방향+크기 데이터 샘플, 임베딩
행렬 2차원 배열, 공간 변환 가중치, 배치 데이터
텐서 n차원 배열 이미지, 텍스트 배치
내적 유사도 측정 뉴런 출력, 추천 시스템
행렬 곱 공간 변환 연산 신경망 각 레이어
전치 행/열 뒤집기 역전파 그래디언트
브로드캐스팅 크기 자동 맞춤 편향 덧셈, 정규화
L2 Norm 벡터 길이 정규화 패널티