【概説】Frank-Wolfe法により誘導されるスパース性

(著者:分析チーム hawai06)

はじめに

ビッグデータや機械学習の技術を活用したDX推進/分析プロジェクトでは、計算量が少なく、機械学習に特有な問題構造を上手く取り扱えるアルゴリズムが必要と考えられる。特に弊社が行なっているようなビジネス課題を解決するプロジェクトの場合は、成果物をビジネスへ活用しやすいよう解釈性に富んでいる方が良く、出力がスパースなアルゴリズムが望ましいと思われる。
 一方Frank-Wolfe法とは制約付き最適化問題を解くアルゴリズムであり、最適制御や均衡問題など幅広く用いられている。このアルゴリズムは計算量が少なく、さらに機械学習によく現れる制約条件を取り扱い易いため、近年では機械学習やデータサイエンスなどの分野にも適用されている。また出力される解がスパースになることが知られていることから、このアルゴリズムはビジネス課題の解決手法として候補たり得るものであると思われる。
 そこで本稿では、Frank-Wolfe法の詳細およびスパース性が誘導される理由を確認するとともに、実務での有用性を簡易な数値実験を通して確認することとする。

Frank-Wolfe法とは

 Frank-Wolfe法は、コンパクトな凸集合$\mathcal{C}$上で定義された、平滑で微分可能な関数$f$の最小値を求めるアルゴリズムである。


\begin{aligned}
\min_{x \in \mathcal{C}} f(x)
\end{aligned}

解の更新方法は適切なステップ幅を$a_k$を用いて


\begin{aligned}
s_k  &= \operatorname{argmin}_{s \in \mathcal{C}}  <\nabla f(x_k), s> \\
x_{k+1} &= (1-a_k) x_k +a_k  s_k \quad a_k \in (0,1]
\end{aligned}

として記述できる。ステップ幅は$f$の性質により決められる方法が異なり、連続微分可能であるならば直線探索や関数の曲率を用いて定めても良く、また凸であるならば$a_k = \frac{2}{k+2}$としても良い。
 $s_k$を求める際は制約付きの線形関数の最小化問題を解くことになるが、制約条件の構造を考慮することで効率良く解ける。例えば$C = \lbrace x | || x|| \le 1 \rbrace$であるならば、Danskin-Bertsekas's Theoremから双対ノルムの劣微分$\partial || \cdot ||_{*}$を用いて$s_k = - \partial || \nabla f(x_k) ||_{*}$となり、計算で求められる場合は簡易に導出できる。$s_k \in \mathcal{C}$より、$x_{k+1}$は常に実行可能解となる。

誘導されるスパース性

初期点$x_0 = 0 \in \mathcal{C}$、$\mathcal{C} \subset \mathbb{R}^n$を仮定すると、Frank-Wolfe法はその解の更新方法より、$x_k$が$\min \lbrace k, n+1 \rbrace$個のデータ点で記述できるため、$k \ll n$の場合はスパース性を持つと言える。このことから$x_k$の非ゼロ要素数はアルゴリズムの収束レートに関連することがわかる。
 スパースな解が得られる例として$\mathcal{C} = \lbrace x | || x ||_1 \le 1 \rbrace$を考えると、$\forall k \in \mathbb{N}$に対して$s_k = \text{sign} \left( \nabla f( x^{k-1} )_{i_{k-1}} \right) \cdot e_{i_{k-1}}$となる。よって$k \le n$の場合は$x_k$は非ゼロ要素数が$k$個となり、収束性が良いほどスパースな解が求まり得る。( ただし、$i_{k-1} = \operatorname{argmax}_{i=1,\dots,n} \left| \nabla f( x^{k-1})_{i} \right|$、$e_i$は$i$番目の要素のみ$1$でそれ以外の要素は$0$のベクトル、$\nabla f( x^{k-1})_{i}$は$\nabla f( x^{k-1})$の$i$番目の要素を指す)。
 上記より収束性を向上させることでスパースな解を得られる様になるが、アルゴリズムを工夫することでもスパース性は達成され得る。例えば、Fully-Corrective Frank-Wolfe algorithm (以下、FCFW)は次のように解を更新することでFrank-Wolfe法よりもスパースな解が得られるとの報告がある。


