AI駆動の実験設計──VibeExpDesign エージェントとつくる実験デザイン
時代はAI駆動開発(VibeCoding)です。最適化も実験計画もAI駆動で行きます。
まず孔明先生からこのブログの概要解説を戴きました。拝読しましょう↓
【孔明の眼】AI駆動の実験設計──VibeCodingの妙
時代は変わった。かつては人の手で練り上げた実験計画も、今やAIの助けを借りて、瞬く間に最適化される時代となった。これは、まさに「知を以て事を制す」時代の到来である。
今回、我が友が記したブログは、まさにその先端を行く内容である。彼は「VibeCoding」という新たな流儀を用い、ColaboratoryのGeminiとPuLPを駆使して、複雑な実験条件の最適化を見事に成し遂げた。
神算鬼謀
実験とは、乱れた情報の海に秩序をもたらす行為である。だが、順序や組み合わせによって意図せぬ影響が混入することもある。制約が多く、定型的な方法が通用しない場面もある。そこで登場するのが、AIエージェントによる整数計画法──まさに、現代の「神算鬼謀」である。
Colaboratory上でGeminiに命じ、PuLPを用いた最適化モデルを構築した。店舗・商材・販売方法という三つの軸を持つ実験条件を、バランスよく割り当てるための数理モデルを、AIが自動生成する様は、まさに「軍師の策を筆にて描くが如し」。
制約条件の定義、目的関数の設計、そして例外条件の追加まで、すべてが明快に記述されており、Geminiの助力によって、複雑な数式も平易に扱えるようになっている。
このような実践は、AI駆動開発の未来を照らす灯火となるであろう。
過分なご評価恐縮です。
ではここから本文です。
前回は実験デザインの考え方について書きました。
今回は具体的な実験組み合わせの最適化の話をします。
以前のブログでお話ししたように、実験は注意するべき点がおおくあります。特に実験の順番や組み合わせで意図しない影響が混入することがあります。
これを回避する一番簡単な方法は完全ランダムに行うことです。
専門的には乱塊法といいます。
しかし、乱塊法では確率的なので場合によっては偏った組み合わせができてしまうことがあります。
バランスよく行うために考え出されたのが、直交計画です。
これを使うと、実験をすべて行わなくてもバランスの良い組み合わせを作ることができます。
ですが、
実験条件の数や様々な制約があって、そういった決まった方法が適用しにくいこともあります。
かといって、自分で探索するのもコーディングに時間かかりそうと躊躇するのではないでしょうか。
今回はお手軽にエージェントの力を借りてPythonの整数計画をつかった実験計画について解説します。
整数計画法というのは数学的な手法で組み合わせ問題を解く方法で、問題を数式で定義する必要があります。ある組み合わせはできないので除外したいとかいった条件を数式にするのは、ハードル高めです。
しかし、最近はエージェントというミカタがいます。
正にエージェントのおかげで面倒な処理が簡単に行えるようになりました。
今回はcolaboratoryのGeminiを使ってみましたが、Copilotなどでもできます。
準備
まず、実験計画するにあたっての想定を考えます。
ありがちな状況として、
いくつかの小売店店舗がいくつかの商材(商品)をあつかっており、
そこで販売方法を変えた時に売り上げが変わるかという実験をしたいものとします。
店舗ごとに立地条件や客層の違いがあり売り上げ規模が違う場合、複数店舗の違いを単純に比較することができません。
販売方法は何でもいいですが、方法1,2,3のように番号を付けておきます。
ここでは、一店舗一商材で一つの方法しか指定できないものとします。
一つの店舗で複数の方法を試すのはハードルが高い場合がありますが、
店舗×商材で複数の方法があれば、客としては得な方しか購入しないのでまずありえないです。
この条件で
まず組み合わせを作ってみます。
例えば下のようにして組み合わせを作りますが、
「店舗と商材と販売方法の3条件で組み合わせを作ってDataFrameとして保存してください。」などとすれば作ってくれます。
import pandas as pd
import numpy as np
# Define the number of stores, products, and sales methods
num_stores = 4
num_products = 5
num_methods = 3
# Create lists for stores, products, and methods
stores = [f'Store_{i+1}' for i in range(num_stores)]
products = [f'Product_{i+1}' for i in range(num_products)]
methods = [f'Method_{i+1}' for i in range(num_methods)]
# Generate sample data
data = []
for store in stores:
for product in products:
for method in methods:
data.append([store, product, method])
# Create a pandas DataFrame
df = pd.DataFrame(data, columns=['Store', 'Product', 'Method'])
まず問題を定義せよ
PuLPを使って問題を定義します。
PuLPとは線形計画問題や整数計画などの最適化問題を解くためのPythonライブラリです。
ざっくりいうと問題を数式にして目的関数と制約式を登録して解いてもらいます。
しかし、この数式にするというところがハードル高いと思われるので、ここをVibeCodingで作ってもらいます。
colaboratoryを開くと下の方にgeminiの入力欄があります。
基本的にここでお願いするとできます。
今回はColaboratoryのGeminiにお願いして作ってもらいました。
丁寧にコメントをつけてくれるので自分で書くよりわかりやすいかも。
Geminiはすでに書かれたコードやテキストも理解したうえでコードを書いてくれます。
とはいえ複雑な指示は無視されたり誤解されたりすることもあるので、
詳細はいったんテキスト(markdown形式)で記述してから命令を簡潔にするといいです。
例えばこんな感じです。
prompt:
2店舗で3商材について販売方法1,2,3について実地検証する計画を立てます。 pulpを使ってバランスよく組み合わせを作ってください。 店舗内で商材毎に一つの方法のみ割り当ててください。
Geminiの回答
承知いたしました。PuLPを使って、2店舗の3商材に販売方法1, 2, 3をバランス良く割り当てる計画を作成するための計画を提案します。
問題設定: 線形計画問題として定式化するために、変数、目的関数、制約条件を定義します。
変数の定義: 各店舗の各商材にどの販売方法を割り当てるかを表す二値変数を定義します。
目的関数の定義: バランスの取れた組み合わせにするための目的関数を定義します。例えば、各販売方法が全体として均等に割り当てられるようにするなどです。
制約条件の定義: 各店舗の各商材には一つの販売方法のみが割り当てられるという制約を設定します。
モデルの構築と解法: PuLP を使ってモデルを構築し、問題を解きます。
結果の表示: 最適な割り当て結果を表示します。
Finish task: 計画と結果をまとめ、ユーザーが理解しやすい形式で提示します。
ここで、承認して実行を押すと実行が始まります。簡単!
最近はエラーが起きると、エラーが解消されるまで修正してくれるようになりました。
店舗(Store)、商材(Product)、販売方法(Method)に関して
組合せの変数を作ります。
変数が多い時はこの作り方がいいです。
import pulp
import pandas as pd
# Create the LP problem instance
prob = pulp.LpProblem("Experimental Design Problem", pulp.LpMinimize)
# Create a list of all possible combinations
combinations = df[['Store', 'Product', 'Method']].values.tolist()
# Create binary decision variables for each combination
x = pulp.LpVariable.dicts("select", combinations, cat='Binary')
# Set the objective function to 0 for a feasibility problem
prob += 0
できてきたコードはわかりやすく説明を付けてくれていますが、英語なので省略して、私がコメントを付けなおしています。
import pulp
# 1. Define the sets for stores, products, and methods
stores = ['Store A', 'Store B']
products = ['Product 1', 'Product 2', 'Product 3']
methods = ['Method 1', 'Method 2', 'Method 3']
# 2. Define the decision variables
# storeとproduct、methodの3次元のセルを作っています。x[s][p][m] は1か0が入る箱です。1が入っているものが採用されるものという意味です。
x = pulp.LpVariable.dicts("assign",
((s, p, m) for s in stores for p in products for m in methods),
cat='Binary')
# 3. 問題を定義します。最小化します。
prob = pulp.LpProblem("Sales Method Assignment", pulp.LpMinimize)
1回命令すると全部うまいこと作ってくれていましたが、解説の都合上分割しています。
制約・条件を決める
で次は問題の制約を決めます。というかgemini先生が作ってくださったpulpのコードの制約部分を解説していきます。
PuLPでは
さきほどLpProblemで作った"prob"というオブジェクトに"+="演算子で追加していく形で、関係式を追加していきます。
だいたい想像つくとは思いますが、"lpSum"は加算をあらわします。
つまり下の1の部分では、各店舗各商材で手法が一つだけ1になるということを表しています。
制約なども一つ一つ関係式をループで追加していきます。
# 制約式を定義します。
# Constraint 1: 店舗の商材毎に1手法を割り当てるという制約です。methodの方向で採用されるものは1という風にコーディングしています。
for s in stores:
for p in products:
prob += pulp.lpSum(x[s, p, m] for m in methods) == 1, f"One method per product in {s} for {p}"
バランスを考える
割り当てバランスを定義して行きます。
geminiが作ったコードを解説します。
2と3で上限下限の変数を作り、6で上限を低く下限を大きくするよう追加しています。
なるほど~と思いました。
# 1. 各方法(method)方向の和としてmethod_assignmentsを定義
method_assignments = {}
for m in methods:
method_assignments[m] = pulp.lpSum(x[s, p, m] for s in stores for p in products)
# 2. 任意のメソッドの最大割り当て数を表す変数を導入
max_assignments = pulp.LpVariable("max_assignments", lowBound=0, cat='Continuous')
# 3. 最小割り当て変数
min_assignments = pulp.LpVariable("min_assignments", lowBound=0, cat='Continuous')
# 4. max_assignments が各方法の割り当て上限の制約を追加
for m in methods:
prob += max_assignments >= method_assignments[m], f"max_assignments_ge_{m}"
# 5. 割り当て下限制約
for m in methods:
prob += min_assignments <= method_assignments[m], f"min_assignments_le_{m}"
# 6. max_assignmentsを小さくmin_assignmentsを大きくする方向に
prob += max_assignments - min_assignments, "Minimize Assignment Difference"
最適解生成
長い問題設定が終わったので、最適解を生成しています。
probオブジェクトのsolveメソッドを呼ぶだけ。
実行時間は規模にもよります。
# Solve the problem
prob.solve()
# Check the status of the solution
print("Status:", pulp.LpStatus[prob.status])
結果表示もお願いすればやってくれます。
少し自分で手を入れてmarkdown形式で出力しています。
selected_combinations = []
for s in stores:
for p in products:
for m in methods:
if x[s, p, m].varValue == 1:
print(f"Store: {s}, Product: {p}, Method: {m}")
selected_combinations.append((s, p, m))
# Create a DataFrame from the selected combinations
selected_df = pd.DataFrame(selected_combinations, columns=['Store', 'Product', 'Method'])
print(selected_df.to_markdown(index=False))
結果
結果の組み合わせはこんな感じです。
Store | Product | Method |
---|---|---|
Store A | Product 1 | Method 2 |
Store A | Product 2 | Method 1 |
Store A | Product 3 | Method 1 |
Store B | Product 1 | Method 3 |
Store B | Product 2 | Method 3 |
Store B | Product 3 | Method 2 |
例外を付けたい
ある組み合わせはできないという場合が有り得ます。
StoreAはProduct1を扱ってはいるが、店舗のレイアウトの問題から方法Method2は適用できないとか有り得ます。逆にこの商材はこの方法しか無理など固定したい場合もあります。
たとえばこうします↓
prob += x['Store A', 'Product 1', 'Method 2'] == 0, "Exclude Store A Product 1 Method 2"
つまり、初めに説明したようにここでは三次元のセルをつくって、1が入っていたら採用、0だったら不採用ということで定義しました。なので、できない組み合わせは0を入れ、固定したい組み合わせは1を入れます。これだけです。
不安だったらGeminiへのプロンプトで分かりやすく説明すると実現できます。
最後に
ColaboratoryのAIによるVibeCodingを使った実験デザインについて解説しました。
コード生成は次第に進化しています。
便利なPuLPがあるとはいえ、数式でモデル化する必要がある最適化はハードル高いです。
でもColaboratoryのGeminiのおかげで驚くほど最適化計算や分析のコーディングが楽になり、時間短縮になりました。
文字列操作はまだ意図がよく伝わっていないと感じるときはありますが、概ね便利です。
副次的ですが、VibeCodingしたおかげでREADMEドキュメントや仕様を書くのがうまくなった気がします。
逆に今までは説明が足りなかったんだと気づかされたりしました。というか、今まで説明の足りない無茶なドキュメントばかりでごめんなさい。
Discussion