1.1 텐서 조작하기

유명곤·2026년 9월 9일

텐서 연산을 이해하려면 어떤 값이 계산되는지와 함께, 연산 전후에 데이터의 모양이 어떻게 달라지는지를 읽어야 한다. 축의 의미와 크기를 구분하면 필요한 연산을 선택하고 결과가 의도한 구조인지 확인할 수 있다.

1. 텐서는 값과 축으로 구성된다

PyTorch에서 텐서(Tensor)는 같은 자료형의 원소를 다차원 배열로 표현하는 자료구조이다. 스칼라·벡터·행렬은 각각 0차원·1차원·2차원 텐서로 표현할 수 있으며, 텐서라는 말이 3차원 이상만을 뜻하지는 않는다.

축(Axis)은 원소의 위치를 구분하는 각 방향이고, 모양(Shape)은 축별 크기를 순서대로 나타낸 것이다. t.ndim 또는 t.dim()은 축의 개수, t.shape 또는 t.size()는 전체 모양, t.size(dim)은 지정한 축의 크기를 알려준다. 축 번호는 0부터 시작하며 dim=-1은 마지막 축을 가리킨다.

shape가 (B, F)인 텐서는 축이 두 개이고 원소는 B×FB\times F개이다. 이를 샘플 BB개의 특성 FF개를 저장한 구조로 정했다면, 0번 축은 샘플을, 1번 축은 특성을 구분한다. 함께 처리하는 샘플 묶음을 배치(Batch), 그 샘플 수를 배치 크기(Batch Size)라고 한다. 축의 크기는 shape로 알 수 있지만, 축의 의미는 데이터를 구성한 방식으로 정해진다.

(F,), (1, F), (F, 1)은 원소 수가 같아도 서로 다른 모양이다. (F,)는 축이 하나인 벡터이고, 나머지는 각각 행과 열이 있는 2차원 텐서이다. 수학에서 성분이 FF개인 벡터를 FF차원 벡터라고 부르는 것과, PyTorch에서 축의 개수를 차원 수라고 부르는 것도 구분해야 한다.

모양 외에도 자료형(Data Type)인 dtype와 데이터를 저장하고 연산하는 장치(Device)인 device를 확인한다. shape가 같다고 원소의 표현 방식이나 연산 장치까지 같은 것은 아니다.

2. 값을 계산할 때는 연산 규칙과 축을 함께 본다

브로드캐스팅과 곱셈의 차이

원소별 연산(Element-wise Operation)은 대응되는 위치의 원소끼리 계산하는 연산이다. 덧셈이나 *, mul을 이용한 곱셈이 여기에 해당한다.

브로드캐스팅(Broadcasting)은 서로 다른 모양의 텐서를 정해진 규칙으로 맞추어 원소별 연산을 수행하는 방식이다. 마지막 축부터 두 입력을 나란히 비교한다. 대응하는 크기가 같거나 한쪽이 1이면 호환되고, 한쪽에 없는 앞쪽 축은 크기 1로 간주한다. 크기가 1인 축의 값은 상대 축에 반복되는 것처럼 계산되며, 이를 위해 입력 데이터를 실제로 복제해 둘 필요는 없다.

반면 행렬 곱(Matrix Multiplication)은 왼쪽 행과 오른쪽 열의 대응 성분을 곱한 뒤 더한다. 두 입력이 2차원일 때 @ 또는 matmul은 다음 조건을 따른다.

A∈Rm×k,B∈Rk×n⟹AB∈Rm×n\mathbf{A}\in\mathbb{R}^{m\times k},\quad \mathbf{B}\in\mathbb{R}^{k\times n} \quad\Longrightarrow\quad \mathbf{A}\mathbf{B}\in\mathbb{R}^{m\times n}

