TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

【読書ノート】Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems

Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems」を読んだので内容をまとめる。

以下の内容は、ほとんどClaude3.5 Sonnet v2を使用して作成している。

この書籍は、ソフトウェアやハードウェアのデバッグに関する9つの基本ルールを解説したものである。システムを理解し、障害を再現し、思い込みを排して観察し、問題を分割して特定し、一度に1つの変更を行い、記録を残し、基本的な確認を怠らず、新しい視点を取り入れ、最後まで確実に修正を確認する。これらのルールを豊富な実例とともに説明しており、経験の浅いエンジニアから熟練者まで、デバッグスキル向上に役立つ実践的な指針となっている。

目次

第1章 はじめに

1. 要約:
本書は効率的なデバッグ手法を1冊にまとめた実践的なガイドである。著者の26年に及ぶシステム設計とデバッグの経験から、長時間を要したバグの原因は基本的なルールの軽視にあり、優れたデバッガーは本能的にこれらのルールを理解し適用していることが判明した。本書で紹介される9つの黄金律は、エンジニアだけでなく、プログラマー、技術者、カスタマーサポート担当者など、問題解決に携わるすべての人々に適用できる。特定の言語やツールに依存せず、普遍的な手法を提示している点が特徴的である。また、ソフトウェアやハードウェアに限らず、自動車、家屋、配管システムなど、様々な領域での問題解決にも応用可能である。本書は品質管理プロセスやテスト手法については扱わず、バグの発見と修正に特化している。トラブルシューティングガイドとは異なり、新しい未知の問題に対しても効果的なアプローチを提供する。

2. 重要なポイント:

  • デバッグの長期化は基本ルールの軽視が原因
  • 熟練デバッガーは無意識にこれらのルールを実践している
  • 9つの黄金律は普遍的で、様々な分野に適用可能
  • 特定のツールや言語に依存しない手法
  • トラブルシューティングとデバッギングの違いを明確に区別
  • プロセス改善やQA活動とは異なるフェーズに焦点
  • 実践的で簡潔な内容構成

3. 考察:
本書の特筆すべき点は、デバッグを「芸術」から「科学」へと昇華させようとする試みにある。多くのエンジニアがデバッグを経験と勘に基づく技能として捉える中、著者は普遍的な法則として体系化することに成功している。

このアプローチは、ソフトウェア工学の歴史的な発展とも符合する。かつてプログラミングは「職人技」として認識されていたが、構造化プログラミングやオブジェクト指向など、体系的な方法論の確立により、より科学的なアプローチが可能となった。本書はデバッグについても同様の進化を目指している。

また、本書が提唱する手法は、現代のソフトウェア開発環境においてより重要性を増している。マイクロサービスアーキテクチャの普及やクラウドネイティブな開発により、システムの複雑性は増大の一途を辿っている。従来の経験則だけでは対処できない問題が増加する中、体系的なデバッグ手法の必要性は高まっている。

特筆すべきは、本書が「トラブルシューティング」と「デバッギング」を明確に区別している点である。既知の問題に対する対処療法的なアプローチと、未知の問題に対する体系的な解決手法の違いを理解することは、効率的な問題解決において極めて重要である。

さらに、本書の手法は単なる技術的なツールの使用方法を超え、問題解決のための思考プロセスそのものを扱っている。これは、技術の進化によってツールが変化しても、基本的な考え方は普遍的に適用可能であることを意味する。

結論として、本書は単なるデバッグ技術の解説書を超え、複雑化する現代のシステム開発において、効率的な問題解決を可能にする思考の枠組みを提供するものといえる。9つの黄金律の習得は、今日のソフトウェアエンジニアにとって必須のスキルとなるだろう。

第2章 ルール集—額に入れて飾るのにふさわしい

1. 要約:
第2章では、効果的なデバッグのための9つの基本規則が提示されている。これらの規則は、シャーロック・ホームズの言葉を引用しながら、一見理論的に見えても実践的な価値があることを強調している。規則は「システムを理解せよ」「故障を再現せよ」「考えるのを止めて観察せよ」「分割して征服せよ」「一度に一つだけ変更せよ」「監査証跡を残せ」「プラグを確認せよ」「新鮮な視点を得よ」「修正できていなければ、修正されていない」という9項目で構成されている。著者はこれらの規則を壁に貼り付けて常に意識することを推奨しており、ユーモアを交えながらも、デバッグ作業における基本的な考え方と手順を明確に示している。これらの規則は、ソフトウェアおよびハードウェアの問題解決において、経験則として確立された実践的なアプローチを表している。

2. 重要ポイント:

  • デバッグの基本規則は9つあり、全て実践的な価値を持つ
  • 規則は暗記して常に参照できる状態にしておくべき
  • システムの理解が最初のステップとして重要
  • 問題の再現性確保が不可欠
  • 観察を重視し、思い込みを避ける
  • 問題を分割して取り組む手法の有効性
  • 変更は一つずつ行い、影響を確認する
  • 作業の記録を残すことの重要性
  • 基本的な確認(プラグ確認)を怠らない
  • 別の視点からの検討の必要性
  • 完全な修正の確認の重要性

3. 考察:
デバッグの9つの規則は、一見すると単純に見えるかもしれないが、実際のソフトウェア開発現場において深い意味を持っている。これらの規則の背後には、長年の実践から得られた知見が凝縮されている。

特に注目すべきは、これらの規則が科学的手法の基本原則と密接に関連していることだ。「システムを理解する」という最初の規則は、問題解決の基礎となる知識の重要性を説いている。現代のソフトウェアシステムは複雑化の一途を辿っており、表面的な理解だけでは効果的なデバッグは不可能である。

「故障を再現する」という規則は、科学的手法における再現性の原則そのものである。再現性のない問題は、解決が極めて困難であり、修正の確認も難しい。この規則は、問題の本質を把握するための重要なステップとなる。

「考えるのを止めて観察する」という規則は、先入観や思い込みによる判断の誤りを防ぐための指針である。現代のデバッグツールは高度化しているが、それらに頼りすぎることなく、実際の現象を注意深く観察することの重要性を説いている。

「分割して征服する」アプローチは、複雑な問題を扱う際の基本戦略として広く認知されている。これは単にコードの分割だけでなく、問題解決プロセス全体に適用される考え方である。

これらの規則は、個々の開発者のスキルレベルに関係なく、チーム全体で共有され実践されるべき重要な指針である。特に、「監査証跡を残す」という規則は、チーム開発において他のメンバーとの情報共有や、将来的な問題解決のための知見の蓄積に不可欠である。

最後の「修正できていなければ、修正されていない」という規則は、品質保証の基本原則を端的に表現している。この規則は、表面的な対処や推測による修正の危険性を警告するものである。

第3章 システムを理解する

1. 要約:
本章は「システムを理解する」という基本的かつ重要なデバッグのルールについて解説している。著者は若手エンジニアとして経験した割り込み処理の失敗から、マニュアルを読むことの重要性を学んだ体験を語る。システムのデバッグには、その設計意図や動作原理を深く理解することが不可欠である。具体的には、マニュアルを隅々まで読むこと、技術分野の基礎知識を持つこと、システム全体の構成を把握すること、そして使用するツールの特性や制限を理解することが重要である。また、記憶に頼らず、仕様や詳細な情報は必ず文書で確認することも強調している。システムを理解することは、問題を理解することとは異なるが、システムが正常に動作すべき状態を知ることで、異常を発見しやすくなる。このルールは、ソフトウェアやハードウェアに限らず、一般的な製品のトラブルシューティングにも適用できる普遍的な原則である。

2. 重要なポイント:

  • マニュアルは問題が発生する前に、cover to coverで読む必要がある
  • システムの正常な動作や設計意図を理解することが不可欠
  • 技術分野の基礎知識があってこそ、問題の本質が理解できる
  • システム全体の構成(ロードマップ)を把握する
  • デバッグツールの機能と限界を理解する
  • 詳細な仕様は必ず文書で確認し、記憶に頼らない
  • 内部仕様書や設計書も重要な情報源となる
  • 参照設計やサンプルプログラムは慎重に扱う

3. 考察:
「システムを理解する」という原則は、現代のソフトウェア開発においてさらに重要性を増している。システムの複雑化、分散化、そして急速な技術革新により、一人のエンジニアが全体を把握することが困難になってきているためである。

現代のシステム開発では、マイクロサービスアーキテクチャクラウドネイティブな環境が一般的となり、システムは多数の独立したコンポーネントの組み合わせとして実装される。各コンポーネントは異なるチームが開発し、様々な技術スタックが使用される。このような環境では、問題が発生したときに、どのコンポーネントで問題が起きているのか、コンポーネント間の相互作用がどのように影響しているのかを理解することが極めて重要である。

また、オープンソースソフトウェアやサードパーティライブラリの利用が一般的となり、自分たちで書いていないコードへの依存が増えている。この状況では、外部コンポーネントの動作原理や制約を理解することが不可欠である。ドキュメントを読むだけでなく、必要に応じてソースコードを読み、テストを書いて動作を確認することも重要である。

さらに、現代のデバッグツールは非常に高度化している。ログ集約、分散トレーシング、性能モニタリング、エラー追跡など、様々なツールが利用可能である。これらのツールを効果的に活用するためには、各ツールの特性と限界を理解し、適切な場面で適切なツールを選択する必要がある。

