TadaoYamaokaの開発日記

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

【読書ノート】最適輸送の理論とアルゴリズム

Stable Diffusion 3は、Flow Matchingが使用されており、Flow Matchingは最適輸送とも関連するということなので、積んでおいた「最適輸送の理論とアルゴリズム」を読んだ。

数式をほとんど読み飛ばして読んだまとめである。
以下の内容は、ほとんどClaude3 Opusを使用して作成している。

概要

第1章 確率分布を比較するツールとしての最適輸送

最適輸送は確率分布を比較するためのツールであり、KLダイバージェンスと比較して、距離構造を捉えられる、距離の公理を満たす、サポートが一致していなくても定義できる、分布の対応関係を得られる、などの利点がある。本書では、ヒストグラムの比較、点群の比較、連続分布の比較の3つの問題設定を扱う。

第1章のポイント
  • 最適輸送は分布の「山」を最適に動かすコストとして解釈できる
  • KLダイバージェンスは距離構造を無視し、サポートが重ならない分布間は無限大になる
  • ワッサースタイン距離は最適輸送コストの特殊例で距離の公理を満たす
  • 最適輸送行列から分布間の対応関係が得られ、応用に役立つ
第1章の質問

1. KLダイバージェンスと最適輸送の主な違いは何ですか?
2. ワッサースタイン距離とはどのようなものですか?
3. 最適輸送行列からどのようなことがわかりますか?

第2章 最適化問題としての定式化

最適輸送問題は線形計画問題として定式化でき、ヒストグラムの比較、点群の比較、連続分布の比較に適用できる。双対問題を導出することで、元問題の解釈や感度分析ができ、最適輸送問題と最小費用流問題の関係も明らかになる。最適輸送問題の最適解は疎になる性質がある。

第2章のポイント
  • 最適輸送問題は線形計画問題として定式化できる
  • 双対問題から元問題の解釈や感度分析ができる
  • 最適輸送問題と最小費用流問題は互いに帰着可能
  • 最適解は疎になり、一様分布の場合は一対一対応となる
第2章の質問

1. 最適輸送問題はどのように定式化されますか?
2. 双対問題を導出するとどのようなメリットがありますか?
3. 最適輸送問題の最適解にはどのような性質がありますか?

第3章 エントロピー正則化とシンクホーンアルゴリズム

最適輸送問題にエントロピー正則化を加えると、シンクホーンアルゴリズムという高速な反復解法が利用できるようになる。シンクホーンアルゴリズムは行列スケーリング問題とも関係が深い。正則化を加えると、パラメータに関して解が微分可能になるため、勾配法による最適化が可能になる。正則化の強さは、ソフトな最適解と高速な計算のトレードオフとなる。

第3章のポイント
第3章の質問

1. エントロピー正則化を加えるとどのようなメリットがありますか?
2. シンクホーンアルゴリズムと行列スケーリング問題の関係は何ですか?
3. 正則化により最適輸送がどのように使いやすくなりますか?

第4章 敵対的ネットワーク

敵対的ネットワークは、二つの分布を区別する分類器を学習させることで、分布間の距離を推定する方法である。これには重みの切り捨てや勾配ペナルティなどの正則化が必要となる。生成モデルの学習に適用したものがワッサースタインGANであり、オートエンコーダやドメイン適応にも応用できる。

第4章のポイント
  • 分類器を使って分布間の距離を推定する
  • 重みの切り捨てや勾配ペナルティで正則化する
  • ワッサースタインGANは生成モデルの学習に用いる
  • オートエンコーダやドメイン適応などにも応用可能
第4章の質問

1. 敵対的ネットワークでは分布間の距離をどのように推定しますか?
2. ワッサースタインGANとはどのようなものですか?
3. 敵対的ネットワークにはどのような応用がありますか?

第5章 スライス法

スライス法は、高次元空間の点群の比較を一次元に射影した上で最適輸送を計算することで高速化する手法である。一般化スライス法では射影関数を柔軟に選べるようになっている。最大化スライス法は最も差が出るような射影を選ぶ。木構造に射影するとより豊かな構造を保ちつつ高速に計算できる。

第5章のポイント
  • 高次元の点群を一次元に射影して効率的に最適輸送を計算する
  • 一般化スライス法では多様な射影関数を選べる
  • 最大化スライス法は最も差が出る射影を用いる
  • 木構造に射影すると豊かな構造を保ちつつ高速に計算できる
第5章の質問

1. スライス法ではどのように計算を高速化しますか?
2. 一般化スライス法と最大化スライス法の違いは何ですか?
3. 木構造に射影するとどのようなメリットがありますか?

第6章 他のダイバージェンスとの比較

確率分布間の距離尺度にはφ-ダイバージェンス積分確率距離があり、最適輸送はこれらの一般化となっている。距離尺度の選択は、サンプルからの推定における収束レートに影響する。最適輸送は分布の弱収束と密接に関わっており、KLダイバージェンスよりも適切な距離となる。

第6章のポイント
  • φ-ダイバージェンス積分確率距離は分布間距離の一般的な枠組み
  • 最適輸送はこれらの距離の一般化になっている
  • サンプルからの推定における収束レートは距離尺度の選択に依存する
  • 最適輸送は分布の弱収束と密接に関連する適切な距離である
第6章の質問

1. φ-ダイバージェンス積分確率距離はどのような枠組みですか?
2. 距離尺度の選択はサンプルからの推定にどのように影響しますか?
3. 最適輸送が分布の弱収束と関連するとはどういうことですか?

第7章 不均衡最適輸送

不均衡最適輸送問題は、比較する分布の質量が異なる場合や、すべての質量を移動させる必要がない場合に用いられる。質量の生成・消滅にペナルティを課すことで定式化される。ペナルティにはL1ノルムやKLダイバージェンスが用いられ、正則化を加えた一般化シンクホーンアルゴリズムで解くことができる。

第7章のポイント
  • 質量が異なる分布の比較に不均衡最適輸送が用いられる
  • 質量の生成・消滅にペナルティを課して定式化する
  • L1ノルムやKLダイバージェンスがペナルティとして使われる
  • 正則化した一般化シンクホーンアルゴリズムで効率的に解ける
第7章の質問

1. 不均衡最適輸送問題とはどのような問題ですか?
2. 不均衡最適輸送ではどのようにペナルティを課しますか?
3. 一般化シンクホーンアルゴリズムとはどのようなものですか?

第8章 ワッサースタイン重心

ワッサースタイン重心は、複数の確率分布の重み付き平均を最適輸送の観点から定義したものである。重心を求める最適化問題には、サポートを固定する定式化と、サポートも最適化に含める定式化がある。前者は線形計画問題や劣勾配法で解け、後者は交互最適化で解ける。正則化を加えるとより効率的に解ける。

第8章のポイント
  • ワッサースタイン重心は分布の重み付き平均を最適輸送の観点で定義したもの
  • サポートを固定する定式化とサポートも最適化する定式化がある
  • 固定サポートは線形計画問題や劣勾配法で解ける
  • 自由サポートは交互最適化で解ける
  • 正則化を加えるとより効率的に解くことができる
第8章の質問

1. ワッサースタイン重心とはどのようなものですか?
2. 固定サポートの定式化ではどのように最適化問題を解きますか?
3. 正則化を加えるとワッサースタイン重心の計算がどのように変わりますか?

第9章 グロモフ・ワッサースタイン距離

グロモフ・ワッサースタイン距離は、異なる空間に埋め込まれた分布間の距離を定義したものである。距離空間間の距離の差が最小となるような分布間の対応を見つける問題として定式化される。距離の公理を満たすが、最適化問題は非凸となる。エントロピー正則化と近接勾配法を用いた解法がある。

第9章のポイント
第9章の質問

1. グロモフ・ワッサースタイン距離はどのような距離ですか?
2. グロモフ・ワッサースタイン距離の最適化問題にはどのような特徴がありますか?
3. グロモフ・ワッサースタイン距離はどのように計算しますか?

