ふぁむたろうのブログ

機械学習系のお話やポエムを投稿します

「機械学習のエッセンス」を読んだ感想とかメモ

先日同期から 機械学習のエッセンス -実装しながら学ぶPython, 数学, アルゴリズム- (Machine Learning) | 加藤 公一 | Amazon を推され、神(著者ご本人様)からの啓示圧力もいただきましたので、買って読みました。

以下に各所感や自分が読んだ際のメモを残します。 全実装は終わってないので注意

結論

  • この記事読むくらい買うか悩んでるなら買いましょう
    • まあ 2,800 円(税抜き)なんて人生において許容誤差ですし
  • 演習問題もちゃんとやりましょう
  • 誤植はある(でも第一刷からに第二刷にかけてかなり減った)ので、数式まで追う人は気をつけましょう
    • 正誤表 は予め見ておきましょう
    • kindle 版買えば刷新されたときに Update できるかも(要確認)
    • 僕は紙版(2刷)買って最初に正誤表見てボールペンで書き込みました
      • そんなに多くなかったのですぐ修正終わりました
    • ただ僕が読んだときは誤植はそこまでクリティカルに感じなかったです
  • 地味にページめくる時にうるさいです
    • 紙版ですとページめくるたびに表紙やカバーがギチギチ言うので、地味に気になります
    • 図書館だと読めないなってくらいです
      • どちらにしろ PC 叩くこと多いので図書館では読むことはなさそうですが
    • プロフェッショナルシリーズとかはそんなことなかったので、気になってきます

個人的には各回帰(リッジ・ロッソ)や SVM の実装において必要となる式の導出から Python 実装までをカバーされていて非常に勉強になりました(Python 3 で動くということも大変ありがたいです)。

SVM 等は 2014〜2016 年頃(自身が修士の頃)ですと、僕の周辺では サポートベクターマシン入門 | ネロ クリスティアニーニ, ジョン ショー‐テイラー, Nello Cristianini, John Shawe‐Taylor, 大北 剛 | Amazonパターン認識と機械学習 上 | C.M. ビショップ, 元田 浩, 栗田 多喜夫, 樋口 知之, 松本 裕治, 村田 昇 | Amazon を参考文献にしていたのですが、これらの文献は深いところまで記載されている一方、理論からコードに落とし込もうとすると「???」となって手が全く動かなくなることがあり、この本のように理論から実装につなげてくれる著書は無かった(見つけられなかった)です。当時欲しかったなあ

一方実装までカバーされている分、理論面では足りないところ(例えばカーネル SVMカーネル部分とか)もあります。ですのでこの本読んでもっと知りたい気持ちを膨らませて上記の本とかにチャレンジするのが良いと思います。

あと演習問題もあってプログラミングに限らず手を動かす場面もあって良かったです。

この本を薦めたい人

  • エンジニアやってきて、もう少しちゃんとキカイガクシュウシタイナーって人
  • 理系学部1年生等の、線形代数微積の授業やってて寝ちゃう人
    • 使い途わかればもう少しちゃんとやれる人って結構いる気がするので
  • 人工知能って単語にワクワクした大学生
  • 行列系の数式読んでて「ウッ」となる人
  • 数式から Python(numpy) コードに落とし込む際に「ウッ」となる人

この本を薦めない人

  • 手を動かしたくない人
    • この本に出てくる計算や演習問題・Python コードは自分で書いて動かしてなんぼなので
      • 今買えば本文中のコードがそのまま動くのに動かさないのはもったいないです
    • 紙版買ったらまず正誤表見て直した方が良いので、手を動かす前提でこの本は買いましょう
  • サクッと概要だけ押さえたい人
    • この本はほどよく深く(浅く)ほどよく広い(狭い)ので、広く浅く概要だけ掴みたい場合には不向きだなと思います
  • モデル選択や細かいパラメータチューニングについて知りたい人
    • 本書の目的にもある通りココらへんはあまり触れてません
  • とりあえず Deep Learning を動かしたい人
    • 著者も述べていますが、他に詳細な本がたくさんあります
    • とはいえ著者が言う "この本が Deep Learning の理解に役に立つ"というのは同意します

各章の所感とか個人用メモ

前提

  • Python の環境構築・基礎あたりはちゃんと読んでません
    • 手元に環境あったので
  • 自分の環境は Python 3.7 です
    • 自分でコード書く時は以下の環境を都度都度(気分で)変えてやってました
      • iPython
      • jupyter notebook
      • jupyter lab

第1章「学習を始めるまえに」

  • 本書の目的と本書が含まないものが載ってて良かったです
    • 初めに必ず目を通すべき