本章で示された原則は、40年以上前から変わらない普遍的な真理であるが、現代のシステム開発においては、より体系的かつ組織的なアプローチが必要となっている。チーム全体でシステムの理解を共有し、継続的に更新していく仕組みを作ることが、効果的なデバッグの前提条件となるだろう。

第4章 再現させる

1. 要約:
バグ修正において最も重要なのは、バグを再現可能にすることである。バグの再現には3つの目的がある。第一に、バグの挙動を観察するため。第二に、原因の特定に集中するため。第三に、修正の確認を行うため。バグの再現にあたっては、既知の初期状態から開始し、バグが発生する手順を明確に記録することが重要である。間欠的なバグの場合は、温度や振動、タイミングなど、制御不能な要因を特定し、それらの条件を制御することで再現性を高める必要がある。また、バグの再現を試みる際は、バグの発生メカニズムをシミュレートするのではなく、バグを引き起こす条件を刺激することが重要である。完全な再現が難しい場合でも、デバッグログを取得して正常時と異常時の違いを分析することで、バグの特徴を把握できる。エンジニアが「そんなことはありえない」と言っても、実際に発生している現象は否定できない事実であり、その背後にある真の原因を探る必要がある。

2. 重要なポイント:

  • バグ再現の3つの目的: 観察、原因特定、修正確認
  • 既知の初期状態からの再現手順の記録が重要
  • 間欠的なバグは制御不能な要因の特定と制御が鍵
  • バグのメカニズムのシミュレーションは避け、条件の刺激を行う
  • デバッグログによる正常/異常の比較分析
  • 「ありえない」現象でも、事実として受け入れて原因を探る
  • デバッグツールは将来の再利用に備えて保存する
  • 統計的な相関関係は誤った方向に導く可能性がある

3. 考察:
「Make It Fail」の原則は、デバッグにおける科学的アプローチの重要性を示している。科学の基本である再現性と観察を、ソフトウェアやハードウェアの問題解決に適用する考え方だ。

特に注目すべきは、バグ再現におけるシミュレーションと刺激の区別である。シミュレーションは問題のメカニズムを推測に基づいて再現しようとするアプローチで、誤った方向に導く危険性がある。一方、刺激は実際の問題が発生する条件を強調または頻発させるアプローチで、より確実な問題解決につながる。

また、間欠的なバグへの対処方法も実践的な示唆に富んでいる。完璧な再現性を求めるのではなく、デバッグログを活用して問題発生時の特徴を捉えるアプローチは、現代の複雑なシステムにおいて特に重要である。クラウドシステムやマイクロサービスアーキテクチャでは、問題の完全な再現が困難な場合が多く、ログ分析が問題解決の主要な手段となっている。

「そんなことはありえない」という反応への警鐘も重要だ。経験豊富なエンジニアほど、自身の経験や知識に基づく思い込みに陥りやすい。しかし、技術の進化とともにシステムは複雑化し、直感的には理解しがたい問題が発生することがある。このような場合、まず事実を受け入れ、先入観を排して調査を進めることが必要である。

デバッグツールの保存と再利用に関する指摘も、現代のソフトウェア開発において重要な意味を持つ。CI/CDの普及により、自動テストやデバッグツールの重要性は増している。これらのツールを再利用可能な形で維持することは、開発効率の向上に直結する。

最後に、この章で示されたアプローチは、単なるデバッグ技法ではなく、問題解決の一般的な方法論としても価値がある。仮説の検証、観察の重要性、先入観の排除といった考え方は、技術分野に限らず広く応用可能な原則である。

第5章 考えるのをやめて観察する

1. 要約:
デバッグにおいて最も重要な原則の一つは「考えるのを止めて、観察せよ」である。エンジニアは考えることが好きで、問題の原因を推測しがちだが、実際の障害を観察せずに推測だけで対処すると、誤った解決策を試みて時間を浪費する危険がある。まず必要なのは、障害が実際に発生している様子を詳細に観察することだ。そのためには、システムに計測機能を組み込むか、外部から計測機器を接続する必要がある。デバッグ用の出力やテストポイント、ログ機能などは設計段階から組み込んでおくことが望ましい。ただし、観察のための計測がシステムの動作に影響を与える可能性(ハイゼンベルク不確定性原理)にも注意が必要である。推測は観察の焦点を絞るためには有効だが、実際の障害を観察する前に解決策を実装してはならない。問題解決には、実際の障害を十分な詳細レベルで観察し、その証拠に基づいて対処することが不可欠である。

2. 重要なポイント:

  • 障害の原因を推測せず、実際の現象を観察することが重要
  • システムには予めデバッグ用の機能を組み込んでおく
  • 外部からの計測機器による観察も有効
  • 計測自体がシステムに影響を与える可能性に注意
  • 推測は観察の方向性を決めるためだけに使用する
  • 障害を十分な詳細レベルで観察する必要がある
  • デバッグ用の機能は、開発時だけでなく製品出荷後の問題解決にも有用
  • 観察に基づかない解決策の実装は時間の無駄になりやすい

3. 考察:
『考えるのを止めて、観察せよ』という原則は、ソフトウェアおよびハードウェアのデバッグにおける基本的かつ重要な指針である。

多くのエンジニアは論理的思考に長けており、問題に直面すると即座に原因を推測し始める。これは創造的な問題解決には有効な特性だが、デバッグにおいては危険な落とし穴となりうる。なぜなら、システムの不具合は予想を超えて複雑な要因から発生することが多く、推測のみに基づいた対処は誤った方向に労力を費やす結果になりやすいからだ。

効果的なデバッグのためには、まず実際の障害現象を詳細に観察することが不可欠である。そのためには、システムに適切な観察手段を確保しておく必要がある。設計段階からデバッグ用の出力機能やテストポイントを組み込んでおくことは、将来の問題解決を容易にする投資といえる。また、ロギング機能やパフォーマンスモニタなどは、製品出荷後の問題解決にも重要な役割を果たす。

一方で、観察手段の追加はシステムの動作に影響を与える可能性があることにも注意が必要だ。これは量子力学ハイゼンベルク不確定性原理になぞらえられる現象で、デバッグ用のコード追加によるタイミングの変化や、測定機器の接続による電気特性の変化などが、本来の障害現象を見えにくくしてしまう可能性がある。

このような課題に対処するためには、段階的なアプローチが有効である。まず大まかな観察から始めて問題領域を特定し、そこに焦点を当てたより詳細な観察を行う。この過程で推測を完全に排除する必要はないが、あくまで観察の方向性を定めるための補助として用い、実際の観察なしに解決策を実装することは避けるべきである。

結論として、デバッグの成功は、論理的な思考力と詳細な観察力の適切なバランスにかかっている。特に、早急な問題解決を求められる現場では推測に頼りがちだが、それは往々にして最も遠回りな道筋となる。「考えるのを止めて、観察せよ」という原則を守ることで、より効率的かつ確実なデバッグが可能となるのである。

第6章 分割統治

1. 要約:
デバッグにおいて最も重要な手法の一つが「分割統治(Divide and Conquer)」である。この手法は、問題の範囲を特定し、その範囲を繰り返し半分に分割しながら、バグの場所を特定していく方法だ。例えば1から100の数字を当てるゲームで7回以内に正解できるように、システムの問題も効率的に特定できる。重要なのは、探索範囲を明確にすることと、各チェックポイントでバグの上流か下流かを判断できることである。データの流れの中で、正常な状態から異常な状態に変化する地点を見つけることが鍵となる。また、テストパターンを注入して問題を見つけやすくすることも有効だ。複数のバグが存在する場合は、発見したバグから順次修正していく。特にノイズのような他の問題の原因となりうるバグは優先的に対処する必要がある。この手法を用いることで、バグの隠れ場所を効率的に特定することができる。

2. 重要なポイント:

  • 問題の範囲を半分ずつ絞り込む二分探索的アプローチを採用する
  • 探索範囲の上限と下限を明確に把握する
  • 各チェックポイントで、バグが上流か下流かを判断する
  • わかりやすいテストパターンを使用して異常を検出しやすくする
  • 問題の末端(異常な状態)から原因を遡って調査する
  • 発見したバグは即座に修正する
  • ノイズなど、他の問題の原因となる基本的な問題を優先的に修正する
  • 複数の入力が合流する箇所では、各入力を個別にチェックする

3. 考察:
分割統治法は、アルゴリズムの設計でも用いられる基本的な考え方だが、デバッグにおいても非常に効果的な手法である。この手法の本質は、大きな問題を小さな問題に分割し、解決可能なサイズまで問題を縮小することにある。

現代のソフトウェア開発において、この手法の重要性はさらに増している。マイクロサービスアーキテクチャクラウドネイティブアプリケーションでは、システムが複数の独立したコンポーネントで構成され、それらが複雑に相互作用する。このような環境では、問題の切り分けがより重要になっている。

効果的な分割統治を実現するために、以下の実践的なアプローチが有効である:
1. ログ収集とモニタリングの充実: システムの各部分で適切なログを収集し、異常を検出しやすい環境を整える
2. テスト環境の整備: 本番環境と同等の環境で、制御された条件下でテストできる環境を用意する
3. フェイルファスト原則の採用: 問題を早期に検出し、異常な状態の伝播を防ぐ

