4 minute read

딥러닝 공부 2일차

Other Basic Ops



View(Reshape)

view함수는 원소의 수를 유지하면서 텐서의 크기를 변화시킬 수 있는 함수이다.

파이토치에서는 View 넘파이에서는 Reshape 이라고 쓴다.

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

[] 기호를 잘보면 2행3열짜리 matrix가 두개 쌓여있는 3D 텐서이다.

print(ft.shape)
torch.Size([2, 2, 3])

나는 처음에 당연히 사이즈가 (2,3,2)로 나올줄 알았다 왜 (2,2,3) 일까 고민해보니 이전글 Tensor Manipulation 1 글에서 알 수 있듯이 2by3 Matrix가 책꽂이처럼 2칸 쌓인 3D Tensor 이기 때문이라고 깨달았다.

참고로 기존 행렬(A,B)까지는 순서가 우리가 글자를 적듯이 적지만 3D 부터는 왼쪽으로 쌓여나간다.

  • 원소의 개수가 그대로라는 점이 키포인트다!
  • 파이토치의 view는 사이즈가 -1로 설정되면 다른 차원으로부터 해당 값을 유추합니다.



3D Tensor를 2D로

텐서를 축소도 가능하다.

print(ft.view([-1, 3])) # ft라는 텐서를 (?, 3)의 크기로 변경
print(ft.view([-1, 3]).shape)

-1의 의미는 컴퓨터보고 알아서 정하라는 의미같다. “나는 모르겠으니까 잘난 너가 정리해바!” 이런느낌이랄까… 그러고 3은 마지막 열을 3개짜리로 맞춰줘라는 의미이다

tensor([[ 0.,  1.,  2.],
        [ 3.,  4.,  5.],
        [ 6.,  7.,  8.],
        [ 9., 10., 11.]])
torch.Size([4, 3])

결과값이다 3열을 맞추기위해 행이 4개짜리로 변환되었다.

3D Tensor 크기변환

텐서를 자유자재로 바꿀 수 잇어서 view함수는 매우 중요하다.

print(ft.view([-1, 1, 3]))
print(ft.view([-1, 1, 3]).shape)

이번에는 차원은 그대로지만 “1행3열을 기준으로 어떻게 쌓을래?” 라고 파이토치에게 시키는 것이다

총 원소의 개수가 12개였으므로 4칸이 쌓여올라갈 것이라고 예측할 수 있다.

tensor([[[ 0.,  1.,  2.]],

        [[ 3.,  4.,  5.]],

        [[ 6.,  7.,  8.]],

        [[ 9., 10., 11.]]])
torch.Size([4, 1, 3])

짜잔~ 참 쉽죠?

Squeeze

스퀴즈함수는 해당차원이 1일경우 그 차원을 없애주는 역할을 한다.

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

현재 3행1열짜리 행렬이 소환되었습니다. 지금 그러면 열공간의 차원이 1이죠?

print(ft.squeeze())
print(ft.squeeze().shape)

스퀴즈함수를 사용해주면 (3,)과 같은 벡터값이 나올거라 예측해볼 수 있습니다.

tensor([0., 1., 2.])
torch.Size([3])

여기서 앞선 글에서 공부했던 (dim=?)도 응용할 수 있는데요, [0., 1., 2.] 과 같은 결과값을 불러오려면 dim에 무엇을 넣어야할까요? 바로 ft.squeeze(dim=1) 이겠죠? 0,1 즉 두번째 해당하는 차원이 1이니까 지워준다.

만약에 ft.squeeze(dim=0) 이였으면 어떤결과가 나올까요? 바로 아무런 축소도 일어나지 않습니다. 1번째 차원인 행공간의 차원이 3이기 때문입니다.

선형대수학과 파이토치에서 같은 용어의 쓰임이 달라서 제가 하는 말이 맞는지는 모르겠지만 어쨋든 그러합니다

Unsqueeze

언스퀴즈함수는 스퀴즈함수의 반대로 특정위치에 1인 차원을 추가해준다.

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

현재는 (3,)인 1차원 벡터인데

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

