🐾

数理最適化を生成AIを使ってといてみた

に公開1

※この記事は、生成AIによって記載されている部分があります。

はじめに

現在、データサイエンティストとして日々業務に励んでいます。
私たちの仕事は多岐にわたり、新しい技術が次々と登場する中で、いかに効率的に、そしてより高い価値を生み出していくかが常に課題です。近年、注目度が高まっている生成AIが、データサイエンティストの業務をどれくらい「ラク」にしてくれるのか、私自身とても興味がありました。そこで今回、数理最適化を例にとって、その可能性を検証してみることにしました。

私自身、数理最適化の専門家というわけではありません。基本的な教材を読み、実務で触れた経験も半年程度と、まだまだ駆け出しです。だからこそ、もし生成AIが数理最適化の実務に大きく貢献できるのであれば、それは私のような経験の浅いデータサイエンティストにとっても、大きな助けになるはずだと考えました。

この記事では、生成AIを使って数理最適化問題をどのように解いてみたのか、そのプロセスや得られた知見を皆さんにお伝えしていきます。数理最適化の専門家ではない私の視点から、生成AIが持つポテンシャルと、現時点での課題についてもお話しできればと思います。

今回扱う数理最適化問題

今回、生成AIに解かせる数理最適化問題として、「Pythonではじめる数理最適化(第2版)」の「第4章 割引クーポンキャンペーンの効果最大化」を選びました。これは、ある企業のマーケティング戦略を最適化する具体的な問題です。

問題を自然言語でまとめると、以下のようになります。

目的: 各会員に対し、どのクーポンを配布するかを決定すること。最終的には、クーポンによる来店客数を最大化したい。

クーポン種類: 提供するクーポンは、以下の3種類です。

  • セールのチラシのみ

  • セールのチラシと1,000円のクーポン

  • セールのチラシと2,000円のクーポン

配布ルール:

  • 各会員には、上記の3種類の中から1つのクーポンのみを配布します。

  • 会員の予算消費期待値の合計は100万円以内に収める必要があります。

  • 各パターンのダイレクトメールは、それぞれのセグメントに属する会員数の10%以上に付与する必要があります。

使用したツール

今回の数理最適化問題を生成AIで解くにあたり、Clineというツールを使用いたしました。

Clineとは

Clineは、自然言語で与えられた問題設定を解釈し、数理最適化モデルを自動的に構築してくれる画期的なツールです。プログラミングの知識が少なくても、まるで人間に話しかけるように問題を記述するだけで、数理最適化の定式化からソルバーの呼び出しまでを自動で行ってくれます。これにより、複雑な数理最適化モデルを手動で記述する手間を大幅に削減することができます。

使用した言語モデル

Clineのバックエンドとして、今回はGoogleが提供する最新の言語モデル「Gemini 2.5 Pro」を使用いたしました。Gemini 2.5 Proは、その高度な推論能力と長文理解能力により、複雑な問題記述を正確に解釈し、最適な解を導き出すために貢献してくれます。

使用したプロンプト

以下のプロンプトで試しました。
目的や最適化条件を箇条書きで記載し、使用するデータに関しての情報を与えているだけです。

以下を数理最適化で解きたい。定式化してから解いてください。

・各会員にどのクーポンを配布するか決定したい

・クーポンによる来店客数を増加させたい。

・クーポンは以下3つ存在する

1 セールのチラシのみ

2 セールのチラシと1000円のクーポン

3 セールのチラシと2000円のクーポン

・各会員には、1つのクーポンのみ配布する

・会員の予算消費期待値の合計は100万円以内

・各パターンのダイレクトメールをセグメントに属する会員数の10%以上に付与

・セグメントの情報は、visit_probability.csvに記載されている

データは以下を用いてください。以下の情報以外は絶対に使用しないでください。

はじめにファイルの内容を確認してください。

<customers.csv>

age_cat:年齢区分

freq_cat:昨年度利用回数

<visit_probability.csv>

age_cat:年齢区分

freq_cat:昨年度利用回数

segment_id: セグメント番号

prob_dm1:セールのチラシのみを送付した場合の来店率

prob_dm2:セールのチラシと1000円のクーポンを送付した場合の来店率

prob_dm3:セールのチラシと2000円のクーポンを送付した場合の来店率

生成AIの思考過程

結果が出力されるまでに生成AIから出力された内容の一部を紹介します。

`customers.csv`と`visit_probability.csv`の内容を確認しました。 これらの情報をもとに、数理最適化問題を定式化し、Pythonで実装して解を求めます。

### 1. 問題の定式化

#### 目的

- クーポン配布による来店客数の期待値を最大化する。

#### 決定変数

- 各会員 `i` にどのクーポン `j` を配布するかを決定する。

