🐪

Apache Camel AI ― DJLコンポーネントを使ったAIの活用方法

2024/09/02に公開

Camel AI

Apache Camel AIは、様々なAI関連技術をCamelと統合して使えるようにするコンポーネント群です。

現在、AI関連技術としてはOpenAI ChatGPTやMeta LlamaなどのLLMが非常に注目を集めており、多くのフレームワークやツールがLLMの活用方法を模索しています。Camel AIにおいてもLangChain4jコンポーネント群が提供されており、Camel BlogでもLangChain4jを使ったLLMの活用方法が紹介されています。

こうしたLLMへの注目の陰で、見過ごされがちですが重要なAI技術があります。Transformerベースでない従来のニューラルネットワークモデルを使った推論です。

Camel DJLコンポーネント

DJLコンポーネントは、Javaの深層学習用ライブラリDeep Java LibraryをCamelに組み込んだコンポーネントです。PyTorch、TensorFlowといった人気の機械学習フレームワークで学習したモデルを実行してJavaで推論できます。また、機械学習モデルの標準的なフォーマットであるONNXモデルにも対応しています。

システム間インテグレーションにこうした従来型の機械学習モデルの推論を組み込む活用方法は、まだ十分に検討されているとは言えません。LLMと比較した従来型の機械学習モデルの特徴は、推論が軽量でインテグレーションの中でオンザフライに実行できることです。

機械学習モデルはLLMと違い、以下のようにそれぞれが特定の用途に特化しています。

  • コンピュータ・ビジョン(CV)
    • 画像分類
    • 物体検出・画像セグメント化
    • 身体・表情・ジェスチャー分析
    • 画像操作
  • 自然言語処理(NLP)
    • 文章理解
    • 機械翻訳
    • Q&A
  • その他
    • 音声認識
    • 時系列予測
    • 推薦エンジン

DJLコンポーネントは、次のCamel 4.8.0 LTSリリースで大きく改良されます[1]。それにより、こうした様々な用途のモデルで推論を実行できるようになります。

この記事では、画像認識、自然言語処理、音声認識を組み合わせた例を2つ紹介します。DJLコンポーネントを使うことで、Camel上でAIを活用して従来できなかったどんなインテリジェントなルーティングが可能になるかを見てください。

Image to Text ルーティング

ニューラルネットワークモデルの得意分野の1つが画像処理です。AI技術を用いないCamelルーティングでは、送られてきた画像から物体を判別したり、そこに何が写っているかを分類したりといった処理を実装するのはほぼ不可能でしょう。DJLコンポーネントを使えば、そうした処理をリアルタイムで行うルーティングを簡単に実装できます。

エッジAIの例を考えてみましょう。エッジ端末でカメラ入力から画像データがスナップショットで記録され、それをCamelが読み込み分類を行い、中央サーバーに結果をルーティングするようなシナリオです。

Image to Text ルーティング

ソースコード

このシナリオを実現するために、学習済みのニューラルネットワークモデルSSDとResNetを組み合わせます。コードは以下の通りです。

image_to_text.java
// camel-k: dependency=camel:djl
//DEPS ai.djl.pytorch:pytorch-engine:0.29.0
//DEPS ai.djl.pytorch:pytorch-model-zoo:0.29.0
//DEPS net.sf.extjwnl:extjwnl:2.0.5
//DEPS net.sf.extjwnl:extjwnl-data-wn31:1.2

import java.util.List;
import org.apache.camel.Exchange;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.djl.DJLConstants;
import ai.djl.modality.Classifications;
import ai.djl.modality.cv.Image;
import net.sf.extjwnl.data.POS;
import net.sf.extjwnl.data.PointerUtils;
import net.sf.extjwnl.dictionary.Dictionary;

