텐서 연산

텐서(tensor)는 데이터와 딥러닝(deep learning) 사이의 공통 언어입니다. 모든 이미지, 문장, 기울기(gradient)가 텐서를 통해 흐릅니다.

유형: Build 언어: Python 선수 지식: Phase 1, Lessons 01 (Linear Algebra Intuition), 02 (Vectors, Matrices & Operations) 예상 시간: 약 90분

학습 목표

  • 형태(shape), 스트라이드(stride), 형태 변경(reshape), 전치(transpose), 원소별 연산(element-wise operation)을 지원하는 텐서 클래스(tensor class)를 처음부터 구현합니다.
  • 브로드캐스팅 규칙(broadcasting rule)을 적용해 서로 다른 형태의 텐서를 데이터 복사 없이 함께 연산합니다.
  • 내적(dot product), 행렬곱(matrix multiplication), 외적(outer product), 배치 연산(batched operation)을 아인슈타인 표기법(einsum) 식으로 작성합니다.
  • 멀티헤드 어텐션(multi-head attention)의 모든 단계에서 정확한 텐서 형태를 추적합니다.

문제

트랜스포머(transformer)를 만들었습니다. 순전파(forward pass)는 깔끔해 보입니다. 실행하니 RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x768 and 512x768)가 나옵니다. 형태를 들여다보다 전치를 시도합니다. 이번에는 Expected 4D input (got 3D input)이라고 합니다. unsqueeze를 하나 추가했더니 다른 곳이 깨집니다.

형태 오류(shape error)는 딥러닝 코드에서 가장 흔한 버그입니다. 개념 자체는 어렵지 않습니다. 각 연산에는 형태 계약(shape contract)이 있습니다. 하지만 트랜스포머에는 형태 변경, 전치, 브로드캐스트가 수십 번 이어지므로 한 축(axis)만 틀려도 오류가 연쇄적으로(cascade) 번집니다. 더 나쁜 경우는 형태 실수가 오류를 내지 않고 잘못된 차원으로 브로드캐스팅되거나 합산되어 쓰레기 값(garbage)을 조용히 만들어 내는 것입니다.

행렬(matrix)은 두 집합 사이의 쌍별 관계(pairwise relationship)를 다룹니다. 실제 데이터는 2차원(dimension)에 갇히지 않습니다. 배치(batch) 32개의 RGB 이미지, 크기 224x224는 4차원 텐서 (32, 3, 224, 224)입니다. 12-헤드 셀프 어텐션(self-attention)도 (batch, heads, seq_len, head_dim) 형태의 4차원 텐서입니다. 임의의 차원으로 일반화할 수 있는 자료구조(data structure)가 필요합니다. 그것이 텐서입니다. 텐서 연산을 익히면 형태 오류는 추적 가능한 문제가 됩니다.

사전 테스트

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

1.텐서(tensor)의 형태(shape)는 무엇을 설명하나요?

2.PyTorch에서 이미지 배치 텐서(image batch tensor)는 기본적으로 어떤 메모리 배치(layout)를 사용하나요?

0/2 답변 완료

개념

텐서(Tensor)란 무엇인가

텐서(tensor)는 균일한 데이터 타입(uniform data type)을 가진 다차원 배열(multi-dimensional array)입니다. 차원(dimension) 수를 계수(rank) 또는 차수(order)라고 부릅니다. 각 차원은 축(axis)입니다. 형태(shape)는 각 축의 크기(size)를 나열한 튜플(tuple)입니다.

graph LR
    S["Scalar<br/>rank 0<br/>shape: ()"] --> V["Vector<br/>rank 1<br/>shape: (3,)"]
    V --> M["Matrix<br/>rank 2<br/>shape: (2,3)"]
    M --> T3["3D Tensor<br/>rank 3<br/>shape: (2,2,2)"]
    T3 --> T4["4D Tensor<br/>rank 4<br/>shape: (B,C,H,W)"]

전체 원소 수(total elements)는 모든 크기의 곱(product)입니다. 형태 (2, 3, 4)2 * 3 * 4 = 24개 원소(element)를 가집니다.

딥러닝(Deep Learning)에서의 텐서 형태

데이터 타입마다 관례적인 텐서 형태가 있습니다.

