선형대수 직관

모든 AI 모델은 멋진 모자를 쓴 행렬 계산입니다.

유형: Learn 언어: Python, Julia 선수 지식: Phase 0 예상 시간: 약 60분

학습 목표

  • Python으로 벡터와 행렬 연산, 즉 덧셈, 내적(dot product), 행렬 곱셈(matrix multiplication)을 직접 구현합니다.
  • 내적(dot product), 사영(Projection), 그램-슈미트 과정(Gram-Schmidt Process)이 기하학적으로 무엇을 하는지 설명합니다.
  • 행 사다리꼴 변환(row reduction)을 사용해 벡터 집합의 선형 독립성(linear independence), 랭크(rank), 기저(basis)를 판단합니다.
  • 선형대수 개념을 임베딩(embedding), 어텐션 점수(attention score), LoRA 같은 AI 응용과 연결합니다.

문제

ML 논문을 아무거나 열어 보면 첫 페이지 안에 벡터, 행렬, 내적, 변환이 등장합니다. 선형대수 직관이 없으면 이것들은 그저 기호처럼 보입니다. 하지만 직관이 생기면 신경망이 실제로 무엇을 하는지, 즉 공간 안의 점들을 어떻게 움직이는지 볼 수 있습니다.

수학자가 될 필요는 없습니다. 이 연산들이 기하학적으로 무엇을 의미하는지 보고, 직접 코드로 구현해 보면 됩니다.

사전 테스트

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

1.두 벡터의 내적(dot product)은 무엇을 측정하나요?

2.AI에서 임베딩(embedding)은 무엇을 의미하나요?

0/2 답변 완료

개념

벡터는 점이자 방향입니다

벡터는 숫자 목록입니다. 하지만 그 숫자들은 의미가 있습니다. 공간 안의 좌표입니다.

2D 벡터 [3, 2]:

xy
32이 벡터는 평면에서 원점 (0,0)에서 (3, 2)를 향합니다.

이 벡터의 크기는 sqrt(3^2 + 2^2) = sqrt(13)이고, 오른쪽 위를 향합니다.

AI에서는 모든 것이 벡터로 표현됩니다.

  • 단어 하나는 768개 숫자로 된 벡터가 됩니다. 임베딩 공간(embedding space) 안에서의 "의미"입니다.
  • 이미지는 수백만 개 픽셀(pixel) 값으로 된 벡터가 됩니다.
  • 사용자는 선호도를 나타내는 벡터가 될 수 있습니다.

행렬은 변환입니다

행렬은 한 벡터를 다른 벡터로 변환합니다. 회전, 확대/축소, 늘리기, 사영을 할 수 있습니다.

graph LR
    subgraph Before
        A["Point A"]
        B["Point B"]
    end
    subgraph Matrix["Matrix Multiplication"]
        M["M (transformation)"]
    end
    subgraph After
        A2["Point A'"]
        B2["Point B'"]
    end
    A --> M
    B --> M
    M --> A2
    M --> B2

AI에서 행렬은 곧 모델입니다.

  • 신경망 가중치(weight)는 입력을 출력으로 바꾸는 행렬입니다.
  • 어텐션 점수는 무엇에 집중할지 결정하는 행렬입니다.
  • 임베딩은 단어를 벡터로 매핑(mapping)하는 행렬입니다.

내적은 유사도를 측정합니다

두 벡터의 내적은 두 벡터가 얼마나 비슷한지 알려 줍니다.

a · b = a1*b1 + a2*b2 + ... + an*bn

같은 방향:   a · b > 0  (유사함)
수직:        a · b = 0  (관련 없음)
반대 방향:   a · b < 0  (유사하지 않음)

검색 엔진, 추천 시스템, RAG는 실제로 이런 방식으로 동작합니다. 내적이 큰 벡터를 찾습니다.

선형 독립성

벡터 집합 안의 어떤 벡터도 다른 벡터들의 조합으로 쓸 수 없다면 그 벡터들은 선형 독립입니다. v1, v2, v3가 독립이면 3차원 공간을 생성(span)합니다. 하나가 다른 벡터들의 조합이면 그것들은 평면까지만 생성합니다.

AI에서 중요한 이유는 특징 행렬(feature matrix)의 열(column)이 선형 독립이어야 하기 때문입니다. 두 특징(feature)이 완전히 상관되어 있으면, 즉 선형 종속이면 모델은 두 특징의 효과를 구분할 수 없습니다. 회귀에서는 이를 다중공선성(multicollinearity)이라고 부릅니다. 가중치 행렬이 불안정해지고, 작은 입력 변화가 큰 출력 변화로 이어질 수 있습니다.

