1. 파이토치(PyTorch) 개요
- PyTorch는 기계 학습 프레임워크(framework) 중 하나다. tensor 데이터를 처리할 수 있다.
- PyTorch의 텐서(tensor)는 NumPy 배열과 매우 유사하다. tensor는 축이 3개 이상인 데이터이다.
- PyTorch를 사용하면, GPU 연동을 통해 효율적으로 딥러닝 모델을 학습할 수 있다.
- Google Colab을 이용하면, 손쉽게 PyTorch를 시작할 수 있다.
- Google Colab에서는 [런타임] - [런타임 유형 변경]에서 GPU를 선택할 수 있다.
근데 코랩으로는 잘 안하겠지... torch 패키지를 우선 깔아주자.
import torch
data = [
[1, 2],
[3, 4]
]
# 텐서란 고차원의 데이터이다. 딥러닝 모델에 입력으로 넣는 기본적인 데이터는 다 텐서 형태라고 이해할 수 있다.
x = torch.tensor(x) # torch에서 tensor 데이터를 선언하는 함수. x에 텐서가 들어갔다.
print(x.is_cuda) # 텐서가 GPU에 있지 않음을 보인다. 기본적으로 CPU에 올라간다.
x = x.cuda() # 텐서 GPU에 올리기
print(x.is_cuda)
x = x.cpu() # 텐서 다시 CPU에 올리기.
print(x.is_cuda)
# cf. 텐서간의 연산을 수행할 때, 기본적으로 두 텐서가 같은 장치에 있어야 한다.
텐서간의 연산이 가능하려면, 연산에 사용되는 텐서들을 모두 같은 장치에 올린 후 수행해야한다. 가능하면 GPU에 모두 올리는 것이 시간적으로 좋을 것이다..
# GPU 장치의 텐서
a = torch.tensor([
[1, 1],
[2, 2]
]).cuda()
# CPU 장치의 텐서
b = torch.tensor([
[5, 6],
[7, 8]
])
# print(torch.matmul(a, b)) # 오류 발생
print(torch.matmul(a.cpu(), b))
참고로 matmul는 torch의 행렬 곱 함수이다.
2. 텐서 소개 및 생성 방법
- PyTorch에서의 텐서(tensor)는 기능적으로 넘파이NumPy와 매우 유사하다. 벡터, 수열 등 수학적으로 정의된 데이터를 표현하는 데 적합하게 이용할 수 있고, 다양한 자료형 데이터를 표현할 수 있다.
- 기본적으로 다차원 배열을 처리하기에 적합한 자료구조로 이해할 수 있다.
- PyTorch의 텐서는 "자동 미분" 기능을 제공한다. 딥러닝 모델의 학습이 매우 편리해진다.
1) 텐서의 속성
모양(shape), 자료형(data type), 저장된 장치 등의 기본 속성이 있다.
# unif(0,1)에서 3x4 데이터 랜덤하게 샘플링
tensor = torch.rand(3,4)
print(tensor)
print(tensor.shape)
print(tensor.dtype)
print(tensor.device)

2) Nunpy 배열에서 텐서를 초기화할 수 있다.
a = torch.tensor([5])
b = torch.tensor([7])
c = (a + b).numpy()
print(c)
print(type(c))
# tensor의 합인 c가 numpy로 초기화 되었다.
# numpy에서 tensor로 초기화 하기 위해서는 .from_numpy() 함수를 이용한다.
tensor = torch.from_numpy(c)
print(tensor)
print(type(tensor))

3) 다른 tensor를 토대로, 다른 tensor를 선언할 수 있다.
x = torch.tensor([
[5, 7],
[1, 2]
])
# x와 같은 모양이지만, 원소 값이 1인 tensor 만들기
x_ones = torch.ones_like(x) # ones_like() 함수
print(x_ones)
# x와 같은 모양이지만, 자료형은 float, 값은 랜덤 값 넣기
x_rand = torch.rand_like(x, dtype=torch.float32) # rand() 함수는 unif(0,1)에서 랜덤 샘플 추출 한다.
print(x_rand)