첫번째 차원인 행공간에 1인차원이 추가됨을 알 수 있습니다. (1,?)가 되어서 파이토치가 계산해주어서 (1,3)이 되었는데요.

이는 view함수와 동일하게도 사용이 가능합니다.

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

이번에는 두번째 차원을 추가해보겠습니다.

print(ft.unsqueeze(1))
print(ft.unsqueeze(1).shape)

두번째 차원인 열공간에 1인차원이 추가될 것이니 (3,1)의 행렬을 예측할 수 있습니다.

tensor([[0.],
        [1.],
        [2.]])
torch.Size([3, 1])

이제 unsqueeze 함수에 -1을 넣으면 어떻게 될까요? 위에 view함수때문에 잠시 헷갈릴 수는 있지만 -1은 인덱스상 마지막에서 첫번째인걸 잊으면 안됩니다.

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

마지막에서 첫번째 차원인 열공간에 1인 차원을 추가하는 위 예제랑 다를바가 없다는 것을 확인 할 수가 있습니다.

정리 : View, Squeeze, Unsqueeze 함수는 텐서의 원소 수를 그대로 유지하면서 모양과 차원을 조절합니다.

Type Casting

이제 각기 다른 언어들에서 많이 나오던 형변환입니다.

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

long 타입의 텐서를 만들어주고

print(lt.float())

텐서에 .float()을 붙이면 바로 float 타입으로 형변환이 이루어집니다.

bt = torch.ByteTensor([True, False, False, True])
print(bt)
tensor([1, 0, 0, 1], dtype=torch.uint8)

이번엔 byte 타입의 텐서입니다. 흔히 bool이라고 하는 true 냐 false 냐 하는 여기에 .float()을 하면 float형태가 .long()을 해주면 long형태로 바뀝니다.

print(bt.long())
print(bt.float())
tensor([1, 0, 0, 1])
tensor([1., 0., 0., 1.])



Concatenate

연결하기함수, 두 텐서를 합쳐준다.

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

2by2인 x 와 y 가 있습니다. 이 두 텐서를 합치고싶은데 합쳐지게되면 어느 한쪽 차원이 늘어나겠죠?

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

위와같이 dim=0 이면 행을 늘려라라는 의미가 됩니다.

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

dim=1 이라서 열이 늘어난 것을 볼 수 있습니다.

Stacking

연결하기함수, Concatenate를 하는 다른 방법

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

torch.stack() 함수로 합쳐줍니다. 이름처럼 x,y,z 세 벡터가 쌓아져 올라가는 모습을 볼 수 있습니다.

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

Stacking은 많은 연산을 포함하고있어서 Concatenate 보다 편리할 때가 많다고 합니다. 지금까지배운 함수들로 stacking을 표현하려면

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

기존의 x,y,z는 (2,)인 벡터였으나 Unsqueeze 함수로인해 행공간에 1차원이 추가되어 (1,2)인 행렬로 변화되고 그 세 행렬이 행방향으로 Concatenate 되는 모습.

기본적인 stacking 함수는 dim=0 을 내포한다고 볼 수 있습니다. 그렇다면 dim=1이라는 조건을 건다면 열방향으로 쌓아올라가는 모습을 볼 수 있습니다.

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



Ones_like & Zeros_like

해당하는 Tensor를 1 또는 0으로 채우는 함수

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

위 텐서들에게 ones_like 를 해주면 모든 요소가 1로 바뀝니다.

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

또한 zero_like 를 해주면 모든 요소가 0으로 바뀐다.

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



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.]])

기존의 값은 변하지 않은 것을 알 수 있습니다. 여기서 연산뒤에 _ 를 붙이면 덮어쓰기연산이 됩니다.

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

In-Place Operation 은 사실 쓸모가 없는게 원래 목적은 연산의 속도를 키우기 위함이지만, 파이토치에서 지원하는 garbage collector가 워낙 훌륭해서 파이토치 측에서는 속도향상에 큰 도움이 되지 않을 것이라고 했다고 한다.

참고문헌 : wikidocs


끝!

Leave a comment