はじめに
この記事はデータ可視化 Advent Calendar 2020 21日目の記事です。
頑張れば、何かがあるって、信じてる。nikkieです。
先日参加した #pyhack でデータ可視化アドベントカレンダーに空きがあることを知り1、最近推しているStreamlit2について締切駆動でアウトプットしてみることにしました。
目次
Streamlitとは
サーバサイドのロジックからUIまでカバーした、ラピッドプロトタイピングツールです。
データ可視化アプリや機械学習モデルを使ったアプリが簡単に作れると注目を集めている印象です。
nikkie的にポイントが高いのは、Streamlitを使えば、Pythonを書くだけでブラウザを通して操作できるアプリができるという点。
チュートリアルを見て「こんなに簡単に作れるのか!」と舌を巻きました。
import pandas as pd import streamlit as st st.title("My first app") df = pd.DataFrame( {"first column": [1, 2, 3, 4], "second column": [10, 20, 30, 40]} ) df
Galleryを覗けば、いろいろなアプリが見つかります。
上記のチュートリアルのTIPで@st.cache
というデコレータが紹介されているのですが、このキャッシュ機能も、Streamlitのヤバい✨機能の1つでした!
この記事では@st.cache
についてドキュメントに沿って見ていきます。
お断り:データ可視化アドベントカレンダーではありますが・・
今回の@st.cache
のアウトプットのサンプルアプリは、データ可視化アプリではありません。
ですが、この記事で登場する@st.cache
の使い方は、Streamlitを使ったどんなアプリにも適用できると考えています。
Streamlitはデータ可視化アプリや機械学習アプリに使われることが多いと思いますが、私自身は「Pythonスクリプトを他の人に使ってもらうように変換する」のにも使えるツールととらえています。
今回は画像リサイズスクリプトを元にした便利ツールアプリを例に@st.cache
を見ていきます。
開発環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.14.6 BuildVersion: 18G4032 $ python -V Python 3.8.6 $ pip install streamlit==0.72.0 Pillow==8.0.1 pandas==1.1.4
@st.cache
とは
ドキュメントの「Improve app performance」というページに、使い方が詳しく書かれています。
Streamlitを使うなら一読して損はないと思います。
※誤読と思われる箇所に気づきましたら、コメントやTwitterでお知らせください。大変ありがたいです
かいつまんで言うと、StreamlitのキャッシュはインメモリのKVS(key-value store)です3。
@st.cache
でデコレートされた関数には、ざっくり言うと、入力された引数のハッシュをキーとし4、関数の実行結果をバリューとするキャッシュが用意されます。
関数を呼び出したとき、引数(のハッシュ)がキャッシュにヒットすれば、キャッシュが使われます(関数は実行されません!)。
キャッシュミスがあった場合は関数が実行され、引数(のハッシュ)と返り値がキャッシュに登録されます。
例(ドキュメントより)
ドキュメントにあるのが以下の例(Example 1):
import time import streamlit as st @st.cache def expensive_computation(a, b): time.sleep(2) return a * b a = 2 b = 21 res = expensive_computation(a, b) st.write("Result:", res)
@st.cache
が付いていないとき、ブラウザでページをリロードするたびにexpensive_computation
が実行されて、表示までに毎回2秒かかります@st.cache
を付けると、初回表示では2秒かかりますが、2回目以降のリロードはキャッシュが使われるので2秒待つことはなくなります
この状態から
expensive_computation
に渡す引数b
の値を変更(Example 2)expensive_computation
の中の実装を変更(Example 3)expensive_computation
から別の関数を呼ぶように変更(Example 4)- さらに呼び出し先の関数の実装も変更(Example 4)
とすると、いずれも(ハッシュ(すなわちKVSのキー)が変わるので)キャッシュミスが起き、関数が実行されるとドキュメントは説明しています。
ヤバい!と思ったのはExample 5。
ユーザが画面のスライダで選択した値をb
とするのですが、
- あるタブでスライダで9を選ぶ(→キャッシュミスにより2秒待つ)
- 別のタブでも表示し、スライダで9を選ぶ→1で登録されたキャッシュがヒットするので、2秒待たずに画面が表示される!!
Streamlitのキャッシュはグローバルにしており、あるユーザが作ったキャッシュが他のユーザのパフォーマンス向上に寄与できる設計とのことです。
This happens because the Streamlit cache is global to all users. So everyone contributes to everyone else’s performance.
キャッシュは参照なので、ValueがMutable objectのとき、Valueを書き換えると、キャッシュのValueも書き換わります(Example 6。CachedObjectMutationWarning
が出ました)。
@st.cache
適用 Before / After
Before
画像5リサイズアプリでは当初以下のような実装をしていました。
uploaded_files = st.file_uploader( "Specify image(s) to be resized in your computer", type=["png", "jpg", "jpeg"], accept_multiple_files=True, ) max_length = st.slider("Specify max length", 100, 500, 300, 50) if uploaded_files: random_id = uuid.uuid4() shrinked_dir_path = Path(f"images/{random_id}") shrinked_dir_path.mkdir(exist_ok=True) for uploaded_file in uploaded_files: resized_image_path = shrinked_dir_path / uploaded_file.name has_resized = resize_image( # 画像を縮小する処理の呼び出し uploaded_file, resized_image_path, max_length ) if has_resized: st.image(str(resized_image_path))
この実装で気になった点は、選択した画像を1つ削除したり、スライダーのサイズ指定をいじったりすると、すでに縮小済みの画像があったとしても毎回縮小した画像が作られる点です。
ファイルが重複してできていって、リソースを無駄にしている感覚がありました。
After
@st.cache
の存在を知り、画像を縮小する処理を関数にしてからデコレートしました。
- 入力:画像ファイル1枚、縮小サイズ
- 処理:画像を縮小して保存する
- 出力:縮小した画像のパス(表示するのに使う)
@st.cache def resize_uploaded_image(image_file_object, max_length): random_id = uuid.uuid4() shrinked_dir_path = Path(f"images/{random_id}") shrinked_dir_path.mkdir(exist_ok=True) resized_image_path = shrinked_dir_path / image_file_object.name has_resized = resize_image( image_file_object, resized_image_path, max_length ) if has_resized: return resized_image_path uploaded_files = st.file_uploader( "Specify image(s) to be resized in your computer", type=["png", "jpg", "jpeg"], accept_multiple_files=True, ) max_length = st.slider("Specify max length", 100, 500, 300, 50) if uploaded_files: for uploaded_file in uploaded_files: resized_image_path = resize_uploaded_image(uploaded_file, max_length) if resized_image_path: st.image(str(resized_image_path))
画像ファイルと縮小サイズの組(のハッシュ)がキャッシュにヒットしたら、すでに縮小済みの画像があるということなので、それを画面に表示します。
キャッシュを使うように処理を書き換えたことで、入力を変更するたびに縮小されなくなりました!
なお、st.file_uploader
の返り値はファイル名以外にファイルのサイズ(や内容?)も見ているようで、全く異なる画像をキャッシュにあるのと同名にしてアップロードしても、キャッシュがヒットしませんでした。
すごいですね!
終わりに
Streamlitの@st.cache
、味方につけるためには関数の設計をカスタマイズする必要がありますが、パフォーマンスにもたらす効果は覿面と感じています。
データ可視化にとどまらず、Pythonスクリプトをアプリにするツールとして可能性を感じているStreamlit、2021年も使い倒していく予定です。
ここまで「魔法」が揃っているStreamlit、魔法をどう実現しているか気になってきますよね。
依存関係で入ってくるtornadoがサーバのようですし、UIはReact6と聞いています。
アドベントカレンダーに手を挙げた時点ではStreamlitの中を見るというのもネタの候補にありました。
今回は間に合わず、みんなのPython勉強会の登壇準備中に知ったネタを使ったのですが、時間のある年末年始に中を見てみようと思っています。
Happy prototyping with Streamlit!
-
↩データ可視化 Advent Calendarまだ空きあるそうです。余裕あったら入ろうかな。 #pyhack https://t.co/23Lx8bvz5J
— Hiroshi Sano (@hrs_sano645) 2020年12月12日 -
この記事は12月のみんなのPython勉強会発表のプチ続編でもあります。「Streamlitヤバい!」と連呼してましたね(語彙力😅) ↩
-
Think of the cache as an in-memory key-value store, (冒頭(=Example 1より前))↩
-
ラピッドプロトタイピングなので、関数の中身が変更されることもあるでしょう。そんな状況でも対応できるように、引数のハッシュ以外に関数のコード(The body of the function)などもキーにしています。上記ドキュメントでは「Advanced caching」の節に詳しいです↩
-
画像はPyCon JP 2019のアルバムより↩