1. 수학적 기본 개념

  • 스칼라(Scalar): 단일 값 (예: 3)

  • 벡터(Vector): 1차원 배열, 크기와 방향을 가짐 (예: [1, 2, 3])

  • 행렬(Matrix): 2차원 배열, 행과 열로 구성 (예: [[1, 2], [3, 4]])

  • 텐서(Tensor): 스칼라·벡터·행렬을 일반화한 n차원 배열.

    • 0차원: 스칼라
    • 1차원: 벡터
    • 2차원: 행렬
    • 3차원 이상: 다차원 배열 (딥러닝 데이터 표현에 활용)

2. 딥러닝에서의 텐서

  • 단일 이미지 불러오기
    • RGB 기준: (C, H, W) → 3차원 텐서
    • 예: (3, 224, 224)
  • 배치 단위로 묶기 (DataLoader 등)
    • 여러 장을 쌓으면 (N, C, H, W) → 4차원 텐서
    • 예: (32, 3, 224, 224) → batch size=32
  • 모델 입력
    • 대부분의 딥러닝 프레임워크는 4차원 형태(NCHW 또는 NHWC) 를 기대
    • 이유: GPU 연산을 벡터화/병렬화하기 위함

3. 파이토치 기본 패키지

  1. torch
    • 메인 네임스페이스입니다. 텐서 등의 다양한 수학 함수가 포함되어져 있으며 Numpy와 유사한 구조를 가집니다.
  2. torch.autograd
    • 자동 미분을 위한 함수들이 포함되어져 있습니다. 자동 미분의 on/off를 제어하는 콘텍스트 매니저(enable_grad/no_grad)나 자체 미분 가능 함수를 정의할 때 사용하는 기반 클래스인 ‘Function’ 등이 포함되어져 있습니다.
  3. torch.nn
    • 신경망을 구축하기 위한 다양한 데이터 구조나 레이어 등이 정의되어져 있습니다. 예를 들어 RNN, LSTM과 같은 레이어, ReLU와 같은 활성화 함수, MSELoss와 같은 손실 함수들이 있습니다.
  4. torch.optim
    • 확률적 경사 하강법(Stochastic Gradient Descent, SGD)를 중심으로 한 파라미터 최적화 알고리즘이 구현되어져 있습니다.
  5. torch.utils.data
    • SGD의 반복 연산을 실행할 때 사용하는 미니 배치용 유틸리티 함수가 포함되어져 있습니다.
  6. torch.onnx
    • ONNX(Open Neural Network Exchange)의 포맷으로 모델을 익스포트(export)할 때 사용합니다. ONNX는 서로 다른 딥 러닝 프레임워크 간에 모델을 공유할 때 사용하는 포맷입니다.

4. 파이토치 기본 텐서 연산

4.1 1D with PyTorch

1차원 텐서/벡터 만들기
t = torch.FloatTensor([0., 1., 2., 3., 4., 5., 6.])
print(t)
tensor([0., 1., 2., 3., 4., 5., 6.])

dim()을 사용하면 현재 텐서의 차원 확인 shape나 size()를 사용하면 크기를 확인

print(t.dim())  # rank. 차원
print(t.shape)  # shape
print(t.size()) # size
1
torch.Size([7])
torch.Size([7])

1차원 텐서, 7개의 원소

슬라이싱
  • start: 시작 인덱스 (포함)
  • end: 끝 인덱스 (미포함)
  • step: 증가(양수)/감소(음수) 간격
print(t[0], t[1], t[-1])  # 인덱스로 접근
print(t[2:5], t[4:-1])    # 슬라이싱
print(t[:2], t[3:])       # 슬라이싱
tensor(0.) tensor(1.) tensor(6.)
tensor([2., 3., 4.]) tensor([4., 5.])
tensor([0., 1.]) tensor([3., 4., 5., 6.])

4.2 2D with PyTorch

2차원 텐서/행렬 만들기
t = torch.FloatTensor([[1., 2., 3.],
                       [4., 5., 6.],
                       [7., 8., 9.],
                       [10., 11., 12.]
                      ])
print(t)
tensor([[ 1.,  2.,  3.],
        [ 4.,  5.,  6.],
        [ 7.,  8.,  9.],
        [10., 11., 12.]])