また、近年のDevOpsプラクティスにおいては、問題の早期発見と迅速な対応が重視される。分割統治法は、継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインにおける自動テストの設計にも応用できる。

さらに、機械学習システムのデバッグにおいても、この手法は有効である。例えば、モデルの性能問題が発生した場合、データの前処理、モデルアーキテクチャ、学習プロセスなど、段階的に問題を切り分けることで効率的なトラブルシューティングが可能になる。

ただし、分割統治法を適用する際は、システムの全体像を把握していることが前提となる。現代のシステムは複雑化しており、一人の開発者が全体を理解することが難しくなっている。そのため、チーム内でのナレッジ共有や、システムドキュメンテーションの整備も、効果的なデバッグの重要な要素となっている。

第7章 一度に1つだけ変更する

1. 要約:
デバッグにおいて最も重要な原則の一つは「一度に1つだけ変更する」ということである。複数の変更を同時に行うと、どの変更が問題を解決したのか、あるいは新たな問題を引き起こしたのかを特定できなくなる。科学的な実験と同様に、一つの変数以外をすべて制御し、その変数の影響を観察することが重要である。また、システムに問題が発生した際は、まず状況を十分に理解してから対処することが必要で、原子力潜水艦のエンジニアが警報時に制御パネル前の真鍮のバーを掴んで状況を確認するように、慌てて複数の対処を行わないことが重要である。システムが正常に動作していた最後の状態から何が変更されたのかを追跡し、問題の切り分けを行うことで、効率的なデバッグが可能となる。また、正常に動作するケースと問題が発生するケースを比較することで、問題の原因を特定しやすくなる。

2. 重要なポイント:

  • 一度に1つだけ変更し、その効果を観察する
  • 試行錯誤的な「ショットガン」アプローチを避け、的を絞った「ライフル」アプローチを取る
  • 変更が期待通りの効果を生まなかった場合は、すぐに元に戻す
  • 正常なケースと問題のあるケースを比較して違いを見つける
  • 最後に正常動作した時点からの変更点を追跡する
  • 問題が発生した際は、まず状況を理解してから対処を始める
  • デバッグログの比較では、関連しないと思われる情報も記録しておく

3. 考察:
「一度に1つだけ変更する」という原則は、単純だが実践することが難しい重要な概念である。特に時間的プレッシャーがある場合や、複雑なシステムのデバッグにおいては、複数の変更を同時に試みたくなる誘惑が強い。しかし、この原則を破ることは、問題解決を遅らせるだけでなく、新たな問題を引き起こす可能性がある。

現代のソフトウェア開発において、この原則は単にデバッグだけでなく、継続的インテグレーション(CI)やテスト駆動開発(TDD)の基礎となっている。小さな変更を頻繁に行い、各変更後にテストを実行することで、問題が発生した際の原因特定が容易になる。

また、この原則はバージョン管理システムの使用方法にも影響を与える。コミットは論理的に独立した小さな単位で行い、各コミットが一つの明確な目的を持つようにすべきである。これにより、問題が発生した際のロールバックや、特定の変更の影響の追跡が容易になる。

さらに、この原則は開発プロセス全体にも適用できる。新機能の導入やシステムの改善を行う際も、大きな変更を小さな段階に分割し、各段階で確実に検証を行うことが重要である。これはアジャイル開発の反復的なアプローチとも整合性がある。

デバッグツールの発展により、変更の影響を観察することは以前より容易になっているが、それでも「一度に1つだけ変更する」という基本原則は変わらない。むしろ、システムの複雑化に伴い、この原則の重要性は増している。デバッグ作業は科学的な方法論に基づいて行われるべきであり、この原則はその中心的な要素の一つとなっている。

第8章 監査証跡を残す

1. 要約:
デバッグにおいて監査証跡(Audit Trail)を残すことは極めて重要である。これは、問題調査時に実施した作業内容、その順序、結果を詳細に記録することを意味する。著者は、ビデオ圧縮チップのデバッグ事例を挙げ、一見無関係に思える着用していたシャツの柄が問題の原因であったことを説明している。このように、デバッグでは些細な詳細が重要な意味を持つ場合がある。記録には具体的な症状、時刻、持続時間などの詳細な情報を含める必要があり、特に複数のシステムが関連する場合は、時刻の同期やイベントの相関関係の記録が重要となる。また、ソースコード管理システムによる設計の監査証跡も、デバッグに有用である。人間の記憶は信頼できないため、重要と思われない詳細も含めて、すべてを文書化することが推奨される。電子的な記録は、バックアップ、共有、自動分析が可能という利点がある。

2. 重要ポイント:

  • デバッグ作業の全工程を詳細に記録する必要性
  • 一見無関係に見える些細な詳細も記録する重要性
  • 症状の具体的な記述(時間、持続時間、影響度など)
  • 複数システム間での時刻同期と相関関係の記録
  • ソースコード管理システムによる変更履歴の重要性
  • 人間の記憶に頼らない電子的な記録の推奨
  • デバッグログへの注釈付けの重要性
  • 問題の再現手順の詳細な記録

3. 考察:
ソフトウェア開発における監査証跡の重要性は、近年のシステムの複雑化に伴ってさらに増している。特に、マイクロサービスアーキテクチャクラウドネイティブアプリケーションの台頭により、問題の原因特定には複数のシステム間の相互作用を理解する必要がある。

現代のデバッグ環境では、著者が提唱する監査証跡の概念は、より洗練された形で実現されている。例えば、分散トレーシングシステムは、マイクロサービス間のリクエストの流れを自動的に記録し、視覚化する。また、ログ集約システムは、複数のサービスからのログを時系列で統合し、相関分析を可能にする。

しかし、これらの自動化されたツールがあっても、人間による観察と記録の重要性は変わらない。むしろ、システムが自動的に収集する定型的なデータと、人間が観察する非定型的な現象を組み合わせることで、より効果的なデバッグが可能となる。

特に注目すべきは、著者が強調する「些細な詳細」の重要性である。現代のシステムでは、環境変数、ネットワーク状態、リソース使用状況など、問題の原因となり得る要素が膨大にある。これらの要素の中から重要な情報を見極めるには、経験と直感が必要だが、それらを補完する詳細な記録が不可欠である。

また、記録を電子的に保存することの利点として、著者が言及していない現代的な価値がある。機械学習やAIを活用したログ分析が可能となり、人間が気付かないパターンや相関関係を発見できる可能性がある。さらに、チーム全体での知識共有や、類似問題の解決に役立つナレッジベースの構築にも貢献する。

結論として、監査証跡の概念は、現代のソフトウェア開発においてより重要性を増している。自動化ツールと人間の観察を組み合わせ、詳細な記録を残すことは、効率的なデバッグの基盤となる。

第9章 プラグを確認する

1. 要約:
本章では、デバッグ時における「明白な事実が最も欺瞞的である」という観点から、基本的な前提条件の確認の重要性を説いている。著者は自宅の温水システムのトラブル事例を挙げ、複雑な原因を探る前に、基本的な設定(温度設定)を確認することで問題が解決した経験を共有している。デバッグにおいて、電源が入っているか、正しいコードを実行しているか、適切な初期化が行われているかなど、基本的な前提条件を疑うことの重要性を強調している。また、開発ツールやデバッグツール自体の信頼性も疑う必要があると指摘している。特に「オーバーヘッド」や「基盤」に関わる要素は見落とされがちだが、これらの確認を怠ると思わぬトラブルに見舞われる可能性がある。

2. 重要なポイント:

  • 明白すぎる前提ほど、実は間違っている可能性がある
  • 問題解決の際は、最も基本的な条件から確認を始める
  • 以下の3つの基本原則を常に意識する必要がある:
    • 前提を疑う(電源は入っているか、正しいコードを実行しているか)
    • 最初から始める(適切な初期化がされているか)
    • ツールを疑う(開発環境やデバッグツールは正しく動作しているか)
  • 「オーバーヘッド」や「基盤」に関わる要素は特に注意が必要
  • デバッグツール自体の信頼性も確認する必要がある

3. 考察:
ソフトウェア開発において、デバッグは最も時間を要する作業の一つである。本章で示された「Check the Plug(電源確認)」という原則は、一見単純に見えるが、実際のソフトウェア開発現場で頻繁に見落とされる重要な概念である。

特に現代のソフトウェア開発環境では、システムの複雑化に伴い、問題の原因を複雑な箇所に求めがちである。例えば、マイクロサービスアーキテクチャにおけるサービス間通信の問題を、通信プロトコルやセキュリティ設定の不具合と考えて調査を進めるが、実際には単純なネットワーク接続の設定ミスであったというケースがよく見られる。

また、本章で触れられている開発ツールの信頼性の問題は、現代ではより重要性を増している。多くの開発者がGitHub Copilotなどのコード生成AIや、様々なオープンソースライブラリを利用しているが、これらのツールやライブラリが想定通りに動作するという前提自体を疑う必要がある。

さらに、クラウドネイティブな開発環境では、開発者が直接制御できない要素が増えており、「基盤」の部分での問題が発生するリスクが高まっている。例えば、クラウドプロバイダーの提供するサービスの設定や、コンテナ環境の基本設定などが、予期せぬ問題の原因となることがある。

