scipy.optimizeで最適化問題を解く入門

scipy.optimizeで最適化問題を解く入門

データサイエンスや機械学習の世界では、「最適な値を見つける」 という作業が頻繁に登場します。

たとえば……

  • 広告費をどう配分すれば利益が最大になるか?
  • 機械学習モデルの誤差を最小にするパラメータは何か?

……といった問題です。

こうした 「ある条件のもとで、最も良い値を見つける問題」最適化問題(さいてきかもんだい) と呼びます。

今回は、SciPyの scipy.optimize モジュールに含まれる minimize() 関数を使って、最適化問題をPythonで解く方法を解説します。

最適化問題とは?

 日常にある「最適化」

実は、私たちは日常生活の中でも無意識に「最適化」をしています。

  • 買い物 → 予算内でできるだけ満足度が高くなるように商品を選ぶ
  • 通勤 → できるだけ時間が短くなるルートを選ぶ
  • 勉強 → 限られた時間で最も効率よく点数が上がる科目に集中する

これらはすべて、「ある目的をできるだけ良くする(最大化・最小化する)」という最適化問題です。

 

 データサイエンスでの最適化

データサイエンスの世界では、最適化はさらに重要な役割を担います。

  • 機械学習 → モデルの予測誤差を最小にするパラメータを見つける
  • マーケ → 広告費の配分を最適化して売上を最大化する
  • 物流 → 配送ルートを最適化してコストを最小化する
  • 金融 → リスクを抑えつつリターンを最大化するポートフォリオを組む

これらの問題を、人間の勘ではなく数学的・アルゴリズム的に解くのが最適化の技術です。

 

最適化の基本用語

最適化問題を理解するために、3つの基本用語を押さえておきましょう。

 ① 目的関数(もくてきかんすう)

目的関数 とは、最小化(または最大化)したい対象を数式で表したものです。「コスト関数」「損失関数」とも呼ばれます。

以下、例です。

問題:「予測誤差を最小にしたい」
目的関数:予測値と実際の値の差の2乗の合計(=誤差を数式にしたもの)
問題:「コストを最小にしたい」
目的関数:材料費 + 人件費 + 輸送費(=コストを数式にしたもの)

 

最適化とは、この目的関数の値をできるだけ小さく(または大きく)する変数の値を見つけることです。

 

 ② 変数(パラメータ)

変数 とは、目的関数の中で自由に調整できる値のことです。

この値を変えることで、目的関数の値が変わります。

y = (x - 3)² + 2

  • この式で x が「変数」、y が「目的関数の値」
  • x の値を変えると y の値が変わる
  • y が最小になる x を見つけるのが最適化

 

 ③ 最小値と最適解

最小値 は目的関数が取る最も小さい値、最適解 はそのときの変数の値です。

y = (x - 3)^2 + 2

  • x = 3 のとき y = (3-3)^2 + 2 = 2 ← これが最小値
  • 最適解x = 3
  • 最小値y = 2
補足:最小化と最大化

scipy.optimize.minimize() は名前のとおり 最小化 を行う関数です。もし「最大化」したい場合は、目的関数にマイナスをつけて最小化問題に変換します。たとえば「利益を最大化」したい場合は、「-利益を最小化」として解きます。この変換テクニックは最適化の世界でよく使われます。

 

minimize() の基本的な使い方

 最もシンプルな例

まず、簡単な数式の最小値を見つけてみましょう。

次の関数を考えます。

目的関数f(x) = (x - 3)^2 + 2

この関数は、x = 3 のとき最小値 2 を取ります。

これをSciPyで求めてみましょう。

以下、コードです。

from scipy.optimize import minimize

# 目的関数を定義
def objective(x):
    return (x - 3) ** 2 + 2

# 最適化を実行(初期値 x=0 からスタート)
result = minimize(objective, x0=0)

print(result)
  • from scipy.optimize import minimize:SciPyの optimize モジュールから minimize 関数を読み込みます
  • def objective(x)::目的関数を Python の関数として定義します。objective は「目的」という意味の名前です(名前は自由に変えられます)
  • return (x - 3) ** 2 + 2:(x – 3)の2乗 + 2 を計算して返します。** はPythonの累乗演算子です
  • result = minimize(objective, x0=0)minimize() に目的関数と初期値を渡して最適化を実行します
  • x0=0:探索の出発点(初期値)です。最適化は、この値から出発して「もっと小さくなる方向」を探していきます

 

以下、実行結果です。

  message: Optimization terminated successfully.
  success: True
   status: 0
      fun: 2.000000000000001
        x: [ 3.000e+00]
      nit: 2
      jac: [ 5.960e-08]
 hess_inv: [[ 5.000e-01]]
     nfev: 6
     njev: 3

 

たくさんの情報が表示されましたが、重要な部分だけ見ていきましょう。

項目 意味
message 最適化の結果メッセージ “Optimization terminated successfully.”(成功)
success 最適化が成功したか True(成功)
fun 目的関数の最小値 2.0(ほぼ正確に2)
x 最適解(最小値を取る変数の値) 3.0(ほぼ正確に3)
nit 反復回数(何回探索を繰り返したか) 2回
nfev 関数の評価回数(目的関数を何回計算したか) 6回

 