graph TD
    subgraph Vision
        V1["(B, C, H, W)<br/>32, 3, 224, 224"]
    end
    subgraph NLP
        N1["(B, T, D)<br/>16, 128, 768"]
    end
    subgraph Attention
        A1["(B, H, T, D)<br/>16, 12, 128, 64"]
    end
    subgraph Weights
        W1["Linear: (out, in)<br/>Conv2D: (out_c, in_c, kH, kW)<br/>Embedding: (vocab, dim)"]
    end

PyTorch는 NCHW, 즉 채널 우선(channels-first)을 사용합니다. TensorFlow는 기본적으로 NHWC, 즉 채널 후행(channels-last)을 사용합니다. 메모리 배치(layout)가 일치하지 않으면 조용한 성능 저하(slowdown)나 오류가 발생합니다.

메모리 배치(Memory Layout)와 스트라이드(Stride)

2차원 배열도 메모리에서는 1차원 바이트(byte) 시퀀스입니다. 스트라이드(stride)는 각 축에서 한 칸 움직일 때 몇 개의 원소를 건너뛰어야 하는지 알려 줍니다.

graph LR
    subgraph "Row-major (C order)"
        R["a b c d e f<br/>strides: (3, 1)"]
    end
    subgraph "Column-major (F order)"
        C["a d b e c f<br/>strides: (1, 2)"]
    end

전치(transpose)는 데이터를 옮기지 않습니다. 스트라이드를 맞바꿀 뿐입니다. 그래서 텐서가 비연속(non-contiguous) 상태가 됩니다. 한 행의 원소가 메모리에서 더 이상 인접하지 않게 됩니다.

브로드캐스팅 규칙(Broadcasting Rule)

브로드캐스팅은 서로 다른 형태의 텐서를 데이터 복사 없이 함께 연산하게 해 줍니다. 형태를 오른쪽에서부터 맞춥니다. 두 차원은 값이 같거나 둘 중 하나가 1이면 호환(compatible)됩니다. 차원 수가 부족하면 왼쪽에 1을 채워(pad) 넣습니다.

Tensor A:     (8, 1, 6, 1)
Tensor B:        (7, 1, 5)
Padded B:     (1, 7, 1, 5)
Result:       (8, 7, 6, 5)

아인슈타인 표기법(Einsum): 보편 텐서 연산(Universal Tensor Operation)

아인슈타인 표기법(Einsum, Einstein summation)은 각 축에 문자를 붙입니다. 입력(input)에는 있지만 출력(output)에는 없는 축은 합산되어(summed) 사라집니다. 양쪽 모두에 있는 축은 유지됩니다.

graph LR
    subgraph "matmul: ik,kj -> ij"
        A["A(I,K)"] --> |"sum over k"| C["C(I,J)"]
        B["B(K,J)"] --> |"sum over k"| C
    end

주요 패턴(pattern):

  • i,i->: 내적(dot product)
  • i,j->ij: 외적(outer product)
  • ii->: 대각합(trace)
  • ij->ji: 전치(transpose)
  • bij,bjk->bik: 배치 행렬곱(batch matmul)
  • bhtd,bhsd->bhts: 어텐션 스코어(attention score)

직접 만들기

코드는 code/tensors.py에 있습니다. 각 단계는 해당 구현을 참조합니다.

단계 1: 텐서 저장소(Tensor Storage)와 스트라이드

텐서는 평탄한(flat) 리스트와 형태 메타데이터(metadata)를 저장합니다. 스트라이드는 다차원 인덱스(multi-dimensional index)를 평탄한 위치(flat position)로 매핑합니다.

class Tensor:
    def __init__(self, data, shape=None):
        if isinstance(data, (list, tuple)):
            self._data, self._shape = self._flatten_nested(data)
        elif isinstance(data, np.ndarray):
            self._data = data.flatten().tolist()
            self._shape = tuple(data.shape)
        else:
            self._data = [data]
            self._shape = ()

        if shape is not None:
            total = reduce(lambda a, b: a * b, shape, 1)
            if total != len(self._data):
                raise ValueError(
                    f"Cannot reshape {len(self._data)} elements into shape {shape}"
                )
            self._shape = tuple(shape)

        self._strides = self._compute_strides(self._shape)

형태 (3, 4)의 스트라이드는 (4, 1)입니다. 행(row)을 한 칸 이동하려면 4개 원소를, 열(column)을 한 칸 이동하려면 1개 원소를 건너뜁니다.

단계 2: 형태 변경(reshape), 축 제거(squeeze), 축 삽입(unsqueeze)

