nykergoto’s blog

機械学習とpythonをメインに

システム運用アンチパターン・本読み section2&3

社内本読みの記録です。GOTO・MEMOが僕のメモ/Discussionがその内容を社内で議論したときの記録になります。

§2: パターナリスト症候群

GOTO・MEMO

キーワード: ゲートキーパー

  • 承認における「決定権を持つ人」の比喩
  • ゲートキーパーはなにかイベントがあって発生するがあってもプロダクトのクオリティ向上には繋がっていない
    • 歴が長いグループだとゲートキーパーたくさん居るイメージ。
    • クオリティのうちのリスクの重み付けがとても大きい会社だとゲートキーパーは必須になってしまうのではないか? この作者の重み条件では最適化するとゲートキーパーは要らない、と言っているに過ぎないような (例えば医療系などミスったら大変なものはゲートキーパー必要と思う)
  • この作者が大事にしているところ
    • 人間のコスト / 煩雑さによる生産性低下
    • 人間のリスク / 「ルールを守らなかっただけでしょ!」とはいえ誰でもまちがえるくない?

自動化の方法

  • p:20 どうやってやるかの最初のステップは「みんなでメリットを共有すること」。
    • 気持ちの統一が大事。
    • 解決の方法には注目しない。
    • [atma的には] お客さんに提案するときもなにやるか先行じゃなくて先に問題を認識してもらうフェーズ入れたりしたほうが話早いかも。
  • エラー処理はこらず、全情報を出すことに集中する
    • [atma的には] フロントエンド実装とかにも言えるかも?

Discussion

ソフトウェア会社に対しての調査

  • 開発外の人が品質保証で入っても品質が上がらない
  • ウォータフォールでもアジャイルでも変わらない
  • 車の開発:外部に不必要・内部に必要なのでは? 現場の人が最終の品質担保に関わるのが良いのでは?

具体例: 開発におけるレビューというフェーズ

  • レビューはもっとも近いゲートキーパーなのではないか。
  • となると、本論通りゲートキーパーが不要なのならば、レビューも究極の形では自動のほうが良い?と言えてしまう
  • ただしレビューはゲートキーパー以外の役割(例えばコミュニケーションや指導など)もあり、正の外部性があるため採用されていると考えるのが良い。
  • レビュー基準として「レビューは早くやる」とよく言われる。これはゲートキーパー的な機能を減らすためともいいかえることができる。
  • 結論レビューに置いても、ゲートキーピング的な役割は少ないほうが良い。できる限り人のタスクを止めないように確認作業をしてマイナスの効果を少なくし、正の効果を受け取れるよう運用するべき。
  • だめな例: 他のPJからきたあまりContextがわかっていない人のレビュー
    • 単なるゲートキーピング化しており正の外部性が小さい

§3: 盲目状態での運用

GOTO・MEMO

  • アプリが何しているかを確認して共有しろ。意外と知らないよ。
    • 難しいドメインのものもそうだし、かんたんでもフローを完全に理解していないとメンタルモデルが間違っていることがあるので、注意しないといけないと思った。
  • 見るものを見ろ。取れてるものを見るな。
    • はい。
    • [atma的には] とくにぐるぐるではアプリが動いていることと問題ないことに差があるので、メトリクス定義して見るようにしたほうが良いかも。たとえば…
      • 1時間あたりの submission の増加数 (submissionが急に無くなったりするともしかすると submission POST のエンドポイントが止まっているかもしれない。)
      • あるいは、1時間あたりの submission エラーの増加数 (もしかしたらスコア計算のコードはおかしくないが、答えのデータなどの設定側がおかしくってエラーが増えているかもしれない)
  • とりあえず取れるログはいっぱい取れ。何かあったらすぐその良さに気づく。
    • 著者、なんかあったんかな…
    • [atma的には] そもそも取ってないやつが多い (cloudwatchには吐き出している?) 500エラーに対してのログなどを定期的に見る癖をつける必要があるかも。
  • logを取る系のサービスは使え。メンテコスト払えるのか?
    • 確かに。作らないのが一番バグらないと通じるところがある。
    • 自分の作業単価を考えてどの作業をするかを考える。

