Machine Learning and Deep Learning

Pytorch - Introduction to PyTorch Tensors

Kkamang 2022. 3. 28. 11:12

출처: Pytorch Official Youtube Channel

Notebook can be downloaded here

 

import torch
import math

Random Tensors and Seeding

torch.manual_seed(1729)
random1 = torch.rand(2, 3)
print(random1)

random2 = torch.rand(2, 3)
print(random2)

torch.manual_seed(1729)
random3 = torch.rand(2, 3)
print(random3)

random4 = torch.rand(2, 3)
print(random4)

 

.manual_seed()를 통해서 RNG(MATLAB® random number generator)의 seed 값을 지정해 줄 수 있으며, 같은 seed number이면 같은 값을 갖게 된다.

Tensor Shapes

x = torch.empty(2, 2, 3)
empty_like_x = torch.empty_like(x)
zeros_like_x = torch.zeros_like(x)
ones_like_x = torch.ones_like(x)
rand_like_x = torch.rand_like(x)
some_constants = torch.tensor([[3.1415926, 2.71828], [1.61803, 0.0072897]])
print(some_constants)

some_integers = torch.tensor((2, 3, 5, 7, 11, 13, 17, 19))
print(some_integers)

more_integers = torch.tensor(((2, 4, 6), [3, 6, 9]))
print(more_integers)

 

torch.tensor()를 사용하면 Tuple이나 List에 들어있는 데이터를 쉽게 tensor로 바꿀 수 있다. 또한 tuple과 list가 collection내에 있을 때(이걸 무슨 형태라고 하지?)에는 multi-dimensional tensor로 변환된다.

Tensor Data Types

a = torch.ones((2, 3), dtype=torch.int16)
print(a)

b = torch.rand((2, 3), dtype=torch.float64) * 20.
print(b)

c = b.to(torch.int32)
print(c)

Tensor의 data type을 지정해주는 가장 간단한 방법은 변수를 생성하는 시점에 옵션을 설정해 주는 것이다. 디폴트로 지정할 때와 달리 data type을 지정해주면 print()의 ouput에서도 data type이 같이 출력된다.

  • torch.int16: 2바이트 정수
  • torch.int64: 8바이트 정수
  • torch.float64: 소수 

여기에서 또 하나 주의깊게 볼 점은, 변수를 선언할 때에 Shape을 정수들을 나열하여(2, 3) 선언한다는 것이다. 이렇게 tuple 형태로 적지 않아도 torch는 정수 arguments를 tensor shape으로 인지하지만, 가독성 있는 코드를 위해 위와 같은 방식으로 적는 것이 좋다. 

다른 방법은 .to() 메소드를 사용하는 것이다. b.to(torch.int32)는 float64이던 변수 b의 data type을 int32로 변환한다. 

Math & Logic with PyTorch Tensors

a = torch.rand(2, 3)
b = torch.rand(3, 2)
print(a * b)

 

torch간의 연산은 요소끼리 이루어진다. 하지만 위와 같은 경우에는 torch의 shape이 다르기 때문에 연산이 불가능하다. 

Tensor Broadcasting

#broadcasting
rand = torch.rand(2, 4)
doubled = rand * (torch.ones(1, 4) * 2)

print(rand)
print(doubled)

Torch의 shape이 달라도 연산이 가능한 때가 있다(broadcasting):

  • 각각의 tensor가 1차원 이상일 때 (빈 tensor가 있으면 안된다).
  • 두 tensor의 차원의 크기를 비교해야 한다(뒤의 차원부터 비교해야 함):
    • 1) 각각의 차원이 모두 일치하거나 (1차원 제외, 뒤에서 부터 일치해야 성립)
    • 2) 하나의 차원의 크기가 1이거나
    • 3) 텐서들 중 하나에는 차원이 존재하지 않거나

같은 shape을 가진 tensor는 당연히 broadcasting이 가능하다. 