형태 변경(reshape)은 원소 순서를 바꾸지 않고 형태만 바꿉니다. 전체 원소 수는 같아야 합니다. -1은 해당 차원의 크기를 자동으로 추론(infer)하라는 뜻입니다.

t = Tensor(list(range(12)), shape=(2, 6))
r = t.reshape((3, 4))
r = t.reshape((-1, 3))

축 제거(squeeze)는 크기 1인 축을 제거합니다. 축 삽입(unsqueeze)은 축을 하나 추가합니다. 축 삽입은 브로드캐스팅에 매우 중요합니다. 편향 벡터(bias vector) (D,)를 배치 (B, T, D)에 더하려면 (1, 1, D)로 축 삽입해야 합니다.

t = Tensor(list(range(6)), shape=(1, 3, 1, 2))
s = t.squeeze()
v = Tensor([1, 2, 3])
u = v.unsqueeze(0)

단계 3: 전치(transpose)와 순열 변경(permute)

전치(transpose)는 두 축을 맞바꿉니다. 순열 변경(permute)은 모든 축의 순서를 바꿉니다. NCHW와 NHWC 사이를 변환할 때 사용합니다.

mat = Tensor(list(range(6)), shape=(2, 3))
tr = mat.transpose(0, 1)

t4d = Tensor(list(range(24)), shape=(1, 2, 3, 4))
perm = t4d.permute((0, 2, 3, 1))

전치나 순열 변경 후 텐서는 메모리에서 비연속(non-contiguous) 상태가 됩니다. PyTorch에서 view는 비연속 텐서에 대해 실패합니다. reshape를 사용하거나 먼저 .contiguous()를 호출해야 합니다.

단계 4: 원소별 연산(Element-wise Operation)과 축약(Reduction)

원소별 연산(덧셈, 곱셈, 뺄셈)은 각 원소에 독립적으로 적용되고 형태를 보존합니다. 축약 연산(reduction; 합, 평균, 최댓값)은 하나 이상의 축을 합쳐(collapse) 없앱니다.

a = Tensor([[1, 2], [3, 4]])
b = Tensor([[10, 20], [30, 40]])
c = a + b
d = a * 2
s = a.sum(axis=0)

합성곱 신경망(CNN)의 전역 평균 풀링(global average pooling)은 (B, C, H, W).mean(axis=[2, 3])으로 (B, C)를 만듭니다. 자연어 처리(NLP)의 시퀀스 평균 풀링(sequence mean pooling)은 (B, T, D).mean(axis=1)(B, D)를 만듭니다.

단계 5: NumPy 브로드캐스팅(Broadcasting)

tensors.pydemo_broadcasting_numpy()는 핵심 패턴을 보여 줍니다.

activations = np.random.randn(4, 3)
bias = np.array([0.1, 0.2, 0.3])
result = activations + bias

images = np.random.randn(2, 3, 4, 4)
scale = np.array([0.5, 1.0, 1.5]).reshape(1, 3, 1, 1)
result = images * scale

a = np.array([1, 2, 3]).reshape(-1, 1)
b = np.array([10, 20, 30, 40]).reshape(1, -1)
outer = a * b

쌍별 거리(pairwise distance)도 브로드캐스팅으로 계산합니다. (M, 2)(M, 1, 2)로, (N, 2)(1, N, 2)로 형태 변경한 뒤 빼고(subtract), 제곱하고(square), 마지막 축에 대해 합산(sum)한 다음 제곱근(square root)을 취하면 결과는 (M, N)이 됩니다.

단계 6: 아인슈타인 표기법(Einsum) 연산

demo_einsum()demo_einsum_gallery()는 자주 사용하는 패턴을 순서대로 보여 줍니다.

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])
dot = np.einsum("i,i->", a, b)

A = np.array([[1, 2], [3, 4], [5, 6]], dtype=float)
B = np.array([[7, 8, 9], [10, 11, 12]], dtype=float)
matmul = np.einsum("ik,kj->ij", A, B)

batch_A = np.random.randn(4, 3, 5)
batch_B = np.random.randn(4, 5, 2)
batch_mm = np.einsum("bij,bjk->bik", batch_A, batch_B)

축약(contraction) 연산 비용은 유지되는 인덱스와 합산되는 인덱스 크기의 곱입니다. bij,bjk->bik에서 B=32, I=128, J=64, K=128이면 32 * 128 * 64 * 128 = 33,554,432번의 곱셈-누산(multiply-add)이 필요합니다.