ref:

Discussion

よく使っているログツールのこと

  • cloudwatchlogs単体だとドメインの情報が全く乗っていない
  • アプリのコア部分・ステータスの状態遷移が多いものは何処で何が起こるかわからなくなる
  • 状態変更時にログを吐くような設計のほうが、あとあとで何が問題だったか追いやすい (ex: 対象のオブジェクトが時系列で更新される場合、ログがないとエラー時の状態を復元できない)

ref: ログをかんたんに取るツール

  • cloudwatch / amplify
  • sentry

ログを取る基準って何?

  • 複雑度が高い難しめのところ
  • 状態がころころと変わるもの (mutableに変更される object)
    • [キケン] status column を持つ
  • バッチ処理 (multi-tenant)
    • いろんなテナントまたいで処理するため何処でエラーになっているかわかるようにする
    • テナントごとにちゃんとできた・できなかったがわかるようにログを取る必要がある

どこからログを取る?

  • 全部でなくて大事なところからはじめていく
  • ビジネス的に大事なところ

ちょっと抽象的なのでルールを作りたいかも…

  • 案1: 更新系・登録系は全部対象とする
    • request / response
    • nginx / uwsgi のログに仕込む
    • AWSの ALB (ただし body は見れない)
  • ただしログイン系はマズイので除外する必要がある
  • 「全部でなくて大事なところからはじめていく」の原則に乗っ取ると、全部を一気にする〜というのはヨクナイのかも。ビジネスの重みを考えて、徐々にはじめていく。「みんなでメリットを共有すること」大事。
  • なんか結局著者が言っているところに落ち着く…この著者、天才なのでは…?!

項目応答理論の理論と実装

モチベーション: 変わった学校の定期テスト

ある学校では期末ごとに定期テストが開催されています。生徒は同じタイミングで同じ問題を解きます。この試験の結果から生徒の能力値を推定してほしいと依頼があったとしましょう。この場合すべての生徒は同じ問題を解いていますから、単純に問題の正解率を見たので十分でしょう。

ただ、この学校は大変に変な学校で、生徒ごとにランダムに違う問題が与えられているとするとしましょう。すると、正答率だけを見ているとたまたまかんたんな問題を与えられた生徒と、すごく優秀だけどめちゃくちゃ難しい問題ばかり与えられた生徒との区別ができませんから、おそらく不満が出てくるに違いないです。

このように、同じ問題を解いていない状態でも、解いている人の能力の比較をする枠組みの一つが項目応答理論と呼ばれるものです。

項目応答理論とは

項目応答理論は、複数のユーザがある問題を解いた結果 (正解 or 不正解) からそれぞれのユーザの能力や問題の難しさを推定する枠組みのことです。

身近な例だと TOEFL などが、この枠組をもとにユーザの得点を計算しています。

ja.wikipedia.org

項目応答理論のモデリング

いくつかのバージョンがありますが、項目応答理論では、ユーザ $i$ の能力 $\theta_i$, 問題 $j$ の難しさ $\phi_j$, 識別度 $a_j$ としたとき, この問題をとけるかどうか?の確率 $q_{i,j}$ が以下の式で表されるとします。

$$ q_{i,j} = \sigma \left( D a_j (\theta_i - \phi_j) \right) $$

ここで $\sigma$ は以下で表されるシグモイド関数で、$D$はロジスティック関数を累積正規分布関数に近似するための定数です。

$$ \sigma (x) = \frac{1}{1 + e^{-x}} $$

シグモイド関数は入力の値が大きくなると 1 に, 小さくなると 0 に徐々に近づきます。すなわち問題が難しくなるとどんどんと解答できる確率がゼロになり、反対だと1に近づきます。反対にユーザの能力が大きくなると1に近づき、小さくなると0に近づきます。

識別度は問題が能力値に応じて解きやすくなる度合いを表します。例えば特定のレベルを超えたユーザはほぼ100%正答できるけれど、そうでないユーザはほぼ不正解になるような問題は、識別度が高いです。

