[Python] 最小二乗法によるデータの区分的多項式近似

目次

理論

多項式の最小二乗法

データ\(\{(x_n, y_n)\}_{n=1}^N\)をP次多項式\(f(x)=c^{(P)}x^P + c^{(P-1)}x^{P-1} + \cdots + c^{(1)}x + c^{(0)}\)で近似したい。


関数によるデータの近似の例

最小二乗法では、多項式の値とデータの値との差の2乗の和の最小化によって多項式近似を行う。

\begin{align} \mathrm{minimize} \quad \mathrm{Error} &= \frac{1}{2} \sum_{n=1}^N \left( y_n - f(x_n) \right)^2 \\ \end{align}

各係数\(c^{(p)}\)でErrorを偏微分した値が0になるという条件から解いていくことで、\(c^{(p)}\)を求めることができる。

\begin{align} 0 &= \frac{\partial \mathrm{Error}}{\partial c^{(p)}} \\ &= \frac{1}{2} \sum_{n=1}^N \frac{\partial}{\partial c^{(p)}} \left( y_n - f(x_n) \right)^2 \\ &= - \sum_{n=1}^N \left( y_n - f(x_n) \right) \frac{\partial f(x_n)}{\partial c^{(p)}} \\ &= - \sum_{n=1}^N \left( y_n - f(x_n) \right) \frac{\partial}{\partial c^{(p)}} \left( c^{(P)}x_n^P + c^{(P-1)}x_n^{P-1} + \cdots + c^{(1)}x_n + c^{(0)} \right) \\ &= - \sum_{n=1}^N \left( y_n - f(x_n) \right) x_n^p \\ &= - \sum_{n=1}^N y_nx_n^p + \sum_{n=1}^N x_n^pf(x_n) \\ &= - \sum_{n=1}^N y_nx_n^p + \sum_{n=1}^N x_n^p \sum_{q=0}^P c^{(q)}x_n^q \\ &= - \sum_{n=1}^N y_nx_n^p + \sum_{q=0}^P \left( \sum_{n=1}^N x_n^{p+q} \right) c^{(q)} \\ \end{align}

これをまとめると、

\begin{align} \begin{pmatrix} \sum_{n=1}^N y_nx_n^0 \\ \sum_{n=1}^N y_nx_n^1 \\ \vdots \\ \sum_{n=1}^N y_nx_n^p \\ \vdots \\ \sum_{n=1}^N y_nx_n^P \\ \end{pmatrix} = \begin{pmatrix} \sum_{n=1}^N x_n^{0} & \sum_{n=1}^N x_n^{1} & \cdots & \sum_{n=1}^N x_n^{q} & \cdots & \sum_{n=1}^N x_n^{P} \\ \sum_{n=1}^N x_n^{1} & \sum_{n=1}^N x_n^{2} & \cdots & \sum_{n=1}^N x_n^{1+q} & \cdots & \sum_{n=1}^N x_n^{1+P} \\ \vdots & \vdots & \ddots & \vdots & & \vdots \\ \sum_{n=1}^N x_n^{p} & \sum_{n=1}^N x_n^{p+1} & \cdots & \sum_{n=1}^N x_n^{p+q} & \cdots & \sum_{n=1}^N x_n^{p+P} \\ \vdots & \vdots & & \vdots & \ddots & \vdots \\ \sum_{n=1}^N x_n^{P} & \sum_{n=1}^N x_n^{P+1} & \cdots & \sum_{n=1}^N x_n^{P+q} & \cdots & \sum_{n=1}^N x_n^{2P} \\ \end{pmatrix} \cdot \begin{pmatrix} c^{(0)} \\ c^{(1)} \\ \vdots \\ c^{(q)} \\ \vdots \\ c^{(P)} \\ \end{pmatrix} \end{align}

となるので、逆行列を使って解くことで多項式の係数を求めることができる。

区分的多項式の最小二乗法

次の関数のように、複数の区間ごとに多項式で定義される関数を区分的多項式という。
特に、区間の節点で関数が滑らかに(=微分可能に)接続されている区分多項式はsplineと呼ぶこともある。