구체적인 예:

v1 = [1, 0, 0]
v2 = [0, 1, 0]
v3 = [2, 1, 0]   # v3 = 2*v1 + v2

v1v2는 독립입니다. 어느 하나도 다른 하나의 스칼라(scalar) 배수나 조합이 아닙니다. 하지만 v3 = 2*v1 + v2이므로 {v1, v2, v3}는 종속 집합입니다. 이 세 벡터는 모두 xy-평면 위에 있습니다. 아무리 조합해도 [0, 0, 1]에는 도달할 수 없습니다. 벡터는 세 개지만 자유도는 두 차원뿐입니다.

데이터셋으로 보면, feature_3 = 2*feature_1 + feature_2라면 feature_3을 추가해도 모델에는 새로운 정보가 하나도 들어오지 않습니다. 더 나쁘게는 정규 방정식(normal equation)이 특이(singular)해집니다. 가중치의 유일한 해가 존재하지 않습니다.

기저(Basis)와 랭크(rank)

기저(basis)는 전체 공간을 생성하는 최소한의 선형 독립 벡터 집합입니다. 기저 벡터(basis vector)의 개수가 그 공간의 차원입니다.

3D 공간의 표준 기저(standard basis)는 {[1,0,0], [0,1,0], [0,0,1]}입니다. 하지만 3D 안의 독립인 벡터 세 개라면 어떤 것이든 유효한 기저가 될 수 있습니다. 기저를 고르는 일은 좌표계를 고르는 일입니다.

행렬의 랭크(rank)는 선형 독립인 열의 수이며, 선형 독립인 행(row)의 수와도 같습니다. rank < min(rows, cols)이면 그 행렬은 랭크 부족(rank-deficient)입니다. 이는 다음을 의미합니다.

  • 시스템에 해가 무한히 많거나, 아예 없을 수 있습니다.
  • 변환 과정에서 정보가 사라집니다.
  • 행렬의 역행렬(inverse)을 구할 수 없습니다.
상황랭크ML에서의 의미
풀 랭크(full rank, rank = min(m, n))가능한 최댓값유일한 최소제곱(least-squares) 해가 존재합니다. 모델이 잘 조건화되어 있습니다.
랭크 부족(rank deficient, rank < min(m, n))최댓값보다 작음특징이 중복됩니다. 가중치 해가 무한히 많을 수 있습니다. 정규화(regularization)가 필요합니다.
랭크 11모든 열이 하나의 벡터를 스케일링한 사본입니다. 모든 데이터가 한 직선 위에 있습니다.
거의 랭크 부족수치적으로 낮음행렬이 잘못 조건화된(ill-conditioned) 상태입니다. 아주 작은 입력 노이즈가 큰 출력 변화를 만들 수 있습니다. SVD 절단(truncation)이나 릿지 회귀(ridge regression)를 사용합니다.

사영(Projection)

벡터 a를 벡터 b 위로 사영하면, a 중에서 b 방향에 해당하는 성분을 얻습니다.

proj_b(a) = (a dot b / b dot b) * b

잔차(residual), 즉 a - proj_b(a)b와 수직입니다. 이 직교 분해(orthogonal decomposition)가 최소제곱 적합(least-squares fitting)의 토대입니다.

ML에서는 사영이 곳곳에 등장합니다.

  • 선형 회귀는 관측값과 열 공간(column space) 사이의 거리를 최소화합니다. 그 해 자체가 사영입니다.
  • PCA는 데이터를 분산(variance)이 가장 큰 방향으로 사영합니다.
  • 트랜스포머(transformer)의 어텐션은 쿼리(query)를 키(key) 위로 사영하는 계산을 포함합니다.
graph LR
    subgraph Projection["a를 b 위로 사영(Projection)"]
        direction TB
        O["Origin"] --> |"b (direction)"| B["b"]
        O --> |"a (original)"| A["a"]
        O --> |"proj_b(a)"| P["projection"]
        A -.-> |"residual (perpendicular)"| P
    end

예: a = [3, 4], b = [1, 0]

proj_b(a) = (3*1 + 4*0) / (1*1 + 0*0) * [1, 0]
          = 3 * [1, 0]
          = [3, 0]

이 사영은 y 성분을 버립니다. 가장 단순한 형태의 차원 축소(dimensionality reduction)입니다. 신경 쓰지 않는 방향을 버리는 것입니다.

그램-슈미트 과정(Gram-Schmidt Process)

그램-슈미트 과정(Gram-Schmidt Process)은 독립인 벡터 집합을 정규직교 기저(orthonormal basis)로 바꿉니다. 정규직교(orthonormal)는 모든 벡터의 길이가 1이고, 모든 벡터 쌍이 서로 수직이라는 뜻입니다.