最尤推定によるパラメータ推定

ユーザの正答確率がモデル化されたので、これを利用して今持っている正解・不正解のデータが得られる確率を計算しましょう。

今持っている回答結果が $N$ 個あり、そのうち $n$ 番目の解答が正しいかどうかを表す変数を $t_{n} \in \left\{0, 1\right\}$ としましょう。このとき、その結果の起こりやすさ(尤度) $p_n$ は

$$ p_n = \sigma (x_n)^{t_n} (1 - \sigma (x_n))^{1-t_n} $$

です。ここで、$x_n$ は$n$番目の問題のユーザを$i$, 問題を $j$ としたときに

$$ x_n = D a_j (\theta_i - \phi_j) $$

となります。これは、先程仮定したユーザが問題を解ける (正解する) 確率を、ベルヌーイ分布に適用したものです。

実際にはこれが$N$個ありますので、すべてを掛け算することで、回答結果データ $D$ 全体の尤度

$$ p (D | \phi, \theta, a) = \prod_{n=1}^N \sigma_n^{t_n} (1 - \sigma_n)^{1 - t_n} $$

となります。実際には数値安定の観点から対数をとったあと全体にマイナスをかけた、負の対数尤度 $f$ の最小化問題とします。

$$ \min_{\theta, \phi, a} f = - \sum_{n=1}^N \left\{ t_n \ln {\sigma(x_n)} + (1 - t_n) \ln({1 - \sigma(x_n)}) \right \} $$

この問題を解く方法はいくつかありますが、今回は勾配降下法 (勾配法) で解くことを考えます。勾配法では各ステップごとに目的関数のパラメータに対する微分を計算して、その方向へパラメータを更新します。今回最適化する対象となるのは $M$ 人のユーザごとの能力 $\theta_i (i = 1, 2, \cdots, M)$ と、$N$ 個の問題の難しさ $\phi_j$ と識別度 $a_j$ です。

はじめに、ユーザの能力に対する微分を考えて見ましょう。

$$ \begin{aligned} \frac{\partial f}{\partial \theta_i} &= - \sum_{n = 1}^N \frac{\partial f_n}{\partial x_n} \frac{\partial x_n}{\partial \theta_i} \\ &= - \sum_{n \in I} \left\{ t_n (1 - \sigma_n) - (1 - t_n) \sigma_n \right\} \cdot \frac{\partial}{\partial \theta_i} \left( D a_j (\theta_i - \phi_j) \right) \\ &= - \sum_{n \in I} (t_n - \sigma_n) D a_j \end{aligned} $$

ここで、ユーザー $i$ が回答している結果の集合を $I$ としました。

1行目から2行目で $\Sigma$ の対象が $N$ すべてから集合 $I$ になっているのは、ユーザ $i$ が関係しない結果に対して $\theta_i$ で微分するとゼロになるためです。またシグモイド関数に関する微分で成り立つ $\sigma' = \sigma (1 - \sigma) $ を利用していることに注意してください。

問題の難しさ、識別度についてもほとんどこれと同様に計算ができます。ここで、問題 $j$ に対する解答の集合を $J$ としました。

$$ \frac{\partial f}{\partial \phi_j} = - \sum_{n \in J} - (t_n - \sigma_n) D a_j $$

$$ \frac{\partial f}{\partial a_j} = - \sum_{n \in J} - (t_n - \sigma_n) D (\theta_i - \phi_j) $$

実際に推定してみる

コードは以下の gist を参照ください。

反応応答理論の実装.ipynb · GitHub

サンプルデータの作成

実際に推定をおこなうために、人工的に問題への回答結果のデータを作成します。

作成方法は以下のとおりです。

  1. まずユーザがもつ能力と問題の難しさと識別度の正しい値をランダムに生成します。
  2. 次にランダムにユーザ・問題を取り出して、正しい値をもとに反応応答理論で計算される確率 $p$ を計算し、
  3. 確率 $p$ で正解, $1-p$ の確率で不正解とするベルヌーイ分布をもとにして、解答が正解したかどうかを作成します。