\begin{align} f(x) = \begin{cases} f_1(x) = c_1^{(P)}x^P + \cdots + c_1^{(1)}x + c_1^{(0)} \quad & (a_0 \leq x < a_1) \\ f_2(x) = c_2^{(P)}x^P + \cdots + c_2^{(1)}x + c_2^{(0)} \quad & (a_1 \leq x < a_2) \\ \quad \vdots \\ f_K(x) = c_k^{(P)}x^P + \cdots + c_k^{(1)}x + c_k^{(0)} \quad & (a_{k-1} \leq x < a_k) \\ \quad \vdots \\ f_K(x) = c_K^{(P)}x^P + \cdots + c_K^{(1)}x + c_K^{(0)} \quad & (a_{K-1} \leq x < a_K) \\ \end{cases} \end{align}

区分的多項式についても通常の多項式と同様の考え方で、データを近似する係数を求めることができる。
区間の個数を\(K\)とする。\(k\)番目の区間を\(I_k = [a_{k-1}, a_k]\)と表し、区間\(I_k\)内のデータを\(\{(x_{k,n}, y_{k,n})\}_{n=1}^{N_k}\)と表す。
各区間の多項式を\(f_k(x)=c_k^{(P)}x^P + c_k^{(P-1)}x^{P-1} + \cdots + c_k^{(1)}x + c_k^{(0)}\)とすると、

\begin{align} \begin{pmatrix} \sum_{n=1}^{N_1} y_{1,n}x_{1,n}^0 \\ \vdots \\ \sum_{n=1}^{N_1} y_{1,n}x_{1,n}^P \\ \vdots \\ \sum_{n=1}^{N_k} y_{k,n}x_{k,n}^0 \\ \vdots \\ \sum_{n=1}^{N_k} y_{k,n}x_{k,n}^P \\ \vdots \\ \sum_{n=1}^{N_K} y_{K,n}x_{K,n}^0 \\ \vdots \\ \sum_{n=1}^{N_K} y_{K,n}x_{K,n}^P \\ \end{pmatrix} = \begin{pmatrix} \sum_{n=1}^{N_1} x_{1,n}^{0} & \cdots & \sum_{n=1}^{{N_1}_1} x_{1,n}^{P} \\ \vdots & \ddots & \vdots \\ \sum_{n=1}^{N_1} x_{1,n}^{P} & \cdots & \sum_{n=1}^{N_1} x_{1,n}^{2P} \\ &&& \ddots \\ &&&& \sum_{n=1}^{N_k} x_{k,n}^{0} & \cdots & \sum_{n=1}^{N_k} x_{k,n}^{P} \\ &&&& \vdots & \ddots & \vdots \\ &&&& \sum_{n=1}^{N_k} x_{k,n}^{P} & \cdots & \sum_{n=1}^{N_k} x_{k,n}^{2P} \\ &&&&&&& \ddots \\ &&&&&&&& \sum_{n=1}^{N_K} x_{K,n}^{0} & \cdots & \sum_{n=1}^{N_K} x_{K,n}^{P} \\ &&&&&&&& \vdots & \ddots & \vdots \\ &&&&&&&& \sum_{n=1}^{N_K} x_{K,n}^{P} & \cdots & \sum_{n=1}^{N_K} x_{K,n}^{2P} \\ \end{pmatrix} \cdot \begin{pmatrix} c_1^{(0)} \\ \vdots \\ c_1^{(P)} \\ \vdots \\ c_k^{(0)} \\ \vdots \\ c_k^{(P)} \\ \vdots \\ c_K^{(0)} \\ \vdots \\ c_K^{(P)} \\ \end{pmatrix} \end{align}

となって係数を求めることができる。

条件付きの区分的多項式の最小二乗法

  1. 境界での関数の具体的な値
  2. 区間の節点で関数が連続
  3. 区間の節点で関数が滑らか

などの境界条件が課される場合を考える。

境界での関数の具体的な値

\(f(a_0) = b_0\)あるいは\(f(a_K) = b_K\)といった条件を課す場合、