a =     torch.ones(4, 3, 2)
b = a * torch.rand(   3, 2) # 2)와 3)의 경우 (뒤에서부터 일치, 하나의 차원 없음) 
c = a * torch.rand(   3, 1) # 2)의 경우 (1차원이 있음, 두번째 차원 일치)
d = a * torch.rand(   1, 2) # 2)의 경우 (1차원이 있음, 세번째 차원 일치)
e = a * torch.rand(4, 3)    # broadcasting 불가(뒤에서부터 일치해야 함)
f = a * torch.rand(   2, 3) # broadcasting 불가
g = a * torch.rand((0, )) #broadcasting 불가
  • b의 곱셈 연산은 a의 모든 layer에 대해서 broadcasting 해주고 있다.
  • c의 연산은 a의 모든 layer와 row에 대해서 broadcasting 해주고 있다.
  • d의 연산은 a의 모든 layer와 column에 대해서 broadcasting 해주고 있다.

Altering Tensors in Place

a = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('a:')
print(a)
print(torch.sin(a))   # 메모리 공간을 새롭게 마련한다.
print(a)              # a는 바뀌지 않음.

b = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('\nb:')
print(b)
print(torch.sin_(b))  # '_'가 추가되었다.
print(b)              # b가 바뀜.

 대부분의 tensor 이진연산들은 제3의 새로운 tensor를 반환한다. 예를 들어 c=a*b라는 연산이 있을 때(a, b 모두 tensor), c라는 새로운 tensor는 a,b와 별도로 새로운 메모리를 차지하게 된다. 만약에 이미 확보된 메모리에 tensor를 대체하고 싶다면,  연산 함수들 뒤에 underscore(_)를 추가하면 이미 있던 변수의 메모리에 계산 값을 지정한다. 

a = torch.ones(2, 2)
b = torch.rand(2, 2)

print('Before:')
print(a)
print(b)
print('\nAfter adding:')
print(a.add_(b)) 
print(a) #a+b 값에 a의 메모리를 할당
print(b) #기존의 b
print('\nAfter multiplying')
print(b.mul_(b)) #b*b 값에 기존 b의 메모리를 할당
print(b) #b*b 값

 

In-place 산수 함수들은 torch.Tensor object 위에서 돌아가는 methods이다. 다른 함수들과 같이 torch module에 연결되어 있지 않다. a.add_(b)에서 처럼 calling tensor(a)가 in-place에서 변화하는 값이다. 

 

a = torch.rand(2, 2)
b = torch.rand(2, 2)
c = torch.zeros(2, 2)
old_id = id(c)

print(c)
d = torch.matmul(a, b, out=c)
print(c)                # c의 메모리가 a*b 값에 할당 된다.

assert c is d           # c,d가 같은 값을 가질 뿐만 아니라 아예 동일한 object인 지 확인
assert id(c), old_id    # 새로운 c가 이전의 c와 동일한 id(동일한 object)를 갖는 지 확인 

torch.rand(2, 2, out=c) # 새로 tensor를 생성할 때에도 out을 지정해줄 수 있다.
print(c)                # c값은 또 변하지만
assert id(c), old_id    # 여전히 같은 object이다(같은 메모리)

 

기존의 값에 계산 값을 대체하는 또다른 방법에는 '할당된 텐서'가 있다. 우리가 그동안 봐왔던 많은 methods와 functions는 결과값을 선언해야 했다. 이때에 출력(out) tensor가 알맞은 shape과 dtype을 가지고 있다면 해당 메모리를 할당할 수 있다.

Copying Tensors

a = torch.ones(2, 2)
b = a

a[0][1] = 561  # we change a...
print(b)       # ...and b is also altered
a = torch.ones(2, 2)
b = a.clone()

assert b is not a      # different objects in memory...
print(torch.eq(a, b))  # ...but still with the same contents!

a[0][1] = 561          # a changes...
print(b)               # ...but b is still all ones

 

만약에 당신의 모델이 forward() method에 다중 연산 경로를 가지고 있다면, 기존의 tensor와 복제본이 모두 모델의 output에 기여할 것이며 이후에 모델이 두개의 tensor 모두에 대한 autograd를 통해 학습하게 된다. 만약에 source tensor의 autograd를 활성화 하여, 가중치 연산에서 미분된 가중치를 학습할 것이다. 

 

