はじめに
黄金比の定義や性質、黄金比を利用した図形やアルゴリズムについて、数式やプログラム、図を用いて理解を目指すシリーズです。
この記事では、三分探索について図を使って解説します。
【他の内容】
【今回の内容】
三分探索の可視化
三分割探索(ternary search)のアルゴリズムを図で確認します。
効率的な三分探索である黄金分割探索については「【Python】黄金分割探索の可視化 - からっぽのしょこ」を参照してください。
利用するライブラリを読み込みます。
# ライブラリを読込 import numpy as np import matplotlib.pyplot as plt
関数の設定
まずは、最小値(または最大値)の探索対象とする関数を設定します。この記事では、最小値の探索のみを扱います。
自作関数を作成します。
# 関数を作成 def eval_fnc(x): # 計算式を指定 y = 0.5 * x**2 - 4.0 * x + 12 return y
下に凸の関数(計算式)を指定します。上に凸の関数の場合は、-y
を出力すると(簡易的に)扱えます。
この例では、 とします。
変数の範囲を指定して、曲線の座標を作成します。
# 変数の範囲を指定 x_min = -5.0 x_max = 15.0 # 曲線用の変数を作成 x_vec = np.linspace(start=x_min, stop=x_max, num=1001) # 関数曲線を計算 f_x_vec = eval_fnc(x_vec)
変数 の範囲を指定して、関数 を計算します。
関数のグラフを作成します。
# ラベル用の文字列を作成 fnc_label = '$f(x) = 0.5 x^2 - 4 x + 12$' # 関数曲線を作図 fig, ax = plt.subplots(figsize=(8, 6), dpi=100, facecolor='white', constrained_layout=True) ax.plot(x_vec, f_x_vec, color='black', label=fnc_label) # 関数 ax.set_xlabel('$x$') ax.set_ylabel('$f(x)$') fig.suptitle('convex function', fontsize=20) ax.grid() ax.legend() plt.show()
この関数(曲線) の最小値となる変数 を求めます。
ちなみにこの例の関数の場合は、平方完成により解析的に求まります。
式変形(クリックで展開)
のとき最小値 になります。
三分割の図
次は、三分割の操作を図で示します。
分割比率を設定します。
# 分割比を指定 r = 1.0 / 3.0 print(r)
0.3333333333333333
三分探索では、三等分する2つの分割比 を用います。
1回目の試行における分割点を作成します。
# 探索範囲を設定 x_lower = x_min x_upper = x_max # 探索範囲を計算 x_size = x_upper - x_lower print(x_size) # 分割点を計算 x1 = x_lower + r * x_size x2 = x_lower + (1.0 - r) * x_size print(x1) print(x2)
20.0
1.666666666666666
8.333333333333336
変数 の下限を 、上限を としたとき、探索範囲のサイズは です。
探索範囲全体(上限と下限の差)を として、下限からのサイズ比が になるような2点で3分割します。2点は次の式で計算できます。
分割比が であれば2点は の関係(図で言うと の点が左側)です。
1回目の試行における三分割をグラフで確認します。
作図コード(クリックで展開)
# グラフサイズを設定 u = 10 y_max = np.ceil(f_x_vec.max() /u)*u # u単位で切り上げ y_min = -0.1 * y_max # 線分の表示位置の調整値を指定 d = 2.0 # ラベル用の文字列を作成 fml_label = f'$r = {r:.3f}, 1-r = {1.0-r:.3f}$' # 三分割を作図 fig, ax = plt.subplots(figsize=(8, 6), dpi=100, facecolor='white', constrained_layout=True) ax.plot(x_vec, f_x_vec, color='black') # 関数 ax.vlines(x=[x_lower, x_upper], ymin=y_min, ymax=y_max, color='black', linestyle='dashed') # 探索範囲 ax.vlines(x=[x1, x2], ymin=y_min, ymax=y_max, color=['red', 'purple'], linestyle='dashed') # 分割値 ax.hlines(y=0.0, xmin=x_lower, xmax=x_upper, color='black', label='$1$') # 線分 1 ax.hlines(y=[-d, -d*2], xmin=[x_lower, x2], xmax=[x1, x_upper], color='red', label='$r$') # 線分 r ax.hlines(y=[-d*2, -d], xmin=[x_lower, x1], xmax=[x2, x_upper], color='purple', label='$1-r$') # 線分 1-r ax.set_xlabel('$x$') ax.set_ylabel('$f(x)$') ax.set_title(fml_label, loc='left') fig.suptitle('ternary section', fontsize=20) ax.grid() ax.legend(loc='upper left') ax.set_ylim(ymin=y_min, ymax=y_max) plt.show()
探索範囲における変数 の下限 と上限 を黒色の破線、2つの分割点 を赤色と紫色の破線で示します。
探索範囲全体のサイズ( から までのサイズ・黒色の実線)を とすると、 のとき、下限または上限から小さい(近い)方の分割サイズ( から までのサイズと から までのサイズ・赤色の実線)との比が 、大きい(遠い)方の分割サイズ( から までのサイズと から までのサイズ・紫色の実線)との比が になります。
よって、 のとき、各点間のサイズ( から まで、 から まで、 から までのサイズ)が等しくなり、探索範囲が三等分されます。
2回目の試行における分割点を作成します。
# 探索範囲の上限を更新 #x_lower = x1 x_upper = x2 # 探索範囲を計算 x_size = x_upper - x_lower print(x_size) # 分割点を計算 x3 = x_lower + r * x_size x4 = x_lower + (1.0 - r) * x_size print(x3) print(x4)
13.333333333333336
-0.5555555555555554
3.888888888888891
の場合は、下限を に更新 します。
の場合は、上限を に更新 します。この例だと、こちらの操作を行います。
1試行目と同様に、更新した探索範囲全体を として、下限からのサイズ比が になるような2点で3分割します。2点は先ほどの式で計算できます。
分割比が であれば2点は の関係(図で言うと の点が左側)です。
2回目の試行における黄金分割をグラフで確認します。
作図コード(クリックで展開)
# 線分の表示位置の調整値を指定 d = 2.0 # 三分割を作図 fig, ax = plt.subplots(figsize=(8, 6), dpi=100, facecolor='white', constrained_layout=True) ax.plot(x_vec, f_x_vec, color='black') # 関数 ax.vlines(x=[x_lower, x_upper], ymin=y_min, ymax=y_max, color='black', linestyle='dashed') # 探索範囲 ax.vlines(x=[x3, x4], ymin=y_min, ymax=y_max, color=['blue', 'gold'], linestyle='dashed') # 分割値 ax.hlines(y=0.0, xmin=x_min, xmax=x_max, color='black', label='$1$') # 線分 1 ax.hlines(y=0.0-d, xmin=x_lower, xmax=x_upper, color='purple', label='$1-r$') # 線分 1-r ax.hlines(y=[-d*2, -d*3], xmin=[x_lower, x4], xmax=[x3, x_upper], color='blue', label='$r (1-r)$') # 線分 r (1-r) ax.hlines(y=[-d*3, -d*2], xmin=[x_lower, x3], xmax=[x4, x_upper], color='gold', label='$(1-r)^2$') # 線分 (1-r)^2 ax.set_xlabel('$x$') ax.set_ylabel('$f(x)$') ax.set_title(fml_label, loc='left') fig.suptitle('ternary section', fontsize=20) ax.set_ylim(ymin=y_min, ymax=y_max) ax.grid() ax.legend(loc='upper left') plt.show()
この試行の探索範囲における変数 の下限 と上限 を黒色の破線、2つの分割点 を青色と黄色の破線で示します。
探索範囲全体のサイズ( から までのサイズ・紫色の実線)を とすると、 のとき、下限または上限から小さい(近い)方の分割サイズ( から までのサイズと から までのサイズ・青色の実線)との比が 、大きい(遠い)方の分割サイズ( から までのサイズと から までのサイズ・黄色の実線)との比が になります。
よって、 のとき、各点間のサイズ( から まで、 から まで、 から までのサイズ)が等しくなり、探索範囲が三等分されます。
元の探索範囲のサイズ(黒色の実線)を とすると、探索範囲全体のサイズ( から までのサイズ・紫色の実線)との比が なので、小さい方の分割サイズとの比が 、大きい方の分割サイズとの比は になります。
三等分に分割を行いながら探索を行います。
三分探索の図
続いて、三分探索の操作を図で示します。
1回目の試行における曲線上の点の座標を作成します。
# 関数値を計算 f_x1 = eval_fnc(x1) f_x2 = eval_fnc(x2) print(f_x1) print(f_x2)
6.722222222222223
13.3888888888889
2つの分割点 の関数の値 を計算します。
1回目の試行における三分探索をグラフで確認します。
作図コード(クリックで展開)
# グラフサイズを設定 u = 10 y_min = 0.0 y_max = np.ceil(f_x_vec.max() /u)*u # u単位で切り上げ # ラベル用の文字列を作成 fnc_label = f'$f(x_1) = {f_x1:.5f}, f(x_2) = {f_x2:.5f}$' # 三分探索を作図 fig, ax = plt.subplots(figsize=(8, 6), dpi=100, facecolor='white', constrained_layout=True) ax.plot(x_vec, f_x_vec, color='black') # 関数 ax.vlines(x=[x_min, x_max], ymin=y_min, ymax=y_max, color='black', linestyle='dashed') # 探索範囲 ax.vlines(x=[x1, x2], ymin=y_min, ymax=y_max, color=['red', 'purple'], linestyle='dashed') # 分割値 ax.scatter(x=x1, y=f_x1, s=100, color='red', label='$(x_1, f(x_1))$') # 分割点 1 ax.scatter(x=x2, y=f_x2, s=100, color='purple', label='$(x_2, f(x_2))$') # 分割点 2 ax.hlines(y=[f_x1, f_x2], xmin=x_min, xmax=[x1, x2], color=['red', 'purple'], linestyle='dotted') # 分割点の値 ax.set_xlabel('$x$') ax.set_ylabel('$f(x)$') ax.set_title(fnc_label, loc='left') fig.suptitle('ternary search', fontsize=20) ax.set_xlim(xmin=x_min, xmax=x_max) ax.set_ylim(ymin=y_min, ymax=y_max) ax.grid() ax.legend(loc='upper left') plt.show()
2つの分割点 に対応する曲線上の点 を比較して、小さい方の点を残し、大きい方の点を次の試行の下限または上限(この例だと を上限)とします。
2回目の試行における曲線上の点の座標を作成します。
# 関数値を計算 f_x3 = eval_fnc(x3) f_x4 = eval_fnc(x4) print(f_x3) print(f_x4)
14.376543209876543
4.006172839506172
新たな分割点 の関数の値 を計算します。
2回目の試行における黄金分割探索をグラフで確認します。
作図コード(クリックで展開)
# ラベル用の文字列を作成 fnc_label = f'$f(x_3) = {f_x3:.5f}, f(x_4) = {f_x4:.5f}$' # 三分探索を作図 fig, ax = plt.subplots(figsize=(8, 6), dpi=100, facecolor='white', constrained_layout=True) ax.plot(x_vec, f_x_vec, color='black') # 関数 ax.vlines(x=[x_lower, x_upper], ymin=y_min, ymax=y_max, color='black', linestyle='dashed') # 探索範囲 ax.vlines(x=[x3, x4], ymin=y_min, ymax=y_max, color=['blue', 'gold'], linestyle='dashed') # 分割値 ax.scatter(x=x3, y=f_x3, s=100, color='blue', label='$(x_3, f(x_3))$') # 分割点 3 ax.scatter(x=x4, y=f_x4, s=100, color='gold', label='$(x_4, f(x_4))$') # 分割点 4 ax.hlines(y=[f_x3, f_x4], xmin=x_min, xmax=[x3, x4], color=['blue', 'gold'], linestyle='dotted') # 分割点の値 ax.set_xlabel('$x$') ax.set_ylabel('$f(x)$') ax.set_title(fnc_label, loc='left') fig.suptitle('ternary search', fontsize=20) ax.set_xlim(xmin=x_min, xmax=x_max) ax.set_ylim(ymin=y_min, ymax=y_max) ax.grid() ax.legend(loc='upper left') plt.show()
1試行目と同様に、2つの分割点 に対応する曲線上の点 を比較して、小さい方の点を残し、大きい方の点を次の試行の下限または上限(この例だと を下限)とします。
三等分による分割と更新の操作を繰り返すことで、下に凸な関数の最小値を探索します。
探索の推移
アニメーションの作図コードは「Mathematics/golden_ratio/code/ternary_search.py at main · anemptyarchive/Mathematics · GitHub」を参照してください。
三分探索の推移をアニメーションで確認します。
各試行における変数 の探索範囲の下限 と上限 を黒色の破線、分割点 をそれぞれ色付きの点と破線で示します。
試行ごとに、探索範囲を三等分し、 の大きい方の点(変数の値)で下限または上限を更新します。
この操作を繰り返すことで、変数の範囲を狭めながら最小値の探索を行えます。
この記事では、三分探索を確認しました。次の記事では、黄金分割探索を確認します。
参考文献
(この本に三分探索は載っていません。この記事を書くきっかけになった黄金分割探索の関連でリンクしています。)
おわりに
三分割を図で説明する必要があるかというとないのですが、目的の黄金分割探索の記事と同じ構成にして比較するために、こちらの記事でもやっておきました。想定して書き始めたわけではないですが、三等分以外も作図できる(ように対応した)ので、作った甲斐が後から生まれた思われます。
派生記事の補足記事的に生まれた記事なので、既存カテゴリに該当せず、新たに「アルゴリズム」内の「探索」としたのですが今後このカテゴリの記事は生えるのでしょうか。
【次の内容】