dim()을 사용하면 현재 텐서의 차원 확인 size()를 사용하면 크기를 확인

print(t.dim())  # rank. 즉, 차원
print(t.size()) # shape
2
torch.Size([4, 3])

2차원, (4, 3)의 크기

슬라이싱
  • 1
print(t[:, 1]) # 첫번째 차원을 전체 선택한 상황에서 두번째 차원의 첫번째 것만 가져온다.
print(t[:, 1].size()) # ↑ 위의 경우의 크기
tensor([ 2.,  5.,  8., 11.])
torch.Size([4])
  • 2
print(t[:, :-1]) # 첫번째 차원을 전체 선택한 상황에서 두번째 차원에서는 맨 마지막에서 첫번째를 제외하고 다 가져온다.
tensor([[ 1.,  2.],
        [ 4.,  5.],
        [ 7.,  8.],
        [10., 11.]])

4.3 브로드캐스팅과 연산

  • 같은 크기 벡터 간 덧셈을 하는 경우 (브로드캐스팅 X)
m1 = torch.FloatTensor([[3, 3]])
m2 = torch.FloatTensor([[2, 2]])
print(m1 + m2)
tensor([[5., 5.]])
  • 벡터와 스칼라 간 덧셈을 하는 경우 (브로드캐스팅 O)
# Vector + scalar
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([3]) # [3] -> [3, 3]
print(m1 + m2)
tensor([[4., 5.]])
  • 서로 다른 벡터 간 덧셈을 하는 경우 (브로드캐스팅 O)
# 2 x 1 Vector + 1 x 2 Vector
m1 = torch.FloatTensor([[1, 2]])
m2 = torch.FloatTensor([[3], [4]])
print(m1 + m2)
tensor([[4., 5.],
       [5., 6.]])
  • 브로드캐스팅
# 브로드캐스팅 과정
[1, 2]
==> [[1, 2],
     [1, 2]]
[3]
[4]
==> [[3, 3],
     [4, 4]]

브로드캐스팅은 편리하지만, 자동으로 실행되는 기능이므로 사용자 입장에서 굉장히 주의해서 사용해야 합니다.

크기가 다른 A 텐서와 B 텐서가 있을 때, 사용자는 이 두 텐서의 크기가 같다고 착각하고 덧셈 연산을 수행한 경우 브로드캐스팅이 수행되어 덧셈 연산이 정상적으로 수행 만약, 두 텐서의 크기가 다르다고 에러를 발생시킨다면 사용자는 이 연산이 잘못되었음을 바로 알 수 있지만 브로드캐스팅은 자동으로 수행되므로 사용자는 나중에 원하는 결과가 나오지 않았더라도 어디서 문제가 발생했는지 찾기가 굉장히 어려울 가능성 존재

4.4 행렬 곱셈 vs 원소 곱셈

행렬로 곱셈을 하는 방법

  • 행렬 곱셈(.matmul)
  • 원소 별 곱셈(.mul)

파이토치 텐서의 행렬 곱셈 matmul()

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])
print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1.matmul(m2)) # 2 x 1

2 x 2 행렬과 2 x 1 행렬(벡터)의 행렬 곱셈 matmul() 의 결과

Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[ 5.],
        [11.]])

element-wise 곱셈

  • 동일한 크기의 행렬이 동일한 위치에 있는 원소끼리 곱하는 것

서로 다른 크기의 행렬이 브로드캐스팅이 된 후에 element-wise 곱셈

m1 = torch.FloatTensor([[1, 2], [3, 4]])
m2 = torch.FloatTensor([[1], [2]])
print('Shape of Matrix 1: ', m1.shape) # 2 x 2
print('Shape of Matrix 2: ', m2.shape) # 2 x 1
print(m1 * m2) # 2 x 2
print(m1.mul(m2))

2 x 2 행렬과 2 x 1 행렬(벡터)의 행렬 곱셈 * , mul() 의 결과

Shape of Matrix 1:  torch.Size([2, 2])
Shape of Matrix 2:  torch.Size([2, 1])
tensor([[1., 2.],
        [6., 8.]])
tensor([[1., 2.],
        [6., 8.]])

element-wise 곱셈을 수행하면, 두 행렬의 크기는 브로드캐스팅이 된 후에 곱셈이 수행