\begin{aligned}
v_t &= \operatorname{argmin}_{v \in \mathcal{C}} \langle v, \nabla f(x_t) \rangle \\

\mathcal{S}_{t+1} &= \mathcal{S}_t \cup \{ v_t \} \\

x_{t+1}  &=  \operatorname{argmin}_{x \in \operatorname{conv} \mathcal{S}_{t+1}} f(x)

\end{aligned}

初期値$\mathcal{S}_{0} = \lbrace x_0 \rbrace$とする。このアルゴリズムは、これまでの反復で選んだ頂点集合の凸包で最適化計算して解を更新するため、計算量が多くなるという課題がある。

アルゴリズムの比較確認

問題設定

 実務でのFrank-Wolfe法の活用を検討するにあたり、機械学習にてよく見られる$l1$-ballを定義域にした次の問題を題材にする。


\begin{aligned}
\min_{ \{x \in \mathbb{R}^{n} | \| x\|_1 \le 1 \} } \frac{1}{2}\| x-x^{*} \|^2_2
\end{aligned}

$x^{*}$は定義域に含まれないよう、定義域の極点をランダムに選んだ$m$点の凸結合に対して$2$倍したものとした。本数値実験では$n$および$m$を適当に変えた場合の、計算時間、出力された解の非ゼロ要素数の個数および$x^{*}$との$l2$-normを確認し、スパース性および解としての適性を評価した。Frank-Wolfe法は解の更新回数により得られる解のスパース度合い変わりうるため、最大更新回数を$10^{6}$と十分多くした。

比較手法

Frank-Wolfe法(以下、FW)、FCFWのスパース性との比較手法として、射影勾配法(以下、PGD)および一般的な方法としてSLSQPを用いた。FWはステップ幅は$\frac{2}{k+2}$および直線探索を別に用いることにし、前者をFW-naive、後者をFW-lineと表記する。PGDは、高次元の問題設定にも適用できるよう、確率単体への射影を基にした射影方法を採用しており、こちらに掲載されているコードをまま使用した。またSLSQPを適用するにあたり、$l1$-ballに対してスラック変数を導入して線形緩和した。これら全ての手法はpythonやそのライブラリを用いて実装している。

結果

次元数$n = 10^{2}$の場合

$x^{*}$の非ゼロ要素数$m$変化させた時の結果を下に示す。結果の傾向は$n = 10^{2}$と同様になった。計算時間はFCFWが最も長く、SLSQPが次に長いという結果になった。他手法は十分計算時間が短い。$|| x-x^{*} ||_2$はPGDとSLSQPが一致しており、それぞれ最適解に到達していると考えられる。厳密な直線探索をしているFW-lineはFW-naiveと比較して最適解に近い結果が得られた一方、FW-naiveは次元数を増やしても値が減少していない。アルゴリズムの出力解の非ゼロ要素数を確認すると、PGDとSLSQPは$|| x-x^{*} ||_{2}$と同様に一致している。PGDは$x^{*}$の非ゼロ要素数$m$が$1$の場合、本実験においては数値的に不安定になり、原点を出力していたことから$0$の値を示している。FW-naiveは$m \ge 20$以上の場合、出力される解の非ゼロ要素数が$1$となっている。

次元数$n = 10^{3}$の場合

先と同様に$m$変化させた時の結果を下に示す。計算時間は、$n = 10^{2}$とは逆にSLSQPが最も長く、FCFWが次に長いという結果となった。他手法は$m$を増加させてもほとんど変わらず、$10^{-3}$秒以下で計算が済んでいる。アルゴリズムの出力解の非ゼロ要素数の傾向は$n = 10^{2}$と同様の結果となったが、縦軸を対数スケールにしているため、上述した$m=1$の時のPGDの値$0$は特異な挙動を示している。

所感

