watsonx.aiでCPLEXのコードを読み解く

# はじめに
IBMの生成AIプラットフォームであるwatsonx.aiを使って、数理最適化に利用されるIBM ILOG CPLEXまたはIBM Decision Optimizationのコードを自然言語で解説させてみました。
# 設定課題
以下の記事を参考にして、オンライン飲み会の部屋割りを最適化するコードを使います。

https://www.ibm.com/blogs/solutions/jp-ja/life-spss-01/

ざっくり設定課題を解説するとAからFまでの6つの部署に所属している60人の社員がおり、各社員を6人ずつ10部屋に振り分けます。それぞれの社員には社員番号が割り振られています。（詳細は元記事を参照してください）
この飲み会では以下の制約条件を満たすように部屋割りを行っています。

制約1　１つの部屋には、既存社員３人と新入社員３人が入る。
制約2　同じ部署から最大４人が参加しなければならない。
制約3　新入社員は、その担当アドバイザーと同じ部屋でなくてはならない。
制約4　部署AとBの社員は同じ部屋には入れない。
制約5　部署EとFの社員は同じ部屋には入れない。
制約6　社員5は、社員41または社員51のどちらかと同じグループでなくてはならない。
制約7　社員15は、社員40か社員51のどちらかと同じグループでなくてはならない。
制約8　社員25は、社員40か社員50のどちらかと同じグループでなくてはならない。
制約9　社員20は社員24と同じグループか、社員22は社員50と同じグループでなくてはならない。

それらの設定課題をコード化したものが下記になります。今回は<INS>コード化された制約条件を自然文に戻してみる</INS>ということに挑戦します。
https://github.com/masakam-cloud/teambuilding

コードの中で制約条件は下記のように書かれています。

```python:Team Building.ipynb
for t in rooms:
    # 1部屋の既存社員は3人（1）
    l_existingemployee = [room[existingemployee] for existingemployee in persons if existingemployee%2==1]
    mdl.add(mdl.count(l_existingemployee, t)==3)
    # 1部屋の新入社員は3人（1）
    l_newemployee = [room[newemployee] for newemployee in persons if newemployee%2==0]
    mdl.add(mdl.count(l_newemployee, t)==3)
    # 同じ部署から最大4人が参加（2）
    for f in range(len(serviceNames)):
        l_person = [room[person] for person in service[f]]
        mdl.add(mdl.count(l_person, t)<=4)
# 部署AとBの社員は同じ部屋には入れない。(3)
for pA in service[serviceNames.index('A')]:
    for pB in service[serviceNames.index('B')]:
        mdl.add_constraint(room[pA] != room[pB])

# 部署EとFの社員は同じ部屋には入れない。(4)
for pE in service[serviceNames.index('E')]:
    for pF in service[serviceNames.index('F')]:
        mdl.add_constraint(room[pE] != room[pF])

# 新入社員は、その担当アドバイザーと同じ部屋でなくてはならない。(5)
for a in pairs:
    mdl.add(room[a.advisor]==room[a.advised])

# 社員5は、社員41または社員51のどちらかと同じ部屋でなくてはならない。(6)
mdl.add((room[5]==room[41]) | (room[5]==room[51]))

# 社員15は、社員40か社員51のどちらかと同じ部屋でなくてはならない。(7)
mdl.add((room[15]==room[40]) | (room[15]==room[51])) 

# 社員25は、社員40か社員50のどちらかと同じ部屋でなくてはならない。(8)
mdl.add((room[25]==room[40]) | (room[25]==room[50]))  

# 社員20は社員24と同じ部屋か、社員22は社員50と同じ部屋でなくてはならない。(9)
mdl.add((room[20]==room[24]) | (room[22]==room[50]))

```
このコードではコメントが書かれていますが、コメントがあってはズルになってしまうので、せっかくですが使用するプロンプトでは削除します。

