Logo Ernie.SG
Berlayar:安定し、拡張しやすく、柔軟で、スケールするスタックを組む…

Berlayar:安定し、拡張しやすく、柔軟で、スケールするスタックを組む…

2023年9月7日
約3分で読めます
利用できるタグはありません
Table of Contents

第1部では、これまで作ってきた簡単なプロトタイプのいくつかと、それらを経て、LLM ベースのプロダクトには安定し、拡張しやすく、柔軟で、スケールするスタックが必要だと思うに至った経緯を書いた。作りたい面白いものが多すぎるからだ。レジストリと基底クラスを使えば、提案している A.I. 駆動ソフトウェア群のコンポーネントを柔軟に差し替えられ、開発者同士も協働しやすくなる、という話もした。では、エンドユーザー体験はどうするのか。僕が狙っている柔軟性とモジュール性を考えると、レジストリパターンとインターフェース駆動設計を組み合わせるのが、おそらく理想に近い。この投稿では、レガシーコードベースのリファクタリング自動化、メールや文書の上に載せるナレッジベース、ソーシャルメディア向けコンテンツの生成と公開、あるいは観客向けの新しいデータ消費方法まで、関心のある各種デプロイに一般化できそうな A.I./LLM エンジニアリングスタック案を整理する。

先へ進む前に、作品のアノテーションという問題に対して、従来型のソフトウェア開発と ML ベースのアプローチがどう違うのかを、いったんおさらいしておく。

評価軸

作品のアノテーションとタグ付けにおける従来型ソフトウェア開発

作品のアノテーションとタグ付けにおける機械学習ベースの開発(例:OpenAI の CLIP)

何をするか

事前に定義したルールとメタデータを使って、作品にアノテーションとタグを付ける。

データから学習したパターンを使って、作品に動的にアノテーションとタグを付ける。

アノテーションの深さ

明示的なルールと利用可能なメタデータに制約される。

より幅広い特徴、テーマ、要素を識別できる。

適応性

新しいスタイルやトレンドに対応するには、手作業で更新する必要がある。

再学習を通じて、新しいスタイルやトレンドに適応できる。

ユーザーエンゲージメント

ユーザーフィードバックの取り込みは手作業になる。

ユーザーフィードバックを自動的に取り込み、モデルのファインチューニングに使える。

計算量

一般に、計算負荷は比較的低い。

より深い分析には、より多くの計算資源が必要になる。

解釈可能性

明示的なルールがあるため、解釈しやすい。

しばしば「ブラックボックス」と見なされ、解釈が難しくなる。

カバレッジ

メタデータの有無と正確さに依存する。

作品そのものの視覚要素を直接扱える。

手作業の量

ルールやテンプレートを定義するために、かなりの手間がかかる。

訓練データから汎化できるモデルの性質により、手作業は減る。

こうしたアプローチの違いがあるので、ワークフローと開発の面でも重要な違いが生まれる。A.I. の利点を取り込みたい組織なら、ここは気にしたほうがいい。両者が重なるところと分かれるところは、だいたい次のようになる。

共通点:

  1. 統合:ML モデルは、従来型のソフトウェア工学の手法で構築された、より大きなソフトウェアシステムの一部として組み込まれることが多い。

  2. テストと検証:どちらのアプローチでも、コードやモデルが期待どおりに振る舞うことを確認するためにテストを使う。ただし、テストの具体的な中身は異なりうる。

  3. デプロイと保守:機械学習モデルであれ従来型ソフトウェアであれ、デプロイし、監視し、保守する必要がある。

  4. バージョン管理:どちらの開発でも、変更と履歴を管理するために Git のようなバージョン管理システムを使うことが多い。ただし機械学習では、データ、モデル、ハイパーパラメータ、結果についてもバージョン管理が必要になる。