# 브로드캐스팅 과정에서 m2 텐서
[1]
[2]
==> [[1, 1],
     [2, 2]]

5.텐서 크기와 차원 변경

5.1 View (reshape)

뷰(View)는 텐서의 크기(Shape)를 변경해주는 역할

3차원 텐서

t = np.array([[[0, 1, 2],
               [3, 4, 5]],
              [[6, 7, 8],
               [9, 10, 11]]])
ft = torch.FloatTensor(t)
print(ft.shape)
torch.Size([2, 2, 3])

텐서의 크기는 (2, 2, 3)

텐서의 차원 변경

ft 텐서를 view를 사용하여 크기(shape)를 2차원 텐서로 변경

print(ft.view([-1, 3])) # ft라는 텐서를 (?, 3)의 크기로 변경
print(ft.view([-1, 3]).shape)
tensor([[ 0.,  1.,  2.],
        [ 3.,  4.,  5.],
        [ 6.,  7.,  8.],
        [ 9., 10., 11.]])
torch.Size([4, 3])

-1은 자동계산되는 차원의 크기 [-1, 3]은 3열에 맞춰 자동 계산 [3, -1]은 3행에 맞춰 자동 계산

3차원 텐서를 2차원 텐서로 변경 (?, 3)의 크기로 변경하라는 의미 결과적으로 (4, 3)의 크기를 가지는 텐서로 변경

내부적으로 크기 변환은 다음과 같이 이루어졌습니다. (2, 2, 3) (2 × 2, 3) (4, 3)

  • view는 기본적으로 변경 전과 변경 후의 텐서 안의 원소의 개수가 유지
  • 파이토치의 view는 사이즈가 -1로 설정되면 다른 차원으로부터 해당 값을 유추

변경 전 텐서의 원소의 수 (2 × 2 × 3) = 12개 변경 후 텐서의 원소의 개수 또한 (4 × 3) = 12개

텐서의 크기 변경

3차원 텐서에서 3차원 텐서로 차원은 유지하되, 크기(shape)를 바꾸는 작업

(2 × 2 × 3) = (? × 1 × 3) = 12를 만족해야 하므로 ?는 4

print(ft.view([-1, 1, 3]))
print(ft.view([-1, 1, 3]).shape)
tensor([[[ 0.,  1.,  2.]],
 
        [[ 3.,  4.,  5.]],
 
        [[ 6.,  7.,  8.]],
 
        [[ 9., 10., 11.]]])
torch.Size([4, 1, 3])
5.2 Squeeze (차원 제거)

스퀴즈는 차원이 1인 경우에는 해당 차원을 제거

ft = torch.FloatTensor([[0], [1], [2]])
print(ft)
print(ft.shape)
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

(3 × 1)의 크기의 텐서 (3,)의 크기의 텐서 squeeze

print(ft.squeeze())
print(ft.squeeze().shape)
tensor([0., 1., 2.])
torch.Size([3])
5.3 Unsqueeze (차원 추가)

언스퀴즈는 스퀴즈와 정반대 특정 위치에 1인 차원을 추가

ft = torch.Tensor([0, 1, 2])
print(ft.shape)
torch.Size([3])

차원이 1개인 1차원 벡터

첫번째 차원에 1인 차원을 추가

print(ft.unsqueeze(0)) # 인덱스가 0부터 시작하므로 0은 첫번째 차원을 의미한다.
print(ft.unsqueeze(0).shape)
tensor([[0., 1., 2.]])
torch.Size([1, 3])

(3,)의 크기를 가졌던 1차원 벡터 (1, 3)의 2차원 텐서로 변경

view로 구현

print(ft.view(1, -1))
print(ft.view(1, -1).shape)
tensor([[0., 1., 2.]])
torch.Size([1, 3])

두번째 차원에 1인 차원을 추가

print(ft.unsqueeze(1))
print(ft.unsqueeze(1).shape)
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

unsqueeze(-1) : -1은 인덱스 상으로 마지막 차원을 의미 현재 크기 (3,) > unsqueeze(-1) > (3, 1)의 크기

print(ft.unsqueeze(-1))
print(ft.unsqueeze(-1).shape)
tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])
  • view(), squeeze(), unsqueeze()는 텐서의 원소 수를 그대로 유지하면서 모양과 차원을 조절