このような状況下で、本章の教訓を現代に適用するならば、以下のような拡張が考えられる:
1. Infrastructure as Codeの設定を含めた基本設定の確認
2. 依存関係にあるサービスやライブラリのバージョン確認
3. 開発支援ツールやAIの出力結果の検証
4. クラウドサービスの基本的な設定状態の確認

最も単純な原因を最初に疑うという本章の教えは、技術が進歩した現代においても普遍的な真理として価値を持ち続けている。

第10章 新しい視点を得る

1. 要約:
デバッグの際には新鮮な視点を得ることが重要である。問題に直面した時、自分の視点だけでは思い込みやバイアスにより解決が難しくなることがある。そこで他者に相談することで、新しい洞察、専門知識、経験に基づくアドバイスを得られる可能性がある。相談する際は、自分の理論や推測ではなく、現象や症状を客観的に説明することが重要だ。助けを求められる相手は、社内の同僚、製品ベンダー、オンラインコミュニティ、技術文書など多岐にわたる。時には人形相手に問題を説明するだけでも、整理することで解決の糸口が見つかることもある。プライドを捨てて助けを求めることは、むしろ問題解決への積極的な姿勢の表れとして評価される。また、不確かな情報でも、違和感を感じた点は報告する価値がある。

2. 重要なポイント:

  • 新しい視点を得る3つの理由:新鮮な洞察、専門知識、経験
  • 問題を説明する際は理論ではなく症状を報告する
  • 相談先の選択肢:
    • 社内の同僚
    • ベンダーのサポート
    • オンラインコミュニティ
    • 技術文書やマニュアル
  • プライドを捨てて助けを求めることは良い判断
  • 不確かな情報でも報告する価値がある
  • 問題を言語化して説明すること自体が解決につながる可能性がある

3. 考察:
デバッグにおける「新鮮な視点を得る」という概念は、単なる問題解決手法以上の意味を持つ。これは現代のソフトウェア開発における重要な価値観を反映している。

まず、複雑化する技術環境において、一人の開発者が全ての領域に精通することは現実的でない。システムは複数の技術レイヤーやコンポーネントが絡み合って構成されており、問題の原因が予期せぬ箇所に潜んでいることも少なくない。本章で紹介された車のドームライトの事例は、この状況を象徴的に表している。

次に、デバッグプロセスにおける「知識の共有」の重要性を指摘できる。現代の開発現場では、個人の能力よりもチームとしての問題解決力が重視される。この文脈で、プライドを捨てて助けを求めることは、チームの生産性向上に寄与する重要な行動となる。

さらに、問題を言語化して説明することの認知的効果も注目に値する。これは「ラバーダック・デバッギング」として知られる手法にも通じる。問題を整理して説明する過程で、開発者自身の思考が整理され、解決策が見えてくることがある。

また、症状報告の重要性は、科学的方法論とも密接に関連している。観察された事実(症状)と解釈(理論)を明確に区別することは、効果的な問題解決の基本となる。これにより、他者が既存のバイアスに影響されることなく、新鮮な視点で問題に取り組むことが可能となる。

現代のソフトウェア開発では、オープンソースコミュニティやStack Overflowなどの技術フォーラムの存在により、「新鮮な視点を得る」機会は格段に増加している。しかし、これらのリソースを効果的に活用するためには、本章で示された原則—症状を客観的に報告し、謙虚に助言を求める姿勢—が依然として重要である。

第11章 修正していないものは修正されていない

1. 要約:
本章では、バグ修正における重要な原則「修正していないものは修正されていない」について解説している。著者は自身の車の故障体験を例に挙げ、単なる部品交換ではなく根本的な原因究明の重要性を説いている。問題が解決したと思われても、それが本当に修正によるものか確認する必要がある。修正を施した後は、必ず問題が本当に解決したかテストを行うべきである。また、修正を行った部分を再度元に戻して問題が再現するか、そして再度修正を適用して問題が解決するか確認することで、本当の原因を特定できたことを証明できる。問題が自然に解決することはなく、一時的に症状が見られなくなっても、根本的な原因が解決されていなければ必ず再発する。さらに、個々の問題解決だけでなく、同様の問題が再発しないよう設計プロセス自体も改善する必要がある。

2. 重要ポイント:

  • 修正後は必ずテストを実施し、問題が解決したことを確認する
  • 修正部分を元に戻して問題が再現するか確認する
  • 問題は自然に解決することはない - 見かけ上消失しても原因が残っていれば再発する
  • 部品交換だけでなく、故障の根本原因を特定し解決する
  • 設計プロセス自体の改善も重要
  • デバッグツールや監視機能を実装し、フィールドでの問題発生時のデータ収集を行う
  • 修正の有効性を証明できない場合は、問題の追跡を継続する

3. 考察:
ソフトウェア開発において、バグ修正は単なる症状の除去以上の意味を持つ重要なプロセスである。本章で示された原則は、特に大規模なシステムや重要なインフラストラクチャーの保守において極めて重要である。

まず、バグ修正の検証プロセスについて考える。現代のソフトウェア開発では、単体テスト、統合テスト、回帰テストなど、様々なレベルでのテストが実施される。これらのテストは、本章で説明された「修正の確認」を体系的に実施するための手段である。特に自動化されたテストスイートの存在は、修正による意図しない副作用の検出に大きく貢献する。

次に、根本原因分析の重要性について考える。表面的な症状に対処するだけでは、同様の問題が異なる形で再発する可能性が高い。特に分散システムやマイクロサービスアーキテクチャでは、問題の真の原因を特定することが困難な場合が多い。このような環境では、ログ収集、トレーシング、モニタリングなどの包括的な観測機能が不可欠となる。

また、プロセス改善の観点も重要である。継続的インテグレーション/継続的デリバリー(CI/CD)の実践、コードレビュー、静的解析ツールの活用など、品質を作り込むための様々な施策が現代のソフトウェア開発では標準となっている。これらは、本章で述べられている「プロセスの修正」を実現するための具体的な手段と言える。

最後に、バグ修正の文化的側面にも注目したい。「問題は自然に解決しない」という原則は、技術的な真実であると同時に、組織の姿勢を表す重要な指針でもある。問題を先送りにせず、根本的な解決を目指す文化を醸成することは、長期的な品質向上につながる。

現代のソフトウェア開発において、本章で示された原則は、より体系的かつ自動化されたアプローチによって実現されている。しかし、その本質的な重要性は変わっておらず、むしろ複雑化するシステムにおいて、より重要性を増していると言える。

第12章 すべてのルールを1つの物語で

1. 要約:
本章では、バッテリーバックアップ付きメモリの読み取り不具合に関する事例を通じて、デバッグの9つの基本ルールの実践的な適用を説明している。エンジニアAは問題をノイズによるものと誤認し、グランド線の追加などの対策を行ったが解決に至らなかった。一方、エンジニアBは体系的なアプローチを採用し、オシロスコープで信号を観察した結果、メモリコントローラからの読み取りパルスが完全に欠落していることを発見。データシートを確認し、チップメーカーに問い合わせることで、電源供給方式の設計ミスが原因であることを特定し、わずか1時間余りで問題を解決した。この事例は、システムの理解、症状の再現、実際の観察、問題の分割、変更の管理、記録の保持、基本の確認、外部の視点の活用、そして確実な検証という9つのデバッグルールの重要性を実証している。

2. 重要ポイント:

  • デバッグの基本ルール全てが実践的に活用された実例
  • システムを理解せずに対症療法的な対応を行うことの危険性
  • 実測による客観的な事実確認の重要性
  • 製造部門の記録が問題解決の重要な手がかりとなった点
  • 専門家への相談による迅速な問題解決
  • 修正後の確実な検証の必要性
  • 体系的なアプローチと記録の重要性
  • 先入観にとらわれない観察の重要性

3. 考察:
本事例は、デバッグにおける体系的アプローチの重要性を明確に示している。特に注目すべき点は、エンジニアAとBの問題解決手法の違いである。

エンジニアAは「ノイズ」という一般的な問題を想定し、その仮説に基づいて対策を実施した。このアプローチは、組み込みシステムでよく遭遇する問題への一般的な対処法ではあるが、本質的な原因究明を怠っている。一方、エンジニアBは基本に立ち返り、信号の観測から始めている。これは「Trust, but verify(信頼は大切だが、検証はもっと大切)」という工学の基本原則に忠実なアプローチである。

特筆すべきは、エンジニアBが製造部門の記録を活用した点である。品質管理における記録の重要性は広く認識されているが、この事例では troubleshooting においても記録が重要な役割を果たしている。同じメモリ内容で動作したり失敗したりする現象から、メモリデータの破損ではないという重要な示唆が得られている。

また、チップメーカーのアプリケーションエンジニアへの相談は、現代のエンジニアリングにおける重要な視点を提供している。複雑化する電子機器において、全ての動作を完全に理解することは困難である。そのため、製品に関する深い知識を持つメーカーの技術サポートを活用することは、効率的な問題解決の有効な手段となる。

この事例は、既存システムへの追加設計における注意点も示唆している。推奨回路からの変更が問題を引き起こした点は、設計変更時の慎重な検討の必要性を示している。特に電源系統は、システムの信頼性に直結する重要な要素であり、変更には十分な検証が必要である。

最後に、エンジニアBが実施した修正の確認プロセスは、「再現性のある検証」の好例である。問題の再現、修正、再度の確認という一連のプロセスは、ソフトウェア開発における回帰テストの考え方とも共通している。この体系的なアプローチは、現代のデバッグ手法の基本として広く認識されている。

