본문 바로가기
R

2강. 텐서 (tensor) 들의 연산 배우기

by 슬통이 2021. 10. 4.
반응형

지난 챕터에서 우리는 텐서가 행렬의 연산에 적용되는 %*%과 호환이 되지 않는 다는 것을 알게되었다. 이번 챕터에서는 텐서들의 연산에 대하여 알아보도록 하자.

토치 (torch) 불러오기 및 준비물 준비

토치 (torch) 를 불러오고, 이번 챕터에 사용될 텐서 A, B, 그리고 C를 준비하자. 지난 챕터에서 배운 난수를 이용한 텐서도 만들 예정이니 난수를 고정한다.

library(torch)

# 난수 생성 시드 고정 
torch_manual_seed(2021)
A <- torch_tensor(1:6)
B <- torch_rand(2, 3)
C <- torch_rand(2, 3, 2)
A; B; C
## torch_tensor
##  1
##  2
##  3
##  4
##  5
##  6
## [ CPULongType{6} ]
## torch_tensor
##  0.1304  0.5134  0.7426
##  0.7159  0.5705  0.1653
## [ CPUFloatType{2,3} ]
## torch_tensor
## (1,.,.) = 
##   0.0443  0.9628
##   0.2943  0.0992
##   0.8096  0.0169
## 
## (2,.,.) = 
##   0.8222  0.1242
##   0.7489  0.3608
##   0.5131  0.2959
## [ CPUFloatType{2,3,2} ]

만들어진 세 개의 텐서 결과를 살펴보면 다음과 같다.

  1. 텐서 A: 정수들로 구성이 되어있고, 6개의 원소들이 벡터를 이루고 있다.
  2. 텐서 B: 실수들로 구성이 되어있고, 똑같이 6개의 원소들이 있지만, 모양이 4행 3열인 2차원 행렬의 모양을 하고 있다.
  3. 텐서 C: 실수들로 구성이 되어있고, 총 원소 갯수는 12개지만, 모양은 3행 2열의 행렬이 두개가 쌓여진 꼴의 3차원 배열 (array) 이다.

텐서의 연산

형(type) 변환

먼저 주목해야 할 것은 바로 텐서 A와 B의 자료형이 다르다는 것이다. 이게 무슨뜻이냐면 A에는 정수만이 담길 수 있고, B에는 실수만이 담길 수 있도록 설계가 되어있다는 것이다. 앞에서 확인한 자료형을 좀 더 명확하게 확인하기 위해서는 type() 사용한다.

A$dtype
## torch_Long
B$dtype
## torch_Float

텐서 A를 실수형 텐서로 바꿔보자. 텐서의 형을 변환할 때에는 A텐서 안에 속성으로 들어가있는 to() 함수를 사용 (좀 더 어려운 관점에서는 OOP의 method를 사용) 해서 바꿔줄 수 있다.

A <- A$to(dtype = torch_double())
A
## torch_tensor
##  1
##  2
##  3
##  4
##  5
##  6
## [ CPUDoubleType{6} ]

torch에는 정말 많은 자료형이 있는데, 그 목록은 다음을 참고하자.

모양 변환

앞에서 텐서 A를 B와 같은 실수를 담을 수 있는 형으로 바꾸었다. 그렇다면 이 두 개를 더할 수 있을까? 답은 “아니올시다.” 이다. 왜냐하면 모양이 다르기 때문이다.

# error will be generated
A + B

모양이 다른 텐서를 더하려고 하면 R은 너무나 많은 에러를 쏟아낼 것이다. 모양이 다른 두 텐서를 더하기 위해서는 모양을 같게 맞춰줘야 한다. A의 모양을 B의 모양과 같이 바꿔보도록 하자. 모양을 바꿀때는 view() 함수를 사용하고, 안에 모양의 형태를 벡터 형식으로 짚어 넣는다는 것을 기억하자.

A <- A$view(c(2, 3))
A
## torch_tensor
##  1  2  3
##  4  5  6
## [ CPUDoubleType{2,3} ]

한가지 짚고 넘어가야하는 기능이 있는데, R에서 행렬을 정의할 때, 주어진 원소벡터를 넣고, 가로행과 세로열 중 하나만 입력을 해도 잘 정의가 되는 것을 기억할 것이다. view 함수 역시 비슷한 기능이 있는데, 바로 -1을 이용해서 모양을 변환시키는 방법이다. 앞선 예제에서 2행 3열이 텐서를 1행의 가로 텐서로 변환 시키려면 다음과 같이 view() 함수의 입력값을 조정할 수 있다.

