Burt.K

Awesome Discovery

다시 생각해 본 수학의 사칙연산

작성일 — 2025년 8월 17일

Table of Contents

나는 오랫동안 수학의 사칙연산이 궁금했다. 계산에 매일 사용하는 사칙 연산을 단순히 계산 도구로만 인식하고 있는 것이 아닐까? 그러던 중 F = ma 라는 물리 힘의 공식을 보면서 왜 m과 a 를 곱해야 하는가? 질량과 가속도를 곱셈하면 새로운 정보인 힘 F 가 정의 되는 것에서 사칙 연산을 정보의 합성과 분해 측면에서 이해할 수 있을까 궁금했다.

추상화: 수학의 가장 위대한 발명

수학의 가장 위대한 발명은 추상화다. 세 개의 사과, 세 명의 사람, 세 시간의 시간 - 이 모든 것을 우리는 ‘3’이라는 하나의 추상적 개념으로 표현한다. 이렇게 현실 세계의 구체적인 대상들을 숫자라는 추상적 기호로 변환하는 순간, 우리는 정보를 다룰 수 있는 강력한 도구를 갖게 된다.

# 구체적인 것들을 추상화
apples = 3      # 사과 3개
people = 3      # 사람 3명
seconds = 3     # 시간 3초

# 모두 같은 추상 연산 적용 가능
total = apples + people  # 6 (의미는 맥락에 따라 달라진다)

사칙연산은 바로 이 추상화된 정보를 조작하는 가장 기본적인 도구다. 덧셈, 뺄셈, 곱셈, 나눗셈 - 이 네 가지 연산은 단순히 숫자를 계산하는 도구가 아니다. 이들은 정보를 합성하고 분해하며, 차원을 확장하고 축소하는 근본적인 변환 도구다.

나는 이것을 두 가지 관점으로 이해하기 시작했다. 덧셈과 뺄셈은 정보의 결합과 분리, 곱셈과 나눗셈은 차원의 확장과 축소를 담당한다.

정보의 결합과 분리: 덧셈과 뺄셈

덧셈이 수행하는 정보의 결합

덧셈은 흩어진 정보들을 하나로 모으는 연산이다. 그런데 이것은 단순한 수치의 증가가 아니다. 서로 독립적으로 존재하던 정보들이 결합되어 새로운 의미를 가진 정보가 탄생하는 과정이다.

예를 들어 월별 매출을 더하면 분기 매출이 된다. 이는 시간축에 흩어진 개별 정보들이 ‘분기’라는 새로운 관점의 정보로 통합되는 것이다. 프로그래밍에서 배열의 reduce 연산이 바로 이러한 정보 결합의 전형적인 예다. 개별 요소들이 가진 정보가 하나의 집계된 정보로 변환된다.

// 월별 매출을 분기 매출로 결합
const monthlyRevenue = [100, 150, 200];
const quarterlyRevenue = monthlyRevenue.reduce((sum, value) => sum + value, 0);
console.log(quarterlyRevenue); // 450

// 시간축에 흩어진 정보가 '분기'라는 새로운 관점으로 통합됐다

그러나 주의할 점은 덧셈이 항상 ‘증가’를 의미하지는 않는다는 것이다. 음수를 더하면 오히려 감소한다. 이것은 덧셈이 ‘방향성 있는 결합’임을 보여준다. 벡터 덧셈을 생각하면 이해가 쉽다. 서로 다른 방향의 벡터들이 더해져 결과 벡터를 만들 때, 각 벡터의 방향과 크기 정보가 모두 보존되면서 결합된다.

balance = 1000
expense = -300  # 지출은 음수
new_balance = balance + expense  # 700
# 덧셈이지만 실제로는 감소를 나타낸다
// 2D 벡터의 덧셈
const vector1 = {x: 3, y: 4};
const vector2 = {x: -1, y: 2};
const result = {
    x: vector1.x + vector2.x,  // 2
    y: vector1.y + vector2.y   // 6
};
// 서로 다른 방향의 정보가 결합됐다

뺄셈이 추출하는 차이 정보

뺄셈은 전체 정보에서 일부를 분리해내는 연산이다. 하지만 이것을 단순한 ‘제거’로만 이해하면 안 된다. 뺄셈의 본질은 ‘비교를 통한 차이 정보의 추출’이다.

오늘 기온에서 어제 기온을 빼면 ‘기온 변화량’이라는 새로운 정보를 얻는다. 전체 예산에서 지출을 빼면 ‘가용 예산’이라는 정보가 나온다. 이처럼 뺄셈은 두 정보 사이의 관계를 수치화하여 새로운 통찰을 제공한다.

# 온도 변화 측정
today_temp = 25
yesterday_temp = 20
temp_change = today_temp - yesterday_temp  # 5
print(f"온도 변화: {temp_change}도 상승")

# 두 시점의 차이라는 새로운 정보를 얻었다

데이터 분석에서 차분(difference) 연산이 중요한 이유가 바로 여기에 있다. 시계열 데이터에서 연속된 값들의 차이를 구하면, 데이터의 변화 추세라는 숨겨진 정보를 발견할 수 있다.

import numpy as np

# 시계열 데이터의 차분
prices = np.array([100, 105, 103, 108, 110])
changes = np.diff(prices)  # [5, -2, 5, 2]
# 가격 데이터에서 변화 추세 정보를 추출했다

차원의 확장과 축소: 곱셈과 나눗셈

곱셈이 만드는 새로운 차원 또는 차원 변환

곱셈의 가장 놀라운 특성은 차원을 확장한다는 것이다. 이것이 무엇을 의미하는지 직사각형의 넓이를 통해 살펴보자.

가로 5미터와 세로 3미터를 곱하면 15제곱미터가 된다. 여기서 주목할 점은 단위다. 미터(m)라는 1차원 단위 두 개가 곱해져서 제곱미터(m²)라는 2차원 단위가 탄생했다. 이것은 단순한 수치 계산이 아니라 차원의 변환이다.

# 1차원 × 1차원 = 2차원
width = 5    # m (미터)
height = 3   # m (미터)
area = width * height  # 15 m² (제곱미터)

# 단위가 m에서 m²로 변했다 - 차원이 확장됐다!

F = ma 공식으로 돌아가 보자. 질량(kg)은 물체가 가진 ‘물질의 양’에 대한 정보다. 가속도(m/s²)는 ‘운동 변화율’에 대한 정보다. 이 두 개의 서로 독립적인 정보를 곱하면 힘(N)이라는 완전히 새로운 차원의 정보가 생성된다. 뉴턴(N)이라는 단위 자체가 kg·m/s²로 정의되는 것은 이러한 차원 결합의 결과다.

# 서로 독립적인 물리량의 결합
mass = 10           # kg (물질의 양)
acceleration = 9.8  # m/s² (운동 변화율)
force = mass * acceleration  # 98 N (뉴턴)

# 두 독립적 정보가 결합되어 '힘'이라는 새로운 차원의 정보가 생성됐다

그런데 곱셈이 항상 차원을 확장하는 것은 아니다. 0.5를 곱하면 오히려 크기가 줄어든다. 이것은 곱셈이 ‘스케일링’의 역할도 한다는 것을 보여준다. 사진을 50% 축소하는 것, 음량을 두 배로 키우는 것 - 이 모든 것이 곱셈을 통한 스케일 변환이다.

# 스케일링 (축소)
original_size = 100
scale_factor = 0.5
reduced_size = original_size * scale_factor  # 50
# 곱셈이지만 크기는 줄어들었다

# 방향 반전
velocity = 10
direction = -1
new_velocity = velocity * direction  # -10
# 곱셈이 방향을 바꿨다

데이터베이스의 카테시안 곱(Cartesian Product)은 곱셈의 차원 확장 특성을 잘 보여준다. 색상 테이블에 3개의 행이 있고 크기 테이블에 4개의 행이 있다면, 두 테이블의 카테시안 곱은 12개의 조합을 만들어낸다. 두 개의 1차원 정보가 결합되어 2차원 매트릭스가 된 것이다.

-- 색상 테이블 (3개)과 크기 테이블 (4개)의 곱
-- 결과: 12개의 조합 (3 × 4 = 12)
SELECT * FROM colors CROSS JOIN sizes;
-- 두 1차원 정보가 2차원 매트릭스가 됐다

나눗셈이 수행하는 정규화

나눗셈은 복합적인 정보를 단위 정보로 분해한다. 이것을 ‘정규화’라고 부르는 이유는 서로 다른 스케일의 정보를 비교 가능한 형태로 변환하기 때문이다.

속도 = 거리 ÷ 시간이라는 공식을 생각해보자. 100미터를 10초에 달렸다는 정보를 ‘초당 10미터’라는 정규화된 정보로 변환한다. 이제 다른 사람이 200미터를 25초에 달렸다고 해도, 초당 8미터라는 정규화된 값으로 쉽게 비교할 수 있다.

# 속도 = 거리 ÷ 시간
distance = 100  # m
time = 10      # s
speed = distance / time  # 10 m/s

# '총 이동 거리'를 '단위 시간당 거리'로 정규화했다

1인당 GDP는 나눗셈을 통한 정규화의 완벽한 예다. 미국의 GDP는 중국보다 크지만, 인구수로 나눈 1인당 GDP로 보면 다른 그림이 나타난다. 나눗셈이 ‘총량’이라는 정보를 ‘개인 단위’라는 정보로 변환하여 진정한 비교를 가능하게 한 것이다.

평균 계산도 차원 축소다.

const scores = [85, 90, 78, 92, 88];
const average = scores.reduce((a, b) => a + b) / scores.length;
console.log(average);  // 86.6

// 5차원 정보(5개 점수)를 1차원 정보(평균)로 압축했다