第2章「Python の基本」

  • 例外処理まで触れられてて良いなと思いました
  • Python 3.6 からは "a: {:2d}".format(a)f"a : {a:2d}" で書けたりするので興味がある人はお試しください
    • "fstring" とか "フォーマット済文字リテラル" とかで調べると出てきます
  • オブジェクト指向
    • ここは深追いすると二度と帰ってこれなくなるので、これくらいの深度で十分だと思います
    • クラス=メソッド+データ 程度の理解だけ頭において先に進んだ方が良いです

第3章「機械学習に必要な数学」

  • "ブロック化"とかたしかにそんな単語あった気がする…
  • 機械学習に必要な線形代数微積がピックアップされてるのすごくありがたい
    • 逆行列とか固有値周りのお話はいつまでも着いてきます
    • 逆行列を持つための条件は知っておくといつか役に立ちそう
      • 3次元以上の行列でも  \rm{det} A \neq 0 の場合に逆行列を持つこととかは自分で調べた方が良い
  • 対称行列(半正定値・正定値)やとかも、単語だけ覚えておいてまたどこかで出てきたらこの本とか他の詳しい本を見れるようにしておきたい
  • 多変数関数の極値とヘッセ行列の関係は知らなかった…(一度聞いて忘れたのだと思われ)
  • 二次形式の勾配の形はまれによく出るので知っておくと良い

第4章「Python による数値計算

  • 桁落ちとか有効桁数・誤差
    • 忘れかけてたので良い戒めになった
    • 機械学習続けてると "1e-8 の誤差とか知らんがな" と思う時が結構あったので
  • 疎行列の扱い
    • めっちゃ参考になりました
    • 行とるか列をとるかで crc_matrixcsr_matrix を使い分ける
    • それぞれ getrow()getcol() の効率が違う
  • 逆行列・numpy の solver・LU分解を使った一次連立方程式の解法
    • 個人的にすごく楽しかったです
    • これまで必死に手計算で方程式を解いてきた人類にとっては、これってものすごく衝撃的だと思うんですよね
      • 人によっては解放感とか爽快感があると思います
      • 大学デビューとはかくあるべき
  • データの可視化
    • 僕自身細かい文法忘れることが多いので、たまにこの本とかに帰ってくる気がしてます
  • 数理最適化
    • 今後機械学習やるなら絶対押さえなきゃいけないところ
    • 線形計画法や2次計画法等の解析的に解ける(≒式変形等で解ける≒手計算で解を出せる)問題から解析的に解けない 問題に対しどのように解を出そうとしているかが載っています
    • 最急降下法ニュートン法について以下の違いは押さえておきましょう
      • 更新式
      • 収束条件
    • "数値微分に関する補足" も地味に大事なお話だなと思いました
    • ラグランジュの未定乗数法
      • チッ…はいはいラグランジュラグランジュ
      • 議論の抽象度的にはここ結構難しかったです…
      • きついと思ったら一旦軽く流して先に進んでも良いと思います
        • 気になったら他の文献とかまたここに戻ってくれば良いですし
    • 統計
      • ここも高校までは手計算地獄だった分「数行で書けちゃう俺Tueeeee」できたりして楽しかったりします
      • 後半は数式多めだけど大事なので体力ある人は追いましょう

第5章「機械学習アルゴリズム