안쪽 크기 kk가 같아야 하며 결과에는 왼쪽의 행 수 mm과 오른쪽의 열 수 nn이 남는다. 원소별 곱과 달리, 행렬을 곱하는 두 축에는 이 조건이 적용된다. 두 입력이 모두 2차원 이상인 matmul에서는 마지막 두 축을 행렬로 다루고, 그 앞의 배치 축에 브로드캐스팅을 적용한다.

축소 연산에서 dim이 뜻하는 것

축소 연산(Reduction)은 지정한 축을 따라 여러 값을 합계·평균·최댓값 등으로 모으는 연산이다. sum, mean, max의 dim은 결과를 남길 축이 아니라 값을 모을 축을 지정한다.

샘플과 특성을 각각 행과 열에 놓은 (B, F) 텐서라면 dim=0은 샘플을 모아 특성별 결과를 만든다. dim=1은 특성을 모아 샘플별 결과를 만든다.

지정계산의 의미기본 결과 shapekeepdim=True
dim=0특성마다 샘플들의 값을 모음(F,)(1, F)
dim=1샘플마다 특성들의 값을 모음(B,)(B, 1)

기본적으로 계산한 축은 제거된다. keepdim=True는 그 축의 크기를 1로 남긴다. 이 결과를 원래 텐서와 다시 계산할 때, 어느 축에 값을 반복 적용해야 하는지를 보존할 수 있다.

max(dim=...)는 최댓값과 해당 축 안에서의 인덱스를 함께 반환한다. 최댓값의 위치만 필요하면 argmax(dim=...)를 사용한다. 위치를 나타내는 인덱스는 0부터 시작하며, 같은 최댓값이 여러 개면 첫 번째 위치가 선택된다. max()처럼 축을 지정하지 않으면 전체 최댓값만 반환한다.

3. 모양을 바꾸는 연산은 축을 어떻게 다루는가

view는 원소를 다른 모양으로 해석한다

뷰(View)는 원본과 데이터를 공유하는 텐서이다. 모양을 지정하는 view는 데이터를 복사하지 않고 같은 원소를 새로운 shape로 읽게 한다. 따라서 변경 전후의 전체 원소 수가 같아야 한다. 크기 하나를 -1로 지정하면 나머지 크기와 전체 원소 수로부터 추론하며, -1은 한 번만 사용할 수 있다.

다만 원소 수가 같은 것만으로 충분하지는 않다. 스트라이드(Stride)는 각 축에서 인덱스가 한 칸 이동할 때 저장 공간에서 건너뛰는 원소 수이다. view가 가능하려면 이 메모리 배치가 새 모양과 호환되어야 한다. reshape는 원소 수에 맞는 모양을 요청하면 가능한 경우 뷰를 반환하고, 필요하면 데이터를 복사한다.

모양을 바꾸는 것과 축을 교환하는 것도 구분해야 한다. (B, F)를 (F, B)로 view했다고 해서 샘플 축과 특성 축이 서로 바뀌지는 않는다. 두 축을 교환하려면 transpose, 여러 축의 순서를 바꾸려면 permute를 사용한다.

크기 1인 축을 추가하거나 제거한다

unsqueeze(dim)는 지정한 위치에 크기 1인 축을 추가한다. squeeze(dim)는 지정한 축의 크기가 1일 때 그 축을 제거한다. 두 연산 모두 원소 수를 유지하며, 값의 모양을 연산이나 모델의 입력 조건에 맞출 때 사용한다.

squeeze()처럼 축을 생략하면 크기 1인 모든 축이 사라진다. 배치 크기가 1인 경우 배치 축까지 제거될 수 있으므로, 유지해야 할 축이 있다면 제거할 축을 지정한다.

cat은 기존 축을 늘리고 stack은 새 축을 만든다

연결(Concatenation)은 기존 축을 따라 텐서를 이어 붙이는 방식이며 cat으로 수행한다. 연결할 축 이외의 크기는 같아야 하고, 결과의 축 개수는 유지된다. 여기서는 비어 있지 않은 일반적인 텐서를 기준으로 설명한다.

