개념
벡터는 점이자 방향입니다
벡터는 숫자 목록입니다. 하지만 그 숫자들은 의미가 있습니다. 공간 안의 좌표입니다.
2D 벡터 [3, 2]:
| x | y | 점 |
|---|
| 3 | 2 | 이 벡터는 평면에서 원점 (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
v1과 v2는 독립입니다. 어느 하나도 다른 하나의 스칼라(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)가 필요합니다. |
| 랭크 1 | 1 | 모든 열이 하나의 벡터를 스케일링한 사본입니다. 모든 데이터가 한 직선 위에 있습니다. |
| 거의 랭크 부족 | 수치적으로 낮음 | 행렬이 잘못 조건화된(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이고, 모든 벡터 쌍이 서로 수직이라는 뜻입니다.
알고리즘은 다음과 같습니다.
- 첫 번째 벡터를 가져와 정규화(normalize)합니다.
- 두 번째 벡터에서 첫 번째 벡터 방향의 사영을 뺀 뒤 정규화합니다.
- 세 번째 벡터에서 이전 모든 벡터 방향의 사영을 뺀 뒤 정규화합니다.
- 남은 벡터에 대해 반복합니다.
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) 안에 있다고 가정한다는 뜻입니다. 이것이 선형대수가 실제로 일을 하는 방식입니다.
연습문제
- 두 벡터 사이의 각도를 도(degree) 단위로 반환하는
Vector.angle_between(other)를 구현합니다.
- x 좌표는 두 배, y 좌표는 세 배로 만드는 2D 스케일링 행렬을 만든 뒤
[1, 1] 벡터에 적용합니다.
- 50차원 단어 같은 벡터(word-like vector) 5개를 무작위로 만들고 코사인 유사도가 가장 큰 두 벡터를 찾습니다.
- 그램-슈미트 과정의 결과가 실제로 정규직교(orthonormal)인지 확인합니다. 모든 벡터 쌍의 내적이 0이고, 모든 벡터의 크기가 1인지 확인합니다.
- 랭크가 2인 3x3 행렬을 만듭니다.
rank() 메서드로 확인한 뒤, 그 열들이 어떤 기하학적 객체(object)를 생성(span)하는지 설명합니다.
[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인 벡터들 |
더 읽을거리