6. Type Casting

long 타입의 텐서

lt = torch.LongTensor([1, 2, 3, 4])
print(lt)
tensor([1, 2, 3, 4])

.float() : float형으로 타입캐스팅

print(lt.float())
tensor([1., 2., 3., 4.])

7. 텐서 간 연결하기

7.1 Concatenate
x = torch.FloatTensor([[1, 2], [3, 4]])
y = torch.FloatTensor([[5, 6], [7, 8]])

torch.cat([ ])를 통해 연결

  • dim=0 : 첫번째 차원
  • dim=1 : 두번째 차원
print(torch.cat([x, y], dim=0))
tensor([[1., 2.],
        [3., 4.],
        [5., 6.],
        [7., 8.]])

dim=0 : 두 개의 (2 × 2) 텐서 > (4 × 2) 텐서

print(torch.cat([x, y], dim=1))
tensor([[1., 2., 5., 6.],
        [3., 4., 7., 8.]])

dim=1 : 두 개의 (2 × 2) 텐서 > (2 × 4) 텐서

두 텐서를 연결해서 입력으로 사용하는 것은 두 가지의 정보를 모두 사용한다는 의미

7.2 Stack
x = torch.FloatTensor([1, 4])
y = torch.FloatTensor([2, 5])
z = torch.FloatTensor([3, 6])

torch.stack

print(torch.stack([x, y, z]))
tensor([[1., 4.],
        [2., 5.],
        [3., 6.]])

(3 x 2) 텐서

위 작업은 아래의 코드와 동일한 작업

print(torch.cat([x.unsqueeze(0), y.unsqueeze(0), z.unsqueeze(0)], dim=0))

x, y, z는 기존에는 전부 (2,)의 크기 .unsqueeze(0) > 3개의 벡터 전부 (1, 2)의 크기의 2차원 텐서로 변경 cat(concatenate) 사용 > (3 x 2) 텐서

tensor([[1., 4.],
        [2., 5.],
        [3., 6.]])

dim=1

print(torch.stack([x, y, z], dim=1))
tensor([[1., 2., 3.],
        [4., 5., 6.]])

(2 x 3) 텐서

8. 특수 텐서 생성

x = torch.FloatTensor([[0, 1, 2], [2, 1, 0]])
print(x)
tensor([[0., 1., 2.],
        [2., 1., 0.]])
예제

8.1 ones_like() - 1로 채워진 텐서

ones_like(x) > 동일한 크기(shape) 1으로만 값이 채워진 텐서

print(torch.ones_like(x)) # 입력 텐서와 크기를 동일하게 하면서 값을 1로 채우기
tensor([[1., 1., 1.],
        [1., 1., 1.]])

8.2 zeros_like() - 0으로 채워진 텐서

zeros_like(x) > 동일한 크기(shape) 0으로만 값이 채워진 텐서

print(torch.zeros_like(x)) # 입력 텐서와 크기를 동일하게 하면서 값을 0으로 채우기
tensor([[0., 0., 0.],
        [0., 0., 0.]])

9. In-place Operation (덮어쓰기 연산)

x = torch.FloatTensor([[1, 2], [3, 4]])

곱하기 연산을 한 값 / 기존의 값을 출력

print(x.mul(2.)) # 곱하기 2를 수행한 결과를 출력
print(x) # 기존의 값 출력
tensor([[2., 4.],
        [6., 8.]])
tensor([[1., 2.],
        [3., 4.]])

첫번째 출력은 곱하기 2가 수행된 결과 두번째 출력은 기존의 값이 그대로 출력 곱하기 2를 수행했지만 이를 x에다가 다시 저장하지 않았으니, 곱하기 연산을 하더라도 기존의 값 x는 변하지 않는 것

연산 뒤에 _를 붙이면 기존의 값을 덮어쓰기

print(x.mul_(2.))  # 곱하기 2를 수행한 결과를 변수 x에 값을 저장하면서 결과를 출력
print(x) # 기존의 값 출력
tensor([[2., 4.],
        [6., 8.]])
tensor([[2., 4.],
        [6., 8.]])

x의 값이 덮어쓰기 되어 2 곱하기 연산이 된 결과가 출력