minimize() の結果から、よく使う値を個別に取り出す方法を確認しておきましょう。

以下、コードです。

from scipy.optimize import minimize

def objective(x):
    return (x - 3) ** 2 + 2

result = minimize(objective, x0=0)

# 必要な値を個別に取り出す
print(f"最適化成功? : {result.success}")
print(f"最適解 x     : {result.x[0]:.4f}")
print(f"最小値 f(x)  : {result.fun:.4f}")
  • result.success:最適化が成功したかどうか(True / False)
  • result.x[0]:最適解。result.x は配列なので、[0] で最初の値を取り出します
  • result.fun:目的関数の最小値
  • :.4f:小数点以下4桁で表示するフォーマット指定です

 

以下、実行結果です。

最適化成功? : True
最適解 x     : 3.0000
最小値 f(x)  : 2.0000

 

minimize() に渡す 初期値(x0) は、最適化の結果に影響を与えることがあります。

 

 なぜ初期値が重要なのか?

minimize() は、初期値から出発して「坂を下るように」目的関数が小さくなる方向を探索します。

しかし、目的関数に「山と谷」が複数ある場合、初期値によってたどり着く谷が異なることがあります。

用語 意味
局所的最小値(きょくしょてき) 「近くの範囲では最小」だが、全体で見ると最小ではない値
大域的最小値(たいいきてき) 全体を通して本当に最小の値(グローバル最小値)

 

以下、コードです。

import numpy as np
from scipy.optimize import minimize

# 複数の谷を持つ目的関数
def multi_valley(x):
    return x ** 4 - 8 * x ** 2 + 5

# 初期値を変えて最適化を実行
initial_values = [-1, 0, 1, 3]

for x0 in initial_values:
    result = minimize(multi_valley, x0=x0)
    print(
        f"初期値 x0={x0:+d} → "
        f"最適解 x={result.x[0]:+.4f}, "
        f"最小値 f(x)={result.fun:.4f}"
    )
  • def multi_valley(x):x^4 - 8x^2 + 5 という、複数の谷を持つ関数を定義しています
  • initial_values = [-1, 0, 1, 3]:4つの異なる初期値を試します
  • for x0 in initial_values::各初期値について最適化を実行し、結果を表示します
  • x0:+d:符号付きの整数で表示(+1, -1 のように)

 

以下、実行結果です。

初期値 x0=-1 → 最適解 x=-2.0000, 最小値 f(x)=-11.0000
初期値 x0=+0 → 最適解 x=+0.0000, 最小値 f(x)=5.0000
初期値 x0=+1 → 最適解 x=+2.0000, 最小値 f(x)=-11.0000
初期値 x0=+3 → 最適解 x=+2.0000, 最小値 f(x)=-11.0000

 

この関数には x = -2 と x = +2 の2つの谷があり、どちらも最小値 -11 です。初期値が -1 のときは x = -2 の谷に、1と3のときは x = +2 の谷にたどり着いています。

視覚的に確認してみましょう。

以下、コードです。

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize

# 目的関数
def multi_valley(x):
    return x ** 4 - 8 * x ** 2 + 5

# 描画用の範囲と値
xs = np.linspace(-4, 4, 400)
ys = multi_valley(xs)

# いくつかの初期値からの最適化を実行し、解を保存
initial_values = [-1, 0, 1, 3]
solutions = []
for x0 in initial_values:
    res = minimize(multi_valley, x0=x0)
    solutions.append((x0, res.x[0], res.fun))

# プロット
plt.figure(figsize=(7, 4))
plt.plot(
    xs, ys, 
    color='gray', lw=2,
    label='multi_valley(x)'
)

# 各初期値と対応する最適解を可視化
colors = ['tab:orange', 'tab:green', 'tab:red', 'tab:purple']
for (x0, x_opt, f_opt), c in zip(solutions, colors):
    # 初期点
    plt.scatter(
        [x0], [multi_valley(x0)], 
        color=c, marker='*', s=200,
        label=f'init x0={x0}'
    )
    # 局所的な最適解
    plt.scatter(
        [x_opt], [f_opt], 
        color=c, edgecolor='k', s=100, 
        label=f'solution x={x_opt:.2f}'
    )
    # 初期点から解への矢印
    plt.annotate('', 
        xy=(x_opt, f_opt), xytext=(x0, multi_valley(x0)),
        arrowprops=dict(
            arrowstyle='->', 
            linestyle='-.', 
            color=c, lw=2
        )
    )

plt.xlabel('x')
plt.ylabel('f(x)')
plt.ylim(-15, 30)
plt.legend(loc='best', fontsize=8, ncol=2)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

 

以下、実行結果です。

 

minimize() では、計算を始めるための初期値を指定します。この初期値によって、たどり着く結果が変わることがあります。

そのため、まずは複数の初期値を試して、結果を比べることが大切です。

また、データや問題の内容を踏まえて、不自然ではない範囲の初期値を置くようにします。

