1. 텐서 기초
a. 텐서 선언하기
- 1차원 텐서
tensor = torch.FloatTensor([0., 1., 2., 3.])
- 2차원 텐서
tensor = torch.FloatTensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.],
[10., 11., 12.]])
print(tensor.dim())
print(tensor.size())
print(tensor[:, :2]) # 첫번째 차원 전체 선택, 두번쨰 차원의 0번째, 1번째 요소 선택
2
torch.Size([4, 3])
tensor([[ 1., 2.], [ 4., 5.], [ 7., 8.], [10., 11.]]
- 데이터로부터 직접 생성
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
- Numpy 배열로부터 생성
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
# 그 역도 가능하다.
t = torch.ones(5)
n = t.numpy()
# tensor와 numpy 배열은 메모리 공간을 공유하기 때문에
# n과 t중 하나만 변경해도 다른 하나도 같이 변경된다.
- 다른 텐서로부터 생성
x_ones = torch.ones_like(x_data) # x_data의 속성을 유지합니다.
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) # x_data의 속성을 덮어씁니다.
print(f"Random Tensor: \n {x_rand} \n")
- 함수를 사용한 특별한 초기화
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
b. 텐서 속성
tensor=torch.rand(3,4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
2. 브로드캐스팅
크기가 서로 다른 텐서끼리 연산할 때 파이토치에서 자동으로 크기를 맞춰주는 기능이다.
a. 덧셈/뺄셈
두 행렬을 서로 더하거나 뺄 때에는 두 행렬의 크기가 같아야 한다. 그러나 파이토치에서는 브로드캐스팅을 통해 크기가 서로 다른 두 텐서의 덧셈과 뺄셈을 지원한다.
우선 두 텐서의 크기가 서로 같은 경우를 보자.
t1 = torch.FloatTensor([[10, 100], [1000, 10000]])
t2 = torch.FloatTensor([[1, 2], [3, 4]]) # 둘다 크기가 (1, 2)인 텐서
print(t1 + t2)
tensor([[ 11., 102.],
[ 1003., 10004.]])
크기가 같으므로 대응하는 원소끼리 더함으로써 간단하게 계산이 된다.
이제 두 텐서의 크기가 서로 다른 경우를 보자.
t1 = torch.FloatTensor([[1, 4]]) # 크기가 (1, 2)인 텐서
t2 = torch.FloatTensor([[3], [4]]) # 크기가 (2, 1)인 텐서
print(t1 + t2)
print((t1 + t2).size())
tensor([[4., 7.],
[5., 8.]])
torch.Size([2, 2])
t1과 t2 둘다 2차원 텐서이다. 하지만 size(크기)는 (1, 2)와 (2, 1)로 서로 다르다. 따라서 브로드캐스팅을 통해 두 텐서의 크기가 다음과 같이 변경되어 연산된다.
[1, 4] ⇒ [[1, 4], [1, 4]]
[[3], [4]] ⇒ [[3, 3], [4, 4]]
브로드캐스팅을 통해 두 텐서의 크기가 동일하게 맞춰진 뒤, 통상 우리가 알고 있는 행렬의 덧셈이 행해진다.
위의 예시에서는 두 텐서의 크기가 (2, 2)로 동일하게 변경되었고, 따라서 덧셈 결과 텐서의 크기도 (2, 2)가 된다.
다른 예시를 보자.
t1 = torch.FloatTensor([[1, 4, 10]]) # 크기가 (1, 3)인 텐서
t2 = torch.FloatTensor([[3], [4]]) # 크기가 (2, 1)인 텐서
print(t1 + t2)
print((t1 + t2).size())
tensor([[ 4., 7., 13.],
[ 5., 8., 14.]])
torch.Size([2, 3])
이번엔 텐서의 크기가 각각 (1, 3)과 (2, 1)이다. 이때는 크기가 (2, 3)으로 변경되어 계산되었다. 차원이 같은 경우 크기는 더 큰 쪽으로 증가한다는 것을 확인할 수 있다.
두 텐서의 차원이 다른 경우에도 뎃셈이 가능할까?
t1 = torch.FloatTensor([[1, 2]])
t2 = torch.FloatTensor([[[3], [4], [5], [6]], [[7], [8], [9], [10]]])
print(t1.size())
print(t2.size())
print(t1 + t2)
print((t1 + t2).size())
torch.Size([1, 2])
torch.Size([2, 4, 1])
tensor([[[ 4., 5.],
[ 5., 6.],
[ 6., 7.],
[ 7., 8.]],
[[ 8., 9.],
[ 9., 10.],
[10., 11.],
[11., 12.]]])
torch.Size([2, 4, 2])
t1은 2차원, t2는 3차원 텐서이다. 두 텐서를 더하는 경우 두 텐서 모두 크기가 (2, 4, 2)로 변경된다.
t1 → [ [[1, 2], [1, 2], [1, 2], [1, 2]],
[[1, 2], [1, 2], [1, 2], [1, 2]] ]
t2 → [ [[3, 3], [4, 4], [5, 5], [6, 6]],
[[7, 7], [8, 8], [9, 9], [10, 10]] ]
b. 곱셈 (행렬곱과 요소별 곱)
행렬 A(m x n)와 B(n x p)에 대해 C = AB에서 C는 m x p인 행렬로 정의된다. 즉 행렬의 곱이 정의되려면 첫번째 행렬의 열과 두번쨰 행렬의 행이 동일해야 한다.
이 행렬 곱셈은 matmul() 함수를 통해 계산이 가능하다.
t1 = torch.FloatTensor([[1, 2], [3, 4]]) # 2 x 2
t2 = torch.FloatTensor([[1], [2]]) # 2 x 1
print(t1.matmul(t2)) # 2 x 1
tensor([[ 5.],
[11.]])
만약 위 조건이 갖춰지지 않은 두 벡터를 matmul로 연산하려고 하는 경우 다음과 같이 에러가 발생한다.
위의 행렬 곱셈 말고도 element-wise 곱셈이라는 것이 존재한다. 이는 행렬의 덧셈처럼 두 벡터의 크기가 같을 때 동일한 위치에 있는 원소들끼리 곱하는 것을 말한다. 위의 예시에서 사용된 텐서를 그대로 element-wise 곱셈해보자.
t1 = torch.FloatTensor([[1, 2], [3, 4]]) # 2 x 2
t2 = torch.FloatTensor([[1], [2]]) # 2 x 1
print(t1 * t2) # 2 x 2, print(t1.mul(t2))와 같다.
tensor([[1., 2.],
[6., 8.]])
두 행렬의 크기가 동일해야 하므로 위의 연산에서는 브로드캐스팅이 작동해서 행렬의 크기가 맞춰지게 된다.
t2 = [[1], [2]]
⇒ [[1, 1], [2, 2]]
3. 평균 (mean 함수)
텐서를 이용해 평균을 구해보자.
평균에 대한 관련 자료가 적어서 일단은 내가 이해한대로 적어보겠다.
위와 같이 mean() 함수를 이용해 평균을 구할 수 있다. 인자를 아무것도 주지 않는다면 단순히 모든 원소들을 각각 더해서 평균을 구한다.
dim=숫자 형식으로 인자를 넘겨주는 경우, 그 차원에서의 평균값을 구한다.
a. dim=0
위에서 dim=0 을 넘겨준 경우, 첫번째 차원에서 평균을 구하라는 말이 된다. 여기서 텐서의 크기가 (3, 4, 2) 이므로 첫번째 차원의 요소의 개수는 3개이다. 따라서 평균을 구하려면 그 3개의 요소를 더해서 3으로 나눠줘야한다. 그 요소 3개는 다음과 같다.
요소1: [[3, 10], [4, 10], [5, 10], [6, 10]]
요소2: [[7, 10], [8, 10], [9, 10], [10, 10]]
요소3: [[11, 10], [12, 10], [13, 10], [14, 10]]
각 요소의 같은 위치에 있는 수끼리 더하고 나눠서 평균을 구하면 다음과 같다.
평균: [[7, 10]. [8, 10], [9,10], [10, 10]]
이때 결과 텐서의 크기는 (4, 2)가 된다. 즉 첫번째 차원의 요소끼리 다 더하고 3으로 나눠줬으므로 첫번째 차원의 요소는 평균이라는 하나의 요소로 합쳐진 것이다. 따라서 계산 대상인 텐서의 크기 (3, 4, 2)에서 3이 사라졌다. 다시말해 차원을 인자로 주는 경우 그 차원이 사라진다고 볼 수 있다.
b. dim=1
dim=1 을 인자로 넘겨주는 경우를 보자. 두번째 차원에서 평균을 구하라는 말인데, 두번째 차원의 크기는 4이므로 요소를 다음과 같이 4개로 나눌 수 있다.
그런데 첫번째 차원의 크기가 3이므로 두번째 차원은 3개가 존재한다.
따라서 요소 그룹이 3개가 나온다.
요소1: [3, 10]
요소2: [4, 10]
요소3: [5, 10]
요소4: [6, 10]
요소1: [7, 10]
요소2: [8, 10]
요소3: [9, 10]
요소4: [10, 10]
요소1: [11, 10]
요소2: [12, 10]
요소3: [13, 10]
요소4: [14, 10]
각 그룹에서, 각 요소의 같은 위치의 숫자끼리 더하고 4로 나누면 된다. 그러면 해당하는 차원의 요소들은 4개에서 1개로 합쳐진다. (정확히는 합쳐져서 나누어져 하나로 된다)
따라서 결과 텐서의 크기는 (3, 2)가 되고 평균은 [[ 4.5000, 10.0000],
[ 8.5000, 10.0000],
[12.5000, 10.0000]] 가 된다.
c. dim=2
앞선 예제와 비슷하게 dim=2인 경우에도 역시 3번째 차원에서 평균이 구해지고 결과 텐서의 크기는 (3, 4)가 된다. 또한 세번째 차원의 크기는 2이고 첫번째와 두번째 차원의 크기가 각각 3과 4이므로 요소 그룹은 12개가 생긴다. 즉 요소는 다음과 같이 나타낼 수 있다.
요소1: 3
요소2: 10
요소1: 4
요소2: 10
요소1: 5
요소2: 10
요소1: 6
요소2: 10
요소1: 7
요소2: 10
요소1: 8
요소2: 10
요소1: 9
요소2: 10
요소1: 10
요소2: 10
요소1: 11
요소2: 10
요소1: 12
요소2: 10
요소1: 13
요소2: 10
요소1: 14
요소2: 10
각 그룹의 요소를 더하고 2로 나누면 평균이 나온다. 따라서 평균은 12개가 나오게 된다.
평균: [[ 6.5000, 7.0000, 7.5000, 8.0000],
[ 8.5000, 9.0000, 9.5000, 10.0000],
[10.5000, 11.0000, 11.5000, 12.0000]]
4. 덧셈 (sum 함수)
sum 함수를 이용해서 텐서의 요소들의 합을 구할 수 있다. 이때 인자로 평균과 마찬가지로 dim인자를 넘겨줄 수 있는데, 평균과 그 의미가 정확히 같다. 다만 결과가 합일 뿐이다. 따라서 위의 결과의 각 요소들을 3, 4, 2로 나눠주면 평균이 나오게 된다.
5. Max와 ArgMax
Max는 원소의 최댓값을, ArgMax는 원소의 최댓값의 인덱스를 리턴한다.
역시 위의 평균이나 합을 구하는 경우와 비슷하다. dim은 원소의 쌍을 무엇으로 볼지 정하기 위한 변수라고 이해하면 될 듯하다.
max에 dim인자를 넘겨주면 max 값뿐만 아니라 maxarg 값도 같이 리턴한다.
예를 들어 [11, 10]은 [2, 0]과 대응되는데, 11이 세번째 요소이고 10은 첫번째 요소이기 때문이다.
6. View
view 함수를 통해 텐서의 크기를 변경할 수 있다. 이때 원소의 개수는 유지된다.
a. 3차원 텐서를 2차원 텐서로 변경하기
(2, 2, 3)의 크기를 가지는 3차원 텐서 ft를 다음과 같이 선언하자.
이제 view를 다음과 같이 사용하면 텐서의 크기가 (4, 3)으로 변한 것을 볼 수 있다.
ft.view([4, 3])는 말 그대로 ft라는 텐서를 (4, 3)의 크기를 가진 텐서로 변경시켜 리턴하라는 의미이다. 원래의 텐서 크기가 2 x 2 x 3 = 12 였고, view를 사용해도 원소의 개수는 변하면 안되므로 4 x 3 = 12로 동일한 것을 확인할 수 있다.
그렇다면 4 x 3 = 12 에서, 12는 고정되어 있으므로 4나 3을 미지수로 치환해도 그 미지수가 무엇인지 간단하게 알 수 있다. 즉 ? x 3 = 12에서 ?는 4가 됨을 쉽게 유추할 수 있다.
따라서 ft.view([4, 3])은 다음과 같이 나타낼 수 있다.
print(ft.view([4, -1])) # ft라는 텐서를 (4, ?)의 크기로 변경
print(ft.view([4, -1]).shape)
print(ft.view([-1, 3])) # ft라는 텐서를 (?, 3)의 크기로 변경
print(ft.view([-1, 3]).shape)
여기서 -1은 위의 ?과 의미가 일치한다. 즉 파이썬에게 나머지 숫자는 알아서 계산하라는 식으로 던져준 것이다. 따라서 위의 두 코드 모두 처음과 같은 결과를 반환한다.
b. 오류
당연히 a x b = 12 에서 a와 b는 하나로 정해질 수 없으므로 아래와 같은 코드는 오류를 뿜는다.
print(ft.view([-1, -1]))
print(ft.view([-1, -1]).shape)
# RuntimeError: only one dimension can be inferred
또한 ? x 5 = 12 에서 ?는 정수가 될 수 없으므로 다음과 같은 코드도 오류를 내보낸다
print(ft.view([-1, 5]))
# RuntimeError: shape '[-1, 5]' is invalid for input of size 12
기본적으로 정수 x 정수 x ... x 정수 = (원소의 개수) 가 되지 않으면 오류가 발생한다.
c. 3차원 텐서의 크기 변경하기
ft 텐서의 크기는 (2 x 2 x 3)이다. 이 텐서의 크기를 (-1 x 1 x 3) 으로 변경한다면 다음과 같이 될 것이다.
7. Squeeze
스퀴즈는 차원의 크기가 1인 경우 해당 차원을 제거한다.
스퀴즈 전: [ [0], [1], [2] ] 크기는 3 x 1
스퀴즈 후: [0, 1, 2] 크기는 3
8. Unsqueeze
Squeeze와 정확히 반대되는 개념으로 특정 위치에 크기가 1인 차원을 추가하는 함수이다. 아래와 같은 텐서를 생각해보자.
ft = torch.Tensor([[0, 1, 2], [3, 4, 5]])
print(ft.shape)
torch.Size([2, 3])
unsqueeze 함수에 0을 인자로 넘겨주면 첫번째 차원에 크기가 1인 차원을 추가하겠다는 뜻이 된다.
print(ft.unsqueeze(0))
print(ft.unsqueeze(0).shape)
tensor([[[0., 1., 2.],
[3., 4., 5.]]])
torch.Size([1, 2, 3])
따라서 출력은 위와 같이 된다. 원래의 벡터를 그저 [ ] 괄호로 감싸는 것과 같다.
1을 인자로 준 경우 다음과 같다.
print(ft.unsqueeze(1))
print(ft.unsqueeze(1).shape)
tensor([[[0., 1., 2.]],
[[3., 4., 5.]]])
torch.Size([2, 1, 3])
2를 인자로 준 경우 다음과 같다.
print(ft.unsqueeze(2))
print(ft.unsqueeze(2).shape)
tensor([[[0.],
[1.],
[2.]],
[[3.],
[4.],
[5.]]])
torch.Size([2, 3, 1])
9. Type Casting
텐서에는 위와 같은 자료형들이 있다.
텐서의 자료형을 변환하는 간단한 예제를 살펴보자.
lt = torch.LongTensor([1, 2, 3, 4])
print(lt)
print(lt.float())
tensor([1, 2, 3, 4])
tensor([1., 2., 3., 4.])
10. Concatenate
x, y, z 텐서를 선언하고 cat 함수를 이용해 텐서들을 연결해보자.
x = torch.FloatTensor([[1, 2], [3, 4]])
y = torch.FloatTensor([[5, 6], [7, 8]])
z = torch.FloatTensor([[9, 10], [11, 12]])
print(torch.cat([x, y. z], dim=0))
print(torch.cat([x, y, z], dim=1))
tensor([[ 1., 2.],
[ 3., 4.],
[ 5., 6.],
[ 7., 8.],
[ 9., 10.],
[11., 12.]])
tensor([[ 1., 2., 5., 6., 9., 10.],
[ 3., 4., 7., 8., 11., 12.]])
dim을 인자로 주면 해당 차원을 늘리겠다는 의미이다. 늘리기 전의 텐서의 크기는 (2 x 2)였지만 늘리고 난 후 크기는 dim=0인 경우 (6 x 2), dim= 1인 경우 (2 x 6)이 되었다.
dim을 인자로 주는 경우 해당 차원에 한해서 텐서들의 크기가 일치해야한다. 만약 그렇지 않은 경우 아래와 같이 에러가 뜬다.
11. Stacking
Concatenate와 같은 역할을 하지만 더 직관적인 함수 stack이 있다.
x = torch.FloatTensor([1, 4])
y = torch.FloatTensor([2, 5])
z = torch.FloatTensor([3, 6])
print(torch.stack([x, y, z]))
print(torch.stack([x, y, z], dim=1))
tensor([[1., 4.],
[2., 5.],
[3., 6.]])
tensor([[1., 2., 3.],
[4., 5., 6.]])
stack 함수는 여러 복잡한 연산을 간단히 압축해 놓은 것에 불과하다. 아래의 두 코드는 같은 결과를 반환한다.
print(torch.stack([x, y, z])
print(torch.cat([x.unsqueeze(0), y.unsqueeze(0), z.unsqueeze(0)], dim=0))
12. Ones-like & Zeros-like
텐서를 0이나 1로 채운다.
13. In-place Operation (덮어쓰기 연산)
연산을 할 때 뒤에 _ 을 붙여주면 원본도 연산 결과에 따라 바뀌게 된다.
파이토치 공식 홈페이지에서는 아래와 같은 이유로 덮어쓰기 연산을 지양하고 있다.
References
https://pytorch.org/docs/stable/torch.html
https://tutorials.pytorch.kr/beginner/basics/tensorqs_tutorial.html