相違点:

  1. 決定論的か確率論的か:従来型のソフトウェア工学は一般に、明示的なルールとロジックに従う決定論的なものだ。一方、機械学習は確率論的で、データからパターンを学ぶ。

  2. 解釈可能性:従来型ソフトウェアは一般にデバッグや解釈がしやすい。機械学習モデル、とくに複雑なモデルは、理解が難しくなりがちだ。

  3. データ依存性:機械学習は、訓練、検証、テストの各段階でデータに大きく依存する。一方、従来型のソフトウェア工学では、これらの段階でデータが本質的に必要になるわけではない。

  4. フィードバックループ:機械学習では、モデルが新しいデータから学び続ける継続的なフィードバックループを含むことが多い。従来型ソフトウェアには、この性質が本質的に備わっているわけではない。

  5. 必要なスキルセット:従来型のソフトウェア工学と機械学習では、求められるスキルセットが違う。ただし、ソフトウェアエンジニアがデータに強くなり、データサイエンティストがソフトウェアのベストプラクティスを学ぶにつれて、重なりは広がっている。

    • ML 固有のスキル:統計・数学の理解(確率、統計、線形代数、微積分)、データ処理のスキル(データクリーニング、前処理、特徴量エンジニアリング、可視化)、モデルの訓練と評価(アルゴリズム選択、ハイパーパラメータ調整、評価指標)、専門的なツールとライブラリ(機械学習フレームワーク、データ操作ライブラリ)、実験(A/B テスト、モデルの解釈可能性)などが含まれる。

上の例で、A.I. エンジニアリングのアーキテクチャと設計パターンが、それ自体として立ち上がりつつある技芸なのだ、という感触が少しでも伝わればと思う。

レジストリベース、インターフェース駆動の設計パターン

オブジェクト指向プログラミングでは、とくに Java、C#、TypeScript のような言語において、「インターフェース」は契約を定める型定義だ。つまり、あるクラスが実装しなければならないメソッド群(場合によってはプロパティも)を指定する。インターフェース自体は実装を持たない。そのインターフェースを実装すると主張するクラスに、どのメソッドが存在すべきかを定義するだけだ。

安定性と実験の柔軟性が必要な A.I. 駆動アプリケーションや LLM ベースのアプリケーションでは、インターフェースと抽象クラスがとくに効く。A.I. 開発では、モデルを調整し、前処理の手順を変え、別のデータソースを試す、といった実験が大量に発生する。システムがモジュール性を前提に設計されていれば(インターフェースと抽象的な設計パターンがそれを支える)、システムの他の部分を壊す危険を抑えながら、実験のためにコンポーネントを差し替えやすくなる。さらに、A.I. システムをスケールさせたりデプロイしたりするときにも、モジュール化された設計は安定性と保守性を担保してくれる。

要するに、インターフェースは「何を期待してよいか」について明確な契約を与える。

レジストリ、抽象クラス、インターフェースを組み合わせると、こうなる。

  • 機能の種類ごとにインターフェースを用意する(たとえば、ファイル処理、チャンク分割、埋め込み、保存、検索)。これにより、各コンポーネントが何をすべきかが明確になる。

  • そのうえで、実行時にこれらのインターフェースの適切な実装を管理し、提供するためにレジストリを使える。これにより、システムは状況や設定に応じて動的に適応できる。

たとえば、異なるファイル種別を処理する場合を考える。

  1. インターフェース駆動の部分

    • まずはインターフェースを定義するところから始める。たとえば、データを取り込み、メタデータを取得するメソッドを持つ DataSource インターフェースを考える。このインターフェースは、すべてのデータソース処理器が何をすべきかについて明確な契約を設定する。

      from abc import ABC, abstractmethod
       
      class DataSource(ABC):
          @abstractmethod
          def ingest(self):
              pass
       
          @abstractmethod
          def get_metadata(self):
              pass
  2. レジストリの部分

    • データが取り込まれたら(たとえば Git リポジトリ内のファイル群)、システムは FILE_TYPE_PROCESSORS レジストリを参照し、各ファイルの拡張子に基づいて適切な処理器を見つけられる。

      FILE_TYPE_PROCESSORS = {
          ".py": PythonFileProcessor,
          ".md": MarkdownFileProcessor,
          # ...
      }

システムがデータを取り込む必要があるときは、このレジストリに問い合わせて、各ファイルの拡張子に応じた適切な処理器を探せる。

  1. DataSource サブクラス内での統一ワークフロー

    • DataSource のサブクラス内では、この二つの手順をまとめられる。まずデータを取り込み、その後、取り込んだ各ファイルに対して適切なファイル処理器を使う。