머신러닝에서 데이터 정규화가 필수적인 이유도 여기에 있다. 키(cm)와 몸무게(kg)처럼 단위가 다른 특징들을 0과 1 사이의 값으로 정규화하면, 알고리즘이 모든 특징을 공평하게 다룰 수 있게 된다.

# Min-Max 정규화
def normalize(value, min_val, max_val):
    return (value - min_val) / (max_val - min_val)

# 임의의 범위를 0과 1 사이로 변환
data = [10, 50, 30, 80, 100]
min_val, max_val = min(data), max(data)
normalized = [normalize(x, min_val, max_val) for x in data]
# [0.0, 0.44, 0.22, 0.78, 1.0]

벡터 연산: 차원 변환의 극적인 예시

벡터 연산은 차원 변환의 본질을 가장 명확하게 보여준다. 내적은 고차원을 스칼라로 압축하고, 외적은 새로운 차원을 창조한다. 이 두 연산이 어떻게 정보를 변환하는지 자세히 살펴보자.

내적: 다차원 정보를 스칼라로 압축

벡터의 내적(Dot Product)은 차원 축소의 극적인 예다. 3차원 벡터 두 개를 내적하면 단 하나의 숫자가 나온다. 이 과정을 자세히 들여다보면 놀라운 사실을 발견하게 된다.

내적 연산은 대응하는 성분끼리 곱한 후 모두 더한다. 예를 들어 [1, 2, 3]과 [4, 5, 6]의 내적은 1×4 + 2×5 + 3×6 = 32다. 여기서 곱셈이 각 차원의 정보를 결합하고, 덧셈이 모든 차원을 하나로 통합한다. 곱셈과 덧셈이 협력하여 차원을 축소하는 것이다.

이 과정의 기하학적 의미를 이해하는 것이 중요하다. 내적 결과는 한 벡터를 다른 벡터 방향으로 투영한 길이와 그 벡터 길이의 곱이다. 수식으로 표현하면 a·b = |a| × |b| × cos(θ)다. 여기서 θ는 두 벡터 사이의 각도다. cos(θ)가 방향 정보를 인코딩한다. 같은 방향이면 cos(0°) = 1, 수직이면 cos(90°) = 0, 반대 방향이면 cos(180°) = -1이다.

import numpy as np

# 3차원 벡터 두 개
vector_a = np.array([1, 2, 3])
vector_b = np.array([4, 5, 6])

# 내적: 3차원 × 3차원 → 0차원
dot_product = np.dot(vector_a, vector_b)
print(f"내적 결과: {dot_product}")  # 32

# 계산 과정: 1×4 + 2×5 + 3×6 = 32
# 곱셈과 덧셈이 협력하여 차원을 축소했다!

내적이 만들어내는 스칼라 값은 두 벡터가 얼마나 같은 방향을 향하는지를 나타낸다. 평행한 벡터의 내적은 크고, 수직인 벡터의 내적은 0이며, 반대 방향 벡터의 내적은 음수가 된다. 복잡한 다차원 방향 정보가 하나의 숫자로 압축된 것이다.

이것이 기계학습에서 중요한 이유를 생각해보자. 신경망의 각 뉴런은 입력 벡터와 가중치 벡터의 내적을 계산한다. 가중치는 뉴런이 감지하려는 패턴을 나타낸다. 입력이 이 패턴과 유사할수록 내적 값이 크다. 수백 차원의 복잡한 패턴 매칭이 단 하나의 활성화 값으로 압축되는 것이다.

# 두 벡터 사이의 각도 계산
def angle_between(v1, v2):
    cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
    return np.degrees(np.arccos(cos_angle))

# 평행한 벡터
parallel_a = np.array([1, 0])
parallel_b = np.array([2, 0])
print(f"평행 벡터 내적: {np.dot(parallel_a, parallel_b)}")  # 2 (양수)

# 수직인 벡터
perpendicular_a = np.array([1, 0])
perpendicular_b = np.array([0, 1])
print(f"수직 벡터 내적: {np.dot(perpendicular_a, perpendicular_b)}")  # 0

# 반대 방향 벡터
opposite_a = np.array([1, 0])
opposite_b = np.array([-1, 0])
print(f"반대 방향 내적: {np.dot(opposite_a, opposite_b)}")  # -1 (음수)

수직인 벡터의 내적이 0이 되는 것은 특별한 의미를 갖는다. 두 벡터가 완전히 독립적이라는 뜻이다. 한 벡터의 정보가 다른 벡터에 전혀 기여하지 않는다. 이것이 직교 좌표계를 사용하는 이유다. x, y, z 축이 서로 수직이므로 각 축의 정보가 독립적으로 유지된다.

이것이 왜 중요할까? 텍스트 검색 엔진을 생각해보자. 문서를 수천 차원의 벡터로 표현하고, 검색어도 같은 차원의 벡터로 만든다. 두 벡터의 내적(코사인 유사도)을 구하면 문서와 검색어의 관련성을 하나의 점수로 얻을 수 있다. 수천 차원의 복잡한 정보가 0과 1 사이의 단순한 숫자로 압축되어, 문서를 순위별로 정렬할 수 있게 된다.

실제 검색 엔진에서는 TF-IDF(Term Frequency-Inverse Document Frequency) 벡터를 사용한다. 각 단어가 차원이 되고, 그 값은 단어의 중요도를 나타낸다. 자주 나타나지만 모든 문서에 공통적인 단어(예: ‘그리고’, ‘하지만’)는 낮은 값을 갖는다. 특정 문서에만 나타나는 고유한 단어는 높은 값을 갖는다. 이렇게 만든 벡터들의 코사인 유사도가 검색 순위를 결정한다.

# 코사인 유사도 계산
def cosine_similarity(vec1, vec2):
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

# 문서를 벡터로 표현 (단어 빈도)
# [python, java, AI, web, data]
doc1 = np.array([3, 0, 4, 0, 2])  
doc2 = np.array([2, 0, 5, 0, 3])  # 유사한 주제
doc3 = np.array([0, 4, 0, 3, 0])  # 다른 주제

sim_1_2 = cosine_similarity(doc1, doc2)
sim_1_3 = cosine_similarity(doc1, doc3)

print(f"문서1-문서2 유사도: {sim_1_2:.2f}")  # 0.98 (매우 유사)
print(f"문서1-문서3 유사도: {sim_1_3:.2f}")  # 0.00 (전혀 다름)

코사인 유사도가 내적을 벡터 크기로 정규화하는 이유는 무엇일까? 문서 길이의 영향을 제거하기 위해서다. 긴 문서는 단어가 많아서 벡터 크기가 크다. 정규화하지 않으면 긴 문서끼리 유사도가 높게 나온다. 정규화하면 문서 길이와 관계없이 순수한 내용의 유사성만 측정할 수 있다.

물리학에서 일(Work) = 힘 · 변위도 내적이다. 힘 벡터와 변위 벡터라는 3차원 정보가 에너지라는 스칼라 값으로 압축된다. 여기서 중요한 것은 힘의 방향과 움직인 방향의 관계다. 같은 방향이면 양의 일을, 반대 방향이면 음의 일을 한다.

짐을 들고 계단을 오를 때를 생각해보자. 힘은 위쪽으로, 이동도 위쪽이다. 두 벡터가 같은 방향이므로 양의 일을 한다. 에너지를 소비한다. 반대로 내려올 때는 중력이 아래로 작용하고 이동도 아래다. 중력이 양의 일을 한다. 우리는 오히려 에너지를 받는다. 이것이 내리막이 편한 이유다.

외적: 새로운 차원의 창조

벡터의 외적(Cross Product)은 정반대의 일을 한다. 3차원 벡터 두 개를 외적하면 그 두 벡터에 수직인 새로운 3차원 벡터가 만들어진다. 차원의 개수는 유지되지만, 완전히 새로운 방향이 창조된다.

외적의 기하학적 의미를 이해하는 것이 중요하다. 두 벡터가 만드는 평면에 수직인 벡터를 만든다. 방향은 오른손 법칙을 따른다. 오른손 손가락을 첫 번째 벡터에서 두 번째 벡터로 감싸면, 엄지손가락이 가리키는 방향이 외적의 방향이다.

x축 단위벡터 [1, 0, 0]과 y축 단위벡터 [0, 1, 0]을 외적하면 z축 단위벡터 [0, 0, 1]이 나온다. 두 개의 방향 정보로부터 제3의 방향 정보가 생성된 것이다. 이것은 2차원 평면에서 3차원 공간으로의 확장을 의미한다.

# 3차원 벡터의 외적
vector_x = np.array([1, 0, 0])  # x축
vector_y = np.array([0, 1, 0])  # y축

# 외적: 두 벡터에 수직인 새 벡터
cross_product = np.cross(vector_x, vector_y)
print(f"x × y = {cross_product}")  # [0 0 1] (z축)

# 순서를 바꾸면 반대 방향
reverse_cross = np.cross(vector_y, vector_x)
print(f"y × x = {reverse_cross}")  # [0 0 -1] (-z축)

외적의 크기는 두 벡터가 만드는 평행사변형의 넓이와 같다. |a × b| = |a| × |b| × sin(θ)다. sin(θ)가 두 벡터가 얼마나 수직에 가까운지를 나타낸다. 평행하면 sin(0°) = 0이고 외적은 영벡터가 된다. 수직이면 sin(90°) = 1이고 외적의 크기가 최대가 된다.

이러한 기하학적 의미 때문에 3D 그래픽스에서 면의 법선 벡터를 구할 때 외적을 사용한다. 삼각형의 두 변을 벡터로 만들고 외적을 구하면, 그 삼각형 면에 수직인 벡터를 얻을 수 있다. 이 법선 벡터는 조명 계산에 필수적이다. 빛이 표면에 얼마나 직각으로 들어오는지 계산할 때 사용한다.