# LLMに与えてみる
実際に与えたプロンプトは以下のとおりです。
```python: プロンプト
以下の数理最適化問題について記述したコードについて、制約条件を箇条書きで答えてください。

import sys
try:
    import docplex.cp
except:
    if hasattr(sys, 'real_prefix'):
        #we are in a virtual env.
        !conda install -y docplex
    else:
        !conda install -y --user docplex

from sys import stdout
from collections import namedtuple
from docplex.cp.model import *

url = None
key = None

nb_persons = 60
persons = range(nb_persons)
print("persons ", list(persons))

nb_rooms = 10
rooms = range(1, nb_rooms+1)
print("rooms ", list(rooms))

serviceNames = [chr(i) for i in range(65,65+6)]

print("serviceNames ",list(serviceNames))
print(serviceNames.index('A'))

a = range(0,20) 
b = range(20,40)
c = range(40,45)
d = range(45,50)
e = range(50,55)
f = range(55,60)
service = [set(a), set(b), set(c), set(d), set(e), set(f)]
print("service ",service)
print(service[serviceNames.index('A')])

pair = namedtuple("pair", ["advisor", "advised"])
pairs = [pair(0, 1), pair(2, 3), pair(4, 5), pair(6, 7), pair(8, 9), pair(10, 11), 
         pair(20, 21), pair(22, 23), pair(24, 25), pair(26, 27), pair(28, 29), pair(30, 31),
         pair(40, 41), pair(42, 43), pair(45, 46), pair(47, 48), pair(50, 51), pair(52, 53), pair(55, 56), pair(57, 58)]
print(pairs)

mdl = CpoModel(name='teambuilding')

room = mdl.integer_var_list(size=nb_persons, min=list(rooms)[0], max=list(rooms)[-1], name='person')

for t in rooms:
    l_existingemployee = [room[existingemployee] for existingemployee in persons if existingemployee%2==1]
    mdl.add(mdl.count(l_existingemployee, t)==3)
    l_newemployee = [room[newemployee] for newemployee in persons if newemployee%2==0]
    mdl.add(mdl.count(l_newemployee, t)==3)
    for f in range(len(serviceNames)):
        l_person = [room[person] for person in service[f]]
        mdl.add(mdl.count(l_person, t)<=4)

for pA in service[serviceNames.index('A')]:
    for pB in service[serviceNames.index('B')]:
        mdl.add_constraint(room[pA] != room[pB])

for pE in service[serviceNames.index('E')]:
    for pF in service[serviceNames.index('F')]:
        mdl.add_constraint(room[pE] != room[pF])

for a in pairs:
    mdl.add(room[a.advisor]==room[a.advised])

mdl.add((room[5]==room[41]) | (room[5]==room[51]))

mdl.add((room[15]==room[40]) | (room[15]==room[51])) 

mdl.add((room[25]==room[40]) | (room[25]==room[50]))  

mdl.add((room[20]==room[24]) | (room[22]==room[50]))

mdl.print_information()

print("\nSolving model....")
msol = mdl.solve(agent='local',TimeLimit=10, RandomSeed=1)

print("Solve status: " + msol.get_solve_status())
if msol.is_solution():
    stdout.write("Solve time: " + str(msol.get_solve_time()) + "\n")
else:
    stdout.write("No solution found\n")

msol.print_solution()

制約条件:
```

llama-3-405b-instructでの結果です。正しくリストアップできていることがわかります！
```python:llama-3-405b-instruct
1. 各部屋には3人の新入社員と3人の既存社員がいること。
2. 各部屋には同じ部門の社員が4人以下であること。
3. 部門Aの社員と部門Bの社員が同じ部屋にいないこと。
4. 部門Eの社員と部門Fの社員が同じ部屋にいないこと。
5. 指定されたペアの社員が同じ部屋にいること。
6. 部門Aの5番目の社員は、部門Cの41番目の社員か部門Eの51番目の社員と同じ部屋にいること。
7. 部門Bの15番目の社員は、部門Cの40番目の社員か部門Eの51番目の社員と同じ部屋にいること。
8. 部門Cの25番目の社員は、部門Cの40番目の社員か部門Eの50番目の社員と同じ部屋にいること。
9. 部門Bの20番目の社員は部門Cの24番目の社員と同じ部屋にいるか、部門Cの22番目の社員は部門Eの50番目の社員と同じ部屋にいること。

```