class GitRepoSource(DataSource):
    def ingest(self):
        files = self._clone_and_filter_files()
        processed_data = []
        
        for file_path in files:
            ext = self._get_file_extension(file_path)
            processor_class = FILE_TYPE_PROCESSORS.get(ext)
            if processor_class:
                processor = processor_class()
                processed_data.append(processor.process(file_path))
                
        return processed_data

組み合わせる利点

この組み合わせには、いくつか利点がある。

  1. 拡張が容易:将来、新しいデータソース種別に対応する必要が出ても、DataSource インターフェースに従う新しいクラスを作り、それを DataSourceRegistry に登録すればよい。既存のコードに手を入れる必要はない。

  2. 期待値が明確DataSource インターフェースにより、すべてのデータソースが共通の API に従うため、取り込みパイプラインの挙動が予測しやすくなる。

  3. 動的な適応性:中央のレジストリがあることで、メインのワークフローに影響を与えずに、異なるデータソース処理器を簡単に差し替えられる。柔軟性がかなり増す。

文書と PDF 用のエージェント、コードリポジトリ用の別エージェント、といった多様なユースケースでは、この組み合わせによって、インターフェースを通じた明確な契約を得つつ、レジストリによって拡張・適応の余地も保てる。安定性はインターフェースから、柔軟性はレジストリから来る。

では、A.I./LLM ベースのアプリに新しい超能力を持たせたいときはどうなるのか。

やり取りと消費の手段を拡張する:

例として、僕が「The Sound of Stories」でやりたいことを考える。LLM が生成したテキストを、多言語ナレーションという追加の消費方法に向けて解析する、というものだ。生成された物語をもとに画像生成を可能にすることも考えられる。消費するのは他のツールかもしれないし、人間かもしれない。僕が想定しているソーシャルメディア向けコンテンツ生成のユースケースでは、ソーシャルメディア API が受け取れる構造化 .json を出力する必要もある。レジストリベース、インターフェース駆動のアプローチなら、次のように消費方法を拡張できる。

  1. レジストリを作る:前の例と同じように、消費方法のレジストリを持たせる(例:テキスト表示、音声出力、アニメーション動画)。

  2. 消費インターフェース

class ConsumptionInterface(ABC):
    
    @abstractmethod
    def display(self, content):
        pass
  1. 具体的な実装
class TextDisplay(ConsumptionInterface):
    def display(self, content):
        # Simply print the content
 
class VoiceOutput(ConsumptionInterface):
    def display(self, content):
        # Use Text-to-Speech to vocalise the content
 
class AnimatedVideo(ConsumptionInterface):
    def display(self, content):
        # Convert content into animated video

さて、いったん一歩引いて、A.I. アプリケーションにおけるデータ駆動開発のサイクル全体を、端から端まで見てみよう。

A.I. アプリのための汎用的で簡略化したエンドツーエンドアーキテクチャ

A.I./LLM アプリのスタックはまだ立ち上がり始めたばかりで、a16z はこれをかなり有用な形で整理している。大まかには、プロンプト構築(検索)、プロンプト実行(推論)、そしてエコシステム内のいくつかのツール群という段階だ。データフローとパイプラインは、このエコシステムのバックエンド配管であり、従来型のソフトウェア工学とのもう一つの重要な違いでもある。以下では、僕が開発上注目している汎用アーキテクチャとコンポーネントを簡略化して示す。

例:作品を対象にしたニューラル検索の一連の流れ

以前、僕は作品を対象にしたニューラル検索のデモを作った。このデモを本番向けに拡張するなら、次のような手順が考えられる。

ステップ

関係するコンポーネント

処理

メモ

1

オーケストレーター

作品メタデータと画像埋め込みの初期データ取り込み

バッチモードで実行

2

ユーザーインターフェース(UI)

ユーザーが検索クエリまたはフィルター条件を入力する

3

メインアプリケーション

ユーザー入力を検証し、設定を取得する

設定管理コンポーネントに確認する

4

オーケストレーター

Vector Database から関連する埋め込みを取得する

Retrieval Engine を使う場合もある

5

応答生成器

ユーザーにわかりやすい出力を組み立てる

