データフィッティングにおける損失関数と堅牢性

コード例:堅牢な損失関数の選択と最適化

2021年9月21日公開

著者:Caleb Hamilton

共著者:George Tsikas

nAGライブラリのインストール

このブログのコードを実行するには、nAGライブラリ for Pythonとライセンスキーをインストールする必要があります。

はじめに

非線形モデルをデータにフィッティングする問題は、通常、最小化問題としてモデル化されます。ここで、目的関数はパラメータに依存するモデルのデータへのフィット品質の測定値として機能します。一般的なモデルでは、データポイントに対して総和を取ります:

\[\begin{align} \text{最小化} \quad & f(x) = \sum_{i=1}^{n_\text{res}} \chi(r_i(x)) \\ \text{制約条件} \quad & x \in \mathbb{R}^{n_\text{var}} \end{align}\]

ここで、\(x\) はモデルパラメータを保持するベクトルで、\(n_\text{var}\) 個のパラメータがあります。\(n_\text{res}\) 個のデータポイントがあり、\(r_i(x) = y_i - \varphi(t_i; x)\)\(i = 1, \ldots, n_\text{res}\)\(i\) 番目の残差で、時刻 \(t_i\) における観測値と予測値の差を表します。ここで、\(y_i\) は観測値、\(\varphi(t_i; x)\) は予測値です。損失関数 \(\chi\) は、下から有界で \(|r_i(x)|\) とともに増加するなどの望ましい性質を持ちます。すべてのデータポイントに対して総和を取ると、目的関数はモデルが全データセットにうまくフィットしているときに小さくなります。これが私たちの望むところです。

関数 \(\chi\) には多くの選択肢がありますが、損失関数の選択はどのようにフィッティング結果に影響するのでしょうか?重要な考慮事項の1つは堅牢性です。観測されたデータポイントのいくつかがフィットされたモデルから大きく外れている場合、そのような外れ値の影響をどのように制御できるでしょうか?堅牢な損失関数とは、データ中の外れ値に簡単に影響されないものです。

import numpy as np
import matplotlib.pyplot as plt
from naginterfaces.base import utils
from naginterfaces.library import opt

単一外れ値の例

堅牢性の側面を調査するために、\(\sin(t)\) から生成されたおもちゃのデータセットを用意しました。このデータセットには \(t = 1.5\) に外れ値があり、これは \(5\sin(t)\) から生成されています。

t = np.linspace(0.5, 2.5, num=21)
y = np.sin(t)
y[10] = 5*np.sin(t[10])
fig1 = plt.plot(t,y,'*b')

このデータを以下のモデルでフィッティングします:

\(\varphi(t; x) = x_1 \sin(x_2 t)\)

nAGのデータフィッティングソルバー handle_solve_nldf (e04gn) が提供する様々な損失関数を使用します。このソルバーは適切な目的関数を自動的に構築します。

handle = opt.handle_init(nvar=nvar)
nres = 21
opt.handle_set_nlnls(handle, nres)
def lsqfun(x, nres, inform, data):
    rx = np.zeros(nres,dtype=float)
    t = data["t"]
    y = data["y"]
    for i in range(nres):
        rx[i] = (y[i] - x[0]*np.sin(x[1]*t[i]))
        
    return rx, inform

def lsqgrd(x, nres, rdx, inform, data):
    t = data["t"]
    nvar = len(x)
    for i in range(nres):
        rdx[i*nvar] = (-np.sin(x[1]*t[i]))
        rdx[i*nvar + 1] = (-t[i]*x[0]*np.cos(x[1]*t[i]))

    return inform
data = {}
data["t"] = t
data["y"] = y

\(l_2\)-ノルム損失関数から始める – 例1

最も一般的な損失関数の1つである \(l_2\)-ノルムから始めて、以下の問題を形成します:

\[\underset{x \in \mathbb{R}^{2}}{\text{最小化}}~f(x) = \sum_{i=1}^{21} r_i(x)^2\]

これは単なる最小二乗回帰です。\(l_2\)-ノルム損失は外れ値に対する堅牢性が低いので、解がこの1つの外れ値によって大きく影響されることが予想されます。初期点を

\(x = (2.1, 1.4)\)

として、この外れ値が最小値にどのような影響を与えるか見てみましょう。

for option in [
    'NLDF Loss Function Type = L2',
    'Print Level = 1',
    'Print Options = No',
    'Print solution = Yes'
]:
    opt.handle_opt_set(handle, option)

iom = utils.FileObjManager(locus_in_output=False)
x = [2.1, 1.4]
soln1 = opt.handle_solve_nldf(
    handle, lsqfun, lsqgrd, x, nres, data=data, io_manager=iom)

E04GN、非線形データフィッティング
状態:収束、最適解が見つかりました
最終目的関数値 1.470963E+01

プライマル変数:
idx 下限 値 上限
1 -inf 1.30111E+00 inf
2 -inf 1.06956E+00 inf

y_l2_fitted = soln1.x[0]*np.sin(soln1.x[1]*t)
plt.title("L2損失関数でフィット")
plt.plot(t,y,'*b')
plt.plot(t,y_l2_fitted)
plt.show()