단계 7: 아인슈타인 표기법으로 어텐션 메커니즘(Attention Mechanism) 구현

demo_attention_einsum()은 멀티헤드 어텐션을 끝까지 구현합니다.

B, H, T, D = 2, 4, 8, 16
E = H * D

X = np.random.randn(B, T, E)
W_q = np.random.randn(E, E) * 0.02

Q = np.einsum("bte,ek->btk", X, W_q)
Q = Q.reshape(B, T, H, D).transpose(0, 2, 1, 3)

scores = np.einsum("bhtd,bhsd->bhts", Q, K) / np.sqrt(D)
weights = softmax(scores, axis=-1)
attn_output = np.einsum("bhts,bhsd->bhtd", weights, V)

concat = attn_output.transpose(0, 2, 1, 3).reshape(B, T, E)
output = np.einsum("bte,ek->btk", concat, W_o)

모든 단계가 텐서 연산입니다. 사영(projection)은 아인슈타인 표기법으로 표현한 행렬곱이고, 헤드 분리(head split)는 형태 변경과 전치, 어텐션 스코어는 아인슈타인 표기법 기반의 배치 행렬곱, 가중합(weighted sum)도 아인슈타인 표기법 기반의 배치 행렬곱, 헤드 결합(head merge)은 전치와 형태 변경, 출력 사영(output projection)도 아인슈타인 표기법으로 표현한 행렬곱입니다.

사용해보기

직접 구현 vs NumPy

연산직접 구현(Tensor 클래스)NumPy
생성Tensor([[1,2],[3,4]])np.array([[1,2],[3,4]])
형태 변경t.reshape((3,4))a.reshape(3,4)
전치t.transpose(0,1)a.T 또는 a.transpose(0,1)
축 제거t.squeeze(0)np.squeeze(a, 0)
합산t.sum(axis=0)a.sum(axis=0)
아인슈타인 표기법N/Anp.einsum("ij,jk->ik", a, b)

직접 구현 vs PyTorch

import torch

t = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
t.shape
t.stride()
t.is_contiguous()

t.reshape(3, 2)
t.unsqueeze(0)
t.transpose(0, 1)
t.transpose(0, 1).contiguous()

torch.einsum("ik,kj->ij", A, B)

PyTorch는 자동 미분(autograd), GPU 지원, 최적화된 BLAS 커널(kernel)을 추가로 제공합니다. 형태 의미론(shape semantics)은 동일합니다. 직접 구현한 버전을 이해하면 PyTorch의 형태 오류 메시지를 읽어낼 수 있습니다.

인공신경망 계층(layer)을 텐서 연산으로 보기

연산텐서 형태아인슈타인 표기법
선형 계층(linear layer)Y = X @ W.T + b"bd,od->bo" + 편향(bias)
어텐션 QKVQ = X @ W_q"btd,dh->bth"
어텐션 스코어Q @ K.T / sqrt(d)"bhtd,bhsd->bhts"
어텐션 출력softmax(scores) @ V"bhts,bhsd->bhtd"
배치 정규화(batch norm)(X - mu) / sigma * gamma원소별 연산 + 브로드캐스트
소프트맥스(softmax)exp(x) / sum(exp(x))원소별 연산 + 축약

산출물 만들기

이 lesson의 검수 대상은 아래 세 가지입니다.

  • code/tensors.py: 직접 구현한 텐서 클래스, 브로드캐스팅, 아인슈타인 표기법, 어텐션 형태 시연
  • outputs/prompt-tensor-shapes.md: 텐서 형태 불일치(shape mismatch)를 진단하고 자주 쓰이는 연산별 해결책을 찾는 프롬프트
  • outputs/prompt-tensor-debugger.md: 형태 오류 메시지와 텐서 형태를 넣어 단계별(step-by-step) 해결책을 받아 보는 프롬프트