public class image_to_text extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        from("file:data/inbox?include=.*\\.(jpg|png)")    // (1)
            .log("Processing: ${headers.camelFileName}")
            .to("djl:cv/object_detection?artifactId=ssd") // (2)
            .convertBodyTo(Image[].class)                 // (3)
            .split(body())                                           // (4)
                .to("djl:cv/image_classification?artifactId=resnet") // (4)
                /*
                 * The output from the image classification model is classified
                 * as one of 1000 labels from WordNet.
                 * Since it's too fine-grained, we want to find the higher-level
                 * group (= hypernym) for the classification using the WordNet
                 * dictionary.
                 */
                .process(this::extractClassName)          // (5)
                .process(this::addHypernym)               // (5)
                .log("  => ${body}");                     // (6)
    }

    void extractClassName(Exchange exchange) {
        var body = exchange.getMessage().getBody(Classifications.class);
        var className = body.best().getClassName().split(",")[0].split(" ", 2)[1];
        exchange.getMessage().setBody(className);
    }

    void addHypernym(Exchange exchange) throws Exception {
        var className = exchange.getMessage().getBody(String.class);
        var dic = Dictionary.getDefaultResourceInstance();
        var word = dic.getIndexWord(POS.NOUN, className);
        if (word == null) {
            throw new RuntimeCamelException("Word not found: " + className);
        }
        var hypernyms = PointerUtils.getDirectHypernyms(word.getSenses().get(0));
        var hypernym = hypernyms.stream()
                .map(h -> h.getSynset().getWords().get(0).getLemma())
                .findFirst().orElse(className);
        exchange.getMessage().setBody(List.of(className, hypernym));
    }
}

このコードがやっていることは次の通りです。

  1. data/inboxディレクトリから画像ファイル(.jpgまたは.png)を読み込む
  2. 読み込んだ画像ファイルを学習済みSSDモデルで物体認識する
  3. DetectedObjects型の結果をImage[]に変換する
  4. Image[]データを1つずつに分割(split)し、今度は学習済みResNetモデルに読み込ませて画像分類を行う
  5. WordNetのデータベースを用いて各画像分類結果の上位語(hyperym)を検索して結果に付与する
  6. 最終結果をログに出力する(実際のシナリオでは中央サーバーへメッセージ送信する)

実行と結果

コードはCamel JBangでそのまま実行できるように書かれているので、以下のようにして実行できます。

camel run --camel-version=4.8.0-SNAPSHOT image_to_text.java

試しに、Dogeの画像をdata/inboxにコピーしてみると、以下のような結果が得られます。

