はじめに
どんな解析もできる、そして将来どんな新しい解析技術が登場しても導入できる、そんな「最強な解析ソフトが欲しいと思い、頑張って作っています。
ドキュメントはこちら。
インストールは pip install himena[recommended]
でできます。
himena
という名前は、なんとなくテトラヒメナって姿も名前も神秘的・近未来的でいいよねっていう浅い理由から来ています1。昔好きだった子の名前とかではない。
ここに至った経緯や動機、現存するソフトウェアの問題点、そしてどのような設計で「万能」を実現していけそうかを書いていこうと思います。
§1. 良い感じのデータ解析ソフトがないんですよね
プログラミング言語というのはチューリング完全なので、いわば「万能」なわけです。しかも拡張可能性が非常に高くデザインされているおかげで、新しい手法・技術を取り入れたり、既存のものと組み合わせたりが簡単にできます。
にもかかわらず、そのプログラミング言語で書かれたデータ解析ソフトはどうかというと、「簡単に拡張できますよ」と言っていて、確かにプラグインのインストールなどで簡単に拡張できるのですが、結局いまだに「このソフトすごい!いろいろできて便利!」というものが見つからないのが現状です。たとえば、僕は生物物理の研究をしているので、解析で扱うのは画像や時系列データ、遺伝子配列などになりますが、これを皆がどうやるかというと
やりたいこと | 使用ソフト |
---|---|
画像処理・画像解析 | ImageJ, napari |
時系列データの可視化 | Excel |
遺伝子配列の設計・解析 | ApE |
統計解析 | Prism |
タンパク質構造関係 | ChimeraX, Pymol |
というふうにソフトウェアを使い分けて、組み合わせて解析をする際には中間の結果をコピーしたりファイルに一度吐き出してほかのソフトウェアで読み込むといった煩雑な手順を踏んでいます2。
ソフトウェアごとにショートカットが違かったり、古いソフトウェアはUIが悪かったりと、かなりフラストレーションがたまります。
§2. 何故これまでのソフトウェアでは「万能」が実現できなかったのか
これまでソフトウェアの拡張がどのようになされ、何が不完全だったかを考えてみます。
§2.1. プラグインという発想
1つのソフトウェアで標準であらゆるものをカバーというのは不可能です。そのため、多くのソフトでは「プラグイン」というシステムを採用することで、必要に応じてサードパーティが機能を追加できる仕組みにしています。上で登場した中ではImageJやnapari、またプログラミング用のものではVSCodeやJupyter Notebookでプラグインは活発に開発されており、日々更新されています。
実装としては、
- ソフトウェアの状態を取得または更新するインターフェイスを用意し、外部プログラムからそれを利用できる。
- 外部プログラムをソフトウェアから呼び出せる。
の2点に過ぎません。シンプルですが、このおかげで標準実装ではできないことが数多くできるようになります。
例えば、現在開いている画像を取得する関数get_image
、その画像を更新する関数set_image
、そして関数をメニューに追加するような関数register_function
が用意されていれば、以下のように様々な画像解析プラグインが作れます。
ファイルの読み書きするプラグイン
標準でサポートしていない画像ファイルの拡張子がある場合、GUIと無関係なパッケージmy_package
があれば、以下のプラグイン関数を介してファイルを読み込めるようになります。
from my_package import read_image, write_image # 画像を読み書きする外部関数
def my_reader_plugin(path):
img = read_image(path)
set_image(img)
def my_writer_plugin(path):
img = get_image()
write_image(img, path)
# 登録
register_function(my_reader_plugin)
register_function(my_writer_plugin)
画像処理するプラグイン
以下の関数を定義し、これをソフトウェアから呼び出せば、どんな画像処理もできるようになります。
def my_plugin():
img = get_image()
img_output = nanika_sugoi_keisan(img)
set_image(img_output)
# 登録
register_function(my_plugin)
ウィジェットプラグイン
ウィジェットを追加するインターフェイスadd_widget
が用意されていれば、GUIそのものを拡張することもできます。
def my_widget_plugin():
my_widget = VeryGoodWidget() # ウィジェット作成
add_widget(my_widget) # メインウィンドウに追加
# 登録
register_function(my_widget_plugin)
当然、VeryGoodWidget
にボタンを追加し、その中でget_image
/ set_image
を呼び出せば、解析のツールボックスのようなものも作れます。
§2.2. プラグインの限界
以上挙げたように、プラグインを使えばかなり自由に拡張できるので、十分なのではないか?と思うかもしれませんが、一つ重大な問題があります。それは、プラグインではプラグインの拡張ができないという点です。
napari
のプラグインの一つであるnapari-skimage-regionpropsを例にとりましょう。このプラグインは、画像の領域ごとの特徴量 (面積、平均強度、長短径など) を計算してテーブルとして表示することができ、使いやすい優れたプラグインです。こんな感じ ↓ で左の画像で手で塗った領域の特徴量が計測され、右のテーブルに出ています。
テーブルがあるので、ここからプロットしたいですよね。例えば、明るさの分布や、その時間依存的な変化などがよく行われる解析です。ところが、それをやるにはCSV出力してほかで読み出すしかないです。何故ならこのプラグインにプラグインシステムがないからです。このテーブルの機能を拡張するには、プラグイン開発者に連絡するしかありません。
また逆に、右上のボタンにあるように、せっかく実装した「Copy to clipboard」と「Save as csv...」も、ほかのプラグインからは使えません。とてももったいないですよね。
§3. himena
のソフトウェアデザイン
himena
はこの欠点の克服を第一に考え、開発者がお互いのプラグインをより良いものにして行ける設計を心がけています。どのように従来のプラグインシステムの欠点を克服しているのかを述べていきます。
§3.1. 強力なアプリケーションフレームワークの提供
himena
は何か特定の解析に特化しません。データ解析ソフト一般を考えたときに必要な、またあると便利な機能を備えているだけで、それ以外の具体的なウィジェットや関数はすべてプラグインに委ねています。言い換えると、ソフトウェアの実装よりも一歩抽象化した階層にあるフレームワークを実装しています。
見た目は以下のような感じです。
ここに、
- メニューバーとツールバーでコマンドを整理
- コマンドパレットで素早くコマンドを検索・実行
- コピー&ペースト、ドラッグ&ドロップによるウィジェット間のやり取り
- セッションの保存
といった、実装されたウィジェットをサポートする機能を多く備えています。
§3.2. WidgetDataModel
によるスタンダード化
ではhimena
ではどのように機能を拡張するのかという点です。GUI操作で解析が行われる以上、ウィジェットの実装が不可欠です。
himena
では、ウィジェットからのデータの入出力は必ずWidgetDataModel
を介して行われます。なお、WidgetDataModel
は以下の通り、1つのPythonオブジェクトにGUI固有の追加情報を付加したようなものです。
WidgetDataModel(
value: Any = ... # 何かしらのPythonオブジェクト
type: str = ... # "text", "table"のような、「ウィジェット型」のようなもの
title : str = ... # サブウィンドウのタイトル
metadata: Any = ... # 各ウィジェット特有の、データには関係ない情報(テーブルの選択範囲など)
...
)
これにより、まず、プラグインの実装は非常にスッキリします3。
ファイルの読み書き
ファイルを読むプラグインは、ファイルを開いてWidgetDataModel
を返すだけの関数です。返ってくるWidgetDataModel
を表示するウィジェットが実装されている必要はありません。
同様に、ファイルに書き込むプラグインは、WidgetDataModel
のデータを指定されたパスに書き込むだけの関数です。ウィジェットからどのようにWidgetDataModel
を読み出すかは次のウィジェットプラグインに委ねているので、ここで考える必要はありません。
ウィジェットプラグイン
ウィジェットは、与えられたWidgetDataModel
から状態を更新するupdate_model()
と、今の状態から適切なWidgetDataModel
を返すto_model()
が実装されているのが基本です。あとはregister_widget_class
で、そのウィジェットがどのようなデータの表示を想定しているかを記述すれば、対応するWidgetDataModel
が出てきたときにこのウィジェットを選んでくれます。
from himena.plugins import register_widget_class
# "text"型が来たらMyWidget使ってねと教える
register_widget_class("text", MyWidget)
これだけあればほかのプラグインとの連携は問題なくできます(他にもいくつか使い勝手を良くするプロトコルが用意されていますが割愛します)。
データ処理プラグイン
データ処理はWidgetDataModel
から新しいWidgetDataModel
を計算するだけの関数です。メニューのどこに配置するか、いつアクティブになるかといった情報をregister_function
で記述することで使えるようになります。
from himena import WidgetDataModel
from himena.plugins import register_function
@register_function(
menus="tools/my-plugins", # ここに配置してねと教える
type="table", # どのデータ型に使えるか
title="すごい計算する", # メニューでの表示名
)
def my_function(model: WidgetDataModel) -> WidgetDataModel:
model_out = ... # 実装部分
return model_out
入力元、出力先のウィジェットがどうかは関係ありません。すなわち、解析手法の開発者はGUIを意識する必要はありません。
以上をまとめた図が次のようになります。
- ユーザーはメモリの層は見えないので、今まで通りファイルを読み込み (1, 2)、テキストからテーブルに変換し (3, 4, 5)、CSVに書き出します (6, 7)。
- 開発者はファイルの読み込み (1)、ウィジェットの実装 (2, 3 および 5, 6)、処理関数の実装 (4)、ファイルへの書き出し (7)を分業します。この設計により、各ステップはプラグイン間で互換性があるので、あらゆるプラグインはほかのプラグインを拡張することになります。
ユーザーと開発者でやっていることが半拍ずつずれているのが何だか面白いですね。
§3.3. ワークフローの記録とセッションの保存
全てのコマンドはregister_function
によって登録されます。このおかげで、実行コマンドやパラメータなど、どのようなワークフローでそのウィジェットがが作られたかがほぼ4記録され、見返して参考にしたり、保存したり、さらには再度実行したりできます。以下のGIFは例として、「CSVの読み込み → "dataframe"型への変換 → プロット」という作業を記録したワークフローを右のウィンドウで表示5しており、ウィンドウをすべて消してもワークフローを実行するコマンドで結果を再取得できていることを実演しています。また、ドラッグ&ドロップで途中までのワークフローを取り出しています。
このワークフローはJSON出力が可能なため、データの再現に非常に便利です。また、himena
には全ウィンドウをまとめてセッションとして保存する機能6があり、その際にもワークフローは便利です。たとえば、ファイルへの書き出しが困難だったり、ファイルが重くなるようなデータは、ワークフローだけ記録しておけばセッションで復元できます。
§3.4. データ型によるフィルタリング
機能の多いソフトウェアは、仕方ないことですが、その機能の多さゆえに重大な欠点があります。
- コマンドが覚えきれない
- メニューが多いし、ネストが深くて探すのに時間がかかる
- ショートカットが足りない
Officeの各製品やAffinity Designerなどが最たる例ですね(悪く言っているわけではないぞ)。himena
はコマンドパレットで全コマンドを管理できるので、この問題は部分的には解決していますが、もう一つ工夫をしています。
サブウィンドウの左上にあるこちらのボタンには、そのサブウィンドウに対応するコマンドのみが格納されます。例えば、標準の状態でテーブルデータでは「Plot」メニューが出てきますが、テキストデータでは出てきません。
himena
の各ウィジェットは1つのデータを有しており、WidgetDataModel
で簡潔に記述されるということを見てきました。WidgetDataModel
にはデータ型の情報が入っているので、わざわざ実装しなくとも、すべてのウィジェットでこのボタンが使えます。
§3.4. Pythonなので「つぶしがきく」
PythonでGUIを実装する利点、それはPythonが使えることです。そのソフトウェアでしか動かないマクロ言語とか覚える必要がないし、やりたいことがなんでもできて非常に体験がいい。
Ctrl+Shift+C
で下部にJupyter QtConsoleが出てきます。
ここの名前空間にはメインウィンドウのハンドラがui
という変数名で入っており、開いているデータすべてにアクセス可能です。例えば、
ui.tabs # タブのリスト(みたいなオブジェクト)
ui.tabs[0] # 最初のタブ=サブウィンドウのリスト(みたいなオブジェクト)
ui.tabs[0][i] # 最初のタブのi番目のサブウィンドウ
ui.tabs[0][i].to_model() # そのサブウィンドウの`WidgetDataModel`
ui.tabs[0][i].to_model().value # その内部データ(`str`, `ndarray`など身近なもの)
かゆい所にとりあえず手が届くので安心。
§4. プラグインの例
§4.1. 今あるプラグイン
僕自身が日ごろ必要になる、少しニッチな機能はほかのパッケージでプラグインとして公開しています。
himena-image
多くの拡張子の画像の読み書きのサポート、scikit-image
のサンプルデータのロード、フィルタ・FFTなどベーシックな画像解析関数と、期待の3次元ビューアーndv
を使ったヌルヌル動くウィジェットを提供しています。作った中では一番完成度が高い。目指せ打倒ImageJ。
himena-bio
生物系の解析をカバーしていきたい。遺伝子配列の基本的操作、アラインメント、PCRのシミュレーションなどができます。目指せ打倒ApE。
himena-seaborn
テーブルデータからseaborn
の関数を呼び出します。seaborn
は引数が多くてコードを書くときは不便だったんですが、GUIにしておくと楽ですね。
himena-stats
検定、確率分布からのサンプリングなど、統計関係の機能が入っています。
§4.2. 未完成・今後作りたいプラグインやアイデアなど
-
himena-terminal
: VSCodeみたいにターミナル動かしたい。やり方全くわからん。 -
himena-lmfit
:lmfit
を使ったフィッティングや最適化...やりたい!テーブルをプロットしてそのままフィッティングとかやりたい! -
himena-sklearn
: scikit-learnでのテーブルデータの解析。PCAとかすぐできたら嬉しい。 -
himena-napari
:napari
のプラグイン全部吸収してやる...(napariはメインウィンドウで動かす前提の設計が多いので、一筋縄では行かない) -
himena-micro-manager
: 光学顕微鏡の操作をするMicroManagerをPythonから動かすpymmcore-plusを導入してイメージング技術を発展させたい。napari
のプラグインではもうあるが、開発者本人も「別にnapariじゃなくてよくね」となっているようだ。 -
geohimena
:geopandas
で衛星データとかの解析(素人並感) -
astrohimena
:astropy
で宇宙とかの解析(素人並感)
最後に
GUI開発は長くやってきましたが、なかなか良いアイデアだと思っていて、これまで作ってきたウィジェットや関数もどんどんプラグインとしてhimena
に移行していこうと考えています。
データサイエンスに取り組んでいる皆さんにも、ぜひユーザーとしてもプラグイン開発者としても使っていただければ幸いです!
-
テトラヒメナの綴りならhymenaでは?となるが、キーボードでの打ちやすさと英語での処女膜hymenを連想しないようhymenaではなくhimenaにした。 ↩
-
ここで挙げたものは、どれも他とかなり独立しているので、別々のソフトでもまあよいのだが、実は電子顕微鏡関係が現在非常にカオスで、ソフトウェアの統制が全く取れていない上にほとんど全部使いづらいのである。 ↩
-
フロントエンドとバックエンドを分けているだけとも言えるが、従来の意味でのフロントエンド・バックエンドの分業かというとそうとも言えない。 ↩
-
「ほぼ」と言っているのは、例えばデータが手動で編集された場合にその追跡方法が定義されていない場合、クリップボードからの画像の貼り付けのように大きなデータをそのまま記録せざるを得ない場合、関数内部で乱数の取得やウィジェットの状態の取得を「暗に」行っている場合といった、記録が困難な場合があるため。しかし、そのような場合でも、解析の流れの記録は見返す際に非常に有用である。 ↩
-
鋭い読者は気づいたかもしれないが、このワークフローを表示しているウィンドウ自体も、ただの"workflow"型の
WidgetDataModel
から来ていて、ワークフローの実行もただのコマンドである(ともに標準実装のプラグイン)。 ↩ -
ちょっとまだexperimental。 ↩