本数値実験において、FCFWは$n \le 10^{3}$においても1秒程度で計算が終了しており、最適解に近く、比較的スパースな解が得られたため、有用な手法であると思われる。しかし、$n = 10^{6}$とした予備実験において$m=10^{2}$では計算時間が3.5時間を超え、$m$を大きくするごとに計算時間が増大していった。内部問題の求解ではできる限り解析的に実装したが、大規模な問題への適用の際はCythonで実装するなど更なる工夫が必要である。
 また、PGDとSLSQPは最適解が得られていると考えられる。その中でもPGDは計算時間がかなり短かった。予備実験においても、PGDは最も計算時間が短く$1$秒以内に解が得られた。そのため本問題を実務で取り扱う際は、活躍する可能性が非常に高いと思われる。
 FWについては、ステップ幅の決定方法により大きく異なる結果が得られた。FW-naiveは出力される解の非ゼロ要素数が$1$などであり、解の更新がほとんどなされていないことがわかる。最適解が定義域の境界の場合、FW-naiveは収束性が非常に悪くなることが知られており、今回の問題設定はこれに該当する。そのためコスト関数の改善が十分小さすぎるために、解の更新がなされないまま、計算の終了基準を満たしてしまったと考えられる。一方、FW-linearは出力される解の非ゼロ要素数はFW-naiveに次いで小さく、また$|| x-x^{*} ||_{2}$も他手法と比してそれほど大きい値ではない。$|| x-x^{*} ||_{2}$を小さくしたい場合は、スパース性が犠牲になるものの計算の終了基準を厳しくすれば良く、実務では柔軟に利用できると考えられる。

まとめ

 本稿では、Frank-Wolfe法は他の手法と比較して、得られる解がスパースになることを解の更新方法および数値実験を通して確認した。数値実験は$l2$ノルムを用いた$l1$-ballへの凸射影を題材にしており、厳密な直線探索を用いたFrank-Wolfe法は計算時間・最適解からの距離の観点から、実用的なスパース解が得られたと思われる。また本稿では実装について述べていないが、実装が比較的容易であった。これらのことから、ビッグデータを取り扱うような実務に際して、Frank-Wolfe法は採用しやすい最適化手法であると言える。

参考文献

参考:使用したコード

以下に本数値実験で使用したコードを掲載する。PGDは、先に述べたようにこちらのコードを用いているため掲載は割愛する。

FW関連(optimization.py)
import numpy as np

def generate_problem(optimal_point : np.array):
    """
    Function Explanation:
        最適解を指定して、コスト関数とその勾配を生成する関数
    Inputs:
        optimal_point : np.array
            最適解
    Returns:   
        cost_function : callable
            コスト関数
        grad_cost_function : callable
            コスト関数の勾配
    """

    def cost_function(x : np.array):
        """
        Function Explanation:
            二乗ノルムを用いたコスト関数
        Inputs:
            x : np.array

        Returns:
            (float.)
                関数の値
        """
        return (np.linalg.norm(x - optimal_point, ord=2)**2)/2


    def grad_cost_function(x : np.array):
        """
        Function Explanation:
            二乗ノルムを用いたコスト関数の勾配
        Inputs:
            x : np.array

        Returns:
            (np.array)
                勾配
        """
        return x - optimal_point
    
    return cost_function, grad_cost_function


def frank_wolfe(x0 : np.array,
                grad_f : callable,
                f      : callable,
                step_size : str,
                max_iter : int = np.inf,
                tol      : float = 1e-6):
    """
    Function Exeplanetion:
        Frank-Wolfe法を用いて、最適化問題を解く関数。
    Inputs:
        x0 : np.array
            初期点
        f : callable
            目的関数
        grad_f : callable
            勾配関数
        step_size : str
            'diminish': 2/2+iteration
            'line search' : 直線探索
        max_iter : int
            最大反復回数
        tol : float
            収束判定の閾値
    Returns:
        x : np.array
            最適解
    """

    # 初期条件の設定
    iteration = 1
    x = x0
    improve = np.inf
    f_list = [f(x0)]

    while (iteration < max_iter) and (improve > tol):
        # 点の設定
        x_k = x
        old = f(x_k)

        # 更新方向
        index = np.argmax(np.abs(grad_f(x)))
        s_k   = np.zeros(len(x))
        s_k[index] =  -np.sign(grad_f(x)[index]) 
        # ステップ幅
        if step_size == 'diminish':
            step_size = 2/(iteration + 2) 
        elif step_size == 'line search':
            norm_s_k = np.linalg.norm(s_k - x, ord=2)
            if  norm_s_k  == 0:
                break    
            else:
                step_size = \
                    -np.dot(s_k, grad_f(x)) / norm_s_k**2
            if step_size < 0:
                step_size = 0
            elif step_size > 1:
                step_size = 1
        # 更新  
        print(iteration)
        x = x_k + step_size * (s_k - x_k) 
        iteration += 1
        improve = 1 - (f(x) / old ) 
        f_list.append(f(x))

    return x#, f_list