A$view(c(1, -1))
## torch_tensor
##  1  2  3  4  5  6
## [ CPUDoubleType{1,6} ]

덧셈과 뺄셈

앞에서 형(type)과 모양(shape)까지 맞춰놨으니, 텐서끼리의 덧셈과 뺄셈을 할 수 있다.

A + B
## torch_tensor
##  1.1304  2.5134  3.7426
##  4.7159  5.5705  6.1653
## [ CPUDoubleType{2,3} ]
A - B
## torch_tensor
##  0.8696  1.4866  2.2574
##  3.2841  4.4295  5.8347
## [ CPUDoubleType{2,3} ]

사실, 텐서끼리의 연산은 모양만 맞으면 가능하다. 즉, 다음의 연산이 성립한다.

A_ <- A$to(dtype = torch_long())
A_ + B
## torch_tensor
##  1.1304  2.5134  3.7426
##  4.7159  5.5705  6.1653
## [ CPUFloatType{2,3} ]

결과에서 알 수 있듯, 정수를 담을 수 있는 텐서와 실수를 담을 수 있는 텐서를 더하면, 결과는 실수를 담을 수 있는 텐서로 반환이 된다. 하지만, 필자는 이러한 코딩은 피해야 한다고 생각한다. 즉, 모든 연산을 할 경우, 명시적으로 형변환을 한 후 연산을 할 것을 권한다. 왜냐하면, 언제나 우리는 코드를 다른 사람이 보았을 때, 이해하기 쉽도록 짜는 것을 추구해야 한다. (코드는 하나의 자신의 생각을 적은 글이다.)

상수와의 연산

R에서와 마찬가지로, 텐서와 상수와의 사칙연산은 각 원소에 적용되는 것을 확인하자.

A + 2
## torch_tensor
##  3  4  5
##  6  7  8
## [ CPUDoubleType{2,3} ]
B^2
## torch_tensor
##  0.0170  0.2636  0.5514
##  0.5125  0.3254  0.0273
## [ CPUFloatType{2,3} ]
A %/% 3
## torch_tensor
##  0  0  1
##  1  1  2
## [ CPUDoubleType{2,3} ]
A %% 3
## torch_tensor
##  1  2  0
##  1  2  0
## [ CPUDoubleType{2,3} ]

제곱근과 로그

제곱근(square root)나 로그(log) 함수 역시 각 원소별 적용이 가능하다.

# error will be generated
A
torch_sqrt(A)

위의 연산이 에러가 나는 이유는 A가 정수를 담는 텐서였는데, 연산을 수행한 후에 실수가 담겨져서 나오는 에러이다. R과는 사뭇다른 예민한 아이 torch를 위해 형을 바꿔준 후에 연산을 실행하도록 하자.

torch_sqrt(A$to(dtype = torch_double()))
## torch_tensor
##  1.0000  1.4142  1.7321
##  2.0000  2.2361  2.4495
## [ CPUDoubleType{2,3} ]
torch_log(B)
## torch_tensor
## -2.0368 -0.6667 -0.2977
## -0.3342 -0.5613 -1.8002
## [ CPUFloatType{2,3} ]

텐서의 곱셈

텐서의 곱셈 역시 모양이 맞아야 하므로, 3행 2열이 두개가 붙어있는 C에서 앞에 한장을 떼어내도록 하자.

B
## torch_tensor
##  0.1304  0.5134  0.7426
##  0.7159  0.5705  0.1653
## [ CPUFloatType{2,3} ]
D <- C[1,,]
D
## torch_tensor
##  0.0443  0.9628
##  0.2943  0.0992
##  0.8096  0.0169
## [ CPUFloatType{3,2} ]

텐서의 곱셈은 torch_matmul() 함수를 사용한다.

# 파이프 사용해도 무방하다.
# B %>% torch_matmul(D)
torch_matmul(B, D)
## torch_tensor
##  0.7580  0.1890
##  0.3334  0.7486
## [ CPUFloatType{2,2} ]

토치의 텐서 곱셈은 다음과 같은 방법들도 있으니 알아두자.

