Rでブレント法

Rで ブレント法 を試みます。

本ポストはこちらの続きです。

Rではさみうち法
Rで はさみうち法 を試みます。本ポストはこちらの続きです。1. はさみうち法とははさみうち法(False Position Method または Regula Falsi)は、二分法と同じく方程式 f(x) = 0 の解を区間で挟みながら...

1. ブレント法とは

ブレント法(Brent’s Method)は、リチャード・ブレントによって開発された、方程式 f(x) = 0 の解を求めるためのアルゴリズムです。この方法は、これまで見てきた二分法の確実性(頑健性)と、はさみうち法や逆2次補間法の速さという、それぞれの長所を組み合わせたハイブリッドなアプローチを取ります。

ブレント法の構成要素

ブレント法は、以下の3つの手法を使い分けます。

  1. 二分法 (Bisection Method)

    • 区間を機械的に半分に狭めていく、最も信頼性の高い方法。
    • 収束は遅いですが、必ず解に近づくことが保証されています。
    • ブレント法では、他の高速な手法がうまく機能しない場合の「安全装置」として機能します。
  2. はさみうち法 (Secant Method / False Position)

    • 区間の両端を結ぶ直線とx軸の交点を次の点とします。
    • 二分法より高速ですが、関数の形状によっては収束が遅くなる弱点があります。
  3. 逆2次補間法 (Inverse Quadratic Interpolation)

    • これは、はさみうち法をさらに発展させた考え方です。3つの既知の点 (x1, y1), (x2, y2), (x3, y3) を通る放物線を考え、その放物線がx軸と交わる点を次の探索点とします。
    • ただし、通常の y = ax^2 + bx + c という形の放物線ではなく、xとyを入れ替えた x = ay^2 + by + c という形の「横向きの放物線」を使います。これにより、y=0 を代入するだけで次の探索点 x=c を計算できます。
    • 解の近くでは極めて高速に収束しますが、3点が解から遠いと不安定になることがあります。

アルゴリズムの選択メカニズム

  1. 基本方針: 可能な限り最速の手法である逆2次補間法を試みます。これが使えない(点が3つ揃っていないなど)場合は、次に速いはさみうち法を試みます。

  2. 妥当性のチェック: 高速な手法で計算された次の探索点 s が、本当に「良い」点かどうかをチェックします。

    • その点 s は、現在の探索区間内にちゃんと収まっているか?
    • 収束が遅くなっていないか?(例えば、前回や前々回の二分法のステップと比較して、区間の縮小幅が十分か?)
  3. 安全装置の発動: もし上記のチェックで s が「良くない」と判断された場合、その結果は棄却され、代わりに最も安全な二分法が強制的に実行されます。

このメカニズムにより、ブレント法は「普段は高速な手法で一気に解に近づき、問題が起きそうになったら安全な二分法に切り替えて着実に進む」という、効率的で安定した動作を実現します。

2. R言語によるシミュレーションコードと実行結果

それでは、これまでと同じ方程式 f(x) = x^3 - x - 1 = 0 をブレント法で解くシミュレーションを行います。アルゴリズムが各ステップでどの手法を選択するかに注目してください。

(注意)ブレント法の完全な実装は複雑なため、ここではそのエッセンス(手法の切り替え)を理解することに主眼を置いた、簡略化されたコードで示します。

# 必要なライブラリを読み込みます
library(ggplot2)
library(dplyr)

# -----------------------------------------------------------------
# 1. シミュレーションの対象となる関数を定義 (二分法、はさみうち法と同じ)
# -----------------------------------------------------------------
# f(x) = x^3 - x - 1
f <- function(x) {
  return(x^3 - x - 1)
}

