Optunaでおいしいコーヒーの淹れ方を探索する

# はじめに

年末年始の休みに入ってすでに4日目。休みでやろうと思ったことが一通り終わってしまいました。今日からいつも行っているカフェが休みだし、暇すぎ・・・ということで、勢いに任せてOptunaでコーヒーの淹れ方の最適化始めました。

はじめようと思い立ったのが今日なので、全然データがないですが、Google Colabで作ったサンプルコードもあげておきます。
https://github.com/shu65/coffee-tuning/blob/main/coffee_tuning_blog%E7%94%A8.ipynb

# Optunaとは？

Optunaは機械学習などで必要になるハイパーパラメータの最適化を自動で行ってくれるオープンソースのフレームワークです。個人的に気に入っている特徴としては、従来からハイパーパラメータ最適化を自動でやってくれるものはいくつかありますが、OptunaはDefine-by-RunスタイルのAPIで探索空間が柔軟に定義できるというところがあります。
詳しくは https://preferred.jp/ja/projects/optuna/

今回の目的に限って言えば、本来想定しているのと少し違う使い方をするので、わりと面倒化も？と思ったのですが、そこはOptuna先生。コードが綺麗なので中身の理解も容易なため、簡単に変な使い方もできました。

# コーヒーを淹れる環境
私のコーヒーを淹れる主な環境は以下の通り