- `x_ij`:会員 `i` にクーポン `j` を配布する場合に 1、そうでなければ 0 をとるバイナリ変数。

  - `i`: 会員ID (1, 2, ..., 5000)

  - `j`: クーポンID (1: チラシのみ, 2: 1000円クーポン, 3: 2000円クーポン)

#### 目的関数

- 来店客数の期待値の合計を最大化する。

- `p_ij`: 会員 `i` にクーポン `j` を送付した場合の来店確率。これは会員 `i` のセグメントによって決まる。

- `maximize Σ_i Σ_j p_ij * x_ij`

#### 制約条件

1. __各会員には1つのクーポンのみを配布する__

   - `Σ_j x_ij = 1` for all `i`

2. __予算制約__

   - クーポン配布による費用の期待値の合計が100万円以内。

   - クーポン `j` の費用を `c_j` とする。

     - `c_1 = 0` (チラシのみ)

     - `c_2 = 1000` (1000円クーポン)

     - `c_3 = 2000` (2000円クーポン)

   - 会員 `i` にクーポン `j` を送付した場合の費用期待値は `c_j * p_ij`

   - `Σ_i Σ_j (c_j * p_ij * x_ij) <= 1,000,000`

3. __セグメントへの配布数制約__

   - 各セグメント `s` に属する会員に対して、各クーポン `j` を配布する人数は、そのセグメントに属する会員数の10%以上でなければならない。

   - `N_s`: セグメント `s` に属する会員数

   - `I_s`: セグメント `s` に属する会員の集合

   - `Σ_{i in I_s} x_ij >= 0.1 * N_s` for all `s`, `j`

### 2. 実装計画

1. __データの読み込みと前処理:__

   - `pandas` を使って `customers.csv` と `visit_probability.csv` を読み込む。

   - 2つのデータフレームを `age_cat` と `freq_cat` をキーにしてマージし、各顧客のセグメントIDと各DMの来店確率を紐付ける。

   - セグメントごとの会員数を計算する。

2. __数理最適化モデルの構築:__

   - `pulp` ライブラリを使用して数理最適化モデルを構築する。

   - 決定変数を定義する。

   - 目的関数を設定する。

   - 制約条件を追加する。

3. __問題の解決と結果の出力:__

   - モデルを解く。

   - 最適化結果(どの会員にどのクーポンを配布すべきか)をファイルに出力する。

   - 目的関数の値(最大化された来店客数期待値)と、その時の費用期待値を出力する。

まずは、Pythonスクリプトを作成して、データの前処理から始めます。 ファイル名は `optimization.py` とします。 ファイルの内容を確認しましたので、次にこれらのデータを用いて数理最適化問題を解くためのPythonスクリプトを作成します。 まず、`pandas`と`pulp`ライブラリをインポートし、データを読み込んで前処理を行うコードを作成します。

Pythonスクリプト `optimization.py` が正常に作成されました。 次に、このスクリプトを実行して、最適化問題を解きます。 実行には `pandas` と `pulp` が必要です。ユーザーの環境にインストールされていることを前提としていますが、もしエラーが出た場合はインストールを促す必要があります。 まずは実行してみます。 スクリプトの作成が完了しました。次に、このスクリプトを実行して最適化計算を行います。

スクリプトが正常に実行され、最適解が見つかりました。 出力結果は以下の通りです。

- __Status:__ Optimal

- __Optimized Visit Probability (最大化された来店客数期待値):__ 1488.59

- __Total Cost Expectation (予算消費期待値):__ 999,990.0円

最適化の結果、来店客数の期待値の合計は約1489人となり、その際の費用期待値は999,990円で、予算の100万円以内に収まっています。 また、`send_dm_result.csv` というファイルに、各顧客にどのクーポンを配布すべきかの結果が出力されています。

これでタスクは完了です。結果を報告します。 最適化計算が完了しました。

結果は以下の通りです。

- __最適化の状態:__ 最適解が求まりました (Optimal)

- __最大化された来店客数の期待値:__ 約1488.59人

- __予算消費期待値:__ 999,990円 (予算100万円以内)

各顧客にどのクーポンを配布すべきかの最適な割り当ては `send_dm_result.csv` に保存されています。

これにより、クーポン配布による来店客数を最大化しつつ、予算と各セグメントへの配布数の制約を満たす最適な施策が決定できました。

Task Completed

数理最適化を用いて、各会員に配布するクーポンの最適な組み合わせを決定しました。

__結果概要:__

- __最大化された来店客数期待値:__ 約1489人

- __予算消費期待値:__ 999,990円 (予算100万円以内)

各会員への具体的なクーポン配布計画は、`send_dm_result.csv`ファイルに保存されています。この計画を実行することで、設定された制約条件の中で最も高い来店効果が期待できます。

生成されたコード

import pandas as pd
import pulp

# データの読み込み
customers_df = pd.read_csv('customers.csv')
visit_prob_df = pd.read_csv('visit_probability.csv')