3. 텐서의 형변환 및 차원 조작
- 텐서는 넘파이(NumPy) 배열처럼 조작할 수 있다.
우리는 Pytorch에서 텐서를 초기화해서 다양한 연산을 적용할 수 있다. Numpy 배열 처럼 조작할 수 있고, 실수형에서 정수 형으로 바꾸는 등의 형 변환이 가능할 뿐 아니라, 고차원 배열로도 사용할 수 있다. 다차원에서의 차원을 조작할 수 있다.
1) 텐서의 특정 차원에 접근하기
tensor = torch.tensor([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
# 특정 열에 접근하기 [행, 열] index는 0부터 시작한다.
print(tensor[0]) # 1행
print(tensor[:, 0]) # 모든 행, 1열
print(tensor[..., -1]) # 마지막 열에 대해 접근

2) 텐서 이어붙이기(Concatenate)
여러 텐서를 이어 붙여 새로운 텐서를 만들 수 있다.
tensor = torch.tensor([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
result1 = torch.cat([tensor, tensor, tensor], dim = 0)
print(result1)
result2 = torch.cat([tensor, tensor, tensor], dim = 1)
print(result2)

3) 텐서 형 변환(Type casting)
- 텐서의 자료형(정수, 실수 등)을 변환할 수 있다.
a = torch.tensor([2], dtype = torch.int)
b = torch.tensor([5,0])
# 텐서 끼리의 연산에서 자료형이 다른 경우, float 기준으로 변경된다.
print(a + b)
# 정수 계산을 하고 싶다면 type() 함수를 사용하여 자료형을 변환하자.
print(a + b.type(torch.int))

4) 텐서의 모양 변경
- view()는 텐서의 모양을 변경할 때 사용한다.
- 이때, 텐서(tensor)의 순서는 변경되지 않는다.
- view() 함수는 주소값을 공유한다는 특징이 있다.
a = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8])
# a tensor 안에는 8개의 원소가 있다. view() 함수를 이용하여 8개 원소의 모양을 바꿔주면 된다. ex) 4x2, 2x4..
b = a.view(4,2)
print(b)
# view 함수는 연산을 일종의 포인터 처럼 넘겨주기에 주소값을 공유하는 특징이 있다.
a[0] = 7
print(b)
# 주소값을 공유하기를 원하지 않는다면, a의 값을 복사(copy)하여 사용하면 된다. clone() 함수를 이용한다.
c = a.clone().view(4, 2)
a[4] = 99
print(c)

5) 텐서의 차원 교환
a = torch.rand((64, 32, 3))
print(a.shape)
b = a.permute(2, 1, 0) # 차원 자체를 교환
# (2번째 축, 1번째 축, 0번째 축)의 형태가 되도록 한다.
print(b.shape)

4. 텐서의 연산과 함수
1) 텐서의 연산
- 텐서는 사칙 연산 등 기본적인 연산을 수행할 수 있다.
- 기본적으로 요소별 연산을 한다.
- 행렬 곱 또한 지원한다. matmul() 함수를 이용한다.
x = torch.tensor([
[5, 7],
[1, 2]
])
y = torch.tensor([
[5, 7],
[1, 2]
])
print(torch.matmul(x, y))
print(x.matmul(y))

2) 텐서의 평균 함수
- 텐서의 평균(mean)을 계산할 수 있다. mean()
a = torch.Tensor([
[1, 2, 3, 4],
[5, 6, 7, 8]
])
print(a)
print(a.mean()) # 전체 원소에 대한 평균
print(a.mean(dim=0)) # 각 열에 대하여 평균 계산
print(a.mean(dim=1)) # 각 행에 대하여 평균 계산