第13章 読者のための簡単な練習問題

1. 要約:
この章では、デバッグの基本ルールがどのように実践で活用されるかを、3つの実例を通して解説している。1つ目は古い家の配線の問題で、掃除機のスイッチが意図せず照明を操作してしまう不具合の原因を、配線の追跡により特定した事例である。2つ目は1977年のコンピュータメモリの不具合で、特定のビットに発生するノイズ問題を、視覚化とシステマティックな原因特定により解決した事例である。3つ目はビデオ会議システムの通信における制限付き通話の不具合で、ISDNとV.35の動作の違いから、ソフトウェアのバグを発見した事例である。これらの事例を通じて、「Make It Fail(失敗を再現する)」「Quit Thinking and Look(考えるのをやめて観察する)」「Understand the System(システムを理解する)」などの基本ルールの重要性が示されている。また、問題解決には、システムの理解、観察、仮説の検証という科学的アプローチが不可欠であることを強調している。

2. 重要なポイント:

  • デバッグの基本ルールは理論だけでなく、実践的な問題解決に直接役立つ
  • 「考えるのをやめて観察する」という原則は、思い込みによる誤った判断を防ぐ
  • システムの理解なしには効果的なデバッグは不可能
  • 問題の再現性を確保することが、効率的な原因特定につながる
  • 修正後の確認と、修正を外して問題が再現することの確認が重要
  • チーム間の責任の押し付け合いは問題解決を遅らせる
  • テスト環境と実環境の違いが新たな問題を引き起こす可能性がある

3. 考察:
デバッグは単なる技術的作業ではなく、科学的な問題解決プロセスとして捉える必要がある。本章で紹介された事例は、1970年代から2000年代にかけての様々な技術的背景を持つが、そこで適用されている問題解決のアプローチは現代のソフトウェア開発にも完全に当てはまる。

特に注目すべきは、「考えるのをやめて観察する」という原則である。エンジニアは往々にして自身の知識や経験に基づいて早急に結論を出そうとする傾向があるが、これが問題解決の妨げとなることが多い。実例として挙げられたビデオ会議システムの事例では、ハードウェアチームとソフトウェアチームの双方が、十分な観察なしに相手側に問題があると決めつけてしまい、解決が遅れている。

また、テスト環境と実環境の違いが新たな問題を引き起こす可能性についても重要な示唆がある。テストスイッチが実際の通信事業者の動作を完全には模倣していなかったことで、重大なバグの発見が遅れた事例は、現代のクラウドサービスの開発でも同様の課題が存在することを示唆している。

さらに、デバッグ作業における記録の重要性も強調されている。問題の発生状況、試行した解決策、その結果などを詳細に記録することで、問題解決のプロセスが効率化されるだけでなく、類似の問題に対する知見の蓄積にもつながる。

このような体系的なアプローチは、現代のより複雑化したシステムのデバッグにおいて、さらに重要性を増している。特に、マイクロサービスアーキテクチャクラウドネイティブな環境では、問題の切り分けと再現がより困難になっており、これらの基本原則を意識的に適用することが、効果的な問題解決の鍵となっている。

第14章 ヘルプデスクからの視点

1. 要約:
この章では、ヘルプデスクの視点からのデバッグについて解説している。ヘルプデスクでは、物理的に離れた場所からユーザーの問題を解決しなければならず、直接システムを確認できない制約がある。また、ユーザーの技術レベルや言語の違いなど、様々な障壁が存在する。このような状況下でも、デバッグの9つの基本ルールを適用する必要があり、特に「システムを理解する」「失敗を再現する」「思考を止めて観察する」「問題を分割して考える」「一度に1つだけ変更する」「記録を取る」「配線を確認する」「新しい視点を得る」「完全に修正されたことを確認する」という原則を守ることが重要である。ログファイルやリモートコントロールツールなどの活用、ユーザーとの正確なコミュニケーション、トラブルシューティングガイドの利用と更新が、効果的な遠隔デバッグの鍵となる。

2. 重要なポイント:

  • 効果的な遠隔デバッグのための方法
    • システム構成図の早期入手
    • ログファイルの活用
    • リモート制御ツールの使用
    • 明確な手順の指示とユーザーからの確認

3. 考察:
ヘルプデスクにおけるデバッグは、通常のデバッグとは異なる特殊な課題を抱えている。現代のリモートワークの増加に伴い、この分野の重要性は一層高まっている。

特に注目すべき点は、ヘルプデスクでのデバッグが「通訳」の役割を果たしているということだ。ユーザーが報告する症状は、技術的な観点からは不正確または不完全なことが多い。例えば、「システムがクラッシュする」という報告は、実際には単なる応答遅延である可能性もある。熟練したヘルプデスク担当者は、ユーザーの言葉を技術的に正確な症状に「翻訳」する能力が求められる。

また、現代のシステムは複雑化しており、問題の原因が自社製品以外にある可能性も高い。クラウドサービス、ネットワーク環境、他社製品との相互作用など、様々な要因を考慮する必要がある。このため、幅広い技術知識とトラブルシューティングの経験が不可欠となっている。

さらに、ヘルプデスクは単なる技術支援以上の役割を担っている。ユーザーの不安や焦りに適切に対応し、問題解決までの過程を効果的にガイドする必要がある。この意味で、テクニカルスキルとコミュニケーションスキルの両方が求められる。

将来的には、AIやチャットボットの活用により、基本的なトラブルシューティングの自動化が進むだろう。しかし、複雑な問題や予期せぬ状況への対応には、依然として人間の判断力と創造性が必要となる。そのため、ヘルプデスク担当者には、技術の進化に合わせて継続的なスキルアップが求められる。

第15章 結論

1. 要約:
本章は、デバッグの9つの黄金律を学んだ後のフォローアップとして、エンジニア、マネージャー、教育者それぞれの立場での活用方法を解説している。エンジニアには規則を実践し、デバッグ後の振り返りを推奨。マネージャーには部下への展開方法として、ポスターの活用や読書時間の確保などの具体的アプローチを提示。教育者には実践的な教材としての活用を提案している。また、これらの規則が普遍的(Universal)、基本的(Fundamental)、不可欠(Essential)、記憶しやすい(Easy to remember)という特徴を持つ「黄金律」であることを強調している。効果的な実践により、デバッグ作業の効率化、チームのスキル向上、そして最終的には早く帰宅できることにつながると結んでいる。

2. 重要ポイント:

  • デバッグの9つの黄金律の特徴
    • 普遍性: あらゆるデバッグ状況に適用可能
    • 基本性: 具体的なツールや技術選択の基礎となる
    • 必要性: 効果的なデバッグに不可欠
    • 記憶容易性: 覚えやすく実践しやすい
  • 立場別の活用方法
    • エンジニア: 実践と振り返り、継続的な改善
    • マネージャー: チーム展開と適切な時間確保
    • 教育者: カリキュラムへの組み込みと実践的教育
  • 実践のための具体的アプローチ
    • ポスターの活用による視覚的リマインド
    • デバッグ後の振り返りによる改善
    • チーム内でのコミュニケーションツールとしての活用

3. 考察:
デバッグの9つの黄金律は、ソフトウェア開発における重要な課題の一つを体系的に解決するためのフレームワークとして高く評価できる。特に注目すべきは、これらの規則が単なる技術的なガイドラインを超えて、組織的な知識移転とスキル向上のためのコミュニケーションツールとしても機能する点である。

現代のソフトウェア開発において、システムの複雑性は急速に増大している。マイクロサービスアーキテクチャクラウドネイティブ環境、AI/ML統合など、デバッグの対象となる領域は従来よりも格段に広がっている。このような状況下で、普遍的かつ基本的な原則に立ち返ることの重要性は増している。

また、この9つの規則は、アジャイル開発やDevOpsの文脈でも重要な意味を持つ。特に「Make It Fail(再現させる)」や「Keep an Audit Trail(記録を残す)」といった原則は、継続的インテグレーション/デリバリー(CI/CD)環境における自動化テストやログ管理の基本概念と密接に関連している。

教育的観点からは、これらの規則が抽象的な概念と具体的な実践をブリッジする役割を果たす点が重要である。新人エンジニアはしばしば、理論と実践の間のギャップに苦しむが、この9つの規則は、その架け橋として機能する。

さらに、組織的な知識管理の視点からも、これらの規則は有効である。デバッグのような経験則に基づく技能は、従来、暗黙知として個人に蓄積されがちだった。しかし、この規則群は、そうした暗黙知形式知化し、組織全体で共有可能な形に昇華している。

結論として、この9つの規則は、技術的スキルの向上だけでなく、組織的な生産性向上とナレッジマネジメントにも寄与する包括的なフレームワークとして評価できる。今後のソフトウェア開発がさらに複雑化する中で、その価値はますます高まるだろう。

書評

デバッグは、ソフトウェアおよびハードウェア開発において最も重要かつ困難な作業の一つである。David J. Agansによる本書は、デバッグの普遍的な9つのルールを提示し、それらを実践的な事例とともに解説している。

これらのルールは、現代のソフトウェア開発においても極めて重要な指針となる。特に「システムを理解せよ」というルールは、マイクロサービスアーキテクチャクラウドネイティブアプリケーションが一般的となった現代において、より重要性を増している。システムの複雑性が増大する中、問題の根本原因を特定するためには、システム全体の理解が不可欠である。