さらに、初期値を少し変えても同じような結果になるかを確認すると、その結果が安定しているかを判断できます。

 

複数の変数を持つ最適化

ここまでは変数が1つ(x だけ)の例でしたが、実際の問題では複数の変数を同時に最適化することが一般的です。

ある商品の利益が、「価格(x₁)」と「広告費(x₂)」によって以下のように決まるとします。

以下、コスト関数(最小化したい)です。
$$
f(x₁, x₂) = (x₁ – 5)^2 + (x₂ – 3)^2 + 10
$$

  • x_1 :価格パラメータ
  • x_2 :広告費パラメータ

最適解は(x_1 = 5, x_2 = 3) で最小値は 10 です。

以下、コードです。

from scipy.optimize import minimize

# 複数の変数を持つ目的関数
# x は配列として受け取る(x[0]が第1変数、x[1]が第2変数)
def cost_function(x):
    return (x[0] - 5) ** 2 + (x[1] - 3) ** 2 + 10

# 初期値:x1=0, x2=0 からスタート
initial_guess = [0, 0]

# 最適化を実行
result = minimize(cost_function, x0=initial_guess)

print(
    f"最適化成功\t{result.success}\n"
    "最適解    \t"
        f"x1={result.x[0]:.4f}, "
        f"x2={result.x[1]:.4f}\n"
    f"最小値   \t{result.fun:.4f}"
)
  • def cost_function(x)::複数の変数を持つ場合、x は配列(リスト)として受け取ります。x[0] が第1変数(価格)、x[1] が第2変数(広告費)です
  • initial_guess = [0, 0]:2つの変数の初期値をリストで指定します。変数の数に合わせて初期値の数も変わります
  • result.x[0], result.x[1]:最適化された各変数の値を取り出します

 

以下、実行結果です。

最適化成功	True
最適解    	 x1=5.0000, x2=3.0000
最小値   	 10.0000

 

最適解が見つかりました。

変数が2つでも3つでも、同じ minimize() の仕組みで解くことができます。

 

最適化アルゴリズム(method)の指定

minimize() は、内部でさまざまな 最適化アルゴリズム(解き方の手順) を使い分けることができます。

何も指定しなければ、minimize() は自動的に適切なアルゴリズムを選んでくれます。

初心者の方は、まずはデフォルトのままで問題ありません。

参考として、代表的なアルゴリズムを紹介します。

アルゴリズム名 特徴 向いている場面
Nelder-Mead 微分を使わないシンプルな方法 目的関数が複雑・微分が難しい場合
BFGS 微分情報を使って高速に探索 滑らかな関数(デフォルトに近い)
L-BFGS-B BFGSの省メモリ版、範囲制約に対応 変数が多い場合
Powell 微分を使わず方向ごとに探索 微分が計算できない場合

アルゴリズムを指定するときは、引数methodに利用するアルゴリズム名を渡します。

# Nelder-Mead法を指定する場合
result = minimize(objective, x0=0, method='Nelder-Mead')
補足:アルゴリズムの選び方

最初はデフォルト(指定なし)で試し、うまくいかない場合に他のアルゴリズムを試すのが実践的なアプローチです。minimize() は賢く設計されており、多くの場合はデフォルト設定で十分な結果が得られます。

 

機械学習との関係

ここまで見てきた最適化は、実は機械学習のとても大事な部分です。

機械学習は、おおまかにいうと次のような流れで進みます。

  • ① データを用意する
  • ② 予測に使う式やルールを決める
  • ③ 予測のズレをはかるしくみを決める
  • ④ そのズレができるだけ小さくなるように調整する
  • ⑤ 調整した結果を使って予測する

ここでいう「学習」とは、人間のように意味を考えて覚えることではありません。

予測のズレができるだけ小さくなるように、式の中の数字を少しずつ調整していくことです。

たとえば、「テスト勉強時間から点数を予測する式」を考えるとします。

最初は予測が外れていても、ズレが小さくなるように式を調整していけば、だんだん実際のデータに合った予測ができるようになります。

この「ズレを小さくする」という考え方が、最適化です。

今回学んだ minimize() も、まさにこの考え方を体験するための道具です。

機械学習のライブラリでは、こうした計算を内部で自動的に行って、よりよい予測ができるようにしています。

そのため、最適化の基本を理解すると、機械学習が何をしているのかをつかみやすくなります。

つまり、機械学習の「学習」とは、予測のズレをできるだけ小さくするように調整することだと考えるとよいでしょう。

 

まとめ

今回のまとめです。

  • 最適化問題 とは、目的関数を最小化(または最大化)する変数の値を見つける問題
  • SciPyの minimize() 関数で簡単に最適化を実行できる
  • minimize() に渡すのは 目的関数初期値(x0) の2つ
  • 初期値によって結果が変わることがあるため、複数の初期値を試すことが大切
  • 複数の変数がある問題にも同じ仕組みで対応できる
  • 最適化は機械学習の核心技術であり、理解しておくと学習の土台になる

次回は、「curve_fitでデータにフィッティングしてみよう」 をテーマに、実際のデータに曲線を当てはめる方法を学んでいきます。