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)
- RGB 기준:
- 배치 단위로 묶기 (DataLoader 등)
- 여러 장을 쌓으면
(N, C, H, W)→ 4차원 텐서 - 예:
(32, 3, 224, 224)→ batch size=32
- 여러 장을 쌓으면
- 모델 입력
- 대부분의 딥러닝 프레임워크는 4차원 형태(NCHW 또는 NHWC) 를 기대
- 이유: GPU 연산을 벡터화/병렬화하기 위함
3. 파이토치 기본 패키지
- torch
- 메인 네임스페이스입니다. 텐서 등의 다양한 수학 함수가 포함되어져 있으며 Numpy와 유사한 구조를 가집니다.
- torch.autograd
- 자동 미분을 위한 함수들이 포함되어져 있습니다. 자동 미분의 on/off를 제어하는 콘텍스트 매니저(enable_grad/no_grad)나 자체 미분 가능 함수를 정의할 때 사용하는 기반 클래스인 ‘Function’ 등이 포함되어져 있습니다.
- torch.nn
- 신경망을 구축하기 위한 다양한 데이터 구조나 레이어 등이 정의되어져 있습니다. 예를 들어 RNN, LSTM과 같은 레이어, ReLU와 같은 활성화 함수, MSELoss와 같은 손실 함수들이 있습니다.
- torch.optim
- 확률적 경사 하강법(Stochastic Gradient Descent, SGD)를 중심으로 한 파라미터 최적화 알고리즘이 구현되어져 있습니다.
- torch.utils.data
- SGD의 반복 연산을 실행할 때 사용하는 미니 배치용 유틸리티 함수가 포함되어져 있습니다.
- 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()) # size1
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()) # shape2
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 12 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 곱하기 연산이 된 결과가 출력