はじめに
これまでの記事で近接勾配法と、それによるスパース解や低ランク解に導く正則化項を付随した最適化問題の解法、そしてその応用を見てきました。正則化項に変数間の絡みがなく各変数が独立に扱える場合は、正則化項のproximal operatorが解析的に求まるため近接勾配法は有効な手段です。ところが、各変数間に絡みがあり分離できない場合、一般的にproximal operatorの計算は容易ではありません(解析的に求まらない)。そこで今回は、変数間に絡みがある正則化項が付いていても最適解を導出することができる交互方向乗数法(Alternating Direction Method of Multipliers : ADMM)のアルゴリズムを見ていきます。また、それを用いた画像のノイズ除去をPythonで実装したので紹介します。
交互方向乗数法(ADMM)とは
いきなりですがADMMのアルゴリズムを記載します。まずADMMの対象となる最適化問題は下記のようなものになります。
この問題を解くために、下記に示す「拡張ラグランジュ関数」というものを定義します。
この拡張ラグランジュ関数に関する詳細は補足の項や参考文献をご参照ください。とりあえず、ここでは通常のラグランジュ関数に等式制約が満たされないことに対する罰則項(第3項)がついたものと考えて問題ありません。この拡張ラグランジュ関数を用いると式(1)を解くADMMアルゴリズムは下記のようになります。
- 、、を適当な値で初期化する
- 以下の操作を収束するまで繰り返す
これだけです。アルゴリズムとしては非常に簡単です。特に式(3)と式(4)ではとそれぞれに関する最適化問題を独立で解いているので、元々の最適化問題が関数同士の和が含まれており解くのが難しい場合でも、式(1)の形式に変換できればADMMにより簡単に解くことができます。このことを次の章で見ていきます。が、その前に補足として、上記アルゴリズムがどのように出てきたかを理解するために、双対上昇法、拡張ラグランジュ関数について簡単に説明します(細かいことはかなり省略するので、詳しい説明は参考文献をご参照ください)。
【補足】双対上昇法、拡張ラグランジュ関数について
ADMMは最適化問題の強双対性という性質を利用しています。ここでは、
と定義されます。が真凸関数である場合は、となるとが存在すれば、が式(6)の最適解です。
更にが真凸関数であるとき式(7)について下記も成り立ちます。
これは強双対性と呼ばれる性質で、左辺は式(6)と同値の問題です。この強双対性を利用して最適化問題を解くのが双対上昇法や拡張ラグランジュ関数法、そして今回の主題であるADMMです。
双対上昇法
双対上昇法は式(6)の問題を強双対性から双対問題という問題に変換し、そちらを解こうという方針の手法です。まず、式(8)の右辺に問題を当てはめ変形すると、
という問題になります。ここで、は以前説明した共役関数でです。この式(9)の問題は双対問題と呼ばれ、これを解く方法を双対上昇法と言います。更にこれを勾配法に基づいて解く場合は宇沢の方法といい、
という更新式になります。で、この勾配を求めるためには、
を計算する必要があります。結局のところこれは、式(7)にあるラグランジュ関数のに関する最小化です。つまり、主問題であるに関する最適化と双対問題であるに関する最適化を交互に行うアルゴリズムとなります。
この双対上昇法では共役関数の勾配を求めるため、共役関数が微分可能でなければ使えません。しかし、一般的に共役関数は微分可能ではなく、しかも関数が強凸という性質*1を持っていなければアルゴリズムの収束も保証されません*2。そこで、次に説明する拡張ラグランジュの登場です。
拡張ラグランジュ関数法
まず式(6)の問題について、
という式に書きなおします。目的関数の第2項としてを付け加えましたが制約条件よりになるため、式(12)は式(6)と等価です。次に式(12)のラグランジュ関数は
となります。これは拡張ラグランジュ関数と呼ばれる関数で、ラグランジュ関数に対し制約を満たさないことに対する罰則項を加えたものとなっています。実はこの罰則項があることにより主問題は強凸となり、たとえの共役関数が微分不可能だったとしても双対問題が滑らかになるため双対上昇法が適用できます(その理由については、参考文献をご参照ください)。このように、ラグランジュ関数に制約条件を満たさないことに対する罰則項を加えることで問題を解きやすくした上で、 との更新を交互していくのが拡張ラグランジュ関数法です。
ADMM再考
さて、式(1)は式(6)において変数がとに分解可能という特別なケースです。式(1)を拡張ラグランジュ関数法で解こうとした時にとに関する同時最適化が困難なことが多いです。そこで1回の更新でとの同時最適化はあきらめ、個別に最適化していこうというのがADMMです。
変数間に絡みのある正則化項付き最適化問題のADMMによる解法
L1ノルム正則化は変数間が独立しており、proximal operatorが解析的に求まります。L2ノルム正則化(2乗しない)はルートを取る段階で変数間の絡みが出てきますがMoreau decompositionをうまく使ってproximal operator計算することができます(近接勾配法の記事参照)。p=2の重複なしのグループ正則化については、重複がないため各グループ独立したL2ノルムとしてproximal operatorとして計算できます。
しかし、上述のように変数間に絡みがあるとproximal operatorは容易には計算できません。例えば、重複ありのグループ正則化(p=2)の場合、同じ変数が複数のグループにまたがって現れるため、各グループ独立してproximal operatorを計算できなくなってしまいます。そこで、そのような正則化がつく最適化問題をうまく式(1)の形式かつがproximal operatorが計算しやすい形に変形し、ADMMで解くことを考えます。重複ありグループ正則化を例にとって考えてみると、
という問題を解くことになるのですが、これを下記のような問題に変形します。
ここでをうまく定義しがグループ間の重複がないようにします。具体的にはを
などのように、それぞれと左からかけると、グループに所属する要素のみを抽出する行列とします。そして、とすると、式(15)より とは各グループに対応する部分ベクトルを並べたベクトルとなります。こうすることで、正則化項は重複なしのグループ正則化になります。
ということで、この式(15)に対してADMMを適用することを考えます。この中でに関する更新式は、
となります。ここで、 は、グループ正則化項のproximal operatorです(こちらの式(21)参照)。このように、元々の問題が正則化項のproximal operaorを求めるのが困難であり近接勾配法を利用できない場合でも、うまく式変形を行いADMMを適用すれば解けるようになることもあるため、ADMMはかなり強力なアルゴリズムだと言えます。
TV正則化によるノイズ除去への応用
冒頭でも述べたように、今回はこのADMMを応用して画像のノイズ除去を行ってみます。今、入力画像をとして、このからのノイズ除去は、全変動正則化(TV正則化)と呼ばれる正則化項を用いて以下のような定式化が提案されています。
式(17)の第1項は出力画像がなるべく入力画像に近する働きがあります。第2項がTV正則化項で下記のような式となります。
ここではピクセル位置を表します*3。基本的に画像は滑らかな性質を有しているはずなので、隣り合うピクセルの差は小さいはずです。そのため、このTV正則化は隣り合うピクセルの差が大きい場合の罰則項となり、画像を滑らかにしノイズ成分を除去する働きがあります。
さて、この式(17)をADMMで解くことを考えます。TV正則化項も重複ありグループ正則化と同じく変数間に絡みがあり、直接proximal operatorを計算するのは困難です。よって、重複ありグループ正則化を考えたときと同じく、として、重複なしグループ正則化項に変換することを考えます。これは以下のように、
と、となるように定義すれば、式(18)はのL2ノルムの和なので、 とすればTV正則化を重複なしグループ正則化に変換することができます。よって、このを用いれば式(16)の更新式でを更新することができます。一方、についての更新式は、式(3)についてで微分してとなる点を求めればよいので解析的に求まり、以下のようになります。
実装と実験
ノイズ除去アルゴリズムの実装
ということで、TV正則化によるノイズ除去を実装し、実験してみました。ここで、全てのピクセルを1つのベクトルとして扱ってしまうと画像サイズによってはメモリが足りなくなってしまうので、超解像を行ったときのように画像パッチを切り出し、それぞれのパッチに対してノイズ除去を行った後、そのパッチをつなぎ合わせて画像を再構築するという方法を取りました。なお、この画像切り出しやパッチのつなぎ合わせなどは、超解像の記事で記した「おれおれ画像・ベクトル・行列変換関数群」も利用してるので、そちらもご参照ください。
import os import sys import numpy as np import scipy as sci from scipy import sparse from numpy.random import * from PIL import Image #ブロックソフト閾値計算 def block_soft_thresh(b, lam): return max(0, 1 - lam/np.linalg.norm(b, 2)) * b #グループ正則化のproximal operator def prox_group_norm(v, groups, gamma, lam): u = np.zeros(v.shape[0]) for i in range(1, np.max(groups) + 1): u[groups == i] = block_soft_thresh(v[groups == i] , gamma * lam) return u #グループ正則化計算 def gloup_l1_norm(x, groups, p): s = 0 for i in range(1, np.max(groups) + 1): s += np.linalg.norm(x[groups == i], p) return s #関数をメモ化する関数 def memoize(f): table = {} def func(*args): if not args in table: table[args] = f(*args) return table[args] return func #ADMMを行う関数 #argmin_x, argmin_y, update_lam, objectiveはそれぞれ変数名に準ずる計算を行う関数 def ADMM(argmin_x, argmin_y, update_lam, p, objective, x_init, y_init, lam_init, tol= 1e-9): x = x_init y = y_init lam = lam_init result = objective(x) while 1: x_new = argmin_x(y, lam, p) y_new = argmin_y(x_new, lam, p) lam_new = update_lam(x_new, y_new, lam, p) result_new = objective(x_new) if result_new < tol or (np.abs(result - result_new)/np.abs(result) < tol) == True : break x = x_new y = y_new lam = lam_new result = result_new return x_new, result_new #TV正則化付きのスムージング #N,M:画像のピクセル行数、列数 #v:入力画像のベクトル #B:正則化の変換行列(スパースなので、scipyのsparse行列を渡す) #groups:変換後ベクトルの各要素の所属グループ #C:正則化の係数 #p:拡張ラグランジュの係数 def TV_reg_smoothing(N, M, v, B, groups, C, p) : #inv(2I + pB^T B)を計算する関数。アルゴリズムによってはpが可変なので、pを引数として受け取る。 #関数をメモ化して同一のpが入力された場合、再計算不要としている。 inv_H = memoize(lambda p:np.array(np.linalg.inv( 2.0 * np.eye(B.shape[1], B.shape[1]) + p * (B.T * B)))) argmin_x = lambda y, lam, p:np.dot(inv_H(p), 2.0 * v - np.array(B.T * (-p * y + lam))) argmin_y = lambda x, lam, p:prox_group_norm((B * x) + lam/p, groups, 1.0/p, C) update_lam = lambda x, y, lam, p:lam + p*((B * x) - y) objective = lambda x:np.linalg.norm(v - x, 2)**2 + C * gloup_l1_norm((B * x), groups, 2) x_init = np.random.randn(B.shape[1]) y_init = (B * x_init) lam_init = np.zeros(B.shape[0]) (x, result) = ADMM(argmin_x, argmin_y, update_lam, p, objective, x_init, y_init, lam_init, 1e-9) return x, result #グループ変換行列を計算する関数 #N,M:画像のピクセル行数、列数 def calc_group_trans_mat(N, M): B = sci.sparse.lil_matrix((2 * (M - 1) * (N - 1) + (M - 1) + (N - 1), M * N)) groups = np.zeros(B.shape[0], 'int') k = 0 for i in range(N): for j in range(M): base = i * M + j if i < N -1 and j < M -1: B[k, base] = 1 B[k, base + 1] = -1 B[k + 1, base] = 1 B[k + 1, base + M] = -1 groups[k] = int(k/2) + int(k % 2) + 1 groups[k + 1] = int(k/2) + int(k % 2) + 1 k += 2 #一番下の行のピクセルは右隣のピクセルとの差分のみ計算 elif i >= N - 1 and j < M - 1: B[k, base] = 1 B[k, base + 1] = -1 groups[k] = int(k/2) + int(k % 2) + 1 k += 1 #一番右の劣のピクセルは下のピクセルとの差分のみ計算 elif i < N - 1 and j >= M - 1: B[k, base] = 1 B[k, base + M] = -1 groups[k] = int(k/2) + int(k % 2) + 1 k += 1 return B, groups #グレースケール画像に対するノイズ除去 #img:画像データ(PILのimageクラス) #patch_hight:切り出す画像パッチの高さ #patch_width:切り出す画像パッチの幅 #shift_num:画像を切り出す際のずらし量(重複して切り出してもOK) #C:正則化の係数 #p:拡張ラグランジュの係数 def denoise_gray_img(img, patch_hight, patch_width, shift_num, C, p): #グループ変換行列の計算 [B, groups] = calc_group_trans_mat(patch_hight, patch_width) #画像パッチの切り出し patchs = gray_im2patch_vec(img, patch_hight, patch_width, shift_num) new_patchs = np.zeros((patch_hight * patch_width, patchs.shape[1])) for i in range(patchs.shape[1]): #各パッチに対してノイズ除去を施す new_patchs[:, i] = TV_reg_smoothing(patch_hight, patch_width, patchs[:, i], B, groups, C, p)[0] #パッチをつなぎ合わせて画像の再構成 [new_img, img_arr] = patch_vecs2gray_im(new_patchs, img.size[0], img.size[1], patch_hight, patch_width, shift_num) return new_img
細かい点はソースコードとそのコメントをご参照ください。とりあえず、denoise_gray_imgにPILのimageクラスのオブジェクトと切り出すパッチのサイズ、パッチ切り出し時のずらし量を渡せばノイズ除去されたimageクラスのオブジェクトを返してくれるように実装しています。超解像の時と同様、パッチを切り出す際に領域の重複を許し、再構成時に重ね合わせて平均を取るようにしています。また、画像の最後の行および列は右隣のピクセル(および下のピクセル)とのみ差分を取るようにBを工夫しています(calc_group_trans_mat関数内)。
注意する点としては、TV_reg_smoothingが受け取るBはスパースな行列なので、scipyのsparse行列のクラス渡します。また、同関数内にあるinv_Hはpを引数にとり、式(19)の逆行列部分を計算する関数ですが、同一のpに対して再度同じ計算を行うと時間がかかるので、memoizeによりメモ化し同じpが入力されたときに再計算しないようにしています。
なお、更新式はADMM関数の中に直接記述するのではなく、ADMM関数はそれぞれの更新式の計算を行う関数を引数として受け取るようにし、目的関数の出力が収束するまでそれらの更新式関数を呼び出すという設計にしているため、更新式の定義もTV_reg_smoothing関数の中で行っています。
実験と結果
いつものように256×256のレナさん画像で実験を行います。
これに対し、標準偏差30のガウスノイズを乗せた画像に対してノイズ除去を行ってみました。
def main(): test_img = Image.open("ADMM_input.png") img_arr = np.array(test_img.convert('L'),'float') img_arr = (img_arr + 30 * (np.random.randn(test_img.size[0], test_img.size[1]) )) img_arr[img_arr >255] = 255 img_arr[img_arr <0] = 0 test_img = Image.fromarray(np.uint8(img_arr)) patch_hight = 20 patch_width = 20 shift_num = 10 C = 50 p = 20 img2 = denoise_gray_img(test_img, patch_hight, patch_width, shift_num, C, p) img2.save("ADMM_result.png")
画像パッチのサイズを20×20ずらし量10として切り出し、C=50、p=20で実験を行った結果は以下のようになります。
元画像レベルとまではいきませんが、それなりにノイズ除去ができているかと思います。ただ、パッチを切り貼りしている影響か、少しゆがんでしまっているようにも見えます。更に追実験として入力画像のノイズレベルを標準偏差50と上げ、その時のノイズ除去もやってみました(C=80、p=1で実験)。
ノイズレベルの割にはある程度ノイズ除去できているかと思いますが、TV正則化項の罰則に引っ張られ画像がノッペリとしてしまいました。Cをもっと調整すればもう少し改善できるかも知れません。
まとめ
今回は近接勾配法よりも幅広い問題を解くことができるADMMについて説明し、それを用いて画像のノイズ除去をやってみました。今回紹介した重複ありグループ正則化やTV正則化以外にも、特徴間のグラフ構造や階層構造なんかを表す正則化項がついた最適化問題もADMMを使えば解けるようになり、加えて別途制約条件がある場合でも式変形をうまくやればADMMで解けるようになるケースもあるなど応用範囲が広い手法です。更に収束も割りと速く、の値も適当に決めてもより大きければ大体収束するので、使い勝手もなかなか良いです。
参考文献
今回はこちらの書籍の主に12章と15章を参考にしました。
機械学習のための連続最適化 (機械学習プロフェッショナルシリーズ)
- 作者: 金森敬文,鈴木大慈,竹内一郎,佐藤一誠
- 出版社/メーカー: 講談社
- 発売日: 2016/12/07
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る