重要な概念の解説

  • 確率分布: 確率変数がとりうる値とその確率を定めたもの。本書では有限離散分布、点群、確率密度関数を持つ分布を扱う。
  • 最適輸送問題: 二つの確率分布の間の輸送コストが最小となる輸送方法を求める問題。輸送コストは輸送前後の点の間のコストの総和で定義される。
  • ワッサースタイン距離: 最適輸送コストの特殊ケースで、コスト関数が距離関数の場合に距離の公理を満たす。数学的な扱いやすさと幾何学的な解釈を併せ持つ。
  • 双対問題: 主問題をラグランジュの未定乗数法により変形した、主問題と最適値が等しくなる別の最適化問題。主問題の構造を知る手がかりとなる。
  • エントロピー正則化: 最適化問題の目的関数に解のエントロピーに関する項を加えること。最適輸送に加えると凸最適化問題になり、シンクホーンアルゴリズムにより高速に解くことができる。
  • シンクホーンアルゴリズム: エントロピー正則化した最適輸送問題を交互最適化により高速に解くアルゴリズム。行列のスケーリングを繰り返すことで最適解に近づく。

要点のまとめ

最適輸送は確率分布間の距離を定義するための数理的なツールであり、機械学習などにおける分布の比較に広く用いられている。特にワッサースタイン距離は数学的な扱いやすさと幾何学的な解釈を兼ね備えており、分布間距離の標準的な選択肢となっている。最適輸送問題は線形計画問題として定式化でき、双対問題を解くことで元問題の知見が得られる。計算効率の面では、エントロピー正則化を施すことで凸最適化問題となり、シンクホーンアルゴリズムにより高速に求解できる。機械学習への応用では敵対的ネットワークの枠組みが重要であり、生成モデルなどに用いられている。最適輸送の考え方は、スライス法、不均衡輸送、ワッサースタイン重心、グロモフ・ワッサースタイン距離など、さまざまな問題設定に拡張されている。本書ではこうした最適輸送の理論と計算手法を包括的に解説しており、最適輸送を実際の問題に活用するための技術の基盤を提供している。

書評

本書「最適輸送の理論とアルゴリズム」は、機械学習分野で急速に注目を集めている最適輸送の理論体系を、数理的な基礎から計算アルゴリズム、実際の応用に至るまで包括的に解説した良書である。

従来の確率分布間距離と比較した最適輸送の利点が、数学的および直観的な議論により明快に示されている。理論面では線形計画問題としての定式化と双対定理により、計算アルゴリズムの導出と最適解の構造に関する洞察が与えられている。実務的には、エントロピー正則化による計算の効率化、シンクホーンアルゴリズムによる高速解法など、大規模な問題を扱うための技術が丁寧に説明されている。

特筆すべきは、理論と実践のバランスの取れた構成である。数学的な定理の導出では、証明の核心をなす数式を丁寧に記述しつつ、直観的な解釈を随所で与えることで、読者の理解を助けている。さらに、数式の意味を図解などで補足することで、数式が苦手な読者でも内容を追いやすいよう配慮されている。実装面でも、現代の深層学習の枠組みとの親和性の高さを示し、技術の実用性を印象付けている。

機械学習分野に新たな地平を拓きつつある最適輸送理論を、理論・アルゴリズム・応用の観点から縦横に論じた好著である。最適輸送を実務に活用したいと考える機械学習実践者のみならず、分野の数理的基礎を理解したい数学者、新しい計算技術に触れたい計算機科学者など、幅広い読者に推薦したい一冊である。

【読書ノート】エリック・エヴァンスのドメイン駆動設計

最近、ソフトウェア開発を行っているので、ソフトウェア設計に関する勉強をしている。
エリック・エヴァンスのドメイン駆動設計」を読んだので、内容をまとめた。
以下の内容は、ほとんどClaude3 Opusで書いている。

概要

第1部 ドメインモデルを機能させる

本書の第1部は、ドメイン駆動設計の中核をなす考え方を説明している。複雑なソフトウェアに立ち向かうには、問題の本質を捉えた優れたドメインモデルが不可欠である。そのモデルを作り出すには、ドメインの知識を注意深く分析し、実装に適した形に洗練する「知識のかみ砕き」のプロセスが必要だ。

モデルはチーム全体で共有され、「ユビキタス言語」を通してソフトウェアに反映されなければならない。開発者は言葉の使い方に細心の注意を払い、コードがモデルを如実に表すようにする。モデルとコードの間に乖離があってはならない。

したがって、伝統的な「分析」と「設計」の分離をやめ、モデリングとコーディングを一体化した「モデル駆動設計」のアプローチを取るべきだ。そのためには、開発チームの全員が「実践的モデラ」となり、ドメインの理解とモデリングのスキルを持たねばならない。

ドメイン駆動設計は、難しいが大きな価値を生む。本書はそのためのノウハウを提供している。

第2部 モデル駆動設計の構成要素

本書の第2部では、ドメイン駆動設計を支える構成要素が詳述されている。

まず、ドメインを他の関心事から隔離することの重要性が説かれ、レイヤ化アーキテクチャによるドメイン層の分離が推奨される。ドメインの中核を成すのはエンティティと値オブジェクトであり、両者の違いを理解して適切に使い分けることが肝要だ。エンティティは同一性によって定義され、状態は変化するが同一性は不変。値オブジェクトは属性値によって定義され、不変で交換可能。

ドメインの操作は、エンティティや値オブジェクトに自然に属さない場合、サービスによってモデル化される。サービスは状態を持たない。

関連するオブジェクトの整合性を保つ必要がある場合、集約を定義する。集約はルートエンティティを中心に構成され、外部からはルートのみ参照可能とすることで不整合を防ぐ。
オブジェクトのライフサイクルを通じて、生成の複雑さを隠蔽するファクトリと、永続化を隠蔽するリポジトリが有用である。

関係データベースの制約にも配慮しつつ、パターンの適用とモデルの継続的な洗練によって、ドメインを忠実に表現する設計を追求することが重要である。

第3部 より深い洞察へ向かうリファクタリング

本書の第3部では、ドメインモデリングと設計をより深いレベルへと洗練させていくための考え方とテクニックが示されている。

鍵となるのは、ドメインへの継続的な学習と、モデルの表現力を高めるための地道なリファクタリングの積み重ねだ。その過程で、ときに飛躍的な前進(ブレイクスルー)が訪れる。こうした機会を活かすには、一時的な混乱を恐れず、チームが集中的に取り組む必要がある。

より良いモデルのためのヒントは、ドメインエキスパートとの対話や、既存のコードのぎこちなさ、ドメインの文献などから得られる。見出した概念は明示的にモデル化し、試行錯誤で洗練させていく。既存のアナリシスパターンデザインパターンも、ドメインの文脈に合わせて応用できる。

こうして作り上げるモデルは、単に精緻なだけでなく、開発者にとって理解と拡張が容易な「しなやかさ」を備えていなければならない。意図が明快で、副作用がなく、概念の本質的な関連に基づく設計を目指す。

より深いモデルと、しなやかな設計は、ソフトウェアに対するチームの継続的な取り組みを通じて、少しずつ、ときに劇的に発展していく。それは直線的でも予測可能でもないプロセスだが、DDD実践者はその価値を信じ、チャンスを逃さずに前進し続ける。

第4部 戦略的設計

本書の第4部では、大規模で複雑なシステムを開発するためのドメイン駆動設計の戦略的な側面が論じられている。複数のチームによって開発が進められると、モデルの断片化が問題となる。そこで、「境界づけられたコンテキスト」の概念を用いて、モデルの適用範囲を明示的に定義し、「コンテキストマップ」によってシステム全体のコンテキストを整理する。コンテキスト間の関係は、共有カーネル、顧客/供給者、順応者など、状況に応じたパターンで統合される。

一方、「戦略的設計」では、モデルの本質的な部分である「コアドメイン」に注力し、汎用的な概念を括り出して純粋なコアを「隔離」「蒸留」する。開発者は、「凝集したメカニズム」を見出し、複雑な処理をまとめて隠蔽することで、ドメインモデルの本質を際立たせる。