「失敗を再現せよ」というルールは、現代のCI/CD(継続的インテグレーション/デリバリー)環境において、自動化されたテストケースの作成につながる重要な概念である。再現可能な失敗は、回帰テストとして実装することで、同様の問題の再発を防ぐことができる。

「考えるのをやめて観察せよ」というルールは、モダンな開発環境において提供される豊富な観察ツール - ロギング、トレーシング、メトリクス収集などと組み合わせることで、より効果的なデバッグを可能にする。特に分散システムにおいては、OpenTelemetryなどの観察可能性フレームワークが、このルールの実践を強力にサポートする。

「一つずつ変更せよ」というルールは、Gitなどのバージョン管理システムやイミュータブルインフラストラクチャの概念と親和性が高い。各変更を独立して追跡し、必要に応じて容易にロールバックできる環境を整えることで、このルールの実践が容易になる。

本書で示されるルールは、1980年代の事例を多く含むものの、その本質は現代のソフトウェア開発においても完全に有効である。むしろ、システムの複雑性が増大し、開発速度が加速する現代において、これらの基本原則に立ち返ることの重要性は増している。デバッグの本質は、論理的思考と実証的アプローチの組み合わせにあり、この原則は時代や技術の変化によって変わるものではない。

これらのルールは、現代のDevOpsプラクティスやサイトリライアビリティエンジニアリング(SRE)の概念とも整合性が高く、システムの信頼性向上に寄与する重要な指針となっている。デバッグは単なる問題解決ではなく、システムの理解を深め、より堅牢なシステムを構築するための学習プロセスとして捉えるべきである。

【dlshogi】ラージカーネル+Transformerモデルの学習

以前に検証したラージカーネルのモデルにTransformerを組み合わせたモデルの学習を行った。

実験段階では20ブロック256フィルタのモデルを使用したが、今回は11月末に行われる電竜戦向けに40ブロック512フィルタのモデルを学習した。

モデル構造

20ブロック256フィルタのモデルで事前検証を行い、精度が高くNPSが大きく下がらないモデル構造を選定した。

ラージカーネル

以前に検証した1x9, 9x1, 1x1の3つのカーネルを並列に並べたものを採用した。
これを5ブロックおきに配置した。

Transformer

標準的なマルチヘッドセルフアテンションを少し変更し、出力のプロジェクション層の前に活性化関数、後に正規化を加えた。
ResNetの構成に近くなりResNetブロックと組み合わせた場合に精度が上がることがわかった。これはLeVitのモデル構造を参考にした。

FFNは、標準的なTransformerと同様に4倍の次元とした。

ResNetの最終ブロックをTransformerブロックに置き換えた。

位置エンコーディング

相対位置エンコーディング、相対位置バイアスなど試したが、効果がなかったため、位置エンコーディングは使用しないことにした。
以前に考察した通り、dlshogiのモデルではプーリングを使用していないため、各座標の特徴量が位置情報を保持している。
そのため、明示的に位置情報をエンコードしなくても、入力に応じた相対的な位置を考慮できる。

入力層の正規化

dlshogiは、入力層に盤面の入力に3x3と1x1のカーネル、持ち駒など数値特徴量に1x1カーネルの畳み込みを使用して、それぞれの出力を加算してから正規化を行っていた。
この構成では、正規化層をレイヤー融合ができないため、加算前に正規化を行うように変更した。
これによりわずかにNPSが改善する。

学習

訓練データとして、hcpe3形式で重複局面を平均化した26億局面を使用した。

バッチサイズ4096で、12エポック学習した。

学習率スケジューラは、Cosineスケジューラだと高い学習率から始めると発散したため、StepLRSchedulerでエポックごとに1/2にした。
Transformerの学習を安定させるため、Warmupも行った。

AMPの混合精度は、float16だとTransformerの学習で損失がNaNになりやすいため、bfloat16で学習した。

学習結果

比較のために、Transformerありとなしのモデルを学習した。
また、訓練データが少しことなるが、以前に学習した40ブロック512フィルタの通常のResNetブロックとも比較する。

結果グラフのラベルの意味は以下の通り。
pre54 : 以前のResNetモデル
pre55 : ラージカーネル+Transformer
pre56 : ラージカーネルのみ

方策損失


価値損失


方策正解率


価値正解率


最終的な精度
モデル 方策損失 価値損失 方策正解率 価値正解率
pre54 1.266429 0.434222 0.569292 0.780081
pre55 1.255327 0.433520 0.571877 0.781784
pre56 1.256707 0.433032 0.571802 0.782097

今回学習したモデル(pre55, pre56)はどちらも以前のResNet(pre54)よりも、方策、価値ともに精度が上がっている。
ラージカーネル+Transformerのモデル(pre55)は、方策正解率は約0.25%、価値正解率は約0.17%向上している。
ラージカーネルのみのモデルの方が正解率はわずかに高い。

NPS

floodgateから抽出した100局面で、4回測定した際のNPSの統計量は以下の通り。
参考として、30ブロック384フィルタのモデル(pre44)も記載する。
RTX 4090で測定した。

pre54 pre55 pre56 pre44(参考)
平均 8008 8766 8682 19221
中央値 7982 8764 8667 19270
最大 9392 10140 9946 20858
最小 7596 8293 8147 17920

同じ40ブロック512フィルタのモデルで、ラージカーネル+TransformerのモデルがNPSが最も高かった。

以前に20ブロック256フィルタで実験した際は、1x9,9x9,1x1のラージカーネルは、3x3カーネルよりも少しNPSが下がったが、512フィルタのモデルでは逆にNPSが改善した。
今回は5ブロック間隔で配置したが、より積極的に使ってもよさそうである。

また、最終ブロックをTransformerに置き換えたモデル(pre55)の方が、ラージカーネルのみのモデルよりNPSが高くなった。
事前の検証ではTransformerにより少しNPSが落ちていたが、これも512フィルタのモデルでは逆にNPSが改善した。
Transformerブロックもより積極的に使ってもよさそうである。

強さ

同一持ち時間

互角局面集を使用して、持ち時間を同一とした場合の強さは以下の通り。
持ち時間は、400秒1手ごとに2秒加算とした。
H100で測定した。

基準ソフトとして、NNUE系もリーグに加えているが互角になるように持ち時間を調整しているため、以下の結果からは除外している。

# PLAYER  :  RATING  ERROR  POINTS  PLAYED   (%)  CFS(%)    W    D    L  D(%)
1 pre55   :    18.1   15.6   832.0    1632    51      50  728  208  696    13
2 pre54   :    18.0   20.3   467.5     935    50      80  414  107  414    11
3 pre56   :     3.9   25.5   264.0     553    48      76  230   68  255    12
4 pre44   :   -10.5   26.4   251.5     550    46      90  210   83  257    15

ラージカーネル+Transformerのモデルが最も勝率が高いが、有意差があるほどではない。

以前のResNetモデル(pre54)よりも明確に強くなることを期待していたが、ほとんど強くなっていない。

30ブロック384フィルタのモデルよりは有意に強くなっている。

同一ノード数

互角局面集を使用して、ノード数を固定した場合の強さは以下の通り。
ノード数は、50万,60万,70万,80万の4パターンで対局した。

# PLAYER  :  RATING  ERROR  POINTS  PLAYED   (%)  CFS(%)    W    D    L  D(%)
1 pre55   :    25.4   28.5   213.0     390    55      83  181   64  145    16
2 pre54   :     1.2   36.6   108.5     220    49      54   96   25   99    11
3 pre56   :    -2.7   51.5    55.5     113    49      55   48   15   50    13
4 pre44   :   -17.0   43.9    78.5     170    46     ---   66   25   79    15

ラージカーネル+Transformerのモデルが最も勝率が高いが、対局数が十分でなく有意差があるとは言えない。

以前のResNetモデル(pre54)よりも、ラージカーネルを加えたモデル(pre56)の勝率が低く、精度の向上が強さに反映されていない。

今回、強さの測定に互角局面集を用いたが、pre55,pre56の訓練データは、dlshogiの定跡で互角の局面を初期局面とした棋譜を増やしている。
そのため、互角局面集を用いた測定で精度が強さに反映されなかったのかもしれない。

まとめ

ラージカーネルとTransformerを使用した40ブロック512フィルタのモデルを学習した。
以前のResNetモデルと比べて、精度とNPSが向上することが確認できた。
しかし、互角局面集で強さを測定したところ有意に強くなったとは言えない。

モデル構造の検証にかなり時間をかけたが、モデル構造の工夫ではあまり強くならなかった。
ただし、精度は通常のResNetよりも早く上昇するため、収束するまで学習できない場合、同一訓練時間では強くなる可能性がある。

今後は、モデルサイズとデータを増やす方向で強くできるか検討したい。
また、終盤はNPSが高い方が有利の可能性もあるため、蒸留したモデルも学習したい。

Rectified Flowで画像生成する その4(CIFAR-10とFID)

前回まではMNISTデータセットでRectified Flowを学習したが、今回はCIFAR-10データセットで学習する。
後で、Stable Diffusion 3で使われている時刻サンプラーの効果を測りたいため、基準としてFIDを計測する。

CIFAR-10