表示用に作品メタデータと画像を整形する

6

ログ記録モジュール

処理をログに記録する

ユーザーのクエリ、応答時間などを保存する

7

検証と評価

結果を自動評価する

基本的なセキュリティチェック

8

ユーザーインターフェース(UI)

検索結果をユーザーに表示する

9

フィードバック収集器

必要に応じてユーザーフィードバックを集める

4か月の開発ロードマップに向けた優先順位付け

つまり、この汎用フレームワークとアーキテクチャの中で、僕が関心を持っていることはかなりたくさんある。

  • 作品を対象にしたニューラル検索

  • 生成された物語のリアルタイム多言語ナレーション

  • コードベースのリファクタリングとテスト作成

  • 文書とデータのナレッジベースと対話するチャット

  • コード生成

  • ソーシャルメディア向けコンテンツの生成と公開

優先順位付けの観点では、読解力と創作力は別々のスキルセットであり、前者は後者にとってかなり価値がある。なので僕の計画としては、LLM に学び方を教えることを優先し、作品を対象にしたニューラル検索やナレッジベースのような、比較的複雑でないユースケースから始める。ざっくりしたタイムラインは、次のようになるだろう。

  • 1〜2か月目作品を対象にしたニューラル検索 - 基礎となるデータパイプラインのため。

  • 2〜3か月目文書とデータのナレッジベースと対話するチャット - 取り込み・検索手法と UI コンポーネントを開発するため。

  • 3〜4か月目コードベースのリファクタリングとテスト作成 - チャット/ナレッジベース段階で得た知見と手法を活用し、より賢い自動テストとリファクタリングにつなげる。

上の方針で、ニューラル検索とナレッジベースの構築に集中しつつも、LLM でコードベースに問い合わせ、コード品質サービスに渡し、その後、処理を連鎖させたりプロンプトテンプレートを使ったりして、リファクタリングやテスト作成のような関連する自動化を行う簡略ワークフローには着手しておくとよさそうだと思っている。最初は手動レビュー用にブランチへ分け、後でテストがすべて通るなら自動化する、という流れだ。

一方で、僕の Git リポジトリではプロジェクトをこんなふうに整理しようとしている。まださらに変わる前提なので、下はかなり作業中のものだが、各フォルダには次の役割を持たせるつもりでいる。

  • db - CRUD など、データベース関連の操作すべて

  • demos - いくつかの「安定した」デモとワークフローを定義する場所

  • gui - ユーザーが目にするものすべて

  • src - インターフェースと実装のソースコード本体

  • tests - 単体テスト、統合テスト、テスト用フィクスチャ

プロジェクト構造をまだ整理している最中で、CI/CD、監視、評価のコンポーネントもまだ欠けている。ただ、National Arts Council Arts x Tech Lab 向けに「The Sound of Stories」(生成された物語のリアルタイム多言語ナレーション)も納品する必要があるので、他の開発者と分割統治で進めたいと思っている。うまくいけば、これらの開発作業はかなり相乗効果を持ち、Berlayar A.I. という会社のための構成要素にもなるはずだ。;)

├── agents
│   └── __init__.py
├── dataloader.ipynb
├── db
│   ├── chunks.py
│   ├── connectors.py
│   ├── __init__.py
│   ├── main.py
│   ├── ops
│   │   ├── deeplake.py
│   │   └── __init__.py
│   └── utils.py
├── demos
│   ├── chat_adf
│   │   ├── chat.py
│   │   ├── tests.py
│   │   └── utils.py
│   ├── neural_search
│   │   └── upload.py
│   └── thesoundofstories
│       ├── audio
│       └── chat.py
├── diagram.py
├── gui
│   └── __init__.py
├── ingest_pdf.log
├── __init__.py
├── log_init.py
├── main.py
├── notebooks
│   ├── architecture.ipynb
│   ├── chat_adf.ipynb
│   └── dataloader.ipynb
├── poetry.lock
├── pyproject - Copy.toml
├── pyproject.toml

一方で、僕はこれから「Good Economics for Bad Times」の試験に集中しに行くので、これはいったん棚上げしておく。


初出は PubPub の erniesg.pubpub.org/pub/wpnv7rhs