\begin{align} b_0 &= f(a_0) = f_1(a_0) = c_1^{(P)}a_0^P + \cdots + c_1^{(1)}a_0 + c_1^{(0)} \\ b_K &= f(a_K) = f_K(a_K) = c_K^{(P)}a_K^P + \cdots + c_K^{(1)}a_K + c_K^{(0)} \\ \end{align}

とならなければならない。
Lagrangeの未定乗数法の考え方で、最小化するErrorに

\begin{align} \lambda_0 \left( c_1^{(P)}a_0^P + \cdots + c_1^{(1)}a_0 + c_1^{(0)} - b_0 \right) \\ \end{align}

あるいは

\begin{align} \lambda_K \left( c_K^{(P)}a_K^P + \cdots + c_K^{(1)}a_K + c_K^{(0)} - b_K \right) \\ \end{align}

の項を追加すれば良い。

\(\frac{\partial\mathrm{Error}}{\partial c_1^{(p)}}\)には\(a_0^pc_1^{(p)}\)が追加され、\(\frac{\partial\mathrm{Error}}{\partial c_K^{(p)}}\)には\(a_K^pc_K^{(p)}\)が追加される。
また、Errorを\(\lambda_0\)あるいは\(\lambda_K\)で偏微分することで、

\begin{align} b_0 &= c_1^{(P)}a_0^P + \cdots + c_1^{(1)}a_0 + c_1^{(0)} \\ b_K &= c_K^{(P)}a_K^P + \cdots + c_K^{(1)}a_K + c_K^{(0)} \\ \end{align}

が方程式に追加される。

区間の節点で関数が連続

区間の節点で関数が連続であるという条件を課す場合、

\begin{align} 0 &= f_k(a_k) - f_{k+1}(a_k) \\ &= c_k^{(P)}a_k^P + \cdots + c_k^{(1)}a_k + c_k^{(0)} - c_{k+1}^{(P)}a_k^P - \cdots - c_{k+1}^{(1)}a_k - c_{k+1}^{(0)} \\ \end{align}

とならなければならない。(\(k=1, 2, \cdots K-1\))
これも同様にLagrangeの未定乗数法を適用して、

\begin{align} \mu_k \left( c_k^{(P)}a_k^P + \cdots + c_k^{(1)}a_k + c_k^{(0)} - c_{k+1}^{(P)}a_k^P - \cdots - c_{k+1}^{(1)}a_k - c_{k+1}^{(0)} \right) \\ \end{align}

の項を追加する。
その結果、\(\frac{\partial\mathrm{Error}}{\partial c_k^{(p)}}\)には\(a_k^p\mu_k - a_{k-1}^p\mu_{k-1}\)が追加される。

区間の節点で関数が滑らか

区間の節点で関数が滑らかという条件を課す場合、

\begin{align} 0 &= f_k'(a_k) - f_{k+1}'(a_k) \\ &= Pc_k^{(P)}a_k^{P-1} + \cdots + c_k^{(1)} - Pc_{k+1}^{(P)}a_k^{P-1} - \cdots - c_{k+1}^{(1)} \\ \end{align}

とならなければならない。(\(k=1, 2, \cdots K-1\))
これも同様にLagrangeの未定乗数法を適用して、

\begin{align} \nu_k \left( Pc_k^{(P)}a_k^{P-1} + \cdots + c_k^{(1)} - Pc_{k+1}^{(P)}a_k^{P-1} - \cdots - c_{k+1}^{(1)} \right) \\ \end{align}

の項を追加する。
その結果、\(\frac{\partial\mathrm{Error}}{\partial c_k^{(p)}}\)には\(pa_k^{p-1}\nu_k - pa_{k-1}^{p-1}\nu_{k-1}\)が追加される。

全て適用する場合

これらの条件を全て適用する場合、連立方程式は次のようになる。