알고리즘은 다음과 같습니다.

  1. 첫 번째 벡터를 가져와 정규화(normalize)합니다.
  2. 두 번째 벡터에서 첫 번째 벡터 방향의 사영을 뺀 뒤 정규화합니다.
  3. 세 번째 벡터에서 이전 모든 벡터 방향의 사영을 뺀 뒤 정규화합니다.
  4. 남은 벡터에 대해 반복합니다.
Input:  v1, v2, v3, ... (linearly independent)

u1 = v1 / |v1|

w2 = v2 - (v2 dot u1) * u1
u2 = w2 / |w2|

w3 = v3 - (v3 dot u1) * u1 - (v3 dot u2) * u2
u3 = w3 / |w3|

Output: u1, u2, u3, ... (orthonormal basis)

QR 분해(QR decomposition)는 내부적으로 이 방식으로 동작합니다. Q는 정규직교 기저이고, R은 사영 계수(projection coefficient)를 담습니다. QR 분해는 다음에 쓰입니다.

  • 가우스 소거법(Gaussian elimination)보다 안정적으로 선형 시스템(linear system) 풀기
  • 고윳값(eigenvalue) 계산하기
  • 최소제곱 회귀(least-squares regression) 계산하기

직접 만들기

Step 1: 벡터 직접 구현하기 (Python)

class Vector:
    def __init__(self, components):
        self.components = list(components)
        self.dim = len(self.components)

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

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

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

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

    def normalize(self):
        mag = self.magnitude()
        return Vector([x / mag for x in self.components])

    def cosine_similarity(self, other):
        return self.dot(other) / (self.magnitude() * other.magnitude())

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


a = Vector([1, 2, 3])
b = Vector([4, 5, 6])

print(f"a + b = {a + b}")
print(f"a · b = {a.dot(b)}")
print(f"|a| = {a.magnitude():.4f}")
print(f"cosine similarity = {a.cosine_similarity(b):.4f}")

Step 2: 행렬 직접 구현하기 (Python)

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

    def __matmul__(self, other):
        if isinstance(other, Vector):
            return Vector([
                sum(self.rows[i][j] * other.components[j] for j in range(self.shape[1]))
                for i in range(self.shape[0])
            ])
        rows = []
        for i in range(self.shape[0]):
            row = []
            for j in range(other.shape[1]):
                row.append(sum(
                    self.rows[i][k] * other.rows[k][j]
                    for k in range(self.shape[1])
                ))
            rows.append(row)
        return Matrix(rows)

    def transpose(self):
        return Matrix([
            [self.rows[j][i] for j in range(self.shape[0])]
            for i in range(self.shape[1])
        ])

    def __repr__(self):
        return f"Matrix({self.rows})"


rotation_90 = Matrix([[0, -1], [1, 0]])
point = Vector([3, 1])

rotated = rotation_90 @ point
print(f"원본: {point}")
print(f"90도 회전 결과: {rotated}")

Step 3: 왜 AI에서 중요한가

import random

random.seed(42)
weights = Matrix([[random.gauss(0, 0.1) for _ in range(3)] for _ in range(2)])
input_vector = Vector([1.0, 0.5, -0.3])

output = weights @ input_vector
print(f"Input (3D): {input_vector}")
print(f"Output (2D): {output}")
print("이것이 신경망 층(layer)이 하는 일입니다. 바로 행렬 곱셈입니다.")

위 코드는 신경망 층(layer)이 하는 일을 아주 작은 형태로 보여 줍니다. 입력 벡터에 가중치 행렬을 곱해 다른 공간의 출력 벡터로 바꿉니다.

Step 4: Julia 버전

a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]

println("a + b = ", a + b)
println("a · b = ", a ⋅ b)       # Julia는 유니코드 연산자(unicode operator)를 지원합니다
println("|a| = ", √(a ⋅ a))
println("cosine = ", (a ⋅ b) / (√(a ⋅ a) * √(b ⋅ b)))

# 행렬-벡터 곱셈
W = [0.1 -0.2 0.3; 0.4 0.5 -0.1]
x = [1.0, 0.5, -0.3]
println("Wx = ", W * x)
println("이것이 신경망 층입니다.")

Step 5: 선형 독립성과 사영 직접 구현하기 (Python)