def find(s, S):
    for i in range(len(S)):
        if np.all(S[i] == s):
            return True
    return False

def FullyCorrectiveFrankWolfe(x0 : np.array,
                              grad_f : callable,
                              f      : callable,
                              optimal_point : np.array,
                              max_iter : int = np.inf,
                              tol      : float = 1e-6,
                              inner_max_iter : int = np.inf,
                              inner_tol : float = 1e-6):  
                              
    """
    Function Explanation:
        Fully Corrective Frank-Wolfe法を用いて、最適化問題を解く関数。
    Inputs:
        x0 : np.array
            初期点
        f : callable
            目的関数
        grad_f : callable
            勾配関数
        max_iter : int
            最大反復回数
        tol : float
            収束判定の閾値
        optimal_point : np.array
            本数値実験の最適解
        inner_max_iter : int
            内部問題の最大反復回数
        inner_tol : float
            内部問題の収束判定の閾値
    Returns:
        x : np.array
            最適解
    """

    # 初期条件の設定
    iteration = 1
    x = x0
    S = list()
    S.append(x)
    impove = np.inf
    f_list = [f(x0)]

    while (iteration < max_iter) and (impove > tol):
        # 点の設定
        x_k = x
        old = f(x_k)

        # Sの集合を構築
        ## 要素取得
        index = np.argmax(np.abs(grad_f(x_k)))
        s_k   = np.zeros(len(x_k))
        s_k[index] =  -np.sign(grad_f(x_k)[index]) 
        ## 要素の追加
        if find(s_k, S) == False:
            S.append(s_k)
            
        
        # 内部問題を解いて解の更新
        inner_improve   = np.inf
        inner_iteration = 1
        while (inner_iteration < inner_max_iter) and (inner_improve > inner_tol):
            # ループ情報の設定
            inner_old = f(x)
            inner_iteration += 1

            # 内部問題の求解
            inner_index = np.argmin(np.dot(np.array(S), grad_f(x)))
            norm_s_k = np.linalg.norm(S[inner_index] - x, ord=2)
            if  norm_s_k  == 0:
                break    
            else:
                step_size = \
                    -np.dot(S[inner_index],grad_f(x)) / norm_s_k**2
            if step_size < 0:
                step_size = 0
            elif step_size > 1:
                step_size = 1
            x =  x + step_size * (S[inner_index] - x)


            # ループ情報の更新
            
            inner_improve = 1 - (f(x) / inner_old ) 
        

        # ループ情報の更新
        iteration += 1
        improve = 1 - (f(x) / old ) 
        f_list.append(f(x))

    return x#, f_list

if __name__ == "__main__":
    # ライブラリの読み込み
    import os
    from problem_setting import generate_optimal_point
    

    # 最適解の生成
    dimensions = 100
    combination_num = 10
    optimal_point = generate_optimal_point(dimensions,
                                           combination_num,
                                           in_range = 'out',
                                           out_coef = 2)
    # 問題の生成
    f, grad_f = generate_problem(optimal_point)

    # 最適化アルゴリズムの実行
    initial_point = np.zeros(dimensions)
    print("FW")
    FW_solution = frank_wolfe(initial_point,
                              grad_f, f,
                              step_size = 'diminish',
                              max_iter = 10000, tol = 1e-6)
    print("FC")
    FCFW_solution = FullyCorrectiveFrankWolfe(
        initial_point,
        grad_f, f,
        optimal_point,
        max_iter = 1000, tol = 1e-6,
        inner_max_iter = 1000, inner_tol = 1e-6)