# 평행사변형과 삼각형 넓이 계산
def parallelogram_area(v1, v2):
    cross = np.cross(v1, v2)
    return np.linalg.norm(cross)

def triangle_area(v1, v2):
    return parallelogram_area(v1, v2) / 2

# 두 벡터가 만드는 도형
side_a = np.array([3, 0, 0])
side_b = np.array([0, 4, 0])

print(f"평행사변형 넓이: {parallelogram_area(side_a, side_b)}")  # 12
print(f"삼각형 넓이: {triangle_area(side_a, side_b)}")  # 6

외적으로 넓이를 구할 수 있는 이유는 무엇일까? 외적의 크기가 평행사변형 넓이와 같기 때문이다. 이것은 2차원 정보(두 변)를 3차원 정보(법선 벡터)로 확장하면서, 그 크기에 넓이 정보를 인코딩한 것이다. 놀라운 것은 3차원 공간에서 넓이라는 2차원 개념을 벡터로 표현할 수 있다는 점이다.

물리학에서 토크(회전력)는 위치 벡터와 힘 벡터의 외적이다. 렌치로 볼트를 돌릴 때, 렌치의 길이(위치 벡터)와 가해지는 힘(힘 벡터)이 결합되어 회전축 방향과 회전력 크기(토크 벡터)라는 새로운 정보를 만들어낸다. 직선 운동의 정보가 회전 운동의 정보로 변환되는 것이다.

# 토크(회전력) 계산
def torque(position, force):
    """토크 = 위치 벡터 × 힘 벡터"""
    return np.cross(position, force)

# 렌치로 볼트 돌리기
lever_arm = np.array([0.3, 0, 0])     # 30cm 렌치
applied_force = np.array([0, -50, 0])  # 50N 아래로

torque_vector = torque(lever_arm, applied_force)
print(f"토크: {torque_vector} N·m")  # [0 0 -15] (z축 중심 시계방향)

# 직선 운동 정보가 회전 운동 정보로 변환됐다!

토크의 방향이 회전축을 나타내는 것도 놀랍다. 오른손 법칙에 따라 토크 벡터 방향으로 오른손 엄지를 향하게 하면, 나머지 손가락이 감기는 방향이 회전 방향이다. 3차원 벡터 하나로 회전축과 회전 방향, 회전력 크기를 모두 표현할 수 있다.

각운동량도 외적으로 정의된다. L = r × p에서 위치 벡터와 운동량 벡터의 외적이 각운동량이다. 직선 운동하는 물체도 원점에서 보면 각운동량을 갖는다. 행성이 태양 주위를 공전할 때, 위치와 속도가 변해도 각운동량은 보존된다. 이것이 케플러의 제2법칙(면적 속도 일정의 법칙)의 수학적 표현이다.

프로그래밍 패러다임에서 보는 사칙연산

프로그래밍은 데이터를 변환하는 과정이다. 이 과정에서 사칙연산은 가장 기본적인 변환 도구가 된다. Map-Reduce, 해시 함수, 컨볼루션 같은 핵심 알고리즘들이 어떻게 사칙연산을 활용하는지 자세히 살펴보자.

Map-Reduce: 차원 변환의 패러다임

함수형 프로그래밍의 Map-Reduce 패러다임은 사칙연산의 본질을 그대로 보여준다. Map은 각 요소에 함수를 적용하여 변환하는 곱셈적 연산이다. Reduce는 모든 요소를 하나로 합치는 덧셈적 연산이다.

Map이 곱셈적이라는 말의 의미를 생각해보자. 곱셈은 같은 값을 여러 번 더하는 것이다. Map도 같은 함수를 여러 요소에 반복 적용한다. 각 요소가 독립적으로 변환되므로 병렬 처리가 가능하다. 이것이 MapReduce가 빅데이터 처리의 핵심이 된 이유다.

Reduce는 여러 값을 하나로 축약한다. 덧셈이 여러 수를 하나로 합치듯이, Reduce는 배열의 모든 요소를 단일 값으로 집계한다. 합계, 평균, 최댓값, 최솟값 등 모든 집계 연산이 Reduce의 변형이다.

const data = [1, 2, 3, 4, 5];

// Map: 곱셈적 변환 (각 요소를 독립적으로 변환)
const mapped = data.map(x => x * 2);
console.log(mapped);  // [2, 4, 6, 8, 10]

// Reduce: 덧셈적 합성 (모든 요소를 하나로)
const reduced = mapped.reduce((sum, val) => sum + val, 0);
console.log(reduced);  // 30

// 5차원 → 5차원 → 1차원으로 변환됐다

이 간단한 예제가 구글 검색의 핵심 원리다. 수십억 개의 웹페이지(data)에서 특정 단어를 찾고(map), 결과를 순위별로 정렬한다(reduce). 각 서버가 독립적으로 map을 수행하고, 결과를 모아서 reduce한다. 이렇게 거대한 문제를 작은 문제로 나누어 해결한다.

실제 데이터 처리 예제를 보자. 온라인 쇼핑몰의 판매 데이터를 분석한다고 가정하자.

# 판매 데이터 분석
sales_data = [
    {'product': 'A', 'quantity': 10, 'price': 100},
    {'product': 'B', 'quantity': 5, 'price': 200},
    {'product': 'C', 'quantity': 8, 'price': 150}
]

# Map: 각 제품의 매출 계산 (곱셈)
revenues = list(map(lambda x: x['quantity'] * x['price'], sales_data))
print(f"제품별 매출: {revenues}")  # [1000, 1000, 1200]

# Reduce: 전체 매출 합계 (덧셈)
from functools import reduce
total_revenue = reduce(lambda a, b: a + b, revenues)
print(f"전체 매출: {total_revenue}")  # 3200

여기서 Map은 각 제품의 수량과 가격을 곱해 매출을 계산한다. 이것은 2차원 정보(수량, 가격)를 1차원 정보(매출)로 변환하는 것이다. Reduce는 모든 제품의 매출을 더해 전체 매출을 구한다. n개의 매출 정보가 1개의 총합 정보로 압축된다.

이 패턴은 빅데이터 처리의 핵심이다. 수백만 개의 데이터를 병렬로 변환(Map)하고, 결과를 집계(Reduce)하여 의미 있는 정보를 추출한다. 구글의 MapReduce, 아파치의 Spark 모두 이 원리를 따른다.

Hadoop의 MapReduce가 단어 수를 세는 과정을 보자. Map 단계에서 각 단어를 (단어, 1) 형태로 변환한다. Reduce 단계에서 같은 단어의 1들을 모두 더한다. “hello world hello”는 [(“hello”, 1), (“world”, 1), (“hello”, 1)]이 되고, 최종적으로 {“hello”: 2, “world”: 1}이 된다. 간단한 사칙연산의 조합이 거대한 텍스트 분석을 가능하게 한다.

해시 함수: 정보의 차원 변환

해시 함수는 임의 길이의 문자열을 고정 길이의 숫자로 변환한다. 이 과정에서 곱셈과 덧셈, 그리고 모듈로 연산이 절묘하게 조합된다.

해시 함수의 핵심은 충돌을 최소화하는 것이다. 서로 다른 입력이 같은 해시값을 갖는 것을 충돌이라 한다. 좋은 해시 함수는 입력을 해시 테이블 전체에 고르게 분산시킨다. 이를 위해 소수를 곱하고, 비트 시프트를 하고, XOR 연산을 수행한다.

def simple_hash(text):
    """간단한 해시 함수 구현"""
    hash_value = 0
    for char in text:
        # 현재 해시값에 31을 곱하고 문자 코드를 더한다
        hash_value = (hash_value * 31 + ord(char)) % (2**32)
    return hash_value

# 테스트
print(f"'hello' 해시: {simple_hash('hello')}")
print(f"'world' 해시: {simple_hash('world')}")

# 무한 차원의 문자열 공간을 유한 차원의 정수 공간으로 압축했다

왜 31을 곱할까? 31은 소수이면서 2^5 - 1이다. 소수를 사용하면 해시값이 고르게 분포한다. 2^5 - 1은 비트 시프트와 뺄셈으로 빠르게 계산할 수 있다. hash * 31은 (hash << 5) - hash와 같다. 컴파일러가 이런 최적화를 자동으로 수행한다.

문자의 위치가 해시값에 영향을 주는 것도 중요하다. “abc”와 “cba”가 다른 해시값을 가져야 한다. 곱셈이 위치 정보를 인코딩한다. 첫 문자는 31^(n-1)을 곱하고, 둘째 문자는 31^(n-2)를 곱한다. 이렇게 각 문자의 위치가 해시값에 반영된다.

모듈로 연산은 결과를 특정 범위로 제한한다. 2^32로 나누면 32비트 정수 범위에 맞춘다. 해시 테이블 크기가 100이면 %100으로 인덱스를 구한다. 이것은 무한 차원을 유한 차원으로 압축하는 과정이다.

해시 함수는 어디에 쓰일까? HashMap, HashSet 같은 자료구조의 핵심이다. O(1) 시간에 검색, 삽입, 삭제가 가능하다. 데이터베이스 인덱스, 캐시 시스템, 로드 밸런서 등 모든 곳에서 사용된다. Git의 커밋 ID도 해시값이다. 파일 내용의 SHA-1 해시가 커밋의 고유 ID가 된다.

컨볼루션: 곱셈과 덧셈의 조화

이미지 처리와 딥러닝에서 핵심적인 컨볼루션 연산은 곱셈과 덧셈의 조화를 보여준다. 커널(필터)을 이미지 위에서 슬라이딩하면서, 각 위치에서 대응하는 픽셀값들을 곱한 후 모두 더한다.

