目次
初めに
今回の取り組みは、機械学習モデルの精度検証用の基盤を構築する中で、
コスト削減につながりそうな処理をどう設計すべきかを考えていたところから始まりました。
当初は「中間テーブルを使い回せばコストを下げられるのでは?」という発想で処理を見直していたのですが、
調査を進めていく中で、そもそもシングルトンという設計パターンが使われており、すでにコスト削減が実現されていたということに気づきました。
本記事では、
- シングルトンとは何か
- なぜこのケースで有効だったのか
- どのように基盤設計に組み込むべきか
といった点を、自分なりに言語化しています。
また、基盤構築の過程ではDRY原則(Don’t Repeat Yourself)をどこまで守るべきか という点にも悩みました。
単純にコードを共通化すれば良いわけではなく、「変更理由をどこに閉じ込めるか」という観点で設計を見直した過程にも触れています。
同じように、
- データ基盤・ML基盤のコストに悩んでいる方
- DRY原則の適用範囲に迷ったことがある方
の参考になれば嬉しいです。
本記事の結論・学び
- DRY原則の本質は「修正箇所を1か所にとどめる」こと。同じコードを書いてはいけない理由はこれ。
- シングルトンとは設計パターンの一種。無駄を省きたい、一貫性を保ちたいときに効果を発揮する。
- 顧客利益最大化を目指すと必然的にスキルアップにつながる。
前提:精度検証基盤とは
本記事で扱う「精度検証基盤」とは、
機械学習モデルの予測精度を自動的・継続的に検証するための基盤を指します。
対象となるモデルはバッチ処理形式で動作しており、
- 月に一度の本番環境での定期実行
- テスト・デバッグ時のテスト環境での実行
といった形で運用されています。
既存のパイプラインには「正常性確認処理」が組み込まれており、
推論結果全体がビジネス上の意思決定に利用可能な水準かどうかは確認されていました。
一方で、
- 推論結果一つ一つの精度
- モデルの劣化や異常を定量的に把握する仕組み
といった モデル精度そのものを検証する処理は存在していませんでした。
この状態では、モデルが「なぜ」その結果を出しているのか今の精度が妥当なのか、偶然良く見えているだけなのかを判断することが難しいと感じました。
そこでまず、
- 精度を定量的に測定できる基盤を作る
- 使用すべき評価指標は何か
- 特徴量は現状のままで問題ないのか
- 本番環境にそのまま適用してよいモデルなのか
といった点を事前に検証できる仕組みをとして、精度検証基盤を構築する判断をしています。
DRY原則の使い方を知れた話
まずはDRY原則の使い方を知れた話から。
- 状況の共有
- もともとやろうとしたこと
- DRY原則を用いて修正した結果
の流れで説明します。
状況の共有
精度検証における課題はコストでした。
以前も記事にしたのですが、1回のフル実行にかかるコストは5.5万円で、気軽に回せるものではありません。
https://zenn.dev/zenn_mita/articles/bd54d81c4dab31
そこでバックアップデータを使用して回せばコスト問題はクリアできると考えたのですが、次の課題としてあるのが源泉データの洗い替え問題。
下記のようなアーキテクチャになっているのですが、実行環境のS3にデータを格納してくれる源泉データが毎月半ばになると新規データに上書きされてしまい、一部中間テーブルがうまく作成できずに学習が回らないという事態に陥ります。