CIFAR-10は、10クラスの32x32のカラー画像のデータセットで、50000枚の訓練データがある。10クラスのラベルの意味は以下の通りである。

  • 0: airplane(飛行機)
  • 1: automobile(自動車)
  • 2: bird(鳥)
  • 3: cat(猫)
  • 4: deer(鹿)
  • 5: dog(犬)
  • 6: frog(カエル)
  • 7: horse(馬)
  • 8: ship(船)
  • 9: truck(トラック)

実装

MNISTの学習コードを元に、データセットの変更とモデルパラメータの変更を行った。


学習結果

1000エポック学習した結果は以下の通り。
訓練データのラベルでテキスト条件付けして学習して、画像生成はラベルを指定して行っている。
各列がラベルに対応する。

10エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

MNISTは10エポック時点ではっきりした数値画像が生成できたが、CIFAR-10では10エポック時点ではぼやけた画像になる。

100エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

100エポック時点では、乗り物の画像はわかるようになるが、動物はわかりずらい。

500エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

500エポックではだいたい何の画像がわかるようになる。

1000エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

1000エポックでもあまり質は向上していない。

訓練損失

訓練損失は順調に低下している。

訓練時間

バッチサイズ1024で、1000エポック学習にRTX4090を使用して8時間33分かかった。

FID

FIDは、Fréchet Inception Distanceの略で、画像生成の品質評価によく使用される指標である。
Inceptionネットワークで特徴量ベクトルを出力して、2つの分布間のFréchet距離を計算する。
torch-fidelityを使用して、100エポックごとにFIDを計測した。
1000枚の画像を生成して、プリセットのcifar10-trainのスコアと比較した。

def evaluate_fid(images):
    os.makedirs("fid", exist_ok=True)
    images = images * 0.5 + 0.5
    images = torch.clamp(images, 0, 1)
    for i, img in enumerate(images):
        torchvision.utils.save_image(img, os.path.join("fid", f"{i}.png"))
    metrics = torch_fidelity.calculate_metrics(
        input1="fid", input2="cifar10-train", fid=True
    )
    return metrics["frechet_inception_distance"]

def fid(checkpoint):
    print(checkpoint)
    state_dict = torch.load(os.path.join("output_cifar10", checkpoint), map_location=device)
    model.load_state_dict(state_dict)
    batch_size = 1000
    for sample_N in [1, 2, 10]:
        images, nfe = euler_sampler(
            model, shape=(batch_size, 3, 32, 32), sample_N=sample_N, device=device
        )
        fid = evaluate_fid(images)
        print(f"sample_N={sample_N}, fid={fid}")
    batch_size = 1000
    images, nfe = rk45_sampler(
        model, shape=(batch_size, 3, 32, 32), device=device
    )
    fid = evaluate_fid(images)
    print(f"rk45, fid={fid}")

なお、torch-fidelityは、scipyの1.11.2以上では動かない。
torch-fidelityをmasterブランチからインストールするか、scipy==1.11.1を使用する必要がある。
AssertionError in FID calculation. · Issue #54 · toshas/torch-fidelity · GitHub

測定結果

RK45が最も高品質だが、オイラー法の10ステップでも近い値になっている。

RK45のステップ数は500エポック時点で152であった。
Rectified Flowは少ないステップで高品質な画像が生成できている。

考察

Rectified Flowの元論文では、CIFAR-10のFIDは1ステップで4.85を達成しており、今回の実験結果の画像品質はそれと比べるとかなり低い。

今回の実験では約4.9万ステップ学習しているが、元論文では、60万ステップ学習しているようなので学習ステップ数が不足していそうである。
モデルサイズも比較的小さいことも影響していそうである(元論文は入力層128フィルタ、今回の実験は入力層32フィルタ)。
あと、元論文ではテキスト条件付けを行っていない。

まとめ

CIFAR-10データセットで、スクラッチ実装したRectified Flowを学習した。
MNISTと比べると、品質の画像を生成するには学習に必要なステップ数がかなり増えることが分かった。
次回は、Stable Diffusion 3で使われている時刻サンプラーを使用して学習を試したい。

Rectified Flowで画像生成する その3(テキスト条件付け)

前回、Rectified Flowをスクラッチで実装してMNISTデータセットの学習を試した。
画像生成は条件を指定しないで生成していたため、0から9の文字がランダムに出力されていた。

今回は、0から9を表す1文字を条件として与えて、条件付けされた画像が生成できるか試す。

テキスト条件付け

Stable Diffusionなどの画像生成では、CLIPテキストエンコーダを使用して、文字列の埋め込みを取得して条件付けを行うが、今回は1文字で条件付けを行うため、埋め込みモデルの学習も同時に行う。

埋め込みの次元は、時刻tの埋め込みと同じ次元とし、時刻の埋め込みに加算することで、前回実装したUnetの時刻による条件付けの仕組みでテキストによる条件付けも行えるようにする。