大規模なシステムでは、「大規模な構造」によって、モデル全体に一貫性のある枠組みを与える。比喩的な「システムのメタファ」、階層的な「責務のレイヤ」、ランタイムの柔軟性を与える「知識レベル」、疎結合な「プラグイン可能なコンポーネントフレームワーク」など、様々なパターンが存在する。ただし、これらの構造は絶対的なものではなく、プロジェクトと共に「進化」させるべきだとされる。

複雑なシステムの核心を見極め、洗練していく過程では、開発チーム全体で「ユビキタス言語」を醸成し、境界づけられたコンテキストごとの「方言」を尊重しつつ、モデルについてのコミュニケーションを深めることが求められる。



各章の主要な概念

第1部 ドメインモデルを機能させる

第1章 知識をかみ砕く

開発チームがソフトウェアを役立つものにするには、対象ドメインの知識を身につける必要がある。しかし、情報は断片的で大量にあり、どれが必要かわからない。モデルはこの知識を選び抜いて意図的に構成したものであり、チームがモデルを作る過程で知識をかみ砕いていく。モデルが改良されるたびに、チームの理解が深まり、さらにモデルが洗練される。こうしたモデルは実用的で厳密でなくてはならない。

第2章 コミュニケーションと言語の使い方

モデルに基づいた言語(ユビキタス言語)を用いることで、チーム内のコミュニケーションが明確になり、実装とつながる。チームは言語を実験的に使用し、モデルを改良する。図やドキュメントも言語によって補完される。チーム全体がこの言語を使うことで相互理解が深まり、言語自体も進化する。

第3章 モデルと実装を結びつける

分析モデルと設計を分けるのではなく、単一のモデルを探し、モデルとコードを緊密に結びつける(モデル駆動設計)。モデルを忠実に実装し、フィードバックによってモデルを改良する。ユビキタス言語によってチーム全体の理解がモデルに反映される。モデリングとコーディングは分離せず、開発者全員がドメインを理解し、モデルに責任を持つ。

第2部 モデル駆動設計の構成要素

第4章 ドメインを隔離する

ドメインの概念を他の関心事から切り離すことで、モデルに集中できる。レイヤ化アーキテクチャでは、ドメイン層を分離。利口なUI アンチパターンは、ドメイン駆動設計と相容れない。レイヤ間の関係づけにはパターンがある。ドメインサービスは、ドメインの操作をモデル化。ドメイン層内でも差別化が必要。

第5章 ソフトウェアで表現されたモデル

関連の設計で無駄をなくす。エンティティは同一性で定義され、値オブジェクトは属性の値で定義される。サービスはドメインの操作を表現。モジュールはモデルを意味のある塊に分割。関係DBの制約に適応しつつ、モデルとの整合性を保つ。

第6章 ドメインオブジェクトのライフサイクル

集約は整合性の境界を定義。ファクトリは生成の複雑さをカプセル化リポジトリは永続化を隠蔽。設計の妥協点を見極めることが重要。

第7章 言語を使用する-応用例

貨物輸送ドメインを段階的にモデリング。レイヤ化、エンティティと値の区別、集約、リポジトリの選択、ファクトリの設計など、ドメイン駆動設計の構成要素を適用。新規機能の追加に伴い、モデルを洗練。

第3部 より深い洞察へ向かうリファクタリング

第8章 ブレイクスルー

開発が進むと、突如として深い洞察が得られ、モデルと設計が大きく前進することがある。これをブレイクスルーと呼ぶ。ブレイクスルーは予測不可能だが、連続的なリファクタリングから生まれる。リスクもあるが、機会を逃してはならない。

第9章 暗黙的な概念を明示的にする

優れたモデルには、ドメインの中心となる概念が明示的に含まれる。開発者は、会話に出てくる手がかりや、設計のぎこちなさ、矛盾などから、そうした概念を見出さなければならない。見出した概念は試行錯誤でモデル化する。

第10章 しなやかな設計

意図が明確で、副作用のない関数や表明によって部品の組み合わせが安全になり、概念の輪郭に従った設計により意味の単位が安定する。こうしたしなやかな設計は、開発者が理解と変更を繰り返しながら、複雑さに立ち向かえるようにする。

第11章 アナリシスパターンを適用する

アナリシスパターンは、ドメインに関する他者の経験を利用できる。だが既製の解決策ではなく、洞察とモデリングの糧となる。プロジェクトの状況に合わせて選択し、現場の知識とかみ合わせる必要がある。

第12章 デザインパターンをモデルに関係づける

オブジェクト指向デザインパターンの一部は、ドメインの概念とも合致する。ドメインとの関連を意識しながらデザインパターンを用いると、モデルがより明確になり、実装面の利点も得られる。

第13章 より深い洞察へ向かうリファクタリング

より深い洞察へ向かうには、ドメインに馴染み、物事に新しい見方をし、ドメインエキスパートとの対話を続けることだ。チームで集中的に取り組み、外部の知識も活用する。危機に見える変化もブレイクスルーの好機となり得る。

第4部 戦略的設計

第14章 モデルの整合性を維持する

複数のチームが関わる大規模プロジェクトでは、モデルの断片化が問題になる。これを防ぐため、モデルが適用される範囲を明示的に定義した「境界づけられたコンテキスト」の概念を導入する。さらに、コンテキストマップを描いてコンテキスト間の関係を整理し、関係性に応じた様々な統合方法を選択する。統合パターンには、共有カーネル、顧客/供給者開発チーム、順応者、腐敗防止層などがある。

第15章 蒸留

巨大で複雑なモデルの本質的な部分に注目し、モデルを煮詰めるプロセスを「戦略的設計」と呼ぶ。戦略的設計では、最も価値のあるコアドメイン、汎用的な概念を扱う汎用サブドメイン、複雑な処理を隠蔽する凝集したメカニズムを見極める。さらに、補助的な要素を切り離して純粋なコアドメインを「隔離」し、深く洞察に満ちたモデルへと「蒸留」する。

第16章 大規模な構造

巨大なモデルを統一的に理解するため、システム全体に対する設計の方針となる「大規模な構造」を導入する。具体的には、ユビキタス言語の比喩表現としての「システムのメタファ」、モデルを階層化する「責務のレイヤ」、状況に応じて変化するモデルを実現する「知識レベル」、疎結合コンポーネント構成の「プラグイン可能なコンポーネントフレームワーク」などのパターンがある。構造はプロジェクトと共に「進化」すべきである。



印象的なフレーズ

第1部 ドメインモデルを機能させる

  • ソフトウェアの核心は、ドメインに関係した問題をユーザのために解決する能力である。
  • 知識をかみ砕く
  • ユビキタス言語
  • モデル駆動設計
  • 実践的モデラ

第2部 モデル駆動設計の構成要素

  • 「利口なUI アンチパターン
  • 「オブジェクトには概念的な同一性がない。そういうオブジェクトは、物事の特徴を記述する。」
  • 「集約をモデリングし、ファクトリとリポジトリを設計へ追加することで、モデルオブジェクトの操作を体系的に、それもライフサイクルを通じて意味のある単位で行えるようになる。」
  • 「時には、単純に『物』とはできないこともある。」
  • 「戦略的設計」
  • 「パターンを使用する際は、フレームワークと対立してはならない。」

第3部 より深い洞察へ向かうリファクタリング

  • 洞察のブレイクスルーをもたらす可能性が作り出される
  • 暗黙的だった概念を明示的にするこうした変化が、時には、深いモデルへとつながるブレイクスルーになることもある
  • 手に負えなくなった評価メソッドは、うまく拡張して独立オブジェクトにする
  • 概念を掘り出す
  • しなやかな設計は、深いモデリングを補完するものだ
  • 設計は、それを変更しようとする開発者にとっても役に立たなければならない
  • アナリシスパターンは活用すべき知識である
  • デザインパターンであると考えられるパターンが、ドメインモデルに対してどう適用できるか
  • より深い洞察へ向かうリファクタリングは、多くの側面を持つプロセスである