연습문제

  1. 쉬움 -- 형태 변경 왕복(reshape round-trip). 형태 (2, 3, 4)인 텐서를 (6, 4), (24,), 다시 (2, 3, 4)로 형태 변경합니다. 각 단계에서 평탄한 데이터를 출력(print)해 원소 순서가 보존되는지 확인합니다.
  2. 중간 -- 브로드캐스팅 구현. Tensor 클래스에 목표 형태(target shape)에 맞춰 크기 1인 차원을 확장(expand)하는 broadcast_to(shape) 메서드(method)를 추가합니다. 그런 다음 _elementwise_op가 자동 브로드캐스팅을 수행하도록 수정합니다. 형태 (3, 1)(1, 4)(3, 4)를 만드는지 시험(test)합니다.
  3. 어려움 -- 아인슈타인 표기법 직접 구현. 내적(i,i->), 행렬곱(ij,jk->ik), 외적(i,j->ij), 전치(ij->ji)를 최소한 처리하는 einsum(subscripts, *tensors) 함수를 구현합니다. 첨자 문자열(subscript string)을 파싱(parse)하고 축약되는 인덱스를 찾아 모든 인덱스 조합을 순회(loop)합니다. 결과를 np.einsum과 비교합니다.
  4. 어려움 -- 어텐션 형태 추적기(attention shape tracker). batch_size, seq_len, embed_dim, num_heads를 받아 멀티헤드 어텐션의 각 단계 형태를 출력하는 함수를 작성합니다. 입력, Q/K/V 사영, 헤드 분리, 어텐션 스코어, 소프트맥스 가중치, 가중합, 헤드 결합, 출력 사영을 모두 확인합니다.

핵심 용어

용어흔한 설명실제 의미
텐서(Tensor)차원이 더 많은 행렬균일한 타입, 정해진 형태, 스트라이드, 연산을 가진 다차원 배열
계수(Rank)차원 수축의 개수. 행렬의 rank(계급)와는 다르며, 행렬 텐서는 계수 2다
형태(Shape)텐서의 크기각 축의 크기를 나열한 튜플. (2, 3)은 행 2개, 열 3개를 뜻한다
스트라이드(Stride)메모리 배치각 축에서 한 칸 이동하기 위해 건너뛰어야 하는 원소 수
브로드캐스팅(Broadcasting)형태가 달라도 연산이 됨오른쪽부터 정렬하고 차원이 같거나 하나가 1이어야 하는 엄격한 규칙
연속(Contiguous)텐서가 정상 배치임논리적 배치와 메모리 저장 순서가 일치해 빈틈이나 재배열이 없는 상태
아인슈타인 표기법(Einsum)행렬곱을 멋지게 쓰는 법텐서 축약, 외적, 대각합, 전치를 한 줄로 표현하는 일반 표기법
뷰(View)형태 변경(reshape)과 같아 보임같은 메모리 버퍼를 공유하면서 형태/스트라이드 메타데이터만 다른 텐서. 비연속 데이터에서는 실패한다
축약(Contraction)인덱스에 대해 합산텐서 사이 공유 인덱스를 곱하고 합산해 더 낮은 계수의 결과를 만드는 연산
NCHW / NHWCPyTorch와 TensorFlow의 형식 차이이미지 텐서 메모리 배치 관례. NCHW는 채널을 공간 차원 앞에, NHWC는 뒤에 둔다

더 읽을거리

  • NumPy Broadcasting — 표준 브로드캐스팅 규칙과 시각적 예시를 제공합니다.
  • PyTorch Tensor Views — 뷰(view)가 언제 동작하고 언제 복사되는지 확인할 수 있습니다.
  • einops — 텐서의 형태 변경을 읽기 쉽고 안전하게 만들어 주는 라이브러리입니다.
  • The Illustrated Transformer — 어텐션을 흐르는 텐서 형태를 시각적으로 이해할 수 있습니다.
  • Einstein Summation in NumPy — 아인슈타인 표기법의 전체 문서와 예시를 제공합니다.

실습 코드

이 강의의 실습 코드 1개

tensors
Code

산출물

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

prompt-tensor-debugger

Step-by-step debugging prompt for tensor shape errors in deep learning code

Prompt
prompt-tensor-shapes

Debug tensor shape mismatches and recommend fixes for common deep learning operations

Prompt

확인 문제

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

1.형태 `(8, 1, 6, 1)`과 `(7, 1, 5)`인 텐서를 브로드캐스팅(broadcasting)하면 결과 형태는 무엇인가요?

2.아인슈타인 표기법(einsum) 식 `bhtd,bhsd->bhts`에서 인덱스 `d`는 어떻게 되나요?

3.PyTorch에서 전치된 텐서(transposed tensor)에 `.view()`를 호출하면 실패하는 이유는 무엇인가요?

0/3 답변 완료