単一の外れ値がフィッティングを乱すことができました。これは、\(l_2\)-ノルム損失が外れ値を目的関数と探索方向に大きく寄与させるためです。

\(l_1\)-ノルム損失関数を試す – 例2

\(l_1\)-ノルム損失を使用すると、以下の問題が得られます:

\(\underset{x \in \mathbb{R}^{2}}{\text{最小化}}~f(x) = \sum_{i=1}^{21} |r_i(x)|,\)

これは外れ値に対してより堅牢です。つまり、データの大部分がある解 \(x^\ast\) によってうまくフィットされる場合、\(x^\ast\) に非常に近い局所最小値が存在する可能性が高く、その局所最小値は \(x^\ast\) に対して外れ値となる残りのデータにほとんど影響されません。ここで、再び \(x = (2.1, 1.4)\) から始めて、\(l_1\) 損失を使用した解を見てみましょう。

opt.handle_opt_set(handle, 'NLDF Loss Function Type = L1')
soln2 = opt.handle_solve_nldf(
    handle, lsqfun, lsqgrd, x, nres, data=data, io_manager=iom)

E04GN、非線形データフィッティング
状態:収束、最適解が見つかりました
最終目的関数値 3.989980E+00

プライマル変数:
idx 下限 値 上限
1 -inf 1.00000E+00 inf
2 -inf 1.00000E+00 inf

y_l1_fitted = soln2.x[0]*np.sin(soln2.x[1]*t)
plt.title("L1損失関数でフィット")
plt.plot(t,y,'*b')
plt.plot(t,y_l1_fitted)
plt.show()

明らかに、これはデータの大部分にとってはるかに良いフィットです。外れ値がモデルをデータの大部分から引き離すことはありませんでした。

損失関数のトレードオフ

ハンドル、残差関数(および勾配)を再利用できます。データとオプションを変更するだけで、損失関数に関するより多くの原則を示すことができます。

非常に堅牢な損失関数を選択することには危険があります。反復的な最適化プロセスの間、外れ値に対して堅牢な損失関数は通常、現在のモデルに近いデータを好みます。これは、アルゴリズムが目的関数の局所最小値を見つける場合、モデルがデータのある部分集合に非常によくフィットしているが、データの大部分に非常に悪くフィットしている局所最小値に陥る可能性があることを意味します。

これを説明するために、同じモデルで再び \(x = (2.1, 1.4)\) から始めて、新しいデータセットをフィットしてみましょう。データの大部分は \(5\sin(t)\) から生成され、両端の3つのデータポイントは \(\sin(t)\) から生成されています。

y_new = y
for i in range(3,18):
    y_new[i] = 5*np.sin(t[i])
plt.plot(t,y_new,'*b')
plt.show()
data_new = {}
data_new["t"] = t
data_new["y"] = y_new

このデータセットを3つの異なる損失関数を使用して、同じモデル \(\varphi(t; x)\) で毎回フィットし、結果をプロットの下でまとめて議論します。

\(l_2\)-ノルム、\(l_1\)-ノルム、およびアークタン損失関数でモデルをフィット

loss_functions = ['L2', 'L1', 'ATAN']

opt.handle_opt_set(handle, 'print file = -1')

for lfunc in loss_functions:
    
    # 損失関数のオプションを設定して解く
    opt.handle_opt_set(handle, 'NLDF Loss Function Type =' + lfunc)
    soln = opt.handle_solve_nldf(
        handle, lsqfun, lsqgrd, x, nres, data=data_new, io_manager=iom)
    # フィットした曲線をプロット
    plt.plot(t, soln.x[0]*np.sin(soln.x[1]*t), label=lfunc)

plt.plot(t,y_new,'*b')
plt.title("様々な損失関数でフィット")
plt.legend()
plt.show()

フィットされたモデルと等高線プロット

最初の行のプロットでは、\(l_2\)-ノルム損失、\(l_1\)-ノルム損失、および \(\arctan\) 損失を使用してデータがフィットされています。各プロットの下には目的関数値の等高線プロットが示されており、黒い円はデータを生成するのに使用されたパラメータを表し、赤い十字はソルバーが見つけた最適パラメータを表しています。

結論

要約すると、損失関数の選択はデータフィッティング問題におけるフィットの堅牢性と品質に大きな影響を与えます。\(l_2\)-ノルム損失関数は外れ値に敏感である一方、\(l_1\)-ノルムと \(\arctan\) 損失関数はより堅牢性を提供します。しかし、より堅牢な損失関数は時として、データの全体的な傾向をうまく表現しない局所最小値に陥ることがあります。データと問題の特性に基づいて適切な損失関数を選択することが非常に重要です。

堅牢な損失関数のデータフィッティングにおける技術的側面についての詳細や更に深い考察については、nAGウェブサイトで入手可能な完全なレポートと追加リソースを参照してください。

関連ページ:データフィッティングにおける損失関数とそのロバスト性の説明

参考文献:

  1. [Edirisinghe, N. C. P. “インデックストラッキングの最適ポートフォリオ選択” Quantitative Finance Letters, Vol. 1, 16-20, (2013).]

タグ: データフィッティング、損失関数、堅牢性、最適化、nAGライブラリ

関連情報
MENU
Privacy Policy  /  Trademarks