第4部 戦略的設計

  • 「木を見て森を見ず」
  • 「信頼するが、検証もする」
  • 剽窃せよ!誰の研究も見逃してはならない。ただ、いつも調査と呼ぶことを忘れずに」
  • 「Less is more」
  • 「オブジェクトはスペシャリストだが、開発者はジェネラリストである」



重要なポイント

第1部 ドメインモデルを機能させる

  • 複雑さに対処するには、ドメインモデルが鍵となる。
  • モデルを作成することはドメインの知識をかみ砕くプロセスである。
  • チーム全体がモデルに基づいた言語を使うことで、コミュニケーションと実装の質が向上する。
  • モデルとコードは密接に結びつけるべきで、分析と設計を分離すべきではない。
  • 開発者全員がドメインモデリングを理解する必要がある。

第2部 モデル駆動設計の構成要素

  • ドメインを他の関心事から隔離することの重要性
  • エンティティと値オブジェクトの違いと使い分け
  • サービスによるドメインの操作のモデル化
  • 集約による整合性の境界の定義
  • ファクトリとリポジトリによるライフサイクル管理の隠蔽
  • 関係データベースとの整合性を保ちつつモデルを表現する工夫
  • パターンの適用とフレームワークとの整合性
  • モデルの継続的な洗練

第3部 より深い洞察へ向かうリファクタリング

  • ブレイクスルーはリスクもあるが、モデルと設計を大きく前進させる機会である
  • ドメインの中心的な概念を見出し、モデルに明示的に表現することが重要である
  • 意図が明確で副作用のない設計により、開発者は複雑さに立ち向かえる
  • アナリシスパターンは、そのまま使える解決策ではなく、モデリングの手がかりを与えてくれる
  • オブジェクト指向デザインパターンドメインの概念に合わせて使うと、モデルが明確になる
  • より深い洞察のためには、ドメインへの理解を深め、新しい見方を取り入れ、対話を重ねる
  • 危機に見える変化もブレイクスルーのチャンスかもしれない

第4部 戦略的設計

  • 複数のモデルを整理するためのコンテキストマップとコンテキスト間の関係パターン
  • 大規模モデルの本質を見極める戦略的設計と、隔離・蒸留によるドメインモデルの深化
  • チーム全体で共有可能な大規模な構造パターンと、状況に応じた「進化」の重要性
  • モデルの断片化を防ぐ継続的な統合と、境界づけられたコンテキスト間の慎重な統合
  • 戦略的設計とユビキタス言語の相互作用による、深い洞察に基づくモデルの構築



理解度チェック用の質問文

第1部 ドメインモデルを機能させる

第1章
  • 知識をかみ砕くとはどういうことか?
  • モデルの目的は何か?
  • チームの理解とモデルの関係はどのようなものか?
第2章
  • ユビキタス言語とは何か?
  • チームはどのようにしてモデルを改良するか?
  • 言語はどのようにしてチームのコミュニケーションを明確にするか?
第3章
  • モデル駆動設計とは何か?
  • 分析モデルと設計を分けることの問題点は何か?
  • 開発者がモデリングから離れてはいけない理由は何か?

第2部 モデル駆動設計の構成要素

第4章
第5章
  • エンティティと値オブジェクトはどのように区別されるか?
  • サービスはドメインモデルにおいてどのような位置づけか?
  • モジュール分割の目的は何か?
第6章
  • 集約はどのような問題を解決するか?
  • ファクトリの役割は何か?
  • リポジトリはオブジェクトのライフサイクルのどの段階を扱うか?
第7章
  • 貨物輸送ドメインにおける集約のルートにふさわしいエンティティは何か?
  • 「荷役イベントのコレクション」から「荷役イベントリポジトリ」への変更の理由は何か?
  • 腐敗防止層はどのような役割を果たしたか?

第3部 より深い洞察へ向かうリファクタリング

第8章
  • ブレイクスルーとは何か?どのようにして起こるか?
  • ブレイクスルーにはどのようなリスクがあるか?
  • ブレイクスルーが起きた時にチームはどう行動すべきか?
第9章
  • ドメインの中心的な概念を見出すために、開発者はどこに着目すべきか?
  • 見出した概念をどのようにモデル化すればよいか?
  • 仕様オブジェクトとは何か?どのような利点があるか?
第10章
  • しなやかな設計とはどのようなものか?なぜ必要か?
  • 意図が明確な設計とは?どう実現できるか?
  • 概念の輪郭とは何か?設計はそれにどう従うべきか?
第11章
第12章
第13章
  • より深い洞察へ向かうリファクタリングで重要な3つのポイントは?
  • より深い洞察へのブレイクスルーはどのようなきっかけで起こるか?
  • ブレイクスルーの機会をどう捉えるべきか?

第4部 戦略的設計

第14章
  • 境界づけられたコンテキストとは何か?なぜそれが重要なのか?
  • コンテキストマップにはどのような要素が含まれるか?
  • 境界づけられたコンテキストを統合する代表的なパターンにはどのようなものがあるか?
第15章
  • 戦略的設計において、なぜコアドメインを見極めることが重要なのか?
  • 汎用サブドメインとはどのようなものか?それをコアドメインから分離する利点は何か?
  • 大規模なモデルを理解しやすくするための「蒸留」とはどのような行為を指すか?
第16章
  • 大規模な構造を導入する目的は何か?代表的なパターンにはどのようなものがあるか?
  • 知識レベルとはどのような概念か?それによってどのような利点が得られるか?
  • 「進化する秩序」とはどういう意味か?なぜ大規模な構造は「進化」すべきなのか?



重要な概念の解説

第1部 ドメインモデルを機能させる

  • ドメインモデル:問題領域(ドメイン)の本質的な概念やルールを表現した抽象化。ソフトウェアの核となる。
  • 知識のかみ砕き:大量の情報から本質を抽出し、実用的なモデルに落とし込む継続的なプロセス。
  • ユビキタス言語:モデルに基づいた、チーム全体で共有される明確な言語。コミュニケーションと実装を強力に結びつける。
  • モデル駆動設計:分析と設計を分けずに、単一の洗練されたモデルをコードに直接的に反映させる設計手法。
  • 実践的モデラ:ドメインとコードの両方に精通し、モデリングとコーディングを一体的に行う開発者。

第2部 モデル駆動設計の構成要素

  • レイヤ化アーキテクチャ:アプリケーションを複数の層に分割する設計手法。ユーザインタフェース層、アプリケーション層、ドメイン層、インフラストラクチャ層に分ける。ドメイン層を他の関心事から隔離できる。
  • エンティティ:同一性によって定義されるオブジェクト。エンティティの状態は変化するが、同一性は不変。
  • 値オブジェクト:属性の値によって定義されるオブジェクト。不変であり、交換可能。
  • サービス:ドメインの操作を表現するが状態は保持しないオブジェクト。エンティティや値オブジェクトに自然に属さない操作を受け持つ。
  • 集約:整合性を保つ必要のある関連オブジェクトの集まり。ルートエンティティを中心に構成され、境界の外部からはルートのみ参照可能。
  • ファクトリ:オブジェクトの生成の複雑さを隠蔽し、生成ロジックを集約するオブジェクト。
  • リポジトリ:永続化の複雑さを隠蔽し、オブジェクトのライフサイクルを管理するオブジェクト。

第3部 より深い洞察へ向かうリファクタリング

  • ブレイクスルー: 開発者がドメインへの深い理解を得て、モデルや設計が飛躍的に改善されること。予測は難しいが、継続的なリファクタリングから生まれる。リスクもあるが、大きなチャンスでもある。
  • 暗黙的な概念: ドメインの重要な概念のうち、まだモデルで明示的に表現されていないもの。会話の端々に表れたり、既存の設計のぎこちなさの原因になっていたりする。見出して明示的にモデル化することが重要。
  • しなやかな設計: 意図が明確で、副作用がなく、概念の輪郭に合致した設計。開発者が理解と拡張を繰り返しやすくなる。深いモデリングと相互に補完し合う。
  • アナリシスパターンドメインに関するベストプラクティスをパターン化したもの。モデリングのヒントを与えてくれるが、そのまま当てはめられるわけではない。プロジェクトの文脈に合わせて応用する。
  • 概念の輪郭: ドメインの中で本質的に関連し合う概念の集まりの境界線。モデルと設計は、この輪郭に沿って構成要素を切り出すべきである。