쌓기(Stacking)는 같은 shape의 텐서들을 새로운 축으로 구분해 묶는 방식이며 stack으로 수행한다. 모든 입력의 shape가 같아야 하고, 결과에는 축이 하나 늘어난다. 기존 샘플 묶음을 더 길게 만들지, 묶음 자체를 구분할 축을 만들지에 따라 선택한다.

4. 자료형 변환과 원본 변경을 구분한다

타입 캐스팅(Type Casting)은 원소의 자료형을 바꾸는 연산이다. x.float()는 float32, x.long()은 int64인 결과를 반환한다. 이후 연산에는 이 반환값을 사용해야 한다. 정수 텐서의 평균이 필요할 때 실수형으로 변환하는 경우처럼, 연산이 요구하는 자료형을 맞추는 데 사용한다.

ones_like(x)와 zeros_like(x)는 각각 1과 0으로 채운 새 텐서를 만든다. 별도로 지정하지 않으면 입력의 shape·dtype·device를 따르므로, 기존 데이터와 조건을 맞춘 텐서를 만들 수 있다.

인플레이스 연산(In-place Operation)은 새 계산 결과만 만드는 대신 기존 데이터를 직접 수정하는 연산이다. PyTorch 텐서 메서드에서는 이름 끝의 밑줄 _로 구분하는 경우가 많다. x.mul(2)는 원본을 유지하며 결과를 반환하고, x.mul_(2)는 원본 값을 바꾼다.

x = x * 2는 새 결과를 이름 x에 연결한다. x.mul_(2)와 이후 x의 값이 같아도, 원래 데이터를 공유하던 다른 텐서에 미치는 영향은 다르다. 특히 뷰로 연결된 텐서는 같은 데이터를 보므로, 공유된 데이터를 직접 바꾸면 다른 뷰에서 읽는 값도 달라진다.

5. 하나의 텐서로 연산 결과 확인하기

두 샘플이 각각 세 특성을 가진다고 하자. 계산에 사용할 실수형 텐서 x는 다음 행렬 X\mathbf{X}를 저장하며, shape는 (2, 3)이다.

X=[285714]\mathbf{X}=\begin{bmatrix}2&8&5\\7&1&4\end{bmatrix}

0번 축은 샘플, 1번 축은 특성이므로 배치 크기는 2이다. 아래 연산은 별도로 밝히지 않는 한 이 원래 x에 각각 적용한다.

어느 축을 모으느냐에 따라 요약이 달라진다

연산결과 값결과 shape
x.sum()27()
x.sum(dim=0)[9, 9, 9](3,)
x.mean(dim=0)[4.5, 4.5, 4.5](3,)
x.mean(dim=1)[5, 4](2,)
x.mean(dim=1, keepdim=True)[[5], [4]](2, 1)
x.max(dim=1).values[8, 7](2,)
x.argmax(dim=1)[1, 0](2,)

dim=0의 평균은 같은 특성에 속한 두 샘플의 값을 모은다. 첫 특성이라면 (2+7)/2=4.5(2+7)/2=4.5이다. dim=1의 평균은 같은 샘플의 세 특성을 모으므로 첫 샘플은 (2+8+5)/3=5(2+8+5)/3=5가 된다.

첫 샘플의 최댓값은 8이며 세 특성 중 인덱스 1에 있다. 두 번째 샘플의 최댓값은 7이고 인덱스는 0이다. 최댓값과 그 위치는 같은 계산에서 얻더라도 서로 다른 정보이다.

평균을 빼는 계산에서 keepdim의 역할

각 샘플에서 그 샘플의 평균을 빼려면, 위에서 구한 평균을 (2, 1)로 유지한다. row_mean이 x.mean(dim=1, keepdim=True)의 결과라면 x - row_mean은 다음과 같다.

[285714]−[54]=[−3303−30]\begin{bmatrix}2&8&5\\7&1&4\end{bmatrix} -\begin{bmatrix}5\\4\end{bmatrix} =\begin{bmatrix}-3&3&0\\3&-3&0\end{bmatrix}