torch_mm(B, D)
## torch_tensor
##  0.7580  0.1890
##  0.3334  0.7486
## [ CPUFloatType{2,2} ]
B$mm(D)
## torch_tensor
##  0.7580  0.1890
##  0.3334  0.7486
## [ CPUFloatType{2,2} ]
B$matmul(D)
## torch_tensor
##  0.7580  0.1890
##  0.3334  0.7486
## [ CPUFloatType{2,2} ]

텐서의 전치(transpose)

전치(transpose)는 주어진 텐서를 뒤집는 것인데, 다음의 문법 구조를 가지고 있다.

torch_transpose(input, dim0, dim1)

dim0, dim1는 바꿀 차원을 의미한다. ‘바꿀 차원은 두 개 밖에 없지 않나?’ 라고 생각할 수 있다. 2 차원 텐서의 경우에는 그렇다. 우리가 행렬을 전치하는 경우에는 transpose를 취하는 대상이 2차원이므로 지정해주는 차원이 정해져있다. 하지만, 텐서의 차원이 3차원 이상이 되면 전치를 해주는 차원을 지정해줘야한다.

A
## torch_tensor
##  1  2  3
##  4  5  6
## [ CPUDoubleType{2,3} ]

위의 텐서 A의 차원은 행과 열, 즉, 2개이다. 다음의 코드들은 A 텐서의 첫번째 차원과 두번째 차원을 뒤집는 효과를 가져온다. 즉, 전치 텐서가 된다.

torch_transpose(A, 1, 2)
## torch_tensor
##  1  4
##  2  5
##  3  6
## [ CPUDoubleType{3,2} ]
A$transpose(1, 2)
## torch_tensor
##  1  4
##  2  5
##  3  6
## [ CPUDoubleType{3,2} ]
A %>% torch_transpose(1, 2)
## torch_tensor
##  1  4
##  2  5
##  3  6
## [ CPUDoubleType{3,2} ]

3차원의 텐서를 살펴보자.

C
## torch_tensor
## (1,.,.) = 
##   0.0443  0.9628
##   0.2943  0.0992
##   0.8096  0.0169
## 
## (2,.,.) = 
##   0.8222  0.1242
##   0.7489  0.3608
##   0.5131  0.2959
## [ CPUFloatType{2,3,2} ]

텐서 C는 위와 같이 2차원 텐서가 두 개 포개져 있다고 생각하면 된다. 텐서의 결과물을 잘 살펴보면, 제일 앞에 위치한 1, 2가 나타내는 것이 우리가 흔히 생각하는 2차원 텐서들의 색인(index) 역할을 한다는 것을 알 수 있다. 앞으로는 편의를 위해서 3차원 텐서의 색인 역할을 하는 차원을 깊이(depth)라고 부르도록 하자. 앞에서 주어진 텐서 C 안의 포개져있는 2차원 텐서들을 전치하기 위해서는 이들을 관할(?)하는 두번째와 세번째 차원을 바꿔줘야 한다.

torch_transpose(C, 2, 3)
## torch_tensor
## (1,.,.) = 
##   0.0443  0.2943  0.8096
##   0.9628  0.0992  0.0169
## 
## (2,.,.) = 
##   0.8222  0.7489  0.5131
##   0.1242  0.3608  0.2959
## [ CPUFloatType{2,2,3} ]

결과를 살펴보면, 잘 바뀌어 있음을 알 수 있다.

R에서의 3차원 배열

앞에서 다룬 torch에서의 3차원 텐서 부분은 R에서 기본적으로 제공하는 array의 문법과 차이가 난다. 다음의 코드를 살펴보자. 먼저 R에서 2행 3열의 행렬을 두 개 포개어 놓은 3차원 배열을 만드는 코드이다.

array(1:12, c(2, 3, 2)) 
## , , 1
## 
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
## 
## , , 2
## 
##      [,1] [,2] [,3]
## [1,]    7    9   11
## [2,]    8   10   12

필자는 참고로 matrix()를 만들때에도 byrow 옵션을 써서 만드는 것을 좋아하는데, array()에서 byrow 옵션 효과를 적용하려면 aperm() 함수를 사용해야 한다. 따라서, 좀 더 직관적으로 쓰기위해서 다음의 함수를 사용하자.

array_3d_byrow <- function(num_vec, nrow, ncol, ndeath){
    aperm(array(num_vec, c(ncol, nrow, ndeath)), c(2, 1, 3))    
}