cp doge.jpg data/inbox
INFO 26599 --- [le://data/inbox] image_to_text.java:26 : Processing: doge.jpg
INFO 26599 --- [le://data/inbox] image_to_text.java:43 :   => [dingo, wild dog]

サンプルコードのGitHubリポジトリ

このサンプルコードは以下のGitHubリポジトリに公開されています。

https://github.com/megacamelus/camel-ai-examples/tree/main/image-to-text

Speech to Text ルーティング

ニューラルネットワークモデルのもう1つの得意分野が自然言語処理です。知識ベースに基づくQ&Aや文章の感情分析、機械翻訳といったアルゴリズムで表し難い曖昧な自然言語の処理にAIは長けています。

ここでは文章の感情分析を音声認識と組み合わせた例を考えてみましょう。マイク入力から録音したWaveファイルを、Camelで音声認識してテキストに変換し、さらにそれを感情分析にかけてそれがポジティブかネガティブかを判定します。

Speech to Text ルーティング

ソースコード

このシナリオを実現するために、学習済みのニューラルネットワークモデルwav2vec 2.0とDistilBERTを組み合わせます。コードは以下の通りです。

// camel-k: dependency=camel:djl
//DEPS ai.djl.pytorch:pytorch-engine:0.29.0
//DEPS ai.djl.pytorch:pytorch-model-zoo:0.29.0

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import org.apache.camel.builder.RouteBuilder;
import ai.djl.MalformedModelException;
import ai.djl.Model;
import ai.djl.modality.audio.translator.SpeechRecognitionTranslator;
import ai.djl.util.ZipUtils;

public class speech_to_text extends RouteBuilder {

    static final String MODEL_URL = "https://resources.djl.ai/test-models/pytorch/wav2vec2.zip";
    static final String MODEL_NAME = "wav2vec2.ptl";

    @Override
    public void configure() throws Exception {
        loadSpeechToTextModel();

        from("file:data/inbox?include=.*\\.wav")                    // (1)
            .log("Processing: ${headers.camelFileName}")
            .to("djl:audio?model=SpeechToTextModel&translator=SpeechToTextTranslator") // (2)
            // The output of the model is all uppercase, which tends to be recognised
            // as negative by the following distilbert model
            .setBody(simple("${body.toLowerCase()}"))
            .log("  => ${body}")
            .to("djl:nlp/sentiment_analysis?artifactId=distilbert") // (3)
            .log("  => ${body.best}");                              // (4)
    }

    void loadSpeechToTextModel() throws IOException, MalformedModelException, URISyntaxException {
        // Load a model
        var model = Model.newInstance(MODEL_NAME);
        // TfModel doesn't allow direct loading from remote input stream yet
        // https://github.com/deepjavalibrary/djl/issues/3303
        var modelDir = Files.createTempDirectory(MODEL_NAME);
        ZipUtils.unzip(new URI(MODEL_URL).toURL().openStream(), modelDir);
        model.load(modelDir);

        // Bind model beans
        var context = getContext();
        context.getRegistry().bind("SpeechToTextModel", model);
        context.getRegistry().bind("SpeechToTextTranslator", new SpeechRecognitionTranslator());
    }
}

このコードがやっていることは次の通りです。

  1. data/inboxディレクトリから音声ファイル(.wav)を読み込む
  2. 読み込んだ音声ファイルを学習済みwav2vec 2.0モデルで音声認識してテキストに変換する
  3. テキストを学習済みDistilBERTモデルに読み込んで感情分析する(positiveまたはnegativeに分類される)
  4. 最良の結果をログに出力する(実際のシナリオではメッセージブローカーのキューに送信する)

実行と結果

こちらも同様にCamel JBangでそのまま実行できます。

camel run --camel-version=4.8.0-SNAPSHOT speech_to_text.java

試しに、サンプルリポジトリにあるWaveファイルのサンプルspeech*.wavをダウンロードして読み込ませてみます。

cp speech1.wav data/inbox
cp speech2.wav data/inbox
cp speech3.wav data/inbox

以下のような結果が得られるでしょう。

INFO 3351853 --- [le://data/inbox] speech_to_text.java:26 : Processing: speech1.wav
INFO 3351853 --- [le://data/inbox] speech_to_text.java:31 :   => oh grreat many things so
INFO 3351853 --- [le://data/inbox] speech_to_text.java:33 :   => {"class": "Positive", "probability": 0.87968}
INFO 3351853 --- [le://data/inbox] speech_to_text.java:26 : Processing: speech2.wav
INFO 3351853 --- [le://data/inbox] speech_to_text.java:31 :   => dan this is not acceptable i've not even been notified about this automatic change
INFO 3351853 --- [le://data/inbox] speech_to_text.java:33 :   => {"class": "Negative", "probability": 0.99945}
INFO 3351853 --- [le://data/inbox] speech_to_text.java:26 : Processing: speech3.wav
INFO 3351853 --- [le://data/inbox] speech_to_text.java:31 :   => many thanks that's so helpful i'm a bit more relieved now
INFO 3351853 --- [le://data/inbox] speech_to_text.java:33 :   => {"class": "Positive", "probability": 0.97949}

サンプルコードのGitHubリポジトリ

このサンプルコードは以下のGitHubリポジトリに公開されています。

https://github.com/megacamelus/camel-ai-examples/tree/main/speech-to-text

推論の精度を高めるには?

DJLコンポーネントを使った2つのサンプルで、Camelとニューラルネットワークの組み合わせに大きな可能性があることが分かってもらえたと思います。一方で、サンプルコードの実行結果に満足できなかった方もいるかもしれません。

機械学習モデルを使ったAIソリューションの実行結果の信頼性は、結局のところ、どれだけその用途に対して精度の高いモデルを使うかにかかっています。今回のサンプルコードはすべて公開されている学習済みモデルを利用しました。世の中には、既に精度の高い学習済みモデルが大量に公開されています。

まずは、公開済みのモデルをいくつか組み合わせてみるだけで、面白いAIソリューションをCamelルーティングとして簡単に構築できるでしょう。

その上で、より厳密な推論精度を求められる段階になったら、最終的には自分でカスタムのモデルを学習させる必要があります。これはCamel AIに限らず、あらゆるAIソリューションの構築に言えることです。その場合、自分でカスタムのデータセットとモデル学習用環境を用意する必要があります。幸い、Camel DJLコンポーネントはPyTorch、TensorFlow、ONNXなどの各種モデルフォーマットに対応しているので、モデル学習にはPyTorchやTensorFlowをそのまま使うことができます。

推論をどこで実行するか:組み込み vs 外部Serving

Camelで機械学習モデルを使ったルーティングを構築するに当たって、もう1つ重要なトピックがあります。推論をどこで行うべきかという問題です。

このサンプルでは、推論はすべてCamelルート内に組み込まれて実行されています。LLMと違い、それが可能な速度で推論できるためです。それでも、メッセージとして送られてくる多次元配列データをモデル内の複数のレイヤーで演算処理する必要があります。処理するメッセージが大量だったり、モデルが何層にもわたる複雑なものだったりした場合、Camelルートの実行にGPUなどの計算資源が求められるようになります。

本格的な機械学習のソリューションをCamelルートで実現する場合、よりスケールアウトしやすいのは推論実行を外部サーバーに移譲する構成です。PyTorchやTensorFlowなどの機械学習フレームワークはServing機能を提供しており、フレームワークで実行する推論をREST APIでサービスとして公開できます。この場合、Camelはルート内で直接推論を実行するのでなく、RESTクライアントとして機械学習フレームワークのサービスを呼び出すことで推論を移譲します。エンタープライズのAIソリューションとしてはこちらの方がスケールアウトしやすいとして、こちらのアプローチを推すエキスパートも多いでしょう。

どちらの構成の方が本当にパフォーマンスが良いかは難しい問題です。私には、往年のアプリケーションサーバーの時代のEJB vs POJOの論争が思い出されます。重い処理をリモートに分散させるべきか、すべてをローカルで実行すべきか、という問題です。実際には、推論実行の効率性と、多次元配列の入力データをネットワーク越しに転送してリモート呼び出しを行うコストとのトレードオフになるでしょう。ベンチマークを実施しないと、どちらが本当にパフォーマンスが良いかは判明しないはずです。

まとめ

次のCamel 4.8.0 LTSリリースで提供されるDJLコンポーネントで、様々なニューラルネットワークモデルを活用してインテリジェントなルーティングが実現できるようになることを見てきました。

Camelが可能にするシステム間インテグレーションにおいて、LLMを始めとしてAI技術を導入する余地はまだまだ多くあります。実際、システム間インテグレーションやエンタープライズインテグレーションパターン(EIP)の世界で、ニューラルネットワークのモデルを活用するアイデアはその探索が始まったばかりです。今後もCamelでは、DJLコンポーネントを拡張し、新しいコンポーネントをさらに追加していくことで、Camel AIの可能性をもっと拡張していく予定です。

また、今後もCamel AIの様々なサンプルコードを以下のリポジトリで公開していきます。このテーマに興味のある方は、ぜひこのリポジトリをフォローしてください。

https://github.com/megacamelus/camel-ai-examples

脚注
  1. 実際には4.7.0から。 https://camel.apache.org/blog/2024/07/camel47-whatsnew/ ↩︎

Discussion