\begin{align} \begin{pmatrix} \sum_{n=1}^{N_1} y_{1,n}x_{1,n}^0 \\ \vdots \\ \sum_{n=1}^{N_1} y_{1,n}x_{1,n}^P \\ \vdots \\ \sum_{n=1}^{N_k} y_{k,n}x_{k,n}^0 \\ \vdots \\ \sum_{n=1}^{N_k} y_{k,n}x_{k,n}^P \\ \vdots \\ \sum_{n=1}^{N_K} y_{K,n}x_{K,n}^0 \\ \vdots \\ \sum_{n=1}^{N_K} y_{K,n}x_{K,n}^P \\ 0 \\ \vdots \\ 0 \\ 0 \\ \vdots \\ 0 \\ 0 \\ \vdots \\ 0 \\ 0 \\ \vdots \\ 0 \\ 0 \\ 0 \\ \end{pmatrix} = \begin{pmatrix} \sum_{n=1}^{N_1} x_{1,n}^{0} & \cdots & \sum_{n=1}^{{N_1}_1} x_{1,n}^{P} &&&&&&&&& 1 &&&&&& 0 &&&&&& 1 \\ \vdots & \ddots & \vdots &&&&&&&&& \vdots &&&&&& \vdots &&&&&& \vdots \\ \sum_{n=1}^{N_1} x_{1,n}^{P} & \cdots & \sum_{n=1}^{N_1} x_{1,n}^{2P} &&&&&&&&& a_1^P &&&&&& Pa_1^{P-1} &&&&&& a_0^P \\ &&& \ddots \\ &&&& \sum_{n=1}^{N_k} x_{k,n}^{0} & \cdots & \sum_{n=1}^{N_k} x_{k,n}^{P} &&&&&&& -1 & 1 &&&&& 0 & 0 \\ &&&& \vdots & \ddots & \vdots &&&&&&& \vdots & \vdots &&&&& \vdots & \vdots \\ &&&& \sum_{n=1}^{N_k} x_{k,n}^{P} & \cdots & \sum_{n=1}^{N_k} x_{k,n}^{2P} &&&&&&& -a_{k-1}^P & a_k^P &&&&& -Pa_{k-1}^{P-1} & Pa_k^{P-1} \\ &&&&&&& \ddots \\ &&&&&&&& \sum_{n=1}^{N_K} x_{K,n}^{0} & \cdots & \sum_{n=1}^{N_K} x_{K,n}^{P} &&&&&& -1 &&&&&& 0 && 1 \\ &&&&&&&& \vdots & \ddots & \vdots &&&&&& \vdots &&&&&& \vdots && \vdots \\ &&&&&&&& \sum_{n=1}^{N_K} x_{K,n}^{P} & \cdots & \sum_{n=1}^{N_K} x_{K,n}^{2P} &&&&&& -a_{K-1}^P &&&&&& -Pa_{K-1}^{P-1} && a_K^P \\ 1 & \cdots & a_1^P \\ \\ &&&& -1 & \cdots & -a_{k-1}^P \\ &&&& 1 & \cdots & a_k^P \\ \\ &&&&&&&& -1 & \cdots & -a_{K-1}^P \\ 0 & \cdots & Pa_1^{P-1} \\ \\ &&&& 0 & \cdots & -Pa_{k-1}^{P-1} \\ &&&& 0 & \cdots & Pa_k^{P-1} \\ \\ &&&&&&&& 0 & \cdots & -Pa_{K-1}^{P-1} \\ 1 & \cdots & a_0^P \\ &&&&&&&& 1 & \cdots & a_K^P \\ \end{pmatrix} \cdot \begin{pmatrix} c_1^{(0)} \\ \vdots \\ c_1^{(P)} \\ \vdots \\ c_k^{(0)} \\ \vdots \\ c_k^{(P)} \\ \vdots \\ c_K^{(0)} \\ \vdots \\ c_K^{(P)} \\ \mu_1 \\ \vdots \\ \mu_{k-1} \\ \mu_k \\ \vdots \\ \mu_{K-1} \\ \nu_2 \\ \vdots \\ \nu_{k-1} \\ \nu_k \\ \vdots \\ \nu_{K-1} \\ \lambda_0 \\ \lambda_K \\ \end{pmatrix} \end{align}

逆行列によって解くことで、条件を満たした上でErrorを最小化する区分的多項式の係数を求めることができる。

ソースコード