# -----------------------------------------------------------------
# 2. ブレント法のシミュレーションを行う関数を定義
# -----------------------------------------------------------------
brent_simulation <- function(func, a, b, tol = 1e-9, max_iter = 30) {
  fa <- func(a)
  fb <- func(b)

  if (fa * fb >= 0) {
    stop("初期区間[a, b]でf(a)とf(b)の符号が同じです。")
  }

  # b を常に最良近似解とするための入れ替え
  if (abs(fa) < abs(fb)) {
    temp <- a
    a <- b
    b <- temp
    temp <- fa
    fa <- fb
    fb <- temp
  }

  c <- a # c は前のb (contrapoint)
  fc <- fa
  d <- b - a # d は今回のステップ幅
  e <- d # e は前回のステップ幅

  cat("--- ブレント法シミュレーション開始 ---\n\n")
  cat(sprintf("対象関数: x^3 - x - 1\n"))
  cat(sprintf("初期区間: [%.1f, %.1f], 許容誤差: %e\n\n", a, b, tol))

  history <- data.frame(
    iter = integer(), a = double(), b = double(),
    s = double(), fs = double(), method = character()
  )

  for (i in 1:max_iter) {
    # bが最良近似解なので、f(b)が0に十分近ければ収束
    if (abs(fb) < tol || b == a) {
      cat(sprintf("\n--- 収束しました (反復 %d 回) ---\n\n", i - 1))
      break
    }

    # 3点が異なる場合、逆2次補間を試す
    if (fa != fc && fb != fc) {
      s <- (a * fb * fc / ((fa - fb) * (fa - fc))) +
        (b * fa * fc / ((fb - fa) * (fb - fc))) +
        (c * fa * fb / ((fc - fa) * (fc - fb)))
      method <- "逆2次補間"
    } else {
      # そうでなければ、はさみうち法を試す
      s <- b - fb * (b - a) / (fb - fa)
      method <- "はさみうち法"
    }

    # ----- 妥当性チェックと安全装置 -----
    # 1. sが区間の外 or 収束が遅すぎるか?
    condition1 <- (s < (3 * a + b) / 4 || s > b)
    # 2. 前回のステップ幅eに比べて今回のステップ幅dが十分小さくなっていないか?
    condition2 <- abs(s - b) >= abs(e / 2)
    # 3. 前回のステップ幅eが許容誤差より小さい(ほぼ収束している)か?
    condition3 <- abs(e) < tol
    # 4. 今回のステップ幅dが許容誤差より小さいか?
    condition4 <- abs(d) < tol

    # 上記のいずれかに該当する場合、計算したsを棄却して二分法に切り替える
    if (condition1 || condition2 || condition3 || condition4) {
      s <- (a + b) / 2
      d <- s - b
      e <- d
      method <- "二分法" # 手法を上書き
    } else {
      e <- d
      d <- s - b
    }

    fs <- func(s)

    cat(sprintf("【反復 %d回目】使用手法: %s\n", i, method))
    cat(sprintf("  探索点 s = %.8f, f(s) = %.2e\n", s, fs))

    history <- rbind(history, data.frame(
      iter = i, a = a, b = b, s = s, fs = fs, method = method
    ))

    # 次の反復のための変数の更新
    c <- b
    fc <- fb

    if (fa * fs < 0) {
      b <- s
      fb <- fs
    } else {
      a <- s
      fa <- fs
    }

    # bが最良近似解であり続けるように、必要ならaとbを入れ替える
    if (abs(fa) < abs(fb)) {
      temp <- a
      a <- b
      b <- temp
      temp <- fa
      fa <- fb
      fb <- temp
    }
  }

  if (i == max_iter) {
    cat("\n--- 最大反復回数に達しました ---\n")
  }

  cat(sprintf("最終的な解の近似値 c = %.8f\n", b))
  cat(sprintf("そのときの関数値 f(c) = %e\n", fb))

  return(history)
}

# -----------------------------------------------------------------
# 3. シミュレーションの実行
# -----------------------------------------------------------------
simulation_history_brent <- brent_simulation(f, 1.0, 2.0)

# -----------------------------------------------------------------
# 4. ggplot2による結果の可視化
# -----------------------------------------------------------------

# 最終的な解の値を取得
final_solution_value <- tail(simulation_history_brent, 1)$s