ユーザの能力と問題の難しさは正規分布から、識別度は 0.5 ~ 2 の間の一様分布としました。問題の数は 100個, ユーザ数は 1000人 で回答結果の組み合わせは 1万件とします。

N_PROB = 100
N_USER = 1000

true_user_levels = np.random.normal(0, 1, size=N_USER)
true_prob_levels = np.random.normal(0, 1, size=N_PROB)
true_problem_disc = np.random.uniform(0.5, 2, size=N_PROB)

results = []

for _ in range(10000):
    i, j = np.random.randint(0, N_USER), np.random.randint(0, N_PROB)
    p = calculate_correct_answer_probability(theta=true_user_levels[i], phi=true_prob_levels[j], a=true_problem_disc[j])
    t = np.random.binomial(n=1, p=p)
    results.append([
        i, j, t
    ])
    
results = np.array(results)

df = pd.DataFrame(results, columns=["user", "problem", "answer"])

最急降下法による推定

はじめに勾配法によってパラメータを推定します。

# 問題の難しさ
problem_levels = np.zeros(shape=N_PROB)
# 問題の識別度
problem_disc = np.ones(shape=N_PROB)
# ユーザの問題を解く能力
user_levels = np.zeros(shape=N_USER)

D = 1.71


for step in range(10):
    _user_levels = df["user"].map(user_levels.__getitem__)
    _prob_levels = df["problem"].map(problem_levels.__getitem__)
    _prob_disc = df["problem"].map(problem_disc.__getitem__)
    
    sigma = calculate_correct_answer_probability(_user_levels, 
                             _prob_levels,
                            _prob_disc)
    
    # 数値安定性のため極端な確率にならないよう min / max をきめて clip
    sigma = np.clip(sigma.values, a_min=1e-8, a_max=1 - 1e-8)
    
    diff = - df["answer"] + sigma
    
    print(objective(df["answer"].values, sigma=sigma))
    
    # 問題の難易度の勾配
    partial_prob_levels = diff * -1 * D * _prob_disc

    # ユーザの能力の勾配
    partial_user_levels = diff * D * _prob_disc

    # 問題の識別度の勾配
    partial_prob_disc = diff * D * (_user_levels - _prob_levels)
    
    # パラメータの更新
    user_levels -= partial_user_levels.groupby(df["user"]).mean().values  - 1e-8 * user_levels
    problem_levels -= partial_prob_levels.groupby(df["problem"]).mean().values - 1e-8 * problem_levels
    problem_disc -= partial_prob_disc.groupby(df["problem"]).mean().values

結果の可視化

正解・不正解の問題に対する期待正答確率の分布は以下の通りになりました。正解のとき確率1に近く、不正解のとき0に近いようにパラメータ更新ができていることがわかります。

次に問題・ユーザの真の能力と推定された能力を見てみます。

横軸に推定された問題の難しさ、縦軸に正しい問題の難しさを scatter plot として可視化したものが以下の図です。これを見ると対角線に並んでいて、概ね良い推定値が得られていることがわかります。

問題の難易度推定結果

同様にユーザの能力に対しても plot したものが以下の図です。ズレはあるもののこちらも概ねよく推定できていることがわかります。

ユーザの能力の推定結果

逐次的にデータが来る: Online Optimization の場合

先程の勾配降下法では、全体のデータを持っている前提で勾配を計算していました。実際の問題では、データが逐次的にやってくるような場合もあります。例えばオンライン英単語学習サイトがあって、ユーザが回答するたびにユーザの能力や問題 (この場合だと単語の難しさ) を更新したい、という場合などです。

このように逐次的にデータが来ることを前提とした最適化問題は Online Optimization と呼ばれます。これを解く方法の一つは、今やって来たデータだけで勾配を計算してパラメータを更新するというやり方です。これは、確率的勾配降下法とやっていることがほぼ同じですが、確率的勾配降下法ではデータセット全体はすでにあると仮定している一方 online optimization ではデータセット全体は手元にないと仮定している点が異なります。また online な設定ではデータは基本的に一回しか使わないですが、確率的勾配降下法では何度も繰り返してデータセットを利用して最適化を行います。

