R言語のuniroot徹底解説:非線形方程式の根を効率的に見つける方法
Rの uniroot
関数は、単一変数の非線形方程式の根(root、解)を数値的に見つけるための関数です。
もう少し詳しく説明します。
uniroot
とは何か?
uniroot
は、ある関数 f(x) が与えられたときに、f(x)=0 となるような x の値(根)を、指定された区間内で探索します。これは、多くの場合、厳密な解析解を求めるのが難しい場合に非常に役立ちます。
どのように機能するか?
uniroot
は、一般的に二分法(Bisection method)やそれに類似した方法(例えば、Brent法など)を用いて根を探索します。これらの方法は、指定された区間の両端で関数の符号が異なることを利用します。もし f(a) と f(b) の符号が異なる場合、a と b の間に少なくとも1つの根が存在するという「中間値の定理」に基づいています。
関数は、区間を徐々に狭めていき、最終的に許容誤差(tol)の範囲内で根を見つけ出します。
uniroot
の主な引数
uniroot(f, interval, ..., lower, upper, tol, maxiter)
maxiter
: 最大反復回数。この回数を超えると探索を中止します。tol
: 許容誤差。探索を停止する精度を指定します。デフォルトはtol = .Machine$double.eps^0.25
です。upper
: 区間の上限。lower
: 区間の下限。- :
f
関数に渡す追加の引数がある場合にここに指定します。 interval
: 根を探索する区間をc(lower, upper)
の形式で指定します。lower
とupper
の値は、f
の評価において符号が異なる必要があります(つまり、$f(lower) \* f(upper) \< 0$)。f
: 根を求めたい関数。これは、引数を1つ取る関数である必要があります。
使用例
例えば、f(x)=x2−2 の根(つまり sqrt2)を求める場合を考えます。
# 根を求めたい関数を定義
my_function <- function(x) {
x^2 - 2
}
# unirootを使って根を探索
# 探索区間は1から2の間(1^2 - 2 = -1, 2^2 - 2 = 2 なので符号が異なる)
result <- uniroot(my_function, interval = c(1, 2))
# 結果を表示
print(result)
# 根の値
print(result$root)
このコードを実行すると、result$root
に sqrt2 の近似値(約1.414214)が出力されます。
- 関数の連続性:
uniroot
は、対象の関数が指定された区間で連続であることを前提としています。 - 複数の根: 指定された区間に複数の根が存在する場合、
uniroot
はそのうちの1つ(通常は最初に見つかったもの)を返します。すべての根を見つけたい場合は、区間を分割して複数回実行するか、他の方法を検討する必要があります。 - 探索区間の指定:
uniroot
は、指定された区間の両端で関数の符号が異なることを前提としています。もし符号が異ならない場合、エラーが発生します。
f() values at end points not of opposite sign (端点での関数値が逆符号ではない)
これは最も頻繁に発生するエラーです。 uniroot
は、指定された区間 interval = c(lower, upper)
の両端 lower
と upper
で、関数 f(x)
の値の符号が異なっていることを前提としています。つまり、$f(lower) \* f(upper) \< 0$ でなければなりません。
原因
- 関数の定義ミス
関数f
の定義が間違っており、期待する振る舞いをしない場合。 - 根が存在するが、区間の端点での符号が同じ
例えば、関数が極小値または極大値を持ち、x軸に接している場合や、区間の両端が根の片側に位置している場合など。 - 根が区間に存在しない
指定した区間に実際に根が存在しない場合。
トラブルシューティング
- 関数の再確認
f
の定義が数学的に正しいか、引数の取り方などがRの期待するものと合っているかを確認します。 - extendInt 引数
uniroot
にはextendInt
という引数があり、デフォルトは"no"
ですが、これを"yes"
や"upX"
,"downX"
に設定することで、自動的に区間を拡張して根を探そうとします。ただし、これは万能ではなく、根が大きく外れた場所にある場合や、複数の根が存在する場合に予期せぬ結果を招く可能性もあります。# uniroot(f, interval = c(3, 5), extendInt = "yes") # これでエラー回避できる場合もある
- 区間の調整
プロットの結果を見て、根を挟むように区間interval
を調整します。 - 関数のプロット
最初に、対象となる関数f
を指定した区間でプロットしてみるのが最も効果的です。curve()
関数を使って、x軸との交点(根)がどこにあるか、そして区間の端点での符号がどうなっているかを確認します。# 例:f(x) = x^2 - 4 の根を探す f <- function(x) x^2 - 4 curve(f, from = -5, to = 5, col = "blue", lwd = 2) abline(h = 0, lty = 2) # x軸を引く # uniroot(f, interval = c(1, 3)) # f(1)=-3, f(3)=5 -> OK # uniroot(f, interval = c(3, 5)) # f(3)=5, f(5)=21 -> エラー (同符号)
no sign change found in <maxiter> iterations (指定した反復回数内で符号の変化が見つからない)
原因
- 関数の挙動が複雑
関数が非常に平坦な部分を持つなど、根を見つけにくい複雑な形状をしている場合。 - tol が小さすぎる / maxiter が少なすぎる
非常に小さいtol
(許容誤差) を設定しているのに、maxiter
(最大反復回数) が十分でない場合、収束する前に反復回数を超えてしまうことがあります。 - 根が存在しない
上記のエラーと同様に、区間に根が存在しないか、関数がx軸を横切らない場合。
トラブルシューティング
- 異なる初期区間
extendInt
を試すか、手動で異なる初期区間を与えてみます。 - tol の調整
厳密すぎる精度を求めていないのであれば、tol
を少し大きくすることで収束しやすくなる場合があります。 - maxiter の増加
maxiter
の値を増やして、より多くの反復を許可します。 - プロットでの確認
やはり関数のプロットで根が存在するかどうか、関数の振る舞いが穏やかかを確認します。
Error in f(lower) : argument "x" is missing, with no default (f(lower) でのエラー: 引数 "x" が見つからない)
原因
- 引数の使用ミス
f
に追加の引数を渡したい場合、uniroot(f, ..., interval = c(lower, upper), other_arg = value)
のように...
の部分に渡す必要があります。 - 関数 f の定義ミス
f
が引数を正しく受け取れていないか、uniroot
がf
に渡す引数名と、f
が期待する引数名が一致しない場合。uniroot
は第一引数としてx
を渡すと想定しています。
トラブルシューティング
- 追加引数の渡し方
f
が複数の引数を取る場合、uniroot
に渡す際は...
を使って正しい引数名を指定します。 - f の引数確認
f
が正しく単一の引数(通常x
)を受け取るように定義されているかを確認します。# 良い例 my_func <- function(x, a) { x^2 - a } uniroot(my_func, interval = c(0, 5), a = 4) # OK # 悪い例 (fがx以外の引数を期待しているのに、unirootがそれを渡さない場合) # my_func2 <- function(y) { y^2 - 4 } # uniroot(my_func2, interval = c(0, 5)) # エラーの可能性
uniroot が期待する根を見つけられない(エラーではないが問題)
原因
- 平坦な関数
関数が非常に平坦な部分を持つ場合、uniroot
が根を特定するのに苦労することがあります。 - 関数の不連続性
uniroot
は関数が連続であることを前提としています。不連続点がある場合、正しく動作しないことがあります。 - 根が区間の端点に非常に近い
浮動小数点数の精度により、厳密な根が区間の端点に非常に近い場合に、期待する結果と異なることがあります。 - 複数の根が存在する
uniroot
は指定された区間内で1つの根しか見つけません。複数の根がある場合、最初に発見されたものが返されます。
- 手動での探索
大まかな根の範囲が分かっている場合、optimize()
やnlm()
などの最適化関数と組み合わせて、根の周辺で最小値(関数の絶対値)を探索することも有効です。 - 他のパッケージの利用
より複雑な根探索が必要な場合、rootSolve
パッケージのmultiroot
など、多変数関数や複数の根を探索できる関数を検討します。 - tol の調整
厳密すぎるtol
が問題を悪化させている可能性もあります。 - プロットと複数の区間
関数のプロットで複数の根があることを確認し、それぞれの根を囲むような異なる区間を指定してuniroot
を複数回実行します。
- ドキュメントを読む
?uniroot
でヘルプページを参照し、引数の意味や例を確認します。 - エラーメッセージをよく読む
Rのエラーメッセージは、どこに問題があるかを示すヒントが含まれています。 - まずはプロット! どのような場合でも、まず関数の挙動を視覚的に理解することが重要です。
curve()
関数を積極的に使いましょう。
例1: 単純な多項式の根を求める
最も基本的な例として、f(x)=x2−4 の根を求めます。この関数の根は x=2 と x=−2 です。uniroot
は区間内の1つの根を見つけます。
# 1. 根を求めたい関数を定義する
f1 <- function(x) {
x^2 - 4
}
# 2. 関数をプロットして、根のあたりを視覚的に確認する(任意だが推奨)
curve(f1, from = -5, to = 5, col = "blue", lwd = 2,
main = "f(x) = x^2 - 4", xlab = "x", ylab = "f(x)")
abline(h = 0, lty = 2, col = "gray") # x軸(y=0)を示す線
# 3. uniroot を使用して根を探索する
# 探索区間は、f(lower) と f(upper) の符号が異なるように設定する
# 例えば、x=1のとき f(1) = 1^2 - 4 = -3
# x=3のとき f(3) = 3^2 - 4 = 5
# なので、[1, 3] の区間に根があることがわかる
result1 <- uniroot(f1, interval = c(1, 3))
# 4. 結果を表示する
print(result1)
# 結果から根の値だけを取り出す
cat("Found root:", result1$root, "\n")
cat("Value at root:", f1(result1$root), "\n\n")
# 別の根(-2)を探す場合
# 例えば、x=-3のとき f(-3) = (-3)^2 - 4 = 5
# x=-1のとき f(-1) = (-1)^2 - 4 = -3
result2 <- uniroot(f1, interval = c(-3, -1))
print(result2)
cat("Found root:", result2$root, "\n")
cat("Value at root:", f1(result2$root), "\n")
解説
result1$root
:uniroot
の結果はリストで返され、$root
要素に発見された根の値が含まれています。uniroot(f1, interval = c(1, 3))
:f1
関数に対して、[1, 3]
の区間で根を探すように指示しています。この区間では、f(1)=−3 と f(3)=5 で符号が異なるため、根が存在すると期待できます。curve()
: 関数をプロットすることで、根の大体の位置や、区間の両端での関数の符号を確認できます。f1 <- function(x) { x^2 - 4 }
:uniroot
に渡す関数は、根を探索したい変数(ここではx
)を引数として受け取ります。
例2: 複数の引数を持つ関数の根を求める
関数が根を探索したい変数 x
以外にも引数を取る場合、uniroot
の ...
引数を使って追加の引数を渡すことができます。
例えば、f(x)=ax−b の根を求めたい場合(ここで a と b は定数)。根は x=b/a です。
# 1. 複数の引数を持つ関数を定義する
f2 <- function(x, a, b) {
a * x - b
}
# 2. uniroot を使用して根を探索する
# a=2, b=6 の場合、根は x = 6/2 = 3 となるはず
# 探索区間は、f(x) の値が符号を変えるように設定 (例: f(0, 2, 6) = -6, f(5, 2, 6) = 4)
result3 <- uniroot(f2, interval = c(0, 5), a = 2, b = 6)
# 3. 結果を表示する
print(result3)
cat("Found root:", result3$root, "\n")
cat("Value at root:", f2(result3$root, a = 2, b = 6), "\n\n")
# 別のパラメータで試す (a=0.5, b=10) -> 根は x = 10/0.5 = 20
result4 <- uniroot(f2, interval = c(10, 30), a = 0.5, b = 10)
print(result4)
cat("Found root:", result4$root, "\n")
cat("Value at root:", f2(result4$root, a = 0.5, b = 10), "\n")
解説
uniroot(f2, interval = c(0, 5), a = 2, b = 6)
:uniroot
にa = 2, b = 6
という追加の引数を渡しています。これらの引数はf2
の呼び出し時に内部的に使用されます。f2 <- function(x, a, b) { a * x - b }
: この関数はx
以外にa
とb
という引数を取ります。
例3: より複雑な超越方程式の根を求める
例えば、f(x)=cos(x)−x の根を求めます。これは解析的に解くのが難しい方程式です。
# 1. 関数を定義する
f3 <- function(x) {
cos(x) - x
}
# 2. プロットで確認
curve(f3, from = -pi, to = pi, col = "darkgreen", lwd = 2,
main = "f(x) = cos(x) - x", xlab = "x", ylab = "f(x)")
abline(h = 0, lty = 2, col = "gray")
# x=0のとき f(0) = cos(0) - 0 = 1
# x=pi/2のとき f(pi/2) = cos(pi/2) - pi/2 = 0 - pi/2 = -pi/2
# なので、[0, pi/2] の間に根があることがわかる
result5 <- uniroot(f3, interval = c(0, pi/2))
print(result5)
cat("Found root:", result5$root, "\n")
cat("Value at root:", f3(result5$root), "\n")
解説
interval
を適切に設定することで、唯一の根(約 0.739)を見つけることができます。- この例では、
cos(x) - x = 0
となるx
を探しています。このような超越方程式はuniroot
が特に役立つ場面です。
例4: 許容誤差 (tol) と最大反復回数 (maxiter) の調整
tol
は、探索を停止する際の根の精度を指定します。maxiter
は、探索の最大反復回数を設定します。
# 1. 関数を定義 (例1と同じ)
f1 <- function(x) {
x^2 - 4
}
# 2. より高い精度で根を探索する
result_high_tol <- uniroot(f1, interval = c(1, 3), tol = 1e-10) # デフォルトより厳しい
cat("High precision root:", result_high_tol$root, "\n")
cat("Value at root (high precision):", f1(result_high_tol$root), "\n\n")
# 3. 非常に少ない反復回数で試す(エラーが発生する可能性あり)
# この例では収束が速いので、エラーにならないかもしれませんが、複雑な関数ではエラーになります
# result_low_iter <- uniroot(f1, interval = c(1, 3), maxiter = 2) # maxiterが少なすぎる例
# print(result_low_iter) # エラー "no sign change found in 2 iterations" が出ることがある
解説
maxiter
: この値を小さくしすぎると、関数が収束する前に反復回数の上限に達し、エラー(no sign change found in <maxiter> iterations
)が発生することがあります。tol = 1e-10
: 根がこの許容誤差内に収まると探索を停止します。デフォルトよりも小さい値に設定することで、より厳密な根を求めることができます。
extendInt
引数は、指定された区間で符号の変化が見つからない場合に、自動的に区間を拡張して根を探そうとします。
# 1. 関数を定義 (例1と同じ)
f1 <- function(x) {
x^2 - 4
}
# 2. 根を含まない区間を与える(通常はエラーになる区間)
# f(3) = 5, f(5) = 21 -> 両方プラスなので通常はエラー
# result_error <- uniroot(f1, interval = c(3, 5)) # これを実行するとエラーになる
# 3. extendInt を使用して区間を拡張させる
# "yes" は両方向に拡張、"downX" は下限方向、"upX" は上限方向に拡張
result_extended <- uniroot(f1, interval = c(3, 5), extendInt = "yes")
cat("Extended interval root:", result_extended$root, "\n")
cat("Value at root (extended):", f1(result_extended$root), "\n")
extendInt = "yes"
: この設定により、uniroot
は自動的に区間を広げて、根を見つけようとします。ただし、これは万能ではなく、複数の根がある場合や関数の挙動が複雑な場合には、予期せぬ結果を返す可能性があるため、注意が必要です。
uniroot.all (rootSolveパッケージ) - 複数の根を探す場合
uniroot
の最大の制約の1つは、指定された区間に複数の根が存在する場合でも、1つの根しか見つけられないことです。rootSolve
パッケージの uniroot.all
関数は、この問題を解決し、指定された区間内のすべての根を見つけようとします。
特徴
- 通常、区間を細かく分割して
uniroot
を複数回実行するような内部処理を行っている。 - 区間内の複数の根を探索できる。
使用例
f(x)=x3−6x2+11x−6 の根を求めます。この多項式の根は x=1,x=2,x=3 です。
# rootSolve パッケージをインストールしていない場合は、以下を実行
# install.packages("rootSolve")
library(rootSolve)
# 根を求めたい関数を定義
f_multi <- function(x) {
x^3 - 6*x^2 + 11*x - 6
}
# プロットで根の位置を確認
curve(f_multi, from = 0, to = 4, col = "purple", lwd = 2,
main = "f(x) = x^3 - 6x^2 + 11x - 6", xlab = "x", ylab = "f(x)")
abline(h = 0, lty = 2, col = "gray")
# uniroot.all を使用してすべての根を探索
# n は区間を分割するセグメントの数を指定(大きいほど精度は上がるが計算コストも増える)
all_roots <- uniroot.all(f_multi, interval = c(0, 4), n = 100)
print(all_roots)
# 結果: [1] 1 2 3 (近似値)
最適化関数 (optimize, nlm, optim など) - 関数の最小値/最大値を探索する場合
厳密には根を探す関数ではありませんが、f(x)=0 という問題を ∣f(x)∣ の最小値を探索する問題として捉えることができます。関数の絶対値が0になるところが根です。これは、根が区間に存在しないが、最も0に近い値を求めたい場合などにも有効です。
optim()
: 一般的な最適化ルーチン(多変数関数にも対応)。nlm()
(Nonlinear Minimization): 非線形最小化のための関数(より複雑な場合に)。optimize()
: 1次元の関数の最小値または最大値を探索します。
使用例
f(x)=x2−4 の根を、|f(x)|
の最小値として探す。
# 最小化したい関数(元の関数の絶対値)
f_abs <- function(x) {
abs(x^2 - 4)
}
# optimize を使用して最小値を探索
# interval: 探索区間
min_result <- optimize(f_abs, interval = c(0, 5))
print(min_result)
cat("Root (from optimization):", min_result$minimum, "\n")
cat("Value at root (from optimization):", f_abs(min_result$minimum), "\n")
# 注意: optimizeはf(x)が0になることを保証するわけではない。
# 例えば、f(x)が常に正で、最小値が0ではない場合もある。
# この場合はf(x) = 0となるxが存在しないことを示唆する。
解説
optimize
は指定された区間内で関数の最小値を見つけます。|f(x)|
の最小値が0に非常に近い場合、その x
の値が根である可能性が高いです。これは、uniroot
が f(lower)
と f(upper)
の符号が異なることを要求するのに対し、optimize
はその制約がないため、異なるアプローチを提供します。
Newton-Raphson法などの自作実装
特定のニーズがある場合や、アルゴリズムの挙動を完全に制御したい場合は、Newton-Raphson法やSecant法などの根探索アルゴリズムを自分で実装することも可能です。
Newton-Raphson法
xn+1​=xn​−f′(xn​)f(xn​)​
(f′(x) は f(x) の導関数)
特徴
- 初期値の選択が重要で、悪い初期値だと収束しない場合や、目的の根以外の根に収束する場合がある。
- 導関数 f′(x) が必要。
- 収束が非常に速い場合がある(特に初期値が根に近い場合)。
使用例
f(x)=x2−2 の根をNewton-Raphson法で求める。導関数は f′(x)=2x。
# 根を求めたい関数
f_newton <- function(x) {
x^2 - 2
}
# 関数の導関数
df_newton <- function(x) {
2 * x
}
# Newton-Raphson法の実装
newton_raphson <- function(f, df, initial_guess, tol = 1e-7, max_iter = 100) {
x_n <- initial_guess
for (i in 1:max_iter) {
f_val <- f(x_n)
df_val <- df(x_n)
# 導関数が0に近いと問題になる
if (abs(df_val) < .Machine$double.eps) {
warning("Derivative is too close to zero.")
return(NA)
}
x_n_plus_1 <- x_n - f_val / df_val
if (abs(x_n_plus_1 - x_n) < tol) {
return(x_n_plus_1)
}
x_n <- x_n_plus_1
}
warning("Maximum iterations reached without convergence.")
return(NA)
}
# 実行
root_newton <- newton_raphson(f_newton, df_newton, initial_guess = 1.5)
cat("Newton-Raphson root:", root_newton, "\n")
cat("Value at root (Newton-Raphson):", f_newton(root_newton), "\n")
解説
Newton-Raphson法は、与えられた初期値から接線をたどり、x軸との交点を次の近似値とします。収束が速い反面、導関数を定義する必要があり、初期値の選び方や導関数の値によって挙動が不安定になる可能性があります。
uniroot
は任意の非線形関数に適用できますが、対象が多項式に限定される場合は、Rの基本関数 polyroot()
が非常に便利です。polyroot()
は、多項式のすべての根(実数根だけでなく複素数根も含む)を見つけることができます。
使用例
x3−6x2+11x−6=0 の根を求める。
# 多項式の係数を降べきの順にベクトルで指定
# x^3 - 6x^2 + 11x - 6 -> c(-6, 11, -6, 1)
# (定数項, xの係数, x^2の係数, x^3の係数...)
coeffs <- c(-6, 11, -6, 1)
# polyroot を使用して根を探索
polynomial_roots <- polyroot(coeffs)
print(polynomial_roots)
# 結果: [1] (1+0i) (3+0i) (2+0i)
# 複素数形式で表示されるが、虚部が0なので実数根であることがわかる
解説
polyroot
は、多項式の係数をベクトルとして受け取り、その多項式のすべての根を複素数の形式で返します。非常に効率的で、多項式に特化しているため、uniroot
や uniroot.all
を使うよりも適している場合があります。
代替方法 | 用途 | メリット | デメリット |
---|---|---|---|
uniroot.all | 指定区間内のすべての根を探索 | 複数の根を効率的に見つけられる | uniroot より計算コストがかかる場合がある |
最適化関数 (optimize など) | ` | f(x) | ` の最小化として根を探索 |
自作実装 (Newton-Raphson) | 特定のアルゴリズムを完全に制御したい場合 | 収束が速い場合がある(適切な初期値で) | 導関数が必要、初期値の選択が重要、不安定になることも |
polyroot() | 多項式のすべての根を探索 | すべての根(複素数根も含む)を効率的に見つける | 多項式に限定される |