def is_linearly_independent(vectors):
    n = len(vectors)
    dim = len(vectors[0].components)
    mat = Matrix([v.components[:] for v in vectors])
    rows = [row[:] for row in mat.rows]
    rank = 0
    for col in range(dim):
        pivot = None
        for row in range(rank, len(rows)):
            if abs(rows[row][col]) > 1e-10:
                pivot = row
                break
        if pivot is None:
            continue
        rows[rank], rows[pivot] = rows[pivot], rows[rank]
        scale = rows[rank][col]
        rows[rank] = [x / scale for x in rows[rank]]
        for row in range(len(rows)):
            if row != rank and abs(rows[row][col]) > 1e-10:
                factor = rows[row][col]
                rows[row] = [rows[row][j] - factor * rows[rank][j] for j in range(dim)]
        rank += 1
    return rank == n


def project(a, b):
    scalar = a.dot(b) / b.dot(b)
    return Vector([scalar * x for x in b.components])


def gram_schmidt(vectors):
    orthonormal = []
    for v in vectors:
        w = v
        for u in orthonormal:
            proj = project(w, u)
            w = w - proj
        if w.magnitude() < 1e-10:
            continue
        orthonormal.append(w.normalize())
    return orthonormal


v1 = Vector([1, 0, 0])
v2 = Vector([1, 1, 0])
v3 = Vector([1, 1, 1])
basis = gram_schmidt([v1, v2, v3])
for i, u in enumerate(basis):
    print(f"u{i+1} = {u}")
    print(f"  |u{i+1}| = {u.magnitude():.6f}")

print(f"u1 · u2 = {basis[0].dot(basis[1]):.6f}")
print(f"u1 · u3 = {basis[0].dot(basis[2]):.6f}")
print(f"u2 · u3 = {basis[1].dot(basis[2]):.6f}")

사용해보기

실무에서는 같은 계산을 NumPy로 처리합니다.

import numpy as np

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

print(f"a + b = {a + b}")
print(f"a · b = {np.dot(a, b)}")
print(f"|a| = {np.linalg.norm(a):.4f}")
print(f"cosine = {np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)):.4f}")

W = np.random.randn(2, 3) * 0.1
x = np.array([1.0, 0.5, -0.3])
print(f"Wx = {W @ x}")

NumPy로 랭크(rank), 사영(Projection), QR 다루기

import numpy as np

A = np.array([[1, 2], [2, 4]])
print(f"랭크: {np.linalg.matrix_rank(A)}")

a = np.array([3, 4])
b = np.array([1, 0])
proj = (np.dot(a, b) / np.dot(b, b)) * b
print(f"{a}{b} 위로 사영한 결과: {proj}")

Q, R = np.linalg.qr(np.random.randn(3, 3))
print(f"Q는 직교(orthogonal)인가?: {np.allclose(Q @ Q.T, np.eye(3))}")
print(f"R은 상삼각(upper triangular)인가?: {np.allclose(R, np.triu(R))}")

PyTorch: 텐서(tensor)는 자동 미분(autodiff)이 붙은 벡터입니다

import torch

x = torch.randn(3, requires_grad=True)
y = torch.tensor([1.0, 0.0, 0.0])

similarity = torch.dot(x, y)
similarity.backward()

print(f"x = {x.data}")
print(f"y = {y.data}")
print(f"내적 = {similarity.item():.4f}")
print(f"d(dot)/dx = {x.grad}")

x에 대한 내적의 그래디언트(gradient)는 y입니다. PyTorch가 이를 자동으로 계산했습니다. 신경망의 모든 연산은 이런 기본 연산, 즉 행렬 곱셈, 내적, 사영 위에 만들어지고, 자동 미분은 그 전체 경로의 그래디언트를 추적합니다.

방금 NumPy가 한 줄로 처리하는 일을 직접 구현했습니다. 이제 내부에서 무슨 일이 일어나는지 알고 있습니다.

산출물 만들기

이 강의의 산출물은 다음 파일입니다.

  • outputs/prompt-linear-algebra-tutor.md: AI 어시스턴트가 선형대수를 기하학적 직관으로 설명하도록 돕는 프롬프트

검수할 때는 이 프롬프트가 아래 기준을 만족하는지 확인합니다.

  • 선형대수 개념을 먼저 기하학적으로 설명하게 합니다.
  • 임베딩, 어텐션, 트랜스포머 같은 AI 응용과 연결합니다.
  • 수식만 제시하지 않고 직관, 직접 구현, NumPy 사용 예시를 함께 요구합니다.

연결점

이 강의의 모든 내용은 현대 AI의 구체적인 부분과 연결됩니다.