この本のメインテーマ(だと思ってます)

  • 準備
    • 回帰と分類の違いは押さえておきましょう
    • インターフェースについては、ここで押さえておくと他のライブラリを使った時なども楽になれます
  • 回帰
    • なにを最適化(最小化 or 最大化)しようとしているのかは押さえましょう
    • 3次元空間でのプロット法が載ってるのすごくありがたいです
    • 外部データを自分で持ってきて読み込ませるのは大事な経験なので初めての人は必ずやりましょう
    • 評価指標は必ず押さえてください
  • リッジ回帰
    • 線形回帰 + 正則化項(L2)
    • 正則化項足すと何が嬉しいのか自分の手で動かして実感しましょう
    • さりげなくベクトルwで微分してるけど、ちゃんと自分の手でもできるか確認しましょう
    • ここで出てくる "ハイパーパラメータ" と "チューニング" は長い付き合いになる可能性があります
      • 人間が手で調整しないために機械学習してるのになんでまたパラメータが出てくるのか
      • 上記は自分の手を動かすと実感が湧く可能性あるので是非やってみましょう
  • 汎化と過学習
    • 交差検証(クロスバリデーション)はめっちゃ大事。めっちゃ大事
  • ラッソ回帰
    • 線形回帰 + 正則化項(L1)
    • 線形回帰・リッジ回帰と何が違うのか
    • 座標降下法(corordinate descent) を絶対値関数にどう適用するか
    • 必要な式変形(うまく j=k の部分のみ抜き出せるか等)
    • 上記の実装部分
  • ロジスティック回帰
    • 回帰と分類がつながる箇所
    • ココらへんも式変形が続くので、気を抜いて一行飛ばしたりすると訳分からなくなるので注意
    • 式変形が長いと変形ばかり気にして何をしようとしているのか頭から抜けちゃうのも注意
  • サポートベクターマシン(要実装)
    • この本の中で一番きつい箇所
      • 本当にきつかったら一度とb(
      • もう、ゴールしてもいいy
      • 実装まで追いきれなかったので余裕あれば別記事にします…
    • 解くべき問題がどんどん変形していくのでゆっくり読み進めた方が良いです
    • KKT 条件出てくるので忘れた方は第4章の該当箇所読み返しましょう
    • 最終的にサポートベクタ周りだけ着目
    • ハードマージン・ソフトマージン・カーネルという単語は押さえる
      • 3つとも必要な数式の導出と(動く)実装まであるのは貴重
  • k-Means 法
    • SVM 超えた方にとってはボーナスステージ
  • 主成分分析(PCA)

個人用メモ(コード周り)

第4章「Python による数値計算

1次連立方程式の解法の比較(逆行列・numpy.linalg.solve・scipy.linalg.lu_factor)

この規模の小ささですと numpy.linalg.solve が最も速かったですが、 行列が大きくなったり b が複数列になってくると LU 分解(lu_factor) が速くなってくるのだと思います。

In [1]: import numpy as np

In [2]: a = np.array([[3, 1, 1], [1, 2, 1], [0, -1, 1]])
   ...: b = np.array([1, 2, 3])

In [3]: np.linalg.inv(a) @ b
Out[3]: array([-0.57142857, -0.14285714,  2.85714286])

In [4]: np.linalg.solve(a, b)
Out[4]: array([-0.57142857, -0.14285714,  2.85714286])

In [5]: from scipy import linalg as slinalg

In [6]: lu, p = slinalg.lu_factor(a); slinalg.lu_solve((lu, p), b)
Out[6]: array([-0.57142857, -0.14285714,  2.85714286])

In [7]: %timeit np.linalg.inv(a) @ b
13.2 µs ± 968 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [8]: %timeit np.linalg.solve(a, b)
8.03 µs ± 79.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [9]: %timeit lu, p = slinalg.lu_factor(a); slinalg.lu_solve((lu, p), b)
28.4 µs ± 569 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [10]: %timeit slinalg.lu_solve((lu, p), b)
17.5 µs ± 96.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

第5章「機械学習アルゴリズム

ラッソ回帰
import numpy as np
import csv

def soft_thresholding(x, y):
    return np.sign(x) * max(abs(x) - y, 0)

class Lasso:
    def __init__(self, lambda_, convergence_thres=0.0001, max_iter=1000):
        self.lambda_ = lambda_
        self.convergence_thres = convergence_thres
        self.max_iter = max_iter
        self.w_ = None
    
    def fit(self, X, t):
        n, d = X.shape
        self.w_ = np.zeros(d + 1)
        avg_L1 = 0.
        for _ in range(self.max_iter):
            avg_L1_prev = avg_L1
            self._update(n, d, X, t)
            avg_L1 = np.abs(self.w_).sum() / self.w_.shape[0]
            if abs(avg_L1 - avg_L1_prev) <= self.convergence_thres:
                break
        
    def _update(self, n, d, X, t):
        self.w_[0] = (t - (X @ self.w_[1:])).sum() / n
        w0vec = np.ones(n) * self.w_[0]
        for k in range(d):
            ww = self.w_[1:]
            ww[k] = 0
            q = (t - w0vec - (X @ ww)) @ X[:, k]
            r = X[:, k] @ X[:, k]
            self.w_[k + 1] = soft_thresholding(q / r, self.lambda_)
    
    def predict(self, X):
        if X.ndim == 1:
            X = X.reshape(X.shape[0], 1)
        Xtil = np.c_[np.ones(X.shape[0]), X]
        return Xtil @ self.w_


def main():
    # Read data
    Xy = []
    with open("winequality-red.csv") as fp:
        for row in csv.reader(fp, delimiter=";"):
            Xy.append(row)
    Xy = np.array(Xy[1:], dtype=np.float64)
    
    # Split train & test data
    np.random.seed(0)
    np.random.shuffle(Xy)
    n_test = 1000
    train_X, train_y = Xy[:-n_test, :-1], Xy[:-n_test, -1]
    test_X, test_y = Xy[-n_test:, :-1], Xy[-n_test:, -1]

    # Show results reach hyper parameters
    for lambda_ in [1., 0.1, 0.01]:
        model = Lasso(lambda_)
        model.fit(train_X, train_y)
        y = model.predict(test_X)
        print(f"--- lambda: {lambda_} ---")
        print("coefficients:")
        print(model.w_)
        mse = ((y - test_y) ** 2).mean()
        print(f"MSE: {mse:.3f}")
    

if __name__ == '__main__':
    main()