class Unet(nn.Module):
    def __init__(
        ...
        if self.condition:
            self.cond_mlp = nn.Sequential(
                nn.Embedding(10, time_dim),
                nn.Linear(time_dim, time_dim),
                nn.GELU(),
                nn.Linear(time_dim, time_dim),
            )
    def forward(self, x, time, cond=None):
        ...
        t = self.time_mlp(time)
        if self.condition:
            t += self.cond_mlp(cond)

変更したU-netのコードの全体

学習

学習時に、訓練データセットの正解ラベルを条件として、入力する。

    for batch, cond in dataloader:
        ...
        score = model(perturbed_data, t * 999, cond.to(device) if condition else None)

推論

推論時は、生成した画像の数値を条件として与える。

    cond = torch.arange(10).repeat(shape[0] // 10).to(device) if condition else None
    with torch.no_grad():
        ...
        for i in range(sample_N):
            ...
            pred = model(x, t * 999, cond)

変更した学習と推論のコードの全体

結果

10エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

入力した数値条件に従った画像が生成できている。

ソルバーの比較

10エポック時点で、ソルバーにより生成画像がどう変わるか比較した。

オイラー法(1ステップ)

入力した数値条件に従った画像が生成できているが、すべて同じようにぼやけた画像が生成されている。
ベクトル場よりも、数値条件が強く効いているようである。

オイラー法(2ステップ)

ノイズの線が含まれているが、画像のバリエーションがでている。

オイラー法(10ステップ)

入力条件に従ったはっきりした画像が生成できている。
画像に多様性もある。

まとめ

クラッチ実装したRectified Flowのコードで、テキストによる条件付けをして画像生成できるか試した。
結果、入力した数値条件に従って画像が生成できることが確認できた。

Rectified Flowで画像生成する その2(スクラッチ実装でMNISTを学習)

前回、Rectified Flowの公式実装で、CIFAR10の学習を試した。

今回は、公式実装を参考に、基本的な部分のみをスクラッチで実装して、MNISTデータセットの学習を試す。

実装の全体像

実装は、以下の3つパートに分かれる。

1. Conditional U-Netの実装
2. Rectified FlowによるODEの学習
3. ODEソルバーを使用した画像生成

以下、それぞれについて解説する。

1. Conditional U-Netの実装

ベクトル場を学習するモデルとして、Conditional U-Netを実装する。
入力として、時刻tの分布(画像)と時刻tを受け取り、時刻tにおけるベクトル(変化の方向)を出力する。
モデル構成は、画像のセグメンテーションなどで使われるU-Netを使用するが、時刻tの埋め込みで条件付けされる。

公式実装は、DDPM++の実装を元にしているが、実装が複雑なため、The Annotated Diffusion Modelの実装を参考にした。
このブログの実装は、Improved Denoising Diffusion Probabilistic Modelsを参考に実装している。


2. Rectified FlowによるODEの学習

Rectified Flowによる損失の計算はシンプルで、以下のように計算する。

1. ガウス分布からランダムにサンプリングを行う(z0とする)
2. 時刻tをesp(非常に小さい値)から1の範囲でサンプリングする
3. z0と訓練データbatch(時刻1)の間を線形で結び、時刻tの分布を求める(perturbed_dataとする)
4. 時刻tの分布と時刻tを入力としてモデルで推論する(結果をscoreとする)
5. z0とbatchの差(正解のベクトル)と、モデルで推論したscoreの平均二乗誤差を損失とする

        z0 = torch.randn_like(batch)
        t = torch.rand(batch.shape[0], device=device) * (1 - eps) + eps

        t_expand = t.view(-1, 1, 1, 1).repeat(
            1, batch.shape[1], batch.shape[2], batch.shape[3]
        )
        perturbed_data = t_expand * batch + (1 - t_expand) * z0
        target = batch - z0

        score = model(perturbed_data, t * 999)

        losses = torch.square(score - target)
        losses = torch.mean(losses.reshape(losses.shape[0], -1), dim=-1)

        loss = torch.mean(losses)

3. ODEソルバーを使用した画像生成

学習済みモデルで画像を生成する際は、常微分方程式のソルバーを利用する。

オイラー

最も単純なソルバーは、オイラー法である。
つまり、時刻0から1をN等分し、初期値z0から1ステップずつ、時刻tの傾き×時間間隔を加算していく。

def euler_sampler(model, shape, sample_N):
    model.eval()
    with torch.no_grad():
        z0 = torch.randn(shape, device=device)
        x = z0.detach().clone()

        dt = 1.0 / sample_N
        for i in range(sample_N):
            num_t = i / sample_N * (1 - eps) + eps
            t = torch.ones(shape[0], device=device) * num_t
            pred = model(x, t * 999)

            x = x.detach().clone() + pred * dt

        nfe = sample_N
        return x.cpu(), nfe
RK45

常微分方程式の初期値問題の近似解を得る方法として、ルンゲ=クッタ法がよく用いられる。
RK45は、4次および5次のルンゲ=クッタ法を組み合わせた数値解法である。
精度を自動的に調整して効率的に計算を行うことができる。

ここでは、scipyのintegrate.solve_ivpを利用して実装する。

def rk45_sampler(model, shape):

    rtol = atol = 1e-05
    model.eval()
    with torch.no_grad():
        z0 = torch.randn(shape, device=device)
        x = z0.detach().clone()

        def ode_func(t, x):
            x = from_flattened_numpy(x, shape).to(device).type(torch.float32)
            vec_t = torch.ones(shape[0], device=x.device) * t
            drift = model(x, vec_t * 999)

            return to_flattened_numpy(drift)

        solution = integrate.solve_ivp(
            ode_func,
            (eps, 1),
            to_flattened_numpy(x),
            rtol=rtol,
            atol=atol,
            method="RK45",
        )
        nfe = solution.nfev
        x = torch.tensor(solution.y[:, -1]).reshape(shape).type(torch.float32)

        return x, nfe
訓練コードの全体

訓練コードの全体は以下の通り。

学習結果

実装したコードで、MNISTデータセットを学習した結果を示す。

訓練損失

10エポック学習した訓練損失は以下の通り。

Epoch 1, Loss: 0.42608851899724526
Epoch 2, Loss: 0.31962877537395906
Epoch 3, Loss: 0.3050450943172105
Epoch 4, Loss: 0.2985018942274773
Epoch 5, Loss: 0.2939571312813362
Epoch 6, Loss: 0.2883960012116158
Epoch 7, Loss: 0.2866359251076733
Epoch 8, Loss: 0.28663503043432986
Epoch 9, Loss: 0.2852076310148117
Epoch 10, Loss: 0.2822702986789919

損失は順調に低下している。

生成画像

1エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

※評価回数: 434
数値に見えるものもあるが、謎の文字が多い。

5エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

※評価回数: 332
数値に見えるものが多くなっている。

10エポック時点での生成画像は、以下の通り(ソルバーにRK45を使用)。

※評価回数: 368
謎の文字も一部含まれるが、はっきりと数値に見えるものが多くなっている。

ソルバーの比較

10エポック時点で、ソルバーにより生成画像がどう変わるか比較した。

オイラー法(1ステップ)

全体的にぼやけており、数値には見えない。

オイラー法(2ステップ)

線ははっきりしてきたが、ノイズのような線が多い。

オイラー法(10ステップ)

10ステップで生成すると数値に見えるようになった。

まとめ

Rectified Flowをスクラッチで実装して、MNISTデータセットによる画像生成を試した。
10エポックの学習ではっきりと数値に見える画像が生成できた。
ODEソルバーの比較では、RK45が精度の高い画像を生成できたがモデルの評価回数は多く時間がかかる。
10ステップのオイラー法でも比較的質の良い画像が生成できた。

Rectified Flowで画像生成する

画像生成モデルは、Stable Diffusionが出てきた頃は、Diffusionモデルが主流であったが、最近のStable Diffusion 3Flux.1では、Flow Machingのモデルが使用されている。

Flow Machingにもいくつかの訓練方法がある。
Rectified Flowは比較的シンプルでスケール可能な方法であり、画像のような高次元の分布にも適用できる。

Flow Maching

Diffusionモデルは確率微分方程式(SDE)で分布の変換を行うが、Flow Machingでは常微分方程式(ODE)で分布の変換を行う。
ODEを使うことで、確率モデルのサンプリングが不要で、高速に変換が可能になる。
Flow Machingの目的は、分布間を変換するベクトル場を学習することである。

最適輸送で、分布を変換する際のコストを最小化する手法などが提案されているが、Rectified Flowではシンプルに線形に変換を行う。
初期値(ガウスノイズなど)が与えられると、訓練済みのモデルで変換方向を表すベクトルを求めて、少ないステップで変換できる。


公式実装を動かす

動かして確認した方が理解しやすいため、公式実装を動かしてみた。

GitHub - gnobitab/RectifiedFlow: Official Implementation of Rectified Flow (ICLR2023 Spotlight)

公式実装では、DDPM++のUNetモデルを流用して、CIFAR10データセットを学習する。

環境構築

WSL2のUbuntu 22.04で構築しようとしたが、公式リポジトリが公開されたのは2022年で、使用されているCUDAのバージョンが古いため、Ubuntu 20.04でないとインストールできなかった。
そこで、WSL2のUbuntu 20.04で構築した。

Pythonの仮想環境作成
conda create -n rectflow python=3.8
conda activate rectflow
PyTorchインストール
conda install pytorch==1.11.0 torchvision==0.12.0 torchaudio==0.11.0 cudatoolkit=11.3 -c pytorch
ライブラリインストール
pip install tensorflow==2.9.0 tensorflow-probability==0.12.2 tensorflow-gan==2.0.0 tensorflow-datasets==4.6.0 numpy==1.21.6 ninja==1.11.1 matplotlib==3.7.0 ml_collections==0.1.1 jax==0.4.6 jaxlib==0.4.6 scipy==1.10.0
ビルド環境

モデルの実装で、torch.utils.cpp_extensionを使用してCUDAカーネルをビルドしている部分があるため、ビルド環境が必要になる。

sudo apt-get install build-essential ninja-build -y
CUDAインストール

ビルドにCUDAが必要になる。

wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin
sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/11.3.0/local_installers/cuda-repo-wsl-ubuntu-11-3-local_11.3.0-1_amd64.deb
sudo dpkg -i cuda-repo-wsl-ubuntu-11-3-local_11.3.0-1_amd64.deb
sudo apt-key add /var/cuda-repo-wsl-ubuntu-11-3-local/7fa2af80.pub
sudo apt-get update
sudo apt-get -y install cuda


実行時に環境変数

CUDA_HOME = /usr/local/cuda

の設定が必要になる。

訓練実行

デフォルトの設定だと、

training.n_iters = 1300001
training.snapshot_freq = 50000

となっており、訓練に時間がかかるため、configs/default_cifar10_configs.pyを編集して、

training.n_iters = 20000
training.snapshot_freq = 10000

20000ステップだけ学習するようにした。

以下のコマンドで、訓練を実行する。

python main.py --config ./configs/rectified_flow/cifar10_rf_gaussian_ddpmpp.py --eval_folder eval --mode train --workdir ./logs/1_rectified_flow

結果

logs/1_rectified_flow/samplesに、スナップショットごとの画像生成サンプルが出力される。

10000ステップ時点

20000ステップ時点

多少ぼんやりしているが、物体の画像が生成できている。

まとめ

Rectified Flowを理解するために公式実装を動かしてみた。
次は、ステップ実行しながら実装の詳細を確認したい。

【Tips】ChatGPT 4oで日本語の数式を含む文書画像をOCRしてMarkdownで出力する

ChatGPTに日本語を含むの画像を添付して、例えば「画像からテキストを抽出し、Markdown形式で出力」のような指示でテキスト化しようとすると、

It seems that the required Japanese language data for optical character recognition (OCR) is not available in my environment, which caused the extraction process to fail.

If you prefer, I can guide you through the process to extract the text on your local machine, or you can describe the content you'd like to extract, and I can help you from there. Let me know how you'd like to proceed!

のような回答が返る。

日本語のOCRに対応していないと言われて処理できない。

以下では、このような場合の対処方法を紹介する。

対処方法1

OCRツールを使用しないでください」という指示を追加する。

例:
「添付した画像の内容を一字一句忠実にMarkdown形式で出力。章節、段落は適切に解釈すること。OCRツールは使わなくてよい。」


なお、ChatGPTが適当に補完しようとするので、OCRの目的であれば「一字一句忠実に」のような指示を入れるとよい。

1つ目の画像で指示を行い、2つ目以降は画像を貼り付けて送信するだけでよい。

対処方法2

GPTsを作ると、より確実に指示に従う。
OCR用途では、機能のチェックはオフにしておく。

指示の例:

添付した画像の内容を一字一句忠実にMarkdown形式で出力してください。章節、段落、図表、数式は適切に解釈してください。段落の途中では改行しないでください。数式はLaTeX記法を使用してください。インライン数式は$で囲い、ブロック数式は$$で囲ってください。複数行の数式にはalign環境を使用してください。文章の途中から始まったり、途中で終わったりする場合があります。画像にない余分な文字列が出力されていないことを確認してください。

使うときは、作成したGPTsを選択して、画像を貼り付けて送信するだけでよい。

1つのチャットで、連続して貼り付けて送信できる。

画像は一度に複数枚貼り付けても良いが、一度に処理できない場合があるため、2枚ずつくらいで処理するのがよい。

たまに、文頭文末に漏れが発生したり、適当に補完される場合があるため、目視チェックは必要である。

ChatGPTのProに加入していても、時間当たりの上限があるため、100ファイルくらい間隔で数時間のクールタイムが必要である。