1. はじめに
夜にシミュレーションを回して帰って、朝に結果を確認することが多いのですが,大量の図を1個1個エクスプローラーからダブルクリックして見るのは面倒なので、簡単に結果を確認するためのレポートを Python から出力させたいと思います。
本ページで紹介すること
- mdutils パッケージを使用した簡単な Markdown レポート生成サンプルの紹介。
- 上記の応用例として、optuna を使ったパラメータ最適化結果のレポートを自動生成させた例の紹介。
Python から HTMLのレポートを直接出力する手もありますが、あとから編集するのが大変なので、ここでは Markdown で出力し、Pandoc や VScode の拡張機能などで HTML や pdf に変換して使用することを想定しています。
2. 基本の Markdown レポート生成例
2.1. 開発環境
- Windows 11
- Python 3.8.10 (3.10.9でも動作を確認)
使用したパッケージ (requirements.txt) はこちら
alembic==1.9.4
cmaes==0.9.1
colorama==0.4.6
colorlog==6.7.0
contourpy==1.0.7
cycler==0.11.0
fonttools==4.38.0
greenlet==2.0.2
importlib-metadata==6.0.0
importlib-resources==5.12.0
joblib==1.2.0
kiwisolver==1.4.4
Mako==1.2.4
MarkupSafe==2.1.2
matplotlib==3.7.0
mdutils==1.5.0
numpy==1.24.2
optuna==3.1.0
packaging==23.0
Pillow==9.4.0
pyparsing==3.0.9
python-dateutil==2.8.2
PyYAML==6.0
scikit-learn==1.2.1
scipy==1.10.1
six==1.16.0
SQLAlchemy==2.0.4
threadpoolctl==3.1.0
tqdm==4.65.0
typing-extensions==4.5.0
zipp==3.15.0
2.2. mdutils を使った基本のレポート生成
2.2.1. Markdown 生成パッケージ
Python から Markdown を生成するパッケージは色々あるのですが、ここでは Star の数が多く、ざっと調べてみた感じ直感的に書けそうな mdutils を選定しました。
2.2.2. mdutils のサンプルコード
まず、簡単なサンプルコードを紹介します。mdutils をインストールします。
$ pip install mdutils
下記の図のような簡単な Markdown レポートを生成するサンプルコードは以下のようになります。
from mdutils.mdutils import MdUtils
def generate_report(export_path, table_header, table_list, image_path):
mdFile = MdUtils(file_name=export_path, title='Python から Markdown を生成するサンプル')
# はじめに
mdFile.new_header(level=1, title='はじめに')
mdFile.new_line("本書は Markdown の自動生成テストしたものです。"
"mdutils を使用して作成しました。")
mdFile.new_header(level=1, title='各要素の例')
# 箇条書き
mdFile.new_header(level=2, title='箇条書き')
mdFile.new_list(["項目1",["項目1.1"],"項目2"])
# 表を貼る
mdFile.new_header(level=2, title='表')
list_of_strings = table_header
for line in table_list:
list_of_strings.extend(line)
mdFile.new_table(columns=3, rows=3, text=list_of_strings, text_align='left')
# イメージを貼る
mdFile.new_header(level=2, title='イメージ')
mdFile.new_line(mdFile.new_inline_image(text='写真', path=image_path))
# リンクを貼る
mdFile.new_header(level=2, title='リンク')
mdFile.new_line("Qiita トップページへのリンクです。")
mdFile.new_line(mdFile.new_inline_link(link="https://qiita.com/", text='リンク'))
# ファイルを生成する
mdFile.create_md_file()
if __name__ == "__main__":
# アプリケーション実行
table_header = ["列1", "列2", "列3"]
table_list = [[0.1, 0.2, 0.3],["apple", "banana", "cabbage"]]
image_path = "./mycat.png"
# レポート生成
generate_report("mdutils_export_sample.md", table_header, table_list, image_path)
コードの補足
-
mdutils の使い方は mdutilsのトップページ や ドキュメント に例が豊富です。
-
mdutils で Markdown を作成する流れは、以下のようになります。
-
インスタンスを作成する。
mdFile = MdUtils(file_name="sample.md", title='Python から Markdown を生成するサンプル')
-
Markdownに出力したい要素を上から順番に追加する。
# ただのテキストを追加する場合 mdFile.new_line("テキスト") # 図を貼る場合 mdFile.new_line(mdFile.new_inline_image(text='写真', path='./sample.png'))
-
ファイルにエクスポートする。
mdFile.create_md_file()
-
-
表の作成だけ少しややこしいので補足すると、すべての項目を1つの list にまとめてから
list_of_strings = ['列1', '列2', '列3', 0.1, 0.2, 0.3, 'apple', 'banana', 'cabbage']
以下のように列数、行数を指定して出力します。
mdFile.new_table(columns=3, rows=3, text=list_of_strings, text_align='left')
2.3. 補足: Pythonの基本機能だけで頑張る
Markdown はただのテキストなので、あまり複雑なレポートを作成しないなら、Python の基本機能だけでべた書きしてもいいと思います。
先ほどの例を Python の基本機能だけで書き換えると以下のようになります。表の作成だけがちょっと大変ですが、それ以外の部分(図の埋め込みだけ自動化したいなどといった用途)だと、地道に書くこちらの方法もありだと思います。
def generate_report(export_path, table_header, table_list, image_path):
"""
Markdown のレポートを生成する
"""
# 変更のない固定文を出力する
text = """\
Python によるMarkdown生成サンプル
================================
# はじめに
本書は Markdown の自動生成テストしたものです。
この例では,Pythonの基本機能ですべてべた書きで記載します。
# 各要素の例
## 箇条書き
* 項目1
* 項目1.1
* 項目2
## 表
"""
# 表を貼る
text += generate_table(table_header, table_list)
# イメージを貼る
text += "\n\n## イメージ\n\n"
text += generate_imagelink(image_path, image_title="写真")
# リンクを貼る
text += "\n\n## リンク\n\nQiita トップページへのリンクです。\n\n"
text += generate_link("https://qiita.com/", title="リンク")
# ファイルに書き出す
with open(export_path, 'w', encoding='utf-8') as f:
f.write(text)
return text
def generate_table(table_header, table_list):
"""
リストから、表を貼り付けるテキストを作成する
"""
# ヘッダー行を作る
text = ""
for header in table_header:
text += f"|{header}"
text += "\n"
# 区切り文字を作る
text += ("|:-" * len(table_header))
text += "\n"
# 中身を作る
for line in table_list:
for item in line:
text += f"|{item}"
text += "\n"
return text
def generate_imagelink(image_path, image_title=""):
"""
画像ファイルパスから、画像貼り付けテキストを作成する
"""
return f"![{image_title}]({image_path})"
def generate_link(link, title=""):
"""
リンク(url)から、リンク貼り付けテキストを作成する
"""
return f"[{title}]({link})"
if __name__ == "__main__":
# アプリケーション実行
table_header = ["列1", "列2", "列3"]
table_list = [[0.1, 0.2, 0.3],["apple", "banana", "cabbage"]]
image_path = "./mycat.png"
# レポート生成
generate_report("basic_export_sample.md", table_header, table_list, image_path)
3. optuna 最適化レポート自動生成例
ここからは、応用例として optuna を使ったパラメータ最適化の結果のレポートを mdutils を使って自動生成したいと思います。
例題
-
サンプルデータを与えて、以下の2次関数の近似曲線を求める(次式の最適なパラメータ
k_0, k_1, k_2
を探索する)y = k_2 x^2 + k_1 x + k_0
-
評価関数としては MSE(平均二乗誤差) を使用する。
-
optuna が提供するサンプラー
BaseSampler
(ベイズ最適化),RandomSampler
(ランダム),CmaEsSampler
(CMA-ES) ごとに以下の結果を確認する。- ステップごとの最適化結果
- 最終的に得られた最適な
k_0, k_1, k_2
とその MSE - 最適化にかかった時間
ここからは以下のパッケージを使用します。
mdutils
numpy
matplotlib
scikit-learn
optuna
3.1. コードと生成されたレポート
3.1.1. コード
長いので、先にコードと生成された Markdown レポートを紹介し、あとからコードについて補足します。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
import optuna
import os
import time
import pickle
from mdutils.mdutils import MdUtils
RESULT_FOLDER = "./results"
def make_sample_data():
"""
サンプルデータを作成する
"""
x = np.arange(0.0, 2, 0.1)
y= -0.9*x**4 +2*x**3 + 0.5*x
os.makedirs(RESULT_FOLDER, exist_ok=True)
fig = plt.figure()
plt.plot(x, y, 'o')
plt.savefig(os.path.join(RESULT_FOLDER, "sample_data.svg"))
return x, y
def fx(x, params):
"""
使用する2次関数
"""
y = params['k2'] * x**2 + params['k1'] * x + params['k0']
return y
def param_tune(x_target, y_target, method="base"):
"""
パラメータの最適化を行う。例として、2次関数の係数の最適化を行う。
"""
x = x_target
def objective(trial):
# 目的関数
k0 = trial.suggest_float('k0', -10, 10)
k1 = trial.suggest_float('k1', -10, 10)
k2 = trial.suggest_float('k2', -10, 10)
y = fx(x, {'k0': k0, 'k1': k1, 'k2': k2})
J = mean_squared_error(y_target,y)
return J
# 最適化手法については https://optuna.readthedocs.io/en/stable/reference/samplers/index.html
if method=="cmaes":
title = "CmaEsSampler"
sampler = optuna.samplers.CmaEsSampler()
study = optuna.create_study(direction="minimize", sampler=sampler)
elif method=="random":
title = "RandomSampler"
sampler = optuna.samplers.RandomSampler()
study = optuna.create_study(direction="minimize", sampler=sampler)
else:
title = "BaseSampler"
study = optuna.create_study(direction="minimize")
# optunaの経過表示を非表示にする
optuna.logging.set_verbosity(optuna.logging.WARNING)
start = time.time()
# 最適化を行う。
study.optimize(objective, n_trials=1000)
# 経過時間を取得する
elapsed_time = time.time() - start
print(study.best_params)
# 最適化の過程から、途中の最適値を取り出す。
values = [each.value for each in study.trials]
params = [each.params for each in study.trials]
best_values = []
best_params = []
for i in range(len(values)):
target = values[:i+1]
best_index = np.argmin(target)
best_values.append(target[best_index])
best_params.append(params[best_index])
# 一番最後のベストパラメータ
best_param = study.best_params
# 図を格納するフォルダを作成する
target_dir = os.path.join(RESULT_FOLDER, method)
os.makedirs(target_dir,exist_ok=True)
# 最適化結果を可視化する。
fig_path = os.path.join(target_dir, "tune_step.svg")
plot_tune_step(title, values, best_values, best_params, fig_path=fig_path)
# 最適化後の結果を表示する。
fig_path = os.path.join(target_dir, "tune_result.svg")
best_mse = plot_tuned_result(x_target, y_target, best_param, fig_path=fig_path)
# 結果をpickleに書き出す
with open(os.path.join(target_dir, 'result.pickle'), 'wb') as f:
result = {"k0": best_param["k0"], "k1": best_param["k1"], "k2": best_param["k2"],"mse": best_mse, "elapsed_time": elapsed_time}
pickle.dump(result, f)
return best_param
def plot_tune_step(title, values, best_values, best_params, fig_path=None):
"""
各最適化手法における最適化の過程を可視化する。
"""
# 可視化する
fig = plt.figure(figsize=[8,6])
# 上の図に目的関数の最適化結果を表示する
ax1 = plt.subplot(211)
ax1.set_title(title)
ax1.plot(values, label="value")
ax1.plot(best_values, label="best value")
ax1.set_yscale('log')
ax1.set_ylabel("Objective value")
# 凡例は外に出す
ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)
# 下の図に、パラメータ結果を表示する。
ax2 = plt.subplot(212)
ax2.plot([param['k0'] for param in best_params], label="k0")
ax2.plot([param['k1'] for param in best_params], label="k1")
ax2.plot([param['k2'] for param in best_params], label="k2")
ax2.set_ylabel("Best parameters")
ax2.set_xlabel("Iteration step")
ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)
plt.subplots_adjust(right=0.8)
if fig_path is not None:
# 結果を保存する
plt.savefig(fig_path)
else:
plt.show()
plt.clf()
plt.close()
def plot_tuned_result(x_target, y_target, params, fig_path=None):
"""
ベストなパラメータを使用したときの、元の点との比較を結果をプロットする。
"""
y = fx(x_target, params)
fig = plt.figure()
plt.plot(x_target,y_target,'o')
plt.plot(x_target,y,'r-')
mse = mean_squared_error(y_target,y)
plt.title(f"MSE: {mse}")
if fig_path is not None:
# 結果を保存する
plt.savefig(fig_path)
else:
plt.show()
plt.clf()
plt.close()
return mse
def write_report(export_path):
"""
Markdownのレポートを生成する
"""
mdFile = MdUtils(file_name=export_path, title='optuna を使用した最適化サンプル')
# はじめに
mdFile.new_header(level=1, title='はじめに')
mdFile.new_line("本書は Markdown の自動生成テストしたものです。"
"サンプルとして optuna を使用し、2次関数の近似線を求めます。")
mdFile.new_line()
mdFile.new_header(level=2, title='最適化に使用するデータ')
target_image = os.path.join(RESULT_FOLDER, "sample_data.svg")
mdFile.new_line(mdFile.new_inline_image(text='サンプルデータ', path=target_image))
mdFile.new_line()
mdFile.new_header(level=2, title='最適化する2次多項式')
mdFile.insert_code("y = k_2 x^2 + k_1 x + k_0", language='math')
mdFile.new_line()
mdFile.new_header(level=1, title='optuna で最適化した結果')
def write_methods_result(title, method):
"""
各最適化手法の結果を書くサブ関数
"""
mdFile.new_header(level=2, title=title)
# 図を貼る
target_image = os.path.join(RESULT_FOLDER, method, "tune_step.svg")
mdFile.new_line(mdFile.new_inline_image(text='最適化過程', path=target_image))
mdFile.new_line()
target_image = os.path.join(RESULT_FOLDER, method, "tune_result.svg")
mdFile.new_line(mdFile.new_inline_image(text='最適化後の結果', path=target_image))
# 結果の表を貼る
with open(os.path.join(RESULT_FOLDER, method, 'result.pickle'), mode="rb") as f:
# 結果が dictの中に入っているので、キーと値を取り出して縦に並べる
result = pickle.load(f)
list_of_strings = ["パラメータ", "値"]
for key, val in result.items():
list_of_strings.extend([key, str(val)])
mdFile.new_line()
mdFile.new_table(columns=2, rows=len(result)+1, text=list_of_strings, text_align='left')
# 各最適化手法ごとの結果を表示
write_methods_result("BaseSampler", "base")
write_methods_result("RandomSampler", "random")
write_methods_result("CmaEsSampler", "cmaes")
# ファイルを生成する
mdFile.create_md_file()
if __name__ == "__main__":
# サンプルの目標データを作成する
x, y = make_sample_data()
# 異なる最適化手法で最適化を実行する
params = param_tune(x, y, method="random")
params = param_tune(x, y, method="cmaes")
params = param_tune(x, y, method="base")
# レポートを生成する
write_report("optimize_report.md")
3.1.2. 生成された Markdown
optuna を使用した最適化サンプル
===================
# はじめに
本書は Markdown の自動生成テストしたものです。サンプルとして optuna を使用し、2次関数の近似線を求めます。
## 最適化に使用するデータ
![サンプルデータ](./results\sample_data.svg)
## 最適化する2次多項式
```math
y = k_2 x^2 + k_1 x + k_0
```
# optuna で最適化した結果
## BaseSampler
![最適化過程](./results\base\tune_step.svg)
![最適化後の結果](./results\base\tune_result.svg)
|パラメータ|値|
| :--- | :--- |
|k0|-0.3123366018336038|
|k1|1.6713739299342616|
|k2|0.18685519487057417|
|mse|0.05304465728892915|
|elapsed_time|25.70290446281433|
## RandomSampler
![最適化過程](./results\random\tune_step.svg)
![最適化後の結果](./results\random\tune_result.svg)
|パラメータ|値|
| :--- | :--- |
|k0|-1.0042492694679446|
|k1|2.3503184374400465|
|k2|-0.17606037160065746|
|mse|0.34791378366778514|
|elapsed_time|1.036989688873291|
## CmaEsSampler
![最適化過程](./results\cmaes\tune_step.svg)
![最適化後の結果](./results\cmaes\tune_result.svg)
|パラメータ|値|
| :--- | :--- |
|k0|-0.29319171439289105|
|k1|1.8609057100494433|
|k2|0.06342857426414414|
|mse|0.048847462205142786|
|elapsed_time|2.283266067504883|
3.1.3. Markdown レポートのレンダリング結果
以下生成された Markdown のレポートを Qiita に貼り付けました。 Qiita でレンダリングさせるために、セクション記号と図の拡張子(svg -> png)を変更しています。
はじめに
本書は Markdown の自動生成テストしたものです。サンプルとして optuna を使用し、2次関数の近似線を求めます。
最適化に使用するデータ
最適化する2次多項式
y = k_2 x^2 + k_1 x + k_0
optuna で最適化した結果
BaseSampler
パラメータ | 値 |
---|---|
k0 | -0.3123366018336038 |
k1 | 1.6713739299342616 |
k2 | 0.18685519487057417 |
mse | 0.05304465728892915 |
elapsed_time | 25.70290446281433 |
RandomSampler
パラメータ | 値 |
---|---|
k0 | -1.0042492694679446 |
k1 | 2.3503184374400465 |
k2 | -0.17606037160065746 |
mse | 0.34791378366778514 |
elapsed_time | 1.036989688873291 |
CmaEsSampler
パラメータ | 値 |
---|---|
k0 | -0.29319171439289105 |
k1 | 1.8609057100494433 |
k2 | 0.06342857426414414 |
mse | 0.048847462205142786 |
elapsed_time | 2.283266067504883 |
MSE と 収束の速さ、最適化にかかった時間(elapsed_time) のいずれを見ても、今回の例の場合は CmaEsSampler が最も良い結果になりました。
3.1.4. コードの補足
メインの関数は param_tune
関数と write_report
関数で、それぞれ optunaによる最適化の実行と、レポートの生成を行っています。
param_tune
関数
-
optuna の使い方について簡単に紹介すると
-
objective 関数で、最適化するパラメータと目的関数を設定します。
def objective(trial): # 目的関数 k0 = trial.suggest_float('k0', -10, 10) k1 = trial.suggest_float('k1', -10, 10) k2 = trial.suggest_float('k2', -10, 10) y = fx(x, {'k0': k0, 'k1': k1, 'k2': k2}) J = mean_squared_error(y_target,y) return J
-
以下のように最適化を実行します。
study = optuna.create_study(direction="minimize") study.optimize(objective, n_trials=1000)
-
sampler は
study = optuna.create_study(direction="minimize", sampler=sampler)
のように 与えることで最適化手法を変更できます。optuna にどのような sampler があるかは optuna.samplers に記載されています。
-
optuna の最適化結果は
study.trials
に記録されています。こちらから途中経過を取り出し、そのときのベストの評価値とパラメータをbest_values, best_params
に出力しました(参考: optuna入門)。 -
最適化手法ごとのプロット画像と結果 (dict) については、あとでレポート生成のときに使用するのでファイル(svg画像とpickle)に出力します。
HTMLに変換する場合は、画像は png や jpeg よりもベクター画像の svg がおすすめです (ベクター画像なので拡大しても滑らかです)。
write_report
関数
-
write_report
関数で Markdown を生成します。param_tune
関数から生成した画像ファイルと、pickle に格納された結果を表に変換して埋め込んでいます。
4. まとめ
今回 Python で Markdown のレポートを生成する方法について紹介しました。今回の例ではレポート全体を自動生成させましたが、一番面倒くさい図や表の作成&貼り付けだけを自動化させて、あとから本文を加筆するのが最も効率的だと思います。普段 Python を使っている方ならすぐにサクサク書けると思うので、おすすめです。
4.1. 今後やりたいこと
今回は Markdown レポートを生成するところだけを紹介しましたが、この発展として、デザインのよい HTML レポートをMarkdownから生成する方法も紹介できればと思っています。
4.2. 参考
以下参考にさせていただきました。ありがとうございます。