E <- array_3d_byrow(1:12, 2, 3, 2)
E
## , , 1
## 
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6
## 
## , , 2
## 
##      [,1] [,2] [,3]
## [1,]    7    8    9
## [2,]   10   11   12

이러한 코드를 앞서 배웠던 torch_tensor() 함수에 넣어보자.

E %>% torch_tensor()
## torch_tensor
## (1,.,.) = 
##   1  7
##   2  8
##   3  9
## 
## (2,.,.) = 
##    4  10
##    5  11
##    6  12
## [ CPULongType{2,3,2} ]

결과를 살펴보면, 우리가 예상했던 2행 3열의 텐서가 두개 겹쳐있는 텐서의 모양이 나오지 않는다는 것을 알 수 있다. 이유는 torch에서 정의된 3차원 텐서의 경우, 첫번째 차원이 텐서가 얼마나 겹쳐있는지를 나타내는 깊이(depth)를 나타내기 때문이다. 문제를 해결하기 위해서는 aperm() 사용해서 차원을 바꿔주면 된다.

E %>% 
  aperm(c(3, 1, 2)) %>% # 3 번째 차원을 맨 앞으로, 나머지는 그대로
  torch_tensor()
## torch_tensor
## (1,.,.) = 
##   1  2  3
##   4  5  6
## 
## (2,.,.) = 
##    7   8   9
##   10  11  12
## [ CPULongType{2,2,3} ]

위의 경우를 좀더 직관적인 함수명으로 바꿔서 사용하도록 하자.

array_to_torch <- function(mat, n_dim = 3){
    torch_tensor(aperm(mat, c(n_dim:3, 1, 2)))
}
E <- array_to_torch(E)
E
## torch_tensor
## (1,.,.) = 
##   1  2  3
##   4  5  6
## 
## (2,.,.) = 
##    7   8   9
##   10  11  12
## [ CPULongType{2,2,3} ]

다차원 텐서와 1차원 벡터 텐서의 연산

R에서 우리가 아주 애용하는 기능 중 하나가 바로 recycling 개념이다. 즉, 길이 혹은 모양이 맞지 않는 개체(object)들을 연산할 때, 자동으로 길이와 모양을 맞춰서 연산을 해주는 기능인데, torch에서도 이러한 기능을 제공한다. 다음의 코드를 살펴보자.

A
## torch_tensor
##  1  2  3
##  4  5  6
## [ CPUDoubleType{2,3} ]
A + torch_tensor(1:3)
## torch_tensor
##  2  4  6
##  5  7  9
## [ CPUDoubleType{2,3} ]
A
## torch_tensor
##  1  2  3
##  4  5  6
## [ CPUDoubleType{2,3} ]
A + torch_tensor(matrix(2:3, ncol = 1))
## torch_tensor
##  3  4  5
##  7  8  9
## [ CPUDoubleType{2,3} ]

1차원 텐서 끼리의 연산, 내적과 외적

1차원 텐서끼리의 연산도 2차원 텐서끼리의 연산과 마찬가지라고 생각하면 된다. 내적과 외적 역시 그냥 모양을 맞춰서 곱하면 된다.

A_1 <- A$view(c(1, -1))
A_1
## torch_tensor
##  1  2  3  4  5  6
## [ CPUDoubleType{1,6} ]
A_2 <- A$view(c(-1, 1))
A_2
## torch_tensor
##  1
##  2
##  3
##  4
##  5
##  6
## [ CPUDoubleType{6,1} ]
A_1$mm(A_2)
## torch_tensor
##  91
## [ CPUDoubleType{1,1} ]
A_2$mm(A_1)
## torch_tensor
##   1   2   3   4   5   6
##   2   4   6   8  10  12
##   3   6   9  12  15  18
##   4   8  12  16  20  24
##   5  10  15  20  25  30
##   6  12  18  24  30  36
## [ CPUDoubleType{6,6} ]

한가지 주의할 점은 1차원 텐서끼리의 연산이더라도 꼭 차원을 선언해줘서 열벡터와 행벡터를 분명히 해줘야 한다는 점이다.

A_3 <- torch_tensor(1:6)
A_1$mm(A_3)

위의 코드는 연산 에러가 나는데, 이유는 A_3의 모양이 A_1의 모양과 맞지 않기 때문이다.

A_1$size()
## [1] 1 6
A_3 <- torch_tensor(1:6)
A_3$size()
## [1] 6
반응형

댓글