컨볼루션이 왜 이미지 처리에 효과적일까? 지역적 패턴을 감지하기 때문이다. 엣지, 모서리, 텍스처 같은 특징은 인접한 픽셀들의 관계에서 나타난다. 컨볼루션은 이런 지역적 관계를 수치화한다.

import numpy as np

def convolve_2d(image, kernel):
    """간단한 2D 컨볼루션 구현"""
    k_height, k_width = kernel.shape
    i_height, i_width = image.shape
    
    # 출력 이미지 크기
    output = np.zeros((i_height - k_height + 1, i_width - k_width + 1))
    
    for i in range(output.shape[0]):
        for j in range(output.shape[1]):
            # 커널과 이미지 패치의 요소별 곱셈
            patch = image[i:i+k_height, j:j+k_width]
            product = patch * kernel
            # 모든 곱셈 결과를 더함
            output[i, j] = np.sum(product)
    
    return output

# 엣지 검출 커널 (Sobel)
sobel_x = np.array([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]
])

# 5x5 샘플 이미지
image = np.array([
    [10, 10, 10, 10, 10],
    [10, 50, 50, 50, 10],
    [10, 50, 50, 50, 10],
    [10, 50, 50, 50, 10],
    [10, 10, 10, 10, 10]
])

edges = convolve_2d(image, sobel_x)
print("엣지 검출 결과:")
print(edges)

Sobel 필터를 자세히 보자. 왼쪽 열은 음수(-1, -2, -1), 오른쪽 열은 양수(1, 2, 1)다. 가운데 열은 0이다. 이 필터를 적용하면 수직 엣지에서 큰 값이 나온다. 왼쪽이 어둡고 오른쪽이 밝으면 양수, 반대면 음수가 된다.

가운데 행의 가중치가 2인 이유는 무엇일까? 가운데 픽셀이 엣지 검출에 더 중요하기 때문이다. 가우시안 분포를 근사한 것이다. 이렇게 하면 노이즈에 강한 엣지 검출이 가능하다.

컨볼루션의 계산 과정을 보자. 3×3 커널과 3×3 이미지 패치가 만난다. 대응하는 9개 위치에서 곱셈이 일어난다. 그 결과를 모두 더한다. 9차원 정보가 1차원으로 압축된다. 이 과정이 이미지 전체에서 반복된다.

CNN(Convolutional Neural Network)이 이미지 인식에서 혁명을 일으킨 이유가 바로 여기에 있다. 컨볼루션 층들이 저수준 특징(엣지, 모서리)부터 고수준 특징(눈, 코, 얼굴)까지 계층적으로 추출하면서, 원본 이미지의 차원을 의미 있는 특징 공간으로 변환한다.

첫 번째 층은 단순한 엣지를 감지한다. 두 번째 층은 엣지의 조합으로 모서리나 곡선을 찾는다. 세 번째 층은 더 복잡한 패턴을 인식한다. 마지막 층은 전체 객체를 분류한다. 각 층은 이전 층의 출력을 입력으로 받아 더 추상적인 특징을 추출한다.

풀링(Pooling)도 차원 축소다. 2×2 맥스 풀링은 4개 픽셀 중 최댓값만 선택한다. 이미지 크기가 1/4로 줄어든다. 중요한 특징은 보존하면서 계산량을 줄인다. 이것도 일종의 정보 압축이다.

정보 이론에서의 사칙연산

정보 이론은 정보를 수학적으로 정량화한다. 여기서 사칙연산은 불확실성을 측정하고, 정보량을 계산하며, 데이터를 압축하는 핵심 도구가 된다. 엔트로피와 상호정보량이 어떻게 곱셈과 덧셈으로 계산되는지 자세히 살펴보자.

엔트로피: 불확실성의 측정

정보 이론의 핵심 개념인 엔트로피는 곱셈과 덧셈으로 계산된다. 각 사건의 확률 p와 그 정보량 log(1/p)를 곱한 후 모두 더한다.

H = -Σ p × log(p)

이 공식의 의미를 하나씩 뜯어보자. log(1/p)는 놀라움의 정도를 나타낸다. 확률이 낮은 사건일수록 일어났을 때 놀랍다. 비가 올 확률이 10%인 날 비가 오면 놀랍지만, 90%인 날 비가 오면 당연하다. 로그를 사용하는 이유는 독립적인 사건들의 정보량을 더할 수 있게 하기 위해서다. 두 독립 사건이 동시에 일어날 확률은 곱셈이지만, 정보량은 덧셈이 되어야 직관적이다.

p × log(1/p)는 각 사건의 ‘평균적 기여도’를 계산한다. 자주 일어나는 사건(p가 큼)은 놀라움이 적고(log(1/p)가 작음), 드물게 일어나는 사건은 놀라움이 크다. 하지만 너무 드문 사건은 거의 일어나지 않으므로 전체 불확실성에 기여하는 바가 작다. 이 둘의 균형점에서 최대 엔트로피가 나타난다.

import numpy as np

def entropy(probabilities):
    """섀넌 엔트로피 계산"""
    H = 0
    for p in probabilities:
        if p > 0:
            # 확률과 정보량을 곱하고
            info = -p * np.log2(p)
            # 모두 더한다
            H += info
    return H

# 동전 던지기 (최대 불확실성)
fair_coin = [0.5, 0.5]
print(f"공정한 동전 엔트로피: {entropy(fair_coin):.2f} bits")  # 1.00

# 편향된 동전 (낮은 불확실성)
biased_coin = [0.9, 0.1]
print(f"편향된 동전 엔트로피: {entropy(biased_coin):.2f} bits")  # 0.47

# 확실한 결과 (불확실성 없음)
certain = [1.0, 0.0]
print(f"확실한 결과 엔트로피: {entropy(certain):.2f} bits")  # 0.00

공정한 동전의 엔트로피가 1비트인 것은 의미가 깊다. 앞면인지 뒷면인지 알려면 정확히 1비트의 정보가 필요하다는 뜻이다. 편향된 동전은 0.47비트로, 평균적으로 절반 정도의 정보만 있으면 된다. 이것은 압축의 이론적 한계를 결정한다. 공정한 동전 던지기 결과 1000번을 저장하려면 최소 1000비트가 필요하지만, 편향된 동전은 470비트면 충분하다.

엔트로피가 압축과 연결되는 이유를 생각해보자. 텍스트 파일에서 ‘e’가 가장 자주 나타나고 ‘z’가 드물게 나타난다면, ‘e’에는 짧은 코드를, ‘z’에는 긴 코드를 할당하는 것이 효율적이다. 모스 부호가 바로 이 원리다. 자주 쓰는 ‘e’는 점 하나(·), 드문 ‘z’는 대시-대시-점-점(–··)이다. 이것이 허프만 코딩의 기본 아이디어다.

이 공식이 압축 알고리즘의 이론적 한계를 결정한다. 데이터의 엔트로피보다 작게는 압축할 수 없다. ZIP, JPEG, MP3 등 모든 압축 기술이 이 원리를 따른다. 무손실 압축의 한계는 데이터의 엔트로피고, 손실 압축은 인간이 감지하지 못하는 정보를 버려서 엔트로피를 줄인다.

상호정보량: 연관성의 정량화

두 변수 X와 Y의 상호정보량은 그들이 공유하는 정보의 양을 측정한다:

I(X;Y) = Σ p(x,y) × log(p(x,y) / (p(x)×p(y)))

이 복잡해 보이는 공식을 단계별로 이해해보자. p(x,y)는 X=x이고 Y=y일 결합 확률이다. p(x)×p(y)는 X와 Y가 독립일 때의 확률이다. 이 둘의 비율 p(x,y)/(p(x)×p(y))가 1이면 독립이고, 1보다 크면 양의 상관관계, 작으면 음의 상관관계가 있다.

로그를 취하면 비율이 정보량으로 변환된다. log(2) = 1비트는 불확실성이 절반으로 줄었다는 의미다. p(x,y)를 곱해서 가중 평균을 구하고, 모든 경우를 더하면 전체 상호정보량이 나온다.

def mutual_information(joint_prob, marginal_x, marginal_y):
    """상호정보량 계산"""
    MI = 0
    for i in range(len(marginal_x)):
        for j in range(len(marginal_y)):
            if joint_prob[i][j] > 0:
                # 결합 확률과 독립 확률의 비율
                ratio = joint_prob[i][j] / (marginal_x[i] * marginal_y[j])
                # 로그를 취하고 확률을 곱한 후 더한다
                MI += joint_prob[i][j] * np.log2(ratio)
    return MI

# 완전 상관 관계
joint_perfect = np.array([[0.5, 0], [0, 0.5]])
marginal = [0.5, 0.5]
mi_perfect = mutual_information(joint_perfect, marginal, marginal)
print(f"완전 상관 상호정보량: {mi_perfect:.2f} bits")  # 1.00

# 독립 관계
joint_independent = np.array([[0.25, 0.25], [0.25, 0.25]])
mi_independent = mutual_information(joint_independent, marginal, marginal)
print(f"독립 관계 상호정보량: {mi_independent:.2f} bits")  # 0.00

완전 상관일 때 상호정보량이 1비트라는 것은 한 변수를 알면 다른 변수를 완전히 알 수 있다는 뜻이다. 독립일 때 0비트는 한 변수가 다른 변수에 대해 아무 정보도 주지 않는다는 의미다.

상호정보량의 실용적 응용을 살펴보자. 기계학습에서 특징 선택을 할 때, 목표 변수와 상호정보량이 높은 특징을 선택한다. 예를 들어 스팸 메일 분류에서 ‘무료’, ‘당첨’ 같은 단어는 스팸 여부와 상호정보량이 높다. 반면 ‘그리고’, ‘하지만’ 같은 단어는 상호정보량이 낮아 분류에 도움이 안 된다.

