UPDATE: 2022-09-04 20:55:22
このノートではRを使ってRで勾配降下法を可視化する方法をまとめておく。下記を大いに参考にさせていただいた。
まずは必要なライブラリを読み込んでおく。
library(tidyverse)
library(numDeriv)
library(gganimate)
library(plot3D)
関数\(f(x)= x^4 - 3x^3 + x^2 + 3x + 1\)を使って、勾配法を可視化する。
<- seq(-1, 2, by = .01)
x <- function(x) x^4 - 3*x^3 + x^2 + 3*x + 1
f <- f(x)
y <- tibble(x = x, y = y)
df df
## # A tibble: 301 × 2
## x y
## <dbl> <dbl>
## 1 -1 3
## 2 -0.99 2.88
## 3 -0.98 2.77
## 4 -0.97 2.65
## 5 -0.96 2.55
## 6 -0.95 2.44
## 7 -0.94 2.34
## 8 -0.93 2.24
## 9 -0.92 2.14
## 10 -0.91 2.04
## # … with 291 more rows
## # ℹ Use `print(n = ...)` to see more rows
関数を可視化するスクリプト。
ggplot(df, aes(x, y)) +
geom_line() +
geom_segment(
data = tibble(x = -0.443, xend = -0.443,y = -Inf, yend = f(-0.443)),
aes(x = x, y=y, xend=xend, yend = yend), col = "red"
+
) theme_classic()
x_0 = 0.5
を初期値として勾配法を可視化する。
<- 0.5
x_0 f(x_0)
## [1] 2.4375
グラフに初期値をプロットする。
ggplot(df, aes(x, y)) +
geom_line() +
geom_point(x = x_0, y = f(x_0), colour = "red") +
theme_classic()
\(x^{4}-3x^{3}+x^{2}+3x+1\)を\(x\)について微分して導関数を使って\(x = 0.5\)の値を計算する
\[ \begin{eqnarray} \displaystyle \frac{d}{dx}(x^{4} - 3x^{3} + x^{2} + 3x + 1) = 4 x^3 - 9 x^2 + 2 x + 3 \\ \end{eqnarray} \]
傾きはnumDeriv
パッケージのgrad
関数で計算できる。
# 上記の導関数にx=0.5の値を代入した結果
<- grad(func = f, x = c(x_0))
grad grad
## [1] 2.25
直線の方程式を式変形すれば、切片を計算できるので、切片を計算する。
# f(x) = intercept + grad * x_0を変形する
<- -grad*x_0 + f(x_0)
intercept intercept
## [1] 1.3125
\(x = 0.5\)における傾きを可視化する。この点での傾きが正なので、左側にすすめる。
ggplot(df, aes(x, y)) +
geom_line() +
geom_point(x = x_0, y = f(x_0), colour = "red") +
geom_abline(slope = grad, intercept = intercept, colour = "blue") +
theme_classic()
更新した値を使って、傾きと切片を求めると、傾きがまだ正なので、左に進めていく。
<- 0.2
learning_rate <- x_0 - learning_rate * grad
x_1 <- grad(func = f, x = c(x_1))
grad_x_1 <- -grad_x_1*x_1 + f(x_1)
intercept_x_1
ggplot(df, aes(x, y)) +
geom_line() +
geom_point(x = x_0, y = f(x_0), colour = "gray") +
geom_point(x = x_1, y = f(x_1), colour = "red") +
geom_abline(slope = grad_x_1, intercept = intercept_x_1, colour = "blue") +
theme_classic()
これを繰り返す。傾きが負になったので、次は右にすすめる。
<- x_1 - learning_rate * grad_x_1
x_2 <- grad(func = f, x = c(x_2))
grad_x_2 <- -grad_x_2*x_2 + f(x_2)
intercept_x_2
ggplot(df, aes(x, y)) +
geom_line() +
geom_point(x = x_0, y = f(x_0), colour = "gray") +
geom_point(x = x_1, y = f(x_1), colour = "gray") +
geom_point(x = x_2, y = f(x_2), colour = "red") +
geom_abline(slope = grad_x_2, intercept = intercept_x_2, colour = "blue") +
theme_classic()
これを繰り返すことで、関数の最小値を計算するアルゴリズムが勾配法。
<- x_2 - learning_rate * grad_x_2
x_3 <- grad(func = f, x = c(x_3))
grad_x_3 <- -grad_x_3*x_3 + f(x_3)
intercept_x_3
ggplot(df, aes(x, y)) +
geom_line() +
geom_point(x = x_0, y = f(x_0), colour = "gray") +
geom_point(x = x_1, y = f(x_1), colour = "gray") +
geom_point(x = x_2, y = f(x_2), colour = "gray") +
geom_point(x = x_3, y = f(x_3), colour = "red") +
geom_abline(slope = grad_x_3, intercept = intercept_x_3, colour = "blue") +
theme_classic()
これをアニメーションで可視化する。まずは可視化用のデータを作るために勾配の繰り返し処理を行う。
<- 200
iter # tolerance <- 10^-6
<- 0.01
eta
<- 0.5
x_1 # To keep track of the values through the iterations
<- x_1
x_1_values <- f(x_1)
y_1_values <- NULL
gradient_values <- NULL
intercept_values
for(i in 1:iter){
# Steepest ascent:
<- grad(func = f, x = c(x_1))
grad
<- -grad*x_1 + f(x_1)
intercept_value # Keeping track
<- c(gradient_values, grad)
gradient_values <- c(intercept_values, intercept_value)
intercept_values
# Updating the value
<- x_1 - eta * grad
x_1 <- f(x_1)
y_1
# Keeping track
<- c(x_1_values, x_1)
x_1_values <- c(y_1_values, y_1)
y_1_values
# Stopping if no improvement (decrease of the cost function too small)
# if(abs(y_1_values[i] - y_1 < tolerance)) break
}
可視化用のデータフレームを作成する。
<- tibble(
df_plot iter = 1:iter,
xx = seq(-1, 2, length.out = iter),
x_1 = x_1_values[-length(x_1_values)],
y = f(x_1),
gradient = gradient_values,
intercept = intercept_values
) head(df_plot)
## # A tibble: 6 × 6
## iter xx x_1 y gradient intercept
## <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 -1 0.5 2.44 2.25 1.31
## 2 2 -1 0.477 2.39 2.34 1.27
## 3 3 -1 0.454 2.33 2.43 1.23
## 4 4 -1 0.430 2.27 2.51 1.19
## 5 5 -1 0.405 2.21 2.60 1.15
## 6 6 -1 0.379 2.14 2.68 1.12
勾配降下法をアニメーションで可視化する。
ggplot(df_plot %>% filter(iter < 100)) +
stat_function(fun = f, aes(xx)) +
geom_point(aes(x = x_1, y = y), colour = "red") +
geom_abline(aes(slope = gradient, intercept = intercept), colour = "blue") +
geom_text(aes(x = x_1, y = y + 0.2, label = gradient)) +
theme_bw() +
transition_time(iter) +
labs(title = "Iteration: {frame_time}", x = 'x') +
ease_aes('elastic-in')
次は3次元の勾配降下法を可視化する。関数\(f(x)=x^2_{1} + x^2_{2}\)を可視化する。
<- x_2 <- seq(-2, 2, by = 0.3)
x_1 <- function(x_1,x_2) x_1^2+x_2^2
z_f <- outer(x_1, x_2, z_f)
z
par(mar = c(1, 1, 1, 1))
<- 1 # 1 or 2
flip = c(-300, 120)[flip]
th <- persp3D(x = x_1, y = x_2, z = z,
pmat colkey = FALSE, # 凡例
contour=TRUE, # 等高線
ticktype = "detailed", # 目盛り
asp = 1, phi = 30, theta = th,
border = "gray10", alpha = 0.4,
expand = .6, # 横広げる
axes = TRUE, box = TRUE
)
初期値を\(X=(1.5, 1.5)\)とする。赤色の点をプロットする。
<- c(x_1 = 1.5, x_2 = 1.5)
theta
<- z_f(theta[["x_1"]], theta[["x_2"]])
zz <- trans3d(theta[["x_1"]], theta[["x_2"]], zz, pmat = pmat)
new_point
<- persp3D(
pmat x = x_1, y = x_2, z = z,
colkey = FALSE, # 凡例
contour = TRUE, # 等高線
ticktype = "detailed", # 目盛り
asp = 1,
phi = 30,
theta = th,
border = "gray10",
alpha = 0.4,
expand = 0.6, # 横広げる
axes = TRUE,
box = TRUE
)points(new_point, pch = 20, col = "red", cex = 2)
更新式は下記の通りなので、
\[ \begin{eqnarray} \begin{bmatrix} x^{(t+1)}_{1} \\ x^{(t+1)}_{2} \end{bmatrix} = \begin{bmatrix} x^{(t)}_{1} \\ x^{(t)}_{2} \end{bmatrix} - \eta \begin{bmatrix} \frac{ \partial f }{ \partial x_{1} }(x^{(t)}_{1}, x^{(t)}_{2}) \\ \frac{ \partial f }{ \partial x_{2} }(x^{(t)}_{1}, x^{(t)}_{2}) \end{bmatrix} \end{eqnarray} \]
初期値を更新するために勾配を計算する。
<- 0.1
learning_rate <- function(theta){
z_f_to_optim <- theta[["x_1"]]
x_1 <- theta[["x_2"]]
x_2 ^2 + x_2^2
x_1
}<- grad(func = z_f_to_optim, x = theta)
grad grad
## [1] 3 3
初期値を更新する。
<- theta[["x_1"]] - learning_rate * grad[1]
updated_x_1 <- theta[["x_2"]] - learning_rate * grad[2]
updated_x_2 <- c(x_1 = updated_x_1, x_2 = updated_x_2)
updated_theta updated_theta
## x_1 x_2
## 1.2 1.2
新しいz
を計算する。
<- z_f(updated_theta[["x_1"]], updated_theta[["x_2"]])
updated_zz <- trans3d(updated_theta[["x_1"]],
new_point_2 "x_2"]],
updated_theta[[
updated_zz,pmat = pmat)
new_point_2
## $x
## [1] 0.2423082
##
## $y
## [1] -0.07318442
これを使って、更新した点をプロットする。
<- persp3D(
pmat x = x_1, y = x_2, z = z,
colkey = FALSE, # 凡例
contour = TRUE, # 等高線
ticktype = "detailed", # 目盛り
asp = 1,
phi = 30,
theta = th,
border = "gray10",
alpha = 0.4,
expand = 0.6, # 横広げる
axes = TRUE,
box = TRUE
)points(new_point,pch = 20,col = "black", cex=2)
points(new_point_2,pch = 20,col = "red", cex=2)
これを2次元のときと同じ用に、繰り返して勾配法を実行する。
<- 0.1
eta <- 50
iter #tolerance <- 10^-5
# Starting values
<- c(x_1 = 1.5, x_2 = 1.5)
theta
# To keep track of what happens at each iteration
<- list(theta)
theta_values <- z_f_to_optim(theta)
y_values
for(i in 1:iter){
# Steepest ascent
<- grad(func = z_f_to_optim, x = theta)
grad
# Updating the parameters
<- theta[["x_1"]] - eta * grad[1]
updated_x_1 <- theta[["x_2"]] - eta * grad[2]
updated_x_2 <- c(x_1 = updated_x_1, x_2 = updated_x_2)
theta
# Keeping track
<- c(theta_values, list(theta))
theta_values
# Checking for improvement
<- z_f_to_optim(theta)
y_updated <- c(y_values, y_updated)
y_values
# if(abs(y_values[i] - y_updated) < tolerance) break
}
勾配法の実行過程を可視化する。
par(mar = c(1, 1, 1, 1))
<- 1 # 1 or 2
flip = c(-300,120)[flip]
th <- persp3D(
pmat x = x_1, y = x_2, z = z,
colkey = FALSE, # 凡例
contour = TRUE, # 等高線
ticktype = "detailed", # 目盛り
asp = 1,
phi = 30,
theta = th,
border = "gray10",
alpha = 0.2,
expand = 0.6, # 横広げる
axes = TRUE,
box = TRUE
)
<- map_dbl(theta_values, "x_1")
xx <- map_dbl(theta_values, "x_2")
yy <- y_values
zz <- trans3d(xx, yy, zz, pmat = pmat)
new_point lines(new_point, pch = 20, col = "red", cex = 1, lwd = 2)
points(new_point, pch = 20, col = "red", cex = 1)
アニメーションさせる場合は下記を利用する。
descent_2D_sphere
# library(animation)
# saveGIF({
# for (j in c(rep(1, 5), 2:(i - 1), rep(i, 10))) {
# par(mar = c(1, 1, 1, 1))
# flip <- 1 # 1 or 2
# th = c(-300, 120)[flip]
# pmat <- persp3D(
# x = x_1, y = x_2, z = z,
# colkey = FALSE, # 凡例
# contour = TRUE, # 等高線
# ticktype = "detailed", # 目盛り
# asp = 1,
# phi = 30,
# theta = th,
# border = "gray10",
# alpha = 0.2,
# expand = 0.6, # 横広げる
# axes = TRUE,
# box = TRUE
# )
#
# xx <- map_dbl(theta_values, "x_1")[1:j]
# yy <- map_dbl(theta_values, "x_2")[1:j]
# zz <- y_values[1:j]
# new_point <- trans3d(xx, yy, zz, pmat = pmat)
# lines(
# new_point,
# pch = 20,
# col = "red",
# cex = 2,
# lwd = 2
# )
# points(new_point,
# pch = 20,
# col = "red",
# cex = 2)
# }
#
# },
# movie.name = "~/Desktop/descent_2D_sphere.gif",
# interval = 0.01,
# ani.width = 720,
# ani.height = 480)