第4部 戦略的設計

  • 境界づけられたコンテキスト: モデルを適用できる範囲を明示的に定義したもの。モデルの断片化を防ぎ、言語的な統一性を保つ。
  • コンテキストマップ: システムを構成する複数のコンテキストとその関係を俯瞰的に示した地図。統合作業の指針となる。
  • 戦略的設計: モデルの本質を見極め、洗練させるための継続的な取り組み。コアドメインを際立たせ、モデルを深化させる。
  • コアドメイン: システムの中で最も価値を生み出す、特徴的なモデルの部分。戦略的設計の主眼となる。
  • 汎用サブドメイン: ドメインに関連するが、標準的な概念を扱う、再利用可能なモデルの部分。コアドメインの対比として重要。
  • 大規模な構造: システム全体を貫く設計上の方針や概念的な枠組み。モデルに一貫性をもたらし、開発者間のコミュニケーションを助ける。
  • 凝集したメカニズム: 複雑だが、まとまりのある一連の処理を隠蔽するサブシステム。ドメインモデルのもつれを解消する。
  • ユビキタス言語: チーム全体で共有されるモデルについての言語。境界づけられたコンテキストごとに「方言」が存在する。
  • パターン: 文脈を共有する問題に対する、再利用可能な解決の枠組み。パターンは名前、問題、解決、結果で構成される。



考察

第1部 ドメインモデルを機能させる

エリック・エヴァンスの「ドメイン駆動設計」は、複雑なソフトウェア開発に挑むための強力な方法論を提示している。その中核をなすのは、問題領域を深く理解し、その本質を巧みにモデル化することだ。開発チームがドメインの専門家と協力し、徹底的に知識をかみ砕いてソフトウェアの設計に落とし込む。コードはモデルを如実に反映し、ドメインの言葉がそのまま生きる。

これは理想的なアプローチだが、実践には高いハードルがある。専門的知識を持つ担当者の確保、チーム全体でのスキル習得、綿密なコミュニケーションなど、なかなか難しい課題が多い。また、モデリングとコーディングの一体化は、技術的複雑性を増す恐れもある。

とはいえ、著者の豊富な経験に基づく指針は示唆に富む。単に形式的な方法論ではなく、プロジェクトを成功に導く哲学と言える。ポイントは、ドメインの専門性とソフトウェア構築のスキルを統合することにある。チームワークと学習を重視する組織文化も欠かせない。

従来の開発方式への根本的な挑戦であるだけに、一朝一夕には浸透しないだろう。だが、イノベーティブなシステムを生み出すには欠かせないアプローチと言える。人材育成を含め、息の長い取り組みが求められる。本書で提示された知見は、これからのソフトウェア開発を導く指針となるはずだ。

第2部 モデル駆動設計の構成要素

本書の第2部は、ドメイン駆動設計の中核をなす構成要素を体系的に提示しており、モデルを実装に落とし込む上で道標となる。レイヤ化による関心事の分離、エンティティと値オブジェクトの使い分け、サービスによる操作のモデル化など、示されたパターンは納得感があり、実践的である。
特に、モデルの実装において陥りがちな、トランザクション整合性の問題に対する集約の導入は腑に落ちる。また、ライフサイクル管理の複雑さを隠蔽するファクトリやリポジトリは、モデルに集中するためにも不可欠だ。終盤の貨物輸送ドメインの例は各要素の適用イメージを掴むのに役立つ。
一方で、関係データベースとの整合性をいかに保つかについては、やや深掘りが足りない印象を受けた。モデルとテーブル設計の乖離は実務ではよく直面する問題であり、両者のバランスについてはもう少し議論が欲しかった。
とはいえ、ドメインモデルをどう実装に落とし込むかという普遍的な課題に対し、筋の通った方法論を提示した点で、本書の価値は非常に高い。経験則に基づくパターンの適用と、フレームワークとの整合性の取り方など、示唆に富む知見が随所に見られた。エンティティや集約など、用語の定義も明快で、ドメインモデリングを行う上で常に参照したくなる良書である。

全体としては、ソフトウェア開発の本質であるモデリングと実装の間の翻訳を、いかにして整合性高く行うかを真摯に考究した良著であり、難解なドメインに立ち向かう開発者必読の書と言えるだろう。

第3部 より深い洞察へ向かうリファクタリング

「エリック・エヴァンスのドメイン駆動設計」の第3部は、洗練されたドメインモデリングと設計のための実践的な指針を提示している。それは、単に技法の羅列ではない。むしろ、開発者に、ドメインを深く理解し、その本質を細部に至るまでソフトウェアに反映させようとする姿勢を求めている。

特に印象的なのは、「ブレイクスルー」というアイデアだ。画期的な発見や大幅な改善は、スケジュール通りに起きるわけではない。だが著者は、日々の地道な歩みを信じ、チャンスを逃さない用意を促している。これは、ソフトウェア開発というクリエイティブな営みの本質を捉えた洞察だと言えるだろう。

また、「しなやかな設計」の重要性も説得力を持って語られている。ドメインを深く理解しても、それを使いやすいコードに落とし込めなければ意味がない。かといって、設計のための設計に走ってもいけない。あくまで、ドメインの概念を明晰に、安全に、過不足なく表現できることが肝要なのだ。

反復的に理解を深め、モデルを練り上げ、設計をしなやかに進化させること。著者が説くこのプロセスは、きれいごとを言っているわけではない。変化を恐れず、試行錯誤を厭わない覚悟が必要だ。トレードオフも避けられない。だがそれでも、ドメインモデリングを真摯に追求する意義は失われないと、私は本書から教えられた。

もちろん、すべてのプロジェクトでここまで突き詰められるわけではないだろう。モデルの妥当性の評価も難しい問題だ。とはいえ、現実と向き合いながら、ソフトウェアにドメインの本質を宿すことをあきらめない姿勢には、大きな感銘を受ける。設計のプロとして、この高い理想に少しでも近づきたいと思わずにはいられない。

第4部 戦略的設計

本書の第4部で提示されている戦略的設計の原則とパターンは、大規模で複雑なシステムを開発する上で、極めて示唆に富む指針となっている。特に、モデルの断片化を防ぐための「境界づけられたコンテキスト」、本質を見極めるための「戦略的設計」、全体を見渡すための「大規模な構造」は、ドメイン駆動設計の真髄といえる考え方であり、複雑なドメインに立ち向かう開発チームにとって、羅針盤のような役割を果たすものといえる。

著者のエリック・エヴァンスは、実際のプロジェクトで得られた知見をもとに、抽象的な原則だけでなく、具体的で実践的なパターンを数多く提示している。「コンテキストマップ」「責務のレイヤ」「プラグイン可能なコンポーネントフレームワーク」など、エンジニアリングの現場ですぐにでも活用可能なテクニックが詰まっていることは、本書の大きな魅力の1つである。

また、「進化する秩序」という考え方は、アジャイル開発の思想とも通底するものがあり、ソフトウェア開発プロセス全般に対する重要な示唆を与えている。drilldownとemergenceのバランスを重視し、状況の変化に柔軟に対応しながら、モデルを洗練していく姿勢は、DDD実践者のみならず、全てのソフトウエアエンジニアが学ぶべき教訓だと言える。

一方で、「凝集したメカニズム」や「知識レベル」といった概念は、やや抽象度が高く、実践の難易度も高いと感じられた。これらのパターンをどのように実装に落とし込むか、具体的な事例を交えた補足説明があればより理解が深まったのではないだろうか。

また、ドメインモデリングユビキタス言語の重要性は本書全体を通して強調されているが、複数の「方言」が生まれた場合の言語的な統合プロセスについては、もう少し踏み込んだ議論が欲しいところである。