Mistral-largeでの結果、なんかグダグダ言っているのと、制約条件6-9がリストアップされていないようにみえます。
少しプロンプトをいじるとうまくいくかもしれませんが、あまり深堀りしないでおきます。
~~~python:Mistral-large
1. 各部屋には、既存の従業員（奇数番号）が3人、新しい従業員（偶数番号）が3人いる。
2. 各部屋には、各サービス（A, B, C, D, E, F）の従業員が最大4人までいる。
3. サービスAの従業員とサービスBの従業員は同じ部屋にいない。
4. サービスEの従業員とサービスFの従業員は同じ部屋にいない。
5. ペアとして指定された従業員は同じ部屋にいる。
6. 特定の従業員のペアが同じ部屋にいるか、他の特定の従業員と同じ部屋にいる。

このコードには、以下の制約条件が含まれています：

1. 各部屋には、既存の従業員（奇数番号）が3人、新しい従業員（偶数番号）が3人いる。
2. 各部屋には、各サービス（A, B, C, D, E, F）の従業員が最大4人までいる。
3. サービスAの従業員とサービスBの従業員は同じ部屋にいない。
4. サービスEの従業員とサービスFの従業員は同じ部屋にいない。
5. ペアとして指定された従業員は同じ部屋にいる。
6. 特定の従業員のペアが同じ部屋にいるか、他の特定の従業員と同じ部屋にいる。

具体的には、以下のように制約条件が設定されています：

1. 各部屋には、既存の従業員（奇数番号）が3人、新しい従業員（偶数番号）が3人いる。
   ```python
   for t in rooms:
       l_existingemployee = [room[existingemployee] for existingemployee in persons if existingemployee%2==1]
       mdl.add(mdl.count(l_existingemployee, t)==3)
       l_newemployee = [room[newemployee] for newemployee in persons if newemployee%2==0]
       mdl.add(mdl.count(l_newemployee, t)==3)
   ```

2. 各部屋には、各サービス（A, B, C, D, E, F）の従業員が最大4人までいる。
   ```python
   for t in rooms:
       for f in range(len(serviceNames)):
           l_person = [room[person] for person in service[f]]
           mdl.add(mdl.count(l_person, t)<=4)
   ```

3. サービスAの従業員とサービスBの従業員は同じ部屋にいない。
   ```python
   for pA in service[serviceNames.index('A')]:
       for pB in service[serviceNames.index('B')]:
           mdl.add_constraint(room[pA] != room[pB])
   ```

4. サービスEの従業員とサービスFの従業員は同じ部屋にいない。
   ```python
   for pE in service[serviceNames.index('E')]:
       for pF in service[serviceNames.index('F')]:
           mdl.add_constraint(room[pE
~~~

コードモデルである、granite-34b-code-instructでも試してみました。当然といえば当然なのですが、日本語があまり得意でないのでルー大柴みたいになっているのと、制約条件もうまくリストアップされていません。
英語でプロンプトを作成して試してもみたのですが、期待している回答は得られませんでした。
~~~python:granite-34b-code-instruct
- 人員は2名ずつのグループに分かれている
- A社員とB社員が同じ部屋にいることはない
- E社員とF社員が同じ部屋にいることはない
- 専門家同士は same room
- 専門家同士以外は different room
~~~

同じくコードモデルのcodellama-34b-instruct-hfでも試してみました。こちらも制約条件を網羅できていませんでした。
~~~python:codellama-34b-instruct-hf
* 部屋は1から10まである
* 新入社員と既存社員がそれぞれ3人ずつ入っていなければならない
* AサービスとBサービスは同じ部屋には居られない
* EサービスとFサービスは同じ部屋には居られない
* アドバイザーとアドバイズされる関係は同じ部屋にいなければならない
~~~

# まとめ
いくつかモデルを試してみましたが、llama-3-405b-instructではしっかりとコードが読み解けて、正確に制約条件をリストアップできていました。やはりパラメータ数が多いモデルは強いですね。一方特にモデルごとのプロンプトフォーマットなどを意識せずに実施したり、few-shot-learningなどを実施しなかったので、そのあたりのテクニックを駆使すればうまくいくかもしれません。