booklista tech blog

booklista のエンジニアリングに関する情報を公開しています。

Streamlitでレコメンドの定性評価アプリを作った話

アイキャッチ

はじめに

MLエンジニアのmixidota2です。ブックリスタでは主に推薦システムの開発などをしています。
今回は推薦モデルの定性評価にstreamlitを使った話をします。
どのような課題をstreamlitで解決したか、またなぜstreamlitが推薦モデルの評価というユースケースに合っていたのかを紹介します。
想定読者としては、推薦システム(や機械学習システム)に関わるMLエンジニア・データサイエンティストやソフトウェアエンジニアの方々です。

推薦モデルの評価とは

機械学習モデルを本番投入する前にはオフライン評価をするのが一般的です。
推薦モデルの場合も同様で、オフライン評価により本番投入前にモデルの性能を事前に見積もることでユーザー体験やビジネス指標を損なわないモデルを本番投入できます。
評価についてはビジネス指標に対する定量的な評価は当然必要ですが、一方で定性的な評価も重要です。
定性評価の例としては、ユーザーに推薦されるアイテムを人が確認することで期待通りの出力になっているかを評価するなどがあります。
特に推薦モデルの出力は推薦の文脈によって多様であるため、定量的な評価だけでは見落としてしまいそうな課題も発見できます。

これまでの課題

理想的には定性評価については色んな人に触ってもらえるアプリのような環境があるとより良いのですが、そういう環境を作るのはフロントやバックエンドの知識も必要で、MLエンジニアだけでは難しいです。
そこでこれまでは推薦モデルの定性評価については、モデルを作ったMLエンジニアが推薦結果をバッチ推論でmarkdownにまとめたものを見てもらうという形で行っていました。

ただしこの方法で運用していく中での課題としては次のようなものがありました。

  • 探索的に見るには見辛い
  • 全てのユーザーやアイテムに対しての推薦結果を見ることができない
  • モデルの改善をするたびに手動で更新する必要があり、モデルの改善に伴う定性評価のコストが高い
  • 定性評価を見たいモデルの数に比例して定性評価を見られるようにするためのコストが増加する

どうやって解決したか

streamlitの採用

既存の課題から、次のようなことが実現できる方法を探していました。

  • インターフェースを使って任意のユーザーやアイテムに対しての推薦結果を見ることができる
  • できればモデルの改善をするたびに手動で更新する必要がない、あるいはその手間が現状より減らせる

この課題の解決手段として結局はwebアプリをホストしてその背後で推薦モデルを動かせる環境が一番手っ取り早いという結論に至りました。
またその上でなるべく専門知識を必要とせずそれを実現する方法を探しました。
そこでstreamlitというwebアプリの知識を必要としなくてもアプリが作成できるライブラリーに辿り着き、その簡便さと豊富なインターフェースが我々のユースケースに合っていると判断し採用しました。

streamlit採用のメリット

streamlitを使うことで次のようなメリットがありました。

  • MLエンジニアだけで簡単にアプリを作ることができた
  • 色んな人に触ってもらいやすいインターフェースができたことで、いろんな視点からフィードバックを受けることができるようになった
  • モデルの改善をするたびに手動で更新する手間が大幅に減らせた
  • アプリの背後でリアルタイム推論を走らせることにより、全てのユーザーやアイテムに対しての推論結果を手軽に見ることができるようになった

実際に作ったもの

最終的にはこのようなアプリを作りました。

ではstreamlitが具体的にどんなもので、我々のユースケースにどうfitしていたのかを説明していきます。

streamlitとは

概要

Streamlit turns data scripts into shareable web apps in minutes.
All in pure Python. No front‑end experience required.
ref. https://streamlit.io/

streamlit公式サイトの説明を引用すると、streamlitはデータのスクリプトをシェア可能なWebアプリに変換できるPythonのライブラリーです。
バックエンドやフロントエンドの知識を必要とすることなく、MLエンジニアやデータサイエンティストのみで簡単にWebアプリを作ることができます。
streamlitはデータのアプリ作成に特化しており、pandasのデータフレームやplotlyのグラフなどのデータの操作や可視化を扱うためのインターフェースが豊富に用意されています。

どんなふうに使うことができるか

基本的には表示したいオブジェクトをコードに並べていくだけで、それが自動的に解釈されてWebアプリとして表示されます。
極端に言えば、jupyter notebook内でコードを可視化する感覚でアプリの画面が書けるという感じです。
例えば次のようなコードを書くだけで、データフレームやグラフを表示するだけのアプリが作成できます。

# ref: https://docs.streamlit.io/library/api-reference/write-magic/magic
# Draw a title and some text to the app:
'''
# This is the document title

This is some _markdown_.
'''

