Vietoris-Rips filtrationのチュートリアル¶
このチュートリアルでは Vietoris-Rips filtrationを使ってパーシステント図を計算する例です。
Vietoris-Rips filtrationは以下のような利点があります。
- 2点間の距離の情報(distance matrix)だけあれば計算できる
- 遺伝子間の距離情報のようにユークリッド空間以外の空間でも計算できる
- alpha filtrationは2次元、3次元でしか現在は計算できないが、より高次元の点でも計算できる
一方で以下のような欠点もあります。
- 組み合わせ爆発により、計算コストが高い
- 特に高次のホモロジーの計算が困難
- そのため、計算する次数を制限することは必須
- また、実用的な計算時間のためある距離より大きな辺は無視する必要がある場合がある
- データの幾何的な情報を近似的にしか反映していない
- 各点に球を置くやりかたとはけっこう大きなずれが生じる
まず必要なライブラリを読み込みます
import numpy as np
import homcloud.interface as hc
from scipy.spatial import distance_matrix
import matplotlib.pyplot as plt
%matplotlib inline
200次元のデータを100個、ランダムに準備します
data = np.random.uniform(size=(100, 200))
distance matrix (距離行列) を scipy.spatial.distance_matrix
を使って計算します。
dmatrix = distance_matrix(data, data)
distance matrix は 100x100 の行列です
dmatrix.shape
(100, 100)
パーシステント図の計算¶
homcloud.interface.PDList.from_rips_filtration
でパーシステント図を計算します。引数の意味は以下の通りです
- 最初の引数は距離行列
maxdim
で最大次数を指定save_to
で保存先を指定- ここでは指定していませんが、
maxvalue
で取り扱う birth/death time の最大値を指定します
%%time
hc.PDList.from_rips_filtration(dmatrix, maxdim=3, save_to="rips.pdgm")
CPU times: user 1 s, sys: 83.8 ms, total: 1.09 s Wall time: 1.08 s
PDList(path=rips.pdgm)
計算時間は maxdim をどうするかでかなり変わります。
- 2 - 一瞬
- 3 - 5-10 sec
- 4 - 2min
- それ以上は試してない。maxdimに対して指数的に計算量が増大すると思われる
これらの計算は ripser というプログラムで計算されています。この ripser は Vietoris-Rips filtration による パーシステント図を計算するには(おそらく)最速のプログラムです。
パーシステント図の可視化¶
以下のようにしてパーシステント図を読み込みます。
pdlist = hc.PDList("rips.pdgm")
1次のパーシステント図を可視化します
pd1 = pdlist.dth_diagram(1)
pd1.histogram().plot(colorbar={"type": "log"})
2次のパーシステント図も可視化します。
pd2 = pdlist.dth_diagram(2)
pd2.histogram().plot(colorbar={"type": "log"})
pd3 = pdlist.dth_diagram(3)
pd3.histogram().plot(colorbar={"type": "log"})
maxvalue の効果¶
さて、ここで PD を計算するときに maxvalue
を指定すると何がおきるかを見ていきましょう。
まず、distance matrixの値がどのように分布しているか、histogramを見てみましょう。
numpy.triu_indices
を使うと距離行列の上半分だけ取り出すことができます。
plt.hist(dmatrix[np.triu_indices(100, 1)], 100); None
5〜6.5の間に分布しているようです。
ここでは 5.6 を上限として計算しましょう。
%%time
hc.PDList.from_rips_filtration(dmatrix, maxdim=3, maxvalue=5.6, save_to="rips-5.6.pdgm")
CPU times: user 18.5 ms, sys: 0 ns, total: 18.5 ms Wall time: 17.9 ms
PDList(path=rips-5.6.pdgm)
さきほどよりかなり計算時間が早くなったことがわかると思います。
それでは PD をプロットしましょう。
pdlist56 = hc.PDList("rips-5.6.pdgm")
pd56_1 = pdlist56.dth_diagram(1)
pd56_1.histogram((5.1, 5.7)).plot(colorbar={"type": "log"})
pd56_2 = pdlist56.dth_diagram(2)
pd56_2.histogram((5.3, 5.9)).plot(colorbar={"type": "log"})
pd56_3 = pdlist56.dth_diagram(3)
pd56_3.histogram((5.3, 5.9)).plot(colorbar={"type": "log"})
PDの範囲の最大値が5.6になっています。maxvalueを指定しない場合と比較してみましょう。 3x2でPDを並べていて,左がmaxvalueの指定なし,右が指定ありです。上から0次,1次,2次と並べています。
fig, axes = plt.subplots(3, 2, figsize=(14, 14))
pd1.histogram((5.1, 5.7)).plot(colorbar={"type": "log", "max": 10}, ax=axes[0][0])
pd56_1.histogram((5.1, 5.7)).plot(colorbar={"type": "log", "max": 10}, ax=axes[0][1])
pd2.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, ax=axes[1][0])
pd56_2.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, ax=axes[1][1])
pd3.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, ax=axes[2][0])
pd56_3.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, ax=axes[2][1])
maxvalueを指定したほうはbirth, deathともに5.6の所で切れていることがわかると思います。5.6よりも下の部分はどちらも同じ ヒストグラムになっています。
ではこれらの切りとられた birth-death pairはどこに行ったのでしょうか?
実はその一部は death time が∞なbirth-death pairとなっています。
そのような birth-death pair の birth time は essential_births
というattributeに保持されています。では見てみましょう。
pd2.essential_births
array([], dtype=float64)
pd56_2.essential_births
array([5.59986973, 5.59731436, 5.5969243 , 5.59630442, 5.59630442, 5.59550047, 5.59471798, 5.59387875, 5.59387875, 5.59291601, 5.59288454, 5.59288454, 5.58777428, 5.58478022, 5.58478022, 5.58323336, 5.58320761, 5.58302402, 5.57969189, 5.57728481, 5.57637548, 5.57592154, 5.57303667, 5.57085276, 5.57085276, 5.56939459, 5.56820822, 5.56689215, 5.56289721, 5.56282139, 5.56219244, 5.56159353, 5.5589695 , 5.5589695 , 5.55742836, 5.55691719, 5.55553007, 5.55448961, 5.55394936, 5.55394936, 5.54576492, 5.54538584, 5.54364109, 5.53661442, 5.53520107, 5.52763557, 5.50185394, 5.50104332, 5.49944115, 5.48618507, 5.45888186])
このように、maxvalueを指定しなかった場合にはessential_birthsは空ですが、指定した場合にはいくつかの値が得られます。 birth が 5.6 以下で death が 5.6 以上の birth-death pair がここに移動します。birth, death がともに 5.6以上の場合には それらの birth-death pair は痕跡もなく消されます。
これらの death time が ∞ な birth-death pair を並べてプロットすることもできます。
pd56_1.histogram((5.1, 5.7)).plot(colorbar={"type": "log", "max": 10}, plot_ess=True)
pd56_2.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, plot_ess=True)
pd56_3.histogram((5.3, 5.9)).plot(colorbar={"type": "log", "max": 10}, plot_ess=True)
逆解析について¶
パーシステント図の上の各点は元データのホモロジー的構造(1次元はリング,2次元は空隙,等々)に対応しています.その構造を取りだすことができればデータ解析に便利でしょう.アルファ複体を使う場合には optimal volume の利用でこれが可能になります.しかし Vietoris-Rips 複体は単体の個数が多いためそのような構造を抽出するのは難しい問題になります.しかし1次元のPDに関してのみはある種の近似により比較的簡単にそういった構造の抽出が可能です.ここではその構造(optimal 1-cycleと呼びます)を抽出する方法について説明しましょう.
optimal 1-cycleを計算するためには,from_rips_filtration
を呼ぶ際にsave_graph=True
という引数を付ける必要があります.1次元PDしか使わないのでmaxdim
も1で計算しましょう.
%%time
hc.PDList.from_rips_filtration(dmatrix, maxdim=1, save_graph=True, save_to="rips_with_graph.pdgm")
CPU times: user 8.49 ms, sys: 441 µs, total: 8.93 ms Wall time: 8.15 ms
PDList(path=rips_with_graph.pdgm)
1次元のPDを読み込んでプロットします.
pd1 = hc.PDList("rips_with_graph.pdgm").dth_diagram(1)
pd1.histogram().plot(colorbar={"type": "log"})
(5.3, 5.6) に一番近い点を取り出します.
pair = pd1.nearest_pair_to(5.3, 5.6)
pair
Pair(5.288362503051758, 5.525618553161621)
Optimal 1-cycleを計算します.計算時間はそれほどかかりません.
optimal_1_cycle = pair.optimal_1_cycle()
boundary_points
というメソッドを呼び出すとリング上の点が得られます.各点は番号で表現され,その番号は最初の点から0, 1, 2, ... と昇順で付けられます.
optimal_1_cycle.boundary_points()
[93, 50, 48, 7, 3]
注意¶
optimal 1-cycle を計算するときに, Pairs with the same birth time
というエラーが発生する場合があります.実は距離行列の対称でない要素が同じ値を持っているとこの機能はうまく動きません.このようなエラーが出場合には距離行列に小さなノイズを加えておくとうまくいくでしょう.対称性を保ったままノイズを加えてください.
以上でこのチュートリアルは終わりです。