하지막 만약에 기존의 tensor와 복제된 tensor 모두가 gradients를 추적할 필요가 없는 경우라면 애초에 autograd를 비활성화 시켜도 된다.

 

마지막으로, 만약 당신의 모델의 forward() 함수를 통해서 중간 단계의 값들을 점검하는 지표(metrics)를 만들고 싶은거라면 복제된 source tensor에 대해서만 gradients를 추적하지 않게 하여서 performance를 개선할 수 있다. 이 경우에는 .detach() 를 사용하면 된다.

 

a = torch.rand(2, 2, requires_grad=True) # turn on autograd
print(a)

b = a.clone()
print(b)

c = a.detach().clone()
print(c)

print(a) #a의 requires_grad는 여전히 True
  • requires_grad를 True로 설정하여 autograd와 연산 history tracking이 모두 켜진다.
  • a를 복제한 b 또한 autograd와 연산 history tracking을 모두 수행한다.
  • a.detach().clone()에서는 연산 history를 추적하지 않는다: 'autograd가 꺼져있는 것처럼 해라'

Moving to GPU

Pytorch의 주요한 장점들 중 하나는 CUDA 호환 가능 Nvidia GPU에 대한 탄탄한 활성화를 해준다는 것이다. (CUDA: Compute Unified Device Architecture, 병렬 연산을 위한 Nvidia의 Platform).

 

GPU를 사용하기 위해서는 먼저 GPU가 사용 가능한 지 확인해야 한다.

주의: 만약에 CUDA 호환 가능한 GPU와 CUDA 드라이버가 설치되어 있지 않다면, GPU 관련 코드가 돌아가지 않을 것이다.

if torch.cuda.is_available():
    print('We have a GPU!')
else:
    print('Sorry, CPU only.')

만약 하나 이상의 GPU가 사용 가능하다고 판단이 되었다면, 갖고 있는 데이터를 GPU가 볼 수 있는 곳에 두어야 한다. CPU가 컴퓨터의 RAM 안에 있는 데이터에 대한 연산을 수행하며, GPU는 따로 할당된 메모리를 가지고 있다. 따라서 우리는 Device가 접근할 수 있는 곳에 데이터를 가져다 주어야 한다.

if torch.cuda.is_available():
    gpu_rand = torch.rand(2, 2, device='cuda')
    print(gpu_rand)
else:
    print('Sorry, CPU only.')

디폴트로 새로 만들어지는 tensor는 CPU 위에 만들어지기 때문에 우리는 GPU 위에 tensor를 만들기 위해서는 device 선언자를 통해서 명시해줘야 한다. 새로운 tensor를 출력하면 Pytorch가 어떤 device 위에 만들어졌는지 말해준다. torch.cuda.device_count()를 통해서 GPU 개수를 구할 수 있으며, 만약 여러대의 GPU를 갖고 있다면 device='cuda:0', device='cuda:1'와 같은 형태 인덱스화 해서 각각의 GPU를 지정해줄 수 있다. 

 

Device를 문자열 상수로 지정하는 것은 매우 취약하다. 실제로는 device handle(운영체제가 식별하기 위한 키값)을 생성하여 tensor에 넘겨줘야 한다. 

if torch.cuda.is_available():
    my_device = torch.device('cuda')
else:
    my_device = torch.device('cpu')
print('Device: {}'.format(my_device))

x = torch.rand(2, 2, device=my_device)
print(x)

만약에 이미 다른 device에 tensor가 위치해 있다면, .to() method를 사용해서 옮길 수 있다. 아래 코드에서는 CPU에 있던 tensor를 특정 device handle로 보내준다.

y = torch.rand(2, 2)
y = y.to(my_device)

두개 이상의 tensor를 포함한 연산을 할 때에는, 모든 tensor들이 같은 device위에 있어야 한다. 아래와 같이 CPU/GPU 각각의 device 위에 연산이 필요한 tensors가 따로 위치하면 에러가 발생한다.

x = torch.rand(2, 2)
y = torch.rand(2, 2, device='gpu')
z = x + y  # exception will be thrown