brent_process_plot <- ggplot(simulation_history_brent, aes(x = iter, y = s)) +

  # 真の解の位置を水平線で示す
  geom_hline(yintercept = final_solution_value, linetype = "dashed", color = "red", linewidth = 1) +
  geom_text(aes(x = 0.5, y = final_solution_value, label = sprintf("真の解\n(約 %.6f)", final_solution_value)),
    color = "red", hjust = 0, vjust = -0.2
  ) +

  # 近似解の推移を線で結ぶ
  geom_line(color = "gray50", linewidth = 1) +

  # 各ステップの近似解と使用した手法を点と形で示す
  geom_point(aes(shape = method, color = method), size = 5, stroke = 1.5) +

  # 手法ごとに色と形を明示的に設定
  scale_color_manual(
    name = "使用された手法",
    values = c("二分法" = "#00BFC4", "はさみうち法" = "#F8766D", "逆2次補間" = "#7CAE00")
  ) +
  scale_shape_manual(
    name = "使用された手法",
    values = c("二分法" = 1, "はさみうち法" = 2, "逆2次補間" = 0)
  ) +

  # 軸ラベルとタイトル
  labs(
    title = "ブレント法の収束プロセス",
    subtitle = "各反復で近似解がどのように真の解に収束するか",
    x = "反復回数 (iter)",
    y = "近似解 (s) の値"
  ) +
  scale_x_continuous(breaks = 1:nrow(simulation_history_brent)) + # X軸の目盛りを整数に
  theme_minimal(base_family = "sans") +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5),
    legend.position = "bottom"
  )

# 新しいプロットを表示
print(brent_process_plot)
--- ブレント法シミュレーション開始 ---

対象関数: x^3 - x - 1
初期区間: [2.0, 1.0], 許容誤差: 1.000000e-09

【反復 1回目】使用手法: 二分法
  探索点 s = 1.50000000, f(s) = 8.75e-01
【反復 2回目】使用手法: はさみうち法
  探索点 s = 1.26666667, f(s) = -2.34e-01
【反復 3回目】使用手法: 二分法
  探索点 s = 1.38333333, f(s) = 2.64e-01
【反復 4回目】使用手法: 二分法
  探索点 s = 1.32500000, f(s) = 1.20e-03
【反復 5回目】使用手法: はさみうち法
  探索点 s = 1.32470208, f(s) = -6.77e-05
【反復 6回目】使用手法: 二分法
  探索点 s = 1.32485104, f(s) = 5.68e-04
【反復 7回目】使用手法: 二分法
  探索点 s = 1.32477656, f(s) = 2.50e-04
【反復 8回目】使用手法: 二分法
  探索点 s = 1.32473932, f(s) = 9.11e-05
【反復 9回目】使用手法: 二分法
  探索点 s = 1.32472070, f(s) = 1.17e-05
【反復 10回目】使用手法: はさみうち法
  探索点 s = 1.32471796, f(s) = -1.73e-10

--- 収束しました (反復 10 回) ---

最終的な解の近似値 c = 1.32471796
そのときの関数値 f(c) = -1.730169e-10
Figure 1

注目点: 二分法(17回)、はさみうち法(15回)に比べ、ブレント法は10回で収束しています。「逆2次補間」は一度も使われず「はさみうち法」と、「二分法」を頻繁に切り替えながら計算を進めています。これは、はさみうち法で計算された次の探索点が、ブレント法の内部的な妥当性チェック(例えば、「収束が十分に速くない」、「探索点が区間内の適切な範囲にない」など)に適合しなかったことを示しています。

Figure 1 の読み方

  • 横軸: シミュレーションの反復回数です。
  • 縦軸: 各ステップで計算された近似解 s の値です。
  • 赤い破線: この方程式の真の解(最終的な収束値)を示します。
  • 点と線:

    • 点は各ステップでの近似解 s の位置を示します。
    • 点の色と形で、そのステップでどの手法が使われたかがわかります(凡例参照)。

      • ○ (円): 二分法が使用されたステップ。プロット上で最も頻繁に見られます。
      • △ (三角形): はさみうち法が使用されたステップ。2回目、5回目、そして最後の10回目に使われています。
    • 灰色の線は、近似解がどのように推移していったかを示します。

以上です。