pyhaya’s diary

プログラミング、特にPythonについての記事を書きます。Djangoや機械学習などホットな話題をわかりやすく説明していきたいと思います。

実験タスクでのデータ管理をちょっと改善した話

TL;DR

実験した結果をクラウドストレージから手元に落として来て分析するときに、ファイルが多いといろいろ辛みがあった。スクリプトを使って使うファイルの管理をしたらデータの分析の効率が少し改善した。

背景

自分は業務でよく、機械学習ジョブを回してその結果を分析するということをやります。その際、ジョブ自体はクラウド上で行い、結果の詳細な分析はデータを手元に落として行うことが多いです。このように、やることによって場所を変えるのは、クラウド上の実行環境はあくまで「実行環境」なのでデバッグや分析をするために毎回ツールをセットアップする必要があったり、分析した結果を可視化するのが難しいという理由からです。

しかし、データを手元に落としてきたはきたで別のツラミがあります。例として、ジョブの実行結果がクラウドストレージに吐き出されていて、それを分析のためにダウンロードするといった状況を考えると、パッと思いつくだけでも以下のようなツラミが生じる可能性があります。

  • 複数のジョブを回す場合に、雑にデータをホイホイダウンロードしているとファイルが多くなったときにどのファイルがどのジョブのものかわからなくなる
  • 1つの実験から複数ファイルが出力として得られるときに、どのファイルが同じ実験から得られたファイルなのか対応がわからなくなる
  • 最初気にしていなかった実験条件が知りたくなったときにストレージのどのファイルを見たら調べられるかわからなくなる

タスクが明確でやりたいことが全部はっきりしているプロジェクトであったら上に挙げたツラミはもしかしたら生じる余地はないのかもしれません。しかし、多くのプロジェクトは多かれ少なかれ「不確かさ」をはらんでいるはずで、そのようなとき、上のツラミが生じることは十分あり得ると思います。この記事では、何らかの方法で少しでもこれを軽減できないかと考えて自分が考え出した1つの解決策を紹介します。

ナイーブな解決策と問題点

上のような問題点を解決する方法として真っ先に思いつくのは、「ファイルをダウンロードして、その後にリンクをどこかにメモしておく」という方法かと思います。しかし、この方法の場合には「リンクをどこかにメモしておく」というステップはスキップ可能で、ダウンロード→分析という過程が簡単に取れてしまいます。したがってこの方法ではツラミが生じる余地を十分減らせていないでしょう。

自分が考えた解決策

そこで私は、「ファイルをダウンロードする」と「リンクをどこかにメモしておく」の順番を逆にすることで問題を軽減できないかと考えました。つまり、必要なデータのリンクをどこかで一元管理しておいて、データのダウンロードはそれを使って行うという方法にすればデータの管理がしやすいのではないかと考えました。

以下が現状で私が使っているデータ管理の形式です。データ分析はPythonでやることが多いので、データ管理にもPythonを使って書いています。

resources = [
	{
		"bucket": "bucket1",
		"blob": "project1",
		"subblob": "experiment1-yyyymmd'd'",
		"files": [
			"parent_dir/file1",
			"file2",
			"file3",
		],
		"destination": "path/to/save/experiment1/yyyymmd'd'",
		"skil_if_exist": True,
		"description": "〇〇を✗✗にして実験した結果",
	},
	{
		"bucket": "bucket1",
		"blob": "project1",
		"subblob": "experiment1-yyyymmdd",
		"files": [
			"parent_dir/file1",
			"file2",
			"file3",
		],
		"destination": "path/to/save/experiment1/yyyymmdd",
		"skil_if_exist": True,
		"description": "〇〇を△△にして実験した結果",
	}
]

この形式にして実際に分析をしてみて感じた利点は、

  • 同じ実験のファイルが一目でわかりやすい
  • ファイル名とパス、ファイルの説明が一箇所にまとまっている
  • 特定の実験で追加のファイルが必要になったときに変更が容易
  • この形式と決めておけば実際のダウンロードに使うスクリプトは使い回せるので楽

だと思っています。

また、まだ試してはいませんが、ダウンロードしたファイルを分析するために読み込むときにも resources を利用できれば、例えば似た名前のファイルを間違って読み込んで分析した、といったミスも減らせる可能性もあります。

この手法のテンプレートはGitHubで公開していますので興味のある人は見てみてください。

github.com

他の手法との比較

Pythonで何かしらのジョブ実行をサポートするものはいくつもあります。その代表例とも言えるのがAirflowです。

airflow.apache.org

Airflowでは上のリンクにあるように、GCSへの多様な操作をできるようになっています。Airflowと比較すると上で紹介した方法というのは機能性という点では劣ってしまいますが、あえて(半分無理やり)Airflowに対する利点を挙げると以下のようなものがあると思います。

  • 実験データ管理だけしたいときには紹介した方法は必要最低限の機能が備わっている
    • Airflowはワークフローエンジンなので実験データ管理以外にも様々な機能がありすぎる
  • データを分析する部分は自由度が大きい
    • 分析部分についてはDAGを作って実行というより試行錯誤しながらやりたいことが多い

補足

実際にダウンロードする際に使うコード
def _download_resource(
    files: list[str],
    destination_dir: str,
    gcs_base: str,
) -> None:
    targets = " ".join([gcs_base + f for f in files])

    result = subprocess.call(
        [
            "gsutil",
            "-m",
            "cp",
            targets,
            destination_dir,
        ]
    )
    if result != 0:
        raise RuntimeError("Script Failed")

def _build_source_and_destination(
    project: str, resource: dict[str, Any]
) -> tuple[str, str]:
    source = f"gs://{resource['bucket']}/{resource['blob']}"
    if resource["subblob"] != "":
        source += f"/{resource['subblob']}/"

    destination = f"{config.REPO_ROOT}/data/{project}/{resource['destination']}"

    return source, destination


def download(project: str, resources: list[dict[str, Any]]):
    for rs in resources:
        source, destination = _build_source_and_destination(project, rs)

        _download_resource(
            rs["files"],
            destination,
            source,
        )

例えばGoogle Cloud StorageからCLI経由でダウンロードする際には、以下のようなコードを使うことができます。(権限があればCloud SDKを使う方法もあります)