import pandas as pd
df = pd.DataFrame({'col1': [1,2,3]})
df  # 👈 Draw the dataframe

x = 10
'x', x  # 👈 Draw the string 'x' and then the value of x

# Also works with most supported chart types
import matplotlib.pyplot as plt
import numpy as np

arr = np.random.normal(1, 1, size=100)
fig, ax = plt.subplots()
ax.hist(arr, bins=20)

fig  # 👈 Draw a Matplotlib chart

もちろん単純にオブジェクトを並べるだけがstreamlitで出来ることの全てではなく、様々なインターフェースや画面構成をstreamlitのモジュールを使用して簡単に表現できます。

streamlitのどんな点が良かったか

今回推薦モデルの定性評価にstreamlitを採用してみて、このユースケースに特にfitしているなと感じた点をいくつか紹介します。

インタラクティブに推薦結果がみれるアプリが簡単に作れる

定性評価のフィードバックを様々な人に受けてもらう観点からすると、使用者の認知負荷が低いインターフェースであることでより多くの人に触ってもらえるようになることが期待できます。
また推薦モデルの推薦起点となるユーザー(あるいはアイテム)の候補数は大量にあることが一般的です。
このような状況において現実的に任意の使用者が任意の対象における推薦結果を定性評価したい場合、コスト的な観点からもリアルタイム推論をできるような仕組みが望ましいです。
streamlitでは、このような仕組みを追加の専門的な知識を必要とせず簡単に作ることができました。
今回のユースケースのように、なるべく多くの人たちに定性評価をしてもらいたい場合には、streamlitでのアプリ作成は手段として有効だと感じました。

jupyter notebookで可視化する感覚でアプリの画面が書ける

streamlit特有の文法をほとんど覚える必要がなく、普段MLエンジニアやデータサイエンティストが使用しているツールをそのまま使用してアプリの開発ができます。
例えばpandasのデータフレームを表示したい場合は簡単には次のように書くことができます。

import pandas as pd 
import streamlit as st

df = pd.DataFrame({'col1': [1,2,3]})
st.write(df) # st.writeに渡すだけで表示してくれる

またグラフを表示したい場合は、次のようにグラフのfigを渡すだけで表示してくれるので普段の可視化のコードをほとんどそのまま使用できます。

import plotly.express as px
import streamlit as st

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length")
st.plotly_chart(fig) # st.plotly_chartにfigを渡すだけで表示してくれる

markdown形式で書いていた文章もそのまま書けます。

import streamlit as st

st.markdown("*Streamlit* is **really** ***cool***.")
st.markdown('''
    :red[Streamlit] :orange[can] :green[write] :blue[text] :violet[in]
    :gray[pretty] :rainbow[colors].''')

このように普段と同じようにコードを書くことができるので、開発コストを低く抑えられる点が推薦モデルを開発している人にとっては嬉しい点でした。

標準で用意されてるインターフェースが豊富なので、やれる操作の幅が広い

アプリ上でのインターフェースについても、streamlitでは標準で用意されているものを使うだけで多くの表現が簡単に実現できます。
例えば任意の推薦枠で任意のユーザーに対しての推薦結果を表示したい、というようなケースにおいては次のようにインターフェースや画面表示を簡単に作ることができます。

# ドロップダウンを使った推薦枠の選択 
frame = st.selectbox('推薦枠を選択してね', ['枠1', '枠2', '枠3'])


# テキストボックスで任意のアイテムIDを入力させる
item_id = st.text_input('item_idを入力してね')


# 入力したアイテムIDについてのitem2itemの推薦結果を表示する
pred_items: list[str] = predict(item_id, frame) # 適当な関数を想像してください
for item_id in pred_items:
    img_url = get_image_url(item_id) # 適当な関数を想像してください
    st.write(item_id)
    st.image(img_url)

詳しくは公式ドキュメントに任せますが、非常に多くのインターフェースが用意されています。
システムの入出力に決まった形式のあるわけではない推薦モデルのようなシステムを再現して定性評価をするようなケースにおいては、このようなインターフェースが豊富であることは有用な点かなと感じています。

streamlit用のサーバーを簡単にホストできる

streamlitはローカルでも実行できますが、アプリを公開するためにはサーバーを建ててそこでアプリをホストしたいです。
ここについても、streamlit側では簡単にサーバーを建てることのできる機能が用意されています。
ホスト環境でstreamlit run app.pyのようにコマンドを打つだけでサーバーを建てることができます。

コードの変更がリアルタイムに反映されるのでプロトタイピングが楽