뒤쪽 축의 크기 3과 1이 호환되므로, 각 행의 평균 하나가 그 행의 세 특성에 적용된다. 결과 shape는 (2, 3)이다. 반면 평균을 (2,)로 두면 마지막 축의 3과 2가 맞지 않아 이 뺄셈은 실패한다.

같은 x의 각 특성에 [1, 0, -1]을 곱하면 원소별 결과는 [[2, 0, -5], [7, 0, -4]]이고 shape는 (2, 3)이다. 이 가중치를 (3, 1)인 행렬 w\mathbf{w}로 만들어 행렬 곱을 하면, 곱한 값들을 샘플별로 더한 결과가 나온다.

Xw=[285714][10−1]=[−33]\mathbf{X}\mathbf{w} =\begin{bmatrix}2&8&5\\7&1&4\end{bmatrix} \begin{bmatrix}1\\0\\-1\end{bmatrix} =\begin{bmatrix}-3\\3\end{bmatrix}

이때 입력 shape는 (2, 3)과 (3, 1), 결과는 (2, 1)이다. 원소별 곱은 각 특성의 계산값을 남기고, 행렬 곱은 이 값들을 합쳐 샘플별 결과 하나를 만든다.

모양 변경과 연결 결과 비교하기

x가 메모리에 연속적으로 배치된 텐서라고 하자. 원소가 6개이므로 x.view(3, 2)의 결과는 [[2, 8], [5, 7], [1, 4]]이다. 반면 x.transpose(0, 1)은 [[2, 7], [8, 1], [5, 4]]이다. 둘 다 shape는 (3, 2)이지만, 전자는 원소를 다른 크기로 묶고 후자는 행과 열의 역할을 교환한다.

연산결과 shape달라지는 구조
x.view(-1)(6,)여섯 원소를 한 축으로 읽음
x.unsqueeze(0)(1, 2, 3)앞에 크기 1인 축을 추가
x.unsqueeze(0).squeeze(0)(2, 3)추가한 축을 제거
torch.cat([x, x], dim=0)(4, 3)샘플 축을 따라 연결
torch.cat([x, x], dim=1)(2, 6)특성 축을 따라 연결
torch.stack([x, x], dim=0)(2, 2, 3)두 묶음을 구분하는 축을 추가

cat의 첫 결과는 샘플 네 개를 한 묶음에 담고, stack의 결과는 샘플 두 개짜리 묶음을 두 개 담는다. 전체 원소 수는 같지만 결과를 읽는 축의 구조가 다르다.

요약과 활용

  • 텐서는 값·축의 개수·축별 크기·축의 의미를 함께 읽는다. dtype와 device도 별도로 확인한다.
  • 계산에서는 어느 원소를 대응시키고 어느 축을 모으는지를 구분한다. 특히 keepdim은 축소한 결과를 원래 텐서와 다시 연산할 때 유용하다.
  • 모양을 바꿀 때는 원소를 다시 묶는지, 축을 교환하는지, 기존 축을 늘리거나 새 축을 만드는지를 먼저 정한다. 원본 데이터의 공유·변경 여부도 살핀다.

연산이 성공했다는 사실만으로 의도한 계산이 되었다고 판단할 수는 없다. 입력과 결과의 shape를 적고 각 축이 무엇을 뜻하는지 확인하면, 오류가 난 이유뿐 아니라 실행은 되지만 잘못 대응된 계산도 찾을 수 있다.

이어서 확인할 내용

  • 3·4차원 텐서의 행렬 곱과 브로드캐스팅: 마지막 두 행렬 축과 앞쪽 배치 축의 규칙을 구분해 결과 shape를 예측한다.
  • CPU·GPU와 dtype의 관계: 텐서와 모델의 장치를 맞추는 방법, 자료형 선택이 정밀도·메모리·연산에 미치는 영향을 확인한다.

참고자료

profile
Werde, der du bist!

0개의 댓글