SLSQP(slsqp.py)
import numpy as np
from problem_setting import generate_optimal_point
from optimization import generate_problem
from scipy.optimize import minimize
import os
import copy



class SLSQP:
    
    def __init__(self,
                 optimal_point : np.array,
                 dimensions    : np.array,
                 initial_point : np.array):
        
        self.dimensions    = dimensions
        self.optimal_point = optimal_point
        self.initial_point = self._generate_initial_point_for_slack_var(
            initial_point
            )
        
    def _generate_initial_point_for_slack_var(self,
                                              optimal_point : np.array):
        copy_optimal_point          = np.copy(optimal_point)
        initial_point_for_slack_var = np.concatenate(
            [optimal_point, copy_optimal_point], 0
            )
        return initial_point_for_slack_var
        
    def _upper_bound_const(self, index):
        return {'type': 'ineq',
                'fun' : lambda x:  x[index + self.dimensions] - x[index]}
    
    def _genrate_upper_bound_const(self):
        const_list = list()
        for index in range(self.dimensions):
            const_list.append(self._upper_bound_const(index))
        return const_list

    def _lower_bound_const(self, index):
        return {'type': 'ineq',
                'fun' : lambda x:  x[index] + x[index + self.dimensions]}

    def _genrate_lower_bound_const(self):
        const_list = list()    
        for index in range(self.dimensions):
            const_list.append(self._lower_bound_const(index))
        return const_list
    

    def _genrate_l1_bound_const(self):
        const_list = list()
        const_list.append(
            {'type': 'ineq',
             'fun' : lambda x:  1- np.sum(x[self.dimensions:])}
            )
        return const_list

    def _generate_cost_function(self):
        """
        Function Explanation:
            二乗ノルムを用いたコスト関数
        Inputs:
            x : np.array

        Returns:
            (float.)
            関数の値
        """
        return lambda x: np.sum(x[self.dimensions:]**2)/2 - \
                         np.dot(x[:self.dimensions], self.optimal_point) + \
                         np.sum(self.optimal_point**2)/2
    
    def solve(self):
        # 問題の生成
        cost_function  = self._generate_cost_function()
        
        # 制約条件の作成
        ## l1-ball
        constraints = []
        constraints.extend(self._genrate_upper_bound_const())
        constraints.extend(self._genrate_lower_bound_const())
        constraints.extend(self._genrate_l1_bound_const())
        ## box_const
        bounds = []
        bounds.extend([(-1, 1) for i in range(self.dimensions)])
        bounds.extend([(0, 1) for i in range(self.dimensions)])
        
        
        solution = minimize(
            cost_function, x0 = self.initial_point,
            method = 'SLSQP',
            constraints = constraints,
            bounds = bounds,
            options = {'maxiter' :1000 , 'disp' : True},
            tol = 1e-9
            )
        return solution
    
    def solution_arrange(self,
                         solution, eps = 1e-6):
        
        solution_mask =  np.abs(
            solution.x[:self.dimensions]
            ) >  eps
        optimal_solution      = np.empty(self.dimensions)
        optimal_solution[solution_mask]  = solution.x[:self.dimensions][solution_mask]
        optimal_solution[~solution_mask] = np.zeros(self.dimensions)[~solution_mask]
        
        return optimal_solution


if __name__ == "__main__":

    # 最適解の生成
    dimensions = 10
    combination_num = 2
    optimal_point = generate_optimal_point(dimensions,
                                           combination_num,
                                           in_range = 'in',
                                           out_coef = 2)
    #optimal_point = np.random.randn(dimensions)
    #optimal_point = optimal_point / np.sum(np.abs(optimal_point))

    initial_point = np.zeros(dimensions)
    
    # 問題の生成
    
    func = SLSQP(optimal_point,
                 dimensions,
                 initial_point)
    func.initial_point.shape
    solution = func.solve()
    optimal_solution = func.solution_arrange(solution)
    
    print(optimal_solution)
    print(optimal_point) 