3) 텐서의 합계 함수
- 텐서의 합계(sum)를 계산할 수 있다.
a = torch.Tensor([
[1, 2, 3, 4],
[5, 6, 7, 8]
])
print(a)
print(a.sum()) # 전체 원소에 대한 평균
print(a.sum(dim=0)) # 각 열에 대하여 합계 계산
print(a.sum(dim=1)) # 각 행에 대하여 합계 계산

# 평균, 합계, 최대 함수는 cat 함수와 달리 dim 0이 열, 1이 행 기준이다.
4) 텐서의 최대 함수
- max() 함수는 원소의 최댓값을 반환한다.
- argmax() 함수는 가장 큰 원소(최댓값)의 인덱스를 반환한다.
a = torch.Tensor([
[1, 2, 3, 4],
[5, 6, 7, 8]
])
print(a)
print(a.max()) # 전체 원소에 대한 최대값
print(a.max(dim=0)) # 각 열에 대한 최댓값
print(a.max(dim=1)) # 각 행에 대한 최대값
print()
print(a.argmax()) # 전체 원소에 대한 최대값 index
print(a.argmax(dim=0)) # 각 열에 대한 최댓값 index
print(a.argmax(dim=1)) # 각 행에 대한 최대값 index

특정 행, 열 중에 최대값을 뽑아주는게 아니라, 가장 큰 행 전체 or 열 전체를 뽑아내는 것이다.
5) 텐서의 차원 줄이기 혹은 늘리기
- unsqueeze() 함수는 크기가 1인 차원을 추가한다.
- 배치(batch) 차원을 추가하기 위한 목적으로 흔히 사용된다.
- squeeze() 함수는 크기가 1인 차원을 제거한다.
스퀴즈... 쥐어 짠다는 뜻을 가진다. 뭔가 꾹꾹 눌러 차원을 제거하거나 풀어주는 느낌으로 생각하자.
a = torch.Tensor([
[1, 2, 3, 4],
[5, 6, 7, 8]
])
print(a.shape)
# 첫 번째 축에 차원 추가
a = a.unsqueeze(0)
print(a)
print(a.shape)
# 네 번째 축에 차원 추가
a = a.unsqueeze(3)
print(a)
print(a.shape)

5. 자동 미분과 기울기(Gradient)
- PyTorch에서는 연산에 대하여 자동 미분을 수행할 수 있다.
- Gradient를 추적하기 위해, require_grad를 True로 만든다.
import torch
# requires_grad를 설정할 때만 기울기 추적
x = torch.tensor([3.0, 4.0], requires_grad=True)
y = torch.tensor([1.0, 2.0], requires_grad=True)
z = x + y
print(z) # [4.0, 6.0]
print(z.grad_fn) # 더하기(add)
print()
out = z.mean()
print(out) # 5.0
print(out.grad_fn) # 평균(mean)
print()
out.backward() # scalar에 대하여 가능
print(x.grad)
print(y.grad)
print(z.grad) # leaf variable에 대해서만 gradient 추적이 가능하다.

backward() 함수를 통해 scalar 값에 대해 각 gradient를 구할 수 있다.
- 일반적으로 모델을 학습할 때는 기울기(gradient)를 추적한다.
- 하지만, 학습된 모델을 사용할 때는 파라미터를 업데이트하지 않으므로, 기울기를 추적하지 않는 것이 일반적이다. torch.no_grad(): 함수를 사용하면 된다.
temp = torch.tensor([3.0, 4.0], requires_grad=True)
print(temp.requires_grad)
print((temp ** 2).requires_grad)
print()
# 기울기 추적을 하지 않기 때문에 계산 속도가 더 빠르다.
with torch.no_grad():
temp = torch.tensor([3.0, 4.0], requires_grad=True)
print(temp.requires_grad)
print((temp ** 2).requires_grad) # 제곱하는 연산 자체는 추적하지 않는다
