はじめに
私は, 巡回セールスマン問題(TSP)のソルバを本格的に開発し, その大規模な実験結果を効率的に管理したいと考えていました.
最初は Python でプロトタイプを実装しましたが, 大規模な問題や多数の実験を行うには, どうしても速度に限界があるという壁にぶつかりました. そこで C#への移植を決断しました. (「C++が最適なのでは?」という声もあるかと思いますが, 今回は自身の技術スタックと生産性を考慮して C#を選択しました. この点には目を瞑っていただけると幸いです.)
しかし, C#で実装を進めるにあたり, 実験管理のデファクトスタンダードである MLflow を直接扱うのが困難であるという問題が発生しました.
この問題を解決するため, 最初に C#を含めたどの言語/環境からでも利用できる MLflow ラッパー API を先に開発しました. その開発経緯と詳細はMLflow のラッパー API を作ってみた記事を参照ください.
本連載では, その API を活用し, 拡張性と保守性を重視して設計/実装した C#製の TSP ソルバ実験フレームワークについて解説します.
本記事「プロジェクトの全体像と設計原則」は連載の第 1 回として, その中核となる設計思想, プロジェクト間の依存関係, そして各プロジェクトのディレクトリ構成と言った, 全体の青写真について解説します
連載の構成
本連載「TSP ソルバ実装記」は全 4 回を通して, フレームワークの全てを公開していきます.
- 連載 1: プロジェクトの全体像と設計原則 ← 本記事
- 連載 2: 制御とインフラストラクチャの実装
- 連載 3: ソルバの抽象化と TSP コアロジック
- 連載 4: 個別アルゴリズムの実装と実験
ぜひ最後までお付き合いください.
目次
設計思想
本プロジェクトの設計思想は, 長期的な保守性と柔軟な拡張性を確保するために, 明確なルールに基づいています. 特に「TSP ソルバ」という計算ロジックと, 「実験管理」という外部連携ロジックを分離することが重要でした.
レイヤードアーキテクチャの適用
プロジェクト全体を, 責務に応じて垂直に階層化するレイヤードアーキテクチャを採用しました.
これにより, 各層が依存する方向を制御し, コードの変更が他の層に予期せぬ影響を与えることを防いでいます.
レイヤ名 | 責務の概要 |
---|---|
アプリケーション層 (Application/Processor) |
実験のワークフロー全体を制御し, ビジネスロジックを実行する. |
ビジネスロジック層 (Solver) |
TSP アルゴリズムを含む, 純粋な問題解決ロジック. |
インフラストラクチャ層 (ExternalInterfaces) |
外部 API(MLflow)やシステム監視など, 外部との接続を担う具体的な実装. |
共有/ドメイン層 (Common) |
レイヤ間で共有されるデータ構造, 設定, インターフェースの定義. |
各プロジェクトの役割
レイヤードアーキテクチャに基づき, C#の各プロジェクト(パッケージ)に役割を与えました.
プロジェクト名 | レイヤ | プロジェクトテンプレート | 説明 |
---|---|---|---|
Application | アプリケーション層 | console | Console で走らせるためのエントリープロジェクト |
Processor | アプリケーション層 | classlib | 実験の実行や記録等, 実際の処理 |
Solver | ビジネスロジック層 | classlib | ソルバの詳細な実装 |
ExternalInterfaces | インフラストラクチャ層 | classlib | WebAPI とのクライアント |
Common | 共有/ドメイン層 | classlib | 設定情報や WebAPI のレコード |
この明確な責務分担により, たとえば「新しいソルバを追加したい」場合はSolverプロジェクトのみを修正すればよく, 「MLflowを別のサービスに切り替えたい」場合はExternalInterfacesプロジェクトのみを修正すれば済むようになっています.
プロジェクト間の依存関係
前のセクションで定義したレイヤードアーキテクチャは, C#プロジェクト(パッケージ)間の依存関係として厳密に適用されています.
この依存関係は, 原則として「内側」に向かう一方通行であり, クリーンアーキテクチャ(Clean Architecture)の原則に準拠しています.
依存関係を制御することで, 依存性逆転の原則が成り立ちます. 上位レイヤ(例: Processor)は下位レイヤ(例: ExternalInterfaces)の具体的な実装に一切依存せず, Commonプロジェクトで定義された抽象的なインターフェースにのみ依存する状態を実現しています.
プロジェクト | 依存先 | 依存の理由とクリーンアーキテクチャへの準拠 |
---|---|---|
Common |
なし | 最も内側のドメイン層であり, 誰も依存しない共通の定義のみを提供します. |
ExternalInterfaces |
Common |
Common で定義された IMLflowClient などのインターフェースを具体的に実装するため. |
Solver |
Common |
Common で定義されたデータモデル(ProblemModel など)や ISolver インターフェースを利用するため. |
Processor |
Common ExternalInterfaces Solver
|
ライフサイクルを制御するため, 抽象化されたソルバ (ISolver) とクライアント (IMLflowClient) の両方に依存します. |
Application |
Processor |
エントリポイントとして, ProcessorComponent を生成し, 実行開始を指示するため. |
パッケージ図は以下の通りです.
矢印は「依存している」方向, すなわち「抽象度の低い層から高い層へ」向かいます.
この設計により, 例えばProcessorはExternalInterfacesを直接知らずに, CommonのIMLflowClient経由でMLflowとの通信を抽象的に利用でき, 柔軟な切り替えを可能にしています.
各プロジェクトのディレクトリ構成
このセクションでは, プロジェクトの論理的な責務(前述のレイヤ構造)が, C#の物理的なディレクトリ構造としてどのように具現化されているかを詳細に解説します.
Application プロジェクト
Application
プロジェクトは, コンソールでアプリケーションを起動するためのエントリーポイントを担います.
Processor
プロジェクトのコンポーネントを呼び出すだけのシンプルなプロジェクトであり, 実行ロジックは一切含まれません.
Common プロジェクト
Common プロジェクトは, すべてのレイヤから依存される最下層のドメイン層です. 全プロジェクトが共有するデータモデル, 設定ファイル構造, そしてインターフェースの定義を一元管理します.
フォルダ名 | 概要と設計上の意図 |
---|---|
DataModels |
全てのデータ構造を定義します. 特に CalculatorModels(純粋な計算データ)と External(外部 API 通信データ)を分離することで, 責務の分離を徹底しています. |
Interfaces |
アプリケーション層とインフラ層の間に立つすべての契約(ISolver, IMLflowClient など)を定義します. 依存性逆転の原則の要となります. |
Common/DataModels/CalculatorModels
CalculatorModels は以下を含みます.
計算とその結果に関連するデータモデルはここに含まれると考えてください.
- AlgorithmModels
- アルゴリズムのモデルを表す列挙体
- CalculationResult
- 計算結果のデータモデル
- ProblemModel
- TSP に用いる問題のデータモデル
- Coordinate
- ProblemModel に属する, 座標のデータモデル
Common/DataModels/External
External は以下を含みます.
実験の開始/終了, パラメータ/メトリクスのロギングはここに含まれると考えてください.
- 実験開始/終了
- ExperimentRequest, ExperimentResponse
- TerminateRunRequest
- データロギング
- LogMetricsRequest, LogParamsRequest
- LogResponse
- MetricPair
- PramamPair
Common/DataModels/Reporter
Reporter は進捗状況のレポート用レコードを含みます.
この名前空間はシンプルで, ファイルが 1 つで完結します.
Common/DataModels/Settings
Settings は以下を含みます. 実験の設定や固定情報はここに含まれると考えてください.
- Configurations
- コンフィグ情報を定義するレコード
- ExperimentSettings
- JSON の "experiments" 配下の一つの実験設定に対応するレコード
- FixedSettings
- JSON の "fixed" セクションに対応する静的な設定レコード
- RootSettings
- Settings ファイルのルート構造を定義するレコード
Common/Interfaces
Interfaces は以下を含みます.
外部とのやり取りを行うクラスのインターフェースはここに含まれると考えてください.
- MLflowInterface: MLflow ラッパー API とのやり取りを行うインターフェース
- IMlflowClient
- IMLflowClientFactory
- Solver: 計算機のインターフェース
- ISolver
- SystemMonitorInterface: システムモニタリング用のインターフェース
- ISystemMonitor
ExternalInterface プロジェクト
ExternalInterfaces
プロジェクトは, インフラストラクチャ層の実装を担います.Common.Interfaces の定義に基づき, 外部サービスとの具体的な通信(WebAPI, システム監視)ロジックを格納します.
ExternalInterface/Mlflow
Mlflow フォルダは, Common.Interfaces.MLflowInterface 直下のクライアント, クライアントのファクトリーを実装します.
ストラテジーパターンとファクトリーパターンを採用しています.
ExternalInterface/SystemMonitor
SystemMonitor フォルダは, Common.Interfaces.SystemMonitorInterface 直下のクライアント, クライアントのファクトリーを実装します.
MLflow と同じく, ストラテジーパターンとファクトリーパターンを採用しています.
Processor プロジェクト
Processor
プロジェクトは, アプリケーション層の核心であり, 実験のワークフロー全体を制御する責務を持ちます.
- ProcessorComponent.cs: 実験のライフサイクル(設定読み込み, 初期化, 実行, ロギング)を統括する中心的なファイルです
- ProcessorUnits/Utils: ProcessorComponent の補助的な役割を持つロジックを格納します
Solver プロジェクト
Solver
プロジェクトは, ビジネスロジック層の実装を担います.TSP の計算ロジックに関する全ての要素を含みます.
フォルダ名/ファイル | 役割と設計上の意図 |
---|---|
AbstractSolver.cs | すべての具体的ソルバが継承する基底クラス.共通処理と, protected なメソッドを通じて限定的な機能を公開します. |
SolverFactory.cs | 実行時に必要なソルバのインスタンスを生成するファクトリー. |
Solvers | 焼きなまし法 (SA), 遺伝的アルゴリズム (GA)など, ISolver を実装した具体的なアルゴリズムのクラスを格納します. |
Helper | AbstractSolver が内部的に使用する補助ロジック.Evaluator(評価)や Shuffle アルゴリズムの実装を格納します. |
まとめと次回予告
本記事では, TSPソルバフレームワークの基盤となる設計の全体像を解説しました.
- 設計思想
- 計算ロジックと外部連携ロジックを分離するため, 厳密なレイヤードアーキテクチャを採用
- 依存関係
- クリーンアーキテクチャの原則に従い, 依存は常に内側(抽象)に向かう一方向である
- ディレクトリ構成
- 各C#プロジェクト(Common, Solver, Processorなど)が, そのレイヤの責務を果たすためにどのような役割と構造を持っているかを確認
この設計により, 拡張性」「保守性」「柔軟性」という, 長期的な開発に必要な土台を築くことができました.
しかし, 現時点ではまだ, 実際に実験を動かし, 結果をMLflowに記録するための具体的なコードは見ていません.
次回の記事「連載2: 制御とインフラストラクチャの実装」では, この設計に基づいて, フレームワークの稼働に不可欠な以下の核心部分を深く掘り下げて解説します.
- データと契約の具現化: Commonプロジェクトで定義したインターフェース(IMLflowClientなど)の詳細
- インフラストラクチャの実装: ExternalInterfacesプロジェクトにおけるWebAPIクライアントの実装とDI(依存性注入)の仕組み
- 実験ワークフローの制御: Processorプロジェクトがどのように全体のライフサイクルを統括しているのか