最適解の生成(problem_setting.py)
import numpy as np


def generate_optimal_point(dimensions : int,
                           combination_num      : int,
                           in_range   : str,
                           out_coef : float = 1.0):
    """
    Function Explanation:
        l1-ball上の極点の凸結合として定義される、スパースな点を生成する関数。
        'in_range'を変更することで、生成される点がl1-ballの内外のどちらになるかを指定できる。
    Inputs:
        dimensions : int
            初期点の次元数
        combination_num : int
            初期点の生成にあたり、利用する極点の数
            (注意:combination_num <= dimensions)
        in_range : str
            生成される点がl1-ballの内外のどちらになるかを指定するパラメータ
            'in' : l1-ballの内部に生成される
            'out' : l1-ballの外部に生成される
        out_coef : float
            'in_range'が'out'の場合に、l1-ballの外部に生成される点の係数
    Returns:
        coordinates : np.array
            生成された点
    """
    # スパースな点が生成されるかの確認
    if combination_num > dimensions:
        raise ValueError("'combination_num' must be less than or equal to 'dimensions'.")
    
    # 初期点生成にあたり、関与する座標の決定
    # 各要素が0からdimensions-1までの整数値のベクトル
    rng = np.random.default_rng()
    coordinates_list = rng.integers(low = 0, high = dimensions,
                                    size= combination_num, dtype=np.int64, endpoint=True)

    # 初期生成のための重み作成
    weights = rng.uniform(low=-1, high=1, size=combination_num)
    weights = weights / np.sum(np.abs(weights))
    

    # 初期点生成
    coordinates = np.zeros(dimensions)
    for i, index in enumerate(coordinates_list):
         coordinates[index] += weights[i]
    
    # l1-ballの外部に生成される場合
    if in_range == 'out':
        coordinates = coordinates * out_coef    
    elif in_range == 'in':
        pass    
    else:
        raise ValueError("in_range must be 'in' or 'out'.")
    
    return coordinates
数値実験(experiment.py)
import os
import pickle
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from time import time
plt.rcParams['font.family'] = 'Hiragino Sans'
plt.rcParams['xtick.direction'] = 'in'
plt.rcParams['ytick.direction'] = 'in'

# 問題設定のライブラリ
from problem_setting import generate_optimal_point
# frank-wolfe法のライブラリ
from optimization import frank_wolfe, FullyCorrectiveFrankWolfe,generate_problem
# SLSQPのライブラリ
from slsqp import SLSQP
# PGDのライブラリ
from pgd import euclidean_proj_l1ball


# 実験の初期点を保存関数(確率的にエラーが出るため、保存)
##################################################
def save_optimal_point(dimensions       : int,
                       non_zero_nums    : dict,
                       in_range         : str,
                       out_coef         : int,
                       save_name        : str):
    # 格納箱
    dim_optimal_dict = {num : None for num in non_zero_nums}
    
    # データ点の生成
    for num in non_zero_nums:
        optimal_point = generate_optimal_point(
            dimensions,
            combination_num = num,
            in_range = in_range, out_coef = out_coef
            )
        dim_optimal_dict[num] = optimal_point
    # データ保存
    with open(save_name, "wb") as f:
        pickle.dump(dim_optimal_dict, f)
    
    return None
##################################################