これは推薦システムの定性評価にメリットがあるというよりはフロントの知識が少ないエンジニアにとってのメリットです。
streamlitでホストしたアプリはコードを変更すると自動的にリロードされる仕組みになっています。
これの恩恵としては、コードを変更するたびに手動でサーバーをホストしなおす必要がないためプロトタイピングをしやすいという点があります。
アプリ開発の時間を少なく抑えて価値提供(今回の場合定性評価ができるようになること)を素早く行うという観点で地味ながら便利な点です。

streamlitのちょっとコツがいる部分

これまで説明してきたように便利な点が多いstreamlitですが、ちょっとコツがいる部分もあります。
特に機械学習モデルをハンドルするという観点で気にしたい点をいくつか紹介します。

重めの処理をする場合はキャッシュを効かせる必要がある

streamlitはインターフェースへのインタラクションあるいはコードの変更のたびにサーバー側でコード全体の再実行が走る仕組みになっています。
そのため、例えば巨大なデータのロードや重めの前処理など実行時間が長くなるようなコードを書いている場合は再実行のたびに再度実行時間の長い処理が走ってしまいます。
このような場合にはstreamlitの機能でキャッシュを効かせることでこれを防ぐことができるのですが、この使い方に少しコツが必要です。

簡単には次のようにオブジェクトを取得する関数にデコレーターをつけることでキャッシュを効かせることができます。

@st.cache_data
def get_data():
    # 重めの処理
    return data

基本的にはデコレーターをつけた関数は、引数が同じであればキャッシュを効かせてくれるので、このように書くだけでキャッシュを効かせることができます。
なのですが、このデコレーターを使う場合にはいくつか注意しなければいけない点があります。

まずはキャッシュに使うデコレーターの種類についてです。
キャッシュのために使用するデコレーターには次の2種類が存在します。

  • st.cache_dataを使う方法

    • pythonでシリアライズ可能なオブジェクトをキャッシュできる
    • キャッシュはセッションベースで行われる
  • st.cache_resourceを使う方法

    • シリアライズ可能でないオブジェクトをキャッシュできる
    • キャッシュはglobalで行われる

どのようなオブジェクトをどのようにキャッシュしたいのかによってこれらを使い分ける必要があります。

次にデコレーターをつける関数の引数についてです。
キャッシュを使うか関数を実行するかは同じ引数を使っているかどうかで決まるのですが、ここの引数はハッシュ可能なものでなければなりません。
なので関数の引数についてはハッシュ可能なものを与えるように実装する必要があります。

まだできてないけどやりたいこと

実験管理との統合

推薦モデルの開発においては、モデルの実験設定や評価結果などを実験管理しています。
現状はアプリ側でこの実験管理の結果を取得して、ベストモデルの推薦結果を見られるようにしています。
ただしこの方法だと過去のモデルと最新のモデルの結果の定性的な比較ができません。
モデルの実験設定によってどのようにモデルの推薦結果が変わるのかが分かると、実験結果をより深く解釈できることが期待できます。
そこで定性評価のアプリと実験管理結果の連携部分を改善して、任意の実験設定で生まれたモデルに対する推薦結果を見られるような仕組みにしたいです。

テンプレート化

現状だと新しい推薦モデルを開発した際に、アプリ側の実装も新しく作っています。
アプリ側に関しては複数箇所について汎用的に表現できそうなことが実装を進める中でわかってきました。
よって汎用表現できる部分はテンプレート化することで、初期実装の開発コストを下げてより早く定性評価を回せるようにしたいです。

実際のプロダクト上と同じ表示のシミュレート

推薦結果を定性評価するにあたって、実際のプロダクト上と同じ表示をシミュレートしたいとも考えています。
理由としては、実際にプロダクト上で見られる推薦結果とただ単に推薦結果を並べただけのものでは、同じアイテムが並んでいたとしても観測され方に異なるバイアスが発生し得るからです。
例としては次のようなケースでバイアスが発生し得ると考えています。

  • プロダクト上では推薦結果をn列m行で表示している場合、ただ配列としてアイテムを並べただけとは発生するポジションバイアスが異なる
  • プロダクト上では推薦結果のおすすめ順ごとに表示するアイテムのサイズが異なる

このようなケースを考慮することで、単に推薦結果を並べただけでは気付けないバイアスを事前に発見できるメリットがあると考えられるため、可能な限り実現したい要素です。

まとめ

今回ご紹介したように、streamlitはひとりのMLエンジニア・データサイエンティストが出せる価値の幅を広くするツールとして有用だと感じています。
推薦モデルの定性評価の手法の選択肢の1つとして、streamlitを検討してみるのも良いのではないでしょうか。

ブックリスタのレコメンドチームでは様々な角度からより良い推薦をより高速に提供するための取り組みをしています。
またブックリスタでは推薦システムの開発を加速させるため、現在MLOpsエンジニアを募集中です。
興味のある方は是非ご応募ください。