とはいえ、これらはあくまで些細な望蜀の念に過ぎない。エリック・エヴァンスが提唱する戦略的設計の原則とパターンは、今日のソフトウエア開発に携わる技術者に対し、示唆と活力を与えてくれる貴重な知的資産である。系統的で論理的な議論の末に、「優れたソフトウエアを作成することは、学び考える活動である」という含蓄のある言葉で結ばれていることからも、著者の豊かな洞察が窺い知れる。本書は、単なる開発技法の解説書ではなく、ソフトウエアエンジニアリングの真髄を問う、1つの思想書としても読み継がれるべき書物だと言えるだろう。

まとめ

エリック・エヴァンスの「ドメイン駆動設計」は、複雑なソフトウェア開発に立ち向かうための強力な方法論を提示している。その中核をなすのは、問題領域を深く理解し、その本質を巧みにモデル化し、ソフトウェアに反映させることだ。開発チームがドメインの専門家と協力し、知識をかみ砕いてモデルを構築する。そのモデルは、ユビキタス言語を通してチーム全体で共有され、コードに直接的に反映される。

本書で解説されている、レイヤ化アーキテクチャ、エンティティと値オブジェクト、集約、ファクトリ、リポジトリなどの構成要素は、モデルを実装に落とし込む上で強力な手がかりとなる。一方、関係データーベースとの整合性の取り方など、実践上の難しさも残されている。

より洗練されたモデルを目指すには、ブレイクスルーのチャンスを逃さず、しなやかな設計を追求する必要がある。そのためには、アナリシスパターンデザインパターンを応用しつつ、試行錯誤を重ねる覚悟が問われる。

さらに、複数のチームが関わる大規模なシステム開発では、戦略的設計の原則とパターンが不可欠だ。境界づけられたコンテキスト、コンテキストマップ、コアドメインの見極めなどにより、モデルの断片化を防ぎ、本質を追求することができる。状況の変化に合わせてモデルを進化させる柔軟な姿勢も重要となる。

ドメイン駆動設計は、理想的ではあるが実践のハードルも高い。組織の文化や個人のスキルの問題も絡む。とはいえ、ソフトウェア開発の真髄を捉えたアプローチであることは間違いない。イノベーティブなシステムを生み出し、ビジネス価値を高めるには、避けて通れない道のりなのだ。本書から学んだ洞察を糧に、地道な努力を重ねていきたい。

dlshogiのPyTorch Lightning対応 その5(Warm-upに対応したスケジューラ)

大規模なモデルの学習に効果があるとされる学習率スケジューリングの手法にWarm-upがある。
しかし、Pytorchの標準のスケジューラには、Warm-upに対応したスケジューラが提供されていない。

PyTorch Lightning Boltsには、Warm-upに対応したCosineAnnealingLRがある。
Linear Warmup Cosine Annealing — Lightning-Bolts 0.7.0 documentation

まだレビュー中のステータスで、機能的にもリスタートや減衰には対応していない。

深層学習界隈でよく使われるtimmには、Warm-upの他にリスタート回数や減衰率の調整が可能なCosineAnnealingLR学習率スケジューラがある。
SGDR - Stochastic Gradient Descent with Warm Restarts | timmdocs

そこで、先日作成したdlshogiのPyTorch Lightning CLIの訓練スクリプトで、timmのCosineAnnealingLRを使おうと試そうとしたが、PyTorchのLRSchedulerを継承していないため、標準的な方法では使うことができなかった。

PyTorch Lightning CLIをカスタマイズすることで使えるようにはできるようだが、PyTorch Lightning CLIを使うメリットが薄れてくるので、できればあまりカスタマイズしたくない。
How to utilize timm's scheduler? · Issue #5555 · Lightning-AI/pytorch-lightning · GitHub

そこで、LRSchedulerを継承して、timmのCosineAnnealingLRと同じ動作をするスケジュールを自作することにした。

LRSchedulerを継承したtimmのCosineAnnealingLRと同等のスケジューラ

""" This code is based on the Cosine Learning Rate Scheduler implementation found at:
https://github.com/huggingface/pytorch-image-models/blob/main/timm/scheduler/cosine_lr.py
"""

import math

from torch.optim.lr_scheduler import LRScheduler


class CosineLRScheduler(LRScheduler):
    def __init__(
        self,
        optimizer,
        t_initial,
        lr_min=0.0,
        cycle_mul=1.0,
        cycle_decay=1.0,
        cycle_limit=1,
        warmup_t=0,
        warmup_lr_init=0,
        warmup_prefix=False,
        k_decay=1.0,
        last_epoch=-1,
    ):
        self.t_initial = t_initial
        self.lr_min = lr_min
        self.cycle_mul = cycle_mul
        self.cycle_decay = cycle_decay
        self.cycle_limit = cycle_limit
        self.warmup_t = warmup_t
        self.warmup_lr_init = warmup_lr_init
        self.warmup_prefix = warmup_prefix
        self.k_decay = k_decay

        if last_epoch == -1:
            base_lrs = [group["lr"] for group in optimizer.param_groups]
        else:
            base_lrs = [group["initial_lr"] for group in optimizer.param_groups]
        if self.warmup_t:
            self.warmup_steps = [(v - warmup_lr_init) / self.warmup_t for v in base_lrs]
        else:
            self.warmup_steps = [1 for _ in base_lrs]

        super().__init__(optimizer, last_epoch)

    def get_lr(self):
        t = self.last_epoch
        if t < self.warmup_t:
            lrs = [self.warmup_lr_init + t * s for s in self.warmup_steps]
        else:
            if self.warmup_prefix:
                t = t - self.warmup_t

            if self.cycle_mul != 1:
                i = math.floor(
                    math.log(
                        1 - t / self.t_initial * (1 - self.cycle_mul), self.cycle_mul
                    )
                )
                t_i = self.cycle_mul**i * self.t_initial
                t_curr = (
                    t - (1 - self.cycle_mul**i) / (1 - self.cycle_mul) * self.t_initial
                )
            else:
                i = t // self.t_initial
                t_i = self.t_initial
                t_curr = t - (self.t_initial * i)

            gamma = self.cycle_decay**i
            lr_max_values = [v * gamma for v in self.base_lrs]
            k = self.k_decay

            if i < self.cycle_limit:
                lrs = [
                    self.lr_min
                    + 0.5
                    * (lr_max - self.lr_min)
                    * (1 + math.cos(math.pi * t_curr**k / t_i**k))
                    for lr_max in lr_max_values
                ]
            else:
                lrs = [self.lr_min for _ in self.base_lrs]

        return lrs

timmの方では、最大の学習率をoptimizerから取得して、メンバ変数に保持しているが、PyTorchのスケジュールは、optimizerのinitial_lrにコピーを持たせているので、その作法に合わせている。

また、timmのスケジューラは、step()にステップ数を渡すようになっているが、PyTorchのスケジューラは、step()は引数なしでlast_epochから取得するようになっているため、そちらに合わせた。

更新間隔

PyTorch Lightningの標準のスケジューラの更新間隔は、epoch単位になっている。
将棋AIでは1epochが大きいステップ数になるため、Warm-upやCosineAnnealingLRを使う場合は、ステップ単位で更新したい。

PyTorch Lightningは、更新間隔を変更するオプションがないため、LightningCLIを継承してクラスメソッドのconfigure_optimizersをオーバーライドする必要がある。
Change the scheduler interval in CLI · Lightning-AI pytorch-lightning · Discussion #13975 · GitHub

class LightningCLI(cli.LightningCLI):
    @staticmethod
    def configure_optimizers(lightning_module, optimizer, lr_scheduler=None):
        if lr_scheduler is None:
            return optimizer
        return {
            "optimizer": optimizer,
            "lr_scheduler": {
                "scheduler": lr_scheduler,
                "interval": "step",
                **(
                    {"monitor": lr_scheduler.monitor}
                    if isinstance(lr_scheduler, ReduceLROnPlateau)
                    else {}
                ),
            },
        }

動作確認

config.yamlに以下のように定義を行い動作確認を行った。
チェックポイントをリジュームして継続できるかも確認した。

lr_scheduler:
  class_path: dlshogi.lr_scheduler.CosineLRScheduler
  init_args:
    t_initial: 200
    lr_min: 1e-8
    cycle_mul: 2
    cycle_limit: 8
    cycle_decay: 0.5
    warmup_t: 100
    warmup_lr_init: 1e-7
    warmup_prefix: true