나눗셈이 결합 확률과 독립 확률의 비율을 계산하고, 로그가 이를 정보량으로 변환하며, 곱셈과 덧셈이 전체 상호정보량을 집계한다. 이것은 기계학습의 특징 선택, 인과관계 추론, 통신 채널 용량 계산 등에 활용된다.

상호정보량은 상관계수와 다르다. 상관계수는 선형 관계만 포착하지만, 상호정보량은 비선형 관계도 감지한다. X와 Y=X²의 상관계수는 0이지만 상호정보량은 최대값을 갖는다. 이것이 상호정보량이 더 강력한 의존성 측정 도구인 이유다.

크로스 엔트로피 H(p,q) = -Σ p(x) × log(q(x))도 중요하다. 실제 분포 p를 예측 분포 q로 인코딩할 때 필요한 평균 비트 수다. 딥러닝의 분류 문제에서 손실 함수로 널리 사용된다. 실제 라벨(원-핫 인코딩)과 예측 확률의 크로스 엔트로피를 최소화하는 것이 학습 목표가 된다.

KL 발산(Kullback-Leibler divergence)은 두 분포의 차이를 측정한다. D_KL(p||q) = H(p,q) - H(p)로, 크로스 엔트로피에서 자체 엔트로피를 뺀 값이다. 이것은 q를 사용했을 때의 추가 비용을 나타낸다. VAE(Variational Autoencoder) 같은 생성 모델에서 근사 분포와 실제 분포의 차이를 측정하는 데 사용된다.

머신러닝에서의 차원 다루기

머신러닝은 본질적으로 차원을 변환하는 과정이다. 고차원 데이터에서 의미 있는 패턴을 찾아 저차원으로 압축하거나, 반대로 저차원 특징을 고차원 공간으로 확장하여 선형 분리를 가능하게 한다. 이 과정에서 사칙연산이 어떻게 정보를 변환하는지 살펴보자.

신경망: 차원 변환 기계

인공 신경망은 본질적으로 차원 변환 기계다. 각 층은 입력을 받아 가중치와 곱하고(곱셈), 더한 후(덧셈), 활성화 함수를 통과시킨다.

입력층이 784차원(28×28 이미지)이고, 은닉층이 128차원, 출력층이 10차원(0-9 숫자 분류)이라면, 네트워크는 784 → 128 → 10으로 차원을 변환한다. 고차원 이미지 공간을 저차원 분류 공간으로 압축하는 것이다.

이 과정을 자세히 들여다보자. 첫 번째 층에서 784개의 픽셀 값은 784×128 크기의 가중치 행렬과 곱해진다. 이것은 784차원 벡터를 128차원 공간으로 투영하는 것이다. 각 은닉 뉴런은 입력 이미지의 특정 패턴을 감지하는 필터 역할을 한다. 어떤 뉴런은 수직선을, 어떤 뉴런은 곡선을, 어떤 뉴런은 특정 각도의 엣지를 감지한다.

import numpy as np

class SimpleLayer:
    """단순한 신경망 레이어"""
    def __init__(self, input_dim, output_dim):
        # 가중치 행렬 (입력 차원 → 출력 차원)
        self.weights = np.random.randn(input_dim, output_dim) * 0.1
        self.bias = np.zeros(output_dim)
    
    def forward(self, x):
        # 행렬 곱셈으로 차원 변환
        z = np.dot(x, self.weights) + self.bias
        # 활성화 함수 (ReLU)
        return np.maximum(0, z)

# 784차원 → 128차원 → 10차원 변환
layer1 = SimpleLayer(784, 128)  # MNIST 이미지 → 은닉층
layer2 = SimpleLayer(128, 10)   # 은닉층 → 클래스

# 테스트 입력 (28×28 이미지를 평탄화)
input_image = np.random.randn(784)

# 차원 변환 과정
hidden = layer1.forward(input_image)  # 784 → 128
output = layer2.forward(hidden)       # 128 → 10

print(f"입력 차원: {input_image.shape}")
print(f"은닉층 차원: {hidden.shape}")
print(f"출력 차원: {output.shape}")

가중치 초기화에서 0.1을 곱하는 이유는 무엇일까? 가중치가 너무 크면 활성화 함수가 포화되고, 너무 작으면 그래디언트가 사라진다. 적절한 크기의 초기값이 학습을 안정적으로 만든다. Xavier 초기화나 He 초기화 같은 고급 기법들은 입력과 출력 차원을 고려하여 최적의 초기값을 계산한다.

ReLU(Rectified Linear Unit) 활성화 함수는 max(0, x)로 정의된다. 음수를 0으로 만들고 양수는 그대로 통과시킨다. 이 간단한 비선형 변환이 신경망에 표현력을 부여한다. 활성화 함수가 없다면 아무리 많은 층을 쌓아도 결국 하나의 선형 변환과 같아진다. 비선형성이 있어야 복잡한 패턴을 학습할 수 있다.

편향(bias)은 뉴런의 활성화 임계값을 조절한다. wx + b에서 b가 음수면 더 큰 입력이 필요하고, 양수면 작은 입력에도 활성화된다. 이것은 뉴런마다 다른 민감도를 갖게 하여 다양한 패턴을 학습할 수 있게 한다.

어텐션 메커니즘: 동적 가중치

트랜스포머 모델의 어텐션은 Query, Key, Value의 상호작용으로 작동한다:

Attention = softmax(QK^T / √d) × V

Query와 Key의 내적(차원 축소)으로 유사도를 계산하고, 이를 Value에 곱해(가중 평균) 중요한 정보를 추출한다. 이것은 문맥에 따라 동적으로 변하는 가중치를 통한 정보 선택이다.

어텐션의 핵심은 “어디에 주목할 것인가”를 학습하는 것이다. 문장 번역을 예로 들면, “나는 사과를 먹었다”를 영어로 번역할 때 “ate”를 생성하는 시점에서 “먹었다”에 가장 주목해야 한다. 어텐션은 이러한 관계를 자동으로 학습한다.

def attention_score(query, key, value, d_k):
    """간단한 어텐션 구현"""
    # Query와 Key의 내적으로 유사도 계산
    scores = np.dot(query, key.T) / np.sqrt(d_k)
    
    # 소프트맥스로 확률 변환
    weights = np.exp(scores) / np.sum(np.exp(scores))
    
    # Value에 가중 평균 적용
    output = np.dot(weights, value)
    return output, weights

# 예제
d_k = 4  # 차원
query = np.random.randn(1, d_k)
key = np.random.randn(3, d_k)    # 3개 위치
value = np.random.randn(3, d_k)

output, weights = attention_score(query, key, value, d_k)
print(f"어텐션 가중치: {weights}")
print(f"출력 차원: {output.shape}")

√d_k로 나누는 이유는 스케일링이다. 차원이 클수록 내적 값이 커지고, 소프트맥스 함수가 극단적인 확률 분포를 만든다. 한 위치에만 1에 가까운 가중치가 집중되고 나머지는 0에 가까워진다. √d_k로 나누면 내적 값이 적절한 범위에 머물러 부드러운 어텐션 분포를 얻을 수 있다.

셀프 어텐션(Self-Attention)은 Query, Key, Value가 모두 같은 시퀀스에서 나온다. 문장의 각 단어가 다른 모든 단어와의 관계를 계산한다. “나는 학교에 다니는 학생이다”에서 “학생”은 “학교”와 “다니는”에 높은 어텐션을 가질 것이다. 이렇게 문맥 정보가 각 단어의 표현에 반영된다.

멀티헤드 어텐션(Multi-Head Attention)은 여러 개의 어텐션을 병렬로 수행한다. 각 헤드가 다른 관계를 학습한다. 어떤 헤드는 문법적 관계를, 어떤 헤드는 의미적 관계를 포착한다. 이들을 결합하여 풍부한 표현을 만든다.

차원 축소 기법들

PCA(주성분 분석)는 고차원 데이터를 저차원으로 투영한다. 데이터의 분산을 최대한 보존하는 방향(주성분)을 찾아, 그 방향으로 투영(내적)한다. 100차원 데이터를 2-3차원으로 축소하여 시각화할 수 있게 된다.

PCA의 수학적 원리는 공분산 행렬의 고유벡터를 찾는 것이다. 고유벡터는 데이터의 주요 변동 방향을 나타낸다. 가장 큰 고유값에 대응하는 고유벡터가 첫 번째 주성분이 된다. 이 방향으로 투영하면 데이터의 분산이 최대가 된다.

from sklearn.decomposition import PCA
import numpy as np

# 100차원 데이터 생성
np.random.seed(42)
high_dim_data = np.random.randn(1000, 100)

# 2차원으로 축소
pca = PCA(n_components=2)
low_dim_data = pca.fit_transform(high_dim_data)

print(f"원본 차원: {high_dim_data.shape}")
print(f"축소된 차원: {low_dim_data.shape}")
print(f"설명된 분산 비율: {pca.explained_variance_ratio_}")

# 100차원 → 2차원으로 압축하면서도 주요 정보는 보존했다

explained_variance_ratio_는 각 주성분이 설명하는 분산의 비율이다. 예를 들어 [0.3, 0.2]라면 첫 번째 주성분이 전체 변동의 30%, 두 번째가 20%를 설명한다는 의미다. 두 주성분으로 전체 정보의 50%를 보존한 것이다.

t-SNE나 UMAP 같은 비선형 차원 축소 기법들은 더 복잡한 변환을 수행한다. 고차원 공간의 이웃 관계를 저차원에서도 유지하려고 노력한다. 이것은 정보의 본질을 보존하면서 차원을 축소하는 예술이다.

