-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy path02-tensor-calculation.Rmd
265 lines (186 loc) · 9.75 KB
/
02-tensor-calculation.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# 텐서 (tensor) 연산 {#operation}
지난 챕터에서 우리는 텐서가 행렬의 연산에 적용되는 `%*%`과 호환이 되지 않는 다는 것을 알게되었다. 이번 챕터에서는 텐서들의 연산에 대하여 알아보도록 하자.
## 토치 (torch) 불러오기 및 준비물 준비
토치 (torch) 를 불러오고, 이번 챕터에 사용될 텐서 A, B, 그리고 C를 준비하자. 지난 챕터에서 배운 난수를 이용한 텐서도 만들 예정이니 난수를 고정한다.
```{r}
library(torch)
# 난수 생성 시드 고정
torch_manual_seed(2021)
```
```{r}
A <- torch_tensor(1:6)
B <- torch_rand(2, 3)
C <- torch_rand(2, 3, 2)
A; B; C
```
만들어진 세 개의 텐서 결과를 살펴보면 다음과 같다.
1. 텐서 A: 정수들로 구성이 되어있고, 6개의 원소들이 벡터를 이루고 있다.
2. 텐서 B: 실수들로 구성이 되어있고, 똑같이 6개의 원소들이 있지만, 모양이 4행 3열인 2차원 행렬의 모양을 하고 있다.
3. 텐서 C: 실수들로 구성이 되어있고, 총 원소 갯수는 12개지만, 모양은 3행 2열의 행렬이 두개가 쌓여진 꼴의 3차원 배열 (array) 이다.
## 텐서의 연산
### 형(type) 변환
먼저 주목해야 할 것은 바로 텐서 A와 B의 자료형이 다르다는 것이다. 이게 무슨뜻이냐면 A에는 정수만이 담길 수 있고, B에는 실수만이 담길 수 있도록 설계가 되어있다는 것이다. 앞에서 확인한 자료형을 좀 더 명확하게 확인하기 위해서는 `type()` 사용한다.
```{r}
A$dtype
B$dtype
```
텐서 A를 실수형 텐서로 바꿔보자. 텐서의 형을 변환할 때에는 A텐서 안에 속성으로 들어가있는 to() 함수를 사용 (좀 더 어려운 관점에서는 OOP의 method를 사용) 해서 바꿔줄 수 있다.
```{r}
A <- A$to(dtype = torch_double())
A
```
torch에는 정말 많은 자료형이 있는데, 그 목록은 [다음](https://torch.mlverse.org/docs/reference/torch_dtype.html)을 참고하자.
### 모양 변환
앞에서 텐서 A를 B와 같은 실수를 담을 수 있는 형으로 바꾸었다. 그렇다면 이 두 개를 더할 수 있을까? 답은 "아니올시다." 이다. 왜냐하면 모양이 다르기 때문이다.
```{r eval=FALSE}
# error will be generated
A + B
```
모양이 다른 텐서를 더하려고 하면 R은 너무나 많은 에러를 쏟아낼 것이다. 모양이 다른 두 텐서를 더하기 위해서는 모양을 같게 맞춰줘야 한다. A의 모양을 B의 모양과 같이 바꿔보도록 하자. 모양을 바꿀때는 `view()` 함수를 사용하고, 안에 모양의 형태를 벡터 형식으로 짚어 넣는다는 것을 기억하자.
```{r}
A <- A$view(c(2, 3))
A
```
한가지 짚고 넘어가야하는 기능이 있는데, R에서 행렬을 정의할 때, 주어진 원소벡터를 넣고, 가로행과 세로열 중 하나만 입력을 해도 잘 정의가 되는 것을 기억할 것이다. view 함수 역시 비슷한 기능이 있는데, 바로 `-1`을 이용해서 모양을 변환시키는 방법이다. 앞선 예제에서 2행 3열이 텐서를 1행의 가로 텐서로 변환 시키려면 다음과 같이 `view()` 함수의 입력값을 조정할 수 있다.
```{r}
A$view(c(1, -1))
```
### 덧셈과 뺄셈
앞에서 형(type)과 모양(shape)까지 맞춰놨으니, 텐서끼리의 덧셈과 뺄셈을 할 수 있다.
```{r}
A + B
```
```{r}
A - B
```
사실, 텐서끼리의 연산은 **모양만 맞으면 가능**하다. 즉, 다음의 연산이 성립한다.
```{r}
A_ <- A$to(dtype = torch_long())
A_ + B
```
결과에서 알 수 있듯, 정수를 담을 수 있는 텐서와 실수를 담을 수 있는 텐서를 더하면, 결과는 실수를 담을 수 있는 텐서로 반환이 된다. 하지만, 필자는 이러한 코딩은 피해야 한다고 생각한다. 즉, 모든 연산을 할 경우, 명시적으로 형변환을 한 후 연산을 할 것을 권한다. 왜냐하면, 언제나 우리는 코드를 다른 사람이 보았을 때, 이해하기 쉽도록 짜는 것을 추구해야 한다. (코드는 하나의 자신의 생각을 적은 글이다.)
### 상수와의 연산
R에서와 마찬가지로, 텐서와 상수와의 사칙연산은 각 원소에 적용되는 것을 확인하자.
```{r}
A + 2
B^2
A %/% 3
A %% 3
```
### 제곱근과 로그
제곱근(square root)나 로그(log) 함수 역시 각 원소별 적용이 가능하다.
```{r eval=FALSE}
# error will be generated
A
torch_sqrt(A)
```
위의 연산이 에러가 나는 이유는 A가 정수를 담는 텐서였는데, 연산을 수행한 후에 실수가 담겨져서 나오는 에러이다. R과는 사뭇다른 예민한 아이 `torch`를 위해 형을 바꿔준 후에 연산을 실행하도록 하자.
```{r}
torch_sqrt(A$to(dtype = torch_double()))
torch_log(B)
```
### 텐서의 곱셈
텐서의 곱셈 역시 모양이 맞아야 하므로, 3행 2열이 두개가 붙어있는 C에서 앞에 한장을 떼어내도록 하자.
```{r}
B
D <- C[1,,]
D
```
텐서의 곱셈은 `torch_matmul()` 함수를 사용한다.
```{r}
# 파이프 사용해도 무방하다.
# B %>% torch_matmul(D)
torch_matmul(B, D)
```
토치의 텐서 곱셈은 다음과 같은 방법들도 있으니 알아두자.
```{r}
torch_mm(B, D)
B$mm(D)
B$matmul(D)
```
### 텐서의 전치(transpose)
전치(transpose)는 주어진 텐서를 뒤집는 것인데, 다음의 문법 구조를 가지고 있다.
```{r, eval=FALSE}
torch_transpose(input, dim0, dim1)
```
`dim0`, `dim1`는 바꿀 차원을 의미한다. '바꿀 차원은 두 개 밖에 없지 않나?' 라고 생각할 수 있다. 2 차원 텐서의 경우에는 그렇다. 우리가 행렬을 전치하는 경우에는 transpose를 취하는 대상이 2차원이므로 지정해주는 차원이 정해져있다. 하지만, 텐서의 차원이 3차원 이상이 되면 전치를 해주는 차원을 지정해줘야한다.
```{r}
A
```
위의 텐서 A의 차원은 행과 열, 즉, 2개이다. 다음의 코드들은 A 텐서의 첫번째 차원과 두번째 차원을 뒤집는 효과를 가져온다. 즉, 전치 텐서가 된다.
```{r}
torch_transpose(A, 1, 2)
A$transpose(1, 2)
A %>% torch_transpose(1, 2)
```
3차원의 텐서를 살펴보자.
```{r}
C
```
텐서 C는 위와 같이 2차원 텐서가 두 개 포개져 있다고 생각하면 된다. 텐서의 결과물을 잘 살펴보면, 제일 앞에 위치한 1, 2가 나타내는 것이 우리가 흔히 생각하는 2차원 텐서들의 색인(index) 역할을 한다는 것을 알 수 있다. 앞으로는 편의를 위해서 3차원 텐서의 색인 역할을 하는 차원을 깊이(depth)라고 부르도록 하자. 앞에서 주어진 텐서 C 안의 포개져있는 2차원 텐서들을 전치하기 위해서는 이들을 관할(?)하는 두번째와 세번째 차원을 바꿔줘야 한다.
```{r}
torch_transpose(C, 2, 3)
```
결과를 살펴보면, 잘 바뀌어 있음을 알 수 있다.
### R에서의 3차원 배열
앞에서 다룬 `torch`에서의 3차원 텐서 부분은 [R에서 기본적으로 제공하는 array의 문법과 차이가 난다.](https://rstudio.github.io/reticulate/articles/arrays.html) 다음의 코드를 살펴보자. 먼저 R에서 2행 3열의 행렬을 두 개 포개어 놓은 3차원 배열을 만드는 코드이다.
```{r}
array(1:12, c(2, 3, 2))
```
필자는 참고로 `matrix()`를 만들때에도 `byrow` 옵션을 써서 만드는 것을 좋아하는데, `array()`에서 `byrow` 옵션 효과를 적용하려면 `aperm()` 함수를 사용해야 한다. 따라서, 좀 더 직관적으로 쓰기위해서 다음의 함수를 사용하자.
```{r}
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
```
이러한 코드를 앞서 배웠던 `torch_tensor()` 함수에 넣어보자.
```{r}
E %>% torch_tensor()
```
결과를 살펴보면, 우리가 예상했던 2행 3열의 텐서가 두개 겹쳐있는 텐서의 모양이 나오지 않는다는 것을 알 수 있다. 이유는 `torch`에서 정의된 3차원 텐서의 경우, 첫번째 차원이 텐서가 얼마나 겹쳐있는지를 나타내는 깊이(depth)를 나타내기 때문이다. 문제를 해결하기 위해서는 `aperm()` 사용해서 차원을 바꿔주면 된다.
```{r}
E %>%
aperm(c(3, 1, 2)) %>% # 3 번째 차원을 맨 앞으로, 나머지는 그대로
torch_tensor()
```
위의 경우를 좀더 직관적인 함수명으로 바꿔서 사용하도록 하자.
```{r}
array_to_torch <- function(mat, n_dim = 3){
torch_tensor(aperm(mat, c(n_dim:3, 1, 2)))
}
E <- array_to_torch(E)
E
```
### 다차원 텐서와 1차원 벡터 텐서의 연산
R에서 우리가 아주 애용하는 기능 중 하나가 바로 `recycling` 개념이다. 즉, 길이 혹은 모양이 맞지 않는 개체(object)들을 연산할 때, 자동으로 길이와 모양을 맞춰서 연산을 해주는 기능인데, torch에서도 이러한 기능을 제공한다. 다음의 코드를 살펴보자.
```{r}
A
A + torch_tensor(1:3)
```
```{r}
A
A + torch_tensor(matrix(2:3, ncol = 1))
```
### 1차원 텐서 끼리의 연산, 내적과 외적
1차원 텐서끼리의 연산도 2차원 텐서끼리의 연산과 마찬가지라고 생각하면 된다. 내적과 외적 역시 그냥 모양을 맞춰서 곱하면 된다.
```{r}
A_1 <- A$view(c(1, -1))
A_1
A_2 <- A$view(c(-1, 1))
A_2
A_1$mm(A_2)
A_2$mm(A_1)
```
한가지 주의할 점은 1차원 텐서끼리의 연산이더라도 꼭 차원을 선언해줘서 열벡터와 행벡터를 분명히 해줘야 한다는 점이다.
```{r, eval=FALSE}
A_3 <- torch_tensor(1:6)
A_1$mm(A_3)
```
위의 코드는 연산 에러가 나는데, 이유는 `A_3`의 모양이 `A_1`의 모양과 맞지 않기 때문이다.
```{r}
A_1$size()
A_3 <- torch_tensor(1:6)
A_3$size()
```