今回は回答結果データは1件づつやってくるとして、その結果についての勾配を使ってパラメータを更新します。 先程の実装と違い df.iterrows() でデータを1件づつ取得して、その勾配を計算している点が異なります。

from tqdm import tqdm

# 問題の難しさ
problem_levels = np.zeros(shape=N_PROB)
# 問題の識別度
problem_disc = np.ones(shape=N_PROB)
# ユーザの問題を解く能力
user_levels = np.zeros(shape=N_USER)

D = 1.71

snapshots = []

for i, row in tqdm(df.iterrows()):
    _user_levels = user_levels[row["user"]]
    _prob_levels = problem_levels[row["problem"]]
    _prob_disc = problem_disc[row["problem"]]

    sigma = calculate_correct_answer_probability(
        theta=_user_levels, 
        phi=_prob_levels, 
        a=_prob_disc
    )
    sigma = np.clip(sigma, a_min=1e-8, a_max=1 - 1e-8)

    diff = sigma - row["answer"]

    # 問題の難易度
    partial_prob_levels = diff * -1 * D * _prob_disc

    # ユーザの能力
    partial_user_levels = diff * D * _prob_disc

    # 問題の識別度
    partial_prob_disc = diff * D * (_user_levels - _prob_levels)

    user_levels[row["user"]] -= partial_user_levels - 1e-4 * _user_levels
    problem_levels[row["problem"]] -= partial_prob_levels - 1e-4 * _prob_levels
    problem_disc[row["problem"]] -= partial_prob_disc * 1e-2 + 1e-6 * (1 - _prob_disc)
    
    if i % 100 == 0:
        snapshots.append([
            user_levels.copy(), problem_levels.copy()
        ])

結果の可視化

さきほどの最急降下法と同じように問題・ユーザの推定結果と正しい値を plot したものが以下の図です。オンラインの設定でも、ある程度の推定ができていることがわかります。

Online設定の場合・問題の難しさの推定結果

Online設定の場合・ユーザの能力の推定結果

ユーザの能力推定値の推移

ユーザの推定能力値は時系列に伴って逐次更新されます。実用上はステップ数が増えると正しい推定値を得てほしいですよね?

ステップごとの変遷を見るため、online の更新 100 回ごとに各々のユーザの推定能力値を可視化したものが以下の図です。

Online設定の場合・横軸がステップ数縦軸がその時点でのユーザの能力推定値。

黒い点線がそのユーザの真の能力で、青い線がステップごとの推定値の遷移です。これを見ると大体の値は推定できていますがたまに大きくハズレた推定をしてしまっているものがあることがわかります。これは online 更新だと今までの回答履歴等を考慮していないため、ある難しい問題にたまたま正解したりすると急に値が増えたり逆に減ったりする可能性があるためです。

同じように問題ごとの推定値変遷を可視化すると以下のようになります。ユーザよりも問題のほうが数が少なくて、一つの問題あたりの回答数が多いため、ユーザの推定値よりも正確性が高そうであることが伺えます。

横軸がステップ数縦軸がその時点での問題の推定値。黒い点線が真の値。

今回の問題設定の課題

今回のモデリングではユーザの能力が時系列に対してかわらないことを仮定しています。実際には時間がたつとユーザの能力や問題の難易度も場合よっては変動しますので、それらを組み込んだモデルを使う必要があります。 (今日はいったんここまで。気が向いたらまた調べる。)

ゼロ状態を考える

今回はコードを書く時の気持の話です.

たとえば配列 x の要素が 0.5以下の掛け算の値を取得する、みたいなことをしたいとします. たとえば以下の code で出来るような処理です (map つかおうやとかは置いておいて...)

import numpy as np

def calc_factor(x):
  if x > .5:
    return None

  return x

X = np.random.uniform(size=10)
retval = None

