지난 챕터에서 우리는 텐서가 행렬의 연산에 적용되는 %*%
과 호환이 되지 않는 다는 것을 알게되었다. 이번 챕터에서는 텐서들의 연산에 대하여 알아보도록 하자.
토치 (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} ]
만들어진 세 개의 텐서 결과를 살펴보면 다음과 같다.
- 텐서 A: 정수들로 구성이 되어있고, 6개의 원소들이 벡터를 이루고 있다.
- 텐서 B: 실수들로 구성이 되어있고, 똑같이 6개의 원소들이 있지만, 모양이 4행 3열인 2차원 행렬의 모양을 하고 있다.
- 텐서 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
'R' 카테고리의 다른 글
4강. 객체지향 프로그래밍 첫걸음 - R6와 텐서 (0) | 2021.10.04 |
---|---|
3강. 텐서 이동시키기 - CPU tensor vs. GPU tensor (0) | 2021.10.04 |
1강. 딥러닝 첫걸음, 텐서 (tensor) 만들기 (0) | 2021.10.04 |
들어가며. torch 패키지 설치하기 (0) | 2021.10.04 |
1강. 마크다운 기초와 사륜안 패키지 시작하기 (0) | 2021.09.30 |
댓글