개념등장하는 곳
내적트랜스포머의 어텐션 점수, RAG의 코사인 유사도(cosine similarity)
행렬 곱셈모든 신경망 층(layer), 모든 선형 변환(linear transformation)
선형 독립성특징 선택(feature selection), 다중공선성 회피
랭크시스템이 풀 수 있는지 판단, LoRA(low-rank adaptation)
사영선형 회귀(열 공간으로 사영), PCA
그램-슈미트 과정 / QR 분해수치 해결기(numerical solver), 고윳값 계산(eigenvalue computation)
정규직교 기저안정적인 수치 계산, 화이트닝 변환(whitening transform)

LoRA는 특별히 언급할 만합니다. LoRA는 가중치 갱신(weight update)을 저랭크 행렬(low-rank matrix)로 분해해 대형 언어 모델을 파인튜닝합니다. 4096x4096 가중치 행렬 전체, 즉 1600만 파라미터를 업데이트하는 대신 4096x16과 16x4096 크기의 두 행렬, 즉 13.1만 파라미터만 업데이트합니다. 랭크 16 제약은 가중치 갱신이 전체 4096차원 공간이 아니라 16차원 부분공간(subspace) 안에 있다고 가정한다는 뜻입니다. 이것이 선형대수가 실제로 일을 하는 방식입니다.

연습문제

  1. 두 벡터 사이의 각도를 도(degree) 단위로 반환하는 Vector.angle_between(other)를 구현합니다.
  2. x 좌표는 두 배, y 좌표는 세 배로 만드는 2D 스케일링 행렬을 만든 뒤 [1, 1] 벡터에 적용합니다.
  3. 50차원 단어 같은 벡터(word-like vector) 5개를 무작위로 만들고 코사인 유사도가 가장 큰 두 벡터를 찾습니다.
  4. 그램-슈미트 과정의 결과가 실제로 정규직교(orthonormal)인지 확인합니다. 모든 벡터 쌍의 내적이 0이고, 모든 벡터의 크기가 1인지 확인합니다.
  5. 랭크가 2인 3x3 행렬을 만듭니다. rank() 메서드로 확인한 뒤, 그 열들이 어떤 기하학적 객체(object)를 생성(span)하는지 설명합니다.
  6. [1, 2, 3] 벡터를 [1, 1, 1] 위로 사영합니다. 결과가 기하학적으로 무엇을 의미하는지 설명합니다.

핵심 용어

용어흔한 설명실제 의미
벡터(Vector)화살표n차원 공간(n-dimensional space) 안의 점이나 방향을 나타내는 숫자 목록
행렬(Matrix)숫자 표벡터를 한 공간에서 다른 공간으로 매핑(mapping)하는 변환
내적(Dot product)곱해서 더하기두 벡터가 얼마나 같은 방향을 향하는지 나타내는 값이며 유사도 검색(similarity search)의 핵심
임베딩(Embedding)AI 마법단어, 이미지, 사용자 같은 대상의 의미를 나타내는 벡터
선형 독립성(Linear independence)겹치지 않음집합 안의 어떤 벡터도 다른 벡터들의 조합으로 쓸 수 없는 상태
랭크(rank)차원 수행렬에서 선형 독립인 열 또는 행의 개수
사영(Projection)그림자한 벡터가 다른 벡터 방향으로 가지는 성분
기저(Basis)좌표축공간을 생성(span)하는 최소한의 독립 벡터 집합
정규직교(Orthonormal)수직인 단위 벡터(unit vector)서로 수직이고 각각 길이가 1인 벡터들

더 읽을거리

  • NumPy Linear Algebra — 랭크, QR, 노름(norm) 같은 선형대수 연산을 NumPy에서 어떻게 제공하는지 확인합니다.
  • PyTorch Tensors — PyTorch 텐서가 벡터와 행렬 연산을 어떻게 표현하는지 확인합니다.
  • Julia LinearAlgebra — Julia에서 내적, 노름, 행렬 연산을 다루는 표준 라이브러리를 확인합니다.
  • 3Blue1Brown: Essence of Linear Algebra — 선형대수를 기하학적 직관으로 복습할 때 좋습니다.

실습 코드

이 강의의 실습 코드 2개

vectors
Code
vectors
Code

산출물

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

prompt-linear-algebra-tutor

Teach linear algebra through geometric intuition and AI applications

Prompt

확인 문제

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

1.세 벡터 `v1=[1,0,0]`, `v2=[0,1,0]`, `v3=[2,1,0]`가 주어졌습니다. 이 벡터들은 선형 독립(linear independence)인가요?

2.머신러닝(machine learning) 맥락에서 행렬의 랭크(rank)는 무엇을 알려 주나요?

3.LoRA(Low-Rank Adaptation)는 선형대수를 어떻게 사용해 대형 언어 모델을 효율적으로 파인튜닝(fine-tuning)하나요?

0/3 답변 완료