for x_i in X:
  # 要素に対して掛け算の値を計算
  r_i = calc_factor(x_i)

  # 条件に合わない時何もしない
  if r_i is None:
    continue
  
  # 今までに値がなかったら今計算した値で置き換え
  if retval is None:
    retval = r_i
  
  # そうでない時掛け算して更新する
  else:
    retval *= r_i

このコードを書いた人は大変真面目なので値が全く存在しないゼロ状態のことを None と表現していることがわかります. それは

  • calc_factor で条件にマッチしない時 None を返していること
  • 計算結果 retval の初期値が None であること

からわかりますね. ここで掛け算という計算(演算)を考えると 1 を書けても結果は変わりません. これは足し算でゼロを足してもゼロになるのと同じです. これを使うと上記は以下のようになるでしょう.

import numpy as np

def calc_factor(x):
  if x > .5:
    # None を返さない
    return 1

  return x

X = np.random.uniform(size=10)
# None で初期値としない
retval = 1

for x_i in X:
  # 全部掛け算していく
  retval *= calc_factor(x_i)

だいぶスッキリ書けましたね! この掛け算という計算に対して何も変わらないものを考えてきれいにする、という操作は、配列の足し算 (concat) とか Object の要素の追加とか、他の概念の演算に対しても言えることです。

配列の追加

たとえば配列の要素から新しい配列を作成して全部まとめるみたいな処理があったとしましょう. map とかを使わないとすると以下のような感じ

def create_array(value) -> Optional[List]:
  if value > 5:
    return None
  
  return [value] * value


new_array = []

for x in range(10):
  value_i = create_array(x)

  if value_i is None:
    continue
  new_array.extend(value_i)

配列の足し算に対して空っぽの配列 [] はゼロの意味を持っています。のでこれを使うと以下のようになります. すっきり!

def create_array(value) -> List:
  if value > 5:
    # なにもないとき None を返さない
    return []
  
  return [value] * value

# None で初期値としない
new_array = []

for x in range(10):
  # 全部足し算していく
  new_array += create_array(x)

pandas.DataFrame の場合

たとえばですが pandas.DataFrame などもこれと同等の操作をできるようにAPIを設計してくれています. 例えば配列の値が 0.5 以下の場合にランダムにデータフレームを作成、横方向につなげる、ということをやりたいとしましょう (そんなん意味あるんかというのは置いておいて、機械学習の何かしらでありそうな感じになってきました.

import pandas as pd

def create_df(x):
  if x > .5:
    return None

  return pd.DataFrame(np.random.uniform(size=(10, 4)))

out_df = None
x = [.3, .4, .6, .2, .3]

for x_i in x:
  _df = create_df(x_i)

  # 値がない時なにもしない
  if _df is None:
    continue

  # 今までに計算している値が無かったら置き換え
  if out_df is None:
    out_df = _df
  # そうでなければ横に足し算する
  else:
    out_df = pd.concat([out_df, _df], axis=1)

実は pd.concat という操作に対して pd.DataFrame() はゼロを意味します(pd.DataFrame() を concat しても結果が変わらない). これを使うと、さきほどの数値に対する掛け算や、配列に対する足し算とおんなじようなことができます.

import pandas as pd

def create_df(x):
  if x > .5:
    # zero 状態の data frame をかえす
    return pd.DataFrame()

  return pd.DataFrame(np.random.uniform(size=(10, 4)))

# zero 状態の data frame を初期値とする
out_df = pd.DataFrame()
x = [.3, .4, .6, .2, .3]

for x_i in x:
  # 全部くっつける
  out_df = pd.concat([out_df, create_df(x_i)], axis=1)    

Django Q object の場合

Django というフレームワークの話です。Djangoではデータベースへのアクセスで、データを絞り込むクエリーを表現する Q instance というものがあります。

絞り込みは複数の and とか or をやりたいので、 Q にたいしても and / or を行うことで複数条件での絞り込みを表現できるように Django が設計してくれています。 例えば Q(hoge=1) & Q(huge=10) とやると hoge=1 かつ huga=10 のデータを検索してくれます。

じつは Q のAPIでは and /or の操作に対して Q() 渡しても結果は変わりません(!) これは先ほどから見ていた「掛け算で1を掛けても結果がかわらない」という関係性と同じですね!

ですから、例えば複数の条件を or でまとめたいな−というとき、以下のようなコードは正しいのですが、

def calc_query_object(value):
  if value > 5:
    return None
  # 何らかの大変な条件の計算によって kwrgs が出来るとする
  kwrgs = {}
  return Q(**kwrgs)

q_object = None

# なんかたくさんの条件を or 条件でまとめたい
for value in range(10):
  q_i = calc_query_object(value)

  if q_i is None:
    continue

  if q_object is None:
    q_object = q_i
  else:
    # or でまとめる
    q_object = q_object | q_i

ゼロ状態を意識して

def calc_query_object(value):
  if value > 5:
    # 空っぽの Q を返す
    return Q()
  kwrgs = {}
  return Q(**kwrgs)

# 空っぽの Q で初期化する
q_object = Q()

for value in range(10):
  # 全部ガッチャンコ
  q_object = q_object | calc_query_object(value)

と記述したくなってきますね;)