Tensorboardのグラフが50ステップ間隔なため、カクカクでわかりにくいが、warmup_tやcycle_mul、cycle_decayのパラメータが機能していることが確認できる。

まとめ

dlshogiにWarm-upに対応したスケジューラを追加した。
Warm-upにどれくらい効果があるかは別途検証したい。

dlshogiモデルの枝刈りを試す

前回、深層強化学習において、モデルの枝刈りによりスケーリングが可能であることを示した論文を紹介した。

dlshogiの強化学習でもモデルの枝刈りが効果があるか試したいと考えているが、まずはモデルの枝刈りのみを行って、精度と探索速度にどう影響するかを調べてみる。

モデルの枝刈り

モデルの枝刈り(pruning)は、休止状態のニューロンを削除することで、モデルサイズを削減し、推論速度を向上させる手法である。

PyTorchには枝刈りの方法として、unstructuredとstructuredの2種類の方法が用意されている。

unstructuredは、層のパラメータ全体から一定の割合でパラメータを削除する。

structuredは、一定の割合で畳み込みのチャンネル全体を削除する。

パラメータの削除は、実際には削除は行っておらずパラメータを0にすることで実装されている。

値が0のパラメータも演算は必要なため、推論速度を向上するには、ハードウェア的な支援が必要になる。

NVIDIAGPUでは、Ampere以降で、structuredの枝刈りに対応している。
NVIDIA Ampere アーキテクチャと TensorRT を使用してスパース性で推論を高速化する - NVIDIA 技術ブログ

ただし、連続する4つのチャンネルのうち2つが0である必要があるため、枝刈り率50%以上でないと効果がない。

先述の論文では、枝刈りは5%でパフォーマンスが上がることが報告されているため、枝刈りしても推論速度の向上は期待できない。

PyTorchでの実装

チェックポイントを読み込んで、モデルの各層のモジュールをnamed_modules()で取得して、畳み込み層の場合、weightに対して、prune.ln_structuredを適用する。

コードは、以下の通り簡単に記述できる。

import argparse

import torch

from dlshogi.common import *
from dlshogi.network.policy_value_network import policy_value_network
from torch.nn.utils import prune

parser = argparse.ArgumentParser()
parser.add_argument("checkpoint")
parser.add_argument("output_checkpoint")
parser.add_argument("--network", default="resnet10_swish", help="network type")
parser.add_argument("--amount", type=float, default=0.05)
args = parser.parse_args()

device = torch.device("cpu")
model = policy_value_network(args.network)

checkpoint = torch.load(args.checkpoint, map_location=device)
model.load_state_dict(checkpoint["model"])

# prune
for name, module in model.named_modules():
    if isinstance(module, torch.nn.Conv2d):
        prune.ln_structured(module, name="weight", n=1, dim=0, amount=args.amount)
        prune.remove(module, "weight")

checkpoint["model"] = model.state_dict()
torch.save(checkpoint, args.output_checkpoint)

実験条件

dlshogiの30ブロック384フィルタのモデルを使用する。
枝刈り率は、論文と同じ5%の他に、推論速度の違いを見るため50%、90%でも試す。
テストデータには、floodgateのR3800以上の棋譜を使用する。
探索速度は、初期局面で10秒思考したときのNPSを測定する。
GPUは4090 1枚を使用する。

精度

通常は枝刈り後にファインチューニングを行うが、ファインチューニング前の精度を確かめた。

枝刈り 方策損失 価値損失 方策正解率 価値正解率
なし 1.3057 0.4402 0.5585 0.7765
5% 4.484 0.644 0.3173 0.6775
50% 7.3963 0.8144 0.0090 0.5878
90% 9.6545 1.5117 0.0001 0.4989


枝刈り5%でも、方策の正解率は24%、価値の正解率は9.9%下がっており、精度はかなり落ちる。
枝刈り50%、90%は、方策の正解率が1%未満であり、使い物にならない精度である。

推論速度

ソース修正

TensorRTで、枝刈りしたモデルの推論速度を向上させるには、オプションの設定が必要になる。

公式ドキュメントのSparsityの項目に説明がある。
Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

ソースコードに以下の行を追加した。

config->setFlag(BuilderFlag::kSPARSE_WEIGHTS);
比較結果
枝刈り NPS
なし 20255
5% 19836
50% 23805
90% 26020

枝刈り5%の場合、枝刈りなしとほぼ同じの探索速度である。
枝刈り50%の場合17.5%、枝刈り90%の場合28.5%向上する。

ハードウェア支援が効果があることが確認できた。

kSPARSE_WEIGHTSオプションなしの場合

kSPARSE_WEIGHTSオプションなしの場合でも確認した。

枝刈り NPS
なし 20255
5% 19698
50% 20898
90% 20470

どの条件もほぼ同じ探索速度である。

まとめ

dlshogiモデルで枝刈りを試した。
5%の枝刈りでも精度が大きく落ちることが分かった。
50%の枝刈りでは方策正解率が1%未満になり使い物にならないほど精度が下がった。

また、TensorRTでオプションを有効にすると、枝刈り50%以上では探索速度が向上することが確認できた。
しかし、枝刈り5%では探索速度は変わらなかった。
論文で報告されている枝刈り率は5%のため、探索速度の向上は期待できない。

ファインチューニングを行った後に精度が回復するかは別途確認したい。

【論文】In deep reinforcement learning, a pruned network is a good networkを読む

DeepMindarXiv上で発表した、深層強化学習のモデルを段階的な枝刈りすることでモデルサイズのスケーリングが可能になることを示した「In deep reinforcement learning, a pruned network is a good network」を読んだ際のメモ。

概要

  • 深層強化学習はモデルサイズをスケーリングことは困難だった
  • 段階的に枝刈りすることでスケーリングが可能であることを実証した
  • 枝刈りすることでパフォーマンスも向上する

導入

  • 深層強化学習では訓練中に多数のパラメータが休止状態になる
  • 最近の研究でResNetバックボーンのDQNで段階的な枝刈りで元のパラメータの10%で50%パフォーマンスが向上ことが発見された
  • この論文では、段階的な枝刈りによって、ネットワークのサイズに比例してパフォーマンスが向上することを示す

関連研究

  • 深層強化学習でネットワークをスケーリングすることは、強化学習特有の不安定性により困難だった
  • CNNよりもResNetがスケーリングに適していることが示された
  • 大きすぎるパラメータのネットワークは過剰適合する傾向がある
  • ネットワークが初期のデータに過剰適合する傾向がある

方法

  • 訓練の20%で枝刈りを開始し、80%で最終的なスパースレベル(0.95)になるように段階的に枝刈りする
  • 15層のResNet
  • 15のアタリゲームで評価
  • オンライン、オフラインの両方で調査

結果

スケール則


  • 枝刈りしないネットワークはスケールを増やすとパフォーマンスが低下する
  • 段階的な枝刈りしたネットワークはスケールを増やすほどパフォーマンスが向上する
  • 段階的な枝刈りを使用すると、パラメータ効率が向上することを示唆している

スパース性


  • 90%から95%でパフォーマンスが向上
  • 99%でパフォーマンス維持

他、CNN、再生率、データ量、オフライン設定の実験結果があるが省略

結論

  • これまでの強化学習エージェントは利用可能なパラメーターを十分に活用しない傾向があった
  • この活用不足は訓練全体を通じて増加し、ネットワーク サイズが増加するにつれて増幅される
  • 段階的な枝刈りがネットワーク パラメータ効率を最大するのに効果的であることを示した
  • ネットワークのスケールを上げることでパフォーマンスが向上することは、一種のスケーリング則を示唆している
  • 長時間トレーニングした場合に安定したパフォーマンスを維持する

感想

将棋AIのモデルでは、パラメータ数を上げるほど強くなることが経験的にわかっている。
現在、大会で実績のある最大のモデルは、dlshogiのResNet 30ブロック384フィルタのモデルである。
これ以上モデルサイズを大きくした場合にどれくらい強くなるかに興味があった。
より大規模なモデルで実験できるように、マルチGPUでの学習に取り組んでいる。