# 実験の初期設定の準備
## 準備
problem_setting_dict = {
    100   : {'non_zero_nums' : [1, 5, 10, 20, 50, 80, 90],
             'method_names'  : ['FW - naive', 'FW - line', 'FCFW', 'PGD', 'SLSQP'],
             'save_name'     : 'dim_100_optimal_dict.pkl',
             'solution_save_name' : 'dim_100_solution_dict.pkl',
             'time_save_name' : 'dim_100_time_dict.pkl'},
    
    
    10**6 : {'non_zero_nums' : [1, 5, 10, 50, 100, 500, 1000, 10000],
             'method_names'  : ['FW - naive', 'FW - line', 'FCFW', 'PGD'],
             'save_name'     : 'dim_10**6_optimal_dict.pkl',
             'solution_save_name' : 'dim_10**6_solution_dict.pkl',
             'time_save_name' : 'dim_10**6_time_dict.pkl'},
    
    1000  : {'non_zero_nums' : [1, 5, 10, 25, 50, 75 ,100, 250, 500, 750],
             'method_names'  : ['FW - naive', 'FW - line', 'FCFW', 'PGD', 'SLSQP'],
             'save_name'     : 'dim_1000_optimal_dict.pkl',
             'solution_save_name' : 'dim_1000_solution_dict.pkl',
             'time_save_name' : 'dim_1000_time_dict.pkl'},
    }

## データ点生成+保存
#dimensions = 100
#dimensions = 10**6
#dimensions = 1000
#save_optimal_point(
#    dimensions,
#    problem_setting_dict[dimensions]['non_zero_nums'],
#    in_range = 'out',
#    out_coef = 2,
#    save_name = problem_setting_dict[dimensions]['save_name']
#    )



# 問題を解く
## 初期設定
dimensions = 1000
## 問題の読み込み
with open(problem_setting_dict[dimensions]['save_name'], 'rb') as f:
    dim_optimal_dict = pickle.load(f)
# 各手法の計算
## 計算結果の格納箱
solution_dict = {
    method : {num : None
              for num in problem_setting_dict[dimensions]['non_zero_nums']}
    for method in problem_setting_dict[dimensions]['method_names']
    }
time_dict  = {
    method : {num : None
              for num in problem_setting_dict[dimensions]['non_zero_nums']}
    for method in problem_setting_dict[dimensions]['method_names']
    }
## 計算
for num in problem_setting_dict[dimensions]['non_zero_nums']:
    # 初期設定
    ## 最適解
    optimal_point = dim_optimal_dict[num]
    
    
    # FC、FCFW
    ## 問題、初期点の生成
    f, grad_f = generate_problem(optimal_point)
    initial_point = np.zeros(dimensions)
    ## 実行
    ### FW diminish
    print('num:', num, ' FW - naiveを開始')
    FW_naive_start = time()
    solution_dict['FW - naive'][num] = frank_wolfe(
        initial_point, grad_f, f,step_size = 'diminish',
        max_iter = 10000, tol = 1e-6
        )
    FW_naive_end  = time()
    time_dict['FW - naive'][num] = FW_naive_end - FW_naive_start
    print('num:', num, ' FW - naiveの時間:', time_dict['FW - naive'][num])
    
    ### FW 直線探索
    print('num:', num, 'FW - lineを開始')
    FW_line_start = time()
    solution_dict['FW - line'][num] = frank_wolfe(
        initial_point, grad_f, f,step_size = 'line search',
        max_iter = 10000, tol = 1e-6
        )
    FW_line_end  = time()
    time_dict['FW - line'][num] = FW_line_end - FW_line_start
    print('num:', num, ' FW - lineの時間:', time_dict['FW - line'][num])
    
    
    ### FCFW
    print('num:', num, 'FCFWを開始')
    FCFW_start = time()    
    solution_dict['FCFW'][num] = FullyCorrectiveFrankWolfe(
        initial_point, grad_f, f,
        optimal_point,
        max_iter = 10000    , tol = 1e-6,
        inner_max_iter = 1000, inner_tol = 1e-6
        )
    FCFW_end  = time()
    time_dict['FCFW'][num] = FCFW_end - FCFW_start
    print('num:', num, ' FCFWの時間:', time_dict['FCFW'][num])

    
    # PGD
    print('num:', num, 'PGDを開始')
    PGD_start = time()    
    solution_dict['PGD'][num] = euclidean_proj_l1ball(
        optimal_point, s=1
        )
    PGD_end  = time()
    time_dict['PGD'][num] = PGD_end - PGD_start
    print('num:', num, ' PGDの時間:', time_dict['PGD'][num])

    
    # SLSQP(次元数100、1000の時だけ実行)
    if (dimensions == 100) | (dimensions == 1000):
        print('num:', num, 'SLSQPを開始')
        SLSQP_start = time()    
        # 初期設定
        func = SLSQP(optimal_point,
                     dimensions,
                     initial_point)
        # 求解
        slsqp_solution = func.solve()
        # 解の整形
        solution_dict['SLSQP'][num] = func.solution_arrange(slsqp_solution)
        SLSQP_end  = time()
        time_dict['SLSQP'][num] = SLSQP_end - SLSQP_start
        print('num:', num, ' SLSQPの時間:', time_dict['SLSQP'][num])

    