言語はPython。必要なパッケージはNumPy。

Data

各区間に含まれるデータを扱うクラス。

class Data(list):
    def sum(self, f):
        s = 0
        for (x, y) in self:
            s += f(x, y)
        return s

    @staticmethod
    def data_split(data_list, interval):
        K = len(interval) - 1
        d = [Data() for _ in range(K)]

        for data in data_list:
            x = data[0]
            appended = False
            for k in range(K):
                if interval[k] <= x <= interval[k+1]:
                    d[k].append(data)
                    appended = True
            if not appended:
                print(f'data {data} is excluded.')

        return d

make_mv

係数計算のための行列とベクトルを作る関数。

  • data - (x, y)のリストを指定する。
  • interval - 区間の端を指定する。例えばinterval=[0, 0.4, 0.7, 1]とすると、[0, 0.4], [0.4, 0.7], [0.7, 1]の3つの区間で多項式近似を行う。
  • order - 多項式の次数。
  • smooth_condition - Trueのとき区間の端で滑らかに繋がる(微分が一致する)。
  • boundary_condition - 区間全体の左右の端における値を指定する。不要な場合はNoneを与える。片側だけ指定する場合は[y, None]のようにする。
def make_mv(data, interval, order, smooth_condition=True, boundary_condition=[0, 0]):
    interval = sorted(interval)
    K = len(interval) - 1

    data = Data.data_split(data, interval)

    size = K*(order+1) + K - 1
    if smooth_condition:
        size += K - 1
    if boundary_condition is not None:
        size += sum(1 for c in boundary_condition if c is not None)
    m = np.zeros((size, size))
    v = np.zeros(size)

    for k in range(K):
        d = data[k]
        sumxp = [d.sum(lambda x, y: x**p) for p in range(2*order+1)]

        i1 = K*(order+1) + k
        i2 = i1 + K - 1

        for p1 in range(order+1):
            offset = k*(order+1)
            for p2 in range(order+1):
                m[offset + p1, offset + p2] = sumxp[p1 + p2]

            v[offset + p1] = d.sum(lambda x, y: y*x**p1)

            if k <= K-2:
                ap = interval[k+1]**p1
                m[i1, offset + p1        ] =  ap
                m[i1, offset + p1 + K - 1] = -ap
                m[offset + p1,         i1] =  ap
                m[offset + p1 + K - 1, i1] = -ap

                if smooth_condition:
                    pap = p1*interval[k+1]**(p1-1) if p1 != 0 else 0
                    m[i2, offset + p1        ] =  pap
                    m[i2, offset + p1 + K - 1] = -pap
                    m[offset + p1,         i2] =  pap
                    m[offset + p1 + K - 1, i2] = -pap

    if boundary_condition is not None:
        v1, v2 = boundary_condition
        if v1 is not None:
            i = -2 if v2 is not None else -1
            v[i] = v1
            for p1 in range(order+1):
                ap = interval[0]**p1
                m[i, p1] = ap
                m[p1, i] = ap
        if v2 is not None:
            j = (K-1)*(order+1)
            v[-1] = v2
            for p1 in range(order+1):
                ap = interval[-1]**p1
                m[-1, j + p1] = ap
                m[j + p1, -1] = ap

    return m, v

fit

データを近似する多項式の係数を求める。
引数はmake_mvと同様。

def fit(data, interval, order, smooth_condition=True, boundary_condition=[0, 0]):
    K = len(interval) - 1
    m, v = make_mv(data, interval, order, smooth_condition, boundary_condition)

    coeffs = np.linalg.inv(m)@v
    return coeffs[:K*(order+1)].reshape([K, -1])

validate

近似の結果を評価。
実際のデータと近似関数とのMSE, L距離を測定する。

def validate(data, interval, coeffs):
    interval = sorted(interval)
    K = len(interval) - 1

    def f(x, coeff):
        y = 0
        for i, c in enumerate(coeff):
            y += c*x**i
        return y

    max_error = 0
    sum_error = 0
    n = 0
    for x, y in data:
        for k in range(K):
            if interval[k] < x < interval[k+1] or (k == K-1 and x == interval[-1]):
                error = (y - f(x, coeffs[k]))**2
                if max_error < error:
                    max_error = error
                sum_error += error
                n += 1

    return sum_error/n, np.sqrt(max_error)