t-SNE는 고차원에서 가까운 점들은 저차원에서도 가깝게, 먼 점들은 멀게 배치한다. 이를 위해 고차원과 저차원에서 각각 확률 분포를 정의하고, 두 분포의 차이(KL divergence)를 최소화한다. 이 과정에서 비선형 변환이 일어나 복잡한 구조도 시각화할 수 있다.

차원 축소가 중요한 이유는 ‘차원의 저주(Curse of Dimensionality)’ 때문이다. 차원이 높아질수록 데이터가 희박해진다. 100차원 공간을 균등하게 채우려면 1차원보다 10^100배 많은 데이터가 필요하다. 차원 축소는 이 문제를 완화하고, 계산 효율성을 높이며, 노이즈를 제거하는 효과가 있다.

물리학에서의 차원 변환

물리학은 자연 현상을 수식으로 표현한다. 이 과정에서 서로 다른 차원의 물리량들이 곱셈과 나눗셈을 통해 결합되고 분해된다. 에너지, 파동, 주파수 같은 개념들이 어떻게 차원 변환을 통해 서로 연결되는지 살펴보자.

에너지의 여러 형태

운동에너지 K = ½mv²를 보면, 질량(kg)과 속도의 제곱(m²/s²)이 곱해져 줄(J)이라는 에너지 단위가 된다. 여기서 속도를 제곱하는 것은 1차원 속도 정보를 2차원으로 확장하는 것이다. 왜 제곱일까? 물체를 가속하는 데 필요한 일이 속도에 비례하고, 그 거리도 속도에 비례하기 때문이다.

운동에너지 공식의 유도 과정을 생각해보자. 일(Work)은 힘과 거리의 곱이다. W = F × d. 일정한 힘으로 물체를 정지 상태에서 속도 v까지 가속시킨다고 하자. 평균 속도는 v/2이고, 걸린 시간은 t = v/a다. 이동 거리는 d = (v/2) × t = v²/2a가 된다. 힘 F = ma를 대입하면 W = ma × v²/2a = ½mv²가 나온다. 속도가 두 번 곱해지는 이유가 여기에 있다.

위치에너지 U = mgh는 질량, 중력가속도, 높이라는 세 가지 독립적 정보를 곱한다. 각각 물체의 양, 지구의 중력장 세기, 공간상의 위치를 나타내는 정보들이 결합되어 ‘저장된 일의 능력’이라는 새로운 정보가 된다.

이 공식에서 각 변수의 차원을 분석해보자. m은 kg, g는 m/s², h는 m이다. 이들을 곱하면 kg × m/s² × m = kg·m²/s²가 된다. 이것이 바로 줄(J)의 정의다. 서로 다른 종류의 정보가 곱셈을 통해 ‘에너지’라는 통일된 개념으로 합성되는 것이다.

# 운동에너지: K = 1/2 × m × v²
def kinetic_energy(mass, velocity):
    """운동에너지 계산"""
    return 0.5 * mass * velocity**2

# 위치에너지: U = m × g × h
def potential_energy(mass, gravity, height):
    """위치에너지 계산"""
    return mass * gravity * height

# 예제: 1kg 물체
mass = 1.0  # kg
velocity = 10.0  # m/s
height = 5.0  # m
g = 9.8  # m/s²

KE = kinetic_energy(mass, velocity)
PE = potential_energy(mass, g, height)

print(f"운동에너지: {KE:.1f} J")  # 50.0 J
print(f"위치에너지: {PE:.1f} J")  # 49.0 J

# 에너지 보존 법칙
total_energy = KE + PE
print(f"전체 에너지: {total_energy:.1f} J")

에너지 보존 법칙은 덧셈의 불변성을 보여준다. 롤러코스터를 타는 물체를 생각해보자. 높은 곳에서는 위치에너지가 크고 속도가 느려 운동에너지가 작다. 낮은 곳에서는 반대다. 하지만 두 에너지의 합은 항상 일정하다. 에너지가 형태를 바꿀 뿐 총량은 보존되는 것이다.

E = mc²는 질량과 에너지의 등가성을 보여준다. 질량(kg)에 광속(m/s)의 제곱을 곱하면 에너지(J)가 된다. 작은 질량이 거대한 에너지를 담고 있는 이유는 c²이 엄청나게 큰 수(약 9×10¹⁶ m²/s²)이기 때문이다. 1그램의 물질이 완전히 에너지로 변환되면 히로시마 원자폭탄과 맞먹는 에너지가 나온다.

파동과 주파수

파동 방정식에서 파장(λ)과 주파수(f)를 곱하면 속도(v)가 된다: v = λf

공간 정보(파장)와 시간 정보(주파수)가 결합되어 전파 속도라는 시공간 정보를 만든다. 빛의 경우 이 값이 항상 c(광속)로 일정하다는 것이 특수 상대성 이론의 출발점이 된다.

파장은 공간상에서 파동이 한 주기를 완성하는 거리다. 주파수는 1초에 몇 번 진동하는지를 나타낸다. 이 둘을 곱하면 1초 동안 파동이 이동한 거리, 즉 속도가 나온다. 차원 분석으로 보면 m × (1/s) = m/s가 된다.

빛의 경우 v = c로 일정하므로 λf = c다. 이것은 파장과 주파수가 반비례 관계임을 의미한다. 파장이 짧으면 주파수가 높고(자외선), 파장이 길면 주파수가 낮다(적외선). 이것이 전자기 스펙트럼의 기본 원리다.

푸리에 변환은 시간 영역의 신호를 주파수 영역으로 변환한다. 복잡한 파형을 단순한 사인파들의 합으로 분해하는 것이다. 이것은 곱셈(각 주파수 성분과 기저 함수의 곱)과 적분(연속적인 덧셈)을 통해 이루어진다.

푸리에 변환의 의미를 음악으로 이해해보자. 오케스트라 연주는 여러 악기의 소리가 섞인 복잡한 파형이다. 푸리에 변환을 하면 각 악기의 주파수 성분을 분리할 수 있다. 바이올린의 고음, 첼로의 저음, 플루트의 중음이 각각 어떤 세기로 연주되는지 알 수 있다. 이것이 음악 앱이 실시간으로 음정을 표시할 수 있는 원리다.

import numpy as np
from scipy.fft import fft, fftfreq

# 복합 신호 생성 (여러 주파수 성분)
sample_rate = 1000  # Hz
duration = 1.0  # seconds
t = np.linspace(0, duration, int(sample_rate * duration))

# 10Hz, 50Hz, 100Hz 성분을 포함한 신호
signal = (np.sin(2 * np.pi * 10 * t) + 
          0.5 * np.sin(2 * np.pi * 50 * t) + 
          0.3 * np.sin(2 * np.pi * 100 * t))

# 푸리에 변환 (시간 → 주파수)
fft_result = fft(signal)
frequencies = fftfreq(len(signal), 1/sample_rate)

# 주파수 성분 추출
positive_freq_idx = frequencies > 0
freqs = frequencies[positive_freq_idx]
magnitude = np.abs(fft_result[positive_freq_idx])

# 주요 주파수 찾기
peak_indices = np.where(magnitude > np.max(magnitude) * 0.1)[0]
peak_frequencies = freqs[peak_indices]

print("검출된 주파수 성분:")
for freq in peak_frequencies:
    print(f"  {freq:.1f} Hz")

이 코드에서 푸리에 변환은 시간 영역의 복잡한 신호를 주파수 영역의 단순한 성분들로 분해한다. 각 주파수에서의 진폭이 그 주파수 성분의 강도를 나타낸다. 이것은 차원 변환의 관점에서 보면, 시간 차원의 정보를 주파수 차원의 정보로 변환하는 것이다.

역푸리에 변환을 하면 다시 원래 신호를 복원할 수 있다. 이것은 정보가 손실 없이 다른 차원으로 변환될 수 있음을 보여준다. MP3 압축, JPEG 이미지 압축, 5G 통신 등 현대 기술의 많은 부분이 이 원리를 활용한다.

양자역학에서 불확정성 원리도 푸리에 변환과 관련이 있다. 위치와 운동량이 푸리에 변환 쌍이기 때문에, 하나를 정확히 알수록 다른 하나는 불확실해진다. 시간-주파수 불확정성도 마찬가지다. 짧은 시간의 신호일수록 주파수 스펙트럼이 넓어진다. 이것은 정보가 서로 다른 차원 사이에서 트레이드오프 관계를 갖는다는 것을 의미한다.

경제학과 금융에서의 응용

경제학과 금융은 사칙연산의 힘을 가장 직접적으로 보여주는 분야다. 돈이 시간에 따라 어떻게 성장하고 축소되는지, 미래의 가치를 현재로 어떻게 환산하는지 살펴보자. 여기서 곱셈과 나눗셈은 시간이라는 차원을 다루는 핵심 도구가 된다.

복리: 지수적 차원 확장

복리 계산은 곱셈의 반복을 통한 지수적 성장을 보여준다:

최종금액 = 원금 × (1 + 이율)^기간

여기서 지수는 시간 차원을 확장한다. 단순 이자가 덧셈적 성장이라면, 복리는 곱셈적 성장이다. 아인슈타인이 복리를 “세계 8번째 불가사의”라고 부른 이유가 여기에 있다.

복리의 마법은 이자가 원금에 더해져서 다시 이자를 낳는 데 있다. 첫해에 100만원에 대한 5% 이자는 5만원이다. 하지만 둘째 해에는 105만원에 대한 5% 이자인 5만 2500원을 받는다. 이 차이가 처음에는 작아 보이지만, 시간이 지날수록 기하급수적으로 커진다.

def compound_interest(principal, rate, years, compounds_per_year=1):
    """복리 계산"""
    amount = principal * (1 + rate/compounds_per_year)**(compounds_per_year * years)
    return amount