また、洗い替えされた後に学習を回してしまうと洗い替えされた後のデータがバックアップに入り込んでしまい、本番よりも少ないデータで学習するようになるため、検証が難しい状況でした。
そこで、精度検証用に特徴量データと中間テーブルのバックアップを固定でとり、洗い替えされてもそのテーブルは絶対に動かないような処理にしています。
ここで新たに課題となったのが、バックアップの有無による処理の切り替えでした。
もともとやろうとしたこと
上記を受けて、
「バックアップがあるかを確認してif分岐させるだけじゃね?」と単純に考えて実装したものが下記のようなもの。
def run_flow():
if not backup_exists: # バックアップがないときの処理
preprocess() # 前処理実行して中間テーブル作成
save_pretable() # 中間テーブルをバックアップ
for media in medias: # 各メディアごとに学習を回す
get_predict_score() # 学習
save_features_table() # 特徴量データをバックアップ
else: # バックアップがあるときの処理
get_pre_table() # 中間テーブルのバックアップを取得
get_features_table() # 特徴量データのバックアップを取得
for media in medias:
get_predict_score_by_backup() # 学習
上記で特に問題なく動くのだが、少し気になっていたのがfor media in mediasだけは処理が変わっておらず、その前後で条件分岐ができること。
DRY原則に照らし合わせると同じ処理を書くのはNGだと認識していたのですが、その同じ処理が1行しかなく、その1行のためにif-elseを2回書くべきなのかで迷いました。
DRY原則を用いて修正した結果
色々考えたあげく、
- DRY原則に従ったほうが従わないより客観的な判断を下したといえる
- 説明可能性が高まる
-
if-elseを分けたほうがバックアップの有無によってどの処理が変化するのかわかりやすい- 可読性の向上
- どっちの処理を採用しても処理時間もコストも実装負担もあまり変わらない
- どっちもどっちだから少しでもプラスの理由があるほうを選択
という理由でDRY原則に従うことにしました。
それが下記。
def run_flow():
if not backup_exists: # バックアップがないときの処理
preprocess() # 前処理実行して中間テーブル作成
save_pretable() # 中間テーブルをバックアップ
else:
get_pre_table() # 中間テーブルのバックアップを取得
get_features_table() # 特徴量データのバックアップを取得
for media in medias: # 各メディアごとに学習を回す
if not backup_exists:
get_predict_score() # 学習
save_features_table() # 特徴量データをバックアップ
else: # バックアップがあるときの処理
get_predict_score_by_backup() # 学習
ぱっと見冗長に見えなくもないですが、実際の処理だと細かな制御が間に挟まったり、バックアップ有無によって関数が違ったりするので、明確に処理を分割することによって今後の保守性の向上にも寄与できたかな、とも思ったりしています。
先人の知恵であるソフトウェア原則には従うべきだなと感じています。
シングルトンの使い方を知れた話
次に本題であるシングルトンの使い方を知れた話についてまとめます。
流れとしては、
- やろうとしたこと
- 現状の処理がどうなっていたのか
- シングルトンとは
という形でいきます。
状況は先ほどのDRY原則のお話と同じなので割愛します。
やろうとしたこと
本モデルは各メディアにおいて実績があるユーザーを学習データに使い、今まで実績のないユーザーに対してどのアプローチが効果的かを判断するために使用されるモデルです。
学習時と推論時でユーザーの状態(実績の有無)が異なることを前提としており、
いわゆるコールドスタート環境での利用を想定した推論モデルとして設計されています。
なので、このモデルでは各メディアごとにモデルが存在するものの、推論したい母集団は同じなので、推論データは1つのテーブルを使いまわせる状態でした。
処理をぱっと見したときに各メディアごとにDBからとってきているように見えたので、これを1度だけとって保持し続ければスキャンコストが落ちるんじゃないかなと考えていました。
現状の処理がどうなっていたのか
処理を深ぼったところ、あっさり答えは見つかりました。それが下記のような処理。
def create_inference_df():
global INFERENCE_DF
if INFERENCE_DF is None:
INFERENCE_DF = run_inference_preprocess()
return INFERENCE_DF
このコードでは、推論に使用する中間テーブルをグローバル変数として保持し、初回呼び出し時にのみ生成するようになっています。
一見すると単なるグローバル変数の利用で、アンチパターンにも見えますが、
実際にはこれは シングルトン(Singleton)パターン を用いた実装でした。
シングルトンとは
Javaエンジニア時代に聞いたことはあったのですが詳しく知らなかったので調べてみました。
シングルトンとは、「あるクラス(またはオブジェクト)が、アプリケーション内で常に1つだけ存在することを保証する設計パターン」だそうです。
つまりはグローバルにアクセスできる、唯一の状態を持つオブジェクト。
ポイントは1回しか作らないことではなく、どこから呼ばれても必ず同じインスタンスが返ること。
シングルトンは設定情報やDBコネクション、重い前処理結果などの性質である
- 作成コストが高い
- 複数回作る意味が薄い
- 状態を共有したい
- 一貫性を保ちたい
という課題を解決するために使用される設計なのだそう。
今回の事例に当てはめると、
- 推論用の中間テーブルはすべてのメディアにおいて共通
- 複数回データを作る意味がない
- 不要な再計算やDBアクセスを避けることで、コストを抑えたい
という課題がある状況だったのでまさにドンピシャ。
つまり、私が当初考えていた「中間テーブルを使い回すことでスキャンコストを削減する」
という施策は、すでにシングルトンによって実装されていたというわけです。
感想
今週は、振り返ってみると学びの多い1週間でした。
「なぜ今まで気づかなかったんだろう」と感じる場面も多く、自分の未熟さに気づかされることもありますが、日々の積み重ねなしに成長はないと思い、引き続き精進していきます。
最近は特に、「顧客への価値提供」という軸を意識するようになったことで、目の前の作業一つ一つに意味を見出せるようになったと感じています。
作業の意味が分かることでモチベーションにつながり、その結果として、難しい課題にも前向きに取り組めている感覚があります。
SESという働き方の性質上、顧客と直接接する機会は限られることも多いですが、だからこそ今できる範囲で最大限の価値を出すことを意識し続けたいと考えています。
その積み重ねが、将来的により直接的な価値提供につながる環境への移行にもつながっていくはずだと思っています。
こうした考え方ができるようになったのも、日々の学びや思考をアウトプットし、整理し続けてきた結果だと感じています。
イメージとしては点と点が少しずつ線になり始めている感覚です。
今後も「顧客への価値提供」「顧客利益の最大化」を軸に据えながら、試行錯誤の過程をアウトプットしていきたいと思います。