【解説】 Pythonの関数とクラス¶
関数¶
Pythonの関数は以下の様に書くのでした.
def 関数名(引数1: 引数1の型, ...)->戻り値の型:
"""関数の説明タイトル
関数の説明
Args:
引数の名前(引数の型): 引数の説明
Returns:
戻り値の型: 戻り値の説明
"""
関数内で行いたい処理
return 戻り値"""で囲まれた文字列は docstrings と呼ばれます. docstrings には関数やクラスの説明を記入するのですが,適切にtype hintを書くことで,かなりの部分を自動で生成することが可能です.関数名はスネークケース(すべて小文字で各単語をアンダースコア(_)で区切る)で名前をつけます. e.g. function_name
引数はなくても良い
戻り値はなくても良い
クラス¶
Pythonのクラスは以下の様に書くのでした.
class クラス名(継承元):
def __init__(self, 引数:型, ...):
self.インスタンス変数名 = 引数
def メソッド1(self, 引数:型)->戻り値の型:
メソッド1内の処理をself.インスタンス変数名 やこのメソッドの引数を使って記述する.
return 戻り値
# インスタンスの初期化
インスタンス名 = クラス名(引数)
# インスタンス変数にアクセス
インスタンス名.インスタンス変数名
# インスタンスのメソッドを実行
インスタンス名.メソッド1(引数)クラス名はパスカルケース(すべての単語の先頭が大文字)で名前をつけます. e.g. ClassName
__init__はコンストラクタ(初期化メソッド)です.ここでインスタンスに最初から持っていて欲しいインスタンス変数を設定しましょう.クラスの中に書いた関数をメソッドと呼びます.
【基本問題】Pythonの基本文法の確認¶
ガイダンス・環境準備に約20分,終わりに10分くらいを使う前提で,問題1〜7の目安合計が約60分になるよう配分している. 数値計算・損失・単純なモデルなど,以降の機械学習の実装にそのままつながる題材にしている. 各自の習熟度に合わせて問題の順序を入れ替えてもよい.時間が足りない場合は後半(問題6・7)を短くするなど配分を調整してよい.
| 目安時間 | 内容 |
|---|---|
| 約5分 | 問題1:if(しきい値による二値判定) |
| 約10分 | 問題2・3:内積・二乗誤差ベクトル(for とリスト) |
| 約15分 | 問題4・5:MSE・L2正則化項(関数とデフォルト引数) |
| 約18分 | 問題6:1入力アフィン変換クラス(線形結合の骨格) |
| 約22分 | 問題7:全結合層(2次元リストの重み行列とバイアス) |
すべて 自分のコードセルに実装し,末尾の assert や表示で動作を確認すること.
問題1:if によるしきい値判定(目安:約5分)¶
活性化や分類の素地として,実数 x がしきい値 threshold 以上かどうかを二値で返す関数を実装せよ.
x >= thresholdなら整数1,そうでなければ0を返す関数binary_decision(x: float, threshold: float) -> intを定義する(if/elseでよい).境界では
x == thresholdを 1 とみなす(左閉区間の「以上」).次の
assertがすべて成功するようにせよ.
assert binary_decision(0.5, 0.0) == 1
assert binary_decision(-0.1, 0.0) == 0
assert binary_decision(2.0, 2.0) == 1
assert binary_decision(1.5, 2.0) == 0def binary_decision(x: float, threshold: float) -> int:
# TODO: ここを実装する
raise NotImplementedError
assert binary_decision(0.5, 0.0) == 1
assert binary_decision(-0.1, 0.0) == 0
assert binary_decision(2.0, 2.0) == 1
assert binary_decision(1.5, 2.0) == 0
print("問題1 OK")問題2:内積(for とベクトル)(目安:約5分)¶
長さが同じ2つのリスト a と b を,ベクトルとみなして内積を求めよ.
for 文とインデックス(range(len(a)))を用いて,変数 dot に結果を格納すること.
(a, b の長さは等しい前提とする.)
a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]
dot = 0.0
# TODO: for で内積を計算する(答えは 32.0)
assert abs(dot - 32.0) < 1e-9
print("問題2 OK")問題3:各サンプルの二乗誤差(for とリスト)(目安:約5分)¶
予測値のリスト pred と正解のリスト target(同じ長さ)に対し,各位置 (i) について を並べたリスト squared_errors を作れ.for でもリスト内包表記でもよい.これは MSE の分子の要素に相当する.
pred = [0.5, 1.0, -0.5]
target = [1.0, 1.0, 0.0]
# 期待: [0.25, 0.0, 0.25]pred = [0.5, 1.0, -0.5]
target = [1.0, 1.0, 0.0]
squared_errors: list[float] = []
# TODO: squared_errors を構築する
assert len(squared_errors) == len(pred)
assert abs(squared_errors[0] - 0.25) < 1e-9
assert squared_errors[1] == 0.0
assert abs(squared_errors[2] - 0.25) < 1e-9
print("問題3 OK")問題4:平均二乗誤差 MSE(目安:約5分)¶
回帰で使う 平均二乗誤差(mean squared error) を実装せよ.
predとtargetは同じ長さ とする.引数・戻り値に 型ヒントを付けること.
上の「Pythonの関数」の例にならい,docstring(
Args/Returns)を書くこと.
def mse(pred: list[float], target: list[float]) -> float:
# TODO: docstring と本体を実装する
raise NotImplementedError
assert abs(mse([0.0, 2.0], [0.0, 0.0]) - 2.0) < 1e-9
assert mse([1.0, 1.0], [1.0, 1.0]) == 0.0
assert abs(mse([0.5, 1.0, -0.5], [1.0, 1.0, 0.0]) - (0.25 / 3.0 + 0.0 + 0.25 / 3.0)) < 1e-9
print("問題4 OK")問題5:L2 正則化項とデフォルト引数(目安:約5分)¶
重みベクトル weights に対する L2 正則化項(重み減衰のスカラー値)
を返す関数 ridge_penalty(weights, l2_lambda=1.0) を定義せよ.
第2引数
l2_lambdaは デフォルト 1.0 とする(呼び出し側で省略可能).型ヒントと簡潔な docstring を付けること(正則化の目的を1文で書けばよい).
def ridge_penalty(weights: list[float], l2_lambda: float = 1.0) -> float:
# TODO: docstring と本体を実装する
raise NotImplementedError
assert abs(ridge_penalty([1.0, 2.0], 1.0) - 5.0) < 1e-9
assert abs(ridge_penalty([1.0, 2.0]) - 5.0) < 1e-9
assert abs(ridge_penalty([1.0, 2.0], 0.5) - 2.5) < 1e-9
print("問題5 OK")問題6:1次元入力の線形モデル(クラス)(目安:約15分)¶
ニューラルネットの最小単位のイメージとして,スカラー入力 (x) に対し
を返すクラス ScalarAffine を実装せよ( を weight, を bias と呼ぶ).
__init__(self, weight: float, bias: float) -> Noneでself.weightとself.biasを保持する.forward(self, x: float) -> floatで を返す.squared_error(self, x: float, y_true: float) -> floatで を返し,問題4のMSEと概念を結び付ける.
クラス名はパスカルケースとする.
class ScalarAffine:
def __init__(self, weight: float, bias: float) -> None:
# TODO
raise NotImplementedError
def forward(self, x: float) -> float:
# TODO
raise NotImplementedError
def squared_error(self, x: float, y_true: float) -> float:
# TODO: (forward(x) - y_true) ** 2
raise NotImplementedError
model = ScalarAffine(2.0, 3.0)
assert abs(model.forward(1.0) - 5.0) < 1e-9
assert abs(model.forward(0.0) - 3.0) < 1e-9
assert abs(model.squared_error(1.0, 6.0) - 1.0) < 1e-9 # 予測5, 正解6 -> 誤差1
print("問題6 OK")問題7:全結合層(クラス)(目安:約10分)¶
深層学習の 全結合層(アフィン変換) を,NumPy 等に頼らず 入れ子の list で実装せよ.
重み行列
weightは 二次元リストlist[list[float]]とする.weight[i][j]は 第 出力ニューロンに対する 第 入力の係数(行 ,列 )とする.バイアス
biasは 一次元リストlist[float]で,出力次元(weightの行数)と同じ長さとする.入力
xは 一次元リスト(入力ベクトル)とする.len(x)はweightの列数と一致する前提でよい.
出力ベクトル の第 成分は
クラス名は Linear とし,__init__(self, weight, bias) で self.weight と self.bias を保持し,forward(self, x: list[float]) -> list[float] で上式に従ったリストを返すこと.ネストした for でよい(問題2の内積を行ごとに繰り返すイメージ).
class Linear:
# TODO: ここを実装する
raise NotImplementedError
# 2x2 の例: y0 = 1*1 + 2*0 + 0.5, y1 = 3*1 + 4*0 - 0.5
layer = Linear([[1.0, 2.0], [3.0, 4.0]], [0.5, -0.5])
out = layer.forward([1.0, 0.0])
assert abs(out[0] - 1.5) < 1e-9
assert abs(out[1] - 2.5) < 1e-9
# 単位行列に近い重み + 零バイアス
eye = Linear([[1.0, 0.0], [0.0, 1.0]], [0.0, 0.0])
assert abs(eye.forward([2.0, 3.0])[0] - 2.0) < 1e-9
assert abs(eye.forward([2.0, 3.0])[1] - 3.0) < 1e-9
# 出力1次元(1行の重み)
single = Linear([[1.0, -1.0]], [0.0])
assert abs(single.forward([1.0, 1.0])[0]) < 1e-9
print("問題7 OK")【発展問題】 より複雑なプログラム¶
必要なライブラリを以下の様にimportしておきます.
`matplotlib_fontja`がインストールされていない場合はコードセルで`!pip install matplotlib_fontja`を実行してください.from typing import TypeAlias
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
import copy
import random
import matplotlib.pyplot as plt
import matplotlib_fontja
import time
from IPython.display import clear_output, display
from IPython.display import HTML
from matplotlib import animation
from matplotlib.patches import Rectangle
plt.rcParams["animation.html"] = "jshtml"【発展問題1】ハノイの塔のアニメーション(再帰関数の復習)¶
ハノイの塔¶
ハノイの塔は,3本の棒と大きさの異なる複数の円盤を使う有名なパズルである。最初はすべての円盤が左の棒に,大きいものが下,小さいものが上になるように積まれている。この円盤の山を,ルールを守りながら別の棒へ移すことが目的である。
ハノイの塔のルール¶
ルールはとても単純で,次の2つだけである。
1回に動かせる円盤は1枚だけである。
大きい円盤を小さい円盤の上に置いてはいけない。
たとえば円盤が3枚なら,最初の状態は [[3, 2, 1], [], []] のように表せる。これは「左の棒に 3, 2, 1 が下から順に積まれていて,中央と右は空」という意味である。ここから最終的に [[], [], [3, 2, 1]] のような状態に移せば完成である。
この問題のおもしろいところは,「大きい円盤を動かしたければ,その上にある小さい円盤たちを先にどかさなければならない」という再帰的な構造を持っている点である。つまり,
いちばん大きい円盤を動かす前に,小さい円盤 n-1 枚を別の棒へ移す いちばん大きい円盤を目的地へ動かす そのあと,小さい円盤 n-1 枚をその上へ移す という形で考えられる。このため,ハノイの塔は再帰関数の説明でよく使われる。
円盤が n 枚のとき,最短手数はになる。
つまり1枚なら 1 手、2枚なら 3 手、3枚なら 7 手、4枚なら 15 手である。円盤が1枚増えるごとに必要な手数がほぼ2倍になるので,枚数が少し増えるだけでも急に大変になる。
実装¶
では,Pythonの復習のために,ハノイの塔のソルバーを作成せよ.ただし,出力は初期状態から終了状態までの盤面を全て記録したリストとすること.例えば以下のようなものになる.
example_states = [
[[3, 2, 1], [], []], # 初期状態
[[3, 2], [], [1]], # 1回目の操作終了時の盤面
[[3], [2], [1]], # 2回目の操作終了時の盤面
[[3], [2, 1], []], # 3回目の操作終了時の盤面
[[], [2, 1], [3]], # ... このまま終了状態まで続ける
]ここで,[[3, 2], [], [1]] は,左の棒に大きい円盤 3 と 2,右の棒に円盤 1 がある状態です.
また,ハノイの塔ソルバーの出力を入力とするビジュアライザ関数animate_hanoi_states() を作成したので,これを使って操作を可視化すること.
Source
HanoiRod: TypeAlias = list[int]
HanoiState: TypeAlias = list[HanoiRod]
def visualize_hanoi(
states: Sequence[Sequence[Sequence[int]]],
*,
interval: int = 600,
repeat: bool = False,
as_html: bool = True,
) -> animation.FuncAnimation | HTML:
"""ハノイの塔の盤面列をアニメーションとして表示する.
Args:
states: 盤面の列です.各盤面は 3 本の棒の状態を表し,
各棒は下から上の順で円盤番号を並べたリストにします.
例: ``[[3, 2], [1], []]``
interval: コマ送り間隔をミリ秒で指定します.
repeat: 最後まで再生したあとに繰り返すかどうかを指定します.
as_html: ``True`` のとき,Notebook でそのまま表示しやすい
``IPython.display.HTML`` を返します.
Returns:
``as_html=True`` なら ``HTML``,それ以外なら
``matplotlib.animation.FuncAnimation`` を返します.
"""
if not states:
raise ValueError("states must not be empty")
normalized_states: list[HanoiState] = []
for state in states:
if len(state) != 3:
raise ValueError("each state must have exactly three rods")
normalized_state = [list(rod) for rod in state]
normalized_states.append(normalized_state)
disk_values = [disk for state in normalized_states for rod in state for disk in rod]
if not disk_values:
raise ValueError("states must contain at least one disk")
if any(disk <= 0 for disk in disk_values):
raise ValueError("disk numbers must be positive integers")
max_disk = max(disk_values)
max_height = max(len(rod) for state in normalized_states for rod in state)
fig, ax = plt.subplots(figsize=(8, 4.5))
fig.patch.set_facecolor("white")
rod_x_positions = [0, 1, 2]
rod_width = 0.08
base_y = 0.0
disk_height = 0.6
disk_base_width = 0.28
disk_width_step = 0.24
colors = plt.cm.viridis(
[i / max(max_disk - 1, 1) for i in range(max_disk)]
)
def setup_axes() -> None:
ax.clear()
ax.set_xlim(-0.6, 2.6)
ax.set_ylim(-0.2, max_height * disk_height + 1.5)
ax.set_xticks(rod_x_positions)
ax.set_xticklabels(["左", "中央", "右"])
ax.set_yticks([])
ax.set_title("ハノイの塔")
for spine in ax.spines.values():
spine.set_visible(False)
ax.add_patch(
Rectangle(
(-0.5, base_y - 0.15),
3.0,
0.15,
color="#6b4f2a",
)
)
for x in rod_x_positions:
ax.add_patch(
Rectangle(
(x - rod_width / 2, base_y),
rod_width,
max_height * disk_height + 0.3,
color="#8b6f47",
)
)
def draw_state(state_index: int) -> list[Any]:
setup_axes()
state = normalized_states[state_index]
artists: list[Any] = []
for rod_index, rod in enumerate(state):
x_center = rod_x_positions[rod_index]
for level, disk in enumerate(rod):
disk_width = disk_base_width + disk * disk_width_step
x_left = x_center - disk_width / 2
y_bottom = base_y + level * disk_height
color = colors[disk - 1]
rect = Rectangle(
(x_left, y_bottom),
disk_width,
disk_height * 0.82,
facecolor=color,
edgecolor="black",
linewidth=1.0,
zorder=3,
)
ax.add_patch(rect)
artists.append(rect)
label = ax.text(
x_center,
y_bottom + disk_height * 0.45,
str(disk),
ha="center",
va="center",
fontsize=10,
color="white",
zorder=4,
)
artists.append(label)
step_label = ax.text(
2.45,
max_height * disk_height + 0.8,
f"step = {state_index}",
ha="right",
va="center",
fontsize=11,
)
artists.append(step_label)
return artists
ani = animation.FuncAnimation(
fig,
draw_state,
frames=len(normalized_states),
interval=interval,
blit=False,
repeat=repeat,
)
plt.close(fig)
if as_html:
#with open("ani.html", "w") as f:
# f.write(ani.to_jshtml())
return HTML(ani.to_jshtml())
return ani
example_states = [
[[3, 2, 1], [], []],
[[3, 2], [], [1]],
[[3], [2], [1]],
[[3], [2, 1], []],
[[], [2, 1], [3]],
]
#visualize_hanoi(example_states)
# ハノイの塔のソルバー
def hanoi_tower(n: int) -> list[HanoiState]:
"""ハノイの塔のソルバー.
Args:
n: 円盤の枚数
Returns:
円盤の移動過程を記録した盤面のリスト
"""
state: HanoiState = [list(range(n, 0, -1)), [], []]
states = [[rod[:] for rod in state]]
# TODO: ハノイの塔のソルバーを実装する(関数内に関数を実装して,再帰関数として利用するといいよ.
return states
# kick
solved_states = hanoi_tower(3)
display(solved_states)
visualize_hanoi(solved_states)【発展問題2】セルオートマトン・ライフゲーム(ループと条件分岐)¶
セルオートマトン¶
セルオートマトンとは,格子状に並んだ多数のセルが,
それぞれ決められた単純な規則にしたがって,次の時刻の状態へ更新されていくモデルである.
各セルは,たとえば 0 と 1 のような限られた状態だけを持ち,
自分自身や近くのセルの状態を見て次の値が決まる.
一つ一つの規則は単純であっても,全体として見ると複雑で面白い模様や振る舞いが現れるのが特徴である.
ライフゲーム¶
セルオートマトンの代表例が ライフゲーム である.
ライフゲームは,数学者ジョン・ホートン・コンウェイによって考案された2次元セルオートマトンであり,
生命の誕生・維持・死滅のような現象を,とても単純な規則で表そうとするものである.
各セルは「生きている (1)」か「死んでいる (0)」のどちらかの状態を持ち,
周囲8個のセルの状態によって,次の時刻に生き残るか,死ぬか,新しく生まれるかが決まる.
ライフゲームのルール¶
ライフゲームの基本ルールは,次のようにまとめられる.
誕生: 死んでいるセルのまわりに生きているセルがちょうど3個あると,次の時刻にそのセルは生き返る.
生存: 生きているセルのまわりに生きているセルが2個または3個あると,次の時刻も生き残る.
過疎: 生きているセルのまわりの生きているセルが少なすぎると,次の時刻に死ぬ.
過密: 生きているセルのまわりの生きているセルが多すぎても,次の時刻に死ぬ.
つまり,生きているセルが多すぎても少なすぎても生き残れず, ちょうどよいバランスのときだけ状態が維持される. また,何もないところから突然生物が生まれるのではなく, まわりの状態がそろったときに新しいセルが誕生する. このような性質が,生態系や集団の振る舞いを連想させるため, 単なるパズル以上に興味深い題材になっている.
実際にライフゲームを計算すると,初期状態の違いだけでまったく異なる模様が現れる. たとえば,一定の形を保ちながら移動する グライダー や, 周期的にグライダーを打ち出す グライダーガン のような有名なパターンが存在する. この点が,セルオートマトンの面白さである. 更新規則そのものは変えなくても,初期配置を変えるだけで,全体の振る舞いが大きく変わるのである.
実装¶
Python でライフゲームを実装するときは, 2次元配列や NumPy 配列を使ってセルの状態を表し, 各セルについて周囲8近傍の和を計算して次の状態を決めることが多い. この考え方は,配列処理,反復処理,条件分岐,可視化などの練習にもなり, プログラミングの学習題材としても非常に扱いやすい. ここではNumPy配列は使わずに,Listを使ってセルの状態を表すことにする.
参考として,ライフゲームの考え方や Python 実装例については, 2次元のセルラー・オートマトン(ライフゲーム)をPythonで試す - Qiita も分かりやすい.
Source
LifeGameBoard = list[list[int]] # ライフゲームの盤面を表す型
def visualize_lifegame(
states: Sequence[LifeGameBoard],
*, # キーワード引数
interval: int = 250,
repeat: bool = False,
as_html: bool = True,
) -> animation.FuncAnimation | HTML:
"""ライフゲームの盤面列をアニメーション表示する.
Args:
states: 各時刻の盤面を表す 2 次元配列の列です.
interval: コマ送り間隔をミリ秒で指定します.
repeat: 最後まで再生したあとに繰り返すかどうかを指定します.
as_html: ``True`` のとき,Notebook で表示しやすい ``HTML`` を返します.
Returns:
``as_html=True`` なら ``HTML``,それ以外なら
``matplotlib.animation.FuncAnimation`` を返します.
"""
if not states:
raise ValueError("states must not be empty")
normalized_states: list[LifeGameBoard] = []
row_count = len(states[0])
col_count = len(states[0][0]) if row_count > 0 else 0
if row_count == 0 or col_count == 0:
raise ValueError("each state must be a non-empty 2D grid")
for state in states:
if len(state) != row_count:
raise ValueError("all states must have the same number of rows")
normalized_state = [list(row) for row in state]
if any(len(row) != col_count for row in normalized_state):
raise ValueError("all rows must have the same length")
normalized_states.append(normalized_state)
fig, ax = plt.subplots(figsize=(6, 6))
image = ax.imshow(normalized_states[0], cmap="Greys", vmin=0, vmax=1)
ax.set_title("ライフゲーム")
ax.set_xticks([])
ax.set_yticks([])
step_text = ax.text(
0.98,
1.02,
"step = 0",
transform=ax.transAxes,
ha="right",
va="bottom",
fontsize=11,
)
def update(frame_index: int) -> list[Any]:
image.set_data(normalized_states[frame_index])
step_text.set_text(f"step = {frame_index}")
return [image, step_text]
ani = animation.FuncAnimation(
fig,
update,
frames=len(normalized_states),
interval=interval,
blit=False,
repeat=repeat,
)
plt.close(fig)
if as_html:
#with open("lifegame.html", "w") as f:
# f.write(ani.to_jshtml())
return HTML(ani.to_jshtml())
return anidef lifegame(
init_state: LifeGameBoard,
*,
n_steps: int = 50,
) -> list[LifeGameBoard]:
"""ライフゲームの盤面列を計算する.
Args:
init_state: 初期状態を表す 2 次元配列です.
n_steps: 何ステップ先まで計算するかを指定します.
Returns:
初期状態を含む,各時刻の盤面のリストです.
"""
if n_steps < 0:
raise ValueError("n_steps must be non-negative")
if not init_state or not init_state[0]:
raise ValueError("init_state must be a non-empty 2D grid")
current: LifeGameBoard = [list(row) for row in init_state]
row_count = len(current)
col_count = len(current[0])
if any(len(row) != col_count for row in current):
raise ValueError("all rows must have the same length")
if any(cell not in (0, 1) for row in current for cell in row):
raise ValueError("cells must be 0 or 1")
states: list[LifeGameBoard] = [[row[:] for row in current]]
# TODO: ライフゲームの盤面列を計算する.
return states
# kick
n_row = 100
n_col = 100
# 完全にランダム
# initial_state = [[random.randint(0, 1) for _ in range(n_col)] for _ in range(n_row)]
# 一文字
initial_state = [[1 if row == 50 else 0 for column in range(n_col) ] for row in range(n_row)]
# 十文字
# initial_state = [[1 if row == 50 or column == 50 else 0 for column in range(n_col) ] for row in range(n_row)]
# 偶数列
# initial_state = [[1 if column % 2 == 0 else 0 for column in range(n_col) ] for row in range(n_row)]
visualize_lifegame(lifegame(initial_state, n_steps=1000), interval=20)