面白いなと思ったら

モナドという概念を調べてみてください. ここで紹介したのはモナドの一部です.

実践Django / 一歩先に進むことが出来る本

このたび実践Django Python による本格Webアプリケーション開発を著者の芝田さんから頂きました。もともとこの本は買おうと思っていたので、大変嬉しくちょうだいいたしました。芝田さん、ありがとうございます:D

頂いてから1ヶ月弱ほどたってしまったのですが、この本を読んで思った良い所、どういう人に読んで欲しいかなど、僕なりの感想を書いていければなと思っています。

www.shoeisha.co.jp

本当にざっくりとですが、本書の特徴的な部分を挙げるとすると以下の2つになると考えています。

  1. Django の難しいところをフォローしている
  2. Web開発に必要な実践的知識がいっぱい詰まっている

Django で詰まる一番難しいところを綺麗にフォローしている

Django は公式ドキュメントが大変に充実していて、チュートリアルもしっかりと用意されています。ただやはり英語がメインになるということと、そもそも Djangoフルスタックなフレームワークであるため、そのしきたりに慣れていくのはなかなかに大変です。

本書では Section1 でステップバイステップで簡単なアプリケーションを実装していきますが、データの流れを可視化した図や、引数に対する意図やコメントなどが随所にあり、初めて実装をする人でも理解しやすいような構成の工夫が取られています。またその後のセクションで、テンプレートやORMなどの機能ごとに説明がありますが、こちらにも随所にフロー図やクラス図があり、Django の複雑なクラス構成や設定の構成が視覚的に理解できるようなっています。

Django を始めたてあるいは python をそれほど長く使っていないユーザは Django のクラス構成や設定部分の理解ができず、チュートリアルは写経でできるけれどそれ以上が難しい、ということがあると思っていて、僕もこのフェーズで相当に苦しんだ経験があります。本書はチュートリアルを本質から理解するための良い手がかりとなってくれるでしょう。

正直この図だけでも「あと3年早く読みたかったな」と思いました。

Web開発に必要な実践的知識がいっぱい詰まっている

次に言えるポイントは「Web開発に必要な知識がたくさんある!」という点でしょう。ここであえて[Web 開発に]と書いたのは、本書で扱っている内容が、単に Django の知識にとどまらないからです。それは例えばデータベース・認証認可・セキュリティ・テストなどが該当します。

例えばデータベースの部分を取り上げましょう。もちろんDjangoに関する本ですから、データベースへのアクセスをDjangoでどのように行なうかという内容は、当然触れられていますが、単に使うだけにとどまらず「では実際のSQLはどうなるか」、「そもそもSQLを効率的に実行するためにはどうすればよいか」、更にはクエリ解析によるパフォーマンス確認にまで言及されています。

正直単にDjangoを使うだけならばここまで書く必要がないだろうと思いますが、実際にWebアプリケーションを作る場合には、DBの動作を考えたり、その確認方法を知っていることは大事なことです。これらの知識は単に Djangoチュートリアルをやり、ドキュメントを読んだだけでは身につかない実践的な内容です。