# 초기 투자금 100만원, 연 5%, 10년
principal = 1000000
annual_rate = 0.05
years = 10

# 연복리
annual_compound = compound_interest(principal, annual_rate, years, 1)
# 월복리
monthly_compound = compound_interest(principal, annual_rate, years, 12)
# 일복리
daily_compound = compound_interest(principal, annual_rate, years, 365)

print(f"초기 투자금: {principal:,}원")
print(f"연복리 (10년): {annual_compound:,.0f}원")
print(f"월복리 (10년): {monthly_compound:,.0f}원")
print(f"일복리 (10년): {daily_compound:,.0f}원")

# 단순이자와 비교
simple_interest = principal * (1 + annual_rate * years)
print(f"단순이자 (10년): {simple_interest:,.0f}원")

# 복리는 곱셈의 반복, 단순이자는 덧셈의 반복이다

이 코드에서 compounds_per_year는 연간 복리 계산 횟수를 나타낸다. 월복리는 12번, 일복리는 365번 계산한다. 계산 횟수가 많을수록 최종 금액이 커지는 것을 볼 수 있다. 이것은 연속 복리의 개념으로 이어지는데, 무한히 자주 계산하면 자연상수 e를 사용한 공식 A = Pe^(rt)가 된다.

단순이자와 복리의 차이를 보면, 10년 후 단순이자는 150만원이지만 연복리는 약 163만원이다. 13만원의 차이는 모두 “이자의 이자”에서 나온 것이다. 이것이 장기 투자에서 복리의 힘이 강조되는 이유다.

복리 공식에서 지수가 하는 역할을 주목해보자. (1 + 이율)을 시간만큼 거듭제곱한다. 이것은 시간이라는 1차원 정보를 지수 차원으로 확장하는 것이다. 10년이면 10제곱, 20년이면 20제곱이 된다. 시간이 두 배가 되면 결과는 두 배가 아니라 제곱이 되는 것이다.

현재가치: 시간 차원의 축소

미래의 돈을 현재가치로 할인할 때는 나눗셈을 사용한다:

현재가치 = 미래가치 / (1 + 할인율)^기간

이것은 미래의 정보를 현재의 관점으로 정규화하는 과정이다. 시간이라는 차원을 제거하여 서로 다른 시점의 현금흐름을 비교 가능하게 만든다.

왜 미래의 돈이 현재의 돈보다 가치가 적을까? 첫째, 인플레이션으로 화폐 가치가 하락한다. 둘째, 현재의 돈은 투자하여 수익을 낼 수 있다. 셋째, 미래는 불확실하다. 이러한 요소들이 할인율에 반영된다.

def present_value(future_value, discount_rate, years):
    """현재가치 계산"""
    return future_value / (1 + discount_rate)**years

# 10년 후 1억원의 현재가치
future_value = 100000000
discount_rate = 0.05
years = 10

pv = present_value(future_value, discount_rate, years)
print(f"10년 후 {future_value:,}원의 현재가치: {pv:,.0f}원")

# 연도별 현재가치 변화
print("\n연도별 현재가치:")
for year in [1, 5, 10, 20]:
    pv = present_value(future_value, discount_rate, year)
    print(f"  {year}년 후: {pv:,.0f}원")

이 계산에서 나눗셈이 수행하는 역할을 보자. 미래가치를 (1 + 할인율)^기간으로 나누는 것은 복리의 정확한 역과정이다. 복리가 시간을 통해 가치를 확장한다면, 현재가치 계산은 시간을 거슬러 가치를 축소한다.

5% 할인율로 계산하면, 10년 후 1억원의 현재가치는 약 6,139만원이다. 즉, 지금 6,139만원을 5% 수익률로 투자하면 10년 후 1억원이 된다는 의미다. 20년 후 1억원의 현재가치는 3,769만원으로 더욱 작아진다. 시간이 멀수록 현재가치가 급격히 감소하는 것이다.

이러한 현재가치 개념은 투자 결정의 핵심이다. 부동산을 살 것인가 임대할 것인가? 회사를 인수할 것인가? 새로운 프로젝트를 시작할 것인가? 모든 결정은 미래 현금흐름의 현재가치를 계산하여 이루어진다. NPV(순현재가치)가 양수면 투자하고, 음수면 투자하지 않는다.

현재가치 계산에서 할인율의 선택이 결과를 크게 좌우한다. 할인율이 높을수록 미래 가치는 작아진다. 위험한 투자일수록 높은 할인율을 적용한다. 국채는 3%, 대기업 회사채는 5%, 벤처 투자는 20% 이상의 할인율을 사용할 수 있다. 이것은 위험을 수치화하는 방법이다.

암호학: 정보 변환의 극치

암호학은 사칙연산을 가장 교묘하게 활용하는 분야다. 여기서 곱셈과 나눗셈, 지수 연산과 모듈로 연산이 어떻게 우리의 정보를 안전하게 보호하는지 살펴보자. 암호화의 핵심은 한 방향으로는 계산이 쉽지만, 역방향으로는 극도로 어렵다는 비대칭성에 있다.

RSA: 소인수분해의 어려움

RSA 암호화는 두 큰 소수 p와 q를 곱한 n = p × q를 사용한다. 곱셈은 쉽지만, 그 역과정인 소인수분해는 극도로 어렵다. 이 비대칭성이 암호의 안전성을 보장한다.

예를 들어 61과 53을 곱하면 3233이 된다. 이 계산은 초등학생도 할 수 있다. 하지만 3233만 주어졌을 때 이것이 어떤 두 소수의 곱인지 찾는 것은 훨씬 어렵다. 작은 수에서는 가능해 보이지만, 실제 RSA에서는 수백 자리의 소수를 사용한다. 현재의 슈퍼컴퓨터로도 수천 년이 걸릴 만큼 어려운 문제가 된다.

def simple_rsa_demo():
    """간단한 RSA 원리 시연"""
    # 작은 소수 사용 (실제로는 매우 큰 소수 필요)
    p, q = 61, 53
    n = p * q  # 곱셈은 쉽다
    print(f"p = {p}, q = {q}")
    print(f"n = p × q = {n}")
    
    # 오일러 파이 함수
    phi = (p - 1) * (q - 1)
    print(f"φ(n) = {phi}")
    
    # 공개키 지수 (보통 65537 사용)
    e = 17
    print(f"공개키: ({n}, {e})")
    
    # 개인키 계산 (모듈러 역원)
    # d × e ≡ 1 (mod φ(n))
    d = pow(e, -1, phi)  # Python 3.8+
    print(f"개인키: {d}")
    
    # 암호화와 복호화
    message = 42
    encrypted = pow(message, e, n)
    decrypted = pow(encrypted, d, n)
    
    print(f"\n원본 메시지: {message}")
    print(f"암호화: {message}^{e} mod {n} = {encrypted}")
    print(f"복호화: {encrypted}^{d} mod {n} = {decrypted}")
    
    return n, e, d

n, e, d = simple_rsa_demo()

# 곱셈은 쉽지만 소인수분해는 어렵다
# n = 3233을 다시 61과 53으로 분해하는 것은 큰 수에서 매우 어렵다

이 코드에서 주목할 점은 오일러 파이 함수 φ(n) = (p-1)(q-1)이다. 이것은 1부터 n까지의 수 중에서 n과 서로소인 수의 개수를 나타낸다. 공개키 e와 개인키 d의 관계는 d × e ≡ 1 (mod φ(n))으로 정의된다. 즉, d와 e를 곱한 것을 φ(n)으로 나눈 나머지가 1이 되어야 한다.

메시지를 암호화할 때는 M^e mod n을 계산하고, 복호화할 때는 C^d mod n을 계산한다. 이것이 가능한 이유는 페르마의 소정리와 오일러 정리라는 수학적 원리 때문이다. 공개키(n, e)는 누구나 알 수 있지만, 개인키 d를 구하려면 n을 소인수분해해야 한다. 이것이 바로 RSA의 안전성의 근거다.

공개키 (n, e)로 암호화: C = M^e mod n 개인키 d로 복호화: M = C^d mod n

지수 연산과 모듈로 연산이 정보를 안전하게 변환하고 복원한다. 여기서 모듈로 연산은 결과를 특정 범위로 제한하는 역할을 한다. 이것은 무한히 커질 수 있는 지수 연산 결과를 다루기 쉬운 크기로 만든다.

타원곡선: 기하학적 연산

타원곡선 암호화는 점들의 덧셈을 정의한다. 점 P를 n번 더한 nP는 쉽게 계산할 수 있지만, nP로부터 n을 찾는 것은 어렵다. 이것은 기하학적 공간에서의 일방향 함수다.

타원곡선은 y² = x³ + ax + b 형태의 방정식으로 정의된다. 이 곡선 위의 두 점을 ‘더한다’는 것은 우리가 아는 일반적인 덧셈과는 완전히 다르다. 기하학적으로는 두 점을 지나는 직선이 곡선과 만나는 세 번째 점을 x축 대칭시킨 것이 덧셈의 결과가 된다.

이러한 특별한 덧셈 연산은 군(group)이라는 수학적 구조를 만족한다. 항등원이 존재하고, 역원이 존재하며, 결합법칙이 성립한다. 이것이 타원곡선을 암호화에 사용할 수 있는 수학적 토대가 된다.

타원곡선 암호화의 강점은 RSA보다 훨씬 작은 키로도 동등한 보안을 제공한다는 것이다. RSA 3072비트 키와 같은 보안 수준을 타원곡선은 256비트 키로 달성할 수 있다. 이것은 모바일 기기나 IoT 장치처럼 연산 능력과 배터리가 제한된 환경에서 특히 중요하다.