ソースコード全体

import numpy as np


class Data(list):
    def sum(self, f):
        s = 0
        for (x, y) in self:
            s += f(x, y)
        return s

    @staticmethod
    def data_split(data_list, interval):
        K = len(interval) - 1
        d = [Data() for _ in range(K)]

        for data in data_list:
            x = data[0]
            appended = False
            for k in range(K):
                if interval[k] <= x <= interval[k+1]:
                    d[k].append(data)
                    appended = True
            if not appended:
                print(f'data {data} is excluded.')

        return d


def make_mv(data, interval, order, smooth_condition=True, boundary_condition=[0, 0]):
    interval = sorted(interval)
    K = len(interval) - 1

    data = Data.data_split(data, interval)

    size = K*(order+1) + K - 1
    if smooth_condition:
        size += K - 1
    if boundary_condition is not None:
        size += sum(1 for c in boundary_condition if c is not None)
    m = np.zeros((size, size))
    v = np.zeros(size)

    for k in range(K):
        d = data[k]
        sumxp = [d.sum(lambda x, y: x**p) for p in range(2*order+1)]

        i1 = K*(order+1) + k
        i2 = i1 + K - 1

        for p1 in range(order+1):
            offset = k*(order+1)
            for p2 in range(order+1):
                m[offset + p1, offset + p2] = sumxp[p1 + p2]

            v[offset + p1] = d.sum(lambda x, y: y*x**p1)

            if k <= K-2:
                ap = interval[k+1]**p1
                m[i1, offset + p1        ] =  ap
                m[i1, offset + p1 + K - 1] = -ap
                m[offset + p1,         i1] =  ap
                m[offset + p1 + K - 1, i1] = -ap

                if smooth_condition:
                    pap = p1*interval[k+1]**(p1-1) if p1 != 0 else 0
                    m[i2, offset + p1        ] =  pap
                    m[i2, offset + p1 + K - 1] = -pap
                    m[offset + p1,         i2] =  pap
                    m[offset + p1 + K - 1, i2] = -pap

    if boundary_condition is not None:
        v1, v2 = boundary_condition
        if v1 is not None:
            i = -2 if v2 is not None else -1
            v[i] = v1
            for p1 in range(order+1):
                ap = interval[0]**p1
                m[i, p1] = ap
                m[p1, i] = ap
        if v2 is not None:
            j = (K-1)*(order+1)
            v[-1] = v2
            for p1 in range(order+1):
                ap = interval[-1]**p1
                m[-1, j + p1] = ap
                m[j + p1, -1] = ap

    return m, v


def fit(data, interval, order, smooth_condition=True, boundary_condition=[0, 0]):
    K = len(interval) - 1
    m, v = make_mv(data, interval, order, smooth_condition, boundary_condition)

    coeffs = np.linalg.inv(m)@v
    return coeffs[:K*(order+1)].reshape([K, -1])


def validate(data, interval, coeffs):
    interval = sorted(interval)
    K = len(interval) - 1

    def f(x, coeff):
        y = 0
        for i, c in enumerate(coeff):
            y += c*x**i
        return y

    max_error = 0
    sum_error = 0
    n = 0
    for x, y in data:
        for k in range(K):
            if interval[k] < x < interval[k+1] or (k == K-1 and x == interval[-1]):
                error = (y - f(x, coeffs[k]))**2
                if max_error < error:
                    max_error = error
                sum_error += error
                n += 1

    return sum_error/n, np.sqrt(max_error)


if __name__ == '__main__':
    interval = [0, 0.35, 0.62, 1]  # example
    data = np.loadtxt('input.txt')

    coeffs = fit(data, interval, 5,
                 smooth_condition=False,
                 boundary_condition=[0, 0])

    mse, max_error = validate(data, interval, coeffs)
    print(f'mse = {mse}, max error = {max_error}')

    np.savetxt('output.txt', coeffs)