上記ではデータベースセクションを取り上げましたが、他のトピックに関しても同様で、単に Django で実装するにはどうすればいいか? の how to にとどまらない、アプリケーションを動かすための知識が詰まっています。個人的にはテストのセクションでどういう方針でテストを書けばよいかや、ユーザモデルの拡張方法とそれぞれのメリットデメリットなど実装していて悩むところに関して「こういう書き方もあるけど、こういうのあるしいいよ!」のような芝田さんの意見や気持ちが載っている部分がとても良いと感じています。時間差でペアプロしているような(?)感覚に近いかもれませんね。

どういう人に読んで欲しいか

本書は以下のような人に特に向いているなと感じました。

  1. PythonでWebアプリを作りたいなと思っている人
  2. Djangoチュートリアルだけやってみたけれどその後が続かなかった人
  3. pythonの他のフレームワークでエンドポイント1/2個の単純なAPIは作ったことあるけれどそれ以上はない人
  4. Djangoを仕事で使っていてもうちょっとステップアップしたい人

1. PythonでWebアプリを作りたいなと思っている人

これから Web アプリを作ってみたい人には、大変オススメできます。最初は少し大変だと思いますが、Python の勉強をしつつ第一章をこなすだけでも相当力が着くと思いますし、公式ドキュメントを見てあれこれするよりも全体像がつかみやすいですので、その後の学習もスムーズになるではないでしょうか。

2. Djangoチュートリアルだけやってみたけれどその後が続かなかった人

これは本書の特徴 1 で書いたように、難しいところをフォローしている部分がドンピシャで刺さると思います。図や適宜あるコメントなどを参考にしつつ、もう一度チャレンジしてもらえると大変学びがあるのではないかと思っています。

3. pythonの他のフレームワークでエンドポイント1/2個の単純なAPIは作ったことあるけれどそれ以上はない人

これはいつもはwebアプリケーションを作るのがメインではなくて、やったことがあるとすると簡単なレスポンスを返すアプリを Flask / Bottle でちょっと作ったことがあるな、ぐらいの人を想定しています。例えば機械学習の推論用サーバだけ実装、見たいな感じでしょうか。

自分もこのレイヤにいたことがあるのでわかるのですが Django はとてもハードルが高そうに見えるのですよね。実際フルスタックなフレームワークですから、覚えることも多く Flask などのようにさっくりとは作れないので、まあいいかで諦める… ということはママあるのだろうなと想像しています。また Web アプリケーションに必要な知識もそこまで持ち合わせていないので、やりたいことを実現するためにはそもそもどうやったら良いのかわからないし普通どうやってやるんだろう、と思っている人が多いのではないでしょうか。

そういう人がちょっと DB との接続もあってユーザ認証とかもあるアプリをやりたいな! と思った時、Djangoの初心者に向けた内容から実践的Webアプリの内容まで含まれている本書は、まさにピンズドな選択肢と言えるでしょう。諸手を挙げておすすめできます。

4. Djangoを仕事で使っていてもうちょっとステップアップしたい人

最後のこれは私です。なのでみんなが該当するとは言わないのですが、読んでいてなんとなくの理解で使っていた部分や知らない部分など見て相当に勉強になりましたし、Django の勉強の意欲をもらえてとても楽しく読んでいました。(完全に感想になってしまった)

まとめ

どんな人が読んでも、一歩先に進むことができる大変良い本です。

良え本や。

www.shoeisha.co.jp

github にサンプルコードがあるようですので、購入前に中身の雰囲気を知りたい方はこちらも参考にしていただくのが良いかもしれません。*1

github.com

*1:readmeにある「本書のコードが動かなくなってしまうかもしれませんが、Djangoソースコードやリリースノートを読みながら動かなくなった原因を探り、修正する過程で得られる知識は無駄にならないはずです。ぜひチャレンジしてみてください。」というのがとても好きです。この本が、単なる初心者向け入門ガイド書にならなかった理由がよくわかります。