はじめに
こんにちは.最近,技術展示会のO’REILLYブースでPython書籍7冊まとめ買いした@khriveです.
厚さにして12cm,ガチ系教科書の一気まとめ買いは,たちまち積読(つんどく) を招く無謀な行為です😄.
回避策は,気負わず構えず雑誌のように気楽にパラパラ斜め読みです.新幹線移動中にパラパラ,お風呂でパラパラ($para^2$)眺めて,自然と目に留まったキーワードがメタクラスでした.ここではPythonの($meta$)メタクラスについて,気軽にパラパラ書いてみます.
この記事は,下記の書籍を参考に執筆しています123456:
一気買いした書籍リスト
考え方
- 面倒な仕事をとことん楽して片付けるために(一種のJobs to be doneか?)
- プログラミング作業さえもプログラムに実行させたいわけです
- そういえば生成AIの副操縦士も,プログラミングするプログラムですね
少し余談
- 少し前,メタプログラミング と呼んでいた概念で,CLOS MOPやRubyが有名です
- 楽をするための初期投資が,やってみたら思いのほか高くつく,よくあるパターンでしょうか
- 舞台裏を覗いて,これはかなり大変と慄きつつ
- 普段は見ることも触れることもない,Pythonの舞台裏の仕組みを覗けて面白かった,とハマっていく展開でしょうか
何が嬉しいの? メタクラス
- 舞台裏の仕組みに,意図的に介入できること
- 舞台裏の卓越した機能を理解し,巧みに流用することで,わずかな時間,少ないコードで最大効果を得ること
- 本質的な意味,本質的な設計思想に回帰すること
- 絶妙に因数分解された関数群から,欲しい機能を合成する見事な魔術を体感すること
...というところに謎めいた魅力を感じています.
メタクラスの挙動を追う
Python Distilled1の 第7章「クラスとオブジェクト指向プログラミング」を参考に,要点整理します.
新しいクラスを定義したら,そのclass文
に応じて新クラスを作るのは type.new_class()関数
が実行します.具体的には,type.new_class()
の引数に,クラス名,基底クラスのタプル,クラス用の名前空間
を渡すことで,動的にクラスを生成できます.
またtype
を継承したメタクラスを明示的に扱うことで,プログラマは下記のステップに意図的に介入して,既定の操作,振る舞い,意味を書き換えることが可能です:
- クラス用の新しい名前空間を用意する
- クラスの実体を新規生成する
- クラスの実体を初期化する
- そのクラスのインスタンスを新規生成する
Justアイディア活用例
よい代替案が多数ある中,手元の書籍たちのななめ読みだけで,どこまで理解できたか? できていないか?ワクワク,面白いそうなことができるかな?できないかな?と思いつきのJustアイディアで3つの事例を考えました.この記事の前菜,魚料理,肉料理とは言い難く,必然性,正確性,厳密性…については,おおらかな寛容で受け止めて頂けますと幸いです.
-
$AliceArithMeta$
四則演算の意味が変わる,不思議のアリスの算術世界 -
$ViceVersaMeta$
逆も真なり,いつでも逆引き可能なり,Dictionary -
$HyperParaMeta$
超パラメタ探索シミュレーション
動作確認した環境
- Windows11, Python3.10.11, pip 22.3.1, JupyterLab
- MacOS sonoma 14.1.2, Python3.10.12, pip 23.2.1, VSCode
- Google colaboraotory (https://colab.research.google.com/)
AliceArithMeta 不思議のアリスの算術世界
数学の世界はきわめて奥深く,常識が通用しない異世界を実感する面白さがあります.
"…量子重ね合わせを超えて迷い込んだ不思議な異世界では,算術の理(ことわり)がまるで違っていた…"
新たな可能性を探求する意味で,非真面目に常識を疑うことは,しばしば大切かもしれません.
- 足し算は,異世界では,かけ算を意味した
- 引き算は,異世界では,割り算を意味した
- かけ算は,異世界では,べき乗を意味した
- 割り算は,異世界では,剰余計算を意味した
そんな世界をシミュレーションする例が,下記のAliceArithMeta
メタクラスです:
# カスタムメタクラス定義は,typeを継承
class AliceArithMeta(type):
def __new__(cls, kls, bases, attrs):
# intクラス = オリジナル
klass = super().__new__(cls, kls, (int,), attrs)
# 四則演算の意味を変える特殊メソッド
def __add__(self, val): # +:*
return int.__mul__(self, val)
klass.__add__ = __add__
def __sub__(self, val): # -:/
return int.__truediv__(self, val)
klass.__sub__ = __sub__
def __mul__(self, val): # *:**
return int.__pow__(self,val) # self ** val
klass.__mul__ = __mul__
def __div__(self, val): # /:%
return int.__mod__(self,val) # self % val
klass.__truediv__ = __div__
return klass
# カスタムメタクラスを指定したクラス定義で挙動が変わる
class AliceArith(metaclass=AliceArithMeta):
pass
- 動作確認
x , y = AliceArith(3) , AliceArith(5)
[x + y, # x * y
x - y, # x / y
x * y, # x ** y
x / y, # x % y
x ** 2, y ** 3, x % y]
# [15, 0.6, 243, 3, 9, 125, 3]
- 出力結果
[15, 0.6, 243, 3, 9, 125, 3]
3+5 == 15
3-5 == 0.6
3*5 == 243
3/5 == 3
これが, 異世界における算術の理なのだ,
そう言われれば受け止めるしかないね,
というお話です.
ViceVersaMeta 逆も真なり,いつでも逆引き可能なり, Dictionary
Pythonの辞書は大変便利です.キーに値を結びつける柔軟なデータ構造,連想配列(Key-Value)
として,古くから有用性が確認されています.
さらにひと手間加えて,辞書はいつでも逆引きできるとより嬉しく感じるはずだと勝手に妄想し,
舞台裏で逆引きをメンテナンスする仕掛けを考えました.
# カスタムメタクラス定義は,typeを継承
class ReverseMeta(type): # 逆引き辞書用 metaclass
def __new__(cls, clsname, bases, attrs): # クラスを生成
alt_class = super().__new__(cls, clsname, (dict,), attrs)
def post_reverse(self): # 逆引き辞書をCreate
self.reverse_dict = {}
for key, value in self.items():
self.reverse_dict[value] = key # value->key
def get_reverse(self): # 逆引き辞書をRefer
return self.reverse_dict
def put_reverse(self, key, value): # 逆引き辞書をUpdate
self.reverse_dict[value] = key # value->key
def del_reverse(self, key, value): # 逆引き辞書から項目をDelete
self.reverse_dict[value] = None
def __setitem__(self, key, value): # 辞書に要素を追加
super(self.__class__, self).__setitem__(key, value) # key->value
self.put_reverse(key, value) # value->key
alt_class.post_reverse = post_reverse
alt_class.get_reverse = get_reverse
alt_class.put_reverse = put_reverse
alt_class.del_reverse = del_reverse
alt_class.__setitem__ = __setitem__
return alt_class
class ReverseDict(metaclass=ReverseMeta):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.post_reverse()
# 動作確認
# オリジナルの辞書の作成
dic = ReverseDict({'Alice':100, 'Bob':90, 'Carol':80})
print(dic) # {'Alice': 100, 'Bob': 90, 'Carol': 80}
# いつでも逆引きできる
print(dic.get_reverse()) # {100: 'Alice', 90: 'Bob', 80: 'Carol'}
# 辞書に項目を追加
dic['Dave'] = 70
print(dic) # {'Alice': 100, 'Bob': 90, 'Carol': 80, 'Dave': 70}
# いつでも逆引きできる
print(dic.get_reverse()) # {100: 'Alice', 90: 'Bob', 80: 'Carol', 70: 'Dave'}
- 動作確認結果
{'Alice': 100, 'Bob': 90, 'Carol': 80}
{100: 'Alice', 90: 'Bob', 80: 'Carol'}
{'Alice': 100, 'Bob': 90, 'Carol': 80, 'Dave': 70}
{100: 'Alice', 90: 'Bob', 80: 'Carol', 70: 'Dave'}
いかがでしょうか.
インラインのコメント文に示したように,普段通りに辞書を作って,使っているだけですが,
いつでも逆引きができる状態が確認できます.
舞台裏のメタクラスに一工夫が集約されるので,あちこちに散らかる懸念も少ないでしょう.
ツボを押さえた最小限のロジックで,安全に影響範囲をコントロールできてちょっと良い感じです.
HyperParaMeta 超パラメタ探索
ハイパーパラメータとは事前の設定値であり,プログラマが手作業で予め設定して,実行結果を見ながら,さじ加減を調整するのが一般です.例えば機械学習で学習回数(epoch),層の数,隠れユニット数をどう決めるのか? 学習モデルにかかる多様な設定値をどう調整するか? 量子アニーリングのハミルトニアン設計において,目的関数に対する制約条件違反のペナルティ重み係数をどれくらいに調整するか? ハイパーパラメタのチューニング作業は,最適化ロジックと別レベルの調整作業として区別したい,と感じた経験はありませんか?
ここでは,optuna7 によるチューニング機構を,メタクラスで設計する例を上げました. 書籍「機械学習エンジニアのためのTransformers」3をパラパラめくり,見つけたRosenbrockのバナナ関数を取り上げています.
-
最適化問題では有名なベンチマーク関数で,山谷の起伏が細かく最小値を探すのが難しい.
-
関数式: $ f(x,y) = a(1-x)^2 + b(y-x^2)^2 $
$(a,b)==(1,100)$の時,
$(x,y)==(1,1)$において
最小値 $f(x,y)==0$ -
Rosenbrockのバナナ関数のグラフ可視化
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def rosenbrock(x, y):
a,b = 1,100
return a * (1 - x)**2 + b * (y - x**2)**2
x = np.arange(-1, 1, 0.1)
y = np.arange(-1, 1, 0.1)
X, Y = np.meshgrid(x, y)
Z = rosenbrock(X, Y)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, cmap='copper')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x, y)')
plt.show()
本題に戻ります.
このバナナ関数の最適解を解く作業と,ハイパーパラメタのチューニング作業は区別する意図で,
optunaを活用したパラメタチューニング機構を,メタクラスに寄せて書いてみたのが下記です.
import optuna
class RosenbrockMeta(type):
@classmethod
def __prepare__(meta,clsname,bases):
print(f'@{meta} __prepare__ namespace for {clsname,bases}')
return super().__prepare__(clsname,bases)
@staticmethod
def __new__(cls, clsname, bases, attrs):
print(f'@{cls} __new__ creating class {clsname,bases}')
rosenbrock_class = super().__new__(cls, clsname, bases, attrs)
# Optunaパラメータチューニング振舞いを設定
rosenbrock_class.tune = cls.tune
return rosenbrock_class
def __init__(cls,clsname,bases,namespace):
print(f'@{cls} __init__ initializing class {clsname,bases,namespace}')
super().__init__(clsname,bases,namespace)
def __call__(cls,*args,**kwargs):
print(f'@{cls} __call__ creating instance of {cls} for args:{args},kwargs:{kwargs}')
return super().__call__(*args,**kwargs)
def tune(cls, n_trials=300):
print(f'@{cls} # tune(cls={cls},n_trials={n_trials})')
rosenbrock = cls # target functions = rosenbrock banana function
# 目的関数
def objective(trial):
x = trial.suggest_float('x', -2, 2)
y = trial.suggest_float('y', -2, 2)
return rosenbrock(x, y)
# Optunaスタディ作成
study = optuna.create_study()
# 最小化探索
study.optimize(objective, n_trials=n_trials,n_jobs=2,show_progress_bar=False)
print(f'Best_params:{study.best_params}, Best_values:{study.best_value}')
# ローゼンブロック関数をクラス定義で
class Rosenbrock(metaclass=RosenbrockMeta):
def __init__(self, a=1, b=100):
print(f'@{self} __init__ creating instance {self} for a:{a},b:{b}')
self.a = a
self.b = b
def __call__(self, x, y): # 関数の本体
print(f'@{self} __call__ {self}(x:{x},y:{y})')
return (self.a - x)**2 + self.b * (y - x**2)**2
if __name__=='__main__':
f = Rosenbrock()
f.tune()
- 動作結果,動作確認
…
[I 2023-12-07 ] Trial 97 finished with value: 13.522714065233073 and parameters: {'x': 0.6471679674139057, 'y': 0.7848622181226452}. Best is trial 88 with value: 0.0009166665556231621.
[I 2023-12-07 ] Trial 98 finished with value: 33.90239933586183 and parameters: {'x': 1.1833673215586284, 'y': 0.8183893546375197}. Best is trial 88 with value: 0.0009166665556231621.
[I 2023-12-07 ] Trial 99 finished with value: 50.923011814736896 and parameters: {'x': 1.2233430590847338, 'y': 0.7833142202943585}. Best is trial 88 with value: 0.0009166665556231621.
Best_params:{'x': 0.9702805498828282, 'y': 0.9408662378754027},
Best_values:0.0009166665556231621
上記の結果は,100回の試行で正解 $(x,y)==(1,1)$で最小値 $f(x,y)==0$ にアプローチしています.
この例では,ハイパーパラメタの調整が求解と重なり,メタクラスとターゲットクラスの結合が十分きれいにセパレートできていない残念さは感じます.
より実用的な応用では,ハイパーパラメタとそのスコープをうまく分離する課題が残ります.
おわりに
厚さ12cmのガチ系教科書を一気買いしたはずみで,妄想と理解度確認の並行作業の混沌から何かを伝えるネタ探しで,記事を書いてみました.
多様な業界の舞台裏を描写したマンガ作品が次々とヒットするように,知らない世界の名もなき物語が人々の心を動かすことは珍しくありません.成長進化著しいICT業界は言うまでもなくおもしろネタの宝庫でしょう.
一例ですが,生成AIの舞台裏でTransformer3,マルチヘッドアテンションがどんな仕組みで頑張っているのか? 簡単便利なプログラミング言語Pythonの舞台下をどんな仕掛けが支えているのか?
普段はもっぱらユーザ視点で眺める世界を,一味違う角度から目を凝らして見直すだけで,知的な好奇心が刺激されます.
今回,Pythonの舞台裏のワンシーン,メタクラスの世界を覗いてみました.単純でわかりやすいことを良しとするPythonic2とは相容れない難解さからか,他の代替手段で置き換える風潮が主流かもしれません.それでも,改めてその面白さに注目して,モチベーションが刺激される,そんな感覚が共有できたら幸いです.
最後まで読んでいただき,ありがとうございました.
おまけ
- 舞台裏には物語が溢れている
- 設計の本質もにじみ出る
- 普段はもっぱらユーザでも,たまには舞台裏に(アド)弁当差し入れて,舞台裏の名もなき達人達とコラボできる楽しさを味わおう
- 異世界の理に健全に介入する好奇心をもとう
- そして,勘違いや妄想に躊躇せず,挑戦してみよう
参考文献
- Python Distilled,プログラミング言語Pythonのエッセンス
- Effective Python 第2版,Pythonプログラムを改良する90項目
- 機械学習エンジニアのためのTransformers, 最先端の自然言語処理ライブラリによるモデル開発
- プログラミング文体練習,Pythonで学ぶ40のプログラミングスタイル
- Pythonチュートリアル(第4版) … Webでも最新情報は読めます