この論文の示唆するところを考慮すると、大規模なモデルは限界がありそうである。
枝刈りの手法により深層強化学習のパフォーマンスが向上することが示されており、将棋AIでも効果があるか試してみたい。
ニューラルネットワークの枝刈りは、推論速度を上げる代わりに精度がある程度犠牲になる認識だったが、深層強化学習では逆にパフォーマンスが向上するというのが興味深い。
枝刈りにより推論速度も上がるため、探索においても効果があるので一石二鳥な手法のように思う。

dlshogiのモデルで、95%に枝刈りして追加学習した場合と、枝刈りなしで追加学習した場合で比較を行ってみたい。

まとめ

段階的な枝刈りにより深層強化学習のパフォーマンスが向上しスケーリングが可能になることを示した論文を読んだ。
将棋AIでも段階的な枝刈りが効果があるか試してみたい。

dlshogiのPyTorch Lightning対応 その4(8GPUで学習)

前回、dlshogiをPyTorch Lightningを使用した並列学習に対応させた、8GPUでどれくらい学習が速くなるか試した。

条件

  • 30ブロック384フィルタのモデル
  • 訓練データ: 35,289,182局面
  • エポック数: 2
  • バッチサイズ: 4096
  • オプティマイザ: AdamW
  • 学習率: 1e-4 (1epochごとに1/2)
  • WeightDecay: 1e-2

訓練時間


※version_0: 8GPU、version_1: 1GPU

8GPUで学習すると、1GPUに対して、学習時間は0.15倍になった。
約6.7倍速くなっている。

精度

訓練損失

8GPUの方が小さく変動しているが、ほぼ同じ傾向である。
変動する理由は、8GPUの方は、分割した1プロセスのログが出力されているため、バッチサイズが1/8になることで分散が増えたためと考えられる。

評価損失

8GPUの方が評価損失は下がっているが、微差である。

方策正解率


1GPUの方がわずかに高いが、微差である。

価値正解率


8GPUの方がわずかに高いが、微差である。

8GPUで学習しても精度は保たれており、学習時間が短縮できることが確認できた。

Sync Batch Normalization

PyTorch Lightningのデフォルトでは、Sync Batch Normalizationが無効になっている。

Sync Batch Normalizationは、Batch Normalizationの統計を複数プロセスで同期する機能である。
Batch Normalizationを使用していて、マルチGPUで学習する場合は有効にした方がよい。

PyTorch Lightningでは、Trainerのsync_batchnormをTrueにすることで有効にできる。

sync_batchnorm=Trueにして、Falseの場合と比較した結果は以下の通り。

訓練時間


※version_2がsync_batchnorm=True

sync_batchnorm=Trueにすると、学習時間が約1.12倍になる。
Batch Normalizationの統計を同期する分遅くなっている。

訓練損失

ほぼ同じである。

評価損失

sync_batchnormなしの方が、少し低い。

方策正解率

sync_batchnormありの方が若干高いが、微差である。

価値正解率

sync_batchnormなしの方が、少し高い。


sync_batchnormなしの方が、評価精度が高そうだが、エポックごとに差がありそうなので、追加で2エポック学習させてみた。

評価損失


※version_3がsync_batchnormなし、version_4がsync_batchnormあり

4エポック目でほぼ同じになった。

方策正解率

sync_batchnormなしの方が若干高いが、微差である。

価値正解率

sync_batchnormありの方が若干高いが、微差である。


エポックごとに差があるが、ほぼ違いはない。

学習時間が少し長くなることを考慮すると、sync_batchnormはなしでも測定結果からは問題なさそうである。
収束するまで学習した際にどうなるかはわからない。

まとめ

8GPUで学習した際の学習時間と精度を確認した。
結果、精度を保ったまま、学習時間を0.15倍にできることがわかった。

dlshogiのPyTorch Lightning対応 その3(DDP)

PyTorch Lightningに対応できたので、DistributedDataParallel によるマルチGPU学習を試してみた。

前回未対応だった、勾配クリッピングはconfig.yamにgradient_clip_valを記述するだけで対応できた。
また、モデルのエクスポートもon_train_end()で実装した。
DeepLearningShogi/dlshogi/ptl.py at feature/pytorch_lightning · TadaoYamaoka/DeepLearningShogi · GitHub

DDP

PyTorch Lightningで、Trainerに複数のデバイスを指定すると自動で、データパラレルでマルチGPUで並列化できる。

昨今、大規模言語モデルでは、並列化にモデル並列などの手法が使われるが、将棋AIのモデルはGPUメモリに十分のるサイズなので、データパラレルで並列化を行う。

データパラレルは、バッチを分割して、各GPUで計算を行うので、各GPUのバッチサイズ×GPU数がバッチサイズになる。
1GPUのバッチサイズが小さいとGPUの利用効率が落ちるので、それぞれのGPUで限界のバッチサイズを割り当てるのが良い。

ただし、合計のバッチサイズが大きすぎるとラージバッチ問題という、ノイズによる効果が失われて汎化性能が失われる問題が起きる。

したがって、大きなモデルで、合計のバッチサイズが抑えられる場合でしか並列化の効果は得られない。

実験

並列化による速度向上と精度を確認するため、今回は20ブロック256フィルタのモデルで実験した。

1GPUあたりのバッチサイズ固定でGPUを増やした場合と、合計のバッチサイズ固定でGPUを増やした場合で確認した。

基準のバッチサイズは4096とする。

データローダの並列化

マルチGPUで学習する場合、データローダが複数プロセスで作成される。

各プロセスでファイルからデータの読み込みが必要になるため、あらかじめ前処理済みのキャッシュファイルを作成しておくと良い。
config.yamlのデータのパラメータにcacheのパスを指定して、0エポックでfitを実行するとキャッシュを作成できる。

data:
  train_files:
    - /work/hcpe3/selfplay_pre42_resnet30x384_relu_b4096lr004-013_floodgate8_s5_po10000-01.hcpe3
  val_files:
    - /work/hcpe3/floodgate.hcpe
  batch_size: 4096
  val_batch_size: 4096
  use_average: true
  use_evalfix: true
  temperature: 1.0
  cache: cache
python ptl.py fit -c config.yaml --trainer.max_epochs 0

実験結果

バージョン GPU バッチサイズ/GPU 合計バッチサイズ
version_0 1GPU 4096 4096
version_1 2GPU 4096 8192
version_2 3GPU 4096 12288
version_3 2GPU 2048 4096
訓練時間

GPUあたりのバッチサイズ固定でGPU数を増やすと、GPU数に反比例して訓練時間が短くなる。
全体のバッチサイズを固定して、GPU数を増やした場合、2GPUで訓練時間は0.53倍になり、少しオーバーヘッドがあるが訓練時間はGPU数に反比例して短縮される。

訓練損失

訓練損失は、GPUあたりのバッチサイズ固定でGPU数を増やした場合、高くなる傾向がある。
合計のバッチサイズを固定してGPU数を増やした場合、ほぼ同じ訓練損失となる。

評価損失

評価損失も訓練損失と同様の傾向である。

以下、評価方策正解率、評価価値正解率についても、GPUあたりのバッチサイズ固定で全体のバッチサイズを上げると精度が下がる傾向は同様である。

評価方策正解率


評価価値正解率


評価方策エントロピー


評価価値エントロピー


まとめ

dlshogiのPyTorch Lightning対応を行い、データパラレルによるマルチGPU並列化を試した。
20ブロック256フィルタのモデルで、合計のバッチサイズを固定して、GPUを増やした場合、訓練時間はGPU数にほぼ反比例して短くなり、精度は1GPUの場合と同じに維持されることが確認できた。

また、GPUあたりのバッチサイズ固定でGPUを増やし全体のバッチサイズを増やした場合は、学習時間はGPU数に反比例して短くなるが、精度が落ちることが確認できた。

マルチGPUで学習できるようになったので、8GPUで大規模将棋モデルの学習を試してみたい。