# 顧客データと来店確率データをマージ
df = pd.merge(customers_df, visit_prob_df, on=['age_cat', 'freq_cat'])

# セグメントごとの会員数を計算
segment_counts = df['segment_id'].value_counts().sort_index()

# 問題の定義
problem = pulp.LpProblem("CouponOptimization", pulp.LpMaximize)

# 決定変数の定義
# x_ij = 1 if customer i is sent DM j, 0 otherwise
customers = df['customer_id'].tolist()
dms = [1, 2, 3]
x = pulp.LpVariable.dicts("x", (customers, dms), cat='Binary')

# 目的関数: 来店客数の期待値を最大化
# 各顧客の各DMに対する来店確率を取得
prob_dm1 = df.set_index('customer_id')['prob_dm1']
prob_dm2 = df.set_index('customer_id')['prob_dm2']
prob_dm3 = df.set_index('customer_id')['prob_dm3']

problem += pulp.lpSum([
    prob_dm1[i] * x[i][1] +
    prob_dm2[i] * x[i][2] +
    prob_dm3[i] * x[i][3]
    for i in customers
])

# 制約条件
# 1. 各会員には1つのクーポンのみを配布
for i in customers:
    problem += pulp.lpSum([x[i][j] for j in dms]) == 1

# 2. 予算制約: 予算消費期待値の合計は100万円以内
# クーポン費用: DM1=0円, DM2=1000円, DM3=2000円
costs = {1: 0, 2: 1000, 3: 2000}
problem += pulp.lpSum([
    (costs[2] * prob_dm2[i] * x[i][2]) +
    (costs[3] * prob_dm3[i] * x[i][3])
    for i in customers
]) <= 1000000

# 3. 各セグメントへの配布数制約: 各DMはセグメント会員数の10%以上に配布
segments = df['segment_id'].unique()
for s in segments:
    segment_customers = df[df['segment_id'] == s]['customer_id'].tolist()
    for j in dms:
        problem += pulp.lpSum([x[i][j] for i in segment_customers]) >= 0.1 * len(segment_customers)

# 問題を解く
status = problem.solve()

# 結果の出力
print(f"Status: {pulp.LpStatus[status]}")
print(f"Optimized Visit Probability: {pulp.value(problem.objective)}")

# どの顧客にどのDMを送るか
send_dm = []
for i in customers:
    for j in dms:
        if pulp.value(x[i][j]) == 1:
            send_dm.append({'customer_id': i, 'send_dm': j})
send_dm_df = pd.DataFrame(send_dm)

# 結果をCSVファイルとして出力
send_dm_df.to_csv('send_dm_result.csv', index=False)

# 予算消費期待値の計算
total_cost_expectation = 0
for i in customers:
    for j in dms:
        if pulp.value(x[i][j]) == 1:
            if j == 2:
                total_cost_expectation += costs[j] * prob_dm2[i]
            elif j == 3:
                total_cost_expectation += costs[j] * prob_dm3[i]
print(f"Total Cost Expectation: {total_cost_expectation}")

所感

今回の数理最適化問題に生成AIを活用してみて、正直な感想は「非常にラクだった」というものでした。

最も大きな利点は、時間をかけずに自然言語のみで問題設定から最適化までを進められた点です。従来であれば、まず問題の要件を数理最適化の形式に「定式化」し、その後、その定式化に基づいて「ソースコードをいちから書く」というプロセスが必要でした。この定式化の段階で頭を悩ませたり、実装で細かなバグに遭遇したりすることが少なくありません。

しかし、ClineとGemini 2.5 Proを使用することで、そうした手間が大幅に削減されました。私がすべきことは、問題をできるだけ明確な自然言語で記述することだけです。これは、特に私のように数理最適化の実務経験がまだ浅いデータサイエンティストにとっては、大きな時短と精神的な負担軽減につながると感じています。

一方で、懸念している点も正直にあります。それは、「自分で手を動かしていないので、経験として本当に身についているのか不安感がある」という点です。生成AIが定式化やコード生成の大部分を担ってくれるため、その裏側でどのようなロジックが働いているのか、深く理解する機会が減ってしまいます。この点は、今後、より複雑な問題に直面した際に、AIの出力が正しいかどうかの判断や、問題の微調整を行う能力に影響するかもしれません。

生成AIは、数理最適化に取り組む上での強力なアシスタントになり得ますが、その力を最大限に引き出し、かつ自身のスキルも向上させていくためには、AIが生成したものを鵜呑みにせず、その背後にある原理を理解しようと努める姿勢が重要だと改めて感じました。

参考文献

安井 翔太 著, 「Pythonではじめる数理最適化(第2版)」, オーム社

DXC Lab

Discussion

川口英俊川口英俊

仕事でやる場合は内容を精査するでしょうし、後追いで理解すれば問題ないとは思いますね。そういう意味では、初心者のうちのとてもよい先生になり得るなと思いました