컴퓨터 그래픽스에서의 차원 변환

컴퓨터 그래픽스는 차원 변환의 연속이다. 3차원 세계를 2차원 화면에 표현하고, 물체를 회전시키고, 크기를 조절하는 모든 과정이 행렬 곱셈으로 이루어진다. 여기서 곱셈과 나눗셈이 어떻게 가상 세계를 만들어내는지 살펴보자.

3D 변환 행렬

3D 그래픽스는 행렬 곱셈으로 변환을 수행한다. 회전, 이동, 스케일링 모두 행렬 연산으로 표현된다.

행렬 곱셈이 왜 3D 변환에 완벽한 도구일까? 첫째, 여러 변환을 하나의 행렬로 합성할 수 있다. 회전하고 이동하고 크기를 바꾸는 세 가지 작업을 각각 수행하는 대신, 세 행렬을 곱해서 하나의 변환 행렬을 만들 수 있다. 이것은 계산 효율성을 크게 높인다.

둘째, 행렬 곱셈은 선형 변환을 보존한다. 직선은 변환 후에도 직선이고, 평행선은 평행을 유지한다. 이것이 3D 모델의 기하학적 구조를 보존하면서 변환할 수 있는 이유다.

import numpy as np

def rotate_3d(point, angle, axis='z'):
    """3D 점을 회전시키는 함수"""
    rad = np.radians(angle)
    
    if axis == 'z':
        rotation_matrix = np.array([
            [np.cos(rad), -np.sin(rad), 0],
            [np.sin(rad),  np.cos(rad), 0],
            [0,            0,           1]
        ])
    elif axis == 'x':
        rotation_matrix = np.array([
            [1, 0,            0],
            [0, np.cos(rad), -np.sin(rad)],
            [0, np.sin(rad),  np.cos(rad)]
        ])
    else:  # y axis
        rotation_matrix = np.array([
            [np.cos(rad),  0, np.sin(rad)],
            [0,            1, 0],
            [-np.sin(rad), 0, np.cos(rad)]
        ])
    
    # 행렬 곱셈으로 변환
    return np.dot(rotation_matrix, point)

# 점 (1, 0, 0)을 z축 중심으로 90도 회전
point = np.array([1, 0, 0])
rotated = rotate_3d(point, 90, 'z')
print(f"원래 점: {point}")
print(f"회전된 점: {rotated}")

# 연속 변환 (회전 후 스케일)
def scale_3d(point, scale_factors):
    """3D 점을 스케일링"""
    scale_matrix = np.diag(scale_factors)
    return np.dot(scale_matrix, point)

scaled = scale_3d(rotated, [2, 2, 2])
print(f"스케일된 점: {scaled}")

이 코드에서 회전 행렬의 각 요소는 삼각함수로 구성된다. cos(θ)와 sin(θ)가 회전 각도를 인코딩한다. z축 회전 행렬을 보면, x와 y 좌표는 서로 영향을 주고받지만 z 좌표는 변하지 않는다. 이것이 z축을 중심으로 한 회전의 수학적 표현이다.

스케일 변환은 대각 행렬을 사용한다. 대각선 요소가 각 축의 스케일 팩터를 나타낸다. [2, 2, 2]는 모든 축을 2배로 확대한다는 의미다. 만약 [2, 1, 0.5]라면 x축은 2배, y축은 그대로, z축은 절반으로 축소된다.

연속 변환을 할 때는 행렬 곱셈의 순서가 중요하다. A × B ≠ B × A이기 때문이다. 먼저 회전하고 이동하는 것과 먼저 이동하고 회전하는 것은 완전히 다른 결과를 만든다. 이것은 3D 애니메이션에서 물체의 회전 중심을 설정할 때 특히 중요하다.

투영: 3D를 2D로

3차원을 2차원 화면에 투영하는 것은 차원 축소의 전형적인 예다. 원근 투영은 나눗셈을 사용하여 깊이 정보를 2D 좌표에 반영한다.

원근 투영의 핵심은 멀리 있는 물체가 작게 보인다는 원리다. 이것을 수학적으로 표현하면, x와 y 좌표를 z 좌표(깊이)로 나누는 것이다. z가 클수록(멀수록) 나눗셈 결과가 작아져서 화면상에서 작게 보인다.

def perspective_projection(point_3d, focal_length=1):
    """원근 투영 (3D → 2D)"""
    x, y, z = point_3d
    if z == 0:
        z = 0.0001  # 0으로 나누기 방지
    
    # 나눗셈으로 차원 축소
    x_2d = focal_length * x / z
    y_2d = focal_length * y / z
    
    return np.array([x_2d, y_2d])

# 3D 큐브의 정점들
cube_vertices = [
    [1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1],
    [-1, 1, 1], [-1, 1, -1], [-1, -1, 1], [-1, -1, -1]
]

# 카메라에서 거리 조정
distance = 3
cube_vertices = [[x, y, z + distance] for x, y, z in cube_vertices]

# 2D로 투영
projected_2d = [perspective_projection(v) for v in cube_vertices]

print("3D 정점 → 2D 투영:")
for v3d, v2d in zip(cube_vertices[:3], projected_2d[:3]):
    print(f"  {v3d} → [{v2d[0]:.2f}, {v2d[1]:.2f}]")

focal_length는 카메라의 초점 거리를 나타낸다. 이 값이 크면 망원렌즈처럼 원근감이 줄어들고, 작으면 광각렌즈처럼 원근감이 과장된다. 이것이 게임에서 FOV(Field of View)를 조절하는 원리다.

나눗셈이 수행하는 차원 축소는 단순한 정보 손실이 아니다. 3차원 깊이 정보를 2차원 크기 정보로 변환하는 것이다. 멀리 있는 물체의 z값이 크므로 x/z와 y/z는 작아지고, 가까운 물체는 크게 보인다. 이 간단한 나눗셈이 우리가 3D 게임과 영화에서 보는 모든 원근감을 만들어낸다.

큐브를 투영할 때 각 정점이 어떻게 변환되는지 보자. [1, 1, 4]라는 3D 점은 [0.25, 0.25]라는 2D 점이 된다. z값 4로 나누었기 때문에 원래 크기의 1/4이 된 것이다. 이렇게 8개의 3D 정점이 8개의 2D 점으로 변환되고, 이들을 연결하면 화면에 큐브가 그려진다.

실제 그래픽스 파이프라인에서는 이보다 복잡한 변환이 일어난다. 모델 변환, 뷰 변환, 투영 변환이 연속적으로 적용된다. 하지만 근본적으로는 모두 행렬 곱셈과 나눗셈의 조합이다. 수십만 개의 폴리곤으로 이루어진 3D 모델이 실시간으로 화면에 그려질 수 있는 것은 이러한 수학적 변환이 GPU에서 병렬로 처리되기 때문이다.

결론: 사칙연산의 깊은 의미

F = ma에서 시작된 나의 의문은 이제 명확해졌다. 질량과 가속도를 곱하는 것은 단순한 수치 계산이 아니라, 물질 정보와 운동 정보를 결합하여 힘이라는 새로운 차원의 정보를 창조하는 것이었다.

우리가 초등학교에서 배운 덧셈, 뺄셈, 곱셈, 나눗셈은 단순한 계산 도구가 아니다. 이들은 정보를 변환하는 근본적인 연산이다.

덧셈과 뺄셈은 같은 차원 내에서 정보를 결합하고 분리한다. 시간축에 흩어진 데이터를 모으고, 전체에서 부분을 추출하며, 변화를 측정한다.

곱셈과 나눗셈은 차원을 변환한다. 독립적인 정보들을 결합하여 새로운 차원을 만들고, 복잡한 정보를 단순한 단위로 정규화한다.

이 네 가지 연산의 조합으로 우리는:

벡터의 내적과 외적은 이러한 차원 변환의 극적인 예시였다. 내적은 곱셈과 덧셈을 조합하여 차원을 축소하고, 외적은 새로운 차원을 창조한다.

수학은 추상화의 언어고, 사칙연산은 그 언어의 가장 기본적인 문법이다. 이 간단한 도구들로 우리는 우주의 작동 원리를 기술하고, 정보를 자유자재로 변환하며, 상상을 현실로 만들어간다.

프로그래밍할 때 우리는 끊임없이 이 연산들을 사용한다. Map은 곱셈적 변환이고, Reduce는 덧셈적 결합이다. 해시 함수는 무한 차원을 유한 차원으로 압축하고, 컨볼루션은 곱셈과 덧셈의 조화로 특징을 추출한다.

머신러닝의 신경망은 차원 변환 기계다. 784차원의 이미지를 10차원의 분류로 변환하는 과정에서, 각 층은 행렬 곱셈으로 차원을 바꾸고 활성화 함수로 비선형성을 더한다.

나는 이제 계산기를 다르게 본다. 2 + 3을 누를 때, 단순히 5를 얻는 것이 아니라 두 정보를 결합하는 것이다. 4 × 5를 계산할 때, 20이라는 숫자가 아니라 새로운 차원의 정보를 창조하는 것이다.

수학은 추상화의 언어고, 사칙연산은 그 언어의 기본 문법이다. 이 간단한 도구로 우리는 우주를 이해하고, 정보를 변환하며, 상상을 현실로 만든다. 초등학교에서 배운 덧셈, 뺄셈, 곱셈, 나눗셈이 이렇게 깊고 아름다운 의미를 담고 있었다니.

다음에 수식을 볼 때, 코드를 작성할 때, 잠시 멈춰서 생각해보길 바란다. 이 연산이 어떤 정보를 결합하거나 분리하는가? 어떤 차원을 확장하거나 축소하는가? 그 순간 당신은 단순한 계산을 넘어, 정보의 본질을 다룰 수 있게 될 것이다.