# 結果の保存
with open(problem_setting_dict[dimensions]['solution_save_name'],
          "wb") as f:
    pickle.dump(solution_dict, f)
    
with open(problem_setting_dict[dimensions]['time_save_name'],
          "wb") as f:
    pickle.dump(time_dict, f)


# 結果を開く
dimensions =100
with open(problem_setting_dict[dimensions]['save_name'], 'rb') as f:
    dim_optimal_dict = pickle.load(f)
with open(problem_setting_dict[dimensions]['solution_save_name'], 'rb') as f:
    solution_dict  = pickle.load(f)
with open(problem_setting_dict[dimensions]['time_save_name'], 'rb') as f:
    time_dict = pickle.load(f)



# 可視化
#################################################
plt.figure(figsize = (30, 5))
if dimensions == 100:
    plt.suptitle('$n=100$')
elif dimensions == 1000:
    plt.suptitle('$n=1000$')

plt.subplot(1,3,1)
for method in problem_setting_dict[dimensions]['method_names']:
    plt.plot(problem_setting_dict[dimensions]['non_zero_nums'],
             [time_dict[method][num]
              for num in problem_setting_dict[dimensions]['non_zero_nums']],
             'x--', label = method)
    plt.xlabel('$x^*$の非ゼロ要素数')
    plt.ylabel('時間')
if dimensions == 100:
    plt.xlim([0,100])
    plt.xticks([20 * i for i in range(6)])
    plt.ylim([1e-5,100])
    plt.yscale('log')
elif dimensions == 1000:
    plt.xlim([0.9,1000])
    plt.ylim([1e-5,10e3])
    plt.xscale('log')
    plt.yscale('log')


plt.subplot(1,3,2)
for method in problem_setting_dict[dimensions]['method_names']:
    plt.plot(problem_setting_dict[dimensions]['non_zero_nums'],
             [np.sum(np.abs(solution_dict[method][num]) > 1e-6)
              for num in problem_setting_dict[dimensions]['non_zero_nums']],
             'x--', label = method)
    plt.xlabel('$x^*$の非ゼロ要素数')
    plt.ylabel('出力された解の非ゼロ要素数')
if dimensions == 100:
    plt.xlim([0,100])
    plt.xticks([20 * i for i in range(6)])
    plt.ylim([0,50])
    plt.yticks([10 * i for i in range(6)])
elif dimensions == 1000:
    plt.xlim([0.9,1000])
    plt.ylim([0.1,1000])
    plt.xscale('log')
    plt.yscale('log')



plt.subplot(1,3,3)
for method in problem_setting_dict[dimensions]['method_names']:
    
    plt.plot(problem_setting_dict[dimensions]['non_zero_nums'],
             [np.linalg.norm(
                 solution_dict[method][num] - dim_optimal_dict[num],
                 ord = 2
                 )
              for num in problem_setting_dict[dimensions]['non_zero_nums']],
             'x--', label = method)
    plt.xlabel('$x^*$の非ゼロ要素数')
    plt.ylabel('$\| x -x^* \|_2$')
    plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))

if dimensions == 100:
    plt.xlim([0,100])
    plt.xticks([20 * i for i in range(6)])
    plt.yscale('log')
    plt.ylim([0.01,10])
elif dimensions == 1000:
    plt.xlim([0.9,1000])
    plt.ylim([1e-2,10])
    plt.xscale('log')
    plt.yscale('log')

plt.show()    
#################################################