* ミル: [Melitta(メリタ) 電動 コーヒーミル ブラック ECG62-1B](https://www.amazon.co.jp/Melitta-%E3%83%A1%E3%83%AA%E3%82%BF-%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC%E3%83%9F%E3%83%AB-%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF-ECG62-1B/dp/B01D2TIAFM)
* ポット: [dretec(ドリテック) 電気ケトル ステンレス コーヒー ドリップ ポット 細口 1.0L PO-135BK](https://www.amazon.co.jp/dretec-%E3%83%89%E3%83%AA%E3%83%86%E3%83%83%E3%82%AF-%E9%9B%BB%E6%B0%97%E3%82%B1%E3%83%88%E3%83%AB-%E3%82%B9%E3%83%86%E3%83%B3%E3%83%AC%E3%82%B9-PO-135BK/dp/B01GQ032MU)
* フレンチプレス: [BODUM ボダム KENYA ケニヤ フレンチプレス コーヒーメーカー 500ml ブラック 10683-01J](https://www.amazon.co.jp/%E3%80%90%E6%AD%A3%E8%A6%8F%E5%93%81%E3%80%91-BODUM-%E3%83%9C%E3%83%80%E3%83%A0-%E3%83%95%E3%83%AC%E3%83%B3%E3%83%81%E3%83%97%E3%83%AC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%92%E3%83%BC%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC-10683-01J/dp/B0018JXAK4)

# Optunaでコーヒーの淹れ方を探索する流れ

Optunaで探索する場合は以下のものが必要になります。

1. パラメータの探索空間
2. 目的関数

今回の場合、パラメータの探索空間は豆の量やお湯を入れてから何分待つか？などになります。
また、目的関数ですが、本来のOptunaの使い方をするのであれば、Optunaが提案したパラメータを受け取ってスコアを返す関数になります。
ただ、コーヒーを淹れるのもおいしいかどうか判定するのも私なので、今回の場合は、Optunaに今までのデータに基づいて次に試すべきパラメータを提案させて、それを見て私がコーヒーを入れて、おいしかったかどうかのスコアを決めるというものを目的関数として使います。

## 超簡単なフレンチプレスを使ったコーヒーの淹れ方の説明
コーヒーを淹れる場合、ペーパードリップで淹れるイメージが多いかと思うので、フレンチプレスを使った淹れ方を知らない人も多いのでは？と思い、まずはフレンチプレスを使った淹れ方を簡単に説明します。

フレンチプレスでは主に以下のようにコーヒーを入れます。

1. 挽いた豆をフレンチプレスに入れる
2. 半分 or 全部のお湯を入れてスプーンなどでかき回す
3. 半分しか入れなかった場合は蒸らして、その後再度お湯を入れる
4. 時間になったら金網フィルターを下ろして完成

参考：https://www.ucc.co.jp/enjoy/brew/frenchpress.html

もうちょっと詳しく知りたいみたいな人は「フレンチプレス　コーヒー 淹れ方」みたいにググってみてください。

ペーパードリップだとお湯の注ぎ方が結構重要な上に安定させるのが素人だと難しいですが、フレンチプレスはそんな細かいこと考えなくてよく、味が安定するのでおすすめです。

## 探索範囲決め

フレンチプレスによるコーヒーの淹れ方がわかったところで、探索範囲をどうするか？ですが、私のコーヒーを淹れる環境で変えられそうな部分は以下の通り。

1. 豆を買った店
2. 豆を買った日と淹れた日の差 (焙煎してから時間がたった豆はあまりおいしくないことが多い)
3. 豆の種類
4. 豆の量
5. ミルで豆を挽く時間
6. お湯を入れてからトータルの時間
7. 蒸らし時間

このうち、1-3については年末ということもありお店も閉まっているので今は制御が難しいということで、4-7を制御することを考えます。

いくつかwebでコーヒーの淹れ方を調べるとおすすめの淹れ方としては大体以下の感じでした。

* 豆の量 (g): 9
* ミルで豆を挽く時間 (sec): 5
* お湯を入れてからトータルの時間 (sec): 240  
* 蒸らし時間 (sec): 30

なので、少し範囲を持たせて以下のようにOptunaでは探索します。

* 豆の量 (g): 8-12
* ミルで豆を挽く時間 (sec): 3-15
* お湯を入れてからトータルの時間 (sec): 180-300 
* 蒸らし時間 (sec): 20-40

これをOptunaで書くと以下の通り。

```python
search_space={
    "豆の量(g)": optuna.distributions.IntUniformDistribution(8, 12),
    "ミルの時間 (sec)": optuna.distributions.IntUniformDistribution(3, 15),
    "トータルの時間 (sec)": optuna.distributions.IntUniformDistribution(180, 300),
    "蒸らし時間 (sec)": optuna.distributions.IntUniformDistribution(20, 40),
    }
```

## データのまとめ方

データはスプレッドシートにまとめるのが楽そうなので、そうしました。このような形で今はまとめています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21366/c9c2fb1b-9b81-b893-02dd-6a5c3013b8c6.png)
（これははblog用のダミーデータです）

Optunaを最適化する際に利用するおいしさを表すスコアは `1` - `10` の10点で評価することにします。

## Optunaに今までのデータを登録する

探索範囲を決めて、いままで探索したパラメータとその時のスコアのデータを用意したので次のパラメータをOptunaに提案してもらいます。その前に、今までの情報をOptunaの `Study` に登録するということをします。

まずGoogleのスプレッドシートからデータを取ってきます。コードとしては以下の通り。

```python
# Google スプレッドシートの認証

from google.colab import auth
from oauth2client.client import GoogleCredentials
import gspread

auth.authenticate_user()
gc = gspread.authorize(GoogleCredentials.get_application_default())

# スプレッドシートからデータを取ってくる
import pandas as pd

ss_name = "shu65コーヒーデータ"
workbook = gc.open(ss_name)
worksheet = workbook.get_worksheet(1)
df = pd.DataFrame(worksheet.get_all_records())
df = df.set_index('淹れた日')
```

この結果 `df` としては以下のようにデータが入っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21366/9c0ccd48-284d-f428-b849-05446e83595c.png)

このデータをOptunaの `Study` に `add_trial()`を使って入れていきます。コードとしては以下の通り。

```python
import optuna

sampler = optuna.samplers.TPESampler(multivariate=True)
study = optuna.create_study(direction='maximize', sampler=sampler)

for record_i, record in df.iterrows():
  print(record.to_dict())
  params = {}
  for key in search_space.keys():
    params[key] = record[key]
  trial = optuna.trial.create_trial(
    params=params,
    distributions=search_space,
    value=record[score_column])
  study.add_trial(trial)
```

当初は `sampler` のseedを固定していたのですが、そうすると、使い方によってはこの後説明するパラメータが毎日同じものを提案してくるようになってしまったので、seedは固定しないで運用します。

## Optunaで次に探索するべきデータを提案してもらう

データの登録が終わればあとは次に探索するパラメータを提案してもらうだけです。コードとしては以下の通り。

```python
trial = study._ask()

new_params = {}
for key, space in search_space.items():
  new_params[key] = trial._suggest(key, space)

for key in  ["豆の量(g)", "ミルの時間 (sec)",  "トータルの時間 (sec)",  "蒸らし時間 (sec)"]:
  print(key, new_params[key])
```

ちなみに次の提案としてはこんなものをされました。

```shell

豆の量(g) 11
ミルの時間 (sec) 10
トータルの時間 (sec) 209
蒸らし時間 (sec) 24
```

フレンチプレスだと挽き方は粗目が良いとよく言われるのですが、10秒もミルを回すと細かくなりすぎる気がしてます。とはいえ、試しに明日はこれでやってみようと思います。


# おわりに

暇すぎて勢いに任せてOptunaを使ったおいしいコーヒーの淹れ方の探索方法の記事を書きました。
Optunaは機械学習に使われるものというイメージがあるかもしれませんが、black box最適化技術は機械学習以外にも様々な応用ができる技術ですので、いろいろ使えると思っています。今回の記事が他の応用を思いつくきっかけになれば幸いです。